Files
Igor Sirotin f9cc782a6f refactor: move services to pkg/services
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
2026-08-21 10:11:05 +02:00

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)
}
}