-module(gleeth@ens). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/gleeth/ens.gleam"). -export([namehash/1, decode_abi_string/1, resolve/2, reverse_resolve/2]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " ENS (Ethereum Name Service) resolution.\n" "\n" " Resolves human-readable `.eth` names to Ethereum addresses (forward)\n" " and addresses back to names (reverse) by querying the ENS registry\n" " and resolver contracts on-chain.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " // Forward: name -> address\n" " let assert Ok(address) = ens.resolve(provider, \"vitalik.eth\")\n" "\n" " // Reverse: address -> name\n" " let assert Ok(name) = ens.reverse_resolve(provider, \"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\")\n" " ```\n" ). -file("src/gleeth/ens.gleam", 90). ?DOC( " Compute the ENS namehash for a domain name.\n" "\n" " The namehash algorithm recursively hashes labels:\n" " `namehash(\"\") = 0x00...00`\n" " `namehash(\"eth\") = keccak256(namehash(\"\") + keccak256(\"eth\"))`\n" " `namehash(\"vitalik.eth\") = keccak256(namehash(\"eth\") + keccak256(\"vitalik\"))`\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " let hash = ens.namehash(\"vitalik.eth\")\n" " ```\n" ). -spec namehash(binary()) -> bitstring(). namehash(Name) -> case Name of <<""/utf8>> -> <<0:256>>; _ -> Labels = gleam@string:split(Name, <<"."/utf8>>), Reversed = lists:reverse(Labels), gleam@list:fold( Reversed, <<0:256>>, fun(Node, Label) -> Label_hash = gleeth@crypto@keccak:keccak256_binary( gleam_stdlib:identity(Label) ), gleeth@crypto@keccak:keccak256_binary( gleam_stdlib:bit_array_concat([Node, Label_hash]) ) end ) end. -file("src/gleeth/ens.gleam", 162). ?DOC( " ABI-decode a string from RPC return data.\n" " Layout: offset(32 bytes) + length(32 bytes at offset) + data.\n" " Shared between ENS and permit modules.\n" ). -spec decode_abi_string(binary()) -> {ok, binary()} | {error, gleeth@rpc@types:gleeth_error()}. decode_abi_string(Result_hex) -> gleam@result:'try'( begin _pipe = gleeth@utils@hex:decode(Result_hex), gleam@result:map_error( _pipe, fun(_) -> {parse_error, <<"Invalid hex response"/utf8>>} end ) end, fun(Bytes) -> case erlang:byte_size(Bytes) >= 64 of false -> {error, {parse_error, <<"ABI string response too short"/utf8>>}}; true -> gleam@result:'try'( begin _pipe@1 = gleam_stdlib:bit_array_slice(Bytes, 0, 32), gleam@result:map_error( _pipe@1, fun(_) -> {parse_error, <<"Failed to read string offset"/utf8>>} end ) end, fun(Offset_bytes) -> Offset = gleeth@utils@hex:bytes_to_int(Offset_bytes), gleam@result:'try'( begin _pipe@2 = gleam_stdlib:bit_array_slice( Bytes, Offset, 32 ), gleam@result:map_error( _pipe@2, fun(_) -> {parse_error, <<"Failed to read string length"/utf8>>} end ) end, fun(Len_bytes) -> Len = gleeth@utils@hex:bytes_to_int( Len_bytes ), case Len > 0 of false -> {error, {parse_error, <<"Empty string in ABI response"/utf8>>}}; true -> gleam@result:'try'( begin _pipe@3 = gleam_stdlib:bit_array_slice( Bytes, Offset + 32, Len ), gleam@result:map_error( _pipe@3, fun(_) -> {parse_error, <<"Failed to read string data"/utf8>>} end ) end, fun(Str_bytes) -> case gleam@bit_array:to_string( Str_bytes ) of {ok, S} -> {ok, S}; {error, _} -> {error, {parse_error, <<"Invalid UTF-8 in ABI string"/utf8>>}} end end ) end end ) end ) end end ). -file("src/gleeth/ens.gleam", 207). ?DOC(" Call a function that takes a bytes32 arg and returns a string.\n"). -spec call_string(gleeth@provider:provider(), binary(), binary(), binary()) -> {ok, binary()} | {error, gleeth@rpc@types:gleeth_error()}. call_string(Provider, Contract, Selector, Arg_hex) -> Arg_clean = gleeth@utils@hex:strip_prefix(Arg_hex), Padded = gleam@string:pad_start(Arg_clean, 64, <<"0"/utf8>>), Calldata = <>, gleam@result:'try'( gleeth@rpc@methods:call_contract(Provider, Contract, Calldata), fun(Result_hex) -> decode_abi_string(Result_hex) end ). -file("src/gleeth/ens.gleam", 226). ?DOC(" Call a function that takes a bytes32 arg and returns an address.\n"). -spec call_address(gleeth@provider:provider(), binary(), binary(), binary()) -> {ok, binary()} | {error, gleeth@rpc@types:gleeth_error()}. call_address(Provider, Contract, Selector, Arg_hex) -> Arg_clean = gleeth@utils@hex:strip_prefix(Arg_hex), Padded = gleam@string:pad_start(Arg_clean, 64, <<"0"/utf8>>), Calldata = <>, gleam@result:'try'( gleeth@rpc@methods:call_contract(Provider, Contract, Calldata), fun(Result_hex) -> gleam@result:'try'( begin _pipe = gleeth@utils@hex:decode(Result_hex), gleam@result:map_error( _pipe, fun(_) -> {parse_error, <<"Invalid hex in address response"/utf8>>} end ) end, fun(Bytes) -> Size = erlang:byte_size(Bytes), case Size >= 32 of false -> {error, {parse_error, <<"Address response too short"/utf8>>}}; true -> gleam@result:'try'( begin _pipe@1 = gleam_stdlib:bit_array_slice( Bytes, 12, 20 ), gleam@result:map_error( _pipe@1, fun(_) -> {parse_error, <<"Failed to extract address bytes"/utf8>>} end ) end, fun(Addr_bytes) -> Addr = <<"0x"/utf8, (string:lowercase( gleam_stdlib:base16_encode( Addr_bytes ) ))/binary>>, {ok, Addr} end ) end end ) end ). -file("src/gleeth/ens.gleam", 41). ?DOC( " Resolve an ENS name to an Ethereum address.\n" "\n" " Queries the ENS registry for the resolver, then queries the resolver\n" " for the address. Returns an error if the name has no resolver or no\n" " address set.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " let assert Ok(addr) = ens.resolve(provider, \"vitalik.eth\")\n" " ```\n" ). -spec resolve(gleeth@provider:provider(), binary()) -> {ok, binary()} | {error, gleeth@rpc@types:gleeth_error()}. resolve(Provider, Name) -> Node = namehash(Name), Node_hex = gleeth@utils@hex:encode(Node), gleam@result:'try'( call_address( Provider, <<"0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"/utf8>>, <<"0x0178b8bf"/utf8>>, Node_hex ), fun(Resolver_address) -> case Resolver_address of <<""/utf8>> -> {error, {parse_error, <<"No resolver found for: "/utf8, Name/binary>>}}; <<"0x0000000000000000000000000000000000000000"/utf8>> -> {error, {parse_error, <<"No resolver found for: "/utf8, Name/binary>>}}; Resolver -> gleam@result:'try'( call_address( Provider, Resolver, <<"0x3b3b57de"/utf8>>, Node_hex ), fun(Address) -> case Address of <<""/utf8>> -> {error, {parse_error, <<"No address set for: "/utf8, Name/binary>>}}; <<"0x0000000000000000000000000000000000000000"/utf8>> -> {error, {parse_error, <<"No address set for: "/utf8, Name/binary>>}}; Addr -> {ok, Addr} end end ) end end ). -file("src/gleeth/ens.gleam", 118). ?DOC( " Reverse resolve an Ethereum address to its primary ENS name.\n" "\n" " Computes the reverse node (`.addr.reverse`), queries the ENS\n" " registry for the reverse resolver, then calls `name(bytes32)` on it.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " let assert Ok(name) = ens.reverse_resolve(\n" " provider,\n" " \"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\",\n" " )\n" " // name = \"vitalik.eth\"\n" " ```\n" ). -spec reverse_resolve(gleeth@provider:provider(), binary()) -> {ok, binary()} | {error, gleeth@rpc@types:gleeth_error()}. reverse_resolve(Provider, Address) -> Clean_addr = string:lowercase(gleeth@utils@hex:strip_prefix(Address)), Reverse_name = <>, Node = namehash(Reverse_name), Node_hex = gleeth@utils@hex:encode(Node), gleam@result:'try'( call_address( Provider, <<"0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"/utf8>>, <<"0x0178b8bf"/utf8>>, Node_hex ), fun(Resolver_address) -> case Resolver_address of <<""/utf8>> -> {error, {parse_error, <<"No reverse resolver for: "/utf8, Address/binary>>}}; <<"0x0000000000000000000000000000000000000000"/utf8>> -> {error, {parse_error, <<"No reverse resolver for: "/utf8, Address/binary>>}}; Resolver -> gleam@result:'try'( call_string( Provider, Resolver, <<"0x691f3431"/utf8>>, Node_hex ), fun(Name) -> case Name of <<""/utf8>> -> {error, {parse_error, <<"No reverse record for: "/utf8, Address/binary>>}}; N -> {ok, N} end end ) end end ).