mirror of
https://github.com/status-im/status-go.git
synced 2026-08-31 09:01:16 +00:00
Part of the Go project layout migration, item 31. Pure move plus import-path rewrite across 687 files. No API or behaviour change. The services keep their grouping under pkg/services/<name> rather than being promoted to pkg/<name>: 27 top-level directories in pkg/ would read worse than what we have, and the grouping is what makes "an RPC service" identifiable at a glance. Paths that follow the move: the logosstorage test target and generate step, the two wallet token-list tools, the migration-order check (and the pre-rebase hook symlinked to it), and the storage env helper. refs #7067
230 lines
6.6 KiB
Go
230 lines
6.6 KiB
Go
package ens
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"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/common"
|
|
|
|
accsmanagement "github.com/status-im/status-go/internal/accounts-management"
|
|
"github.com/status-im/status-go/internal/ipfs"
|
|
"github.com/status-im/status-go/internal/rpc"
|
|
"github.com/status-im/status-go/params"
|
|
"github.com/status-im/status-go/pkg/services/ens/ensresolver"
|
|
"github.com/status-im/status-go/pkg/services/wallet/pendingtxtracker"
|
|
)
|
|
|
|
func NewAPI(rpcClient *rpc.Client, accountsManager *accsmanagement.AccountsManager, pendingTracker *pendingtxtracker.PendingTxTracker,
|
|
config *params.NodeConfig, appDb *sql.DB, timeSource func() time.Time, syncUserDetailFunc *syncUsernameDetail) *API {
|
|
return &API{
|
|
ensResolver: ensresolver.NewEnsResolver(rpcClient),
|
|
|
|
accountsManager: accountsManager,
|
|
pendingTracker: pendingTracker,
|
|
config: config,
|
|
db: NewEnsDatabase(appDb),
|
|
|
|
timeSource: timeSource,
|
|
syncUserDetailFunc: syncUserDetailFunc,
|
|
}
|
|
}
|
|
|
|
func ValidateUsername(username string) error {
|
|
// Validate ENS name format: allows subdomains (e.g., subdomain.domain.eth)
|
|
ensPattern := `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.eth$`
|
|
matched, err := regexp.MatchString(ensPattern, username)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to validate username: %w", err)
|
|
}
|
|
if !matched {
|
|
return fmt.Errorf("user name must be a valid ENS name ending with .eth")
|
|
}
|
|
// Make sure the username does not end with .stateofus.eth.stateofus.eth, which happens due to a bug in some older clients
|
|
if strings.HasSuffix(username, ".stateofus.eth.stateofus.eth") {
|
|
return fmt.Errorf("user name must not have a duplicated domain")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type URI struct {
|
|
Scheme string
|
|
Host string
|
|
Path string
|
|
}
|
|
|
|
// use this to avoid using messenger directly to avoid circular dependency (protocol->ens->protocol)
|
|
type syncUsernameDetail func(context.Context, *UsernameDetail) error
|
|
|
|
type API struct {
|
|
ensResolver *ensresolver.EnsResolver
|
|
accountsManager *accsmanagement.AccountsManager
|
|
pendingTracker *pendingtxtracker.PendingTxTracker
|
|
config *params.NodeConfig
|
|
|
|
db *Database
|
|
syncUserDetailFunc *syncUsernameDetail
|
|
|
|
timeSource func() time.Time
|
|
}
|
|
|
|
func (api *API) Stop() {
|
|
api.ensResolver.Stop()
|
|
}
|
|
|
|
func (api *API) EnsResolver() *ensresolver.EnsResolver {
|
|
return api.ensResolver
|
|
}
|
|
|
|
func (api *API) unixTime() uint64 {
|
|
return uint64(api.timeSource().Unix())
|
|
}
|
|
|
|
func (api *API) GetEnsUsernames(ctx context.Context) ([]*UsernameDetail, error) {
|
|
removed := false
|
|
return api.db.GetEnsUsernames(&removed)
|
|
}
|
|
|
|
func (api *API) Add(ctx context.Context, chainID uint64, username string) error {
|
|
if err := ValidateUsername(username); err != nil {
|
|
return err
|
|
}
|
|
ud := &UsernameDetail{Username: username, ChainID: chainID, Clock: api.unixTime()}
|
|
err := api.db.AddEnsUsername(ud)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return (*api.syncUserDetailFunc)(ctx, ud)
|
|
}
|
|
|
|
func (api *API) Remove(ctx context.Context, chainID uint64, username string) error {
|
|
ud := &UsernameDetail{Username: username, ChainID: chainID, Clock: api.unixTime()}
|
|
affected, err := api.db.RemoveEnsUsername(ud)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected {
|
|
return (*api.syncUserDetailFunc)(ctx, ud)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (api *API) GetRegistrarAddress(ctx context.Context, chainID uint64) (common.Address, error) {
|
|
return api.ensResolver.GetRegistrarAddress(ctx, chainID)
|
|
}
|
|
|
|
func (api *API) Resolver(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
|
|
return api.ensResolver.Resolver(ctx, chainID, username)
|
|
}
|
|
|
|
func (api *API) GetName(ctx context.Context, chainID uint64, address common.Address) (string, error) {
|
|
return api.ensResolver.GetName(ctx, chainID, address)
|
|
}
|
|
|
|
func (api *API) OwnerOf(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
|
|
return api.ensResolver.OwnerOf(ctx, chainID, username)
|
|
}
|
|
|
|
func (api *API) ContentHash(ctx context.Context, chainID uint64, username string) ([]byte, error) {
|
|
return api.ensResolver.ContentHash(ctx, chainID, username)
|
|
}
|
|
|
|
func (api *API) PublicKeyOf(ctx context.Context, chainID uint64, username string) (string, error) {
|
|
return api.ensResolver.PublicKeyOf(ctx, chainID, username)
|
|
}
|
|
|
|
func (api *API) AddressOf(ctx context.Context, chainID uint64, username string) (*common.Address, error) {
|
|
return api.ensResolver.AddressOf(ctx, chainID, username)
|
|
}
|
|
|
|
func (api *API) ExpireAt(ctx context.Context, chainID uint64, username string) (string, error) {
|
|
return api.ensResolver.ExpireAt(ctx, chainID, username)
|
|
}
|
|
|
|
func (api *API) Price(ctx context.Context, chainID uint64) (string, error) {
|
|
return api.ensResolver.Price(ctx, chainID)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
parsedURL, _ := url.Parse(ipfs.GatewayURL)
|
|
// Remove scheme from the url
|
|
host := parsedURL.Hostname() + parsedURL.Path + str
|
|
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)
|
|
}
|
|
}
|