feat: Add ens service (#2467)

This commit is contained in:
Anthony Laibe 2021-12-21 16:05:09 +01:00 committed by GitHub
parent b244188702
commit 12c727df25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 19955 additions and 0 deletions

3
go.mod
View File

@ -22,6 +22,7 @@ require (
github.com/golang/protobuf v1.5.2
github.com/google/uuid v1.3.0
github.com/imdario/mergo v0.3.12
github.com/ipfs/go-cid v0.0.7
github.com/ipfs/go-ds-sql v0.2.0
github.com/ipfs/go-log v1.0.5
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
@ -38,6 +39,7 @@ require (
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/multiformats/go-multiaddr v0.4.0
github.com/multiformats/go-multibase v0.0.3
github.com/multiformats/go-multihash v0.0.15
github.com/multiformats/go-varint v0.0.6
github.com/mutecomm/go-sqlcipher v0.0.0-20190227152316-55dbde17881f
github.com/nfnt/resize v0.0.0-00010101000000-000000000000
@ -62,6 +64,7 @@ require (
github.com/tsenart/tb v0.0.0-20181025101425-0d2499c8b6e9
github.com/vacp2p/mvds v0.0.24-0.20201124060106-26d8e94130d8
github.com/wealdtech/go-ens/v3 v3.5.0
github.com/wealdtech/go-multicodec v1.4.0
github.com/xeipuuv/gojsonschema v1.2.0
go.uber.org/zap v1.19.0
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e

View File

@ -32,6 +32,7 @@ import (
accountssvc "github.com/status-im/status-go/services/accounts"
appmetricsservice "github.com/status-im/status-go/services/appmetrics"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/ens"
localnotifications "github.com/status-im/status-go/services/local-notifications"
"github.com/status-im/status-go/services/mailservers"
"github.com/status-im/status-go/services/peer"
@ -105,6 +106,7 @@ type StatusNode struct {
wakuExtSrvc *wakuext.Service
wakuV2Srvc *wakuv2.Waku
wakuV2ExtSrvc *wakuv2ext.Service
ensSrvc *ens.Service
}
// New makes new instance of StatusNode.
@ -411,6 +413,7 @@ func (n *StatusNode) stop() error {
n.wakuExtSrvc = nil
n.wakuV2Srvc = nil
n.wakuV2ExtSrvc = nil
n.ensSrvc = nil
n.publicMethods = make(map[string]bool)
return nil

View File

@ -24,6 +24,7 @@ import (
accountssvc "github.com/status-im/status-go/services/accounts"
appmetricsservice "github.com/status-im/status-go/services/appmetrics"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/ens"
"github.com/status-im/status-go/services/ext"
localnotifications "github.com/status-im/status-go/services/local-notifications"
"github.com/status-im/status-go/services/mailservers"
@ -65,6 +66,7 @@ func (b *StatusNode) initServices(config *params.NodeConfig) error {
services = appendIf(config.EnableNTPSync, services, b.timeSource())
services = appendIf(b.appDB != nil && b.multiaccountsDB != nil, services, b.accountsService(accountsFeed))
services = appendIf(config.BrowsersConfig.Enabled, services, b.browsersService())
services = appendIf(config.ENSConfig.Enabled, services, b.ensService())
services = appendIf(config.PermissionsConfig.Enabled, services, b.permissionsService())
services = appendIf(config.MailserversConfig.Enabled, services, b.mailserversService())
if config.WakuConfig.Enabled {
@ -351,6 +353,13 @@ func (b *StatusNode) browsersService() *browsers.Service {
return b.browsersSrvc
}
func (b *StatusNode) ensService() *ens.Service {
if b.ensSrvc == nil {
b.ensSrvc = ens.NewService(b.rpcClient)
}
return b.ensSrvc
}
func (b *StatusNode) permissionsService() *permissions.Service {
if b.permissionsSrvc == nil {
b.permissionsSrvc = permissions.NewService(permissions.NewDB(b.appDB))

View File

@ -502,6 +502,9 @@ type NodeConfig struct {
// BrowsersConfig extra configuration for browsers.Service.
BrowsersConfig BrowsersConfig
// ENSConfig extra configuration for ens.Service.
ENSConfig ENSConfig `json:"EnsConfig" validate:"structonly"`
// PermissionsConfig extra configuration for permissions.Service.
PermissionsConfig PermissionsConfig
@ -542,6 +545,11 @@ type BrowsersConfig struct {
Enabled bool
}
// ENSConfig extra configuration for ens.Service.
type ENSConfig struct {
Enabled bool
}
// PermissionsConfig extra configuration for permissions.Service.
type PermissionsConfig struct {
Enabled bool

321
services/ens/api.go Normal file
View File

@ -0,0 +1,321 @@
package ens
import (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"github.com/ipfs/go-cid"
"github.com/multiformats/go-multibase"
"github.com/multiformats/go-multihash"
"github.com/pkg/errors"
"github.com/wealdtech/go-multicodec"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/rpc"
)
func NewAPI(rpcClient *rpc.Client) *API {
return &API{
contractMaker: &contractMaker{
rpcClient: rpcClient,
},
}
}
type uri struct {
Scheme string
Host string
Path string
}
type publicKey struct {
X [32]byte
Y [32]byte
}
type API struct {
contractMaker *contractMaker
}
func (api *API) Resolver(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
registry, err := api.contractMaker.newRegistry(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
resolver, err := registry.Resolver(callOpts, nameHash(username))
if err != nil {
return nil, err
}
return &resolver, nil
}
func (api *API) OwnerOf(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
registry, err := api.contractMaker.newRegistry(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
owner, err := registry.Owner(callOpts, nameHash(username))
if err != nil {
return nil, nil
}
return &owner, nil
}
func (api *API) ContentHash(ctx context.Context, chainID uint64, username string) ([]byte, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return nil, err
}
resolver, err := api.contractMaker.newPublicResolver(chainID, resolverAddress)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contentHash, err := resolver.Contenthash(callOpts, nameHash(username))
if err != nil {
return nil, nil
}
return contentHash, nil
}
func (api *API) PublicKeyOf(ctx context.Context, chainID uint64, username string) (*publicKey, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return nil, err
}
resolver, err := api.contractMaker.newPublicResolver(chainID, resolverAddress)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
pubKey, err := resolver.Pubkey(callOpts, nameHash(username))
if err != nil {
return nil, err
}
return &publicKey{pubKey.X, pubKey.Y}, nil
}
func (api *API) AddressOf(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
resolverAddress, err := api.Resolver(ctx, chainID, username)
if err != nil {
return nil, err
}
resolver, err := api.contractMaker.newPublicResolver(chainID, resolverAddress)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
addr, err := resolver.Addr(callOpts, nameHash(username))
if err != nil {
return nil, err
}
return &addr, nil
}
func (api *API) ExpireAt(ctx context.Context, chainID uint64, username string) (*big.Int, error) {
err := validateENSUsername(username)
if err != nil {
return nil, err
}
registrar, err := api.contractMaker.newUsernameRegistrar(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
expTime, err := registrar.GetExpirationTime(callOpts, nameHash(username))
if err != nil {
return nil, err
}
return expTime, nil
}
func (api *API) Price(ctx context.Context, chainID uint64) (*big.Int, error) {
registrar, err := api.contractMaker.newUsernameRegistrar(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
price, err := registrar.GetPrice(callOpts)
if err != nil {
return nil, err
}
return price, nil
}
// TODO: implement once the send tx as been refactored
// func (api *API) Release(ctx context.Context, chainID uint64, from string, gasPrice *big.Int, gasLimit uint64, password string, username string) (string, error) {
// err := validateENSUsername(username)
// if err != nil {
// return "", err
// }
// registrar, err := api.contractMaker.newUsernameRegistrar(chainID)
// if err != nil {
// return "", err
// }
// txOpts := &bind.TransactOpts{
// From: common.HexToAddress(from),
// Signer: func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
// // return types.SignTx(tx, types.NewLondonSigner(chainID), selectedAccount.AccountKey.PrivateKey)
// return nil, nil
// },
// GasPrice: gasPrice,
// GasLimit: gasLimit,
// }
// tx, err := registrar.Release(txOpts, nameHash(username))
// if err != nil {
// return "", err
// }
// return tx.Hash().String(), nil
// }
// func (api *API) Register(ctx context.Context, chainID uint64, from string, gasPrice *big.Int, gasLimit uint64, password string, username string, x [32]byte, y [32]byte) (string, error) {
// err := validateENSUsername(username)
// if err != nil {
// return "", err
// }
// registrar, err := api.contractMaker.newUsernameRegistrar(chainID)
// if err != nil {
// return "", err
// }
// txOpts := &bind.TransactOpts{
// From: common.HexToAddress(from),
// Signer: func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
// // return types.SignTx(tx, types.NewLondonSigner(chainID), selectedAccount.AccountKey.PrivateKey)
// return nil, nil
// },
// GasPrice: gasPrice,
// GasLimit: gasLimit,
// }
// tx, err := registrar.Register(
// txOpts,
// nameHash(username),
// common.HexToAddress(from),
// x,
// y,
// )
// if err != nil {
// return "", err
// }
// return tx.Hash().String(), nil
// }
func (api *API) ResourceURL(ctx context.Context, chainID uint64, username string) (*uri, error) {
scheme := "https"
contentHash, err := api.ContentHash(ctx, chainID, username)
if err != nil {
return nil, err
}
if len(contentHash) == 0 {
return &uri{}, nil
}
data, codec, err := multicodec.RemoveCodec(contentHash)
if err != nil {
return nil, err
}
codecName, err := multicodec.Name(codec)
if err != nil {
return nil, err
}
switch codecName {
case "ipfs-ns":
thisCID, err := cid.Parse(data)
if err != nil {
return nil, errors.Wrap(err, "failed to parse CID")
}
str, err := thisCID.StringOfBase(multibase.Base32)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain base36 representation")
}
host := str + ".ipfs.cf-ipfs.com"
return &uri{scheme, host, ""}, nil
case "ipns-ns":
id, offset := binary.Uvarint(data)
if id == 0 {
return nil, fmt.Errorf("unknown CID")
}
data, _, err := multicodec.RemoveCodec(data[offset:])
if err != nil {
return nil, err
}
decodedMHash, err := multihash.Decode(data)
if err != nil {
return nil, err
}
return &uri{scheme, string(decodedMHash.Digest), ""}, nil
case "swarm-ns":
id, offset := binary.Uvarint(data)
if id == 0 {
return nil, fmt.Errorf("unknown CID")
}
data, _, err := multicodec.RemoveCodec(data[offset:])
if err != nil {
return nil, err
}
decodedMHash, err := multihash.Decode(data)
if err != nil {
return nil, err
}
path := "/bzz:/" + hex.EncodeToString(decodedMHash.Digest) + "/"
return &uri{scheme, "swarm-gateways.net", path}, nil
default:
return nil, fmt.Errorf("unknown codec name %s", codecName)
}
}

148
services/ens/api_test.go Normal file
View File

@ -0,0 +1,148 @@
package ens
import (
"context"
"database/sql"
"io/ioutil"
"os"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/params"
statusRPC "github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/t/utils"
"github.com/status-im/status-go/transactions/fake"
)
func createDB(t *testing.T) (*sql.DB, func()) {
tmpfile, err := ioutil.TempFile("", "service-ens-tests-")
require.NoError(t, err)
db, err := appdatabase.InitializeDB(tmpfile.Name(), "service-ens-tests")
require.NoError(t, err)
return db, func() {
require.NoError(t, db.Close())
require.NoError(t, os.Remove(tmpfile.Name()))
}
}
func setupTestAPI(t *testing.T) (*API, func()) {
db, cancel := createDB(t)
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
require.NoError(t, err)
// Creating a dummy status node to simulate what it's done in get_status_node.go
upstreamConfig := params.UpstreamRPCConfig{
URL: "https://mainnet.infura.io/v3/800c641949d64d768a5070a1b0511938",
Enabled: true,
}
txServiceMockCtrl := gomock.NewController(t)
server, _ := fake.NewTestServer(txServiceMockCtrl)
client := gethrpc.DialInProc(server)
_ = client
rpcClient, err := statusRPC.NewClient(nil, 1, upstreamConfig, nil, db)
require.NoError(t, err)
// import account keys
utils.Init()
require.NoError(t, utils.ImportTestAccount(keyStoreDir, utils.GetAccount1PKFile()))
return NewAPI(rpcClient), cancel
}
func TestResolver(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
r, err := api.Resolver(context.Background(), 1, "rramos.eth")
require.NoError(t, err)
require.Equal(t, "0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41", r.String())
}
func TestOwnerOf(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
r, err := api.OwnerOf(context.Background(), 1, "rramos.eth")
require.NoError(t, err)
require.Equal(t, "0x7d28Ab6948F3Db2F95A43742265D382a4888c120", r.String())
}
func TestContentHash(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
r, err := api.ContentHash(context.Background(), 1, "simpledapp.eth")
require.NoError(t, err)
require.Equal(t, []byte{0xe3, 0x1, 0x1, 0x70, 0x12, 0x20, 0x79, 0x5c, 0x1e, 0xa0, 0xce, 0xaf, 0x4c, 0xee, 0xdc, 0x98, 0x96, 0xf1, 0x4b, 0x73, 0xbb, 0x30, 0xe9, 0x78, 0xe4, 0x85, 0x5e, 0xe2, 0x21, 0xb9, 0xa5, 0x7f, 0x5a, 0x93, 0x42, 0x68, 0x28, 0xe}, r)
}
func TestPublicKeyOf(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
pubKey, err := api.PublicKeyOf(context.Background(), 1, "rramos.eth")
require.NoError(t, err)
require.Equal(
t,
[32]byte{226, 93, 166, 153, 78, 162, 220, 74, 199, 7, 39, 224, 126, 202, 21, 58, 233, 43, 247, 96, 157, 183, 190, 251, 126, 189, 206, 170, 211, 72, 244, 252},
pubKey.X,
)
}
func TestAddressOf(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
r, err := api.AddressOf(context.Background(), 1, "rramos.eth")
require.NoError(t, err)
require.Equal(t, "0x7d28Ab6948F3Db2F95A43742265D382a4888c120", r.String())
}
func TestExpireAt(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
r, err := api.ExpireAt(context.Background(), 1, "rramos.eth")
require.NoError(t, err)
require.Equal(t, "0", r.String())
}
func TestPrice(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
r, err := api.Price(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, "10000000000000000000", r.String())
}
func TestResourceURL(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
uri, err := api.ResourceURL(context.Background(), 1, "simpledapp.eth")
require.NoError(t, err)
require.Equal(t, "https", uri.Scheme)
require.Equal(t, "bafybeidzlqpkbtvpjtxnzgew6ffxhozq5f4ojbk64iq3tjl7lkjue2biby.ipfs.cf-ipfs.com", uri.Host)
require.Equal(t, "", uri.Path)
uri, err = api.ResourceURL(context.Background(), 1, "swarm.eth")
require.NoError(t, err)
require.Equal(t, "https", uri.Scheme)
require.Equal(t, "swarm-gateways.net", uri.Host)
require.Equal(t, "/bzz:/b00909fbabe78f57fda93218323db5721ce256fda442ce02b46813404c6d8958/", uri.Path)
uri, err = api.ResourceURL(context.Background(), 1, "noahzinsmeister.eth")
require.NoError(t, err)
require.Equal(t, "https", uri.Scheme)
require.Equal(t, "noahzinsmeister.com", uri.Host)
require.Equal(t, "", uri.Path)
}

66
services/ens/contracts.go Normal file
View File

@ -0,0 +1,66 @@
package ens
import (
"errors"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/ens/registrar"
"github.com/status-im/status-go/services/ens/resolver"
)
var errorNotAvailableOnChainID = errors.New("not available for chainID")
var resolversByChainID = map[uint64]common.Address{
1: common.HexToAddress("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"), // mainnet
}
var usernameRegistrarsByChainID = map[uint64]common.Address{
1: common.HexToAddress("0xDB5ac1a559b02E12F29fC0eC0e37Be8E046DEF49"), // mainnet
3: common.HexToAddress("0xdaae165beb8c06e0b7613168138ebba774aff071"), // ropsten
}
type contractMaker struct {
rpcClient *rpc.Client
}
func (c *contractMaker) newRegistry(chainID uint64) (*resolver.ENSRegistryWithFallback, error) {
if _, ok := resolversByChainID[chainID]; !ok {
return nil, errorNotAvailableOnChainID
}
backend, err := c.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return resolver.NewENSRegistryWithFallback(
resolversByChainID[chainID],
backend,
)
}
func (c *contractMaker) newPublicResolver(chainID uint64, resolverAddress *common.Address) (*resolver.PublicResolver, error) {
backend, err := c.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return resolver.NewPublicResolver(*resolverAddress, backend)
}
func (c *contractMaker) newUsernameRegistrar(chainID uint64) (*registrar.UsernameRegistrar, error) {
if _, ok := usernameRegistrarsByChainID[chainID]; !ok {
return nil, errorNotAvailableOnChainID
}
backend, err := c.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return registrar.NewUsernameRegistrar(
usernameRegistrarsByChainID[chainID],
backend,
)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
package registrar
//go:generate abigen -sol Registrar.sol -pkg registrar -out registrar.go

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
package resolver
//go:generate abigen -sol ENS.sol -pkg resolver -out resolver.go

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

43
services/ens/service.go Normal file
View File

@ -0,0 +1,43 @@
package ens
import (
"github.com/ethereum/go-ethereum/p2p"
ethRpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/rpc"
)
// NewService initializes service instance.
func NewService(rpcClient *rpc.Client) *Service {
return &Service{rpcClient}
}
// Service is a browsers service.
type Service struct {
rpcClient *rpc.Client
}
// Start a service.
func (s *Service) Start() error {
return nil
}
// Stop a service.
func (s *Service) Stop() error {
return nil
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []ethRpc.API {
return []ethRpc.API{
{
Namespace: "ens",
Version: "0.1.0",
Service: NewAPI(s.rpcClient),
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

32
services/ens/strings.go Normal file
View File

@ -0,0 +1,32 @@
package ens
import (
"fmt"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func nameHash(name string) common.Hash {
node := common.Hash{}
if len(name) > 0 {
labels := strings.Split(name, ".")
for i := len(labels) - 1; i >= 0; i-- {
labelSha := crypto.Keccak256Hash([]byte(labels[i]))
node = crypto.Keccak256Hash(node.Bytes(), labelSha.Bytes())
}
}
return node
}
func validateENSUsername(username string) error {
if !strings.HasSuffix(username, ".eth") {
return fmt.Errorf("username must end with .eth")
}
return nil
}