Eth.cd - A social hub for ENS domains

eth.cd is a Web3 social networking platform that leverages Ethereum Name Service (ENS) to create dynamic, decentralized profiles based on users’ ENS records. Our platform gamifies user engagement through leaderboards and integrates social networking functionalities, such as decentralized messaging and community building, using blockchain technology.

eth.cd.Homepage_small

Feature Overview: eth.cd takes a unique approach to highlighting active and engaged members of the ENS community. The homepage is ingeniously designed to showcase the top 100 profiles. This dynamic feature is not just about popularity, it’s a measure of how comprehensively users have utilized their ENS domains by adding various records. This incentivizes users to fully explore and utilize the capabilities of ENS, thereby enriching their digital presence.

Benefits for Users:

  • Visibility: Being featured on the homepage increases visibility within the ENS community, offering recognition and prestige.
  • Motivation for Improvement: This feature motivates users to continually update and enrich their ENS records, fostering a dynamic and engaged community.
  • Discovery and Networking: It aids in the discovery of interesting profiles, potentially leading to networking and collaboration opportunities within the decentralized space.
    ENS profiles

Every ENS domain represents a unique and personalized profile.

When someone navigates to hidayath.eth.cd , they will be greeted with a rich tapestry of social interactions and content. The page dynamically showcases the user’s Twitter feed, a curated list of GitHub repositories and activities, and an engaging Instagram feed, all seamlessly integrated. Additionally, visitors can preview the content of the user’s IPFS website, linked via the content hash added to their ENS. We are continually expanding our integration capabilities to ensure that each ENS profile page becomes a comprehensive hub, offering visitors a holistic view of the user’s digital presence across various platforms.

Project Description

  • Objective: To create a unique, user-friendly social networking platform that offers ENS users a comprehensive digital identity, enhanced by aggregated social media content and decentralized applications.
  • Features:
    • ENS-Based Profiles: User profiles created with the data added to their ENS domains, which showcase their digital identity.
    • Gamification: Leaderboards and rewards for users who actively update and enrich their profiles.
    • Social Networking Tools: Decentralized messaging (via XMTP), following, and friend requests.
    • Integration with Ethereum Follow Protocol: Allows for decentralized following functionalities.
    • Content Aggregation: Aggregates content from multiple platforms based on ENS record data.

Decentralized Content Creation and Curation

  • User-Generated Content: Encourage users to create content directly on the platform. This could be blogs, vlogs, or other forms of media that are then stored on IPFS.
  • Decentralized Content Curation: Implement a decentralized system for content curation where the community can vote on or rate content, helping to surface quality posts.

Future Enhancements

Enhanced Communication Tools

  • Decentralized Chatrooms and Forums: Create topic-specific chatrooms or forums where users can discuss various subjects(via XMTP). These could be moderated by community-elected members.

  • Video and Voice Calling: Implement decentralized, encrypted video and voice calling capabilities for private conversations or group meetings, by integrating tools like huddle01.

  • We plan to introduce more nuanced criteria for ranking, incorporating factors like community endorsements, the creativity of profiles, and integration with other decentralized services.

Proposal for Standardizing of Records key value in ENS Domains
Currently, ENS records incorporate a limited selection of predefined keys for various social media and platforms, such as:

  • “com.twitter”
  • “com.discord”
  • “com.github”
  • “com.reddit”
  • “org.telegram”

To accommodate the ever-evolving digital landscape, there’s a pressing need for a standardized format for additional social media platforms. I propose we adopt a consistent “com.” prefix for all future social media records. This approach would not only streamline the integration process but also ensure uniformity and ease of use. Suggested additions could include:

  • “com.instagram”
  • “com.facebook”
  • “com.lens”
  • “com.farcaster”
  • “com.bluesky”

This standardization would be immensely beneficial for future developments and integrations. @nick.eth , your insights and guidance on this matter would be invaluable.

We would love to hear your thoughts on the following:

  • New Features: Are there any specific features or integrations you would like to see on our platform?
  • User Experience: How can we make your experience more seamless and engaging?
  • Community Building: What tools or functionalities would you like to have to connect and interact with other users more effectively?
  • Any Other Ideas: We’re open to any and all suggestions. Nothing is too big or small!
8 Likes

Very nice!

Keys here are reversed domain names - so com.instagram is fine, because Instagram is on instagram.com. Lens should be xyz.lens.

3 Likes

Understood! We will work on documentation that will guide the community on how to incorporate various social media and other platforms into their ENS records. Thanks @nick.eth

1 Like

I added records for com.instagram & xyz.lens. These records appear under the “other” section as opposed to under the “account” section.

