Commit 99cf7b95 authored by Pavle Batuta's avatar Pavle Batuta Committed by GitHub

Add ENS contract address parameter to config (#1029)

parent aad68011
......@@ -5,11 +5,12 @@
package ens
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
goens "github.com/wealdtech/go-ens/v3"
......@@ -17,7 +18,10 @@ import (
"github.com/ethersphere/bee/pkg/swarm"
)
const swarmContentHashPrefix = "/swarm/"
const (
defaultENSContractAddress = "00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
swarmContentHashPrefix = "/swarm/"
)
// Address is the swarm bzz address.
type Address = swarm.Address
......@@ -36,15 +40,19 @@ var (
ErrInvalidContentHash = errors.New("invalid swarm content hash")
// errNotImplemented denotes that the function has not been implemented.
errNotImplemented = errors.New("function not implemented")
// errNameNotRegistered denotes that the name is not registered.
errNameNotRegistered = errors.New("name is not registered")
)
// Client is a name resolution client that can connect to ENS via an
// Ethereum endpoint.
type Client struct {
endpoint string
ethCl *ethclient.Client
dialFn func(string) (*ethclient.Client, error)
resolveFn func(bind.ContractBackend, string) (string, error)
endpoint string
contractAddr string
ethCl *ethclient.Client
connectFn func(string, string) (*ethclient.Client, *goens.Registry, error)
resolveFn func(*goens.Registry, common.Address, string) (string, error)
registry *goens.Registry
}
// Option is a function that applies an option to a Client.
......@@ -54,7 +62,7 @@ type Option func(*Client)
func NewClient(endpoint string, opts ...Option) (client.Interface, error) {
c := &Client{
endpoint: endpoint,
dialFn: ethclient.Dial,
connectFn: wrapDial,
resolveFn: wrapResolve,
}
......@@ -63,20 +71,32 @@ func NewClient(endpoint string, opts ...Option) (client.Interface, error) {
o(c)
}
// Connect to the name resolution service.
if c.dialFn == nil {
return nil, fmt.Errorf("dialFn: %w", errNotImplemented)
// Set the default ENS contract address.
if c.contractAddr == "" {
c.contractAddr = defaultENSContractAddress
}
ethCl, err := c.dialFn(c.endpoint)
// Establish a connection to the ENS.
if c.connectFn == nil {
return nil, fmt.Errorf("connectFn: %w", errNotImplemented)
}
ethCl, registry, err := c.connectFn(c.endpoint, c.contractAddr)
if err != nil {
return nil, fmt.Errorf("%v: %w", err, ErrFailedToConnect)
}
c.ethCl = ethCl
c.registry = registry
return c, nil
}
// WithContractAddress will set the ENS contract address.
func WithContractAddress(addr string) Option {
return func(c *Client) {
c.contractAddr = addr
}
}
// IsConnected returns true if there is an active RPC connection with an
// Ethereum node at the configured endpoint.
func (c *Client) IsConnected() bool {
......@@ -94,7 +114,7 @@ func (c *Client) Resolve(name string) (Address, error) {
return swarm.ZeroAddress, fmt.Errorf("resolveFn: %w", errNotImplemented)
}
hash, err := c.resolveFn(c.ethCl, name)
hash, err := c.resolveFn(c.registry, common.HexToAddress(c.contractAddr), name)
if err != nil {
return swarm.ZeroAddress, fmt.Errorf("%v: %w", err, ErrResolveFailed)
}
......@@ -121,18 +141,50 @@ func (c *Client) Close() error {
return nil
}
func wrapResolve(backend bind.ContractBackend, name string) (string, error) {
func wrapDial(endpoint string, contractAddr string) (*ethclient.Client, *goens.Registry, error) {
// Dial the eth client.
ethCl, err := ethclient.Dial(endpoint)
if err != nil {
return nil, nil, fmt.Errorf("dial: %w", err)
}
// Obtain the ENS registry.
registry, err := goens.NewRegistryAt(ethCl, common.HexToAddress(contractAddr))
if err != nil {
return nil, nil, fmt.Errorf("new registry: %w", err)
}
// Ensure that the ENS registry client is deployed to the given contract address.
_, err = registry.Owner("")
if err != nil {
return nil, nil, fmt.Errorf("owner: %w", err)
}
return ethCl, registry, nil
}
func wrapResolve(registry *goens.Registry, addr common.Address, name string) (string, error) {
// Ensure the name is registered.
ownerAddress, err := registry.Owner(name)
if err != nil {
return "", fmt.Errorf("owner: %w", err)
}
// If the name is not registered, return an error.
if bytes.Equal(ownerAddress.Bytes(), goens.UnknownAddress.Bytes()) {
return "", errNameNotRegistered
}
// Connect to the ENS resolver for the provided name.
ensR, err := goens.NewResolver(backend, name)
// Obtain the resolver for this domain name.
ensR, err := registry.Resolver(name)
if err != nil {
return "", err
return "", fmt.Errorf("resolver: %w", err)
}
// Try and read out the content hash record.
ch, err := ensR.Contenthash()
if err != nil {
return "", err
return "", fmt.Errorf("contenthash: %w", err)
}
return goens.ContenthashToString(ch)
......
......@@ -20,11 +20,12 @@ func TestENSntegration(t *testing.T) {
defaultAddr := swarm.MustParseHexAddress("00cb23598c2e520b6a6aae3ddc94fed4435a2909690bdd709bf9d9e7c2aadfad")
testCases := []struct {
desc string
endpoint string
name string
wantAdr swarm.Address
wantErr error
desc string
endpoint string
contractAddress string
name string
wantAdr swarm.Address
wantErr error
}{
// TODO: add a test targeting a resolver with an invalid contenthash
// record.
......@@ -53,6 +54,12 @@ func TestENSntegration(t *testing.T) {
name: "nocontent.resolver.test.swarm.eth",
wantErr: ens.ErrResolveFailed,
},
{
desc: "invalid contract address",
contractAddress: "0xFFFFFFFF",
name: "example.resolver.test.swarm.eth",
wantErr: ens.ErrFailedToConnect,
},
{
desc: "ok",
name: "example.resolver.test.swarm.eth",
......@@ -65,9 +72,9 @@ func TestENSntegration(t *testing.T) {
tC.endpoint = defaultEndpoint
}
ensClient, err := ens.NewClient(tC.endpoint)
ensClient, err := ens.NewClient(tC.endpoint, ens.WithContractAddress(tC.contractAddress))
if err != nil {
if !errors.Is(err, ens.ErrFailedToConnect) {
if !errors.Is(err, tC.wantErr) {
t.Errorf("got %v, want %v", err, tC.wantErr)
}
return
......
......@@ -8,9 +8,10 @@ import (
"errors"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
goens "github.com/wealdtech/go-ens/v3"
"github.com/ethersphere/bee/pkg/resolver/client/ens"
"github.com/ethersphere/bee/pkg/swarm"
......@@ -20,29 +21,30 @@ func TestNewENSClient(t *testing.T) {
testCases := []struct {
desc string
endpoint string
dialFn func(string) (*ethclient.Client, error)
address string
connectFn func(string, string) (*ethclient.Client, *goens.Registry, error)
wantErr error
wantEndpoint string
}{
{
desc: "nil dial function",
endpoint: "someaddress.net",
dialFn: nil,
wantErr: ens.ErrNotImplemented,
desc: "nil dial function",
endpoint: "someaddress.net",
connectFn: nil,
wantErr: ens.ErrNotImplemented,
},
{
desc: "error in dial function",
endpoint: "someaddress.com",
dialFn: func(string) (*ethclient.Client, error) {
return nil, errors.New("dial error")
connectFn: func(s1, s2 string) (*ethclient.Client, *goens.Registry, error) {
return nil, nil, errors.New("dial error")
},
wantErr: ens.ErrFailedToConnect,
},
{
desc: "regular endpoint",
endpoint: "someaddress.org",
dialFn: func(string) (*ethclient.Client, error) {
return &ethclient.Client{}, nil
connectFn: func(s1, s2 string) (*ethclient.Client, *goens.Registry, error) {
return &ethclient.Client{}, nil, nil
},
wantEndpoint: "someaddress.org",
},
......@@ -50,7 +52,8 @@ func TestNewENSClient(t *testing.T) {
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
cl, err := ens.NewClient(tC.endpoint,
ens.WithDialFunc(tC.dialFn),
ens.WithConnectFunc(tC.connectFn),
ens.WithContractAddress(tC.address),
)
if err != nil {
if !errors.Is(err, tC.wantErr) {
......@@ -75,8 +78,8 @@ func TestClose(t *testing.T) {
ethCl := ethclient.NewClient(rpc.DialInProc(rpcServer))
cl, err := ens.NewClient("",
ens.WithDialFunc(func(string) (*ethclient.Client, error) {
return ethCl, nil
ens.WithConnectFunc(func(endpoint, contractAddr string) (*ethclient.Client, *goens.Registry, error) {
return ethCl, nil, nil
}),
)
if err != nil {
......@@ -94,8 +97,8 @@ func TestClose(t *testing.T) {
})
t.Run("not connected", func(t *testing.T) {
cl, err := ens.NewClient("",
ens.WithDialFunc(func(string) (*ethclient.Client, error) {
return nil, nil
ens.WithConnectFunc(func(endpoint, contractAddr string) (*ethclient.Client, *goens.Registry, error) {
return nil, nil, nil
}),
)
if err != nil {
......@@ -114,13 +117,16 @@ func TestClose(t *testing.T) {
}
func TestResolve(t *testing.T) {
addr := swarm.MustParseHexAddress("aaabbbcc")
testContractAddrString := "00000000000C2E074eC69A0dFb2997BA6C702e1B"
testContractAddr := common.HexToAddress(testContractAddrString)
testSwarmAddr := swarm.MustParseHexAddress("aaabbbcc")
testCases := []struct {
desc string
name string
resolveFn func(bind.ContractBackend, string) (string, error)
wantErr error
desc string
name string
contractAddr string
resolveFn func(*goens.Registry, common.Address, string) (string, error)
wantErr error
}{
{
desc: "nil resolve function",
......@@ -129,38 +135,48 @@ func TestResolve(t *testing.T) {
},
{
desc: "resolve function internal error",
resolveFn: func(bind.ContractBackend, string) (string, error) {
resolveFn: func(*goens.Registry, common.Address, string) (string, error) {
return "", errors.New("internal error")
},
wantErr: ens.ErrResolveFailed,
},
{
desc: "resolver returns empty string",
resolveFn: func(bind.ContractBackend, string) (string, error) {
resolveFn: func(*goens.Registry, common.Address, string) (string, error) {
return "", nil
},
wantErr: ens.ErrInvalidContentHash,
},
{
desc: "resolve does not prefix address with /swarm",
resolveFn: func(bind.ContractBackend, string) (string, error) {
return addr.String(), nil
resolveFn: func(*goens.Registry, common.Address, string) (string, error) {
return testSwarmAddr.String(), nil
},
wantErr: ens.ErrInvalidContentHash,
},
{
desc: "resolve returns prefixed address",
resolveFn: func(bind.ContractBackend, string) (string, error) {
return ens.SwarmContentHashPrefix + addr.String(), nil
resolveFn: func(*goens.Registry, common.Address, string) (string, error) {
return ens.SwarmContentHashPrefix + testSwarmAddr.String(), nil
},
wantErr: ens.ErrInvalidContentHash,
},
{
desc: "expect properly set contract address",
resolveFn: func(b *goens.Registry, c common.Address, s string) (string, error) {
if c != testContractAddr {
return "", errors.New("invalid contract address")
}
return ens.SwarmContentHashPrefix + testSwarmAddr.String(), nil
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
cl, err := ens.NewClient("example.com",
ens.WithDialFunc(func(string) (*ethclient.Client, error) {
return nil, nil
ens.WithContractAddress(testContractAddrString),
ens.WithConnectFunc(func(endpoint, contractAddr string) (*ethclient.Client, *goens.Registry, error) {
return nil, nil, nil
}),
ens.WithResolveFunc(tC.resolveFn),
)
......
......@@ -5,23 +5,24 @@
package ens
import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
goens "github.com/wealdtech/go-ens/v3"
)
const SwarmContentHashPrefix = swarmContentHashPrefix
var ErrNotImplemented = errNotImplemented
// WithDialFunc will set the Dial function implementaton.
func WithDialFunc(fn func(ep string) (*ethclient.Client, error)) Option {
// WithConnectFunc will set the Dial function implementaton.
func WithConnectFunc(fn func(endpoint string, contractAddr string) (*ethclient.Client, *goens.Registry, error)) Option {
return func(c *Client) {
c.dialFn = fn
c.connectFn = fn
}
}
// WithResolveFunc will set the Resolve function implementation.
func WithResolveFunc(fn func(backend bind.ContractBackend, input string) (string, error)) Option {
func WithResolveFunc(fn func(registry *goens.Registry, addr common.Address, input string) (string, error)) Option {
return func(c *Client) {
c.resolveFn = fn
}
......
......@@ -74,14 +74,9 @@ func NewMultiResolver(opts ...Option) *MultiResolver {
// Attempt to conect to each resolver using the connection string.
for _, c := range mr.cfgs {
// Warn user that the resolver address field is not used.
if c.Address != "" {
log.Warningf("name resolver: connection string %q contains resolver address field, which is currently unused", c.Address)
}
// NOTE: if we want to create a specific client based on the TLD
// we can do it here.
mr.connectENSClient(c.TLD, c.Endpoint)
mr.connectENSClient(c.TLD, c.Address, c.Endpoint)
}
return mr
......@@ -189,13 +184,18 @@ func getTLD(name string) string {
return path.Ext(strings.ToLower(name))
}
func (mr *MultiResolver) connectENSClient(tld string, endpoint string) {
func (mr *MultiResolver) connectENSClient(tld string, address string, endpoint string) {
log := mr.logger
log.Debugf("name resolver: resolver for %q: connecting to endpoint %s", tld, endpoint)
ensCl, err := ens.NewClient(endpoint)
if address == "" {
log.Debugf("name resolver: resolver for %q: connecting to endpoint %s", tld, endpoint)
} else {
log.Debugf("name resolver: resolver for %q: connecting to endpoint %s with contract address %s", tld, endpoint, address)
}
ensCl, err := ens.NewClient(endpoint, ens.WithContractAddress(address))
if err != nil {
log.Errorf("name resolver: resolver for %q domain: failed to connect to %q: %v", tld, endpoint, err)
log.Errorf("name resolver: resolver for %q domain on endpoint %q: %v", tld, endpoint, err)
} else {
log.Infof("name resolver: resolver for %q domain: connected to %s", tld, endpoint)
mr.PushResolver(tld, ensCl)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment