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  }