The “account” section which I’ll call the default/standard records include logos/favicons and most link to the respective webpage (the email link is also functional). They are also easier to set, as they have their own section (at the top) of the UI flow.

Learning that all domains are supported with the reverse format (Instagram.com = com.instagram) is pretty cool! However, it would improve the UI/UX (maybe overall record adoption) if there were more records that are default/standard meaning they appear in the “account” section. As an example if there were a “catch all” record (like a custom record) specifically for websites, and the record appear in the “account section” complete with the logo/favicon and functional link.

4 Likes

gm @nick.eth, @ENSPunks.eth added this social records, with key values as shown in the attached screenshot. Are there any changes you suggest for these records?

We’re constructing a social records verification module where users can authenticate ENS-added records. Once verified, we plan to add a verification badge to the ENS name via an NFT or a custom record. We’re exploring the best implementation method on an L2. Your feedback and guidance would be invaluable.

If ENS Labs is already developing a records verification module, please inform us. We’d be willing to adjust our roadmap accordingly.

CleanShot 2023-12-14 at 1.26.05@2x

1 Like

Are we using xyz.farcaster and xyz.lens? That wouldn’t be my first pick since it’s not obvious those are the TLDs. I feel like the web2.5/hybrid stuff should be farcaster and lens.

1 Like

As nick.eth mentioned, ENS record keys are reversed domain names, and Twitter along with other legacy social media records are added in that style. I believe adopting one consistent method would be better.

CleanShot 2024-02-06 at 12 .54.26@2x

Ooh… this is interesting. I didn’t realise that this was the justification/standard in this regard. When I first saw it I instantly thought ‘iOS Bundle IDs’ which take this approach. In that case it really is just an arbitrary identifier and if the apps domain changes it doesn’t really matter - its not user facing.

In this case… blockchain companies seem to be changing their domains weekly. It doesn’t seem good to have the standardised identifier for ‘social media app’ be ‘xyz.socialmediaapp’ because pre VC funding/bull-run they were using socialmediaapp.xyz when now they have acquired the .com

hmm

As @raffy mentioned above… both Lens and Farcaster are web2.5+ protocol types that’s not tied to single domain.tld for frontend/UI access, & there’s no official ENS list of such protocol and domains yet so we’re using their primary tld.domain as key for text records.

On other side… we’ve to use both com.x and com.twitter for backwards compatibility? :rofl:

1 Like

Keeping client diversity in mind, separating out client-agnostic social protocols is perhaps a good idea.

Yes. I’m saying the standard is not appropriate (IMO) for the use case because of this. The Twitter/X example is perfect…

If standardization is lacking, frontend development becomes challenging. How can a frontend application determine which record to add? For example, if some users add ‘farcaster’ and others add ‘warpcast’ as the record type. With multiple frontend applications available for ‘farcaster’ and ‘lens,’ similar to the unlimited possibilities for every protocol, the situation becomes even more complex.

A related topic is discoverability. I believe the official app is relying on a standard set of text keys and address types, and subgraph data from event-emitting resolvers. This means that CCIP names (gasless DNS, L2 stuff, cb.id, etc.) are disadvantaged with non-standard records and incorporating new “standard” records will always lag.

If it was efficient to multicall in the general case, you could just probe multiple keys but that still sucks and doesn’t let you discover new things—just easily test 3 different Farcaster permutations. Ideally you’d just query a directory of available records.


I’m just gonna throw an idea out: named prebuilt calldata. This combines discoverability and call-efficiency into one idea, that fits in an address record.

First, pick a set of records and encode them:

// pre-defined calldata
let calldata = abi.encode([
    encode("text(bytes32,string)",  0x0, "avatar"),
    encode("text(bytes32,string)",  0x1,   "name"),
    encode("addr(bytes32,uint256)", 0x2,       60),
]);

It can be accessed under a new coinType C:

addr(node, C) = calldata

At some predictable ENS name, eg. 1.profile.ens.eth, have write-once resolver, that responds with the same data, for all nodes, such that addr(_, C) = namehash("<keccak256(calldata)>.profile.reverse")

Store a copy on-chain under that same name <keccak256(calldata)>.profile.reverse with a resolver where addr(node, C) = calldata and with corresponding hash.

Elsewhere: if addr(node, C) returns 32 bytes it’s a node (and this resolution should repeat), otherwise stop, and decode it as an array of calls.

When we want to publish a new “standard”, 2.profile.ens.eth, etc. Clients switch their default profile whenever.

Now when you resolve raffy.eth, do the following:

