NFT 'Showcase' ENS record and record standardization

Can you clarify for me, what exactly is the benefit of this scheme over generic URIs? My claim would be that since we already need to fetch the avatar, additional client-logic seems fine.

TBH, I’ve always been skeptical of the contenthash mechanic (and encoded coinType addr() records), as they seem like huge technical burdens when simpler, generic encodings get you nearly the same thing (reduced storage).

For a person using a wildcard resolver, storage is a non-issue. I’d expect future efficiency thru a “multicall” ccip or “multicall” ExtendedResolver (single fetch). However, for a person using an on-chain resolver, it seems desirable to mix on-chain and external records. The question is how to reference them efficiently and at what “logic layer” are they resolved (re: avatar.)

My quarter-baked idea above about self was few things mixed together: (1) sharing some subdata between multiple records, (2) parallelizing the access (single fetch), and (3) allowing record-level partial content hashes for integrity (opt-in).


Again, just throwing ideas out there:

(1) We should optimize the Public Resolver contract for more efficient storage. At the moment, you pay 1 slot for 1-31 bytes, 2 slots for 32 bytes, and 3 slots for 33-64 bytes.

I did a quick inventory on the Public Resolvers for “avatar”, “description”, “email”, and “url”. The median record is 33 bytes, which means half of all text records consume an extra sstore (20000 gas) for no reason.


(2) What if we create an ENS Text Record URI, like the avatar string, but more general:

For reference, avatar string: eip155:1/eip1155:contract/token

What about: enstxt:[chain]/[ens]/[text-key], eg. enstxt:1/raffy.eth/url
This resolves by ENS resolving "raffy.eth" on mainnet and returning the text record for "url"

If you couple this with a helper contract that uses EIP-165 to auto-detect contract type and generate the avatar string dynamically, then:
enstxt:1/1.azuki.nft-owner.eth/avatareip155:1/erc721:0xed5af388653567af2f388e6224dc7c4b3241c544/1


(3) We should have self-referencing resolver records:

Either, allow records to recursively refer to themselves through some template mechanism that’s resolved client-side:
url = "https://raffy.antistupid.com"
avatar = "{url}/avatar.img"

Or, just implement the same template logic into the resolver itself:
url = <lit:28:"https://raffy.antistupid.com">
avatar = <ref:3:"url"><lit:11:"/avatar.img">
This could also support hex encoding:
$eth = <hex:20:0x51050ec063d393217B436747617aD1C2285Aeeee>

This would allow collapsing addr and text into the same storage mechanism.

Pseudo-code 👈️
 // use efficient storage tech instead of default solidity bytes/string storage
setRaw(bytes32 node, string key, bytes memory v) { ... }
raw(bytes32 node, string key) returns (bytes) {}

uint256 constant LIT = 0;
uint256 constant HEX = 2;
uint256 constant SELF_REF = 3; // same resolver, same node
uint256 constant NODE_REF = 4; // same resolver, diff node (NAME_REF might be better in all cases)
uint256 constant NAME_REF = 5; // diff resolver, diff node
function concat(tuple(uint8 type, bytes data)[]) pure returns (bytes) { 
  // return bytes that can be decoded by expand()
}
expand(bytes32 node, bool text, bytes memory v) returns (bytes) {
   // treat the input as a sequence of tokens, of the following types:
   // 1. LIT(eral): data = consume(readLen())
   // eg. <lit:5:"raffy">
   // 2. HEX: data = consume(readLen(), 
   // eg. <hex:2:0xABCD>
   //   convert to "0xABCD" if text is true
   // 3. SELF_REF(erence): data = expand(node, string(consume(readLen())))
   // eg. <ref:3:"url">
   // 4. NODE_REF(erence): data = expand(consume(32), string(consume(readLen()))
   // eg. <noderef:0x123...:6:"avatar">
   // 5. NAME_REF(erence):
   /// eg. <nameref:"10:raffy2.eth":6:"avatar">
   // node = namehash(consume(readLen()); 
   // resolver = SmartResolver(ens.resolver(node));
   // key = string(consume(readLen()));
   // data = resolver.expand(node, text, resolve.raw(node, key));
   //
   // return the concatenated result 
}
addr(bytes32 node, uint256 coinType) returns (bytes) {
   // convert coinType to a record key + coder contract 
   (string key, AddrCoder coder) = coderFromCoinType(60)
   // read the raw record, expand it, decode it
   return coder.decode(coinType, expand(node, false, raw(node, key)));
}
text(bytes32 node, string key) returns (string) {
   // read the raw record, expand it as a string
   return string(expand(node, true, raw(node, key)));
}

interface AddrCoder {
   function decode(uint256 coinType, bytes memory v) pure returns (bytes memory);
   function encode(uint256 coinType, bytes memory v) pure returns (bytes memory);
}
// allow "eth" addr record to be accesses via "$eth" too
// EthCoder contract enforces 20 bytes and applies address checksum
defineAddrCoder(60, "$eth", <address of EthCoder contract>);

// traditional setters are just wrappers around setData()
setAddr(node, coinType, bytes value) {
    (string key, AddrCoder coder) = coderFromCoinType(coinType)
    // this would be inefficient since you can run the coder offchain
    setRaw(node, key, coder.encode(value));
}
setText(node, key, string value) {
   setRaw(node, key, concat([(LIT, bytes(value))]);
}

// new setters
setHex(node, key, bytes value) => setRaw(node, key, concat([(HEX, value)]));

Example usage:

node = namehash("raffy.eth")

// set url traditionally
setText(node, "url", "https://raffy.antistupid.com")
// set coinType via hex setter
setHex(node, "$eth", '0x51050ec063d393217B436747617aD1C2285Aeeee')
// both work:
text(node, "$eth") => "0x51050ec063d393217B436747617aD1C2285Aeeee"
addr(node, 60) => 0x51050ec063d393217B436747617aD1C2285Aeeee
// set avatar via reference
setRaw(node, "avatar", concat([(SELF_REF, "url"), (LIT, "/ens.jpg")]));
text(node, "avatar") => "https://raffy.antistupid.com/ens.jpg"
3 Likes