github.com/ethersphere/bee/v2@v2.2.0/pkg/resolver/client/ens/ens.go (about) 1 // Copyright 2020 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package ens 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "strings" 12 13 "github.com/ethereum/go-ethereum/common" 14 "github.com/ethereum/go-ethereum/ethclient" 15 goens "github.com/wealdtech/go-ens/v3" 16 17 "github.com/ethersphere/bee/v2/pkg/resolver" 18 "github.com/ethersphere/bee/v2/pkg/resolver/client" 19 "github.com/ethersphere/bee/v2/pkg/swarm" 20 ) 21 22 const ( 23 defaultENSContractAddress = "00000000000C2E074eC69A0dFb2997BA6C7d2e1e" 24 swarmContentHashPrefix = "bzz://" 25 ) 26 27 // Address is the swarm bzz address. 28 type Address = swarm.Address 29 30 // Make sure Client implements the resolver.Client interface. 31 var _ client.Interface = (*Client)(nil) 32 33 var ( 34 // ErrFailedToConnect denotes that the resolver failed to connect to the 35 // provided endpoint. 36 ErrFailedToConnect = errors.New("failed to connect") 37 // ErrResolveFailed denotes that a name could not be resolved. 38 ErrResolveFailed = errors.New("resolve failed") 39 // ErrNotImplemented denotes that the function has not been implemented. 40 ErrNotImplemented = errors.New("function not implemented") 41 // errNameNotRegistered denotes that the name is not registered. 42 errNameNotRegistered = errors.New("name is not registered") 43 ) 44 45 // Client is a name resolution client that can connect to ENS via an 46 // Ethereum endpoint. 47 type Client struct { 48 endpoint string 49 contractAddr string 50 ethCl *ethclient.Client 51 connectFn func(string, string) (*ethclient.Client, *goens.Registry, error) 52 resolveFn func(*goens.Registry, common.Address, string) (string, error) 53 registry *goens.Registry 54 } 55 56 // Option is a function that applies an option to a Client. 57 type Option func(*Client) 58 59 // NewClient will return a new Client. 60 func NewClient(endpoint string, opts ...Option) (client.Interface, error) { 61 c := &Client{ 62 endpoint: endpoint, 63 connectFn: wrapDial, 64 resolveFn: wrapResolve, 65 } 66 67 // Apply all options to the Client. 68 for _, o := range opts { 69 o(c) 70 } 71 72 // Set the default ENS contract address. 73 if c.contractAddr == "" { 74 c.contractAddr = defaultENSContractAddress 75 } 76 77 // Establish a connection to the ENS. 78 if c.connectFn == nil { 79 return nil, fmt.Errorf("connectFn: %w", ErrNotImplemented) 80 } 81 ethCl, registry, err := c.connectFn(c.endpoint, c.contractAddr) 82 if err != nil { 83 return nil, fmt.Errorf("%w: %w", err, ErrFailedToConnect) 84 } 85 c.ethCl = ethCl 86 c.registry = registry 87 88 return c, nil 89 } 90 91 // WithContractAddress will set the ENS contract address. 92 func WithContractAddress(addr string) Option { 93 return func(c *Client) { 94 c.contractAddr = addr 95 } 96 } 97 98 // IsConnected returns true if there is an active RPC connection with an 99 // Ethereum node at the configured endpoint. 100 func (c *Client) IsConnected() bool { 101 return c.ethCl != nil 102 } 103 104 // Endpoint returns the endpoint the client was connected to. 105 func (c *Client) Endpoint() string { 106 return c.endpoint 107 } 108 109 // Resolve implements the resolver.Client interface. 110 func (c *Client) Resolve(name string) (Address, error) { 111 if c.resolveFn == nil { 112 return swarm.ZeroAddress, fmt.Errorf("resolveFn: %w", ErrNotImplemented) 113 } 114 115 hash, err := c.resolveFn(c.registry, common.HexToAddress(c.contractAddr), name) 116 if err != nil { 117 return swarm.ZeroAddress, fmt.Errorf("%w: %w", err, ErrResolveFailed) 118 } 119 120 // Ensure that the content hash string is in a valid format, eg. 121 // "bzz://<address>". 122 if !strings.HasPrefix(hash, swarmContentHashPrefix) { 123 return swarm.ZeroAddress, fmt.Errorf("check content hash prefix %s: %w", hash, resolver.ErrInvalidContentHash) 124 } 125 126 // Trim the prefix and try to parse the result as a bzz address. 127 addr, err := swarm.ParseHexAddress(strings.TrimPrefix(hash, swarmContentHashPrefix)) 128 if err != nil { 129 return swarm.ZeroAddress, fmt.Errorf("parse response hash %s: %w", hash, resolver.ErrInvalidContentHash) 130 } 131 132 return addr, nil 133 } 134 135 // Close closes the RPC connection with the client, terminating all unfinished 136 // requests. If the connection is already closed, this call is a noop. 137 func (c *Client) Close() error { 138 if c.ethCl != nil { 139 c.ethCl.Close() 140 141 } 142 c.ethCl = nil 143 144 return nil 145 } 146 147 func wrapDial(endpoint, contractAddr string) (*ethclient.Client, *goens.Registry, error) { 148 // Dial the eth client. 149 ethCl, err := ethclient.Dial(endpoint) 150 if err != nil { 151 return nil, nil, fmt.Errorf("dial: %w", err) 152 } 153 154 // Obtain the ENS registry. 155 registry, err := goens.NewRegistryAt(ethCl, common.HexToAddress(contractAddr)) 156 if err != nil { 157 return nil, nil, fmt.Errorf("new registry: %w", err) 158 } 159 160 // Ensure that the ENS registry client is deployed to the given contract address. 161 _, err = registry.Owner("") 162 if err != nil { 163 return nil, nil, fmt.Errorf("owner: %w", err) 164 } 165 166 return ethCl, registry, nil 167 } 168 169 func wrapResolve(registry *goens.Registry, _ common.Address, name string) (string, error) { 170 // Ensure the name is registered. 171 ownerAddress, err := registry.Owner(name) 172 if err != nil { 173 return "", fmt.Errorf("owner: %w: %w", err, resolver.ErrNotFound) 174 } 175 176 // If the name is not registered, return an error. 177 if bytes.Equal(ownerAddress.Bytes(), goens.UnknownAddress.Bytes()) { 178 return "", fmt.Errorf("%w: %w", errNameNotRegistered, resolver.ErrNotFound) 179 } 180 181 // Obtain the resolver for this domain name. 182 ensR, err := registry.Resolver(name) 183 if err != nil { 184 return "", fmt.Errorf("resolver: %w: %w", err, resolver.ErrServiceNotAvailable) 185 } 186 187 // Try and read out the content hash record. 188 ch, err := ensR.Contenthash() 189 if err != nil { 190 return "", fmt.Errorf("contenthash: %w: %w", err, resolver.ErrInvalidContentHash) 191 } 192 193 addr, err := goens.ContenthashToString(ch) 194 if err != nil { 195 return "", fmt.Errorf("contenthash to string: %w: %w", err, resolver.ErrInvalidContentHash) 196 } 197 198 return addr, nil 199 }