// (A) get resolver
(resolver, version, offset) = getResolver("raffy.eth")
// offset = byte offset of first non-null resolver
// version 0 = og ens
// version 1 = ensip-10
// version 2 = ensip-10 + multicall support (backwards compat with v0 and v1)

// (B) get calldata from various sources:
//   1. "ens standard records, edition v1"
bytes32 profile1 = ENS.resolve("1.profile.ens.eth").addr("raffy.eth", C)
bytes[] calls = abi.decode(ENS.resolve(profile1).addr(profile1, C))
///  2. "your records"
// this can be ccip-read
// this can be a node or encoded-calldata
bytes32 node = namehash("raffy.eth");
while let v = ens.resolver(node).addr(node, C) and v.length == 32 { node = v }
if (v.length > 32) calls.push(abi.decode(v, bytes[]))

// inject the queried node into the calldata:
// since all records of the form: ƒ(bytes32, ...)
for each call:
    replace bytes 36,68 w/ namehash("raffy.eth")

// (C) make the reads
version == 2:
    resolver.multi(["raffy.eth"], calls)
version == 1:
    UniversalResolver("raffy.eth", calls)
version == 0:
    calls.map(v => resolver.staticcall(v))

The new resolver function is:

// this is flattened outer product of names x calls
multi(bytes[] names, bytes[] records) returns (bytes[])

The resolver versions are related in the following ways:

addr(node, ct) := multi([], [abi.encode("addr", node, ct)])
resolve(name, call) := multi([name], [call])
multi(names[], calls[]) := names.flatMap(n => calls.map(c => resolver.call(c~n)))
multi([], calls[]) := calls.map(c => r.call(c))

If you only need a few records, you can construct the calldata yourself, eg. addr(node, 60).

If you want the standard records, you can look them up on-chain by version.

You also get your own set of records per name. You can use a shared list of records (eg. popular) by using the namehash of the reverse name, a namehash of another ENS, or the raw calldata itself. addr() itself can be CCIP-read. It can also be null.

You can ignore/filter selectors you don’t understand.

