diff --git a/services/wallet/bridge/bridge.go b/services/wallet/bridge/bridge.go index 5b8c5a5f7..ce65e62a8 100644 --- a/services/wallet/bridge/bridge.go +++ b/services/wallet/bridge/bridge.go @@ -2,14 +2,16 @@ package bridge import ( "encoding/hex" + "fmt" "math/big" + "strings" ethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/status-im/status-go/account" - "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" "github.com/status-im/status-go/services/wallet/token" @@ -36,6 +38,7 @@ const ( ERC1155TransferName = "ERC1155Transfer" ENSRegisterName = "ENSRegister" ENSReleaseName = "ENSRelease" + ENSPublicKeyName = "ENSPublicKey" ) func getSigner(chainID uint64, from types.Address, verifiedAccount *account.SelectedExtKey) bind.SignerFn { @@ -152,6 +155,14 @@ type Bridge interface { BuildTx(params BridgeParams) (*ethTypes.Transaction, error) } +func ValidateENSUsername(username string) error { + if !strings.HasSuffix(username, ".eth") { + return fmt.Errorf("username must end with .eth") + } + + return nil +} + func extractCoordinates(pubkey string) ([32]byte, [32]byte) { x, _ := hex.DecodeString(pubkey[4:68]) y, _ := hex.DecodeString(pubkey[68:132]) @@ -172,3 +183,18 @@ func usernameToLabel(username string) [32]byte { return label } + +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 +} diff --git a/services/wallet/bridge/ens_public_key.go b/services/wallet/bridge/ens_public_key.go new file mode 100644 index 000000000..d62e40bb7 --- /dev/null +++ b/services/wallet/bridge/ens_public_key.go @@ -0,0 +1,136 @@ +package bridge + +import ( + "context" + "errors" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/status-im/status-go/account" + "github.com/status-im/status-go/contracts" + "github.com/status-im/status-go/contracts/resolver" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/ens" + walletCommon "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/transactions" +) + +type ENSPublicKeyBridge struct { + contractMaker *contracts.ContractMaker + transactor transactions.TransactorIface + ensService *ens.Service +} + +func NewENSPublicKeyBridge(rpcClient *rpc.Client, transactor transactions.TransactorIface, ensService *ens.Service) *ENSPublicKeyBridge { + return &ENSPublicKeyBridge{ + contractMaker: &contracts.ContractMaker{ + RPCClient: rpcClient, + }, + transactor: transactor, + ensService: ensService, + } +} + +func (s *ENSPublicKeyBridge) Name() string { + return ENSPublicKeyName +} + +func (s *ENSPublicKeyBridge) AvailableFor(params BridgeParams) (bool, error) { + return params.FromChain.ChainID == walletCommon.EthereumMainnet || params.FromChain.ChainID == walletCommon.EthereumSepolia, nil +} + +func (s *ENSPublicKeyBridge) CalculateFees(params BridgeParams) (*big.Int, *big.Int, error) { + return ZeroBigIntValue, ZeroBigIntValue, nil +} + +func (s *ENSPublicKeyBridge) PackTxInputData(params BridgeParams, contractType string) ([]byte, error) { + resolverABI, err := abi.JSON(strings.NewReader(resolver.PublicResolverABI)) + if err != nil { + return []byte{}, err + } + + x, y := extractCoordinates(params.PublicKey) + return resolverABI.Pack("setPubkey", nameHash(params.Username), x, y) +} + +func (s *ENSPublicKeyBridge) EstimateGas(params BridgeParams) (uint64, error) { + contractAddress, err := s.GetContractAddress(params) + if err != nil { + return 0, err + } + + input, err := s.PackTxInputData(params, "") + if err != nil { + return 0, err + } + + ethClient, err := s.contractMaker.RPCClient.EthClient(params.FromChain.ChainID) + if err != nil { + return 0, err + } + + msg := ethereum.CallMsg{ + From: params.FromAddr, + To: &contractAddress, + Value: ZeroBigIntValue, + Data: input, + } + + estimation, err := ethClient.EstimateGas(context.Background(), msg) + if err != nil { + return 0, err + } + + increasedEstimation := float64(estimation) * IncreaseEstimatedGasFactor + + return uint64(increasedEstimation), nil +} + +func (s *ENSPublicKeyBridge) BuildTx(params BridgeParams) (*ethTypes.Transaction, error) { + toAddr := types.Address(params.ToAddr) + inputData, err := s.PackTxInputData(params, "") + if err != nil { + return nil, err + } + + sendArgs := &TransactionBridge{ + TransferTx: &transactions.SendTxArgs{ + From: types.Address(params.FromAddr), + To: &toAddr, + Value: (*hexutil.Big)(ZeroBigIntValue), + Data: inputData, + }, + ChainID: params.FromChain.ChainID, + } + + return s.BuildTransaction(sendArgs) +} + +func (s *ENSPublicKeyBridge) Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (hash types.Hash, err error) { + return s.transactor.SendTransactionWithChainID(sendArgs.ChainID, *sendArgs.TransferTx, verifiedAccount) +} + +func (s *ENSPublicKeyBridge) BuildTransaction(sendArgs *TransactionBridge) (*ethTypes.Transaction, error) { + return s.transactor.ValidateAndBuildTransaction(sendArgs.ChainID, *sendArgs.TransferTx) +} + +func (s *ENSPublicKeyBridge) CalculateAmountOut(params BridgeParams) (*big.Int, error) { + return params.AmountIn, nil +} + +func (s *ENSPublicKeyBridge) GetContractAddress(params BridgeParams) (common.Address, error) { + addr, err := s.ensService.API().Resolver(context.Background(), params.FromChain.ChainID, params.Username) + if err != nil { + return common.Address{}, err + } + if *addr == ZeroAddress { + return common.Address{}, errors.New("ENS resolver not found") + } + return *addr, nil +} diff --git a/services/wallet/router/router.go b/services/wallet/router/router.go index 06535055a..ce6bf996d 100644 --- a/services/wallet/router/router.go +++ b/services/wallet/router/router.go @@ -285,6 +285,7 @@ func NewRouter(rpcClient *rpc.Client, transactor *transactions.Transactor, token paraswap := bridge.NewSwapParaswap(rpcClient, transactor, tokenManager) ensRegister := bridge.NewENSRegisterBridge(rpcClient, transactor, ensService) ensRelease := bridge.NewENSReleaseBridge(rpcClient, transactor, ensService) + ensPublicKey := bridge.NewENSPublicKeyBridge(rpcClient, transactor, ensService) bridges[transfer.Name()] = transfer bridges[erc721Transfer.Name()] = erc721Transfer @@ -294,6 +295,7 @@ func NewRouter(rpcClient *rpc.Client, transactor *transactions.Transactor, token bridges[paraswap.Name()] = paraswap bridges[ensRegister.Name()] = ensRegister bridges[ensRelease.Name()] = ensRelease + bridges[ensPublicKey.Name()] = ensPublicKey return &Router{ rpcClient: rpcClient, @@ -546,7 +548,7 @@ func (r *Router) SuggestedRoutes( estimatedTime := r.feesManager.TransactionEstimatedTime(ctx, network.ChainID, maxFees) for _, brdg := range r.bridges { // Skip bridges that are added because of the Router V2, to not break the current functionality - if brdg.Name() == bridge.ENSRegisterName || brdg.Name() == bridge.ENSReleaseName { + if brdg.Name() == bridge.ENSRegisterName || brdg.Name() == bridge.ENSReleaseName || brdg.Name() == bridge.ENSPublicKeyName { continue } diff --git a/services/wallet/router/router_send_type.go b/services/wallet/router/router_send_type.go index e941e350a..873fccad9 100644 --- a/services/wallet/router/router_send_type.go +++ b/services/wallet/router/router_send_type.go @@ -110,6 +110,8 @@ func (s SendType) canUseBridge(b bridge.Bridge) bool { return bridgeName == bridge.ENSRegisterName case ENSRelease: return bridgeName == bridge.ENSReleaseName + case ENSSetPubKey: + return bridgeName == bridge.ENSPublicKeyName default: return true }