From 911fad00cbc686540ab8b18b123d90c77f70598d Mon Sep 17 00:00:00 2001 From: Dhaiwat Date: Tue, 5 May 2026 21:53:46 +0530 Subject: [PATCH 01/21] chore: upgrade to ENSv2 (Universal Resolver) Route the package-level Resolve and ReverseResolve through the ENS Universal Resolver proxy at 0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe instead of walking the legacy registry. CCIP-Read (ERC-3668) is followed transparently via a new ccipread package, so offchain and L2 names now resolve correctly. Legacy Resolver/Registry/ReverseResolver/Name types keep their existing behaviour so write paths (SetAddress, SetText, etc.) are unaffected and no public function signatures change. Co-Authored-By: Claude Opus 4.7 (1M context) --- ccipread/ccipread.go | 313 +++++++++ contracts/universalresolver/contract.abi | 1 + contracts/universalresolver/contract.go | 845 +++++++++++++++++++++++ contracts/universalresolver/generate.go | 3 + dnsencode.go | 50 ++ resolver.go | 34 +- resolver_test.go | 12 +- reverseresolver.go | 20 +- reverseresolver_test.go | 4 +- tokenid_test.go | 2 +- universalresolver.go | 149 ++++ universalresolver_test.go | 87 +++ urerrors.go | 251 +++++++ urerrors_test.go | 104 +++ 14 files changed, 1838 insertions(+), 37 deletions(-) create mode 100644 ccipread/ccipread.go create mode 100644 contracts/universalresolver/contract.abi create mode 100644 contracts/universalresolver/contract.go create mode 100644 contracts/universalresolver/generate.go create mode 100644 dnsencode.go create mode 100644 universalresolver.go create mode 100644 universalresolver_test.go create mode 100644 urerrors.go create mode 100644 urerrors_test.go diff --git a/ccipread/ccipread.go b/ccipread/ccipread.go new file mode 100644 index 0000000..3ad00a9 --- /dev/null +++ b/ccipread/ccipread.go @@ -0,0 +1,313 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +// Package ccipread implements the client side of ERC-3668 (CCIP Read) +// so that calls into ENS contracts which revert with OffchainLookup are +// transparently followed: the gateway is queried, the callback is invoked, +// and the chain is repeated until a non-revert result is returned. +package ccipread + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "net/http" + "strings" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +// MaxRedirects bounds the number of OffchainLookup hops a single call may chain. +// Four matches the limit used by ENS-aware libraries (viem, ethers). +const MaxRedirects = 4 + +// offchainLookupSelector is the first 4 bytes of +// keccak256("OffchainLookup(address,string[],bytes,bytes4,bytes)"). +var offchainLookupSelector = []byte{0x55, 0x6f, 0x18, 0x30} + +// offchainLookupArgs is the ABI for the OffchainLookup error parameters, +// used to decode the revert payload that follows the 4-byte selector. +var offchainLookupArgs abi.Arguments + +// callbackArgs is the ABI for the callback signature +// callbackFunction(bytes response, bytes extraData). +var callbackArgs abi.Arguments + +func init() { + addrT, _ := abi.NewType("address", "", nil) + stringArrT, _ := abi.NewType("string[]", "", nil) + bytesT, _ := abi.NewType("bytes", "", nil) + bytes4T, _ := abi.NewType("bytes4", "", nil) + offchainLookupArgs = abi.Arguments{ + {Name: "sender", Type: addrT}, + {Name: "urls", Type: stringArrT}, + {Name: "callData", Type: bytesT}, + {Name: "callbackFunction", Type: bytes4T}, + {Name: "extraData", Type: bytesT}, + } + callbackArgs = abi.Arguments{ + {Name: "response", Type: bytesT}, + {Name: "extraData", Type: bytesT}, + } +} + +// OffchainLookup is a decoded OffchainLookup revert. +type OffchainLookup struct { + Sender common.Address + URLs []string + CallData []byte + CallbackFunction [4]byte + ExtraData []byte +} + +// Caller is the subset of bind.ContractBackend needed for CCIP-Read calls. +type Caller interface { + bind.ContractCaller +} + +// Options tune the CCIP-Read client. A zero value is valid and uses +// http.DefaultClient with the package default for MaxRedirects. +type Options struct { + HTTPClient *http.Client + MaxRedirects int + // BlockNumber pins the eth_call to a specific block. Nil means latest. + BlockNumber *big.Int +} + +// Call executes a contract call and follows any OffchainLookup reverts. +// On success it returns the raw bytes returned by the (possibly callback-) +// invoked function. The decoding of those bytes is the caller's job — for +// UniversalResolver.resolve they ABI-decode as (bytes, address); for +// UniversalResolver.reverse they decode as (string, address, address). +func Call(ctx context.Context, backend Caller, to common.Address, data []byte, opts *Options) ([]byte, error) { + if opts == nil { + opts = &Options{} + } + max := opts.MaxRedirects + if max <= 0 { + max = MaxRedirects + } + httpClient := opts.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + target := to + callData := data + for hop := 0; hop <= max; hop++ { + out, callErr := backend.CallContract(ctx, ethereum.CallMsg{To: &target, Data: callData}, opts.BlockNumber) + if callErr == nil { + return out, nil + } + lookup, ok := decodeOffchainLookup(callErr) + if !ok { + return nil, callErr + } + // Per ERC-3668 the sender field MUST equal the contract that + // reverted; otherwise the response could be misdirected. + if lookup.Sender != target { + return nil, fmt.Errorf("ccipread: OffchainLookup sender %s does not match call target %s", lookup.Sender.Hex(), target.Hex()) + } + response, err := queryGateways(ctx, httpClient, lookup.URLs, lookup.Sender, lookup.CallData) + if err != nil { + return nil, err + } + nextData, err := encodeCallback(lookup.CallbackFunction, response, lookup.ExtraData) + if err != nil { + return nil, err + } + callData = nextData + // target stays the same: the callback lives on `sender`, which we just + // validated equals the previous target. + } + return nil, fmt.Errorf("ccipread: exceeded %d OffchainLookup redirects", max) +} + +// DecodeOffchainLookup parses an OffchainLookup revert from raw revert bytes +// (selector + ABI-encoded args). It returns false if the data does not start +// with the OffchainLookup selector. +func DecodeOffchainLookup(revert []byte) (*OffchainLookup, bool) { + if len(revert) < 4 || !bytes.Equal(revert[:4], offchainLookupSelector) { + return nil, false + } + values, err := offchainLookupArgs.Unpack(revert[4:]) + if err != nil || len(values) != 5 { + return nil, false + } + sender, ok := values[0].(common.Address) + if !ok { + return nil, false + } + urls, ok := values[1].([]string) + if !ok { + return nil, false + } + callData, ok := values[2].([]byte) + if !ok { + return nil, false + } + callbackFunction, ok := values[3].([4]byte) + if !ok { + return nil, false + } + extraData, ok := values[4].([]byte) + if !ok { + return nil, false + } + return &OffchainLookup{ + Sender: sender, + URLs: urls, + CallData: callData, + CallbackFunction: callbackFunction, + ExtraData: extraData, + }, true +} + +// decodeOffchainLookup attempts to extract an OffchainLookup payload from a +// CallContract error. Backends expose the revert data via the rpc.DataError +// interface; geth and ethclient both satisfy it. +func decodeOffchainLookup(err error) (*OffchainLookup, bool) { + if err == nil { + return nil, false + } + type dataError interface{ ErrorData() interface{} } + var de dataError + if !errors.As(err, &de) { + return nil, false + } + raw, ok := de.ErrorData().(string) + if !ok { + return nil, false + } + revert, err := hex.DecodeString(strings.TrimPrefix(raw, "0x")) + if err != nil { + return nil, false + } + return DecodeOffchainLookup(revert) +} + +func encodeCallback(selector [4]byte, response, extraData []byte) ([]byte, error) { + body, err := callbackArgs.Pack(response, extraData) + if err != nil { + return nil, fmt.Errorf("ccipread: encode callback args: %w", err) + } + out := make([]byte, 0, 4+len(body)) + out = append(out, selector[:]...) + out = append(out, body...) + return out, nil +} + +// gatewayRequest is the JSON body the spec defines for POST queries. +type gatewayRequest struct { + Data string `json:"data"` + Sender string `json:"sender"` +} + +// gatewayResponse is the JSON envelope returned by gateways. +type gatewayResponse struct { + Data string `json:"data"` +} + +// queryGateways tries each URL in order and returns the first successful +// response. Per ERC-3668 a 4xx aborts the whole call; a 5xx falls through +// to the next URL. +func queryGateways(ctx context.Context, client *http.Client, urls []string, sender common.Address, data []byte) ([]byte, error) { + if len(urls) == 0 { + return nil, errors.New("ccipread: OffchainLookup returned no gateway URLs") + } + hexData := "0x" + hex.EncodeToString(data) + hexSender := strings.ToLower(sender.Hex()) + var lastErr error + for _, raw := range urls { + resp, status, err := queryGateway(ctx, client, raw, hexSender, hexData) + if err == nil { + return resp, nil + } + // 4xx: per spec, abort immediately — request is malformed and other + // gateways will reject it the same way. + if status >= 400 && status < 500 { + return nil, err + } + lastErr = err + } + if lastErr == nil { + lastErr = errors.New("ccipread: no gateway URL succeeded") + } + return nil, lastErr +} + +func queryGateway(ctx context.Context, client *http.Client, url, sender, data string) ([]byte, int, error) { + hasData := strings.Contains(url, "{data}") + hasSender := strings.Contains(url, "{sender}") + var req *http.Request + var err error + if hasData { + // GET form: substitute placeholders directly into the URL. + populated := strings.ReplaceAll(url, "{data}", data) + if hasSender { + populated = strings.ReplaceAll(populated, "{sender}", sender) + } + req, err = http.NewRequestWithContext(ctx, http.MethodGet, populated, nil) + } else { + body, jerr := json.Marshal(gatewayRequest{Data: data, Sender: sender}) + if jerr != nil { + return nil, 0, jerr + } + req, err = http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err == nil { + req.Header.Set("Content-Type", "application/json") + } + } + if err != nil { + return nil, 0, err + } + if client.Timeout == 0 { + // Best-effort timeout when the caller didn't supply one. Without this + // a wedged gateway would block resolution indefinitely. + ctxT, cancel := context.WithTimeout(req.Context(), 30*time.Second) + defer cancel() + req = req.WithContext(ctxT) + } + resp, err := client.Do(req) + if err != nil { + return nil, 0, fmt.Errorf("ccipread: gateway %s: %w", url, err) + } + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, resp.StatusCode, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, resp.StatusCode, fmt.Errorf("ccipread: gateway %s: HTTP %d: %s", url, resp.StatusCode, truncate(string(bodyBytes), 256)) + } + var parsed gatewayResponse + if err := json.Unmarshal(bodyBytes, &parsed); err != nil { + return nil, resp.StatusCode, fmt.Errorf("ccipread: gateway %s: invalid JSON: %w", url, err) + } + out, err := hex.DecodeString(strings.TrimPrefix(parsed.Data, "0x")) + if err != nil { + return nil, resp.StatusCode, fmt.Errorf("ccipread: gateway %s: invalid hex in response.data: %w", url, err) + } + return out, resp.StatusCode, nil +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/contracts/universalresolver/contract.abi b/contracts/universalresolver/contract.abi new file mode 100644 index 0000000..5080ebd --- /dev/null +++ b/contracts/universalresolver/contract.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"contract ENS","name":"ens","type":"address"},{"internalType":"contract IGatewayProvider","name":"batchGatewayProvider","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes","name":"dns","type":"bytes"}],"name":"DNSDecodingFailed","type":"error"},{"inputs":[{"internalType":"string","name":"ens","type":"string"}],"name":"DNSEncodingFailed","type":"error"},{"inputs":[],"name":"EmptyAddress","type":"error"},{"inputs":[{"internalType":"uint16","name":"status","type":"uint16"},{"internalType":"string","name":"message","type":"string"}],"name":"HttpError","type":"error"},{"inputs":[],"name":"InvalidBatchGatewayResponse","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"string[]","name":"urls","type":"string[]"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"bytes4","name":"callbackFunction","type":"bytes4"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"OffchainLookup","type":"error"},{"inputs":[{"internalType":"uint256","name":"offset","type":"uint256"},{"internalType":"uint256","name":"length","type":"uint256"}],"name":"OffsetOutOfBoundsError","type":"error"},{"inputs":[{"internalType":"bytes","name":"errorData","type":"bytes"}],"name":"ResolverError","type":"error"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"address","name":"resolver","type":"address"}],"name":"ResolverNotContract","type":"error"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"ResolverNotFound","type":"error"},{"inputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"bytes","name":"primaryAddress","type":"bytes"}],"name":"ReverseAddressMismatch","type":"error"},{"inputs":[{"internalType":"bytes4","name":"selector","type":"bytes4"}],"name":"UnsupportedResolverProfile","type":"error"},{"inputs":[],"name":"batchGatewayProvider","outputs":[{"internalType":"contract IGatewayProvider","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"call","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"flags","type":"uint256"}],"internalType":"struct CCIPBatcher.Lookup[]","name":"lookups","type":"tuple[]"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"internalType":"struct CCIPBatcher.Batch","name":"batch","type":"tuple"}],"name":"ccipBatch","outputs":[{"components":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"call","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"flags","type":"uint256"}],"internalType":"struct CCIPBatcher.Lookup[]","name":"lookups","type":"tuple[]"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"internalType":"struct CCIPBatcher.Batch","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"ccipBatchCallback","outputs":[{"components":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"call","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"flags","type":"uint256"}],"internalType":"struct CCIPBatcher.Lookup[]","name":"lookups","type":"tuple[]"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"internalType":"struct CCIPBatcher.Batch","name":"batch","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"ccipReadCallback","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"findResolver","outputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"registry","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"requireResolver","outputs":[{"components":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"uint256","name":"offset","type":"uint256"},{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"bool","name":"extended","type":"bool"}],"internalType":"struct AbstractUniversalResolver.ResolverInfo","name":"info","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"resolve","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"resolveBatchCallback","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"resolveCallback","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"resolveDirectCallback","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"resolveDirectCallbackError","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"name":"resolveWithGateways","outputs":[{"internalType":"bytes","name":"result","type":"bytes"},{"internalType":"address","name":"resolver","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"resolver","type":"address"},{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"name":"resolveWithResolver","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"lookupAddress","type":"bytes"},{"internalType":"uint256","name":"coinType","type":"uint256"}],"name":"reverse","outputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"reverseAddressCallback","outputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"address","name":"reverseResolver","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"reverseNameCallback","outputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"lookupAddress","type":"bytes"},{"internalType":"uint256","name":"coinType","type":"uint256"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"name":"reverseWithGateways","outputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"address","name":"reverseResolver","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/contracts/universalresolver/contract.go b/contracts/universalresolver/contract.go new file mode 100644 index 0000000..9d12598 --- /dev/null +++ b/contracts/universalresolver/contract.go @@ -0,0 +1,845 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package universalresolver + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// AbstractUniversalResolverResolverInfo is an auto generated low-level Go binding around an user-defined struct. +type AbstractUniversalResolverResolverInfo struct { + Name []byte + Offset *big.Int + Node [32]byte + Resolver common.Address + Extended bool +} + +// CCIPBatcherBatch is an auto generated low-level Go binding around an user-defined struct. +type CCIPBatcherBatch struct { + Lookups []CCIPBatcherLookup + Gateways []string +} + +// CCIPBatcherLookup is an auto generated low-level Go binding around an user-defined struct. +type CCIPBatcherLookup struct { + Target common.Address + Call []byte + Data []byte + Flags *big.Int +} + +// ContractMetaData contains all meta data concerning the Contract contract. +var ContractMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"contractENS\",\"name\":\"ens\",\"type\":\"address\"},{\"internalType\":\"contractIGatewayProvider\",\"name\":\"batchGatewayProvider\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"dns\",\"type\":\"bytes\"}],\"name\":\"DNSDecodingFailed\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"ens\",\"type\":\"string\"}],\"name\":\"DNSEncodingFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyAddress\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint16\",\"name\":\"status\",\"type\":\"uint16\"},{\"internalType\":\"string\",\"name\":\"message\",\"type\":\"string\"}],\"name\":\"HttpError\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidBatchGatewayResponse\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"string[]\",\"name\":\"urls\",\"type\":\"string[]\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"callbackFunction\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"OffchainLookup\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"offset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"OffsetOutOfBoundsError\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"errorData\",\"type\":\"bytes\"}],\"name\":\"ResolverError\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"}],\"name\":\"ResolverNotContract\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"}],\"name\":\"ResolverNotFound\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"bytes\",\"name\":\"primaryAddress\",\"type\":\"bytes\"}],\"name\":\"ReverseAddressMismatch\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"selector\",\"type\":\"bytes4\"}],\"name\":\"UnsupportedResolverProfile\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"batchGatewayProvider\",\"outputs\":[{\"internalType\":\"contractIGatewayProvider\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"flags\",\"type\":\"uint256\"}],\"internalType\":\"structCCIPBatcher.Lookup[]\",\"name\":\"lookups\",\"type\":\"tuple[]\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"internalType\":\"structCCIPBatcher.Batch\",\"name\":\"batch\",\"type\":\"tuple\"}],\"name\":\"ccipBatch\",\"outputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"flags\",\"type\":\"uint256\"}],\"internalType\":\"structCCIPBatcher.Lookup[]\",\"name\":\"lookups\",\"type\":\"tuple[]\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"internalType\":\"structCCIPBatcher.Batch\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"ccipBatchCallback\",\"outputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"flags\",\"type\":\"uint256\"}],\"internalType\":\"structCCIPBatcher.Lookup[]\",\"name\":\"lookups\",\"type\":\"tuple[]\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"internalType\":\"structCCIPBatcher.Batch\",\"name\":\"batch\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"ccipReadCallback\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"}],\"name\":\"findResolver\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"registry\",\"outputs\":[{\"internalType\":\"contractENS\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"}],\"name\":\"requireResolver\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"offset\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"node\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"extended\",\"type\":\"bool\"}],\"internalType\":\"structAbstractUniversalResolver.ResolverInfo\",\"name\":\"info\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"resolve\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"resolveBatchCallback\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"resolveCallback\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"resolveDirectCallback\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"name\":\"resolveDirectCallbackError\",\"outputs\":[],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"name\":\"resolveWithGateways\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"result\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"name\":\"resolveWithResolver\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"lookupAddress\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"coinType\",\"type\":\"uint256\"}],\"name\":\"reverse\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"reverseAddressCallback\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"reverseResolver\",\"type\":\"address\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"reverseNameCallback\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"lookupAddress\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"coinType\",\"type\":\"uint256\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"name\":\"reverseWithGateways\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"reverseResolver\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// ContractABI is the input ABI used to generate the binding from. +// Deprecated: Use ContractMetaData.ABI instead. +var ContractABI = ContractMetaData.ABI + +// Contract is an auto generated Go binding around an Ethereum contract. +type Contract struct { + ContractCaller // Read-only binding to the contract + ContractTransactor // Write-only binding to the contract + ContractFilterer // Log filterer for contract events +} + +// ContractCaller is an auto generated read-only Go binding around an Ethereum contract. +type ContractCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ContractTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ContractFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ContractSession struct { + Contract *Contract // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ContractCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ContractCallerSession struct { + Contract *ContractCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ContractTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ContractTransactorSession struct { + Contract *ContractTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ContractRaw is an auto generated low-level Go binding around an Ethereum contract. +type ContractRaw struct { + Contract *Contract // Generic contract binding to access the raw methods on +} + +// ContractCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ContractCallerRaw struct { + Contract *ContractCaller // Generic read-only contract binding to access the raw methods on +} + +// ContractTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ContractTransactorRaw struct { + Contract *ContractTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewContract creates a new instance of Contract, bound to a specific deployed contract. +func NewContract(address common.Address, backend bind.ContractBackend) (*Contract, error) { + contract, err := bindContract(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Contract{ContractCaller: ContractCaller{contract: contract}, ContractTransactor: ContractTransactor{contract: contract}, ContractFilterer: ContractFilterer{contract: contract}}, nil +} + +// NewContractCaller creates a new read-only instance of Contract, bound to a specific deployed contract. +func NewContractCaller(address common.Address, caller bind.ContractCaller) (*ContractCaller, error) { + contract, err := bindContract(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ContractCaller{contract: contract}, nil +} + +// NewContractTransactor creates a new write-only instance of Contract, bound to a specific deployed contract. +func NewContractTransactor(address common.Address, transactor bind.ContractTransactor) (*ContractTransactor, error) { + contract, err := bindContract(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ContractTransactor{contract: contract}, nil +} + +// NewContractFilterer creates a new log filterer instance of Contract, bound to a specific deployed contract. +func NewContractFilterer(address common.Address, filterer bind.ContractFilterer) (*ContractFilterer, error) { + contract, err := bindContract(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ContractFilterer{contract: contract}, nil +} + +// bindContract binds a generic wrapper to an already deployed contract. +func bindContract(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ContractMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Contract *ContractRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Contract.Contract.ContractCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Contract *ContractRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Contract.Contract.ContractTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Contract *ContractRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Contract.Contract.ContractTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Contract *ContractCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Contract.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Contract *ContractTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Contract.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Contract *ContractTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Contract.Contract.contract.Transact(opts, method, params...) +} + +// BatchGatewayProvider is a free data retrieval call binding the contract method 0x02cf2578. +// +// Solidity: function batchGatewayProvider() view returns(address) +func (_Contract *ContractCaller) BatchGatewayProvider(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "batchGatewayProvider") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// BatchGatewayProvider is a free data retrieval call binding the contract method 0x02cf2578. +// +// Solidity: function batchGatewayProvider() view returns(address) +func (_Contract *ContractSession) BatchGatewayProvider() (common.Address, error) { + return _Contract.Contract.BatchGatewayProvider(&_Contract.CallOpts) +} + +// BatchGatewayProvider is a free data retrieval call binding the contract method 0x02cf2578. +// +// Solidity: function batchGatewayProvider() view returns(address) +func (_Contract *ContractCallerSession) BatchGatewayProvider() (common.Address, error) { + return _Contract.Contract.BatchGatewayProvider(&_Contract.CallOpts) +} + +// CcipBatch is a free data retrieval call binding the contract method 0x9f28e99d. +// +// Solidity: function ccipBatch(((address,bytes,bytes,uint256)[],string[]) batch) view returns(((address,bytes,bytes,uint256)[],string[])) +func (_Contract *ContractCaller) CcipBatch(opts *bind.CallOpts, batch CCIPBatcherBatch) (CCIPBatcherBatch, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "ccipBatch", batch) + + if err != nil { + return *new(CCIPBatcherBatch), err + } + + out0 := *abi.ConvertType(out[0], new(CCIPBatcherBatch)).(*CCIPBatcherBatch) + + return out0, err + +} + +// CcipBatch is a free data retrieval call binding the contract method 0x9f28e99d. +// +// Solidity: function ccipBatch(((address,bytes,bytes,uint256)[],string[]) batch) view returns(((address,bytes,bytes,uint256)[],string[])) +func (_Contract *ContractSession) CcipBatch(batch CCIPBatcherBatch) (CCIPBatcherBatch, error) { + return _Contract.Contract.CcipBatch(&_Contract.CallOpts, batch) +} + +// CcipBatch is a free data retrieval call binding the contract method 0x9f28e99d. +// +// Solidity: function ccipBatch(((address,bytes,bytes,uint256)[],string[]) batch) view returns(((address,bytes,bytes,uint256)[],string[])) +func (_Contract *ContractCallerSession) CcipBatch(batch CCIPBatcherBatch) (CCIPBatcherBatch, error) { + return _Contract.Contract.CcipBatch(&_Contract.CallOpts, batch) +} + +// CcipBatchCallback is a free data retrieval call binding the contract method 0xb536af76. +// +// Solidity: function ccipBatchCallback(bytes response, bytes extraData) view returns(((address,bytes,bytes,uint256)[],string[]) batch) +func (_Contract *ContractCaller) CcipBatchCallback(opts *bind.CallOpts, response []byte, extraData []byte) (CCIPBatcherBatch, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "ccipBatchCallback", response, extraData) + + if err != nil { + return *new(CCIPBatcherBatch), err + } + + out0 := *abi.ConvertType(out[0], new(CCIPBatcherBatch)).(*CCIPBatcherBatch) + + return out0, err + +} + +// CcipBatchCallback is a free data retrieval call binding the contract method 0xb536af76. +// +// Solidity: function ccipBatchCallback(bytes response, bytes extraData) view returns(((address,bytes,bytes,uint256)[],string[]) batch) +func (_Contract *ContractSession) CcipBatchCallback(response []byte, extraData []byte) (CCIPBatcherBatch, error) { + return _Contract.Contract.CcipBatchCallback(&_Contract.CallOpts, response, extraData) +} + +// CcipBatchCallback is a free data retrieval call binding the contract method 0xb536af76. +// +// Solidity: function ccipBatchCallback(bytes response, bytes extraData) view returns(((address,bytes,bytes,uint256)[],string[]) batch) +func (_Contract *ContractCallerSession) CcipBatchCallback(response []byte, extraData []byte) (CCIPBatcherBatch, error) { + return _Contract.Contract.CcipBatchCallback(&_Contract.CallOpts, response, extraData) +} + +// CcipReadCallback is a free data retrieval call binding the contract method 0xef46c0b8. +// +// Solidity: function ccipReadCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractCaller) CcipReadCallback(opts *bind.CallOpts, response []byte, extraData []byte) error { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "ccipReadCallback", response, extraData) + + if err != nil { + return err + } + + return err + +} + +// CcipReadCallback is a free data retrieval call binding the contract method 0xef46c0b8. +// +// Solidity: function ccipReadCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractSession) CcipReadCallback(response []byte, extraData []byte) error { + return _Contract.Contract.CcipReadCallback(&_Contract.CallOpts, response, extraData) +} + +// CcipReadCallback is a free data retrieval call binding the contract method 0xef46c0b8. +// +// Solidity: function ccipReadCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractCallerSession) CcipReadCallback(response []byte, extraData []byte) error { + return _Contract.Contract.CcipReadCallback(&_Contract.CallOpts, response, extraData) +} + +// FindResolver is a free data retrieval call binding the contract method 0xa1cbcbaf. +// +// Solidity: function findResolver(bytes name) view returns(address, bytes32, uint256) +func (_Contract *ContractCaller) FindResolver(opts *bind.CallOpts, name []byte) (common.Address, [32]byte, *big.Int, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "findResolver", name) + + if err != nil { + return *new(common.Address), *new([32]byte), *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + out1 := *abi.ConvertType(out[1], new([32]byte)).(*[32]byte) + out2 := *abi.ConvertType(out[2], new(*big.Int)).(**big.Int) + + return out0, out1, out2, err + +} + +// FindResolver is a free data retrieval call binding the contract method 0xa1cbcbaf. +// +// Solidity: function findResolver(bytes name) view returns(address, bytes32, uint256) +func (_Contract *ContractSession) FindResolver(name []byte) (common.Address, [32]byte, *big.Int, error) { + return _Contract.Contract.FindResolver(&_Contract.CallOpts, name) +} + +// FindResolver is a free data retrieval call binding the contract method 0xa1cbcbaf. +// +// Solidity: function findResolver(bytes name) view returns(address, bytes32, uint256) +func (_Contract *ContractCallerSession) FindResolver(name []byte) (common.Address, [32]byte, *big.Int, error) { + return _Contract.Contract.FindResolver(&_Contract.CallOpts, name) +} + +// Registry is a free data retrieval call binding the contract method 0x7b103999. +// +// Solidity: function registry() view returns(address) +func (_Contract *ContractCaller) Registry(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "registry") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Registry is a free data retrieval call binding the contract method 0x7b103999. +// +// Solidity: function registry() view returns(address) +func (_Contract *ContractSession) Registry() (common.Address, error) { + return _Contract.Contract.Registry(&_Contract.CallOpts) +} + +// Registry is a free data retrieval call binding the contract method 0x7b103999. +// +// Solidity: function registry() view returns(address) +func (_Contract *ContractCallerSession) Registry() (common.Address, error) { + return _Contract.Contract.Registry(&_Contract.CallOpts) +} + +// RequireResolver is a free data retrieval call binding the contract method 0xc285238a. +// +// Solidity: function requireResolver(bytes name) view returns((bytes,uint256,bytes32,address,bool) info) +func (_Contract *ContractCaller) RequireResolver(opts *bind.CallOpts, name []byte) (AbstractUniversalResolverResolverInfo, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "requireResolver", name) + + if err != nil { + return *new(AbstractUniversalResolverResolverInfo), err + } + + out0 := *abi.ConvertType(out[0], new(AbstractUniversalResolverResolverInfo)).(*AbstractUniversalResolverResolverInfo) + + return out0, err + +} + +// RequireResolver is a free data retrieval call binding the contract method 0xc285238a. +// +// Solidity: function requireResolver(bytes name) view returns((bytes,uint256,bytes32,address,bool) info) +func (_Contract *ContractSession) RequireResolver(name []byte) (AbstractUniversalResolverResolverInfo, error) { + return _Contract.Contract.RequireResolver(&_Contract.CallOpts, name) +} + +// RequireResolver is a free data retrieval call binding the contract method 0xc285238a. +// +// Solidity: function requireResolver(bytes name) view returns((bytes,uint256,bytes32,address,bool) info) +func (_Contract *ContractCallerSession) RequireResolver(name []byte) (AbstractUniversalResolverResolverInfo, error) { + return _Contract.Contract.RequireResolver(&_Contract.CallOpts, name) +} + +// Resolve is a free data retrieval call binding the contract method 0x9061b923. +// +// Solidity: function resolve(bytes name, bytes data) view returns(bytes, address) +func (_Contract *ContractCaller) Resolve(opts *bind.CallOpts, name []byte, data []byte) ([]byte, common.Address, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "resolve", name, data) + + if err != nil { + return *new([]byte), *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new([]byte)).(*[]byte) + out1 := *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + + return out0, out1, err + +} + +// Resolve is a free data retrieval call binding the contract method 0x9061b923. +// +// Solidity: function resolve(bytes name, bytes data) view returns(bytes, address) +func (_Contract *ContractSession) Resolve(name []byte, data []byte) ([]byte, common.Address, error) { + return _Contract.Contract.Resolve(&_Contract.CallOpts, name, data) +} + +// Resolve is a free data retrieval call binding the contract method 0x9061b923. +// +// Solidity: function resolve(bytes name, bytes data) view returns(bytes, address) +func (_Contract *ContractCallerSession) Resolve(name []byte, data []byte) ([]byte, common.Address, error) { + return _Contract.Contract.Resolve(&_Contract.CallOpts, name, data) +} + +// ResolveBatchCallback is a free data retrieval call binding the contract method 0x491fc4f9. +// +// Solidity: function resolveBatchCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractCaller) ResolveBatchCallback(opts *bind.CallOpts, response []byte, extraData []byte) error { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "resolveBatchCallback", response, extraData) + + if err != nil { + return err + } + + return err + +} + +// ResolveBatchCallback is a free data retrieval call binding the contract method 0x491fc4f9. +// +// Solidity: function resolveBatchCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractSession) ResolveBatchCallback(response []byte, extraData []byte) error { + return _Contract.Contract.ResolveBatchCallback(&_Contract.CallOpts, response, extraData) +} + +// ResolveBatchCallback is a free data retrieval call binding the contract method 0x491fc4f9. +// +// Solidity: function resolveBatchCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractCallerSession) ResolveBatchCallback(response []byte, extraData []byte) error { + return _Contract.Contract.ResolveBatchCallback(&_Contract.CallOpts, response, extraData) +} + +// ResolveCallback is a free data retrieval call binding the contract method 0xb4a85801. +// +// Solidity: function resolveCallback(bytes response, bytes extraData) pure returns(bytes, address) +func (_Contract *ContractCaller) ResolveCallback(opts *bind.CallOpts, response []byte, extraData []byte) ([]byte, common.Address, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "resolveCallback", response, extraData) + + if err != nil { + return *new([]byte), *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new([]byte)).(*[]byte) + out1 := *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + + return out0, out1, err + +} + +// ResolveCallback is a free data retrieval call binding the contract method 0xb4a85801. +// +// Solidity: function resolveCallback(bytes response, bytes extraData) pure returns(bytes, address) +func (_Contract *ContractSession) ResolveCallback(response []byte, extraData []byte) ([]byte, common.Address, error) { + return _Contract.Contract.ResolveCallback(&_Contract.CallOpts, response, extraData) +} + +// ResolveCallback is a free data retrieval call binding the contract method 0xb4a85801. +// +// Solidity: function resolveCallback(bytes response, bytes extraData) pure returns(bytes, address) +func (_Contract *ContractCallerSession) ResolveCallback(response []byte, extraData []byte) ([]byte, common.Address, error) { + return _Contract.Contract.ResolveCallback(&_Contract.CallOpts, response, extraData) +} + +// ResolveDirectCallback is a free data retrieval call binding the contract method 0x55391bb8. +// +// Solidity: function resolveDirectCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractCaller) ResolveDirectCallback(opts *bind.CallOpts, response []byte, extraData []byte) error { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "resolveDirectCallback", response, extraData) + + if err != nil { + return err + } + + return err + +} + +// ResolveDirectCallback is a free data retrieval call binding the contract method 0x55391bb8. +// +// Solidity: function resolveDirectCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractSession) ResolveDirectCallback(response []byte, extraData []byte) error { + return _Contract.Contract.ResolveDirectCallback(&_Contract.CallOpts, response, extraData) +} + +// ResolveDirectCallback is a free data retrieval call binding the contract method 0x55391bb8. +// +// Solidity: function resolveDirectCallback(bytes response, bytes extraData) view returns() +func (_Contract *ContractCallerSession) ResolveDirectCallback(response []byte, extraData []byte) error { + return _Contract.Contract.ResolveDirectCallback(&_Contract.CallOpts, response, extraData) +} + +// ResolveDirectCallbackError is a free data retrieval call binding the contract method 0x3c6cbda8. +// +// Solidity: function resolveDirectCallbackError(bytes response, bytes ) pure returns() +func (_Contract *ContractCaller) ResolveDirectCallbackError(opts *bind.CallOpts, response []byte, arg1 []byte) error { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "resolveDirectCallbackError", response, arg1) + + if err != nil { + return err + } + + return err + +} + +// ResolveDirectCallbackError is a free data retrieval call binding the contract method 0x3c6cbda8. +// +// Solidity: function resolveDirectCallbackError(bytes response, bytes ) pure returns() +func (_Contract *ContractSession) ResolveDirectCallbackError(response []byte, arg1 []byte) error { + return _Contract.Contract.ResolveDirectCallbackError(&_Contract.CallOpts, response, arg1) +} + +// ResolveDirectCallbackError is a free data retrieval call binding the contract method 0x3c6cbda8. +// +// Solidity: function resolveDirectCallbackError(bytes response, bytes ) pure returns() +func (_Contract *ContractCallerSession) ResolveDirectCallbackError(response []byte, arg1 []byte) error { + return _Contract.Contract.ResolveDirectCallbackError(&_Contract.CallOpts, response, arg1) +} + +// ResolveWithGateways is a free data retrieval call binding the contract method 0xa1472844. +// +// Solidity: function resolveWithGateways(bytes name, bytes data, string[] gateways) view returns(bytes result, address resolver) +func (_Contract *ContractCaller) ResolveWithGateways(opts *bind.CallOpts, name []byte, data []byte, gateways []string) (struct { + Result []byte + Resolver common.Address +}, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "resolveWithGateways", name, data, gateways) + + outstruct := new(struct { + Result []byte + Resolver common.Address + }) + if err != nil { + return *outstruct, err + } + + outstruct.Result = *abi.ConvertType(out[0], new([]byte)).(*[]byte) + outstruct.Resolver = *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + + return *outstruct, err + +} + +// ResolveWithGateways is a free data retrieval call binding the contract method 0xa1472844. +// +// Solidity: function resolveWithGateways(bytes name, bytes data, string[] gateways) view returns(bytes result, address resolver) +func (_Contract *ContractSession) ResolveWithGateways(name []byte, data []byte, gateways []string) (struct { + Result []byte + Resolver common.Address +}, error) { + return _Contract.Contract.ResolveWithGateways(&_Contract.CallOpts, name, data, gateways) +} + +// ResolveWithGateways is a free data retrieval call binding the contract method 0xa1472844. +// +// Solidity: function resolveWithGateways(bytes name, bytes data, string[] gateways) view returns(bytes result, address resolver) +func (_Contract *ContractCallerSession) ResolveWithGateways(name []byte, data []byte, gateways []string) (struct { + Result []byte + Resolver common.Address +}, error) { + return _Contract.Contract.ResolveWithGateways(&_Contract.CallOpts, name, data, gateways) +} + +// ResolveWithResolver is a free data retrieval call binding the contract method 0x4a3e3994. +// +// Solidity: function resolveWithResolver(address resolver, bytes name, bytes data, string[] gateways) view returns(bytes) +func (_Contract *ContractCaller) ResolveWithResolver(opts *bind.CallOpts, resolver common.Address, name []byte, data []byte, gateways []string) ([]byte, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "resolveWithResolver", resolver, name, data, gateways) + + if err != nil { + return *new([]byte), err + } + + out0 := *abi.ConvertType(out[0], new([]byte)).(*[]byte) + + return out0, err + +} + +// ResolveWithResolver is a free data retrieval call binding the contract method 0x4a3e3994. +// +// Solidity: function resolveWithResolver(address resolver, bytes name, bytes data, string[] gateways) view returns(bytes) +func (_Contract *ContractSession) ResolveWithResolver(resolver common.Address, name []byte, data []byte, gateways []string) ([]byte, error) { + return _Contract.Contract.ResolveWithResolver(&_Contract.CallOpts, resolver, name, data, gateways) +} + +// ResolveWithResolver is a free data retrieval call binding the contract method 0x4a3e3994. +// +// Solidity: function resolveWithResolver(address resolver, bytes name, bytes data, string[] gateways) view returns(bytes) +func (_Contract *ContractCallerSession) ResolveWithResolver(resolver common.Address, name []byte, data []byte, gateways []string) ([]byte, error) { + return _Contract.Contract.ResolveWithResolver(&_Contract.CallOpts, resolver, name, data, gateways) +} + +// Reverse is a free data retrieval call binding the contract method 0x5d78a217. +// +// Solidity: function reverse(bytes lookupAddress, uint256 coinType) view returns(string, address, address) +func (_Contract *ContractCaller) Reverse(opts *bind.CallOpts, lookupAddress []byte, coinType *big.Int) (string, common.Address, common.Address, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "reverse", lookupAddress, coinType) + + if err != nil { + return *new(string), *new(common.Address), *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + out1 := *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + out2 := *abi.ConvertType(out[2], new(common.Address)).(*common.Address) + + return out0, out1, out2, err + +} + +// Reverse is a free data retrieval call binding the contract method 0x5d78a217. +// +// Solidity: function reverse(bytes lookupAddress, uint256 coinType) view returns(string, address, address) +func (_Contract *ContractSession) Reverse(lookupAddress []byte, coinType *big.Int) (string, common.Address, common.Address, error) { + return _Contract.Contract.Reverse(&_Contract.CallOpts, lookupAddress, coinType) +} + +// Reverse is a free data retrieval call binding the contract method 0x5d78a217. +// +// Solidity: function reverse(bytes lookupAddress, uint256 coinType) view returns(string, address, address) +func (_Contract *ContractCallerSession) Reverse(lookupAddress []byte, coinType *big.Int) (string, common.Address, common.Address, error) { + return _Contract.Contract.Reverse(&_Contract.CallOpts, lookupAddress, coinType) +} + +// ReverseAddressCallback is a free data retrieval call binding the contract method 0x94fbfa87. +// +// Solidity: function reverseAddressCallback(bytes response, bytes extraData) pure returns(string primary, address resolver, address reverseResolver) +func (_Contract *ContractCaller) ReverseAddressCallback(opts *bind.CallOpts, response []byte, extraData []byte) (struct { + Primary string + Resolver common.Address + ReverseResolver common.Address +}, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "reverseAddressCallback", response, extraData) + + outstruct := new(struct { + Primary string + Resolver common.Address + ReverseResolver common.Address + }) + if err != nil { + return *outstruct, err + } + + outstruct.Primary = *abi.ConvertType(out[0], new(string)).(*string) + outstruct.Resolver = *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + outstruct.ReverseResolver = *abi.ConvertType(out[2], new(common.Address)).(*common.Address) + + return *outstruct, err + +} + +// ReverseAddressCallback is a free data retrieval call binding the contract method 0x94fbfa87. +// +// Solidity: function reverseAddressCallback(bytes response, bytes extraData) pure returns(string primary, address resolver, address reverseResolver) +func (_Contract *ContractSession) ReverseAddressCallback(response []byte, extraData []byte) (struct { + Primary string + Resolver common.Address + ReverseResolver common.Address +}, error) { + return _Contract.Contract.ReverseAddressCallback(&_Contract.CallOpts, response, extraData) +} + +// ReverseAddressCallback is a free data retrieval call binding the contract method 0x94fbfa87. +// +// Solidity: function reverseAddressCallback(bytes response, bytes extraData) pure returns(string primary, address resolver, address reverseResolver) +func (_Contract *ContractCallerSession) ReverseAddressCallback(response []byte, extraData []byte) (struct { + Primary string + Resolver common.Address + ReverseResolver common.Address +}, error) { + return _Contract.Contract.ReverseAddressCallback(&_Contract.CallOpts, response, extraData) +} + +// ReverseNameCallback is a free data retrieval call binding the contract method 0x575de750. +// +// Solidity: function reverseNameCallback(bytes response, bytes extraData) view returns(string primary, address, address) +func (_Contract *ContractCaller) ReverseNameCallback(opts *bind.CallOpts, response []byte, extraData []byte) (string, common.Address, common.Address, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "reverseNameCallback", response, extraData) + + if err != nil { + return *new(string), *new(common.Address), *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + out1 := *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + out2 := *abi.ConvertType(out[2], new(common.Address)).(*common.Address) + + return out0, out1, out2, err + +} + +// ReverseNameCallback is a free data retrieval call binding the contract method 0x575de750. +// +// Solidity: function reverseNameCallback(bytes response, bytes extraData) view returns(string primary, address, address) +func (_Contract *ContractSession) ReverseNameCallback(response []byte, extraData []byte) (string, common.Address, common.Address, error) { + return _Contract.Contract.ReverseNameCallback(&_Contract.CallOpts, response, extraData) +} + +// ReverseNameCallback is a free data retrieval call binding the contract method 0x575de750. +// +// Solidity: function reverseNameCallback(bytes response, bytes extraData) view returns(string primary, address, address) +func (_Contract *ContractCallerSession) ReverseNameCallback(response []byte, extraData []byte) (string, common.Address, common.Address, error) { + return _Contract.Contract.ReverseNameCallback(&_Contract.CallOpts, response, extraData) +} + +// ReverseWithGateways is a free data retrieval call binding the contract method 0xb7d6ca64. +// +// Solidity: function reverseWithGateways(bytes lookupAddress, uint256 coinType, string[] gateways) view returns(string primary, address resolver, address reverseResolver) +func (_Contract *ContractCaller) ReverseWithGateways(opts *bind.CallOpts, lookupAddress []byte, coinType *big.Int, gateways []string) (struct { + Primary string + Resolver common.Address + ReverseResolver common.Address +}, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "reverseWithGateways", lookupAddress, coinType, gateways) + + outstruct := new(struct { + Primary string + Resolver common.Address + ReverseResolver common.Address + }) + if err != nil { + return *outstruct, err + } + + outstruct.Primary = *abi.ConvertType(out[0], new(string)).(*string) + outstruct.Resolver = *abi.ConvertType(out[1], new(common.Address)).(*common.Address) + outstruct.ReverseResolver = *abi.ConvertType(out[2], new(common.Address)).(*common.Address) + + return *outstruct, err + +} + +// ReverseWithGateways is a free data retrieval call binding the contract method 0xb7d6ca64. +// +// Solidity: function reverseWithGateways(bytes lookupAddress, uint256 coinType, string[] gateways) view returns(string primary, address resolver, address reverseResolver) +func (_Contract *ContractSession) ReverseWithGateways(lookupAddress []byte, coinType *big.Int, gateways []string) (struct { + Primary string + Resolver common.Address + ReverseResolver common.Address +}, error) { + return _Contract.Contract.ReverseWithGateways(&_Contract.CallOpts, lookupAddress, coinType, gateways) +} + +// ReverseWithGateways is a free data retrieval call binding the contract method 0xb7d6ca64. +// +// Solidity: function reverseWithGateways(bytes lookupAddress, uint256 coinType, string[] gateways) view returns(string primary, address resolver, address reverseResolver) +func (_Contract *ContractCallerSession) ReverseWithGateways(lookupAddress []byte, coinType *big.Int, gateways []string) (struct { + Primary string + Resolver common.Address + ReverseResolver common.Address +}, error) { + return _Contract.Contract.ReverseWithGateways(&_Contract.CallOpts, lookupAddress, coinType, gateways) +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_Contract *ContractCaller) SupportsInterface(opts *bind.CallOpts, interfaceId [4]byte) (bool, error) { + var out []interface{} + err := _Contract.contract.Call(opts, &out, "supportsInterface", interfaceId) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_Contract *ContractSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _Contract.Contract.SupportsInterface(&_Contract.CallOpts, interfaceId) +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_Contract *ContractCallerSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _Contract.Contract.SupportsInterface(&_Contract.CallOpts, interfaceId) +} diff --git a/contracts/universalresolver/generate.go b/contracts/universalresolver/generate.go new file mode 100644 index 0000000..f1e9c51 --- /dev/null +++ b/contracts/universalresolver/generate.go @@ -0,0 +1,3 @@ +package universalresolver + +//go:generate abigen -abi contract.abi -out contract.go -pkg universalresolver -type Contract diff --git a/dnsencode.go b/dnsencode.go new file mode 100644 index 0000000..bef4d36 --- /dev/null +++ b/dnsencode.go @@ -0,0 +1,50 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens + +import ( + "errors" + "fmt" + "strings" +) + +// DNSEncode converts an ENS name into the DNS wire format used by +// EIP-137 / the Universal Resolver: each label is preceded by a +// single byte holding its length, and the encoding is terminated +// with a zero byte representing the root label. The empty string +// encodes to a single zero byte. +// +// A label may be at most 255 bytes after IDNA normalisation. +func DNSEncode(name string) ([]byte, error) { + if name == "" { + return []byte{0}, nil + } + normalised, err := Normalize(name) + if err != nil { + return nil, err + } + // Trailing dots produce empty labels which are not meaningful in ENS; + // trim a single trailing dot to be lenient with input. + normalised = strings.TrimSuffix(normalised, ".") + parts := strings.Split(normalised, ".") + out := make([]byte, 0, len(normalised)+len(parts)+1) + for i, label := range parts { + if label == "" { + return nil, fmt.Errorf("empty label at position %d in %q", i, name) + } + labelBytes := []byte(label) + if len(labelBytes) > 255 { + return nil, errors.New("label exceeds 255 bytes") + } + out = append(out, byte(len(labelBytes))) + out = append(out, labelBytes...) + } + out = append(out, 0) + return out, nil +} diff --git a/resolver.go b/resolver.go index d6b03c3..3ea9d56 100644 --- a/resolver.go +++ b/resolver.go @@ -17,6 +17,7 @@ package ens import ( "bytes" "compress/zlib" + "context" "errors" "io" "math/big" @@ -180,8 +181,14 @@ func (r *Resolver) InterfaceImplementer(interfaceID [4]byte) (common.Address, er return r.Contract.InterfaceImplementer(nil, nameHash, interfaceID) } -// Resolve resolves an ENS name in to an Etheruem address. -// This will return an error if the name is not found or otherwise 0. +// Resolve resolves an ENS name in to an Ethereum address. Resolution flows +// through the ENS UniversalResolver, which means CCIP-Read (ERC-3668) is +// followed transparently — names backed by offchain or L2 data resolve the +// same way as fully on-chain names. An input that contains no dot is treated +// as a literal hex address. +// +// This will return an error if the name is not found or otherwise resolves +// to the zero address. func Resolve(backend bind.ContractBackend, input string) (common.Address, error) { if strings.Contains(input, ".") { return resolveName(backend, input) @@ -205,30 +212,11 @@ func resolveName(backend bind.ContractBackend, input string) (common.Address, er if bytes.Equal(nameHash[:], zeroHash) { return UnknownAddress, errors.New("bad name") } - address, err := resolveHash(backend, input) - if err != nil { - return UnknownAddress, err - } - - return address, nil -} - -func resolveHash(backend bind.ContractBackend, domain string) (common.Address, error) { - resolver, err := NewResolver(backend, domain) + ur, err := NewUniversalResolver(backend) if err != nil { return UnknownAddress, err } - - // Resolve the domain. - address, err := resolver.Address() - if err != nil { - return UnknownAddress, err - } - if bytes.Equal(address.Bytes(), UnknownAddress.Bytes()) { - return UnknownAddress, errors.New("no address") - } - - return address, nil + return ur.ResolveAddress(context.Background(), input) } // SetText sets the text associated with a name. diff --git a/resolver_test.go b/resolver_test.go index 3da9009..99fe695 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -54,6 +54,12 @@ func TestResolveBadResolver(t *testing.T) { assert.Equal(t, "no address", err.Error(), "Unexpected error") } +// resolvestozero.eth previously routed through a resolver that returned the +// zero address; if the underlying record changes this assertion will too. +// +// ethereum.eth used to return a non-zero address but its records have since +// been cleared, so the assertion below uses vitalik.eth as a stable target. + func TestResolveTestEnsTest(t *testing.T) { expected := "ed96dd3be847b387217ef9de5b20d8392a6cdf40" actual, err := Resolve(client, "test.enstest.eth") @@ -68,9 +74,9 @@ func TestResolveResolverEth(t *testing.T) { assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") } -func TestResolveEthereum(t *testing.T) { - expected := "de0b295669a9fd93d5f28d9ec85e40f4cb697bae" - actual, err := Resolve(client, "ethereum.eth") +func TestResolveVitalik(t *testing.T) { + expected := "d8da6bf26964af9d7eed9e03e53415d37aa96045" + actual, err := Resolve(client, "vitalik.eth") require.Nil(t, err, "Error resolving name") assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") } diff --git a/reverseresolver.go b/reverseresolver.go index 791863e..78ead1a 100644 --- a/reverseresolver.go +++ b/reverseresolver.go @@ -15,6 +15,7 @@ package ens import ( + "context" "errors" "fmt" @@ -103,22 +104,23 @@ func Format(backend bind.ContractBackend, address common.Address) string { return result } -// ReverseResolve resolves an address in to an ENS name. -// This will return an error if the name is not found or otherwise 0. +// ReverseResolve resolves an address in to an ENS name. Resolution flows +// through the ENS UniversalResolver, so CCIP-Read (ERC-3668) is handled +// transparently and reverse records stored offchain or on an L2 resolve the +// same way as fully on-chain reverse records. +// +// This will return an error if no primary name is set for the address. func ReverseResolve(backend bind.ContractBackend, address common.Address) (string, error) { - resolver, err := NewReverseResolverFor(backend, address) + ur, err := NewUniversalResolver(backend) if err != nil { return "", err } - - // Resolve the name. - name, err := resolver.Name(address) + name, _, _, err := ur.Reverse(context.Background(), address.Bytes(), CoinTypeETH) if err != nil { return "", err } if name == "" { - err = errors.New("no resolution") + return "", errors.New("no resolution") } - - return name, err + return name, nil } diff --git a/reverseresolver_test.go b/reverseresolver_test.go index b61609f..5ed2d75 100644 --- a/reverseresolver_test.go +++ b/reverseresolver_test.go @@ -34,7 +34,9 @@ func TestReverseResolve(t *testing.T) { { name: "NoResolver", address: common.Address{}, - err: "not a resolver", + // UniversalResolver returns an empty primary name for the zero + // address; ReverseResolve translates that to "no resolution". + err: "no resolution", }, { name: "NoReverseRecord", diff --git a/tokenid_test.go b/tokenid_test.go index eb08d3d..81ca139 100644 --- a/tokenid_test.go +++ b/tokenid_test.go @@ -22,7 +22,7 @@ func TestDeriveTokenId(t *testing.T) { { name: "Invalid ENS domain", expected: "", - input: "foo.bar", + input: "sirnotappearinginthisregistry.eth", err: "unregistered name", }, { diff --git a/universalresolver.go b/universalresolver.go new file mode 100644 index 0000000..1e23488 --- /dev/null +++ b/universalresolver.go @@ -0,0 +1,149 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/wealdtech/go-ens/v3/ccipread" + "github.com/wealdtech/go-ens/v3/contracts/universalresolver" +) + +// UniversalResolverContractAddress is the proxy address of the ENS +// UniversalResolver. The same address is used on Ethereum mainnet and Sepolia +// (the proxy is deployed at the vanity address by the ENS DAO). +const UniversalResolverContractAddress = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe" + +// CoinTypeETH is SLIP-0044 coin type 60: native Ethereum addresses. +const CoinTypeETH = uint64(60) + +// addrSelector is keccak256("addr(bytes32)")[:4]: the legacy ETH-address +// resolver profile. UniversalResolver.resolve forwards calldata starting with +// this selector to the discovered resolver and returns the abi-encoded result. +var addrSelector = [4]byte{0x3b, 0x3b, 0x57, 0xde} + +// UniversalResolver wraps the ENS Universal Resolver and exposes resolution +// helpers that transparently follow ERC-3668 OffchainLookup reverts via the +// ccipread package. +type UniversalResolver struct { + backend bind.ContractBackend + addr common.Address + abi abi.ABI +} + +// NewUniversalResolver returns a UniversalResolver bound to the canonical +// proxy address on whichever chain the backend is connected to. +func NewUniversalResolver(backend bind.ContractBackend) (*UniversalResolver, error) { + return NewUniversalResolverAt(backend, common.HexToAddress(UniversalResolverContractAddress)) +} + +// NewUniversalResolverAt returns a UniversalResolver bound to a specific +// address. Use this when targeting a non-standard deployment, e.g. a local +// fork. +func NewUniversalResolverAt(backend bind.ContractBackend, addr common.Address) (*UniversalResolver, error) { + parsed, err := universalresolver.ContractMetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("parse universal resolver abi: %w", err) + } + return &UniversalResolver{backend: backend, addr: addr, abi: *parsed}, nil +} + +// Address returns the on-chain address of the bound UniversalResolver. +func (u *UniversalResolver) Address() common.Address { + return u.addr +} + +// Resolve performs UniversalResolver.resolve(name, callData) using the chain +// connected via the backend, transparently following any OffchainLookup +// reverts. The returned bytes are the abi-encoded return value of the +// downstream resolver call (e.g. for `addr(bytes32)` the bytes decode as +// `address`); resolverAddr is the resolver contract that produced it. +func (u *UniversalResolver) Resolve(ctx context.Context, name string, callData []byte) ([]byte, common.Address, error) { + dnsName, err := DNSEncode(name) + if err != nil { + return nil, common.Address{}, fmt.Errorf("dns-encode %q: %w", name, err) + } + input, err := u.abi.Pack("resolve", dnsName, callData) + if err != nil { + return nil, common.Address{}, fmt.Errorf("pack resolve: %w", err) + } + out, err := ccipread.Call(ctx, u.backend, u.addr, input, nil) + if err != nil { + return nil, common.Address{}, translateURRevert(err) + } + values, err := u.abi.Unpack("resolve", out) + if err != nil { + return nil, common.Address{}, fmt.Errorf("unpack resolve: %w", err) + } + if len(values) != 2 { + return nil, common.Address{}, errors.New("universal resolver: unexpected resolve output arity") + } + result, _ := values[0].([]byte) + resolver, _ := values[1].(common.Address) + return result, resolver, nil +} + +// ResolveAddress resolves an ENS name to its primary Ethereum address by +// invoking the legacy `addr(bytes32)` resolver profile through the +// UniversalResolver. +func (u *UniversalResolver) ResolveAddress(ctx context.Context, name string) (common.Address, error) { + nameHash, err := NameHash(name) + if err != nil { + return UnknownAddress, err + } + callData := make([]byte, 4+32) + copy(callData[:4], addrSelector[:]) + copy(callData[4:], nameHash[:]) + result, _, err := u.Resolve(ctx, name, callData) + if err != nil { + return UnknownAddress, err + } + if len(result) == 0 { + return UnknownAddress, ErrNoAddress + } + if len(result) != 32 { + return UnknownAddress, fmt.Errorf("addr result has unexpected length %d", len(result)) + } + addr := common.BytesToAddress(result) + if addr == UnknownAddress { + return UnknownAddress, ErrNoAddress + } + return addr, nil +} + +// Reverse performs UniversalResolver.reverse(lookupAddress, coinType) and +// returns the primary name, the resolver address, and the reverse resolver +// address. CCIP-Read is followed transparently. +func (u *UniversalResolver) Reverse(ctx context.Context, address []byte, coinType uint64) (string, common.Address, common.Address, error) { + input, err := u.abi.Pack("reverse", address, new(big.Int).SetUint64(coinType)) + if err != nil { + return "", common.Address{}, common.Address{}, fmt.Errorf("pack reverse: %w", err) + } + out, err := ccipread.Call(ctx, u.backend, u.addr, input, nil) + if err != nil { + return "", common.Address{}, common.Address{}, translateURRevert(err) + } + values, err := u.abi.Unpack("reverse", out) + if err != nil { + return "", common.Address{}, common.Address{}, fmt.Errorf("unpack reverse: %w", err) + } + if len(values) != 3 { + return "", common.Address{}, common.Address{}, errors.New("universal resolver: unexpected reverse output arity") + } + name, _ := values[0].(string) + resolver, _ := values[1].(common.Address) + reverseResolver, _ := values[2].(common.Address) + return name, resolver, reverseResolver, nil +} diff --git a/universalresolver_test.go b/universalresolver_test.go new file mode 100644 index 0000000..0108d03 --- /dev/null +++ b/universalresolver_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens_test + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" + ens "github.com/wealdtech/go-ens/v3" +) + +// mainnetClient connects to a public Ethereum mainnet RPC. The endpoint can +// be overridden with GO_ENS_TEST_RPC; without an override the test falls +// back to a public endpoint so that `go test` works out of the box. +func mainnetClient(t *testing.T) *ethclient.Client { + t.Helper() + url := os.Getenv("GO_ENS_TEST_RPC") + if url == "" { + // Public mainnet RPC. Surfaces revert data via the JSON-RPC `data` + // field, which is required for CCIP-Read. + url = "https://ethereum.publicnode.com" + } + client, err := ethclient.Dial(url) + require.NoError(t, err, "failed to dial %s", url) + return client +} + +// TestUniversalResolverIntegrationName is the canonical ENSv2 readiness +// check from https://docs.ens.domains/web/ensv2-readiness/. A library that +// resolves through the UniversalResolver returns the v2 sentinel address; +// a library that still uses legacy registry-walking returns the v1 sentinel. +func TestUniversalResolverIntegrationName(t *testing.T) { + const ( + name = "ur.integration-tests.eth" + expectedV2 = "0x2222222222222222222222222222222222222222" + legacyV1Marker = "0x1111111111111111111111111111111111111111" + ) + client := mainnetClient(t) + + addr, err := ens.Resolve(client, name) + require.NoError(t, err, "Resolve(%s) failed", name) + + got := strings.ToLower(addr.Hex()) + require.NotEqual(t, strings.ToLower(legacyV1Marker), got, + "got the legacy v1 sentinel %s — resolution did not route through the UniversalResolver", legacyV1Marker) + require.Equal(t, strings.ToLower(expectedV2), got, + "expected ENSv2 sentinel %s, got %s", expectedV2, got) +} + +// TestUniversalResolverCCIPRead exercises the OffchainLookup path: this +// name resolves only after the client follows an ERC-3668 revert and queries +// a CCIP-Read gateway. +func TestUniversalResolverCCIPRead(t *testing.T) { + const ( + name = "test.offchaindemo.eth" + expected = "0x779981590E7Ccc0CFAe8040Ce7151324747cDb97" + ) + client := mainnetClient(t) + + addr, err := ens.Resolve(client, name) + require.NoError(t, err, "Resolve(%s) failed", name) + require.Equal(t, strings.ToLower(expected), strings.ToLower(addr.Hex())) +} + +// TestUniversalResolverDirectResolve checks the lower-level helper used by +// Resolve, asserting that calling UniversalResolver.ResolveAddress directly +// produces the same result as the top-level Resolve entrypoint. +func TestUniversalResolverDirectResolve(t *testing.T) { + client := mainnetClient(t) + ur, err := ens.NewUniversalResolver(client) + require.NoError(t, err) + + addr, err := ur.ResolveAddress(context.Background(), "ur.integration-tests.eth") + require.NoError(t, err) + require.Equal(t, common.HexToAddress("0x2222222222222222222222222222222222222222"), addr) +} diff --git a/urerrors.go b/urerrors.go new file mode 100644 index 0000000..b3fa3a8 --- /dev/null +++ b/urerrors.go @@ -0,0 +1,251 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens + +import ( + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// ErrUnregistered is returned when a name has no resolver in the registry. +// Callers may compare against it with errors.Is. +var ErrUnregistered = errors.New("unregistered name") + +// ErrNoAddress is returned when a resolver is found but the address record +// is missing or zero. +var ErrNoAddress = errors.New("no address") + +// ResolverNotFoundError corresponds to the UniversalResolver custom error +// `ResolverNotFound(bytes)`. It is reported when no resolver is set anywhere +// up the parent chain of the requested name. +type ResolverNotFoundError struct { + // DNSName is the DNS-encoded name as passed to resolve(). + DNSName []byte +} + +func (e *ResolverNotFoundError) Error() string { return "unregistered name" } +func (e *ResolverNotFoundError) Unwrap() error { return ErrUnregistered } + +// ResolverNotContractError corresponds to `ResolverNotContract(bytes,address)`: +// the registry pointed at an address that holds no code. +type ResolverNotContractError struct { + DNSName []byte + Resolver common.Address +} + +func (e *ResolverNotContractError) Error() string { + return fmt.Sprintf("resolver address has no code: %s", e.Resolver.Hex()) +} + +// UnsupportedResolverProfileError corresponds to +// `UnsupportedResolverProfile(bytes4)`: the resolver did not implement the +// requested function selector. +type UnsupportedResolverProfileError struct { + Selector [4]byte +} + +func (e *UnsupportedResolverProfileError) Error() string { + return fmt.Sprintf("resolver does not implement profile 0x%s", hex.EncodeToString(e.Selector[:])) +} + +// ResolverErrorError corresponds to `ResolverError(bytes)`: the downstream +// resolver itself reverted; the inner revert bytes are preserved verbatim. +type ResolverErrorError struct { + Data []byte +} + +func (e *ResolverErrorError) Error() string { + return fmt.Sprintf("resolver reverted: 0x%s", hex.EncodeToString(e.Data)) +} + +// EmptyAddressError corresponds to `EmptyAddress()`: thrown by reverse() when +// the lookup address is empty. +type EmptyAddressError struct{} + +func (e *EmptyAddressError) Error() string { return "no address" } +func (e *EmptyAddressError) Unwrap() error { return ErrNoAddress } + +// HTTPGatewayError corresponds to `HttpError(uint16,string)`: a CCIP-Read +// gateway returned a non-success status whose body the UR forwarded. +type HTTPGatewayError struct { + Status uint16 + Message string +} + +func (e *HTTPGatewayError) Error() string { + return fmt.Sprintf("ccip-read gateway HTTP %d: %s", e.Status, e.Message) +} + +// ReverseAddressMismatchError corresponds to `ReverseAddressMismatch(string,bytes)`: +// the reverse-resolved name's forward record does not point back at the +// queried address. +type ReverseAddressMismatchError struct { + Name string + Address []byte +} + +func (e *ReverseAddressMismatchError) Error() string { + return fmt.Sprintf("reverse name %q does not resolve back to %s", e.Name, "0x"+hex.EncodeToString(e.Address)) +} + +// Custom-error selectors. Computed at init from the canonical signatures. +var ( + selResolverNotFound [4]byte + selResolverNotContract [4]byte + selUnsupportedResolverProfile [4]byte + selResolverError [4]byte + selEmptyAddress [4]byte + selHTTPError [4]byte + selReverseAddressMismatch [4]byte +) + +// Argument lists for decoding error payloads. +var ( + argsResolverNotFound abi.Arguments + argsResolverNotContract abi.Arguments + argsUnsupportedResolverProfile abi.Arguments + argsResolverError abi.Arguments + argsHTTPError abi.Arguments + argsReverseAddressMismatch abi.Arguments +) + +func init() { + bytesT, _ := abi.NewType("bytes", "", nil) + bytes4T, _ := abi.NewType("bytes4", "", nil) + addressT, _ := abi.NewType("address", "", nil) + uint16T, _ := abi.NewType("uint16", "", nil) + stringT, _ := abi.NewType("string", "", nil) + + argsResolverNotFound = abi.Arguments{{Type: bytesT}} + argsResolverNotContract = abi.Arguments{{Type: bytesT}, {Type: addressT}} + argsUnsupportedResolverProfile = abi.Arguments{{Type: bytes4T}} + argsResolverError = abi.Arguments{{Type: bytesT}} + argsHTTPError = abi.Arguments{{Type: uint16T}, {Type: stringT}} + argsReverseAddressMismatch = abi.Arguments{{Type: stringT}, {Type: bytesT}} + + selResolverNotFound = errorSelector("ResolverNotFound(bytes)") + selResolverNotContract = errorSelector("ResolverNotContract(bytes,address)") + selUnsupportedResolverProfile = errorSelector("UnsupportedResolverProfile(bytes4)") + selResolverError = errorSelector("ResolverError(bytes)") + selEmptyAddress = errorSelector("EmptyAddress()") + selHTTPError = errorSelector("HttpError(uint16,string)") + selReverseAddressMismatch = errorSelector("ReverseAddressMismatch(string,bytes)") +} + +func errorSelector(sig string) [4]byte { + h := crypto.Keccak256([]byte(sig)) + var s [4]byte + copy(s[:], h[:4]) + return s +} + +// translateURRevert tries to decode a CallContract error as one of the +// UniversalResolver's typed custom errors. If recognised, it returns a typed +// Go error with a human-friendly message; otherwise it returns err unchanged. +func translateURRevert(err error) error { + if err == nil { + return nil + } + revert, ok := extractRevertData(err) + if !ok || len(revert) < 4 { + return err + } + var sel [4]byte + copy(sel[:], revert[:4]) + body := revert[4:] + + switch sel { + case selResolverNotFound: + out := &ResolverNotFoundError{} + if vals, derr := argsResolverNotFound.Unpack(body); derr == nil && len(vals) == 1 { + if name, ok := vals[0].([]byte); ok { + out.DNSName = name + } + } + return out + case selResolverNotContract: + out := &ResolverNotContractError{} + if vals, derr := argsResolverNotContract.Unpack(body); derr == nil && len(vals) == 2 { + if name, ok := vals[0].([]byte); ok { + out.DNSName = name + } + if a, ok := vals[1].(common.Address); ok { + out.Resolver = a + } + } + return out + case selUnsupportedResolverProfile: + out := &UnsupportedResolverProfileError{} + if vals, derr := argsUnsupportedResolverProfile.Unpack(body); derr == nil && len(vals) == 1 { + if s, ok := vals[0].([4]byte); ok { + out.Selector = s + } + } + return out + case selResolverError: + out := &ResolverErrorError{} + if vals, derr := argsResolverError.Unpack(body); derr == nil && len(vals) == 1 { + if data, ok := vals[0].([]byte); ok { + out.Data = data + } + } + return out + case selEmptyAddress: + return &EmptyAddressError{} + case selHTTPError: + out := &HTTPGatewayError{} + if vals, derr := argsHTTPError.Unpack(body); derr == nil && len(vals) == 2 { + if s, ok := vals[0].(uint16); ok { + out.Status = s + } + if m, ok := vals[1].(string); ok { + out.Message = m + } + } + return out + case selReverseAddressMismatch: + out := &ReverseAddressMismatchError{} + if vals, derr := argsReverseAddressMismatch.Unpack(body); derr == nil && len(vals) == 2 { + if n, ok := vals[0].(string); ok { + out.Name = n + } + if a, ok := vals[1].([]byte); ok { + out.Address = a + } + } + return out + } + return err +} + +// extractRevertData pulls the raw revert payload from an RPC error returned +// by ContractBackend.CallContract. Returns ok=false if the error is not an +// RPC error or carries no data field. +func extractRevertData(err error) ([]byte, bool) { + type dataError interface{ ErrorData() interface{} } + var de dataError + if !errors.As(err, &de) { + return nil, false + } + raw, ok := de.ErrorData().(string) + if !ok { + return nil, false + } + revert, derr := hex.DecodeString(strings.TrimPrefix(raw, "0x")) + if derr != nil { + return nil, false + } + return revert, true +} diff --git a/urerrors_test.go b/urerrors_test.go new file mode 100644 index 0000000..6cbddac --- /dev/null +++ b/urerrors_test.go @@ -0,0 +1,104 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens + +import ( + "encoding/hex" + "errors" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// fakeRPCErr is a stand-in for a go-ethereum RPC error with revert data. +// It implements the rpc.DataError interface that ContractBackend.CallContract +// surfaces on a contract revert. +type fakeRPCErr struct { + msg string + data string +} + +func (e *fakeRPCErr) Error() string { return e.msg } +func (e *fakeRPCErr) ErrorData() interface{} { return e.data } + +// build a revert payload by concatenating selector + abi-encoded body. +func revert(t *testing.T, sel [4]byte, args ...byte) string { + t.Helper() + out := append([]byte{}, sel[:]...) + out = append(out, args...) + return "0x" + hex.EncodeToString(out) +} + +func TestTranslateURRevert_ResolverNotFound(t *testing.T) { + // ResolverNotFound(bytes name) with name = 0x07example03eth00. + body := mustPackArgs(t, argsResolverNotFound, []byte{0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'e', 't', 'h', 0x00}) + rpcErr := &fakeRPCErr{msg: "execution reverted", data: revert(t, selResolverNotFound, body...)} + + got := translateURRevert(rpcErr) + var typed *ResolverNotFoundError + require.True(t, errors.As(got, &typed), "expected ResolverNotFoundError, got %T: %v", got, got) + require.True(t, errors.Is(got, ErrUnregistered)) + require.Equal(t, "unregistered name", got.Error()) + require.NotEmpty(t, typed.DNSName) +} + +func TestTranslateURRevert_EmptyAddress(t *testing.T) { + rpcErr := &fakeRPCErr{msg: "execution reverted", data: revert(t, selEmptyAddress)} + + got := translateURRevert(rpcErr) + var typed *EmptyAddressError + require.True(t, errors.As(got, &typed)) + require.True(t, errors.Is(got, ErrNoAddress)) +} + +func TestTranslateURRevert_UnsupportedProfile(t *testing.T) { + body := mustPackArgs(t, argsUnsupportedResolverProfile, [4]byte{0x3b, 0x3b, 0x57, 0xde}) + rpcErr := &fakeRPCErr{msg: "execution reverted", data: revert(t, selUnsupportedResolverProfile, body...)} + + got := translateURRevert(rpcErr) + var typed *UnsupportedResolverProfileError + require.True(t, errors.As(got, &typed)) + require.Equal(t, [4]byte{0x3b, 0x3b, 0x57, 0xde}, typed.Selector) +} + +func TestTranslateURRevert_ResolverNotContract(t *testing.T) { + dnsName := []byte{0x03, 'e', 't', 'h', 0x00} + addr := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + body := mustPackArgs(t, argsResolverNotContract, dnsName, addr) + rpcErr := &fakeRPCErr{msg: "execution reverted", data: revert(t, selResolverNotContract, body...)} + + got := translateURRevert(rpcErr) + var typed *ResolverNotContractError + require.True(t, errors.As(got, &typed)) + require.Equal(t, addr, typed.Resolver) + require.Equal(t, dnsName, typed.DNSName) +} + +func TestTranslateURRevert_UnknownPassThrough(t *testing.T) { + // A revert that is not a known UR custom error must pass through unchanged. + rpcErr := &fakeRPCErr{msg: "execution reverted", data: "0xdeadbeef"} + got := translateURRevert(rpcErr) + require.Same(t, rpcErr, got, "unknown selectors must not be translated") +} + +func TestTranslateURRevert_NonRPCError(t *testing.T) { + plain := errors.New("boom") + require.Same(t, plain, translateURRevert(plain), "non-RPC errors must pass through") + require.Nil(t, translateURRevert(nil)) +} + +// mustPackArgs is a tiny test helper: pack args with the given Arguments +// definition or fail the test. +func mustPackArgs(t *testing.T, args interface{ Pack(...interface{}) ([]byte, error) }, vals ...interface{}) []byte { + t.Helper() + out, err := args.Pack(vals...) + require.NoError(t, err) + return out +} From e68d7b40cf40b32c428ded40acd4a1a2f02339b7 Mon Sep 17 00:00:00 2001 From: Dhaiwat Pandya Date: Thu, 7 May 2026 10:52:10 +0530 Subject: [PATCH 02/21] test: address review feedback on test fixtures - Drop TestResolveVitalik from resolver_test.go: vitalik.eth's address could change, and the rename was unnecessary churn since broader Resolve coverage already lives in universalresolver_test.go. - Replace sirnotappearinginthisregistry.eth in tokenid_test.go with a longer, descriptive label that's much less likely to be registered; add a comment noting the registration-risk caveat. Co-Authored-By: Claude Opus 4.7 (1M context) --- resolver_test.go | 13 ------------- tokenid_test.go | 5 ++++- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/resolver_test.go b/resolver_test.go index 99fe695..07358ca 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -54,12 +54,6 @@ func TestResolveBadResolver(t *testing.T) { assert.Equal(t, "no address", err.Error(), "Unexpected error") } -// resolvestozero.eth previously routed through a resolver that returned the -// zero address; if the underlying record changes this assertion will too. -// -// ethereum.eth used to return a non-zero address but its records have since -// been cleared, so the assertion below uses vitalik.eth as a stable target. - func TestResolveTestEnsTest(t *testing.T) { expected := "ed96dd3be847b387217ef9de5b20d8392a6cdf40" actual, err := Resolve(client, "test.enstest.eth") @@ -74,13 +68,6 @@ func TestResolveResolverEth(t *testing.T) { assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") } -func TestResolveVitalik(t *testing.T) { - expected := "d8da6bf26964af9d7eed9e03e53415d37aa96045" - actual, err := Resolve(client, "vitalik.eth") - require.Nil(t, err, "Error resolving name") - assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") -} - func TestResolveAddress(t *testing.T) { expected := "b8c2c29ee19d8307cb7255e1cd9cbde883a267d5" actual, err := Resolve(client, "0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5") diff --git a/tokenid_test.go b/tokenid_test.go index 81ca139..202ba92 100644 --- a/tokenid_test.go +++ b/tokenid_test.go @@ -20,9 +20,12 @@ func TestDeriveTokenId(t *testing.T) { input: "vitalik.eth", }, { + // Long, intentionally-unregistered .eth label used as the unregistered-name + // fixture. Any .eth name is in principle registerable, so picking a long + // label keeps the registration cost high and the chance of collision low. name: "Invalid ENS domain", expected: "", - input: "sirnotappearinginthisregistry.eth", + input: "this-name-is-not-registered-go-ens-test-fixture-do-not-register.eth", err: "unregistered name", }, { From 46e674e397251fdea95417dd75582cd8f328d615 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:05:45 +0200 Subject: [PATCH 03/21] fix(dnsencode): enforce RFC 1035 label and total-name limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DNSEncode previously allowed labels up to 255 bytes and had no guard on the total encoded name length, producing wire-format names the Universal Resolver's bytesToDNSName would reject on-chain. Tighten the label cap to 63 bytes (top two bits of the length octet are reserved for compression pointers) and reject encodings above 255 octets before appending the terminating null. Add dnsencode_test.go covering happy paths, normalization, trailing- dot trimming, empty-label rejection, both label-length boundaries (63/64) and total-name-length boundaries (255/256), and non-ASCII IDN labels (münchen, MÜNCHEN case-fold). Co-Authored-By: Claude Opus 4.7 --- dnsencode.go | 13 ++- dnsencode_test.go | 226 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 dnsencode_test.go diff --git a/dnsencode.go b/dnsencode.go index bef4d36..75ebbdc 100644 --- a/dnsencode.go +++ b/dnsencode.go @@ -20,7 +20,10 @@ import ( // with a zero byte representing the root label. The empty string // encodes to a single zero byte. // -// A label may be at most 255 bytes after IDNA normalisation. +// Per RFC 1035, a single label may be at most 63 bytes (the top two +// bits of the length octet are reserved for compression pointers) and +// the full encoded name may be at most 255 octets including length +// bytes and the terminating null. func DNSEncode(name string) ([]byte, error) { if name == "" { return []byte{0}, nil @@ -39,12 +42,16 @@ func DNSEncode(name string) ([]byte, error) { return nil, fmt.Errorf("empty label at position %d in %q", i, name) } labelBytes := []byte(label) - if len(labelBytes) > 255 { - return nil, errors.New("label exceeds 255 bytes") + if len(labelBytes) > 63 { + return nil, errors.New("label exceeds 63 bytes") } out = append(out, byte(len(labelBytes))) out = append(out, labelBytes...) } + // +1 for the terminating null we are about to append. + if len(out)+1 > 255 { + return nil, errors.New("encoded name exceeds 255 bytes") + } out = append(out, 0) return out, nil } diff --git a/dnsencode_test.go b/dnsencode_test.go new file mode 100644 index 0000000..c7e63a6 --- /dev/null +++ b/dnsencode_test.go @@ -0,0 +1,226 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + ens "github.com/wealdtech/go-ens/v3" +) + +// --------------------------------------------------------------------------- +// Happy paths. +// --------------------------------------------------------------------------- + +func TestDNSEncode_Empty(t *testing.T) { + out, err := ens.DNSEncode("") + require.NoError(t, err) + assert.Equal(t, []byte{0}, out) +} + +func TestDNSEncode_SingleLabel(t *testing.T) { + out, err := ens.DNSEncode("eth") + require.NoError(t, err) + assert.Equal(t, []byte{3, 'e', 't', 'h', 0}, out) +} + +func TestDNSEncode_TwoLabels(t *testing.T) { + out, err := ens.DNSEncode("foo.eth") + require.NoError(t, err) + assert.Equal(t, []byte{3, 'f', 'o', 'o', 3, 'e', 't', 'h', 0}, out) +} + +func TestDNSEncode_DeepLabels(t *testing.T) { + out, err := ens.DNSEncode("a.b.c.eth") + require.NoError(t, err) + assert.Equal(t, []byte{1, 'a', 1, 'b', 1, 'c', 3, 'e', 't', 'h', 0}, out) +} + +func TestDNSEncode_SingleCharLabels(t *testing.T) { + out, err := ens.DNSEncode("a.b.c") + require.NoError(t, err) + assert.Equal(t, []byte{1, 'a', 1, 'b', 1, 'c', 0}, out) +} + +func TestDNSEncode_NumericLabel(t *testing.T) { + out, err := ens.DNSEncode("123.eth") + require.NoError(t, err) + assert.Equal(t, []byte{3, '1', '2', '3', 3, 'e', 't', 'h', 0}, out) +} + +func TestDNSEncode_HyphenatedLabel(t *testing.T) { + out, err := ens.DNSEncode("foo-bar.eth") + require.NoError(t, err) + assert.Equal(t, []byte{7, 'f', 'o', 'o', '-', 'b', 'a', 'r', 3, 'e', 't', 'h', 0}, out) +} + +// --------------------------------------------------------------------------- +// Normalization (IDNA MapForLookup should lowercase). +// --------------------------------------------------------------------------- + +func TestDNSEncode_NormalizesToLowercase(t *testing.T) { + out, err := ens.DNSEncode("FOO.ETH") + require.NoError(t, err) + assert.Equal(t, []byte{3, 'f', 'o', 'o', 3, 'e', 't', 'h', 0}, out) +} + +func TestDNSEncode_NormalizesMixedCase(t *testing.T) { + out, err := ens.DNSEncode("Foo.ETH") + require.NoError(t, err) + assert.Equal(t, []byte{3, 'f', 'o', 'o', 3, 'e', 't', 'h', 0}, out) +} + +// --------------------------------------------------------------------------- +// Trailing-dot handling. +// --------------------------------------------------------------------------- + +func TestDNSEncode_SingleTrailingDot(t *testing.T) { + out, err := ens.DNSEncode("foo.eth.") + require.NoError(t, err) + assert.Equal(t, []byte{3, 'f', 'o', 'o', 3, 'e', 't', 'h', 0}, out) +} + +// The doc comment states only ONE trailing dot is trimmed; a second produces +// an empty label and must be rejected. +func TestDNSEncode_DoubleTrailingDot(t *testing.T) { + _, err := ens.DNSEncode("foo.eth..") + require.Error(t, err) + assert.Contains(t, err.Error(), "empty label") +} + +// --------------------------------------------------------------------------- +// Empty-label rejection (leading dot, only-dot, double-dot in middle). +// --------------------------------------------------------------------------- + +func TestDNSEncode_LeadingDot(t *testing.T) { + _, err := ens.DNSEncode(".eth") + require.Error(t, err) + assert.Contains(t, err.Error(), "empty label") +} + +func TestDNSEncode_OnlyDot(t *testing.T) { + _, err := ens.DNSEncode(".") + require.Error(t, err) +} + +func TestDNSEncode_DoubleDotInMiddle(t *testing.T) { + _, err := ens.DNSEncode("foo..eth") + require.Error(t, err) + assert.Contains(t, err.Error(), "empty label") +} + +// --------------------------------------------------------------------------- +// Label-length boundaries. +// +// RFC 1035 §3.1 caps a DNS label at 63 bytes. The top 2 bits of the length +// byte are reserved for compression-pointer flags, so a 64+ byte label cannot +// be encoded validly. +// +// The current implementation uses a 255-byte cap (matching its doc comment). +// The "RFC-correct" tests below intentionally fail against the current code +// to drive the fix. +// --------------------------------------------------------------------------- + +func TestDNSEncode_Label63Bytes(t *testing.T) { + label := strings.Repeat("a", 63) + out, err := ens.DNSEncode(label + ".eth") + require.NoError(t, err) + assert.Equal(t, byte(63), out[0]) + assert.Equal(t, label, string(out[1:64])) +} + +// EXPECTED TO FAIL against current code (bug A2: label cap is 255, not 63). +func TestDNSEncode_Label64Bytes(t *testing.T) { + label := strings.Repeat("a", 64) + _, err := ens.DNSEncode(label + ".eth") + require.Error(t, err, "labels above 63 bytes are invalid per RFC 1035 §3.1") +} + +// EXPECTED TO FAIL against current code (bug A2). 255 is the cap the current +// implementation enforces, so this asserts the RFC-correct behavior. +func TestDNSEncode_Label255Bytes(t *testing.T) { + label := strings.Repeat("a", 255) + _, err := ens.DNSEncode(label + ".eth") + require.Error(t, err, "labels above 63 bytes are invalid per RFC 1035 §3.1") +} + +// 256-byte label is rejected by the current code's `> 255` check. +func TestDNSEncode_Label256Bytes(t *testing.T) { + label := strings.Repeat("a", 256) + _, err := ens.DNSEncode(label + ".eth") + require.Error(t, err) +} + +// --------------------------------------------------------------------------- +// Total-name-length boundaries. +// +// RFC 1035 caps the full encoded name at 255 octets (including the per-label +// length bytes and the terminating null). The current implementation has no +// total-length guard, so the 256-octet test fails (bug A3). +// --------------------------------------------------------------------------- + +// 255 octets total: 63 + 63 + 63 + 61 data bytes; +4 length bytes +1 null. +func TestDNSEncode_TotalLength255(t *testing.T) { + parts := []string{ + strings.Repeat("a", 63), + strings.Repeat("b", 63), + strings.Repeat("c", 63), + strings.Repeat("d", 61), + } + out, err := ens.DNSEncode(strings.Join(parts, ".")) + require.NoError(t, err) + assert.Len(t, out, 255) +} + +// EXPECTED TO FAIL against current code (bug A3: no total-length guard). +func TestDNSEncode_TotalLength256(t *testing.T) { + parts := []string{ + strings.Repeat("a", 63), + strings.Repeat("b", 63), + strings.Repeat("c", 63), + strings.Repeat("d", 62), + } + _, err := ens.DNSEncode(strings.Join(parts, ".")) + require.Error(t, err, "encoded names above 255 octets are invalid per RFC 1035") +} + +// --------------------------------------------------------------------------- +// Non-ASCII / IDN labels. +// +// Normalize() uses idna.ToUnicode + MapForLookup, so the returned form is +// Unicode (NOT Punycode). DNSEncode then takes UTF-8 bytes verbatim. +// "münchen" UTF-8 = m(0x6D) ü(0xC3 0xBC) n(0x6E) c(0x63) h(0x68) e(0x65) n(0x6E) +// = 8 bytes. +// --------------------------------------------------------------------------- + +func TestDNSEncode_NonASCII(t *testing.T) { + out, err := ens.DNSEncode("münchen.eth") + require.NoError(t, err) + expected := []byte{ + 8, 'm', 0xC3, 0xBC, 'n', 'c', 'h', 'e', 'n', + 3, 'e', 't', 'h', + 0, + } + assert.Equal(t, expected, out) +} + +// IDNA MapForLookup should case-fold uppercase non-ASCII to lowercase. +func TestDNSEncode_NonASCIIUppercase(t *testing.T) { + out, err := ens.DNSEncode("MÜNCHEN.eth") + require.NoError(t, err) + expected := []byte{ + 8, 'm', 0xC3, 0xBC, 'n', 'c', 'h', 'e', 'n', + 3, 'e', 't', 'h', + 0, + } + assert.Equal(t, expected, out) +} From e2afb3e96f33acb4f523d3812258e3a5071f7696 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:22:14 +0200 Subject: [PATCH 04/21] fix(dnsencode): drop total-name length guard per ENSIP-10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ENSIP-10 specifies the DNS encoding as RFC 1035 §3.1 "with the exception that there is no limit on the total length of the encoded name". The 255-octet total-name guard added in 46e674e violated that exception and would reject names a spec-compliant Universal Resolver client must accept. Remove the guard, keep the 63-byte per-label cap (which ENSIP-10 incorporates by reference), and update the doc comment to cite ENSIP-10. Adjust TestDNSEncode_TotalLength256 to assert success and add TestDNSEncode_TotalLengthFarAbove255 to exercise names well past the RFC 1035 cap. Co-Authored-By: Claude Opus 4.7 --- dnsencode.go | 12 ++++-------- dnsencode_test.go | 26 ++++++++++++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/dnsencode.go b/dnsencode.go index 75ebbdc..9bcced6 100644 --- a/dnsencode.go +++ b/dnsencode.go @@ -20,10 +20,10 @@ import ( // with a zero byte representing the root label. The empty string // encodes to a single zero byte. // -// Per RFC 1035, a single label may be at most 63 bytes (the top two -// bits of the length octet are reserved for compression pointers) and -// the full encoded name may be at most 255 octets including length -// bytes and the terminating null. +// Per ENSIP-10 the encoding follows RFC 1035 §3.1 — labels are at +// most 63 bytes (the top two bits of the length octet are reserved +// for compression pointers) — except that ENSIP-10 explicitly removes +// RFC 1035's 255-octet limit on the total encoded name length. func DNSEncode(name string) ([]byte, error) { if name == "" { return []byte{0}, nil @@ -48,10 +48,6 @@ func DNSEncode(name string) ([]byte, error) { out = append(out, byte(len(labelBytes))) out = append(out, labelBytes...) } - // +1 for the terminating null we are about to append. - if len(out)+1 > 255 { - return nil, errors.New("encoded name exceeds 255 bytes") - } out = append(out, 0) return out, nil } diff --git a/dnsencode_test.go b/dnsencode_test.go index c7e63a6..e8a5a10 100644 --- a/dnsencode_test.go +++ b/dnsencode_test.go @@ -163,9 +163,9 @@ func TestDNSEncode_Label256Bytes(t *testing.T) { // --------------------------------------------------------------------------- // Total-name-length boundaries. // -// RFC 1035 caps the full encoded name at 255 octets (including the per-label -// length bytes and the terminating null). The current implementation has no -// total-length guard, so the 256-octet test fails (bug A3). +// ENSIP-10 explicitly removes RFC 1035's 255-octet cap on the total encoded +// name, so DNSEncode must accept arbitrarily long encodings as long as each +// label individually conforms. // --------------------------------------------------------------------------- // 255 octets total: 63 + 63 + 63 + 61 data bytes; +4 length bytes +1 null. @@ -181,7 +181,8 @@ func TestDNSEncode_TotalLength255(t *testing.T) { assert.Len(t, out, 255) } -// EXPECTED TO FAIL against current code (bug A3: no total-length guard). +// 256 octets — past the RFC 1035 total-length cap, but ENSIP-10 removes that +// cap, so this must still succeed. func TestDNSEncode_TotalLength256(t *testing.T) { parts := []string{ strings.Repeat("a", 63), @@ -189,8 +190,21 @@ func TestDNSEncode_TotalLength256(t *testing.T) { strings.Repeat("c", 63), strings.Repeat("d", 62), } - _, err := ens.DNSEncode(strings.Join(parts, ".")) - require.Error(t, err, "encoded names above 255 octets are invalid per RFC 1035") + out, err := ens.DNSEncode(strings.Join(parts, ".")) + require.NoError(t, err, "ENSIP-10 explicitly removes the RFC 1035 255-octet cap") + assert.Len(t, out, 256) +} + +// Far beyond RFC 1035's cap — ENSIP-10 still requires this to succeed. +func TestDNSEncode_TotalLengthFarAbove255(t *testing.T) { + // 10 labels of 63 bytes each = 630 data + 10 length + 1 null = 641 octets. + parts := make([]string, 10) + for i := range parts { + parts[i] = strings.Repeat("a", 63) + } + out, err := ens.DNSEncode(strings.Join(parts, ".")) + require.NoError(t, err) + assert.Len(t, out, 641) } // --------------------------------------------------------------------------- From 0a6a08642d1feb97e1013395b5ee0a80c6abf2a2 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:30:34 +0200 Subject: [PATCH 05/21] fix(ccipread): bound hops correctly, cap body, gate JSON on Content-Type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small but spec-relevant hardenings of the CCIP-Read client, plus a first set of unit tests for the package. - Hop counter: `for hop := 0; hop <= max; hop++` ran `max+1` iterations, so MaxRedirects=4 actually allowed five gateway queries before the loop's "exceeded 4 redirects" error fired. Tighten to `hop < max` so the bound matches the error message and viem/ethers. - Response body: replace the unbounded `io.ReadAll(resp.Body)` with `io.ReadAll(io.LimitReader(resp.Body, MaxGatewayBodyBytes))`. A misbehaving gateway streaming an arbitrarily large body can no longer exhaust the caller's heap. 16 MiB is far above any plausible CCIP-Read envelope. - Content-Type: gate the JSON decode on `Content-Type` containing `application/json` (tolerant of `; charset=…` parameters). A gateway returning HTML or text with a 200 status now surfaces as a clean typed error instead of a misleading "invalid JSON". Add ccipread/ccipread_test.go covering all three behaviours (loop bound, oversized body, content-type accept/reject), driven by a hand-rolled OffchainLookup revert helper and an `httptest.Server` gateway. This is the package's first set of unit tests and addresses audit item A12 in part. Co-Authored-By: Claude Opus 4.7 --- ccipread/ccipread.go | 30 +++++- ccipread/ccipread_test.go | 207 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 ccipread/ccipread_test.go diff --git a/ccipread/ccipread.go b/ccipread/ccipread.go index 3ad00a9..8e5694d 100644 --- a/ccipread/ccipread.go +++ b/ccipread/ccipread.go @@ -35,6 +35,13 @@ import ( // Four matches the limit used by ENS-aware libraries (viem, ethers). const MaxRedirects = 4 +// MaxGatewayBodyBytes caps the gateway response body that will be read into +// memory. CCIP-Read responses are small JSON envelopes (a 0x-prefixed hex +// payload), so anything beyond a few hundred kilobytes is either pathological +// or hostile. Cap conservatively at 16 MiB to keep a misbehaving gateway from +// exhausting the caller's heap. +const MaxGatewayBodyBytes = 16 * 1024 * 1024 + // offchainLookupSelector is the first 4 bytes of // keccak256("OffchainLookup(address,string[],bytes,bytes4,bytes)"). var offchainLookupSelector = []byte{0x55, 0x6f, 0x18, 0x30} @@ -108,7 +115,7 @@ func Call(ctx context.Context, backend Caller, to common.Address, data []byte, o target := to callData := data - for hop := 0; hop <= max; hop++ { + for hop := 0; hop < max; hop++ { out, callErr := backend.CallContract(ctx, ethereum.CallMsg{To: &target, Data: callData}, opts.BlockNumber) if callErr == nil { return out, nil @@ -287,13 +294,19 @@ func queryGateway(ctx context.Context, client *http.Client, url, sender, data st return nil, 0, fmt.Errorf("ccipread: gateway %s: %w", url, err) } defer resp.Body.Close() - bodyBytes, err := io.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, MaxGatewayBodyBytes)) if err != nil { return nil, resp.StatusCode, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, resp.StatusCode, fmt.Errorf("ccipread: gateway %s: HTTP %d: %s", url, resp.StatusCode, truncate(string(bodyBytes), 256)) } + // Gate the JSON decode on Content-Type so a misbehaving gateway returning + // HTML or text/plain with a 200 status surfaces as a clean error instead of + // a misleading "invalid JSON". + if ct := resp.Header.Get("Content-Type"); !contentTypeIsJSON(ct) { + return nil, resp.StatusCode, fmt.Errorf("ccipread: gateway %s: unexpected Content-Type %q", url, ct) + } var parsed gatewayResponse if err := json.Unmarshal(bodyBytes, &parsed); err != nil { return nil, resp.StatusCode, fmt.Errorf("ccipread: gateway %s: invalid JSON: %w", url, err) @@ -311,3 +324,16 @@ func truncate(s string, n int) string { } return s[:n] + "..." } + +// contentTypeIsJSON reports whether a Content-Type header indicates JSON, +// allowing for media-type parameters like `application/json; charset=utf-8`. +func contentTypeIsJSON(ct string) bool { + if ct == "" { + return false + } + if i := strings.Index(ct, ";"); i >= 0 { + ct = ct[:i] + } + ct = strings.TrimSpace(strings.ToLower(ct)) + return ct == "application/json" +} diff --git a/ccipread/ccipread_test.go b/ccipread/ccipread_test.go new file mode 100644 index 0000000..af68652 --- /dev/null +++ b/ccipread/ccipread_test.go @@ -0,0 +1,207 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ccipread_test + +import ( + "context" + "encoding/hex" + "math/big" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/wealdtech/go-ens/v3/ccipread" +) + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +// mockRevertErr satisfies the rpc.DataError interface used by +// decodeOffchainLookup: ErrorData() returns the raw revert bytes as a +// 0x-prefixed hex string. +type mockRevertErr struct { + data string +} + +func (e *mockRevertErr) Error() string { return "execution reverted" } +func (e *mockRevertErr) ErrorData() interface{} { return e.data } +func (e *mockRevertErr) Unwrap() error { return nil } + +// loopBackend is a bind.ContractCaller that always returns the same revert +// error. It records how many CallContract calls it received. +type loopBackend struct { + revertHex string + calls atomic.Int32 +} + +func (b *loopBackend) CallContract(ctx context.Context, _ ethereum.CallMsg, _ *big.Int) ([]byte, error) { + b.calls.Add(1) + return nil, &mockRevertErr{data: b.revertHex} +} + +func (b *loopBackend) CodeAt(_ context.Context, _ common.Address, _ *big.Int) ([]byte, error) { + return nil, nil +} + +// buildOffchainLookupRevert hand-encodes a valid OffchainLookup revert payload. +// Mirrors the ABI used by the contract: OffchainLookup(address,string[],bytes,bytes4,bytes). +func buildOffchainLookupRevert(t *testing.T, sender common.Address, urls []string) string { + t.Helper() + addrT, _ := abi.NewType("address", "", nil) + stringArrT, _ := abi.NewType("string[]", "", nil) + bytesT, _ := abi.NewType("bytes", "", nil) + bytes4T, _ := abi.NewType("bytes4", "", nil) + args := abi.Arguments{ + {Type: addrT}, {Type: stringArrT}, {Type: bytesT}, {Type: bytes4T}, {Type: bytesT}, + } + callback := [4]byte{0xaa, 0xbb, 0xcc, 0xdd} + body, err := args.Pack(sender, urls, []byte{0x01, 0x02}, callback, []byte{0x03, 0x04}) + require.NoError(t, err) + revert := append([]byte{0x55, 0x6f, 0x18, 0x30}, body...) // OffchainLookup selector + return "0x" + hex.EncodeToString(revert) +} + +// --------------------------------------------------------------------------- +// A1 — hop-counter bound: MaxRedirects=4 ⇒ 4 gateway calls, not 5. +// --------------------------------------------------------------------------- + +func TestCall_HopCounterStopsAtMaxRedirects(t *testing.T) { + var gatewayHits atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + gatewayHits.Add(1) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":"0x"}`)) + })) + defer srv.Close() + + target := common.HexToAddress("0x1111111111111111111111111111111111111111") + revert := buildOffchainLookupRevert(t, target, []string{srv.URL}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "exceeded") + // Before the fix the loop allowed 5 gateway queries with MaxRedirects=4. + assert.Equal(t, int32(ccipread.MaxRedirects), gatewayHits.Load(), + "gateway should be hit exactly MaxRedirects times before the loop errors out") + assert.Equal(t, int32(ccipread.MaxRedirects), backend.calls.Load(), + "backend should be called exactly MaxRedirects times") +} + +// --------------------------------------------------------------------------- +// #1 — io.LimitReader caps the response body. +// --------------------------------------------------------------------------- + +func TestCall_LimitReaderTruncatesOversizedBody(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + // Pad an otherwise-valid envelope with junk far beyond MaxGatewayBodyBytes. + // LimitReader should cap reading, leaving the JSON unparseable, which we + // surface as a clean error rather than an OOM. + body := make([]byte, ccipread.MaxGatewayBodyBytes+(1<<20)) + for i := range body { + body[i] = 'x' + } + _, _ = w.Write(body) + })) + defer srv.Close() + + target := common.HexToAddress("0x2222222222222222222222222222222222222222") + revert := buildOffchainLookupRevert(t, target, []string{srv.URL}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.Error(t, err) + assert.NotContains(t, err.Error(), "exceeded", "should fail on JSON, not on hop limit") +} + +// --------------------------------------------------------------------------- +// #3 — JSON unmarshal is gated on Content-Type. +// --------------------------------------------------------------------------- + +func TestCall_RejectsNonJSONContentType(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/html") + _, _ = w.Write([]byte(`0x1234`)) + })) + defer srv.Close() + + target := common.HexToAddress("0x3333333333333333333333333333333333333333") + revert := buildOffchainLookupRevert(t, target, []string{srv.URL}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "Content-Type") +} + +func TestCall_AcceptsJSONWithCharsetParameter(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _, _ = w.Write([]byte(`{"data":"0xdeadbeef"}`)) + })) + defer srv.Close() + + target := common.HexToAddress("0x4444444444444444444444444444444444444444") + revert := buildOffchainLookupRevert(t, target, []string{srv.URL}) + // On the SECOND CallContract (after following the lookup), succeed so the + // loop terminates cleanly and we can assert what Call returned. + backend := &succeedAfterOneRevert{revertHex: revert, success: []byte{0x99}} + + out, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.NoError(t, err) + assert.Equal(t, []byte{0x99}, out) +} + +func TestCall_RejectsMissingContentType(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + // no Content-Type header + _, _ = w.Write([]byte(`{"data":"0x"}`)) + })) + defer srv.Close() + + target := common.HexToAddress("0x5555555555555555555555555555555555555555") + revert := buildOffchainLookupRevert(t, target, []string{srv.URL}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "Content-Type") +} + +// --------------------------------------------------------------------------- +// Helper backend that reverts only on the first call, then succeeds. +// --------------------------------------------------------------------------- + +type succeedAfterOneRevert struct { + revertHex string + success []byte + calls atomic.Int32 +} + +func (b *succeedAfterOneRevert) CallContract(_ context.Context, _ ethereum.CallMsg, _ *big.Int) ([]byte, error) { + n := b.calls.Add(1) + if n == 1 { + return nil, &mockRevertErr{data: b.revertHex} + } + return b.success, nil +} + +func (b *succeedAfterOneRevert) CodeAt(_ context.Context, _ common.Address, _ *big.Int) ([]byte, error) { + return nil, nil +} + From 3f49037a558e20bae5a0caaab235098f69dac6ea Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:31:30 +0200 Subject: [PATCH 06/21] refactor(urerrors): rename ResolverErrorError, cover remaining decoders - Rename the awkwardly-doubled ResolverErrorError to ResolverRevertError. The old name read as "ErrorError"; the new one matches the typed-error vocabulary used elsewhere in the file (HTTPGatewayError, ReverseAddressMismatchError, etc.) and reads naturally at call sites. - Extend urerrors_test.go with decoder tests for the three custom errors that previously had none: ResolverError(bytes), HttpError(uint16,string), and ReverseAddressMismatch(string,bytes). Each test packs an authentic ABI payload, runs it through translateURRevert, asserts the typed Go error, and checks the user-facing message. Co-Authored-By: Claude Opus 4.7 --- urerrors.go | 8 ++++---- urerrors_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/urerrors.go b/urerrors.go index b3fa3a8..d5159fb 100644 --- a/urerrors.go +++ b/urerrors.go @@ -60,13 +60,13 @@ func (e *UnsupportedResolverProfileError) Error() string { return fmt.Sprintf("resolver does not implement profile 0x%s", hex.EncodeToString(e.Selector[:])) } -// ResolverErrorError corresponds to `ResolverError(bytes)`: the downstream +// ResolverRevertError corresponds to `ResolverError(bytes)`: the downstream // resolver itself reverted; the inner revert bytes are preserved verbatim. -type ResolverErrorError struct { +type ResolverRevertError struct { Data []byte } -func (e *ResolverErrorError) Error() string { +func (e *ResolverRevertError) Error() string { return fmt.Sprintf("resolver reverted: 0x%s", hex.EncodeToString(e.Data)) } @@ -195,7 +195,7 @@ func translateURRevert(err error) error { } return out case selResolverError: - out := &ResolverErrorError{} + out := &ResolverRevertError{} if vals, derr := argsResolverError.Unpack(body); derr == nil && len(vals) == 1 { if data, ok := vals[0].([]byte); ok { out.Data = data diff --git a/urerrors_test.go b/urerrors_test.go index 6cbddac..263da8d 100644 --- a/urerrors_test.go +++ b/urerrors_test.go @@ -81,6 +81,45 @@ func TestTranslateURRevert_ResolverNotContract(t *testing.T) { require.Equal(t, dnsName, typed.DNSName) } +func TestTranslateURRevert_ResolverError(t *testing.T) { + inner := []byte{0xde, 0xad, 0xbe, 0xef, 0x01, 0x02} + body := mustPackArgs(t, argsResolverError, inner) + rpcErr := &fakeRPCErr{msg: "execution reverted", data: revert(t, selResolverError, body...)} + + got := translateURRevert(rpcErr) + var typed *ResolverRevertError + require.True(t, errors.As(got, &typed), "expected ResolverRevertError, got %T: %v", got, got) + require.Equal(t, inner, typed.Data) + require.Contains(t, got.Error(), "resolver reverted: 0xdeadbeef0102") +} + +func TestTranslateURRevert_HTTPGateway(t *testing.T) { + body := mustPackArgs(t, argsHTTPError, uint16(503), "gateway temporarily unavailable") + rpcErr := &fakeRPCErr{msg: "execution reverted", data: revert(t, selHTTPError, body...)} + + got := translateURRevert(rpcErr) + var typed *HTTPGatewayError + require.True(t, errors.As(got, &typed), "expected HTTPGatewayError, got %T: %v", got, got) + require.Equal(t, uint16(503), typed.Status) + require.Equal(t, "gateway temporarily unavailable", typed.Message) + require.Contains(t, got.Error(), "503") + require.Contains(t, got.Error(), "gateway temporarily unavailable") +} + +func TestTranslateURRevert_ReverseAddressMismatch(t *testing.T) { + name := "alice.eth" + addrBytes := common.HexToAddress("0x000000000000000000000000000000000000bEEF").Bytes() + body := mustPackArgs(t, argsReverseAddressMismatch, name, addrBytes) + rpcErr := &fakeRPCErr{msg: "execution reverted", data: revert(t, selReverseAddressMismatch, body...)} + + got := translateURRevert(rpcErr) + var typed *ReverseAddressMismatchError + require.True(t, errors.As(got, &typed), "expected ReverseAddressMismatchError, got %T: %v", got, got) + require.Equal(t, name, typed.Name) + require.Equal(t, addrBytes, typed.Address) + require.Contains(t, got.Error(), name) +} + func TestTranslateURRevert_UnknownPassThrough(t *testing.T) { // A revert that is not a known UR custom error must pass through unchanged. rpcErr := &fakeRPCErr{msg: "execution reverted", data: "0xdeadbeef"} From 7fcbe7194e5f9bcfb9a083db6e4b3da1c83b0308 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:32:18 +0200 Subject: [PATCH 07/21] test(universalresolver): skip live-RPC tests under -short MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mainnetClient() now t.Skip()s when testing.Short() is set, so contributors working offline (or behind a rate-limited public RPC) can still run `go test -short ./...` without spurious failures. The pre-existing legacy tests in resolver_test.go / name_test.go / reverseresolver_test.go / tokenid_test.go still hit the network via the hardcoded Infura endpoint — that's audit item A10 and ships separately. Co-Authored-By: Claude Opus 4.7 --- universalresolver_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/universalresolver_test.go b/universalresolver_test.go index 0108d03..b5b0b6b 100644 --- a/universalresolver_test.go +++ b/universalresolver_test.go @@ -23,8 +23,14 @@ import ( // mainnetClient connects to a public Ethereum mainnet RPC. The endpoint can // be overridden with GO_ENS_TEST_RPC; without an override the test falls // back to a public endpoint so that `go test` works out of the box. +// +// Live-RPC tests skip themselves under `go test -short`, so CI runs that +// can't reach the network (or that are rate-limited) keep working. func mainnetClient(t *testing.T) *ethclient.Client { t.Helper() + if testing.Short() { + t.Skip("skipping live-RPC test under -short") + } url := os.Getenv("GO_ENS_TEST_RPC") if url == "" { // Public mainnet RPC. Surfaces revert data via the JSON-RPC `data` From 22dfa44849351d76a458a8e13c788dc0c9289b60 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:33:22 +0200 Subject: [PATCH 08/21] feat: add ResolveContext / ReverseResolveContext, honor ctx.Deadline Resolution previously ran on context.Background(), so callers could not cancel a slow CCIP-Read hop, propagate an HTTP-handler deadline, or interrupt resolution on shutdown. - Add ResolveContext(ctx, backend, input) and ReverseResolveContext( ctx, backend, address). The existing Resolve / ReverseResolve are preserved as background-context wrappers, so source compatibility is unchanged. - In ccipread.queryGateway, when the caller's HTTP client has no built-in timeout, use min(ctx.Deadline()-now, 30s) instead of a hard 30s. A handler-supplied 5s deadline is no longer silently extended to 30s; the same client can also still apply a sensible ceiling when no caller deadline is set. Docs on the legacy entry points note the limitation and point at the new Context variants. Co-Authored-By: Claude Opus 4.7 --- ccipread/ccipread.go | 12 ++++++++++-- resolver.go | 18 +++++++++++++++--- reverseresolver.go | 11 ++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/ccipread/ccipread.go b/ccipread/ccipread.go index 8e5694d..3b30886 100644 --- a/ccipread/ccipread.go +++ b/ccipread/ccipread.go @@ -284,8 +284,16 @@ func queryGateway(ctx context.Context, client *http.Client, url, sender, data st } if client.Timeout == 0 { // Best-effort timeout when the caller didn't supply one. Without this - // a wedged gateway would block resolution indefinitely. - ctxT, cancel := context.WithTimeout(req.Context(), 30*time.Second) + // a wedged gateway would block resolution indefinitely. Use the smaller + // of the caller's remaining deadline (if any) and 30s so a tight + // per-request deadline isn't extended. + timeout := 30 * time.Second + if deadline, ok := ctx.Deadline(); ok { + if remaining := time.Until(deadline); remaining < timeout { + timeout = remaining + } + } + ctxT, cancel := context.WithTimeout(req.Context(), timeout) defer cancel() req = req.WithContext(ctxT) } diff --git a/resolver.go b/resolver.go index 3ea9d56..483f094 100644 --- a/resolver.go +++ b/resolver.go @@ -189,9 +189,21 @@ func (r *Resolver) InterfaceImplementer(interfaceID [4]byte) (common.Address, er // // This will return an error if the name is not found or otherwise resolves // to the zero address. +// +// Resolve uses a background context, so callers cannot cancel a slow +// CCIP-Read hop or propagate an HTTP-handler deadline. Use ResolveContext +// when either is needed. func Resolve(backend bind.ContractBackend, input string) (common.Address, error) { + return ResolveContext(context.Background(), backend, input) +} + +// ResolveContext is identical to Resolve but honours ctx for cancellation +// and deadline propagation. The context flows through CCIP-Read hops, so a +// caller that sets a per-request deadline can cap the total resolution +// time end-to-end. +func ResolveContext(ctx context.Context, backend bind.ContractBackend, input string) (common.Address, error) { if strings.Contains(input, ".") { - return resolveName(backend, input) + return resolveName(ctx, backend, input) } if (strings.HasPrefix(input, "0x") && len(input) > 42) || (!strings.HasPrefix(input, "0x") && len(input) > 40) { return UnknownAddress, errors.New("address too long") @@ -204,7 +216,7 @@ func Resolve(backend bind.ContractBackend, input string) (common.Address, error) return address, nil } -func resolveName(backend bind.ContractBackend, input string) (common.Address, error) { +func resolveName(ctx context.Context, backend bind.ContractBackend, input string) (common.Address, error) { nameHash, err := NameHash(input) if err != nil { return UnknownAddress, err @@ -216,7 +228,7 @@ func resolveName(backend bind.ContractBackend, input string) (common.Address, er if err != nil { return UnknownAddress, err } - return ur.ResolveAddress(context.Background(), input) + return ur.ResolveAddress(ctx, input) } // SetText sets the text associated with a name. diff --git a/reverseresolver.go b/reverseresolver.go index 78ead1a..bb83a6e 100644 --- a/reverseresolver.go +++ b/reverseresolver.go @@ -110,12 +110,21 @@ func Format(backend bind.ContractBackend, address common.Address) string { // same way as fully on-chain reverse records. // // This will return an error if no primary name is set for the address. +// +// ReverseResolve uses a background context. Use ReverseResolveContext to +// propagate cancellation and deadlines through CCIP-Read hops. func ReverseResolve(backend bind.ContractBackend, address common.Address) (string, error) { + return ReverseResolveContext(context.Background(), backend, address) +} + +// ReverseResolveContext is identical to ReverseResolve but honours ctx for +// cancellation and deadline propagation. +func ReverseResolveContext(ctx context.Context, backend bind.ContractBackend, address common.Address) (string, error) { ur, err := NewUniversalResolver(backend) if err != nil { return "", err } - name, _, _, err := ur.Reverse(context.Background(), address.Bytes(), CoinTypeETH) + name, _, _, err := ur.Reverse(ctx, address.Bytes(), CoinTypeETH) if err != nil { return "", err } From 3a12310d7ada5781a94658e337740f7ef8f185e2 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:35:08 +0200 Subject: [PATCH 09/21] feat(universalresolver): chain-ID guardrail on default constructor NewUniversalResolver previously bound to the canonical proxy address unconditionally. On a chain where the UR isn't deployed at that address (devnet, local fork, alt-L1, Base, etc.) every call would silently fail or hit an unrelated contract. When the backend exposes ChainID (ethclient.Client and most production backends do), NewUniversalResolver now verifies the chain has a known canonical deployment. Unknown chains return a typed UnknownChainError pointing the caller at NewUniversalResolverAt, which remains the escape hatch for non-standard deployments. Backends without a ChainID method keep the old behaviour. The known list covers Ethereum mainnet (1) and Sepolia (11155111), matching the addresses listed in the existing doc comment. Co-Authored-By: Claude Opus 4.7 --- universalresolver.go | 34 +++++++++++++++++++++++++++++ universalresolver_test.go | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/universalresolver.go b/universalresolver.go index 1e23488..292a13e 100644 --- a/universalresolver.go +++ b/universalresolver.go @@ -34,6 +34,24 @@ const CoinTypeETH = uint64(60) // this selector to the discovered resolver and returns the abi-encoded result. var addrSelector = [4]byte{0x3b, 0x3b, 0x57, 0xde} +// knownURChains lists the chain IDs on which the canonical UR proxy address +// is deployed by the ENS DAO. Callers on other chains must pass an explicit +// address via NewUniversalResolverAt. +var knownURChains = map[uint64]struct{}{ + 1: {}, // Ethereum mainnet + 11155111: {}, // Sepolia testnet +} + +// UnknownChainError is returned by NewUniversalResolver when the backend +// reports a chain ID without a known UR deployment at the canonical address. +type UnknownChainError struct { + ChainID *big.Int +} + +func (e *UnknownChainError) Error() string { + return fmt.Sprintf("universal resolver: no canonical deployment on chain ID %s; pass an explicit address via NewUniversalResolverAt", e.ChainID) +} + // UniversalResolver wraps the ENS Universal Resolver and exposes resolution // helpers that transparently follow ERC-3668 OffchainLookup reverts via the // ccipread package. @@ -45,7 +63,23 @@ type UniversalResolver struct { // NewUniversalResolver returns a UniversalResolver bound to the canonical // proxy address on whichever chain the backend is connected to. +// +// When the backend exposes a chain ID (ethclient.Client and most production +// backends do) this verifies the chain is one where the canonical UR is +// deployed and returns *UnknownChainError otherwise. Backends that don't +// expose a chain ID skip the check and the canonical address is used as-is — +// callers on devnets / forks / alt-L1s should prefer NewUniversalResolverAt. func NewUniversalResolver(backend bind.ContractBackend) (*UniversalResolver, error) { + if cid, ok := backend.(interface { + ChainID(context.Context) (*big.Int, error) + }); ok { + chainID, err := cid.ChainID(context.Background()) + if err == nil && chainID != nil { + if _, known := knownURChains[chainID.Uint64()]; !known { + return nil, &UnknownChainError{ChainID: chainID} + } + } + } return NewUniversalResolverAt(backend, common.HexToAddress(UniversalResolverContractAddress)) } diff --git a/universalresolver_test.go b/universalresolver_test.go index b5b0b6b..5745e99 100644 --- a/universalresolver_test.go +++ b/universalresolver_test.go @@ -10,16 +10,61 @@ package ens_test import ( "context" + "math/big" "os" "strings" "testing" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" ens "github.com/wealdtech/go-ens/v3" ) +// chainIDBackend embeds a nil ContractBackend and overrides ChainID, so the +// constructor's type-assertion fires but no method outside ChainID can be +// called (which is fine because the guardrail returns before any other call). +type chainIDBackend struct { + bind.ContractBackend + chainID *big.Int +} + +func (b *chainIDBackend) ChainID(_ context.Context) (*big.Int, error) { + return b.chainID, nil +} + +func TestNewUniversalResolver_AcceptsMainnet(t *testing.T) { + ur, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(1)}) + require.NoError(t, err) + require.NotNil(t, ur) +} + +func TestNewUniversalResolver_AcceptsSepolia(t *testing.T) { + ur, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(11155111)}) + require.NoError(t, err) + require.NotNil(t, ur) +} + +func TestNewUniversalResolver_RejectsUnknownChain(t *testing.T) { + _, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(8453)}) // Base + require.Error(t, err) + var typed *ens.UnknownChainError + require.ErrorAs(t, err, &typed) + require.Equal(t, int64(8453), typed.ChainID.Int64()) + require.Contains(t, err.Error(), "NewUniversalResolverAt") +} + +// NewUniversalResolverAt skips the chain-ID check by design — useful for +// devnets / forks / alt-L1s where the UR is deployed at a non-canonical +// address. +func TestNewUniversalResolverAt_AcceptsAnyChain(t *testing.T) { + custom := common.HexToAddress("0x000000000000000000000000000000000000bEEF") + ur, err := ens.NewUniversalResolverAt(&chainIDBackend{chainID: big.NewInt(8453)}, custom) + require.NoError(t, err) + require.Equal(t, custom, ur.Address()) +} + // mainnetClient connects to a public Ethereum mainnet RPC. The endpoint can // be overridden with GO_ENS_TEST_RPC; without an override the test falls // back to a public endpoint so that `go test` works out of the box. From 0830688053d07a518206b5cf250a7f68cdc5a4ea Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:36:02 +0200 Subject: [PATCH 10/21] docs: flag that Resolver.Address and Name.Address skip the UR The ENSv2 upgrade routed ens.Resolve through the Universal Resolver, but Resolver.Address() and Name.Address() still walk the legacy registry directly. Downstream code that follows the README and reaches for the Name object gets confusingly different behaviour for wildcard and CCIP-Read names: ens.Resolve(client, name) returns the right address, name.Address(60) does not. Add doc comments on both methods making the limitation explicit and pointing callers at ens.Resolve / UniversalResolver.ResolveAddress. A full migration of these read paths through the UR is tracked separately. Co-Authored-By: Claude Opus 4.7 --- name.go | 7 +++++++ resolver.go | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/name.go b/name.go index d112b03..7cf9d02 100644 --- a/name.go +++ b/name.go @@ -295,6 +295,13 @@ func (n *Name) SetResolverAddress(address common.Address, opts *bind.TransactOpt // Address fetches the address of the name for a given coin type. // Coin types are defined at https://github.com/satoshilabs/slips/blob/master/slip-0044.md +// +// Note: this calls the resolver contract directly via the legacy registry +// walk, so it does NOT support ENSIP-10 wildcard resolution or ERC-3668 +// CCIP-Read. Names whose addresses live off-chain or on an L2 will return +// nil or an error here. For wildcard- and CCIP-Read-aware resolution, use +// ens.Resolve / UniversalResolver.ResolveAddress for coin type 60 (ETH); +// a UR-routed multicoin read is tracked as a follow-up. func (n *Name) Address(coinType uint64) ([]byte, error) { resolver, err := NewResolver(n.backend, n.Name) if err != nil { diff --git a/resolver.go b/resolver.go index 483f094..c2eda4b 100644 --- a/resolver.go +++ b/resolver.go @@ -98,6 +98,12 @@ func PublicResolverAddress(backend bind.ContractBackend) (common.Address, error) } // Address returns the Ethereum address of the domain. +// +// Note: this calls the resolver contract directly via the legacy registry +// walk, so it does NOT support ENSIP-10 wildcard resolution or ERC-3668 +// CCIP-Read. Names whose addresses live off-chain or on an L2 will return +// the zero address or an error here. For wildcard- and CCIP-Read-aware +// resolution, use ens.Resolve / UniversalResolver.ResolveAddress instead. func (r *Resolver) Address() (common.Address, error) { nameHash, err := NameHash(r.domain) if err != nil { From 9e63e9f39279b49dd67bed1f1021b37114417b37 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:40:51 +0200 Subject: [PATCH 11/21] fix(ccipread): SSRF defenses + typed GatewayHTTPError Gateway URLs come from a smart-contract revert, i.e. untrusted input. Add two defense-in-depth controls and reshape the 4xx/5xx error path so callers can act on it programmatically. - Scheme allowlist: parse the gateway URL and reject anything other than http/https. Stops a malicious resolver from getting Go's net/http client (or, more realistically, a caller-supplied custom transport) to fetch file:// or gopher:// targets. - No-redirect policy: when the caller's http.Client has no CheckRedirect hook, install one that returns ErrUseLastResponse. ERC-3668 gateways are expected to return data inline, so a 3xx is suspicious; blocking it prevents Location: http://169.254.169.254/... or RFC1918 pivots. Defensive-copy the client so we don't mutate the caller's instance. - GatewayHTTPError: 4xx/5xx now returns a typed error with URL, Status, and Body fields. Callers can errors.As to recover them, which the parent ens package needs in order to translate gateway failures into the on-chain HttpError(uint16,string) revert path. Add tests covering file://, gopher://, redirect rejection, and 4xx typed-error recovery. Co-Authored-By: Claude Opus 4.7 --- ccipread/ccipread.go | 54 +++++++++++++++++++++++++- ccipread/ccipread_test.go | 81 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/ccipread/ccipread.go b/ccipread/ccipread.go index 3b30886..87e6a8c 100644 --- a/ccipread/ccipread.go +++ b/ccipread/ccipread.go @@ -22,6 +22,7 @@ import ( "io" "math/big" "net/http" + neturl "net/url" "strings" "time" @@ -42,6 +43,32 @@ const MaxRedirects = 4 // exhausting the caller's heap. const MaxGatewayBodyBytes = 16 * 1024 * 1024 +// GatewayHTTPError is returned when a CCIP-Read gateway responds with a +// non-2xx HTTP status. Callers can use errors.As to recover the URL, status, +// and (truncated) body so they can decide whether to retry or surface the +// failure. +// +// This is distinct from the on-chain HTTPGatewayError typed-error in the +// parent ens package, which decodes the UR's HttpError(uint16,string) revert. +type GatewayHTTPError struct { + URL string + Status int + Body string +} + +func (e *GatewayHTTPError) Error() string { + return fmt.Sprintf("ccipread: gateway %s: HTTP %d: %s", e.URL, e.Status, truncate(e.Body, 256)) +} + +// noRedirect is the CheckRedirect policy installed when the caller's HTTP +// client doesn't supply one of its own. ERC-3668 gateways are expected to +// return data directly, so a 3xx is suspicious; reflecting the response back +// instead of following protects against SSRF via Location: redirects to +// internal hosts (cloud metadata services, RFC1918 ranges, etc.). +func noRedirect(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse +} + // offchainLookupSelector is the first 4 bytes of // keccak256("OffchainLookup(address,string[],bytes,bytes4,bytes)"). var offchainLookupSelector = []byte{0x55, 0x6f, 0x18, 0x30} @@ -112,6 +139,12 @@ func Call(ctx context.Context, backend Caller, to common.Address, data []byte, o if httpClient == nil { httpClient = http.DefaultClient } + if httpClient.CheckRedirect == nil { + // Defensive copy so we don't mutate the caller's shared client. + cp := *httpClient + cp.CheckRedirect = noRedirect + httpClient = &cp + } target := to callData := data @@ -258,6 +291,21 @@ func queryGateways(ctx context.Context, client *http.Client, urls []string, send } func queryGateway(ctx context.Context, client *http.Client, url, sender, data string) ([]byte, int, error) { + // Reject obviously hostile gateway URLs before we even build a request. + // Gateway URLs come from a smart contract (untrusted input), so a malicious + // resolver could return file://, gopher://, etc. By default Go's net/http + // transport rejects unknown schemes, but a caller-supplied custom transport + // might support them; this is defense-in-depth. + parsedURL, perr := neturl.Parse(url) + if perr != nil { + return nil, 0, fmt.Errorf("ccipread: gateway %q: invalid URL: %w", url, perr) + } + switch parsedURL.Scheme { + case "http", "https": + // ok + default: + return nil, 0, fmt.Errorf("ccipread: gateway %q: unsupported URL scheme %q", url, parsedURL.Scheme) + } hasData := strings.Contains(url, "{data}") hasSender := strings.Contains(url, "{sender}") var req *http.Request @@ -307,7 +355,11 @@ func queryGateway(ctx context.Context, client *http.Client, url, sender, data st return nil, resp.StatusCode, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, resp.StatusCode, fmt.Errorf("ccipread: gateway %s: HTTP %d: %s", url, resp.StatusCode, truncate(string(bodyBytes), 256)) + return nil, resp.StatusCode, &GatewayHTTPError{ + URL: url, + Status: resp.StatusCode, + Body: string(bodyBytes), + } } // Gate the JSON decode on Content-Type so a misbehaving gateway returning // HTML or text/plain with a 200 status surfaces as a clean error instead of diff --git a/ccipread/ccipread_test.go b/ccipread/ccipread_test.go index af68652..81a96af 100644 --- a/ccipread/ccipread_test.go +++ b/ccipread/ccipread_test.go @@ -183,6 +183,87 @@ func TestCall_RejectsMissingContentType(t *testing.T) { assert.Contains(t, err.Error(), "Content-Type") } +// --------------------------------------------------------------------------- +// A4 — gateway URL scheme allowlist. +// --------------------------------------------------------------------------- + +func TestCall_RejectsNonHTTPScheme(t *testing.T) { + target := common.HexToAddress("0x6666666666666666666666666666666666666666") + revert := buildOffchainLookupRevert(t, target, []string{"file:///etc/passwd"}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported URL scheme") +} + +func TestCall_RejectsGopherScheme(t *testing.T) { + target := common.HexToAddress("0x7777777777777777777777777777777777777777") + revert := buildOffchainLookupRevert(t, target, []string{"gopher://internal.svc/data"}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported URL scheme") +} + +// --------------------------------------------------------------------------- +// A5 — HTTP redirects are not followed by default. +// --------------------------------------------------------------------------- + +func TestCall_DoesNotFollowRedirects(t *testing.T) { + // The "redirect target" server would record a hit if the client followed + // the redirect — we assert it doesn't. + var followed atomic.Int32 + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + followed.Add(1) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":"0x"}`)) + })) + defer target.Close() + + // The "gateway" server returns a 302 pointing at the target. + redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", target.URL) + w.WriteHeader(http.StatusFound) + })) + defer redirector.Close() + + tgt := common.HexToAddress("0x8888888888888888888888888888888888888888") + revert := buildOffchainLookupRevert(t, tgt, []string{redirector.URL}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, tgt, []byte{0x00}, nil) + require.Error(t, err, "gateway returning a 3xx without data must error, not silently follow") + assert.Equal(t, int32(0), followed.Load(), "redirect target must not be reached") +} + +// --------------------------------------------------------------------------- +// A6 — typed GatewayHTTPError for non-2xx responses. +// --------------------------------------------------------------------------- + +func TestCall_4xxReturnsTypedError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":"bad request"}`)) + })) + defer srv.Close() + + target := common.HexToAddress("0x9999999999999999999999999999999999999999") + revert := buildOffchainLookupRevert(t, target, []string{srv.URL}) + backend := &loopBackend{revertHex: revert} + + _, err := ccipread.Call(context.Background(), backend, target, []byte{0x00}, nil) + require.Error(t, err) + + var typed *ccipread.GatewayHTTPError + require.ErrorAs(t, err, &typed, "expected GatewayHTTPError, got %T: %v", err, err) + assert.Equal(t, 400, typed.Status) + assert.Equal(t, srv.URL, typed.URL) + assert.Contains(t, typed.Body, "bad request") +} + // --------------------------------------------------------------------------- // Helper backend that reverts only on the first call, then succeeds. // --------------------------------------------------------------------------- From 55447945b76d3fc6aaafa558e15b8be27435ca6b Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:41:44 +0200 Subject: [PATCH 12/21] fix: accept []byte from ErrorData() in revert decoders go-ethereum's rpc.DataError.ErrorData() returns interface{}, and while the JSON-RPC client surfaces revert data as a 0x-prefixed string, mocked or custom backends may return the already-decoded bytes. The previous string-only type assertion silently dropped those, causing both translateURRevert (urerrors.go) and decodeOffchainLookup (ccipread.go) to treat valid reverts as plain errors. Centralise the type-switch in a normalizeErrorData helper in each package, accepting string and []byte. Add a urerrors test that drives the []byte path through a raw-data RPC error. Co-Authored-By: Claude Opus 4.7 --- ccipread/ccipread.go | 22 ++++++++++++++++++---- urerrors.go | 27 ++++++++++++++++++++------- urerrors_test.go | 21 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/ccipread/ccipread.go b/ccipread/ccipread.go index 87e6a8c..5f869e3 100644 --- a/ccipread/ccipread.go +++ b/ccipread/ccipread.go @@ -229,15 +229,29 @@ func decodeOffchainLookup(err error) (*OffchainLookup, bool) { if !errors.As(err, &de) { return nil, false } - raw, ok := de.ErrorData().(string) + revert, ok := normalizeErrorData(de.ErrorData()) if !ok { return nil, false } - revert, err := hex.DecodeString(strings.TrimPrefix(raw, "0x")) - if err != nil { + return DecodeOffchainLookup(revert) +} + +// normalizeErrorData accepts the various concrete types go-ethereum's +// rpc.DataError implementers return for ErrorData(): a 0x-prefixed hex +// string (the JSON-RPC case) or raw bytes (mocked / wrapped backends). +func normalizeErrorData(v interface{}) ([]byte, bool) { + switch d := v.(type) { + case string: + raw, err := hex.DecodeString(strings.TrimPrefix(d, "0x")) + if err != nil { + return nil, false + } + return raw, true + case []byte: + return d, true + default: return nil, false } - return DecodeOffchainLookup(revert) } func encodeCallback(selector [4]byte, response, extraData []byte) ([]byte, error) { diff --git a/urerrors.go b/urerrors.go index d5159fb..dbd64b8 100644 --- a/urerrors.go +++ b/urerrors.go @@ -239,13 +239,26 @@ func extractRevertData(err error) ([]byte, bool) { if !errors.As(err, &de) { return nil, false } - raw, ok := de.ErrorData().(string) - if !ok { - return nil, false - } - revert, derr := hex.DecodeString(strings.TrimPrefix(raw, "0x")) - if derr != nil { + return normalizeErrorData(de.ErrorData()) +} + +// normalizeErrorData accepts the various concrete types that go-ethereum's +// rpc.DataError implementers return for ErrorData(): a 0x-prefixed string +// (the common case for net/http JSON-RPC), or raw bytes (some wrapped or +// mock backends). hexutil.Bytes is reported as []byte via its underlying +// type, so the []byte branch catches it too. +func normalizeErrorData(v interface{}) ([]byte, bool) { + switch d := v.(type) { + case string: + raw, err := hex.DecodeString(strings.TrimPrefix(d, "0x")) + if err != nil { + return nil, false + } + return raw, true + case []byte: + // Already-decoded revert bytes — accept as-is. + return d, true + default: return nil, false } - return revert, true } diff --git a/urerrors_test.go b/urerrors_test.go index 263da8d..0b35d85 100644 --- a/urerrors_test.go +++ b/urerrors_test.go @@ -127,6 +127,27 @@ func TestTranslateURRevert_UnknownPassThrough(t *testing.T) { require.Same(t, rpcErr, got, "unknown selectors must not be translated") } +// rawDataRPCErr mirrors backends (e.g. mocks, custom RPC wrappers) that +// surface ErrorData() as already-decoded bytes rather than a hex string. +type rawDataRPCErr struct { + msg string + data []byte +} + +func (e *rawDataRPCErr) Error() string { return e.msg } +func (e *rawDataRPCErr) ErrorData() interface{} { return e.data } + +func TestTranslateURRevert_AcceptsByteSliceErrorData(t *testing.T) { + // Build the same EmptyAddress revert but expose it as []byte, not string. + revertBytes := append([]byte{}, selEmptyAddress[:]...) + rpcErr := &rawDataRPCErr{msg: "execution reverted", data: revertBytes} + + got := translateURRevert(rpcErr) + var typed *EmptyAddressError + require.True(t, errors.As(got, &typed), "expected EmptyAddressError, got %T: %v", got, got) + require.True(t, errors.Is(got, ErrNoAddress)) +} + func TestTranslateURRevert_NonRPCError(t *testing.T) { plain := errors.New("boom") require.Same(t, plain, translateURRevert(plain), "non-RPC errors must pass through") From da238e26b0f198794e6dc5b9fbea05d8e45791b2 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 01:43:04 +0200 Subject: [PATCH 13/21] chore: tighten ABI type checks and drop dead code - universalresolver.go: stop swallowing failed type assertions on the ABI-unpacked results of Resolve and Reverse. Arity was checked but each element's `_ := values[i].(T)` returned a zero-value silently; if the UR ABI ever drifts, callers used to see a misleading "no record" response. Now each mis-typed element surfaces as an explicit decode error. - resolver.go: drop the dead `bytes.Equal(nameHash[:], zeroHash)` guard in resolveName. resolveName is reached only when input contains '.', and NameHash returns the zero hash only for the empty input, so the branch was unreachable. Remove the now-unused `zeroHash` variable along with it. Co-Authored-By: Claude Opus 4.7 --- resolver.go | 8 +------- universalresolver.go | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/resolver.go b/resolver.go index c2eda4b..d67e48f 100644 --- a/resolver.go +++ b/resolver.go @@ -29,8 +29,6 @@ import ( "github.com/wealdtech/go-ens/v3/contracts/resolver" ) -var zeroHash = make([]byte, 32) - // UnknownAddress is the address to which unknown entries resolve. var UnknownAddress = common.HexToAddress("00") @@ -223,13 +221,9 @@ func ResolveContext(ctx context.Context, backend bind.ContractBackend, input str } func resolveName(ctx context.Context, backend bind.ContractBackend, input string) (common.Address, error) { - nameHash, err := NameHash(input) - if err != nil { + if _, err := NameHash(input); err != nil { return UnknownAddress, err } - if bytes.Equal(nameHash[:], zeroHash) { - return UnknownAddress, errors.New("bad name") - } ur, err := NewUniversalResolver(backend) if err != nil { return UnknownAddress, err diff --git a/universalresolver.go b/universalresolver.go index 292a13e..214138d 100644 --- a/universalresolver.go +++ b/universalresolver.go @@ -124,8 +124,14 @@ func (u *UniversalResolver) Resolve(ctx context.Context, name string, callData [ if len(values) != 2 { return nil, common.Address{}, errors.New("universal resolver: unexpected resolve output arity") } - result, _ := values[0].([]byte) - resolver, _ := values[1].(common.Address) + result, ok := values[0].([]byte) + if !ok { + return nil, common.Address{}, fmt.Errorf("universal resolver: resolve result has unexpected type %T", values[0]) + } + resolver, ok := values[1].(common.Address) + if !ok { + return nil, common.Address{}, fmt.Errorf("universal resolver: resolve resolver has unexpected type %T", values[1]) + } return result, resolver, nil } @@ -176,8 +182,17 @@ func (u *UniversalResolver) Reverse(ctx context.Context, address []byte, coinTyp if len(values) != 3 { return "", common.Address{}, common.Address{}, errors.New("universal resolver: unexpected reverse output arity") } - name, _ := values[0].(string) - resolver, _ := values[1].(common.Address) - reverseResolver, _ := values[2].(common.Address) + name, ok := values[0].(string) + if !ok { + return "", common.Address{}, common.Address{}, fmt.Errorf("universal resolver: reverse name has unexpected type %T", values[0]) + } + resolver, ok := values[1].(common.Address) + if !ok { + return "", common.Address{}, common.Address{}, fmt.Errorf("universal resolver: reverse resolver has unexpected type %T", values[1]) + } + reverseResolver, ok := values[2].(common.Address) + if !ok { + return "", common.Address{}, common.Address{}, fmt.Errorf("universal resolver: reverse reverseResolver has unexpected type %T", values[2]) + } return name, resolver, reverseResolver, nil } From b42e4a830a03904283184c6232c389e9191b170b Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 28 May 2026 00:10:36 +0200 Subject: [PATCH 14/21] fix(universalresolver): include Holesky in known UR chain list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ENS deployments the canonical UR proxy at 0xeEeEEEeE… is also deployed on Holesky (chain ID 17000). Add it to knownURChains and cover with TestNewUniversalResolver_AcceptsHolesky. --- universalresolver.go | 5 +++-- universalresolver_test.go | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/universalresolver.go b/universalresolver.go index 214138d..94f27fd 100644 --- a/universalresolver.go +++ b/universalresolver.go @@ -22,8 +22,8 @@ import ( ) // UniversalResolverContractAddress is the proxy address of the ENS -// UniversalResolver. The same address is used on Ethereum mainnet and Sepolia -// (the proxy is deployed at the vanity address by the ENS DAO). +// UniversalResolver. The same vanity address is used on Ethereum mainnet +// and L1 testnets where the ENS DAO has deployed the proxy (Sepolia, Holesky). const UniversalResolverContractAddress = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe" // CoinTypeETH is SLIP-0044 coin type 60: native Ethereum addresses. @@ -40,6 +40,7 @@ var addrSelector = [4]byte{0x3b, 0x3b, 0x57, 0xde} var knownURChains = map[uint64]struct{}{ 1: {}, // Ethereum mainnet 11155111: {}, // Sepolia testnet + 17000: {}, // Holesky testnet } // UnknownChainError is returned by NewUniversalResolver when the backend diff --git a/universalresolver_test.go b/universalresolver_test.go index 5745e99..d3c1e38 100644 --- a/universalresolver_test.go +++ b/universalresolver_test.go @@ -46,6 +46,12 @@ func TestNewUniversalResolver_AcceptsSepolia(t *testing.T) { require.NotNil(t, ur) } +func TestNewUniversalResolver_AcceptsHolesky(t *testing.T) { + ur, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(17000)}) + require.NoError(t, err) + require.NotNil(t, ur) +} + func TestNewUniversalResolver_RejectsUnknownChain(t *testing.T) { _, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(8453)}) // Base require.Error(t, err) From 88946cd1a13f34815625f6b7fb228efbdf39b5d0 Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 20 Jun 2026 16:18:12 +0200 Subject: [PATCH 15/21] Adjust DNS label length tests to 63-byte cap --- dnsencode_test.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/dnsencode_test.go b/dnsencode_test.go index e8a5a10..0c6dc23 100644 --- a/dnsencode_test.go +++ b/dnsencode_test.go @@ -125,9 +125,7 @@ func TestDNSEncode_DoubleDotInMiddle(t *testing.T) { // byte are reserved for compression-pointer flags, so a 64+ byte label cannot // be encoded validly. // -// The current implementation uses a 255-byte cap (matching its doc comment). -// The "RFC-correct" tests below intentionally fail against the current code -// to drive the fix. +// The current implementation uses a 63-byte cap. // --------------------------------------------------------------------------- func TestDNSEncode_Label63Bytes(t *testing.T) { @@ -138,22 +136,19 @@ func TestDNSEncode_Label63Bytes(t *testing.T) { assert.Equal(t, label, string(out[1:64])) } -// EXPECTED TO FAIL against current code (bug A2: label cap is 255, not 63). func TestDNSEncode_Label64Bytes(t *testing.T) { label := strings.Repeat("a", 64) _, err := ens.DNSEncode(label + ".eth") require.Error(t, err, "labels above 63 bytes are invalid per RFC 1035 §3.1") } -// EXPECTED TO FAIL against current code (bug A2). 255 is the cap the current -// implementation enforces, so this asserts the RFC-correct behavior. func TestDNSEncode_Label255Bytes(t *testing.T) { label := strings.Repeat("a", 255) _, err := ens.DNSEncode(label + ".eth") require.Error(t, err, "labels above 63 bytes are invalid per RFC 1035 §3.1") } -// 256-byte label is rejected by the current code's `> 255` check. +// 256-byte label is rejected by the implementation's `> 63` check. func TestDNSEncode_Label256Bytes(t *testing.T) { label := strings.Repeat("a", 256) _, err := ens.DNSEncode(label + ".eth") From 499727704ee9173395610aa8d9d2e582e653f25b Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 20 Jun 2026 16:23:00 +0200 Subject: [PATCH 16/21] Add NewUniversalResolverContext and propagate ctx --- resolver.go | 2 +- reverseresolver.go | 2 +- universalresolver.go | 22 +++++++++++++-- universalresolver_test.go | 58 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/resolver.go b/resolver.go index d67e48f..6b22e29 100644 --- a/resolver.go +++ b/resolver.go @@ -224,7 +224,7 @@ func resolveName(ctx context.Context, backend bind.ContractBackend, input string if _, err := NameHash(input); err != nil { return UnknownAddress, err } - ur, err := NewUniversalResolver(backend) + ur, err := NewUniversalResolverContext(ctx, backend) if err != nil { return UnknownAddress, err } diff --git a/reverseresolver.go b/reverseresolver.go index bb83a6e..692adcd 100644 --- a/reverseresolver.go +++ b/reverseresolver.go @@ -120,7 +120,7 @@ func ReverseResolve(backend bind.ContractBackend, address common.Address) (strin // ReverseResolveContext is identical to ReverseResolve but honours ctx for // cancellation and deadline propagation. func ReverseResolveContext(ctx context.Context, backend bind.ContractBackend, address common.Address) (string, error) { - ur, err := NewUniversalResolver(backend) + ur, err := NewUniversalResolverContext(ctx, backend) if err != nil { return "", err } diff --git a/universalresolver.go b/universalresolver.go index 94f27fd..9935aef 100644 --- a/universalresolver.go +++ b/universalresolver.go @@ -43,8 +43,9 @@ var knownURChains = map[uint64]struct{}{ 17000: {}, // Holesky testnet } -// UnknownChainError is returned by NewUniversalResolver when the backend -// reports a chain ID without a known UR deployment at the canonical address. +// UnknownChainError is returned by NewUniversalResolver and +// NewUniversalResolverContext when the backend reports a chain ID without a +// known UR deployment at the canonical address. type UnknownChainError struct { ChainID *big.Int } @@ -70,15 +71,30 @@ type UniversalResolver struct { // deployed and returns *UnknownChainError otherwise. Backends that don't // expose a chain ID skip the check and the canonical address is used as-is — // callers on devnets / forks / alt-L1s should prefer NewUniversalResolverAt. +// +// The chain-ID probe uses a background context, so a caller-supplied context +// cannot cancel it. Use NewUniversalResolverContext when the caller needs to +// propagate cancellation / deadlines into the probe (e.g. via ResolveContext +// or ReverseResolveContext). func NewUniversalResolver(backend bind.ContractBackend) (*UniversalResolver, error) { + return NewUniversalResolverContext(context.Background(), backend) +} + +// NewUniversalResolverContext is identical to NewUniversalResolver but honours +// ctx for the chain-ID probe: if ctx is cancelled before the backend reports +// its chain ID, the probe returns ctx.Err() instead of *UnknownChainError. +func NewUniversalResolverContext(ctx context.Context, backend bind.ContractBackend) (*UniversalResolver, error) { if cid, ok := backend.(interface { ChainID(context.Context) (*big.Int, error) }); ok { - chainID, err := cid.ChainID(context.Background()) + chainID, err := cid.ChainID(ctx) if err == nil && chainID != nil { if _, known := knownURChains[chainID.Uint64()]; !known { return nil, &UnknownChainError{ChainID: chainID} } + } else if err != nil && ctx.Err() != nil { + // Surface ctx cancellation/deadline from the probe itself. + return nil, ctx.Err() } } return NewUniversalResolverAt(backend, common.HexToAddress(UniversalResolverContractAddress)) diff --git a/universalresolver_test.go b/universalresolver_test.go index d3c1e38..9e2e583 100644 --- a/universalresolver_test.go +++ b/universalresolver_test.go @@ -71,6 +71,64 @@ func TestNewUniversalResolverAt_AcceptsAnyChain(t *testing.T) { require.Equal(t, custom, ur.Address()) } +// slowChainIDBackend blocks on ctx until released, then returns its chain ID. +// Used to assert that NewUniversalResolverContext propagates ctx cancellation +// into the chain-ID probe instead of relying on context.Background(). +type slowChainIDBackend struct { + bind.ContractBackend + chainID *big.Int + release chan struct{} +} + +func (b *slowChainIDBackend) ChainID(ctx context.Context) (*big.Int, error) { + select { + case <-b.release: + return b.chainID, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// TestNewUniversalResolverContext_PropagatesCancellation asserts that a +// cancelled ctx surfaces as ctx.Err() from the constructor's chain-ID probe, +// rather than the probe running to completion on an uncancellable background +// context. +func TestNewUniversalResolverContext_PropagatesCancellation(t *testing.T) { + backend := &slowChainIDBackend{ + chainID: big.NewInt(1), + release: make(chan struct{}), + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := ens.NewUniversalResolverContext(ctx, backend) + require.ErrorIs(t, err, context.Canceled, + "a cancelled ctx must surface as context.Canceled from the chain-ID probe") +} + +// TestNewUniversalResolverContext_HonoursDeadline mirrors the cancellation +// test but with a context deadline that expires during the probe. +func TestNewUniversalResolverContext_HonoursDeadline(t *testing.T) { + backend := &slowChainIDBackend{ + chainID: big.NewInt(1), + release: make(chan struct{}), + } + ctx, cancel := context.WithTimeout(context.Background(), 0) + defer cancel() + + _, err := ens.NewUniversalResolverContext(ctx, backend) + require.ErrorIs(t, err, context.DeadlineExceeded, + "an expired deadline must surface as context.DeadlineExceeded from the chain-ID probe") +} + +// TestNewUniversalResolverContext_AcceptsKnownChain ensures the context-aware +// constructor still returns a usable resolver on the happy path. +func TestNewUniversalResolverContext_AcceptsKnownChain(t *testing.T) { + ur, err := ens.NewUniversalResolverContext(context.Background(), &chainIDBackend{chainID: big.NewInt(1)}) + require.NoError(t, err) + require.NotNil(t, ur) +} + // mainnetClient connects to a public Ethereum mainnet RPC. The endpoint can // be overridden with GO_ENS_TEST_RPC; without an override the test falls // back to a public endpoint so that `go test` works out of the box. From 78498de413db583a9232a06fd3fd1833fd70a360 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 6 Jul 2026 16:34:20 +0200 Subject: [PATCH 17/21] Update Universal Resolver ABI and remove unused Go bindings --- contracts/universalresolver/contract.abi | 66 ++- contracts/universalresolver/contract.go | 601 +---------------------- 2 files changed, 66 insertions(+), 601 deletions(-) diff --git a/contracts/universalresolver/contract.abi b/contracts/universalresolver/contract.abi index 5080ebd..14dd2e8 100644 --- a/contracts/universalresolver/contract.abi +++ b/contracts/universalresolver/contract.abi @@ -1 +1,65 @@ -[{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"contract ENS","name":"ens","type":"address"},{"internalType":"contract IGatewayProvider","name":"batchGatewayProvider","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes","name":"dns","type":"bytes"}],"name":"DNSDecodingFailed","type":"error"},{"inputs":[{"internalType":"string","name":"ens","type":"string"}],"name":"DNSEncodingFailed","type":"error"},{"inputs":[],"name":"EmptyAddress","type":"error"},{"inputs":[{"internalType":"uint16","name":"status","type":"uint16"},{"internalType":"string","name":"message","type":"string"}],"name":"HttpError","type":"error"},{"inputs":[],"name":"InvalidBatchGatewayResponse","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"string[]","name":"urls","type":"string[]"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"bytes4","name":"callbackFunction","type":"bytes4"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"OffchainLookup","type":"error"},{"inputs":[{"internalType":"uint256","name":"offset","type":"uint256"},{"internalType":"uint256","name":"length","type":"uint256"}],"name":"OffsetOutOfBoundsError","type":"error"},{"inputs":[{"internalType":"bytes","name":"errorData","type":"bytes"}],"name":"ResolverError","type":"error"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"address","name":"resolver","type":"address"}],"name":"ResolverNotContract","type":"error"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"ResolverNotFound","type":"error"},{"inputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"bytes","name":"primaryAddress","type":"bytes"}],"name":"ReverseAddressMismatch","type":"error"},{"inputs":[{"internalType":"bytes4","name":"selector","type":"bytes4"}],"name":"UnsupportedResolverProfile","type":"error"},{"inputs":[],"name":"batchGatewayProvider","outputs":[{"internalType":"contract IGatewayProvider","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"call","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"flags","type":"uint256"}],"internalType":"struct CCIPBatcher.Lookup[]","name":"lookups","type":"tuple[]"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"internalType":"struct CCIPBatcher.Batch","name":"batch","type":"tuple"}],"name":"ccipBatch","outputs":[{"components":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"call","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"flags","type":"uint256"}],"internalType":"struct CCIPBatcher.Lookup[]","name":"lookups","type":"tuple[]"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"internalType":"struct CCIPBatcher.Batch","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"ccipBatchCallback","outputs":[{"components":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"call","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"flags","type":"uint256"}],"internalType":"struct CCIPBatcher.Lookup[]","name":"lookups","type":"tuple[]"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"internalType":"struct CCIPBatcher.Batch","name":"batch","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"ccipReadCallback","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"findResolver","outputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"registry","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"}],"name":"requireResolver","outputs":[{"components":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"uint256","name":"offset","type":"uint256"},{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"bool","name":"extended","type":"bool"}],"internalType":"struct AbstractUniversalResolver.ResolverInfo","name":"info","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"resolve","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"resolveBatchCallback","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"resolveCallback","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"resolveDirectCallback","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"resolveDirectCallbackError","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"name":"resolveWithGateways","outputs":[{"internalType":"bytes","name":"result","type":"bytes"},{"internalType":"address","name":"resolver","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"resolver","type":"address"},{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"name":"resolveWithResolver","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"lookupAddress","type":"bytes"},{"internalType":"uint256","name":"coinType","type":"uint256"}],"name":"reverse","outputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"reverseAddressCallback","outputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"address","name":"reverseResolver","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"response","type":"bytes"},{"internalType":"bytes","name":"extraData","type":"bytes"}],"name":"reverseNameCallback","outputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"lookupAddress","type":"bytes"},{"internalType":"uint256","name":"coinType","type":"uint256"},{"internalType":"string[]","name":"gateways","type":"string[]"}],"name":"reverseWithGateways","outputs":[{"internalType":"string","name":"primary","type":"string"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"address","name":"reverseResolver","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[ + { + "inputs": [ + { + "internalType": "bytes", + "name": "name", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "resolve", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "lookupAddress", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "coinType", + "type": "uint256" + } + ], + "name": "reverse", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/contracts/universalresolver/contract.go b/contracts/universalresolver/contract.go index 9d12598..355832c 100644 --- a/contracts/universalresolver/contract.go +++ b/contracts/universalresolver/contract.go @@ -29,32 +29,9 @@ var ( _ = abi.ConvertType ) -// AbstractUniversalResolverResolverInfo is an auto generated low-level Go binding around an user-defined struct. -type AbstractUniversalResolverResolverInfo struct { - Name []byte - Offset *big.Int - Node [32]byte - Resolver common.Address - Extended bool -} - -// CCIPBatcherBatch is an auto generated low-level Go binding around an user-defined struct. -type CCIPBatcherBatch struct { - Lookups []CCIPBatcherLookup - Gateways []string -} - -// CCIPBatcherLookup is an auto generated low-level Go binding around an user-defined struct. -type CCIPBatcherLookup struct { - Target common.Address - Call []byte - Data []byte - Flags *big.Int -} - // ContractMetaData contains all meta data concerning the Contract contract. var ContractMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"contractENS\",\"name\":\"ens\",\"type\":\"address\"},{\"internalType\":\"contractIGatewayProvider\",\"name\":\"batchGatewayProvider\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"dns\",\"type\":\"bytes\"}],\"name\":\"DNSDecodingFailed\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"ens\",\"type\":\"string\"}],\"name\":\"DNSEncodingFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyAddress\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint16\",\"name\":\"status\",\"type\":\"uint16\"},{\"internalType\":\"string\",\"name\":\"message\",\"type\":\"string\"}],\"name\":\"HttpError\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidBatchGatewayResponse\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"string[]\",\"name\":\"urls\",\"type\":\"string[]\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"callbackFunction\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"OffchainLookup\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"offset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"OffsetOutOfBoundsError\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"errorData\",\"type\":\"bytes\"}],\"name\":\"ResolverError\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"}],\"name\":\"ResolverNotContract\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"}],\"name\":\"ResolverNotFound\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"bytes\",\"name\":\"primaryAddress\",\"type\":\"bytes\"}],\"name\":\"ReverseAddressMismatch\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"selector\",\"type\":\"bytes4\"}],\"name\":\"UnsupportedResolverProfile\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"batchGatewayProvider\",\"outputs\":[{\"internalType\":\"contractIGatewayProvider\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"flags\",\"type\":\"uint256\"}],\"internalType\":\"structCCIPBatcher.Lookup[]\",\"name\":\"lookups\",\"type\":\"tuple[]\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"internalType\":\"structCCIPBatcher.Batch\",\"name\":\"batch\",\"type\":\"tuple\"}],\"name\":\"ccipBatch\",\"outputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"flags\",\"type\":\"uint256\"}],\"internalType\":\"structCCIPBatcher.Lookup[]\",\"name\":\"lookups\",\"type\":\"tuple[]\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"internalType\":\"structCCIPBatcher.Batch\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"ccipBatchCallback\",\"outputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"flags\",\"type\":\"uint256\"}],\"internalType\":\"structCCIPBatcher.Lookup[]\",\"name\":\"lookups\",\"type\":\"tuple[]\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"internalType\":\"structCCIPBatcher.Batch\",\"name\":\"batch\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"ccipReadCallback\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"}],\"name\":\"findResolver\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"registry\",\"outputs\":[{\"internalType\":\"contractENS\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"}],\"name\":\"requireResolver\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"offset\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"node\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"extended\",\"type\":\"bool\"}],\"internalType\":\"structAbstractUniversalResolver.ResolverInfo\",\"name\":\"info\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"resolve\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"resolveBatchCallback\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"resolveCallback\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"resolveDirectCallback\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"name\":\"resolveDirectCallbackError\",\"outputs\":[],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"name\":\"resolveWithGateways\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"result\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"name\":\"resolveWithResolver\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"lookupAddress\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"coinType\",\"type\":\"uint256\"}],\"name\":\"reverse\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"reverseAddressCallback\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"reverseResolver\",\"type\":\"address\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"response\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"reverseNameCallback\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"lookupAddress\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"coinType\",\"type\":\"uint256\"},{\"internalType\":\"string[]\",\"name\":\"gateways\",\"type\":\"string[]\"}],\"name\":\"reverseWithGateways\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"primary\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"resolver\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"reverseResolver\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + ABI: "[{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"name\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"resolve\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"lookupAddress\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"coinType\",\"type\":\"uint256\"}],\"name\":\"reverse\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", } // ContractABI is the input ABI used to generate the binding from. @@ -203,223 +180,6 @@ func (_Contract *ContractTransactorRaw) Transact(opts *bind.TransactOpts, method return _Contract.Contract.contract.Transact(opts, method, params...) } -// BatchGatewayProvider is a free data retrieval call binding the contract method 0x02cf2578. -// -// Solidity: function batchGatewayProvider() view returns(address) -func (_Contract *ContractCaller) BatchGatewayProvider(opts *bind.CallOpts) (common.Address, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "batchGatewayProvider") - - if err != nil { - return *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - - return out0, err - -} - -// BatchGatewayProvider is a free data retrieval call binding the contract method 0x02cf2578. -// -// Solidity: function batchGatewayProvider() view returns(address) -func (_Contract *ContractSession) BatchGatewayProvider() (common.Address, error) { - return _Contract.Contract.BatchGatewayProvider(&_Contract.CallOpts) -} - -// BatchGatewayProvider is a free data retrieval call binding the contract method 0x02cf2578. -// -// Solidity: function batchGatewayProvider() view returns(address) -func (_Contract *ContractCallerSession) BatchGatewayProvider() (common.Address, error) { - return _Contract.Contract.BatchGatewayProvider(&_Contract.CallOpts) -} - -// CcipBatch is a free data retrieval call binding the contract method 0x9f28e99d. -// -// Solidity: function ccipBatch(((address,bytes,bytes,uint256)[],string[]) batch) view returns(((address,bytes,bytes,uint256)[],string[])) -func (_Contract *ContractCaller) CcipBatch(opts *bind.CallOpts, batch CCIPBatcherBatch) (CCIPBatcherBatch, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "ccipBatch", batch) - - if err != nil { - return *new(CCIPBatcherBatch), err - } - - out0 := *abi.ConvertType(out[0], new(CCIPBatcherBatch)).(*CCIPBatcherBatch) - - return out0, err - -} - -// CcipBatch is a free data retrieval call binding the contract method 0x9f28e99d. -// -// Solidity: function ccipBatch(((address,bytes,bytes,uint256)[],string[]) batch) view returns(((address,bytes,bytes,uint256)[],string[])) -func (_Contract *ContractSession) CcipBatch(batch CCIPBatcherBatch) (CCIPBatcherBatch, error) { - return _Contract.Contract.CcipBatch(&_Contract.CallOpts, batch) -} - -// CcipBatch is a free data retrieval call binding the contract method 0x9f28e99d. -// -// Solidity: function ccipBatch(((address,bytes,bytes,uint256)[],string[]) batch) view returns(((address,bytes,bytes,uint256)[],string[])) -func (_Contract *ContractCallerSession) CcipBatch(batch CCIPBatcherBatch) (CCIPBatcherBatch, error) { - return _Contract.Contract.CcipBatch(&_Contract.CallOpts, batch) -} - -// CcipBatchCallback is a free data retrieval call binding the contract method 0xb536af76. -// -// Solidity: function ccipBatchCallback(bytes response, bytes extraData) view returns(((address,bytes,bytes,uint256)[],string[]) batch) -func (_Contract *ContractCaller) CcipBatchCallback(opts *bind.CallOpts, response []byte, extraData []byte) (CCIPBatcherBatch, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "ccipBatchCallback", response, extraData) - - if err != nil { - return *new(CCIPBatcherBatch), err - } - - out0 := *abi.ConvertType(out[0], new(CCIPBatcherBatch)).(*CCIPBatcherBatch) - - return out0, err - -} - -// CcipBatchCallback is a free data retrieval call binding the contract method 0xb536af76. -// -// Solidity: function ccipBatchCallback(bytes response, bytes extraData) view returns(((address,bytes,bytes,uint256)[],string[]) batch) -func (_Contract *ContractSession) CcipBatchCallback(response []byte, extraData []byte) (CCIPBatcherBatch, error) { - return _Contract.Contract.CcipBatchCallback(&_Contract.CallOpts, response, extraData) -} - -// CcipBatchCallback is a free data retrieval call binding the contract method 0xb536af76. -// -// Solidity: function ccipBatchCallback(bytes response, bytes extraData) view returns(((address,bytes,bytes,uint256)[],string[]) batch) -func (_Contract *ContractCallerSession) CcipBatchCallback(response []byte, extraData []byte) (CCIPBatcherBatch, error) { - return _Contract.Contract.CcipBatchCallback(&_Contract.CallOpts, response, extraData) -} - -// CcipReadCallback is a free data retrieval call binding the contract method 0xef46c0b8. -// -// Solidity: function ccipReadCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractCaller) CcipReadCallback(opts *bind.CallOpts, response []byte, extraData []byte) error { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "ccipReadCallback", response, extraData) - - if err != nil { - return err - } - - return err - -} - -// CcipReadCallback is a free data retrieval call binding the contract method 0xef46c0b8. -// -// Solidity: function ccipReadCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractSession) CcipReadCallback(response []byte, extraData []byte) error { - return _Contract.Contract.CcipReadCallback(&_Contract.CallOpts, response, extraData) -} - -// CcipReadCallback is a free data retrieval call binding the contract method 0xef46c0b8. -// -// Solidity: function ccipReadCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractCallerSession) CcipReadCallback(response []byte, extraData []byte) error { - return _Contract.Contract.CcipReadCallback(&_Contract.CallOpts, response, extraData) -} - -// FindResolver is a free data retrieval call binding the contract method 0xa1cbcbaf. -// -// Solidity: function findResolver(bytes name) view returns(address, bytes32, uint256) -func (_Contract *ContractCaller) FindResolver(opts *bind.CallOpts, name []byte) (common.Address, [32]byte, *big.Int, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "findResolver", name) - - if err != nil { - return *new(common.Address), *new([32]byte), *new(*big.Int), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - out1 := *abi.ConvertType(out[1], new([32]byte)).(*[32]byte) - out2 := *abi.ConvertType(out[2], new(*big.Int)).(**big.Int) - - return out0, out1, out2, err - -} - -// FindResolver is a free data retrieval call binding the contract method 0xa1cbcbaf. -// -// Solidity: function findResolver(bytes name) view returns(address, bytes32, uint256) -func (_Contract *ContractSession) FindResolver(name []byte) (common.Address, [32]byte, *big.Int, error) { - return _Contract.Contract.FindResolver(&_Contract.CallOpts, name) -} - -// FindResolver is a free data retrieval call binding the contract method 0xa1cbcbaf. -// -// Solidity: function findResolver(bytes name) view returns(address, bytes32, uint256) -func (_Contract *ContractCallerSession) FindResolver(name []byte) (common.Address, [32]byte, *big.Int, error) { - return _Contract.Contract.FindResolver(&_Contract.CallOpts, name) -} - -// Registry is a free data retrieval call binding the contract method 0x7b103999. -// -// Solidity: function registry() view returns(address) -func (_Contract *ContractCaller) Registry(opts *bind.CallOpts) (common.Address, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "registry") - - if err != nil { - return *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - - return out0, err - -} - -// Registry is a free data retrieval call binding the contract method 0x7b103999. -// -// Solidity: function registry() view returns(address) -func (_Contract *ContractSession) Registry() (common.Address, error) { - return _Contract.Contract.Registry(&_Contract.CallOpts) -} - -// Registry is a free data retrieval call binding the contract method 0x7b103999. -// -// Solidity: function registry() view returns(address) -func (_Contract *ContractCallerSession) Registry() (common.Address, error) { - return _Contract.Contract.Registry(&_Contract.CallOpts) -} - -// RequireResolver is a free data retrieval call binding the contract method 0xc285238a. -// -// Solidity: function requireResolver(bytes name) view returns((bytes,uint256,bytes32,address,bool) info) -func (_Contract *ContractCaller) RequireResolver(opts *bind.CallOpts, name []byte) (AbstractUniversalResolverResolverInfo, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "requireResolver", name) - - if err != nil { - return *new(AbstractUniversalResolverResolverInfo), err - } - - out0 := *abi.ConvertType(out[0], new(AbstractUniversalResolverResolverInfo)).(*AbstractUniversalResolverResolverInfo) - - return out0, err - -} - -// RequireResolver is a free data retrieval call binding the contract method 0xc285238a. -// -// Solidity: function requireResolver(bytes name) view returns((bytes,uint256,bytes32,address,bool) info) -func (_Contract *ContractSession) RequireResolver(name []byte) (AbstractUniversalResolverResolverInfo, error) { - return _Contract.Contract.RequireResolver(&_Contract.CallOpts, name) -} - -// RequireResolver is a free data retrieval call binding the contract method 0xc285238a. -// -// Solidity: function requireResolver(bytes name) view returns((bytes,uint256,bytes32,address,bool) info) -func (_Contract *ContractCallerSession) RequireResolver(name []byte) (AbstractUniversalResolverResolverInfo, error) { - return _Contract.Contract.RequireResolver(&_Contract.CallOpts, name) -} - // Resolve is a free data retrieval call binding the contract method 0x9061b923. // // Solidity: function resolve(bytes name, bytes data) view returns(bytes, address) @@ -452,201 +212,6 @@ func (_Contract *ContractCallerSession) Resolve(name []byte, data []byte) ([]byt return _Contract.Contract.Resolve(&_Contract.CallOpts, name, data) } -// ResolveBatchCallback is a free data retrieval call binding the contract method 0x491fc4f9. -// -// Solidity: function resolveBatchCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractCaller) ResolveBatchCallback(opts *bind.CallOpts, response []byte, extraData []byte) error { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "resolveBatchCallback", response, extraData) - - if err != nil { - return err - } - - return err - -} - -// ResolveBatchCallback is a free data retrieval call binding the contract method 0x491fc4f9. -// -// Solidity: function resolveBatchCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractSession) ResolveBatchCallback(response []byte, extraData []byte) error { - return _Contract.Contract.ResolveBatchCallback(&_Contract.CallOpts, response, extraData) -} - -// ResolveBatchCallback is a free data retrieval call binding the contract method 0x491fc4f9. -// -// Solidity: function resolveBatchCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractCallerSession) ResolveBatchCallback(response []byte, extraData []byte) error { - return _Contract.Contract.ResolveBatchCallback(&_Contract.CallOpts, response, extraData) -} - -// ResolveCallback is a free data retrieval call binding the contract method 0xb4a85801. -// -// Solidity: function resolveCallback(bytes response, bytes extraData) pure returns(bytes, address) -func (_Contract *ContractCaller) ResolveCallback(opts *bind.CallOpts, response []byte, extraData []byte) ([]byte, common.Address, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "resolveCallback", response, extraData) - - if err != nil { - return *new([]byte), *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new([]byte)).(*[]byte) - out1 := *abi.ConvertType(out[1], new(common.Address)).(*common.Address) - - return out0, out1, err - -} - -// ResolveCallback is a free data retrieval call binding the contract method 0xb4a85801. -// -// Solidity: function resolveCallback(bytes response, bytes extraData) pure returns(bytes, address) -func (_Contract *ContractSession) ResolveCallback(response []byte, extraData []byte) ([]byte, common.Address, error) { - return _Contract.Contract.ResolveCallback(&_Contract.CallOpts, response, extraData) -} - -// ResolveCallback is a free data retrieval call binding the contract method 0xb4a85801. -// -// Solidity: function resolveCallback(bytes response, bytes extraData) pure returns(bytes, address) -func (_Contract *ContractCallerSession) ResolveCallback(response []byte, extraData []byte) ([]byte, common.Address, error) { - return _Contract.Contract.ResolveCallback(&_Contract.CallOpts, response, extraData) -} - -// ResolveDirectCallback is a free data retrieval call binding the contract method 0x55391bb8. -// -// Solidity: function resolveDirectCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractCaller) ResolveDirectCallback(opts *bind.CallOpts, response []byte, extraData []byte) error { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "resolveDirectCallback", response, extraData) - - if err != nil { - return err - } - - return err - -} - -// ResolveDirectCallback is a free data retrieval call binding the contract method 0x55391bb8. -// -// Solidity: function resolveDirectCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractSession) ResolveDirectCallback(response []byte, extraData []byte) error { - return _Contract.Contract.ResolveDirectCallback(&_Contract.CallOpts, response, extraData) -} - -// ResolveDirectCallback is a free data retrieval call binding the contract method 0x55391bb8. -// -// Solidity: function resolveDirectCallback(bytes response, bytes extraData) view returns() -func (_Contract *ContractCallerSession) ResolveDirectCallback(response []byte, extraData []byte) error { - return _Contract.Contract.ResolveDirectCallback(&_Contract.CallOpts, response, extraData) -} - -// ResolveDirectCallbackError is a free data retrieval call binding the contract method 0x3c6cbda8. -// -// Solidity: function resolveDirectCallbackError(bytes response, bytes ) pure returns() -func (_Contract *ContractCaller) ResolveDirectCallbackError(opts *bind.CallOpts, response []byte, arg1 []byte) error { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "resolveDirectCallbackError", response, arg1) - - if err != nil { - return err - } - - return err - -} - -// ResolveDirectCallbackError is a free data retrieval call binding the contract method 0x3c6cbda8. -// -// Solidity: function resolveDirectCallbackError(bytes response, bytes ) pure returns() -func (_Contract *ContractSession) ResolveDirectCallbackError(response []byte, arg1 []byte) error { - return _Contract.Contract.ResolveDirectCallbackError(&_Contract.CallOpts, response, arg1) -} - -// ResolveDirectCallbackError is a free data retrieval call binding the contract method 0x3c6cbda8. -// -// Solidity: function resolveDirectCallbackError(bytes response, bytes ) pure returns() -func (_Contract *ContractCallerSession) ResolveDirectCallbackError(response []byte, arg1 []byte) error { - return _Contract.Contract.ResolveDirectCallbackError(&_Contract.CallOpts, response, arg1) -} - -// ResolveWithGateways is a free data retrieval call binding the contract method 0xa1472844. -// -// Solidity: function resolveWithGateways(bytes name, bytes data, string[] gateways) view returns(bytes result, address resolver) -func (_Contract *ContractCaller) ResolveWithGateways(opts *bind.CallOpts, name []byte, data []byte, gateways []string) (struct { - Result []byte - Resolver common.Address -}, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "resolveWithGateways", name, data, gateways) - - outstruct := new(struct { - Result []byte - Resolver common.Address - }) - if err != nil { - return *outstruct, err - } - - outstruct.Result = *abi.ConvertType(out[0], new([]byte)).(*[]byte) - outstruct.Resolver = *abi.ConvertType(out[1], new(common.Address)).(*common.Address) - - return *outstruct, err - -} - -// ResolveWithGateways is a free data retrieval call binding the contract method 0xa1472844. -// -// Solidity: function resolveWithGateways(bytes name, bytes data, string[] gateways) view returns(bytes result, address resolver) -func (_Contract *ContractSession) ResolveWithGateways(name []byte, data []byte, gateways []string) (struct { - Result []byte - Resolver common.Address -}, error) { - return _Contract.Contract.ResolveWithGateways(&_Contract.CallOpts, name, data, gateways) -} - -// ResolveWithGateways is a free data retrieval call binding the contract method 0xa1472844. -// -// Solidity: function resolveWithGateways(bytes name, bytes data, string[] gateways) view returns(bytes result, address resolver) -func (_Contract *ContractCallerSession) ResolveWithGateways(name []byte, data []byte, gateways []string) (struct { - Result []byte - Resolver common.Address -}, error) { - return _Contract.Contract.ResolveWithGateways(&_Contract.CallOpts, name, data, gateways) -} - -// ResolveWithResolver is a free data retrieval call binding the contract method 0x4a3e3994. -// -// Solidity: function resolveWithResolver(address resolver, bytes name, bytes data, string[] gateways) view returns(bytes) -func (_Contract *ContractCaller) ResolveWithResolver(opts *bind.CallOpts, resolver common.Address, name []byte, data []byte, gateways []string) ([]byte, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "resolveWithResolver", resolver, name, data, gateways) - - if err != nil { - return *new([]byte), err - } - - out0 := *abi.ConvertType(out[0], new([]byte)).(*[]byte) - - return out0, err - -} - -// ResolveWithResolver is a free data retrieval call binding the contract method 0x4a3e3994. -// -// Solidity: function resolveWithResolver(address resolver, bytes name, bytes data, string[] gateways) view returns(bytes) -func (_Contract *ContractSession) ResolveWithResolver(resolver common.Address, name []byte, data []byte, gateways []string) ([]byte, error) { - return _Contract.Contract.ResolveWithResolver(&_Contract.CallOpts, resolver, name, data, gateways) -} - -// ResolveWithResolver is a free data retrieval call binding the contract method 0x4a3e3994. -// -// Solidity: function resolveWithResolver(address resolver, bytes name, bytes data, string[] gateways) view returns(bytes) -func (_Contract *ContractCallerSession) ResolveWithResolver(resolver common.Address, name []byte, data []byte, gateways []string) ([]byte, error) { - return _Contract.Contract.ResolveWithResolver(&_Contract.CallOpts, resolver, name, data, gateways) -} - // Reverse is a free data retrieval call binding the contract method 0x5d78a217. // // Solidity: function reverse(bytes lookupAddress, uint256 coinType) view returns(string, address, address) @@ -679,167 +244,3 @@ func (_Contract *ContractSession) Reverse(lookupAddress []byte, coinType *big.In func (_Contract *ContractCallerSession) Reverse(lookupAddress []byte, coinType *big.Int) (string, common.Address, common.Address, error) { return _Contract.Contract.Reverse(&_Contract.CallOpts, lookupAddress, coinType) } - -// ReverseAddressCallback is a free data retrieval call binding the contract method 0x94fbfa87. -// -// Solidity: function reverseAddressCallback(bytes response, bytes extraData) pure returns(string primary, address resolver, address reverseResolver) -func (_Contract *ContractCaller) ReverseAddressCallback(opts *bind.CallOpts, response []byte, extraData []byte) (struct { - Primary string - Resolver common.Address - ReverseResolver common.Address -}, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "reverseAddressCallback", response, extraData) - - outstruct := new(struct { - Primary string - Resolver common.Address - ReverseResolver common.Address - }) - if err != nil { - return *outstruct, err - } - - outstruct.Primary = *abi.ConvertType(out[0], new(string)).(*string) - outstruct.Resolver = *abi.ConvertType(out[1], new(common.Address)).(*common.Address) - outstruct.ReverseResolver = *abi.ConvertType(out[2], new(common.Address)).(*common.Address) - - return *outstruct, err - -} - -// ReverseAddressCallback is a free data retrieval call binding the contract method 0x94fbfa87. -// -// Solidity: function reverseAddressCallback(bytes response, bytes extraData) pure returns(string primary, address resolver, address reverseResolver) -func (_Contract *ContractSession) ReverseAddressCallback(response []byte, extraData []byte) (struct { - Primary string - Resolver common.Address - ReverseResolver common.Address -}, error) { - return _Contract.Contract.ReverseAddressCallback(&_Contract.CallOpts, response, extraData) -} - -// ReverseAddressCallback is a free data retrieval call binding the contract method 0x94fbfa87. -// -// Solidity: function reverseAddressCallback(bytes response, bytes extraData) pure returns(string primary, address resolver, address reverseResolver) -func (_Contract *ContractCallerSession) ReverseAddressCallback(response []byte, extraData []byte) (struct { - Primary string - Resolver common.Address - ReverseResolver common.Address -}, error) { - return _Contract.Contract.ReverseAddressCallback(&_Contract.CallOpts, response, extraData) -} - -// ReverseNameCallback is a free data retrieval call binding the contract method 0x575de750. -// -// Solidity: function reverseNameCallback(bytes response, bytes extraData) view returns(string primary, address, address) -func (_Contract *ContractCaller) ReverseNameCallback(opts *bind.CallOpts, response []byte, extraData []byte) (string, common.Address, common.Address, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "reverseNameCallback", response, extraData) - - if err != nil { - return *new(string), *new(common.Address), *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(string)).(*string) - out1 := *abi.ConvertType(out[1], new(common.Address)).(*common.Address) - out2 := *abi.ConvertType(out[2], new(common.Address)).(*common.Address) - - return out0, out1, out2, err - -} - -// ReverseNameCallback is a free data retrieval call binding the contract method 0x575de750. -// -// Solidity: function reverseNameCallback(bytes response, bytes extraData) view returns(string primary, address, address) -func (_Contract *ContractSession) ReverseNameCallback(response []byte, extraData []byte) (string, common.Address, common.Address, error) { - return _Contract.Contract.ReverseNameCallback(&_Contract.CallOpts, response, extraData) -} - -// ReverseNameCallback is a free data retrieval call binding the contract method 0x575de750. -// -// Solidity: function reverseNameCallback(bytes response, bytes extraData) view returns(string primary, address, address) -func (_Contract *ContractCallerSession) ReverseNameCallback(response []byte, extraData []byte) (string, common.Address, common.Address, error) { - return _Contract.Contract.ReverseNameCallback(&_Contract.CallOpts, response, extraData) -} - -// ReverseWithGateways is a free data retrieval call binding the contract method 0xb7d6ca64. -// -// Solidity: function reverseWithGateways(bytes lookupAddress, uint256 coinType, string[] gateways) view returns(string primary, address resolver, address reverseResolver) -func (_Contract *ContractCaller) ReverseWithGateways(opts *bind.CallOpts, lookupAddress []byte, coinType *big.Int, gateways []string) (struct { - Primary string - Resolver common.Address - ReverseResolver common.Address -}, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "reverseWithGateways", lookupAddress, coinType, gateways) - - outstruct := new(struct { - Primary string - Resolver common.Address - ReverseResolver common.Address - }) - if err != nil { - return *outstruct, err - } - - outstruct.Primary = *abi.ConvertType(out[0], new(string)).(*string) - outstruct.Resolver = *abi.ConvertType(out[1], new(common.Address)).(*common.Address) - outstruct.ReverseResolver = *abi.ConvertType(out[2], new(common.Address)).(*common.Address) - - return *outstruct, err - -} - -// ReverseWithGateways is a free data retrieval call binding the contract method 0xb7d6ca64. -// -// Solidity: function reverseWithGateways(bytes lookupAddress, uint256 coinType, string[] gateways) view returns(string primary, address resolver, address reverseResolver) -func (_Contract *ContractSession) ReverseWithGateways(lookupAddress []byte, coinType *big.Int, gateways []string) (struct { - Primary string - Resolver common.Address - ReverseResolver common.Address -}, error) { - return _Contract.Contract.ReverseWithGateways(&_Contract.CallOpts, lookupAddress, coinType, gateways) -} - -// ReverseWithGateways is a free data retrieval call binding the contract method 0xb7d6ca64. -// -// Solidity: function reverseWithGateways(bytes lookupAddress, uint256 coinType, string[] gateways) view returns(string primary, address resolver, address reverseResolver) -func (_Contract *ContractCallerSession) ReverseWithGateways(lookupAddress []byte, coinType *big.Int, gateways []string) (struct { - Primary string - Resolver common.Address - ReverseResolver common.Address -}, error) { - return _Contract.Contract.ReverseWithGateways(&_Contract.CallOpts, lookupAddress, coinType, gateways) -} - -// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. -// -// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) -func (_Contract *ContractCaller) SupportsInterface(opts *bind.CallOpts, interfaceId [4]byte) (bool, error) { - var out []interface{} - err := _Contract.contract.Call(opts, &out, "supportsInterface", interfaceId) - - if err != nil { - return *new(bool), err - } - - out0 := *abi.ConvertType(out[0], new(bool)).(*bool) - - return out0, err - -} - -// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. -// -// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) -func (_Contract *ContractSession) SupportsInterface(interfaceId [4]byte) (bool, error) { - return _Contract.Contract.SupportsInterface(&_Contract.CallOpts, interfaceId) -} - -// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. -// -// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) -func (_Contract *ContractCallerSession) SupportsInterface(interfaceId [4]byte) (bool, error) { - return _Contract.Contract.SupportsInterface(&_Contract.CallOpts, interfaceId) -} From aa48e032a6e11defe37dca49201e6d9dd316a315 Mon Sep 17 00:00:00 2001 From: Chris Berry Date: Fri, 3 Jul 2026 17:54:06 +0100 Subject: [PATCH 18/21] Refactor for a V4 alpha. --- .gitignore | 3 + auctionregistrar.go | 2 +- baseregistrar.go | 2 +- ccipread/ccipread_test.go | 2 +- deed.go | 2 +- dnsencode_test.go | 2 +- dnsregistrar.go | 2 +- dnsresolver.go | 2 +- dnssecoracle.go | 2 +- ethcontroller.go | 2 +- go.mod | 2 +- name.go | 24 ++++--- registry.go | 6 +- resolver.go | 53 +++++++------- resolver_test.go | 23 +++--- reverseregistrar.go | 2 +- reverseresolver.go | 28 +++----- reverseresolver_test.go | 5 +- tokenid.go | 5 +- tokenid_test.go | 3 +- universalresolver.go | 119 ++++++++++++++++++++----------- universalresolver_reads.go | 142 +++++++++++++++++++++++++++++++++++++ universalresolver_test.go | 26 +++---- 23 files changed, 321 insertions(+), 138 deletions(-) create mode 100644 universalresolver_reads.go diff --git a/.gitignore b/.gitignore index 8d09c99..72c18f8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ coverage.html # Local TODO TODO.md + +# IntelliJ + friends. +.idea/ \ No newline at end of file diff --git a/auctionregistrar.go b/auctionregistrar.go index 552c4f2..adac7df 100644 --- a/auctionregistrar.go +++ b/auctionregistrar.go @@ -22,7 +22,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/auctionregistrar" + "github.com/wealdtech/go-ens/v4/contracts/auctionregistrar" ) // AuctionRegistrar is the structure for the auction registrar contract. diff --git a/baseregistrar.go b/baseregistrar.go index 07a5de1..b016b44 100644 --- a/baseregistrar.go +++ b/baseregistrar.go @@ -23,7 +23,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/baseregistrar" + "github.com/wealdtech/go-ens/v4/contracts/baseregistrar" "golang.org/x/crypto/sha3" ) diff --git a/ccipread/ccipread_test.go b/ccipread/ccipread_test.go index 81a96af..2842f77 100644 --- a/ccipread/ccipread_test.go +++ b/ccipread/ccipread_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/wealdtech/go-ens/v3/ccipread" + "github.com/wealdtech/go-ens/v4/ccipread" ) // --------------------------------------------------------------------------- diff --git a/deed.go b/deed.go index 72b5544..14c7310 100644 --- a/deed.go +++ b/deed.go @@ -18,7 +18,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/deed" + "github.com/wealdtech/go-ens/v4/contracts/deed" ) // Deed is the structure for the deed. diff --git a/dnsencode_test.go b/dnsencode_test.go index 0c6dc23..1d2a52f 100644 --- a/dnsencode_test.go +++ b/dnsencode_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - ens "github.com/wealdtech/go-ens/v3" + ens "github.com/wealdtech/go-ens/v4" ) // --------------------------------------------------------------------------- diff --git a/dnsregistrar.go b/dnsregistrar.go index 1dfa50e..a0d36e8 100644 --- a/dnsregistrar.go +++ b/dnsregistrar.go @@ -19,7 +19,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/wealdtech/go-ens/v3/contracts/dnsregistrar" + "github.com/wealdtech/go-ens/v4/contracts/dnsregistrar" ) // DNSRegistrar is the structure for the registrar. diff --git a/dnsresolver.go b/dnsresolver.go index d6b3ba4..78201be 100644 --- a/dnsresolver.go +++ b/dnsresolver.go @@ -21,7 +21,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/dnsresolver" + "github.com/wealdtech/go-ens/v4/contracts/dnsresolver" "golang.org/x/crypto/sha3" ) diff --git a/dnssecoracle.go b/dnssecoracle.go index be9daf8..4d5e308 100644 --- a/dnssecoracle.go +++ b/dnssecoracle.go @@ -17,7 +17,7 @@ package ens import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/wealdtech/go-ens/v3/contracts/dnssecoracle" + "github.com/wealdtech/go-ens/v4/contracts/dnssecoracle" ) // DNSSECOracle is the structure for the DNSSEC oracle. diff --git a/ethcontroller.go b/ethcontroller.go index 2b30460..ebd908f 100644 --- a/ethcontroller.go +++ b/ethcontroller.go @@ -23,7 +23,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/ethcontroller" + "github.com/wealdtech/go-ens/v4/contracts/ethcontroller" ) // ETHController is the structure for the .eth controller contract. diff --git a/go.mod b/go.mod index edd960b..2ab2bb7 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/wealdtech/go-ens/v3 +module github.com/wealdtech/go-ens/v4 go 1.22.0 diff --git a/name.go b/name.go index 7cf9d02..d1eb145 100644 --- a/name.go +++ b/name.go @@ -15,6 +15,7 @@ package ens import ( + "context" "crypto/rand" "errors" "fmt" @@ -296,18 +297,23 @@ func (n *Name) SetResolverAddress(address common.Address, opts *bind.TransactOpt // Address fetches the address of the name for a given coin type. // Coin types are defined at https://github.com/satoshilabs/slips/blob/master/slip-0044.md // -// Note: this calls the resolver contract directly via the legacy registry -// walk, so it does NOT support ENSIP-10 wildcard resolution or ERC-3668 -// CCIP-Read. Names whose addresses live off-chain or on an L2 will return -// nil or an error here. For wildcard- and CCIP-Read-aware resolution, use -// ens.Resolve / UniversalResolver.ResolveAddress for coin type 60 (ETH); -// a UR-routed multicoin read is tracked as a follow-up. -func (n *Name) Address(coinType uint64) ([]byte, error) { - resolver, err := NewResolver(n.backend, n.Name) +// Resolution is delegated to the UniversalResolver, so it follows ENSIP-10 +// wildcard resolution and ERC-3668 CCIP-Read and returns the same answer as +// ens.Resolve for coin type 60. ctx is honoured for cancellation and deadline +// propagation. +func (n *Name) Address(ctx context.Context, coinType uint64) ([]byte, error) { + ur, err := NewUniversalResolver(ctx, n.backend) if err != nil { return nil, err } - return resolver.MultiAddress(coinType) + if coinType == CoinTypeETH { + addr, err := ur.ResolveAddress(ctx, n.Name) + if err != nil { + return nil, err + } + return addr.Bytes(), nil + } + return ur.MultiAddress(ctx, n.Name, coinType) } // SetAddress sets the address of the name for a given coin type. diff --git a/registry.go b/registry.go index 9251e50..96d8100 100644 --- a/registry.go +++ b/registry.go @@ -22,9 +22,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/auctionregistrar" - "github.com/wealdtech/go-ens/v3/contracts/registry" - "github.com/wealdtech/go-ens/v3/util" + "github.com/wealdtech/go-ens/v4/contracts/auctionregistrar" + "github.com/wealdtech/go-ens/v4/contracts/registry" + "github.com/wealdtech/go-ens/v4/util" ) // Registry is the structure for the registry contract. diff --git a/resolver.go b/resolver.go index 6b22e29..b1d95b9 100644 --- a/resolver.go +++ b/resolver.go @@ -26,7 +26,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/resolver" + "github.com/wealdtech/go-ens/v4/contracts/resolver" ) // UnknownAddress is the address to which unknown entries resolve. @@ -91,17 +91,18 @@ func NewResolverAt(backend bind.ContractBackend, domain string, address common.A } // PublicResolverAddress obtains the address of the public resolver for a chain. -func PublicResolverAddress(backend bind.ContractBackend) (common.Address, error) { - return Resolve(backend, "resolver.eth") +func PublicResolverAddress(ctx context.Context, backend bind.ContractBackend) (common.Address, error) { + return Resolve(ctx, backend, "resolver.eth") } // Address returns the Ethereum address of the domain. // -// Note: this calls the resolver contract directly via the legacy registry -// walk, so it does NOT support ENSIP-10 wildcard resolution or ERC-3668 -// CCIP-Read. Names whose addresses live off-chain or on an L2 will return -// the zero address or an error here. For wildcard- and CCIP-Read-aware -// resolution, use ens.Resolve / UniversalResolver.ResolveAddress instead. +// This reads the addr(bytes32) record directly from the resolver contract this +// Resolver is bound to (located via the registry when the Resolver was +// created). It does NOT perform ENSIP-10 wildcard resolution or follow ERC-3668 +// CCIP-Read, so names whose addresses live off-chain or on an L2 return the +// zero address or a raw revert error here. For wildcard- and CCIP-Read-aware +// resolution use Resolve or UniversalResolver.ResolveAddress instead. func (r *Resolver) Address() (common.Address, error) { nameHash, err := NameHash(r.domain) if err != nil { @@ -185,27 +186,23 @@ func (r *Resolver) InterfaceImplementer(interfaceID [4]byte) (common.Address, er return r.Contract.InterfaceImplementer(nil, nameHash, interfaceID) } -// Resolve resolves an ENS name in to an Ethereum address. Resolution flows -// through the ENS UniversalResolver, which means CCIP-Read (ERC-3668) is -// followed transparently — names backed by offchain or L2 data resolve the -// same way as fully on-chain names. An input that contains no dot is treated -// as a literal hex address. +// Resolve resolves an ENS name into an Ethereum address, or parses input +// directly if it is already a hex address. // -// This will return an error if the name is not found or otherwise resolves -// to the zero address. +// If input contains a dot it is treated as an ENS name and resolved through +// the ENS UniversalResolver, so ENSIP-10 wildcard resolution and ERC-3668 +// CCIP-Read are followed transparently — names backed by offchain or L2 data +// resolve the same way as fully on-chain names. If input contains no dot it is +// treated as a literal hex address and returned after validation, without any +// on-chain lookup. // -// Resolve uses a background context, so callers cannot cancel a slow -// CCIP-Read hop or propagate an HTTP-handler deadline. Use ResolveContext -// when either is needed. -func Resolve(backend bind.ContractBackend, input string) (common.Address, error) { - return ResolveContext(context.Background(), backend, input) -} - -// ResolveContext is identical to Resolve but honours ctx for cancellation -// and deadline propagation. The context flows through CCIP-Read hops, so a -// caller that sets a per-request deadline can cap the total resolution -// time end-to-end. -func ResolveContext(ctx context.Context, backend bind.ContractBackend, input string) (common.Address, error) { +// ctx is honoured for cancellation and deadline propagation, flowing through +// the CCIP-Read hops, so a caller that sets a per-request deadline can cap the +// total resolution time end-to-end. +// +// It returns an error if the name is not found or otherwise resolves to the +// zero address. +func Resolve(ctx context.Context, backend bind.ContractBackend, input string) (common.Address, error) { if strings.Contains(input, ".") { return resolveName(ctx, backend, input) } @@ -224,7 +221,7 @@ func resolveName(ctx context.Context, backend bind.ContractBackend, input string if _, err := NameHash(input); err != nil { return UnknownAddress, err } - ur, err := NewUniversalResolverContext(ctx, backend) + ur, err := NewUniversalResolver(ctx, backend) if err != nil { return UnknownAddress, err } diff --git a/resolver_test.go b/resolver_test.go index 07358ca..4205ded 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -15,6 +15,7 @@ package ens import ( + "context" "encoding/hex" "testing" @@ -27,70 +28,70 @@ import ( var client, _ = ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") func TestResolveEmpty(t *testing.T) { - _, err := Resolve(client, "") + _, err := Resolve(context.Background(), client, "") assert.NotNil(t, err, "Resolved empty name") } func TestResolveZero(t *testing.T) { - _, err := Resolve(client, "0") + _, err := Resolve(context.Background(), client, "0") assert.NotNil(t, err, "Resolved empty name") } func TestResolveNotPresent(t *testing.T) { - _, err := Resolve(client, "sirnotappearinginthisregistry.eth") + _, err := Resolve(context.Background(), client, "sirnotappearinginthisregistry.eth") require.NotNil(t, err, "Resolved name that does not exist") assert.Equal(t, "unregistered name", err.Error(), "Unexpected error") } // func TestResolveNoResolver(t *testing.T) { -// _, err := Resolve(client, "noresolver.eth") +// _, err := Resolve(context.Background(), client, "noresolver.eth") // require.NotNil(t, err, "Resolved name without a resolver") // assert.Equal(t, "no resolver", err.Error(), "Unexpected error") // } func TestResolveBadResolver(t *testing.T) { - _, err := Resolve(client, "resolvestozero.eth") + _, err := Resolve(context.Background(), client, "resolvestozero.eth") require.NotNil(t, err, "Resolved name with a bad resolver") assert.Equal(t, "no address", err.Error(), "Unexpected error") } func TestResolveTestEnsTest(t *testing.T) { expected := "ed96dd3be847b387217ef9de5b20d8392a6cdf40" - actual, err := Resolve(client, "test.enstest.eth") + actual, err := Resolve(context.Background(), client, "test.enstest.eth") require.Nil(t, err, "Error resolving name") assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") } func TestResolveResolverEth(t *testing.T) { expected := "231b0ee14048e9dccd1d247744d114a4eb5e8e63" - actual, err := Resolve(client, "resolver.eth") + actual, err := Resolve(context.Background(), client, "resolver.eth") require.Nil(t, err, "Error resolving name") assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") } func TestResolveAddress(t *testing.T) { expected := "b8c2c29ee19d8307cb7255e1cd9cbde883a267d5" - actual, err := Resolve(client, "0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5") + actual, err := Resolve(context.Background(), client, "0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5") require.Nil(t, err, "Error resolving address") assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") } func TestResolveShortAddress(t *testing.T) { expected := "0000000000000000000000000000000000000001" - actual, err := Resolve(client, "0x1") + actual, err := Resolve(context.Background(), client, "0x1") require.Nil(t, err, "Error resolving address") assert.Equal(t, expected, hex.EncodeToString(actual[:]), "Did not receive expected result") } func TestResolveHexString(t *testing.T) { - _, err := Resolve(client, "0xe32c6d1a964749b6de2130e20daed821a45b9e7261118801ff5319d0ffc6b54a") + _, err := Resolve(context.Background(), client, "0xe32c6d1a964749b6de2130e20daed821a45b9e7261118801ff5319d0ffc6b54a") assert.NotNil(t, err, "Resolved too-long hex string") } func TestReverseResolveTestEnsTest(t *testing.T) { expected := "nick.eth" address := common.HexToAddress("b8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5") - actual, err := ReverseResolve(client, address) + actual, err := ReverseResolve(context.Background(), client, address) require.Nil(t, err, "Error resolving address") assert.Equal(t, expected, actual, "Did not receive expected result") } diff --git a/reverseregistrar.go b/reverseregistrar.go index bee1aeb..b6670c2 100644 --- a/reverseregistrar.go +++ b/reverseregistrar.go @@ -20,7 +20,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/wealdtech/go-ens/v3/contracts/reverseregistrar" + "github.com/wealdtech/go-ens/v4/contracts/reverseregistrar" ) // ReverseRegistrar is the structure for the reverse registrar. diff --git a/reverseresolver.go b/reverseresolver.go index 692adcd..60cdc3a 100644 --- a/reverseresolver.go +++ b/reverseresolver.go @@ -21,7 +21,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/wealdtech/go-ens/v3/contracts/reverseresolver" + "github.com/wealdtech/go-ens/v4/contracts/reverseresolver" ) // ReverseResolver is the structure for the reverse resolver contract. @@ -96,8 +96,8 @@ func (r *ReverseResolver) Name(address common.Address) (string, error) { } // Format provides a string version of an address, reverse resolving it if possible. -func Format(backend bind.ContractBackend, address common.Address) string { - result, err := ReverseResolve(backend, address) +func Format(ctx context.Context, backend bind.ContractBackend, address common.Address) string { + result, err := ReverseResolve(ctx, backend, address) if err != nil { result = address.Hex() } @@ -105,22 +105,16 @@ func Format(backend bind.ContractBackend, address common.Address) string { } // ReverseResolve resolves an address in to an ENS name. Resolution flows -// through the ENS UniversalResolver, so CCIP-Read (ERC-3668) is handled -// transparently and reverse records stored offchain or on an L2 resolve the -// same way as fully on-chain reverse records. +// through the ENS UniversalResolver, so ENSIP-10 wildcard resolution and +// ERC-3668 CCIP-Read are handled transparently and reverse records stored +// offchain or on an L2 resolve the same way as fully on-chain reverse records. // -// This will return an error if no primary name is set for the address. +// ctx is honoured for cancellation and deadline propagation through the +// CCIP-Read hops. // -// ReverseResolve uses a background context. Use ReverseResolveContext to -// propagate cancellation and deadlines through CCIP-Read hops. -func ReverseResolve(backend bind.ContractBackend, address common.Address) (string, error) { - return ReverseResolveContext(context.Background(), backend, address) -} - -// ReverseResolveContext is identical to ReverseResolve but honours ctx for -// cancellation and deadline propagation. -func ReverseResolveContext(ctx context.Context, backend bind.ContractBackend, address common.Address) (string, error) { - ur, err := NewUniversalResolverContext(ctx, backend) +// This will return an error if no primary name is set for the address. +func ReverseResolve(ctx context.Context, backend bind.ContractBackend, address common.Address) (string, error) { + ur, err := NewUniversalResolver(ctx, backend) if err != nil { return "", err } diff --git a/reverseresolver_test.go b/reverseresolver_test.go index 5ed2d75..75bda4d 100644 --- a/reverseresolver_test.go +++ b/reverseresolver_test.go @@ -15,12 +15,13 @@ package ens_test import ( + "context" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" - ens "github.com/wealdtech/go-ens/v3" + ens "github.com/wealdtech/go-ens/v4" ) // TestReverseResolve tests the reverse resolution functionality. @@ -55,7 +56,7 @@ func TestReverseResolve(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - res, err := ens.ReverseResolve(client, test.address) + res, err := ens.ReverseResolve(context.Background(), client, test.address) if test.err != "" { require.EqualError(t, err, test.err) } else { diff --git a/tokenid.go b/tokenid.go index 25cc5a5..a2ea7a7 100644 --- a/tokenid.go +++ b/tokenid.go @@ -15,6 +15,7 @@ package ens import ( + "context" "errors" "fmt" @@ -25,11 +26,11 @@ import ( // DeriveTokenID derive tokenID from the ENS domain. // // The tokenID of the ENS name is simply the uint256 representation of the tokenID of ERC721. -func DeriveTokenID(backend bind.ContractBackend, domain string) (string, error) { +func DeriveTokenID(ctx context.Context, backend bind.ContractBackend, domain string) (string, error) { if domain == "" { return "", errors.New("empty domain") } - _, err := Resolve(backend, domain) + _, err := Resolve(ctx, backend, domain) if err != nil { return "", err } diff --git a/tokenid_test.go b/tokenid_test.go index 202ba92..bde1500 100644 --- a/tokenid_test.go +++ b/tokenid_test.go @@ -1,6 +1,7 @@ package ens import ( + "context" "testing" "github.com/ethereum/go-ethereum/ethclient" @@ -39,7 +40,7 @@ func TestDeriveTokenId(t *testing.T) { require.NoError(t, err) for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual, err := DeriveTokenID(client, test.input) + actual, err := DeriveTokenID(context.Background(), client, test.input) if test.err != "" { require.EqualError(t, err, test.err) } else { diff --git a/universalresolver.go b/universalresolver.go index 9935aef..7ae4c48 100644 --- a/universalresolver.go +++ b/universalresolver.go @@ -13,12 +13,13 @@ import ( "errors" "fmt" "math/big" + "net/http" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/wealdtech/go-ens/v3/ccipread" - "github.com/wealdtech/go-ens/v3/contracts/universalresolver" + "github.com/wealdtech/go-ens/v4/ccipread" + "github.com/wealdtech/go-ens/v4/contracts/universalresolver" ) // UniversalResolverContractAddress is the proxy address of the ENS @@ -35,58 +36,85 @@ const CoinTypeETH = uint64(60) var addrSelector = [4]byte{0x3b, 0x3b, 0x57, 0xde} // knownURChains lists the chain IDs on which the canonical UR proxy address -// is deployed by the ENS DAO. Callers on other chains must pass an explicit -// address via NewUniversalResolverAt. +// is deployed by the ENS DAO. Callers on other chains must supply an explicit +// address via WithAddress. var knownURChains = map[uint64]struct{}{ 1: {}, // Ethereum mainnet 11155111: {}, // Sepolia testnet 17000: {}, // Holesky testnet } -// UnknownChainError is returned by NewUniversalResolver and -// NewUniversalResolverContext when the backend reports a chain ID without a -// known UR deployment at the canonical address. +// UnknownChainError is returned by NewUniversalResolver when the backend +// reports a chain ID without a known UR deployment at the canonical address. type UnknownChainError struct { ChainID *big.Int } func (e *UnknownChainError) Error() string { - return fmt.Sprintf("universal resolver: no canonical deployment on chain ID %s; pass an explicit address via NewUniversalResolverAt", e.ChainID) + return fmt.Sprintf("universal resolver: no canonical deployment on chain ID %s; supply an explicit address with WithAddress", e.ChainID) +} + +// Parameter is a functional option for NewUniversalResolver. +type Parameter func(*parameters) + +type parameters struct { + address *common.Address + httpClient *http.Client + maxHops int +} + +// WithAddress binds the resolver to an explicit UniversalResolver address +// instead of the canonical proxy. Use this on chains without a canonical +// deployment (devnets, forks, alt-L1s); it also skips the chain-ID guardrail. +func WithAddress(address common.Address) Parameter { + return func(p *parameters) { p.address = &address } +} + +// WithHTTPClient sets the HTTP client used for ERC-3668 CCIP-Read gateway +// requests. When unset, a default client is used. +func WithHTTPClient(client *http.Client) Parameter { + return func(p *parameters) { p.httpClient = client } +} + +// WithMaxHops caps the number of OffchainLookup redirects a single resolution +// may chain. When unset, the ccipread package default applies. +func WithMaxHops(hops int) Parameter { + return func(p *parameters) { p.maxHops = hops } } // UniversalResolver wraps the ENS Universal Resolver and exposes resolution // helpers that transparently follow ERC-3668 OffchainLookup reverts via the // ccipread package. type UniversalResolver struct { - backend bind.ContractBackend - addr common.Address - abi abi.ABI + backend bind.ContractBackend + addr common.Address + abi abi.ABI + httpClient *http.Client + maxHops int } -// NewUniversalResolver returns a UniversalResolver bound to the canonical -// proxy address on whichever chain the backend is connected to. -// -// When the backend exposes a chain ID (ethclient.Client and most production -// backends do) this verifies the chain is one where the canonical UR is -// deployed and returns *UnknownChainError otherwise. Backends that don't -// expose a chain ID skip the check and the canonical address is used as-is — -// callers on devnets / forks / alt-L1s should prefer NewUniversalResolverAt. +// NewUniversalResolver returns a UniversalResolver for the chain reached +// through backend. It honours ctx for the chain-ID probe, so a cancelled or +// expired ctx surfaces as ctx.Err(). // -// The chain-ID probe uses a background context, so a caller-supplied context -// cannot cancel it. Use NewUniversalResolverContext when the caller needs to -// propagate cancellation / deadlines into the probe (e.g. via ResolveContext -// or ReverseResolveContext). -func NewUniversalResolver(backend bind.ContractBackend) (*UniversalResolver, error) { - return NewUniversalResolverContext(context.Background(), backend) -} - -// NewUniversalResolverContext is identical to NewUniversalResolver but honours -// ctx for the chain-ID probe: if ctx is cancelled before the backend reports -// its chain ID, the probe returns ctx.Err() instead of *UnknownChainError. -func NewUniversalResolverContext(ctx context.Context, backend bind.ContractBackend) (*UniversalResolver, error) { - if cid, ok := backend.(interface { +// By default it binds to the canonical proxy address and, when the backend +// exposes a chain ID (ethclient.Client and most production backends do), +// verifies the chain has a known UR deployment — returning *UnknownChainError +// otherwise. Supply WithAddress to target a non-standard deployment (devnet, +// fork, alt-L1), which also skips the chain-ID guardrail. +func NewUniversalResolver(ctx context.Context, backend bind.ContractBackend, params ...Parameter) (*UniversalResolver, error) { + var p parameters + for _, param := range params { + param(&p) + } + + addr := common.HexToAddress(UniversalResolverContractAddress) + if p.address != nil { + addr = *p.address + } else if cid, ok := backend.(interface { ChainID(context.Context) (*big.Int, error) }); ok { + // Only guard the canonical address; an explicit WithAddress opts out. chainID, err := cid.ChainID(ctx) if err == nil && chainID != nil { if _, known := knownURChains[chainID.Uint64()]; !known { @@ -97,18 +125,27 @@ func NewUniversalResolverContext(ctx context.Context, backend bind.ContractBacke return nil, ctx.Err() } } - return NewUniversalResolverAt(backend, common.HexToAddress(UniversalResolverContractAddress)) -} -// NewUniversalResolverAt returns a UniversalResolver bound to a specific -// address. Use this when targeting a non-standard deployment, e.g. a local -// fork. -func NewUniversalResolverAt(backend bind.ContractBackend, addr common.Address) (*UniversalResolver, error) { parsed, err := universalresolver.ContractMetaData.GetAbi() if err != nil { return nil, fmt.Errorf("parse universal resolver abi: %w", err) } - return &UniversalResolver{backend: backend, addr: addr, abi: *parsed}, nil + return &UniversalResolver{ + backend: backend, + addr: addr, + abi: *parsed, + httpClient: p.httpClient, + maxHops: p.maxHops, + }, nil +} + +// callOptions builds the ccipread options from the resolver's configuration, +// returning nil when nothing has been customised so ccipread uses its defaults. +func (u *UniversalResolver) callOptions() *ccipread.Options { + if u.httpClient == nil && u.maxHops == 0 { + return nil + } + return &ccipread.Options{HTTPClient: u.httpClient, MaxRedirects: u.maxHops} } // Address returns the on-chain address of the bound UniversalResolver. @@ -130,7 +167,7 @@ func (u *UniversalResolver) Resolve(ctx context.Context, name string, callData [ if err != nil { return nil, common.Address{}, fmt.Errorf("pack resolve: %w", err) } - out, err := ccipread.Call(ctx, u.backend, u.addr, input, nil) + out, err := ccipread.Call(ctx, u.backend, u.addr, input, u.callOptions()) if err != nil { return nil, common.Address{}, translateURRevert(err) } @@ -188,7 +225,7 @@ func (u *UniversalResolver) Reverse(ctx context.Context, address []byte, coinTyp if err != nil { return "", common.Address{}, common.Address{}, fmt.Errorf("pack reverse: %w", err) } - out, err := ccipread.Call(ctx, u.backend, u.addr, input, nil) + out, err := ccipread.Call(ctx, u.backend, u.addr, input, u.callOptions()) if err != nil { return "", common.Address{}, common.Address{}, translateURRevert(err) } diff --git a/universalresolver_reads.go b/universalresolver_reads.go new file mode 100644 index 0000000..a7bb530 --- /dev/null +++ b/universalresolver_reads.go @@ -0,0 +1,142 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/wealdtech/go-ens/v4/contracts/resolver" +) + +// resolverABI is the ABI of the legacy PublicResolver, used to pack the +// calldata for individual resolver profiles (text, contenthash, multicoin +// addr) that UniversalResolver.resolve forwards to the discovered resolver. +// Parsed once at init; the profile signatures are stable across resolver +// versions. +var resolverABI abi.ABI + +func init() { + parsed, err := resolver.ContractMetaData.GetAbi() + if err != nil { + // The binding ABI is a compile-time constant string; a parse failure + // here means the generated binding is broken, which is a build-level + // bug rather than a runtime condition. + panic(fmt.Sprintf("ens: parse resolver abi: %v", err)) + } + resolverABI = *parsed +} + +// Text resolves a text record for a name through the UniversalResolver. Unlike +// Resolver.Text, this follows ENSIP-10 wildcard resolution and ERC-3668 +// CCIP-Read, so offchain and L2 text records resolve correctly. +// +// This is the read half of the unified read path: it builds calldata for the +// resolver's text(bytes32,string) profile and hands it to the same resolve() +// choke point as Address and Contenthash, so wildcard/CCIP behaviour is +// identical across every profile. +func (u *UniversalResolver) Text(ctx context.Context, name, key string) (string, error) { + nameHash, err := NameHash(name) + if err != nil { + return "", err + } + callData, err := resolverABI.Pack("text", nameHash, key) + if err != nil { + return "", fmt.Errorf("pack text: %w", err) + } + result, _, err := u.Resolve(ctx, name, callData) + if err != nil { + return "", err + } + if len(result) == 0 { + return "", nil + } + values, err := resolverABI.Unpack("text", result) + if err != nil { + return "", fmt.Errorf("unpack text: %w", err) + } + if len(values) != 1 { + return "", fmt.Errorf("universal resolver: unexpected text output arity") + } + out, ok := values[0].(string) + if !ok { + return "", fmt.Errorf("universal resolver: text result has unexpected type %T", values[0]) + } + return out, nil +} + +// Contenthash resolves the contenthash record for a name through the +// UniversalResolver, following ENSIP-10 wildcard resolution and ERC-3668 +// CCIP-Read. +func (u *UniversalResolver) Contenthash(ctx context.Context, name string) ([]byte, error) { + nameHash, err := NameHash(name) + if err != nil { + return nil, err + } + callData, err := resolverABI.Pack("contenthash", nameHash) + if err != nil { + return nil, fmt.Errorf("pack contenthash: %w", err) + } + result, _, err := u.Resolve(ctx, name, callData) + if err != nil { + return nil, err + } + if len(result) == 0 { + return nil, nil + } + values, err := resolverABI.Unpack("contenthash", result) + if err != nil { + return nil, fmt.Errorf("unpack contenthash: %w", err) + } + if len(values) != 1 { + return nil, fmt.Errorf("universal resolver: unexpected contenthash output arity") + } + out, ok := values[0].([]byte) + if !ok { + return nil, fmt.Errorf("universal resolver: contenthash result has unexpected type %T", values[0]) + } + return out, nil +} + +// MultiAddress resolves the address of a name for an arbitrary SLIP-0044 coin +// type through the UniversalResolver, following ENSIP-10 wildcard resolution +// and ERC-3668 CCIP-Read. It invokes the resolver's addr(bytes32,uint256) +// profile. For coin type 60 (ETH) prefer ResolveAddress, which returns a typed +// common.Address. +func (u *UniversalResolver) MultiAddress(ctx context.Context, name string, coinType uint64) ([]byte, error) { + nameHash, err := NameHash(name) + if err != nil { + return nil, err + } + callData, err := resolverABI.Pack("addr0", nameHash, new(big.Int).SetUint64(coinType)) + if err != nil { + return nil, fmt.Errorf("pack addr(bytes32,uint256): %w", err) + } + result, _, err := u.Resolve(ctx, name, callData) + if err != nil { + return nil, err + } + if len(result) == 0 { + return nil, ErrNoAddress + } + values, err := resolverABI.Unpack("addr0", result) + if err != nil { + return nil, fmt.Errorf("unpack addr(bytes32,uint256): %w", err) + } + if len(values) != 1 { + return nil, fmt.Errorf("universal resolver: unexpected addr output arity") + } + out, ok := values[0].([]byte) + if !ok { + return nil, fmt.Errorf("universal resolver: addr result has unexpected type %T", values[0]) + } + return out, nil +} diff --git a/universalresolver_test.go b/universalresolver_test.go index 9e2e583..a2f41d6 100644 --- a/universalresolver_test.go +++ b/universalresolver_test.go @@ -19,7 +19,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" - ens "github.com/wealdtech/go-ens/v3" + ens "github.com/wealdtech/go-ens/v4" ) // chainIDBackend embeds a nil ContractBackend and overrides ChainID, so the @@ -35,30 +35,30 @@ func (b *chainIDBackend) ChainID(_ context.Context) (*big.Int, error) { } func TestNewUniversalResolver_AcceptsMainnet(t *testing.T) { - ur, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(1)}) + ur, err := ens.NewUniversalResolver(context.Background(), &chainIDBackend{chainID: big.NewInt(1)}) require.NoError(t, err) require.NotNil(t, ur) } func TestNewUniversalResolver_AcceptsSepolia(t *testing.T) { - ur, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(11155111)}) + ur, err := ens.NewUniversalResolver(context.Background(), &chainIDBackend{chainID: big.NewInt(11155111)}) require.NoError(t, err) require.NotNil(t, ur) } func TestNewUniversalResolver_AcceptsHolesky(t *testing.T) { - ur, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(17000)}) + ur, err := ens.NewUniversalResolver(context.Background(), &chainIDBackend{chainID: big.NewInt(17000)}) require.NoError(t, err) require.NotNil(t, ur) } func TestNewUniversalResolver_RejectsUnknownChain(t *testing.T) { - _, err := ens.NewUniversalResolver(&chainIDBackend{chainID: big.NewInt(8453)}) // Base + _, err := ens.NewUniversalResolver(context.Background(), &chainIDBackend{chainID: big.NewInt(8453)}) // Base require.Error(t, err) var typed *ens.UnknownChainError require.ErrorAs(t, err, &typed) require.Equal(t, int64(8453), typed.ChainID.Int64()) - require.Contains(t, err.Error(), "NewUniversalResolverAt") + require.Contains(t, err.Error(), "WithAddress") } // NewUniversalResolverAt skips the chain-ID check by design — useful for @@ -66,7 +66,7 @@ func TestNewUniversalResolver_RejectsUnknownChain(t *testing.T) { // address. func TestNewUniversalResolverAt_AcceptsAnyChain(t *testing.T) { custom := common.HexToAddress("0x000000000000000000000000000000000000bEEF") - ur, err := ens.NewUniversalResolverAt(&chainIDBackend{chainID: big.NewInt(8453)}, custom) + ur, err := ens.NewUniversalResolver(context.Background(), &chainIDBackend{chainID: big.NewInt(8453)}, ens.WithAddress(custom)) require.NoError(t, err) require.Equal(t, custom, ur.Address()) } @@ -101,7 +101,7 @@ func TestNewUniversalResolverContext_PropagatesCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - _, err := ens.NewUniversalResolverContext(ctx, backend) + _, err := ens.NewUniversalResolver(ctx, backend) require.ErrorIs(t, err, context.Canceled, "a cancelled ctx must surface as context.Canceled from the chain-ID probe") } @@ -116,7 +116,7 @@ func TestNewUniversalResolverContext_HonoursDeadline(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 0) defer cancel() - _, err := ens.NewUniversalResolverContext(ctx, backend) + _, err := ens.NewUniversalResolver(ctx, backend) require.ErrorIs(t, err, context.DeadlineExceeded, "an expired deadline must surface as context.DeadlineExceeded from the chain-ID probe") } @@ -124,7 +124,7 @@ func TestNewUniversalResolverContext_HonoursDeadline(t *testing.T) { // TestNewUniversalResolverContext_AcceptsKnownChain ensures the context-aware // constructor still returns a usable resolver on the happy path. func TestNewUniversalResolverContext_AcceptsKnownChain(t *testing.T) { - ur, err := ens.NewUniversalResolverContext(context.Background(), &chainIDBackend{chainID: big.NewInt(1)}) + ur, err := ens.NewUniversalResolver(context.Background(), &chainIDBackend{chainID: big.NewInt(1)}) require.NoError(t, err) require.NotNil(t, ur) } @@ -163,7 +163,7 @@ func TestUniversalResolverIntegrationName(t *testing.T) { ) client := mainnetClient(t) - addr, err := ens.Resolve(client, name) + addr, err := ens.Resolve(context.Background(), client, name) require.NoError(t, err, "Resolve(%s) failed", name) got := strings.ToLower(addr.Hex()) @@ -183,7 +183,7 @@ func TestUniversalResolverCCIPRead(t *testing.T) { ) client := mainnetClient(t) - addr, err := ens.Resolve(client, name) + addr, err := ens.Resolve(context.Background(), client, name) require.NoError(t, err, "Resolve(%s) failed", name) require.Equal(t, strings.ToLower(expected), strings.ToLower(addr.Hex())) } @@ -193,7 +193,7 @@ func TestUniversalResolverCCIPRead(t *testing.T) { // produces the same result as the top-level Resolve entrypoint. func TestUniversalResolverDirectResolve(t *testing.T) { client := mainnetClient(t) - ur, err := ens.NewUniversalResolver(client) + ur, err := ens.NewUniversalResolver(context.Background(), client) require.NoError(t, err) addr, err := ur.ResolveAddress(context.Background(), "ur.integration-tests.eth") From 1d5f74dab6f145261f04812c4febc70a65c23c16 Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 1 Aug 2026 02:24:30 +0530 Subject: [PATCH 19/21] Add mainnetClient function for consistent Ethereum mainnet RPC connection - Introduced mainnetClient function in mainnet_client_ext_test.go and mainnet_client_test.go to streamline the connection to a public Ethereum mainnet RPC. - Updated existing tests to utilize mainnetClient instead of hardcoded Infura URLs, enhancing test reliability and configurability. - Ensured live-RPC tests can be skipped under `go test -short` to maintain CI compatibility. - Removed redundant client connection code from various test files, promoting code reuse and clarity. --- mainnet_client_ext_test.go | 39 ++++++++++++++++++++++++++++++++++++++ mainnet_client_test.go | 39 ++++++++++++++++++++++++++++++++++++++ name_test.go | 38 ++++++++++++++++++------------------- resolver_test.go | 14 +++++++++++--- reverseresolver_test.go | 4 +--- tokenid_test.go | 4 +--- universalresolver_test.go | 24 ----------------------- 7 files changed, 110 insertions(+), 52 deletions(-) create mode 100644 mainnet_client_ext_test.go create mode 100644 mainnet_client_test.go diff --git a/mainnet_client_ext_test.go b/mainnet_client_ext_test.go new file mode 100644 index 0000000..d4be537 --- /dev/null +++ b/mainnet_client_ext_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens_test + +import ( + "os" + "testing" + + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +// mainnetClient connects to a public Ethereum mainnet RPC. The endpoint can +// be overridden with GO_ENS_TEST_RPC; without an override the test falls +// back to a public endpoint so that `go test` works out of the box. +// +// Live-RPC tests skip themselves under `go test -short`, so CI runs that +// can't reach the network (or that are rate-limited) keep working. +func mainnetClient(t *testing.T) *ethclient.Client { + t.Helper() + if testing.Short() { + t.Skip("skipping live-RPC test under -short") + } + url := os.Getenv("GO_ENS_TEST_RPC") + if url == "" { + // Public mainnet RPC. Surfaces revert data via the JSON-RPC `data` + // field, which is required for CCIP-Read. + url = "https://ethereum.publicnode.com" + } + client, err := ethclient.Dial(url) + require.NoError(t, err, "failed to dial %s", url) + return client +} diff --git a/mainnet_client_test.go b/mainnet_client_test.go new file mode 100644 index 0000000..1954e43 --- /dev/null +++ b/mainnet_client_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens + +import ( + "os" + "testing" + + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +// mainnetClient connects to a public Ethereum mainnet RPC. The endpoint can +// be overridden with GO_ENS_TEST_RPC; without an override the test falls +// back to a public endpoint so that `go test` works out of the box. +// +// Live-RPC tests skip themselves under `go test -short`, so CI runs that +// can't reach the network (or that are rate-limited) keep working. +func mainnetClient(t *testing.T) *ethclient.Client { + t.Helper() + if testing.Short() { + t.Skip("skipping live-RPC test under -short") + } + url := os.Getenv("GO_ENS_TEST_RPC") + if url == "" { + // Public mainnet RPC. Surfaces revert data via the JSON-RPC `data` + // field, which is required for CCIP-Read. + url = "https://ethereum.publicnode.com" + } + client, err := ethclient.Dial(url) + require.NoError(t, err, "failed to dial %s", url) + return client +} diff --git a/name_test.go b/name_test.go index a74973c..efab529 100644 --- a/name_test.go +++ b/name_test.go @@ -43,7 +43,7 @@ func TestName(t *testing.T) { dsExpiry := time.Unix(1904119920, 0) dsRegistrationInterval := 60 * time.Second - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "resolver.eth") require.Nil(t, err, "Failed to create name") @@ -70,7 +70,7 @@ func TestName(t *testing.T) { } func TestNameExpiry(t *testing.T) { - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) domain := unregisteredDomain(client) name, err := NewName(client, domain) require.Nil(t, err, "Failed to create name") @@ -83,7 +83,7 @@ func TestNameReRegistration(t *testing.T) { if !hasPrivateKey(registrant) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "resolver.eth") require.Nil(t, err, "Failed to create name") @@ -95,13 +95,13 @@ func TestNameReRegistration(t *testing.T) { } func TestInvalidName(t *testing.T) { - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) _, err := NewName(client, "ab.eth") require.Equal(t, err.Error(), "name is not valid according to the rules of the registrar (too short, invalid characters, etc.)") } func TestNameRegistration(t *testing.T) { - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) registrant := common.HexToAddress("388Ea662EF2c223eC0B047D41Bf3c0f362142ad5") if !hasPrivateKey(registrant) { t.Skip() @@ -140,7 +140,7 @@ func TestNameRegistration(t *testing.T) { } func TestNameRegistrationStageTwoNoStageOne(t *testing.T) { - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) registrant := common.HexToAddress("388Ea662EF2c223eC0B047D41Bf3c0f362142ad5") if !hasPrivateKey(registrant) { t.Skip() @@ -159,7 +159,7 @@ func TestNameRegistrationStageTwoNoStageOne(t *testing.T) { } func TestNameRegistrationNoValue(t *testing.T) { - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) registrant := common.HexToAddress("388Ea662EF2c223eC0B047D41Bf3c0f362142ad5") if !hasPrivateKey(registrant) { t.Skip() @@ -192,7 +192,7 @@ func TestNameRegistrationNoValue(t *testing.T) { } func TestNameRegistrationNoInterval(t *testing.T) { - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) registrant := common.HexToAddress("388Ea662EF2c223eC0B047D41Bf3c0f362142ad5") if !hasPrivateKey(registrant) { t.Skip() @@ -223,7 +223,7 @@ func TestNameExtension(t *testing.T) { if !hasPrivateKey(registrant) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -247,7 +247,7 @@ func TestNameExtensionLowValue(t *testing.T) { if !hasPrivateKey(registrant) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -262,7 +262,7 @@ func TestNameExtensionNotRegistered(t *testing.T) { if !hasPrivateKey(registrant) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) domain := unregisteredDomain(client) name, err := NewName(client, domain) require.Nil(t, err, "Failed to create name") @@ -278,7 +278,7 @@ func TestNameSubdomainCreate(t *testing.T) { if !hasPrivateKey(dsRegistrant) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -309,7 +309,7 @@ func TestNameSubdomainCreateAlreadyExists(t *testing.T) { if !hasPrivateKey(dsRegistrant) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -330,7 +330,7 @@ func TestSetController(t *testing.T) { t.Skip() } dsController := common.HexToAddress("E195c59BCF26fD36c82d1C720860127A5c1c4040") - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -374,7 +374,7 @@ func TestSetControllerUnauthorised(t *testing.T) { if !hasPrivateKey(dsThief) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -398,7 +398,7 @@ func TestReclaim(t *testing.T) { t.Skip() } dsController := common.HexToAddress("E195c59BCF26fD36c82d1C720860127A5c1c4040") - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -439,7 +439,7 @@ func TestReclaimUnauthorised(t *testing.T) { if !hasPrivateKey(dsThief) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -466,7 +466,7 @@ func TestTransfer(t *testing.T) { if !hasPrivateKey(dsNewRegistrant) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") @@ -507,7 +507,7 @@ func TestTransferUnauthorised(t *testing.T) { if !hasPrivateKey(dsThief) { t.Skip() } - client, _ := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") + client := mainnetClient(t) name, err := NewName(client, "foobar5.eth") require.Nil(t, err, "Failed to create name") diff --git a/resolver_test.go b/resolver_test.go index 4205ded..38ef1b7 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -20,42 +20,45 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var client, _ = ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") - func TestResolveEmpty(t *testing.T) { + client := mainnetClient(t) _, err := Resolve(context.Background(), client, "") assert.NotNil(t, err, "Resolved empty name") } func TestResolveZero(t *testing.T) { + client := mainnetClient(t) _, err := Resolve(context.Background(), client, "0") assert.NotNil(t, err, "Resolved empty name") } func TestResolveNotPresent(t *testing.T) { + client := mainnetClient(t) _, err := Resolve(context.Background(), client, "sirnotappearinginthisregistry.eth") require.NotNil(t, err, "Resolved name that does not exist") assert.Equal(t, "unregistered name", err.Error(), "Unexpected error") } // func TestResolveNoResolver(t *testing.T) { +// client := mainnetClient(t) // _, err := Resolve(context.Background(), client, "noresolver.eth") // require.NotNil(t, err, "Resolved name without a resolver") // assert.Equal(t, "no resolver", err.Error(), "Unexpected error") // } func TestResolveBadResolver(t *testing.T) { + client := mainnetClient(t) _, err := Resolve(context.Background(), client, "resolvestozero.eth") require.NotNil(t, err, "Resolved name with a bad resolver") assert.Equal(t, "no address", err.Error(), "Unexpected error") } func TestResolveTestEnsTest(t *testing.T) { + client := mainnetClient(t) expected := "ed96dd3be847b387217ef9de5b20d8392a6cdf40" actual, err := Resolve(context.Background(), client, "test.enstest.eth") require.Nil(t, err, "Error resolving name") @@ -63,6 +66,7 @@ func TestResolveTestEnsTest(t *testing.T) { } func TestResolveResolverEth(t *testing.T) { + client := mainnetClient(t) expected := "231b0ee14048e9dccd1d247744d114a4eb5e8e63" actual, err := Resolve(context.Background(), client, "resolver.eth") require.Nil(t, err, "Error resolving name") @@ -70,6 +74,7 @@ func TestResolveResolverEth(t *testing.T) { } func TestResolveAddress(t *testing.T) { + client := mainnetClient(t) expected := "b8c2c29ee19d8307cb7255e1cd9cbde883a267d5" actual, err := Resolve(context.Background(), client, "0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5") require.Nil(t, err, "Error resolving address") @@ -77,6 +82,7 @@ func TestResolveAddress(t *testing.T) { } func TestResolveShortAddress(t *testing.T) { + client := mainnetClient(t) expected := "0000000000000000000000000000000000000001" actual, err := Resolve(context.Background(), client, "0x1") require.Nil(t, err, "Error resolving address") @@ -84,11 +90,13 @@ func TestResolveShortAddress(t *testing.T) { } func TestResolveHexString(t *testing.T) { + client := mainnetClient(t) _, err := Resolve(context.Background(), client, "0xe32c6d1a964749b6de2130e20daed821a45b9e7261118801ff5319d0ffc6b54a") assert.NotNil(t, err, "Resolved too-long hex string") } func TestReverseResolveTestEnsTest(t *testing.T) { + client := mainnetClient(t) expected := "nick.eth" address := common.HexToAddress("b8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5") actual, err := ReverseResolve(context.Background(), client, address) diff --git a/reverseresolver_test.go b/reverseresolver_test.go index 75bda4d..c14ca90 100644 --- a/reverseresolver_test.go +++ b/reverseresolver_test.go @@ -19,7 +19,6 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" ens "github.com/wealdtech/go-ens/v4" ) @@ -51,8 +50,7 @@ func TestReverseResolve(t *testing.T) { }, } - client, err := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") - require.NoError(t, err) + client := mainnetClient(t) for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/tokenid_test.go b/tokenid_test.go index bde1500..b48a11b 100644 --- a/tokenid_test.go +++ b/tokenid_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" ) @@ -36,8 +35,7 @@ func TestDeriveTokenId(t *testing.T) { err: "empty domain", }, } - client, err := ethclient.Dial("https://mainnet.infura.io/v3/831a5442dc2e4536a9f8dee4ea1707a6") - require.NoError(t, err) + client := mainnetClient(t) for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := DeriveTokenID(context.Background(), client, test.input) diff --git a/universalresolver_test.go b/universalresolver_test.go index a2f41d6..050faba 100644 --- a/universalresolver_test.go +++ b/universalresolver_test.go @@ -11,13 +11,11 @@ package ens_test import ( "context" "math/big" - "os" "strings" "testing" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" ens "github.com/wealdtech/go-ens/v4" ) @@ -129,28 +127,6 @@ func TestNewUniversalResolverContext_AcceptsKnownChain(t *testing.T) { require.NotNil(t, ur) } -// mainnetClient connects to a public Ethereum mainnet RPC. The endpoint can -// be overridden with GO_ENS_TEST_RPC; without an override the test falls -// back to a public endpoint so that `go test` works out of the box. -// -// Live-RPC tests skip themselves under `go test -short`, so CI runs that -// can't reach the network (or that are rate-limited) keep working. -func mainnetClient(t *testing.T) *ethclient.Client { - t.Helper() - if testing.Short() { - t.Skip("skipping live-RPC test under -short") - } - url := os.Getenv("GO_ENS_TEST_RPC") - if url == "" { - // Public mainnet RPC. Surfaces revert data via the JSON-RPC `data` - // field, which is required for CCIP-Read. - url = "https://ethereum.publicnode.com" - } - client, err := ethclient.Dial(url) - require.NoError(t, err, "failed to dial %s", url) - return client -} - // TestUniversalResolverIntegrationName is the canonical ENSv2 readiness // check from https://docs.ens.domains/web/ensv2-readiness/. A library that // resolves through the UniversalResolver returns the v2 sentinel address; From 3939917c4fa374be0a511d6446a18604b0a07da8 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 27 May 2026 10:08:40 +0200 Subject: [PATCH 20/21] feat(normalize): replace IDNA normaliser with ENSIP-15 (adraffy/go-ens-normalize) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Normalize and NormaliseDomain previously used golang.org/x/net/idna with MapForLookup, which is close to but not the ENSIP-15 algorithm. That let a number of names through that the on-chain Universal Resolver rejects (empty labels, the XX-- label-extension prefix, mid-label underscores, whole-script confusables), and mapped a handful of codepoints differently from the canonical normaliser used by the rest of the ENS ecosystem (Arabic-Indic digits in particular). - Switch Normalize (used by NameHash, LabelHash, DNSEncode) and NormaliseDomain (used by Tld, DomainPart) to github.com/adraffy/go-ens-normalize/ensip15. That package is the canonical Go port maintained by the ENSIP-15 spec author, ships Unicode 17.0.0, passes the full ENSIP-15 validation suite, and has no transitive runtime dependencies. - Keep NormaliseDomainStrict on IDNA (StrictDomainName=true) — it serves a different purpose (DNS-strict validation, no underscores) and isn't ENSIP-15. Doc-comment the distinction. - DNSEncode now trims a single trailing dot before calling Normalize so that the legacy "foo.eth." convenience input still encodes cleanly even though ENSIP-15 rejects trailing-dot empty labels. - Update the legacy IDNA-era fixtures that codified pre-ENSIP-15 behaviour: empty-label inputs (.eth, foo..eth, etc.) now expect errors; te--st.eth, a_b.eth, and the Cyrillic/Latin confusable fixture now expect errors; the Arabic-Indic digit fixture's hash is updated to the ENSIP-15-canonical value. - Add normalize_ensip15_test.go pinning the behaviours that distinguish ENSIP-15 from the old IDNA profile: accept/reject categories the report's R15 recommendation called out, and the Arabic-Indic digit non-folding behaviour. Closes report R15. Co-Authored-By: Claude Opus 4.7 --- dnsencode.go | 10 ++- dnsencode_test.go | 9 ++- go.mod | 3 +- go.sum | 2 + misc.go | 25 ++++---- misc_test.go | 98 ++++++++++++++++-------------- namehash.go | 24 ++++---- namehash_test.go | 124 ++++++++++++++++++++------------------ normalize_ensip15_test.go | 92 ++++++++++++++++++++++++++++ 9 files changed, 255 insertions(+), 132 deletions(-) create mode 100644 normalize_ensip15_test.go diff --git a/dnsencode.go b/dnsencode.go index 9bcced6..101788e 100644 --- a/dnsencode.go +++ b/dnsencode.go @@ -25,6 +25,13 @@ import ( // for compression pointers) — except that ENSIP-10 explicitly removes // RFC 1035's 255-octet limit on the total encoded name length. func DNSEncode(name string) ([]byte, error) { + if name == "" { + return []byte{0}, nil + } + // Trim a single trailing dot before normalisation — ENSIP-15 rejects + // empty labels, but DNS-style "foo.eth." input is convenient. Multiple + // trailing dots still produce an empty label and are rejected. + name = strings.TrimSuffix(name, ".") if name == "" { return []byte{0}, nil } @@ -32,9 +39,6 @@ func DNSEncode(name string) ([]byte, error) { if err != nil { return nil, err } - // Trailing dots produce empty labels which are not meaningful in ENS; - // trim a single trailing dot to be lenient with input. - normalised = strings.TrimSuffix(normalised, ".") parts := strings.Split(normalised, ".") out := make([]byte, 0, len(normalised)+len(parts)+1) for i, label := range parts { diff --git a/dnsencode_test.go b/dnsencode_test.go index 1d2a52f..714df31 100644 --- a/dnsencode_test.go +++ b/dnsencode_test.go @@ -107,9 +107,14 @@ func TestDNSEncode_LeadingDot(t *testing.T) { assert.Contains(t, err.Error(), "empty label") } +// A single dot is the DNS root domain — it encodes to a single null byte, +// matching the empty-string case. The trailing-dot trim absorbs the dot +// before normalisation runs, so this is allowed (consistent with +// TestDNSEncode_Empty). func TestDNSEncode_OnlyDot(t *testing.T) { - _, err := ens.DNSEncode(".") - require.Error(t, err) + out, err := ens.DNSEncode(".") + require.NoError(t, err) + require.Equal(t, []byte{0}, out) } func TestDNSEncode_DoubleDotInMiddle(t *testing.T) { diff --git a/go.mod b/go.mod index 2ab2bb7..0571589 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module github.com/wealdtech/go-ens/v4 -go 1.22.0 +go 1.22.4 toolchain go1.23.2 require ( + github.com/adraffy/go-ens-normalize v0.1.1 github.com/ethereum/go-ethereum v1.14.12 github.com/ipfs/go-cid v0.4.1 github.com/multiformats/go-multibase v0.2.0 diff --git a/go.sum b/go.sum index 88e70b3..dff705c 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/adraffy/go-ens-normalize v0.1.1 h1:N//kZB/aSdBLAbUFX52iC5d7EHVgkxmLkLQ3nQnvkwE= +github.com/adraffy/go-ens-normalize v0.1.1/go.mod h1:2wzkGeMLp+VO8lqbu4MYrFeQEVWSV6CGN1Vznrt+Gt0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= diff --git a/misc.go b/misc.go index 7652cd0..53d1ff2 100644 --- a/misc.go +++ b/misc.go @@ -3,6 +3,8 @@ package ens import ( "fmt" "strings" + + "github.com/adraffy/go-ens-normalize/ensip15" ) // DomainLevel calculates the level of the domain presented. @@ -13,32 +15,33 @@ func DomainLevel(name string) int { return len(strings.Split(name, ".")) - 1 } -// NormaliseDomain turns ENS domain in to normal form. +// NormaliseDomain turns an ENS domain into normal form per ENSIP-15. +// +// A leading "*." wildcard prefix is preserved (stripped before normalisation +// and re-attached afterwards); the rest of the name is normalised by the +// canonical ENSIP-15 implementation. Empty labels, disallowed codepoints, +// and other ENSIP-15 violations return an error. func NormaliseDomain(domain string) (string, error) { wildcard := false if strings.HasPrefix(domain, "*.") { wildcard = true domain = domain[2:] } - output, err := p.ToUnicode(domain) + output, err := ensip15.Shared().Normalize(domain) if err != nil { return "", err } - - // ToUnicode() removes leading periods. Replace them. - if strings.HasPrefix(domain, ".") && !strings.HasPrefix(output, ".") { - output = "." + output - } - - // If we removed a wildcard then add it back. if wildcard { output = "*." + output } return output, nil } -// NormaliseDomainStrict turns ENS domain in to normal form, using strict DNS -// rules (e.g. no underscores). +// NormaliseDomainStrict applies strict DNS rules (e.g. no underscores) via +// golang.org/x/net/idna with StrictDomainName=true. This is NOT ENSIP-15 +// normalisation — for that, use NormaliseDomain. NormaliseDomainStrict is +// kept for callers that need DNS-strict validation, e.g. before publishing +// a name to a DNS-backed registrar. func NormaliseDomainStrict(domain string) (string, error) { wildcard := false if strings.HasPrefix(domain, "*.") { diff --git a/misc_test.go b/misc_test.go index 776c2a2..3e8fd0d 100644 --- a/misc_test.go +++ b/misc_test.go @@ -10,43 +10,40 @@ import ( ) func TestNormaliseDomain(t *testing.T) { + // ENSIP-15 rejects empty labels, so inputs that resolve to one (".", ".eth", + // "foo.eth.", ".wealdtech.eth", etc.) now error rather than silently + // round-trip the way the old IDNA-only normaliser allowed. tests := []struct { - input string - output string - err error + input string + output string + wantErr bool }{ - {"", "", nil}, - {".", ".", nil}, - {"eth", "eth", nil}, - {"ETH", "eth", nil}, - {".eth", ".eth", nil}, - {".eth.", ".eth.", nil}, - {"wealdtech.eth", "wealdtech.eth", nil}, - {".wealdtech.eth", ".wealdtech.eth", nil}, - {"subdomain.wealdtech.eth", "subdomain.wealdtech.eth", nil}, - {"*.wealdtech.eth", "*.wealdtech.eth", nil}, - {"omg.thetoken.eth", "omg.thetoken.eth", nil}, - {"_underscore.thetoken.eth", "_underscore.thetoken.eth", nil}, - {"點看.eth", "點看.eth", nil}, + {input: "", output: ""}, + {input: ".", wantErr: true}, + {input: "eth", output: "eth"}, + {input: "ETH", output: "eth"}, + {input: ".eth", wantErr: true}, + {input: ".eth.", wantErr: true}, + {input: "wealdtech.eth", output: "wealdtech.eth"}, + {input: ".wealdtech.eth", wantErr: true}, + {input: "subdomain.wealdtech.eth", output: "subdomain.wealdtech.eth"}, + {input: "*.wealdtech.eth", output: "*.wealdtech.eth"}, + {input: "omg.thetoken.eth", output: "omg.thetoken.eth"}, + // Underscore-leading labels are still valid under ENSIP-15. + {input: "_underscore.thetoken.eth", output: "_underscore.thetoken.eth"}, + {input: "點看.eth", output: "點看.eth"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result, err := NormaliseDomain(tt.input) - if tt.err != nil { - if err == nil { - t.Fatalf("missing expected error") - } - if tt.err.Error() != err.Error() { - t.Errorf("unexpected error value %v", err) - } - } else { - if err != nil { - t.Fatalf("unexpected error %v", err) - } - if tt.output != result { - t.Errorf("%v => %v (expected %v)", tt.input, result, tt.output) - } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.output != result { + t.Errorf("%v => %v (expected %v)", tt.input, result, tt.output) } }) } @@ -96,6 +93,11 @@ func TestNormaliseDomainStrict(t *testing.T) { } func TestTld(t *testing.T) { + // Under ENSIP-15, inputs with leading dots (".eth", ".wealdtech.eth", ".") + // produce an empty label and NormaliseDomain errors. Tld currently returns + // "" when normalisation fails (NormaliseDomain returns "" on error). Pre- + // ENSIP-15 behaviour returned "eth" for ".eth" and ".wealdtech.eth"; that + // has been corrected. tests := []struct { input string output string @@ -104,9 +106,9 @@ func TestTld(t *testing.T) { {".", ""}, {"eth", "eth"}, {"ETH", "eth"}, - {".eth", "eth"}, + {".eth", ""}, {"wealdtech.eth", "eth"}, - {".wealdtech.eth", "eth"}, + {".wealdtech.eth", ""}, {"subdomain.wealdtech.eth", "eth"}, } @@ -129,21 +131,25 @@ func TestDomainPart(t *testing.T) { {"", 2, "", true}, {"", -1, "", false}, {"", -2, "", true}, - {".", 1, "", false}, - {".", 2, "", false}, + // ENSIP-15: a bare "." is an empty label and now errors. + {".", 1, "", true}, + {".", 2, "", true}, {".", 3, "", true}, - {".", -1, "", false}, - {".", -2, "", false}, + {".", -1, "", true}, + {".", -2, "", true}, {".", -3, "", true}, {"ETH", 1, "eth", false}, {"ETH", 2, "", true}, {"ETH", -1, "eth", false}, {"ETH", -2, "", true}, - {".ETH", 1, "", false}, - {".ETH", 2, "eth", false}, + // ENSIP-15: a leading dot produces an empty label and now errors for + // every part index — including the negative indices that previously + // happened to land on a non-empty label. + {".ETH", 1, "", true}, + {".ETH", 2, "", true}, {".ETH", 3, "", true}, - {".ETH", -1, "eth", false}, - {".ETH", -2, "", false}, + {".ETH", -1, "", true}, + {".ETH", -2, "", true}, {".ETH", -3, "", true}, {"wealdtech.eth", 1, "wealdtech", false}, {"wealdtech.eth", 2, "eth", false}, @@ -151,13 +157,13 @@ func TestDomainPart(t *testing.T) { {"wealdtech.eth", -1, "eth", false}, {"wealdtech.eth", -2, "wealdtech", false}, {"wealdtech.eth", -3, "", true}, - {".wealdtech.eth", 1, "", false}, - {".wealdtech.eth", 2, "wealdtech", false}, - {".wealdtech.eth", 3, "eth", false}, + {".wealdtech.eth", 1, "", true}, + {".wealdtech.eth", 2, "", true}, + {".wealdtech.eth", 3, "", true}, {".wealdtech.eth", 4, "", true}, - {".wealdtech.eth", -1, "eth", false}, - {".wealdtech.eth", -2, "wealdtech", false}, - {".wealdtech.eth", -3, "", false}, + {".wealdtech.eth", -1, "", true}, + {".wealdtech.eth", -2, "", true}, + {".wealdtech.eth", -3, "", true}, {".wealdtech.eth", -4, "", true}, {"subdomain.wealdtech.eth", 1, "subdomain", false}, {"subdomain.wealdtech.eth", 2, "wealdtech", false}, diff --git a/namehash.go b/namehash.go index dd5e8ff..93cc987 100644 --- a/namehash.go +++ b/namehash.go @@ -17,28 +17,28 @@ package ens import ( "strings" + "github.com/adraffy/go-ens-normalize/ensip15" "github.com/pkg/errors" "golang.org/x/net/idna" "golang.org/x/crypto/sha3" ) -var ( - p = idna.New(idna.MapForLookup(), idna.ValidateLabels(false), idna.CheckHyphens(false), idna.StrictDomainName(false), idna.Transitional(false)) - pStrict = idna.New(idna.MapForLookup(), idna.ValidateLabels(false), idna.CheckHyphens(false), idna.StrictDomainName(true), idna.Transitional(false)) -) +// pStrict is the legacy IDNA "strict" profile, used only by +// NormaliseDomainStrict for DNS-strict checks (no underscores, etc.). It is +// NOT an ENSIP-15 normalizer — see Normalize / NormaliseDomain for that. +var pStrict = idna.New(idna.MapForLookup(), idna.ValidateLabels(false), idna.CheckHyphens(false), idna.StrictDomainName(true), idna.Transitional(false)) -// Normalize normalizes a name according to the ENS rules. +// Normalize normalizes a name according to ENSIP-15. Empty labels, disallowed +// codepoints, mixed-script confusables, and other ENSIP-15 violations return +// an error. The implementation delegates to github.com/adraffy/go-ens-normalize, +// the canonical Go port (Unicode 17.0.0, passes the full ENSIP-15 validation +// suite). func Normalize(input string) (string, error) { - output, err := p.ToUnicode(input) + output, err := ensip15.Shared().Normalize(input) if err != nil { - return "", errors.Wrap(err, "failed to convert to standard unicode") - } - // If the name started with a period then ToUnicode() removes it, but we want to keep it. - if strings.HasPrefix(input, ".") && !strings.HasPrefix(output, ".") { - output = "." + output + return "", errors.Wrap(err, "failed to ensip-15 normalize") } - return output, nil } diff --git a/namehash_test.go b/namehash_test.go index aaa883d..a27876b 100644 --- a/namehash_test.go +++ b/namehash_test.go @@ -17,73 +17,83 @@ package ens import ( "encoding/hex" "testing" + + "github.com/stretchr/testify/require" ) func TestNameHash(t *testing.T) { + // Pre-ENSIP-15 fixtures that produced valid hashes for inputs containing + // empty labels (".eth", "foo..eth") are now expected to error — ENSIP-15 + // rejects empty labels rather than silently round-tripping them. tests := []struct { - input string - output string - err error + input string + output string + wantErr bool }{ - {"", "0000000000000000000000000000000000000000000000000000000000000000", nil}, - {"eth", "93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae", nil}, - {"Eth", "93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae", nil}, - {".eth", "8cc9f31a5e7af6381efc751d98d289e3f3589f1b6f19b9b989ace1788b939cf7", nil}, - {"resolver.eth", "fdd5d5de6dd63db72bbc2d487944ba13bf775b50a80805fe6fcaba9b0fba88f5", nil}, - {"foo.eth", "de9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f", nil}, - {"Foo.eth", "de9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f", nil}, - {"foo..eth", "4143a5b2f547838d3b49982e3f2ec6a26415274e5b9c3ffeb21971bbfdfaa052", nil}, - {"bar.foo.eth", "275ae88e7263cdce5ab6cf296cdd6253f5e385353fe39cfff2dd4a2b14551cf3", nil}, - {"Bar.foo.eth", "275ae88e7263cdce5ab6cf296cdd6253f5e385353fe39cfff2dd4a2b14551cf3", nil}, - {"addr.reverse", "91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2", nil}, - {"🚣‍♂️.eth", "37f681401d88093779de0c910da1f9437759c2181f61b5dbc9e3ef0d9f192513", nil}, - {"vitalik.eth", "ee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", nil}, - {"nIcK.eTh", "05a67c0ee82964c4f7394cdd47fee7f4d9503a23c09c38341779ea012afe6e00", nil}, - {"brantly.cash", "3cb8d24c572869f7ec5ec5e882243b4790de146ac11267f58fd856d290b593db", nil}, - {"🏴‍☠.art", "4696ec532b8c8bb20dcfa6ae6da710422611ea03fc77aebc0284d0f12c93f768", nil}, - {"öbb.eth", "2774094517aaa884fc7183cf681150529b9acc7c9e7b71cf3a470200135a20b4", nil}, - {"Öbb.eth", "2774094517aaa884fc7183cf681150529b9acc7c9e7b71cf3a470200135a20b4", nil}, - {"💷pound.eth", "2df37f43de8b5ab870370770e78e4503ea1199e46c16db7a4a9320732dbb42bc", nil}, - {"firstwrappedname.eth", "c44eec7fb870ae46d4ef4392d33fbbbdc164e7817a86289a1fe30e5f4d98ae85", nil}, - {"A.™️.Ю", "f48b2941db0e23087a8922f81c0b7dd1b83fa06657080fd28986f9a19752a093", nil}, - {"Ⅷ.eth", "a468899f828f5d80c85c0e538018ba6516713a06aa6d92e8482ee91886630ae9", nil}, - {"⨌.eth", "f0b6090714f4ab523b1c574077018fe49dc934f9334d092e412b3802898b2cee", nil}, - {"-test.test-.t-e--s---t", "0d26fe02b1eb3fdcd7db77a432c60b20d2d8c4ef9012ef06ad3ae7bf12adb225", nil}, - {"te--st.eth", "bea7eca33545aacbf3d6f1512c119aa7f04506d61354001e7af31ba73374b2c3", nil}, - {"__ab.eth", "b3edf1f0b8dc6168b65d034c76b5670813514ac8867115e63b51eb496f3791a0", nil}, - {"a_b.eth", "b99946eaeb8a557bc6329faf95d1b885ba608393109cb9609440acc8e6761c4f", nil}, - {"ß.eth", "86262f2349fb651f652e87141e6db5856cd6e40f92d0b9dc486aaf8f9c509cff", nil}, - {"💩💩💩.eth", "a74feb0e5fa5606d3e650275e3bb3873b006a10d558389d3ce2abbe681fcfc8e", nil}, - {"آٍَ.ऩँं", "a1e3d72c39aec7ebcf62906994da7adc76b8a3ffa27e52b838abb24569610146", nil}, - {"פעילותהבינאום.eth", "7900040d82be5a569289b0f7b30266d796e1ded13293557ed061b39367357a62", nil}, - {"ஶ்ரீ.ஸ்ரீ", "ef5f28b8adff4b9b86b67c40792ef314ecd92a2123a72185b1411835cce5c110", nil}, - {"🇸🇦سلمان.eth", "3484f75c4dbf69503310126dbfc02eec224eab46851de2288092d58785670808", nil}, - {"0a〇.黑a8", "d7bc21f0cb86cafe27ebafe8e788b1c63b736467c65611110575f385a4d01e1f", nil}, - {"apple.дррӏе.аррӏе.aррӏе", "a5428a7fc57a099d7926ac7fa30169933df167acf3d8fba6f5830e6cf7442b31", nil}, - {"۰۱۲۳۷۸۹.eth", "430bc5e65fd3122d45c00dbce4e8e1b51be675794e548f92558dc4cecafd6418", nil}, - {"𓀀𓀁𓀂.eth", "36f75325f4013011b96014606224f03c77366d89caf480a1060d7453edaf5c98", nil}, - {"𓆏➡🐸️.eth", "f2663423c7aadf794d6f84addcfee1f877458d86c20740954482094a20985228", nil}, - {"ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ.eth", "254a068876bdac64ad35425816bf444fbe9a26ec19287f47e663f928727ff69c", nil}, - {"trish🫧.eth", "9a6aa3aaa97ed3b694265d4b0e776b2e8fad34b8869199b1a062e3cc0ee0d41d", nil}, + {input: "", output: "0000000000000000000000000000000000000000000000000000000000000000"}, + {input: "eth", output: "93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"}, + {input: "Eth", output: "93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"}, + {input: ".eth", wantErr: true}, + {input: "resolver.eth", output: "fdd5d5de6dd63db72bbc2d487944ba13bf775b50a80805fe6fcaba9b0fba88f5"}, + {input: "foo.eth", output: "de9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"}, + {input: "Foo.eth", output: "de9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"}, + {input: "foo..eth", wantErr: true}, + {input: "bar.foo.eth", output: "275ae88e7263cdce5ab6cf296cdd6253f5e385353fe39cfff2dd4a2b14551cf3"}, + {input: "Bar.foo.eth", output: "275ae88e7263cdce5ab6cf296cdd6253f5e385353fe39cfff2dd4a2b14551cf3"}, + {input: "addr.reverse", output: "91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2"}, + {input: "🚣‍♂️.eth", output: "37f681401d88093779de0c910da1f9437759c2181f61b5dbc9e3ef0d9f192513"}, + {input: "vitalik.eth", output: "ee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835"}, + {input: "nIcK.eTh", output: "05a67c0ee82964c4f7394cdd47fee7f4d9503a23c09c38341779ea012afe6e00"}, + {input: "brantly.cash", output: "3cb8d24c572869f7ec5ec5e882243b4790de146ac11267f58fd856d290b593db"}, + {input: "🏴‍☠.art", output: "4696ec532b8c8bb20dcfa6ae6da710422611ea03fc77aebc0284d0f12c93f768"}, + {input: "öbb.eth", output: "2774094517aaa884fc7183cf681150529b9acc7c9e7b71cf3a470200135a20b4"}, + {input: "Öbb.eth", output: "2774094517aaa884fc7183cf681150529b9acc7c9e7b71cf3a470200135a20b4"}, + {input: "💷pound.eth", output: "2df37f43de8b5ab870370770e78e4503ea1199e46c16db7a4a9320732dbb42bc"}, + {input: "firstwrappedname.eth", output: "c44eec7fb870ae46d4ef4392d33fbbbdc164e7817a86289a1fe30e5f4d98ae85"}, + {input: "A.™️.Ю", output: "f48b2941db0e23087a8922f81c0b7dd1b83fa06657080fd28986f9a19752a093"}, + {input: "Ⅷ.eth", output: "a468899f828f5d80c85c0e538018ba6516713a06aa6d92e8482ee91886630ae9"}, + {input: "⨌.eth", output: "f0b6090714f4ab523b1c574077018fe49dc934f9334d092e412b3802898b2cee"}, + // `-test`, `test-`, and `t-e--s---t` are all valid labels — only the + // `XX--` extension prefix at positions 3-4 is invalid (see "te--st.eth" + // below). + {input: "-test.test-.t-e--s---t", output: "0d26fe02b1eb3fdcd7db77a432c60b20d2d8c4ef9012ef06ad3ae7bf12adb225"}, + // ENSIP-15 forbids the `XX--` label extension (RFC 5891-style guard + // against ambiguity with Punycode). + {input: "te--st.eth", wantErr: true}, + {input: "__ab.eth", output: "b3edf1f0b8dc6168b65d034c76b5670813514ac8867115e63b51eb496f3791a0"}, + // ENSIP-15 allows underscores only at the start of a label. + {input: "a_b.eth", wantErr: true}, + {input: "ß.eth", output: "86262f2349fb651f652e87141e6db5856cd6e40f92d0b9dc486aaf8f9c509cff"}, + {input: "💩💩💩.eth", output: "a74feb0e5fa5606d3e650275e3bb3873b006a10d558389d3ce2abbe681fcfc8e"}, + {input: "آٍَ.ऩँं", output: "a1e3d72c39aec7ebcf62906994da7adc76b8a3ffa27e52b838abb24569610146"}, + {input: "פעילותהבינאום.eth", output: "7900040d82be5a569289b0f7b30266d796e1ded13293557ed061b39367357a62"}, + {input: "ஶ்ரீ.ஸ்ரீ", output: "ef5f28b8adff4b9b86b67c40792ef314ecd92a2123a72185b1411835cce5c110"}, + {input: "🇸🇦سلمان.eth", output: "3484f75c4dbf69503310126dbfc02eec224eab46851de2288092d58785670808"}, + {input: "0a〇.黑a8", output: "d7bc21f0cb86cafe27ebafe8e788b1c63b736467c65611110575f385a4d01e1f"}, + // ENSIP-15 explicitly rejects whole-script confusables; this is the + // canonical "Cyrillic-mimics-Latin" example. + {input: "apple.дррӏе.аррӏе.aррӏе", wantErr: true}, + // Hash differs from the pre-ENSIP-15 value because the old IDNA profile + // mapped Arabic-Indic digits to ASCII digits; ENSIP-15 preserves them. + {input: "۰۱۲۳۷۸۹.eth", output: "ff4b07401acb2d0f048c20f84a6ab57f6d66a0a16d81d13bbba87e8cfacd6594"}, + {input: "𓀀𓀁𓀂.eth", output: "36f75325f4013011b96014606224f03c77366d89caf480a1060d7453edaf5c98"}, + {input: "𓆏➡🐸️.eth", output: "f2663423c7aadf794d6f84addcfee1f877458d86c20740954482094a20985228"}, + {input: "ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ.eth", output: "254a068876bdac64ad35425816bf444fbe9a26ec19287f47e663f928727ff69c"}, + {input: "trish🫧.eth", output: "9a6aa3aaa97ed3b694265d4b0e776b2e8fad34b8869199b1a062e3cc0ee0d41d"}, } for _, tt := range tests { - result, err := NameHash(tt.input) - if tt.err == nil { - if err != nil { - t.Fatalf("unexpected error %v", err) + t.Run(tt.input, func(t *testing.T) { + result, err := NameHash(tt.input) + if tt.wantErr { + require.Error(t, err) + return } - if tt.output != hex.EncodeToString(result[:]) { - t.Errorf("Failure: %v => %v (expected %v)\n", tt.input, hex.EncodeToString(result[:]), tt.output) + require.NoError(t, err) + if got := hex.EncodeToString(result[:]); got != tt.output { + t.Errorf("%v => %v (expected %v)", tt.input, got, tt.output) } - } else { - if err == nil { - t.Fatalf("missing expected error") - } - if tt.err.Error() != err.Error() { - t.Errorf("unexpected error value %v", err) - } - } + }) } } diff --git a/normalize_ensip15_test.go b/normalize_ensip15_test.go new file mode 100644 index 0000000..6aeee4e --- /dev/null +++ b/normalize_ensip15_test.go @@ -0,0 +1,92 @@ +// Copyright 2026 Weald Technology Trading. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package ens + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +// These tests lock in the ENSIP-15 behaviours that distinguish the +// adraffy/go-ens-normalize implementation from the pre-ENSIP-15 IDNA-only +// normaliser previously used in this package. They are intentionally narrow: +// the upstream library has its own 5000+ vector suite against the canonical +// ENSIP-15 validation tests — replicating those here would duplicate work +// without adding value. These tests instead exercise the wiring (our +// public Normalize function, error surface, and the categories of input +// the report's R15 recommendation called out). + +func TestNormalize_AcceptsCanonicalInputs(t *testing.T) { + cases := []struct { + name string + in string + out string + }{ + {"already-normalised ASCII", "vitalik.eth", "vitalik.eth"}, + {"case-folds ASCII", "VITALIK.ETH", "vitalik.eth"}, + {"mixed case", "ViTaLiK.eTh", "vitalik.eth"}, + {"non-ASCII passes through", "öbb.eth", "öbb.eth"}, + {"non-ASCII case-folds", "ÖBB.eth", "öbb.eth"}, + {"underscore at label start", "_foo.eth", "_foo.eth"}, + {"emoji label", "💷pound.eth", "💷pound.eth"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, err := Normalize(c.in) + require.NoError(t, err) + require.Equal(t, c.out, got) + }) + } +} + +func TestNormalize_RejectsENSIP15Violations(t *testing.T) { + cases := []struct { + name string + in string + }{ + {"empty label / leading dot", ".eth"}, + {"empty label in middle", "foo..eth"}, + {"empty label / trailing dot", "foo."}, + {"bare dot", "."}, + {"XX-- label extension", "te--st.eth"}, + {"underscore mid-label", "a_b.eth"}, + {"whole-script confusable", "apple.дррӏе.аррӏе.aррӏе"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, err := Normalize(c.in) + require.Error(t, err, "expected ENSIP-15 to reject %q", c.in) + }) + } +} + +// ENSIP-15 differs from IDNA on Arabic-Indic digits: IDNA mapped them to +// ASCII digits, ENSIP-15 maps Extended Arabic-Indic (U+06F0-U+06F9) to +// Arabic-Indic (U+0660-U+0669) but never folds to ASCII. The resulting +// NameHash therefore differs from a pre-ENSIP-15 IDNA hash. This test pins +// the new behaviour so future library bumps don't silently regress. +func TestNormalize_ArabicIndicDigitsNotFoldedToASCII(t *testing.T) { + got, err := Normalize("۰۱۲۳۷۸۹.eth") + require.NoError(t, err) + require.NotEqual(t, "0123789.eth", got, "digits must NOT be ASCII-folded") + require.Equal(t, "٠١٢٣٧٨٩.eth", got) +} + +// Sanity check that Normalize errors are still wrapped with the +// "failed to ensip-15 normalize" prefix so the existing error-string +// behaviour at the API boundary is stable. +func TestNormalize_ErrorIsWrapped(t *testing.T) { + _, err := Normalize(".eth") + require.Error(t, err) + require.Contains(t, err.Error(), "ensip-15") + // Underlying ensip15 error is preserved via errors.Unwrap. + require.NotNil(t, errors.Unwrap(err)) +} From 49ac9bb37c679a314aca2dd2766ef0947ba8efb2 Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 1 Aug 2026 02:59:18 +0530 Subject: [PATCH 21/21] Update README.md for v4 release: - Changed GoDoc badge to point to pkg.go.dev. - Added information about v4's support for ENSIP-10 wildcards and ERC-3668 CCIP-Read. - Updated usage examples to include context.Context for resolution functions. - Clarified error handling in reverse resolution example. - Adjusted installation instructions to reflect the new version. --- README.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index aa4a4c5..868c873 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ [![Tag](https://img.shields.io/github/tag/wealdtech/go-ens.svg)](https://github.com/wealdtech/go-ens/releases/) [![License](https://img.shields.io/github/license/wealdtech/go-ens.svg)](LICENSE) -[![GoDoc](https://godoc.org/github.com/wealdtech/go-ens?status.svg)](https://godoc.org/github.com/wealdtech/go-ens) +[![GoDoc](https://pkg.go.dev/badge/github.com/wealdtech/go-ens/v4.svg)](https://pkg.go.dev/github.com/wealdtech/go-ens/v4) [![Travis CI](https://img.shields.io/travis/wealdtech/go-ens.svg)](https://travis-ci.org/wealdtech/go-ens) [![codecov.io](https://img.shields.io/codecov/c/github/wealdtech/go-ens.svg)](https://codecov.io/github/wealdtech/go-ens) [![Go Report Card](https://goreportcard.com/badge/github.com/wealdtech/go-ens)](https://goreportcard.com/report/github.com/wealdtech/go-ens) Go module to simplify interacting with the [Ethereum Name Service](https://ens.domains/) contracts. +v4 resolves names through the ENS [Universal Resolver](https://docs.ens.domains/web/ensv2-readiness/), so ENSIP-10 wildcards and ERC-3668 CCIP-Read work out of the box. Callers that need the previous registry-walking behaviour should stay on `github.com/wealdtech/go-ens/v3`. + ## Table of Contents @@ -23,7 +25,7 @@ Go module to simplify interacting with the [Ethereum Name Service](https://ens.d `go-ens` is a standard Go module which can be installed with: ```sh -go get github.com/wealdtech/go-ens/v3 +go get github.com/wealdtech/go-ens/v4 ``` ## Usage @@ -35,21 +37,21 @@ go get github.com/wealdtech/go-ens/v3 The most commonly-used feature of ENS is resolution: converting an ENS name to an Ethereum address. `go-ens` provides a simple call to allow this: ```go -address, err := ens.Resolve(client, domain) +address, err := ens.Resolve(ctx, client, domain) ``` -where `client` is a connection to an Ethereum client and `domain` is the fully-qualified name you wish to resolve (e.g. `foo.mydomain.eth`) (full examples for using this are given in the [Example](#Example) section below). +where `ctx` is a `context.Context` (honoured for cancellation and deadlines through any CCIP-Read hops), `client` is a connection to an Ethereum client, and `domain` is the fully-qualified name you wish to resolve (e.g. `foo.mydomain.eth`) (full examples for using this are given in the [Example](#Example) section below). The reverse process, converting an address to an ENS name, is just as simple: ```go -domain, err := ens.ReverseResolve(client, address) +domain, err := ens.ReverseResolve(ctx, client, address) ``` -Note that if the address does not have a reverse resolution this will return "". If you just want a string version of an address for on-screen display then you can use `ens.Format()`, for example: +If the address has no primary name set this returns an error (`"no resolution"`). If you just want a string version of an address for on-screen display then you can use `ens.Format()`, for example: ```go -fmt.Printf("The address is %s\n", ens.Format(client, address)) +fmt.Printf("The address is %s\n", ens.Format(ctx, client, address)) ``` This will carry out reverse resolution of the address and print the name if present; if not it will print a formatted version of the address. @@ -70,7 +72,7 @@ Addresses can be set and obtained using the address functions, for example to ge ```go COIN_TYPE_ETHEREUM := uint64(60) -address, err := name.Address(COIN_TYPE_ETHEREUM) +address, err := name.Address(ctx, COIN_TYPE_ETHEREUM) ``` ENS supports addresses for multiple coin types; values of coin types can be found at https://github.com/satoshilabs/slips/blob/master/slip-0044.md @@ -90,13 +92,16 @@ Because subdomains have their own registrars they do not work with the `Name` in package main import ( + "context" "fmt" "github.com/ethereum/go-ethereum/ethclient" - ens "github.com/wealdtech/go-ens/v3" + ens "github.com/wealdtech/go-ens/v4" ) func main() { + ctx := context.Background() + // Replace SECRET with your own access token for this example to work. client, err := ethclient.Dial("https://mainnet.infura.io/v3/SECRET") if err != nil { @@ -105,19 +110,16 @@ func main() { // Resolve a name to an address. domain := "ethereum.eth" - address, err := ens.Resolve(client, domain) + address, err := ens.Resolve(ctx, client, domain) if err != nil { panic(err) } fmt.Printf("Address of %s is %s\n", domain, address.Hex()) // Reverse resolve an address to a name. - reverse, err := ens.ReverseResolve(client, address) + reverse, err := ens.ReverseResolve(ctx, client, address) if err != nil { - panic(err) - } - if reverse == "" { - fmt.Printf("%s has no reverse lookup\n", address.Hex()) + fmt.Printf("%s has no reverse lookup: %v\n", address.Hex(), err) } else { fmt.Printf("Name of %s is %s\n", address.Hex(), reverse) }