Client-side, you invoke:

  • A+B(a) => [x,y,z] → “this is what this person a is showing public”
  • A+C([a], [x,y]) → for user a, give me x and y
  • A+C([a], [addr(_, 60)] → for user a give me addr(x, 60)
  • C([], [addr(a, 60)]) → give me addr(a, 60)

Usage: at minimum, you do nothing and get the standard set of records when querying through the MultiResolver. You can also discover anyones full list of records.

You set a single slot on chain, to a namehash of a community name like “ethmoji-degens.eth”, which periodically updates a CCIP (or on-chain record) with their communities precomputed calldata of records/stats/attributes/data. tkn.eth could set a calldata[] record for all their supported chains and information.

This idea might be too half-baked and/or I might not of explained it enough, however I’ll leave this as a draft.

I’ll have to refine this some more but this is technically (3) ideas.

  1. templated calldata (bytes[]) in addr(C)
  2. minting immutable calldata under *.profile.reverse where:
    • addr(${keccak256(calldata)}.profile.reverse, C) = calldata
  3. the multi() / version = 2 for better multicalls

Just focusing on (1), to query a profile, you’d fetch the following calldata:

  1. addr(latest.profile.ens.eth, C) = calldata for "current standard records"
  2. addr(your name, C) = (optional) calldata for "your extra records"

Union that together and then read those records.

This is essentially the shapes idea from MerkleResolver.sol and idea (2) is a shapes registrar that makes abcd...1234.profile.reverse.

This makes it so:

  1. the DAO can publish new standards
  2. the official app can use latest.profile.ens.eth
  3. clients can use whichever standard they have rendering support for
  4. users can supply their own records
  5. offchain servers can expose their custom records

To make it fancier, the user’s profile has a custom resolution process, which is: if the data you read from addr(C) is 32-bytes, intrepret it as a namehash and loop until you get 0 bytes or abi.encoded calldata.

This makes it so:

  1. name A can use their own template
  2. name A can use the template of another name B
    (use parent, use community, etc…)

eg. a.b.eth can have addr(a.eth, C) = namehash(b.eth) and addr(b.eth, C) = <calldata> specific to the b.eth community, which might include extra records like text("height"), addr(69), etc.

I think this is much better than literal key or coin arrays:

  • text("more keys") = "key1, key2, key3"
  • addr(<some number>) = abi.encode([1, 2, 3])

Focusing on (2), multi()

// this is flattened outer product of names x calls
multi(bytes[] names, bytes[] calls) returns (bytes[])

If names is populated, the receiver is responsible for injecting the namehash into the calls before invoking them (templated):

multi([A, B], [x, y]) = [xA(), yA(), xB(), yB()]

When names is empty, the calls are invoked as-is.

multi([], [x, y]) = [x(), y()]


One name for multiple records:

multi(["raffy.eth"], [text("name"), addr(60), 
                      text("url"),  addr(0) ])

A few records for multiple names of the same domain:

multi(["raffy.cb.id", "chonk.cb.id"], [addr(60), text("avatar")])

One record across an entire domain:

multi(["a.eth", "b.eth", 
       "c.eth", "d.eth"], [addr(60)])

Ragged structure:

multi([], [addr("a.eth", 60), addr("b.eth", "60), 
                              text("b.eth", "url")])

Unlike what I described initially, this can be shuttled on top of resolve() since resolve(name, call) can be written as resolve("", multi([name], [call])).

Prebuilt calldata from Idea (1) can be passed directly to multi().


Apparently, I can only reply to myself 3 times :slight_smile:

Post #4

I experimented with this idea a bit for a custom solution for TKN.

They currently use this contract which enables structured multicall-like record access and is easy to integrate as you simply read the ABI, call the function, and get data.

However, the issue is that the ABI and the contract implementing it is fixed.

struct Metadata
struct Metadata {
        address contractAddress;
        string name;
        string url;
        string avatar;
        string description;
        string notice;
        string version;
        string decimals;
        string twitter;
        string github;
        bytes dweb;
        address payable arb1_address;
        address payable avaxc_address;
        address payable base_address;
        address payable bsc_address;
        address payable cro_address;
        address payable ftm_address;
        address payable gno_address;
        address payable matic_address;
        bytes near_address;
        address payable op_address;
        bytes sol_address;
        bytes trx_address;
        bytes zil_address; 
        address payable goerli_address; 
        address payable sepolia_address; 
    }

I applied the profile idea above, and created a demo called TNS which is an on-chain profile manager + CCIP-Read enabled lookup(string) wrapper around ENS.

(1) The contract has a basename, eg. tkn.eth

(2) addFields(data) / removeFieldAt(index) / fieldNames() / makeCalls(node)

These functions support building a profile of text(key), addr(type), contenthash(), etc. in a compressed form.

  • text(key) has implied key
  • addr(60) has a separate key, like eth, auto-prefixed with $
  • arg-less func(node) has a separate key, like contenthash, auto-prefixed with #

TNSFieldMaker.sol has some example field encodings.

  • fieldNames() returns (string[]) gives the human-readable keys for the fields.
  • makeCalls(node) returns (bytes[]) generates calldata corresponding to those fields, for a given node.

(2) lookup(string tick) -> [string, string][]

This function finds the resolver of the basename, and asks it to resolve {tick}.{basename} using resolve(multicall()) supplied with makeCalls(node). If that throws OffchainLookup, it rewrites its callback before throwing again.

On response, it uses the original resolver to decode the response, and then builds a key-value array, where the key is from fieldNames() and the calldata is either a literal UTF8-string or encoded as a hex-string.

(3) On the client, this looks like this:

import {ethers} from 'ethers';
let provider = new ethers.CloudProvider();
let contract = new ethers.Contract('0xAE845C0322693369A9a4f5BaE42F0fEC60cc1fC5', [
	'function lookup(string tick) external view returns (tuple(string, string)[] calls)',
], provider);
let fields = Object.fromEntries(await contract.lookup("eth", {enableCcipRead: true}));
// {
//    "name": "Ether",
//    "description": "Ethereum"
//    "$eth": 0x1234...
// }

TKN can now efficiently inscribe their API into mainnet. They moment they add a new record, every caller gets it automatically, it also emits FieldsChanged().

As long as there is some restraint on removeFieldAt() and/or some layer of security to ensure backwards compatibility, this is a good mix of flexibility and rigidity.

Underneath, it’s just using ENS with a TOR and an off-chain gateway, which enables cool stuff like, lookup("0x1234.base") becomes {addr}.{chain}.tkn.eth which is an offchain reverse lookup that returns the token information of that address on that chain.


This is essentially a compressed and mutable version of the profile idea. If we store compressed arrays of named calldata on-chain and give them ENS names, we can use this as a standard for querying profile data (or any subset of it.)

Additionally, if we allow names to specify a custom profile, then any name can advertise additional non-standard records efficiently.

If this custom profile record is read using CCIP, then off-chain and non-imported DNS names can expose custom records.

If these profiles are named/on-chain/immutable, they can be cached.

If the custom profile record can also be a pointer: to (1) a named profile or (2) namehash, then on-chain profiles can be efficiently shared between names (eg. members of a community, subdomains of a project, etc.)

A further extension of this would be having on-chain coin coders for address bytes → human readable strings.

2 Likes