Sale Djenic 28506bcd17 chore_: improvements of the sending route generated by the router process
This commit simplifies the sending process of the best route suggested by the router.
It also makes the sending process the same for accounts (key pairs) migrated to a keycard
and those stored locally in local keystore files.

Deprecated endpoints:
- `CreateMultiTransaction`
- `ProceedWithTransactionsSignatures`

Deprecated signal:
- `wallet.sign.transactions`

New endpoints:
- `BuildTransactionsFromRoute`
- `SendRouterTransactionsWithSignatures`

The flow for sending the best router suggested by the router:
- call `BuildTransactionsFromRoute`
- wait for the `wallet.router.sign-transactions` signal
- sign received hashes using `SignMessage` call or sign on keycard
- call `SendRouterTransactionsWithSignatures` with the signatures of signed hashes from the previous step
- `wallet.router.transactions-sent` signal will be sent after transactions are sent or if an error occurs

New signals:
- `wallet.router.sending-transactions-started` // notifies client that the sending transactions process started
- `wallet.router.sign-transactions` // notifies client about the list of transactions that need to be signed
- `wallet.router.transactions-sent` // notifies client about transactions that are sent
- `wallet.transaction.status-changed` // notifies about status of sent transactions
2024-10-01 14:30:33 +02:00

1153 lines
45 KiB
Go

package wallet
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
gethrpc "github.com/ethereum/go-ethereum/rpc"
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
abi_spec "github.com/status-im/status-go/abi-spec"
"github.com/status-im/status-go/account"
statusErrors "github.com/status-im/status-go/errors"
"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/rpc/network"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/services/wallet/activity"
"github.com/status-im/status-go/services/wallet/collectibles"
wcommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/currency"
"github.com/status-im/status-go/services/wallet/history"
"github.com/status-im/status-go/services/wallet/onramp"
"github.com/status-im/status-go/services/wallet/requests"
"github.com/status-im/status-go/services/wallet/responses"
"github.com/status-im/status-go/services/wallet/router"
"github.com/status-im/status-go/services/wallet/router/fees"
"github.com/status-im/status-go/services/wallet/router/pathprocessor"
"github.com/status-im/status-go/services/wallet/router/sendtype"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/walletconnect"
"github.com/status-im/status-go/signal"
"github.com/status-im/status-go/transactions"
)
func NewAPI(s *Service) *API {
rpcClient := s.GetRPCClient()
transactor := s.GetTransactor()
tokenManager := s.GetTokenManager()
ensService := s.GetEnsService()
stickersService := s.GetStickersService()
featureFlags := s.FeatureFlags()
router := router.NewRouter(rpcClient, transactor, tokenManager, s.GetMarketManager(), s.GetCollectiblesService(),
s.GetCollectiblesManager(), ensService, stickersService)
transfer := pathprocessor.NewTransferProcessor(rpcClient, transactor)
router.AddPathProcessor(transfer)
erc721Transfer := pathprocessor.NewERC721Processor(rpcClient, transactor)
router.AddPathProcessor(erc721Transfer)
erc1155Transfer := pathprocessor.NewERC1155Processor(rpcClient, transactor)
router.AddPathProcessor(erc1155Transfer)
hop := pathprocessor.NewHopBridgeProcessor(rpcClient, transactor, tokenManager, rpcClient.NetworkManager)
router.AddPathProcessor(hop)
if featureFlags.EnableCelerBridge {
// TODO: Celar Bridge is out of scope for 2.30, check it thoroughly once we decide to include it again
cbridge := pathprocessor.NewCelerBridgeProcessor(rpcClient, transactor, tokenManager)
router.AddPathProcessor(cbridge)
}
paraswap := pathprocessor.NewSwapParaswapProcessor(rpcClient, transactor, tokenManager)
router.AddPathProcessor(paraswap)
ensRegister := pathprocessor.NewENSRegisterProcessor(rpcClient, transactor, ensService)
router.AddPathProcessor(ensRegister)
ensRelease := pathprocessor.NewENSReleaseProcessor(rpcClient, transactor, ensService)
router.AddPathProcessor(ensRelease)
ensPublicKey := pathprocessor.NewENSPublicKeyProcessor(rpcClient, transactor, ensService)
router.AddPathProcessor(ensPublicKey)
buyStickers := pathprocessor.NewStickersBuyProcessor(rpcClient, transactor, stickersService)
router.AddPathProcessor(buyStickers)
return &API{s, s.reader, router}
}
// API is class with methods available over RPC.
type API struct {
s *Service
reader *Reader
router *router.Router
}
func (api *API) StartWallet(ctx context.Context) error {
return api.reader.Start()
}
func (api *API) StopWallet(ctx context.Context) error {
api.router.Stop()
return api.s.Stop()
}
func (api *API) GetPairingsJSONFileContent() ([]byte, error) {
return api.s.keycardPairings.GetPairingsJSONFileContent()
}
func (api *API) SetPairingsJSONFileContent(content []byte) error {
return api.s.keycardPairings.SetPairingsJSONFileContent(content)
}
// Used by mobile
func (api *API) GetWalletToken(ctx context.Context, addresses []common.Address) (map[common.Address][]token.StorageToken, error) {
currency, err := api.s.accountsDB.GetCurrency()
if err != nil {
return nil, err
}
activeNetworks, err := api.s.rpcClient.NetworkManager.GetActiveNetworks()
if err != nil {
return nil, err
}
chainIDs := wcommon.NetworksToChainIDs(activeNetworks)
clients, err := api.s.rpcClient.EthClients(chainIDs)
if err != nil {
return nil, err
}
return api.reader.GetWalletToken(ctx, clients, addresses, currency)
}
// GetBalancesByChain return a map with key as chain id and value as map of account address and map of token address and balance
// [chainID][account][token]balance
func (api *API) GetBalancesByChain(ctx context.Context, chainIDs []uint64, addresses, tokens []common.Address) (map[uint64]map[common.Address]map[common.Address]*hexutil.Big, error) {
clients, err := api.s.rpcClient.EthClients(chainIDs)
if err != nil {
return nil, err
}
return api.s.tokenManager.GetBalancesByChain(ctx, clients, addresses, tokens)
}
func (api *API) FetchOrGetCachedWalletBalances(ctx context.Context, addresses []common.Address) (map[common.Address][]token.StorageToken, error) {
activeNetworks, err := api.s.rpcClient.NetworkManager.GetActiveNetworks()
if err != nil {
return nil, err
}
chainIDs := wcommon.NetworksToChainIDs(activeNetworks)
clients, err := api.s.rpcClient.EthClients(chainIDs)
if err != nil {
return nil, err
}
return api.reader.FetchOrGetCachedWalletBalances(ctx, clients, addresses, false)
}
type DerivedAddress struct {
Address common.Address `json:"address"`
PublicKey types.HexBytes `json:"public-key,omitempty"`
Path string `json:"path"`
HasActivity bool `json:"hasActivity"`
AlreadyCreated bool `json:"alreadyCreated"`
}
// @deprecated
func (api *API) CheckRecentHistory(ctx context.Context, addresses []common.Address) error {
return api.s.transferController.CheckRecentHistory([]uint64{api.s.rpcClient.UpstreamChainID}, addresses)
}
// @deprecated
func (api *API) CheckRecentHistoryForChainIDs(ctx context.Context, chainIDs []uint64, addresses []common.Address) error {
return api.s.transferController.CheckRecentHistory(chainIDs, addresses)
}
func hexBigToBN(hexBig *hexutil.Big) *big.Int {
var bN *big.Int
if hexBig != nil {
bN = hexBig.ToInt()
}
return bN
}
// @deprecated
// GetTransfersByAddress returns transfers for a single address
func (api *API) GetTransfersByAddress(ctx context.Context, address common.Address, toBlock, limit *hexutil.Big, fetchMore bool) ([]transfer.View, error) {
log.Debug("[WalletAPI:: GetTransfersByAddress] get transfers for an address", "address", address)
var intLimit = int64(1)
if limit != nil {
intLimit = limit.ToInt().Int64()
}
return api.s.transferController.GetTransfersByAddress(ctx, api.s.rpcClient.UpstreamChainID, address, hexBigToBN(toBlock), intLimit, fetchMore)
}
// @deprecated
// LoadTransferByHash loads transfer to the database
// Only used by status-mobile
func (api *API) LoadTransferByHash(ctx context.Context, address common.Address, hash common.Hash) error {
log.Debug("[WalletAPI:: LoadTransferByHash] get transfer by hash", "address", address, "hash", hash)
return api.s.transferController.LoadTransferByHash(ctx, api.s.rpcClient, address, hash)
}
// @deprecated
func (api *API) GetTransfersByAddressAndChainID(ctx context.Context, chainID uint64, address common.Address, toBlock, limit *hexutil.Big, fetchMore bool) ([]transfer.View, error) {
log.Debug("[WalletAPI:: GetTransfersByAddressAndChainIDs] get transfers for an address", "address", address)
return api.s.transferController.GetTransfersByAddress(ctx, chainID, address, hexBigToBN(toBlock), limit.ToInt().Int64(), fetchMore)
}
// @deprecated
func (api *API) GetTransfersForIdentities(ctx context.Context, identities []transfer.TransactionIdentity) ([]transfer.View, error) {
log.Debug("wallet.api.GetTransfersForIdentities", "identities.len", len(identities))
return api.s.transferController.GetTransfersForIdentities(ctx, identities)
}
func (api *API) FetchDecodedTxData(ctx context.Context, data string) (*thirdparty.DataParsed, error) {
log.Debug("[Wallet: FetchDecodedTxData]")
return api.s.decoder.Decode(data)
}
// GetBalanceHistory retrieves token balance history for token identity on multiple chains
func (api *API) GetBalanceHistory(ctx context.Context, chainIDs []uint64, addresses []common.Address, tokenSymbol string, currencySymbol string, timeInterval history.TimeInterval) ([]*history.ValuePoint, error) {
log.Debug("wallet.api.GetBalanceHistory", "chainIDs", chainIDs, "address", addresses, "tokenSymbol", tokenSymbol, "currencySymbol", currencySymbol, "timeInterval", timeInterval)
var fromTimestamp uint64
now := uint64(time.Now().UTC().Unix())
switch timeInterval {
case history.BalanceHistoryAllTime:
fromTimestamp = 0
case history.BalanceHistory1Year:
fallthrough
case history.BalanceHistory6Months:
fallthrough
case history.BalanceHistory1Month:
fallthrough
case history.BalanceHistory7Days:
fromTimestamp = now - history.TimeIntervalDurationSecs(timeInterval)
default:
return nil, fmt.Errorf("unknown time interval: %v", timeInterval)
}
return api.GetBalanceHistoryRange(ctx, chainIDs, addresses, tokenSymbol, currencySymbol, fromTimestamp, now)
}
// GetBalanceHistoryRange retrieves token balance history for token identity on multiple chains for a time range
// 'toTimestamp' is ignored for now, but will be used in the future to limit the range of the history
func (api *API) GetBalanceHistoryRange(ctx context.Context, chainIDs []uint64, addresses []common.Address, tokenSymbol string, currencySymbol string, fromTimestamp uint64, _ uint64) ([]*history.ValuePoint, error) {
log.Debug("wallet.api.GetBalanceHistoryRange", "chainIDs", chainIDs, "address", addresses, "tokenSymbol", tokenSymbol, "currencySymbol", currencySymbol, "fromTimestamp", fromTimestamp)
return api.s.history.GetBalanceHistory(ctx, chainIDs, addresses, tokenSymbol, currencySymbol, fromTimestamp)
}
func (api *API) GetTokenList(ctx context.Context) (*token.ListWrapper, error) {
log.Debug("call to get token list")
rst := api.s.tokenManager.GetList()
log.Debug("result from token list", "len", len(rst.Data))
return rst, nil
}
// @deprecated
func (api *API) GetTokens(ctx context.Context, chainID uint64) ([]*token.Token, error) {
log.Debug("call to get tokens")
rst, err := api.s.tokenManager.GetTokens(chainID)
log.Debug("result from token store", "len", len(rst))
return rst, err
}
// @deprecated
func (api *API) GetCustomTokens(ctx context.Context) ([]*token.Token, error) {
log.Debug("call to get custom tokens")
rst, err := api.s.tokenManager.GetCustoms(true)
log.Debug("result from database for custom tokens", "len", len(rst))
return rst, err
}
func (api *API) DiscoverToken(ctx context.Context, chainID uint64, address common.Address) (*token.Token, error) {
log.Debug("call to get discover token")
token, err := api.s.tokenManager.DiscoverToken(ctx, chainID, address)
return token, err
}
func (api *API) AddCustomToken(ctx context.Context, token token.Token) error {
log.Debug("call to create or edit custom token")
if token.ChainID == 0 {
token.ChainID = api.s.rpcClient.UpstreamChainID
}
err := api.s.tokenManager.UpsertCustom(token)
log.Debug("result from database for create or edit custom token", "err", err)
return err
}
// @deprecated
func (api *API) DeleteCustomToken(ctx context.Context, address common.Address) error {
log.Debug("call to remove custom token")
err := api.s.tokenManager.DeleteCustom(api.s.rpcClient.UpstreamChainID, address)
log.Debug("result from database for remove custom token", "err", err)
return err
}
func (api *API) DeleteCustomTokenByChainID(ctx context.Context, chainID uint64, address common.Address) error {
log.Debug("call to remove custom token")
err := api.s.tokenManager.DeleteCustom(chainID, address)
log.Debug("result from database for remove custom token", "err", err)
return err
}
// @deprecated
// Not used by status-desktop anymore
func (api *API) GetPendingTransactions(ctx context.Context) ([]*transactions.PendingTransaction, error) {
log.Debug("wallet.api.GetPendingTransactions")
rst, err := api.s.pendingTxManager.GetAllPending()
log.Debug("wallet.api.GetPendingTransactions RESULT", "len", len(rst))
return rst, err
}
// @deprecated
// Not used by status-desktop anymore
func (api *API) GetPendingTransactionsForIdentities(ctx context.Context, identities []transfer.TransactionIdentity) (
result []*transactions.PendingTransaction, err error) {
log.Debug("wallet.api.GetPendingTransactionsForIdentities")
result = make([]*transactions.PendingTransaction, 0, len(identities))
var pt *transactions.PendingTransaction
for _, identity := range identities {
pt, err = api.s.pendingTxManager.GetPendingEntry(identity.ChainID, identity.Hash)
result = append(result, pt)
}
log.Debug("wallet.api.GetPendingTransactionsForIdentities RES", "len", len(result))
return
}
// @deprecated
// TODO - #11861: Remove this and replace with EventPendingTransactionStatusChanged event and Delete to confirm the transaction where it is needed
func (api *API) WatchTransactionByChainID(ctx context.Context, chainID uint64, transactionHash common.Hash) (err error) {
log.Debug("wallet.api.WatchTransactionByChainID", "chainID", chainID, "transactionHash", transactionHash)
defer func() {
log.Debug("wallet.api.WatchTransactionByChainID return", "err", err, "chainID", chainID, "transactionHash", transactionHash)
}()
return api.s.transactionManager.WatchTransaction(ctx, chainID, transactionHash)
}
func (api *API) GetCryptoOnRamps(ctx context.Context) ([]onramp.CryptoOnRamp, error) {
log.Debug("call to GetCryptoOnRamps")
return api.s.cryptoOnRampManager.GetProviders(ctx)
}
func (api *API) GetCryptoOnRampURL(ctx context.Context, providerID string, parameters onramp.Parameters) (string, error) {
log.Debug("call to GetCryptoOnRampURL")
return api.s.cryptoOnRampManager.GetURL(ctx, providerID, parameters)
}
/*
Collectibles API Start
*/
func (api *API) FetchCachedBalancesByOwnerAndContractAddress(ctx context.Context, chainID wcommon.ChainID, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
log.Debug("call to FetchCachedBalancesByOwnerAndContractAddress")
return api.s.collectiblesManager.FetchCachedBalancesByOwnerAndContractAddress(ctx, chainID, ownerAddress, contractAddresses)
}
func (api *API) FetchBalancesByOwnerAndContractAddress(ctx context.Context, chainID wcommon.ChainID, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
log.Debug("call to FetchBalancesByOwnerAndContractAddress")
return api.s.collectiblesManager.FetchBalancesByOwnerAndContractAddress(ctx, chainID, ownerAddress, contractAddresses)
}
func (api *API) GetCollectibleOwnership(id thirdparty.CollectibleUniqueID) ([]thirdparty.AccountBalance, error) {
return api.s.collectiblesManager.GetCollectibleOwnership(id)
}
func (api *API) RefetchOwnedCollectibles() error {
log.Debug("wallet.api.RefetchOwnedCollectibles")
api.s.collectibles.RefetchOwnedCollectibles()
return nil
}
func (api *API) GetOwnedCollectiblesAsync(requestID int32, chainIDs []wcommon.ChainID, addresses []common.Address, filter collectibles.Filter, offset int, limit int, dataType collectibles.CollectibleDataType, fetchCriteria collectibles.FetchCriteria) error {
log.Debug("wallet.api.GetOwnedCollectiblesAsync", "requestID", requestID, "chainIDs.count", len(chainIDs), "addr.count", len(addresses), "offset", offset, "limit", limit, "dataType", dataType, "fetchCriteria", fetchCriteria)
api.s.collectibles.GetOwnedCollectiblesAsync(requestID, chainIDs, addresses, filter, offset, limit, dataType, fetchCriteria)
return nil
}
func (api *API) GetCollectiblesByUniqueIDAsync(requestID int32, uniqueIDs []thirdparty.CollectibleUniqueID, dataType collectibles.CollectibleDataType) error {
log.Debug("wallet.api.GetCollectiblesByUniqueIDAsync", "requestID", requestID, "uniqueIDs.count", len(uniqueIDs), "dataType", dataType)
api.s.collectibles.GetCollectiblesByUniqueIDAsync(requestID, uniqueIDs, dataType)
return nil
}
func (api *API) FetchCollectionSocialsAsync(contractID thirdparty.ContractID) error {
log.Debug("wallet.api.FetchCollectionSocialsAsync", "contractID", contractID)
return api.s.collectiblesManager.FetchCollectionSocialsAsync(contractID)
}
func (api *API) GetCollectibleOwnersByContractAddress(ctx context.Context, chainID wcommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
log.Debug("call to GetCollectibleOwnersByContractAddress")
return api.s.collectiblesManager.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
}
func (api *API) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID wcommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
log.Debug("call to FetchCollectibleOwnersByContractAddress")
return api.s.collectiblesManager.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
}
func (api *API) SearchCollectibles(ctx context.Context, chainID wcommon.ChainID, text string, cursor string, limit int, providerID string) (*thirdparty.FullCollectibleDataContainer, error) {
log.Debug("call to SearchCollectibles")
return api.s.collectiblesManager.SearchCollectibles(ctx, chainID, text, cursor, limit, providerID)
}
func (api *API) SearchCollections(ctx context.Context, chainID wcommon.ChainID, text string, cursor string, limit int, providerID string) (*thirdparty.CollectionDataContainer, error) {
log.Debug("call to SearchCollections")
return api.s.collectiblesManager.SearchCollections(ctx, chainID, text, cursor, limit, providerID)
}
/*
Collectibles API End
*/
func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error {
log.Debug("call to AddEthereumChain")
return api.s.rpcClient.NetworkManager.Upsert(&network)
}
func (api *API) DeleteEthereumChain(ctx context.Context, chainID uint64) error {
log.Debug("call to DeleteEthereumChain")
return api.s.rpcClient.NetworkManager.Delete(chainID)
}
func (api *API) GetEthereumChains(ctx context.Context) ([]*network.CombinedNetwork, error) {
log.Debug("call to GetEthereumChains")
return api.s.rpcClient.NetworkManager.GetCombinedNetworks()
}
// @deprecated
func (api *API) FetchPrices(ctx context.Context, symbols []string, currencies []string) (map[string]map[string]float64, error) {
log.Debug("call to FetchPrices")
return api.s.marketManager.FetchPrices(symbols, currencies)
}
// @deprecated
func (api *API) FetchMarketValues(ctx context.Context, symbols []string, currency string) (map[string]thirdparty.TokenMarketValues, error) {
log.Debug("call to FetchMarketValues")
return api.s.marketManager.FetchTokenMarketValues(symbols, currency)
}
func (api *API) GetHourlyMarketValues(ctx context.Context, symbol string, currency string, limit int, aggregate int) ([]thirdparty.HistoricalPrice, error) {
log.Debug("call to GetHourlyMarketValues")
return api.s.marketManager.FetchHistoricalHourlyPrices(symbol, currency, limit, aggregate)
}
func (api *API) GetDailyMarketValues(ctx context.Context, symbol string, currency string, limit int, allData bool, aggregate int) ([]thirdparty.HistoricalPrice, error) {
log.Debug("call to GetDailyMarketValues")
return api.s.marketManager.FetchHistoricalDailyPrices(symbol, currency, limit, allData, aggregate)
}
// @deprecated
func (api *API) FetchTokenDetails(ctx context.Context, symbols []string) (map[string]thirdparty.TokenDetails, error) {
log.Debug("call to FetchTokenDetails")
return api.s.marketManager.FetchTokenDetails(symbols)
}
func (api *API) GetSuggestedFees(ctx context.Context, chainID uint64) (*fees.SuggestedFeesGwei, error) {
log.Debug("call to GetSuggestedFees")
return api.router.GetFeesManager().SuggestedFeesGwei(ctx, chainID)
}
func (api *API) GetEstimatedLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
log.Debug("call to GetEstimatedLatestBlockNumber, chainID:", chainID)
return api.s.blockChainState.GetEstimatedLatestBlockNumber(ctx, chainID)
}
func (api *API) GetTransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Float) (fees.TransactionEstimation, error) {
log.Debug("call to getTransactionEstimatedTime")
return api.router.GetFeesManager().TransactionEstimatedTime(ctx, chainID, gweiToWei(maxFeePerGas)), nil
}
func gweiToWei(val *big.Float) *big.Int {
res, _ := new(big.Float).Mul(val, big.NewFloat(1000000000)).Int(nil)
return res
}
func (api *API) GetSuggestedRoutes(ctx context.Context, input *requests.RouteInputParams) (*router.SuggestedRoutes, error) {
log.Debug("call to GetSuggestedRoutes")
return api.router.SuggestedRoutes(ctx, input)
}
func (api *API) GetSuggestedRoutesAsync(ctx context.Context, input *requests.RouteInputParams) {
log.Debug("call to GetSuggestedRoutesAsync")
api.router.SuggestedRoutesAsync(input)
}
func (api *API) StopSuggestedRoutesAsyncCalculation(ctx context.Context) {
log.Debug("call to StopSuggestedRoutesAsyncCalculation")
api.router.StopSuggestedRoutesAsyncCalculation()
}
func (api *API) StopSuggestedRoutesCalculation(ctx context.Context) {
log.Debug("call to StopSuggestedRoutesCalculation")
api.router.StopSuggestedRoutesCalculation()
}
// Generates addresses for the provided paths, response doesn't include `HasActivity` value (if you need it check `GetAddressDetails` function)
func (api *API) GetDerivedAddresses(ctx context.Context, password string, derivedFrom string, paths []string) ([]*DerivedAddress, error) {
info, err := api.s.gethManager.AccountsGenerator().LoadAccount(derivedFrom, password)
if err != nil {
return nil, err
}
return api.getDerivedAddresses(info.ID, paths)
}
// Generates addresses for the provided paths derived from the provided mnemonic, response doesn't include `HasActivity` value (if you need it check `GetAddressDetails` function)
func (api *API) GetDerivedAddressesForMnemonic(ctx context.Context, mnemonic string, paths []string) ([]*DerivedAddress, error) {
mnemonicNoExtraSpaces := strings.Join(strings.Fields(mnemonic), " ")
info, err := api.s.gethManager.AccountsGenerator().ImportMnemonic(mnemonicNoExtraSpaces, "")
if err != nil {
return nil, err
}
return api.getDerivedAddresses(info.ID, paths)
}
// Generates addresses for the provided paths, response doesn't include `HasActivity` value (if you need it check `GetAddressDetails` function)
func (api *API) getDerivedAddresses(id string, paths []string) ([]*DerivedAddress, error) {
addedAccounts, err := api.s.accountsDB.GetActiveAccounts()
if err != nil {
return nil, err
}
info, err := api.s.gethManager.AccountsGenerator().DeriveAddresses(id, paths)
if err != nil {
return nil, err
}
derivedAddresses := make([]*DerivedAddress, 0)
for accPath, acc := range info {
derivedAddress := &DerivedAddress{
Address: common.HexToAddress(acc.Address),
PublicKey: types.Hex2Bytes(acc.PublicKey),
Path: accPath,
}
for _, account := range addedAccounts {
if types.Address(derivedAddress.Address) == account.Address {
derivedAddress.AlreadyCreated = true
break
}
}
derivedAddresses = append(derivedAddresses, derivedAddress)
}
return derivedAddresses, nil
}
func (api *API) AddressExists(ctx context.Context, address types.Address) (bool, error) {
return api.s.accountsDB.AddressExists(address)
}
// AddressDetails returns details for passed params (passed address, chains to check, timeout for the call to complete)
// if chainIDs is empty, it will use all active chains
// if timeout is zero, it will wait until the call completes
// response doesn't include derivation path
func (api *API) AddressDetails(ctx context.Context, params *requests.AddressDetails) (*DerivedAddress, error) {
if err := params.Validate(); err != nil {
return nil, err
}
result := &DerivedAddress{
Address: common.HexToAddress(params.Address),
}
addressExists, err := api.s.accountsDB.AddressExists(types.Address(result.Address))
if err != nil {
return result, err
}
result.AlreadyCreated = addressExists
chainIDs := params.ChainIDs
if len(chainIDs) == 0 {
activeNetworks, err := api.s.rpcClient.NetworkManager.GetActiveNetworks()
if err != nil {
return nil, err
}
chainIDs = wcommon.NetworksToChainIDs(activeNetworks)
}
clients, err := api.s.rpcClient.EthClients(chainIDs)
if err != nil {
return nil, err
}
if params.TimeoutInMilliseconds > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(params.TimeoutInMilliseconds)*time.Millisecond)
defer cancel()
}
for _, client := range clients {
balance, err := api.s.tokenManager.GetChainBalance(ctx, client, result.Address)
if err != nil {
if err != nil && errors.Is(err, context.DeadlineExceeded) {
return result, nil
}
return result, err
}
result.HasActivity = balance.Cmp(big.NewInt(0)) != 0
if result.HasActivity {
break
}
}
return result, nil
}
// @deprecated replaced by AddressDetails
// GetAddressDetails returns details for the passed address (response doesn't include derivation path)
func (api *API) GetAddressDetails(ctx context.Context, chainID uint64, address string) (*DerivedAddress, error) {
result := &DerivedAddress{
Address: common.HexToAddress(address),
}
addressExists, err := api.s.accountsDB.AddressExists(types.Address(result.Address))
if err != nil {
return result, err
}
result.AlreadyCreated = addressExists
chainClient, err := api.s.rpcClient.EthClient(chainID)
if err != nil {
return result, err
}
balance, err := api.s.tokenManager.GetChainBalance(ctx, chainClient, result.Address)
if err != nil {
return result, err
}
result.HasActivity = balance.Cmp(big.NewInt(0)) != 0
return result, nil
}
func (api *API) SignMessage(ctx context.Context, message types.HexBytes, address common.Address, password string) (string, error) {
log.Debug("[WalletAPI::SignMessage]", "message", message, "address", address)
selectedAccount, err := api.s.gethManager.VerifyAccountPassword(api.s.Config().KeyStoreDir, address.Hex(), password)
if err != nil {
return "", err
}
return api.s.transactionManager.SignMessage(message, selectedAccount)
}
func (api *API) BuildTransaction(ctx context.Context, chainID uint64, sendTxArgsJSON string) (response *transfer.TxResponse, err error) {
log.Debug("[WalletAPI::BuildTransaction]", "chainID", chainID, "sendTxArgsJSON", sendTxArgsJSON)
var params transactions.SendTxArgs
err = json.Unmarshal([]byte(sendTxArgsJSON), &params)
if err != nil {
return nil, err
}
return api.s.transactionManager.BuildTransaction(chainID, params)
}
func (api *API) BuildRawTransaction(ctx context.Context, chainID uint64, sendTxArgsJSON string, signature string) (response *transfer.TxResponse, err error) {
log.Debug("[WalletAPI::BuildRawTransaction]", "chainID", chainID, "sendTxArgsJSON", sendTxArgsJSON, "signature", signature)
sig, err := hex.DecodeString(signature)
if err != nil {
return nil, err
}
var params transactions.SendTxArgs
err = json.Unmarshal([]byte(sendTxArgsJSON), &params)
if err != nil {
return nil, err
}
return api.s.transactionManager.BuildRawTransaction(chainID, params, sig)
}
func (api *API) SendTransactionWithSignature(ctx context.Context, chainID uint64, txType transactions.PendingTrxType,
sendTxArgsJSON string, signature string) (hash types.Hash, err error) {
log.Debug("[WalletAPI::SendTransactionWithSignature]", "chainID", chainID, "txType", txType, "sendTxArgsJSON", sendTxArgsJSON, "signature", signature)
sig, err := hex.DecodeString(signature)
if err != nil {
return hash, err
}
var params transactions.SendTxArgs
err = json.Unmarshal([]byte(sendTxArgsJSON), &params)
if err != nil {
return hash, err
}
return api.s.transactionManager.SendTransactionWithSignature(chainID, params, sig)
}
// Deprecated: `CreateMultiTransaction` is the old way of sending transactions and should not be used anymore.
//
// The flow that should be used instead:
// - call `BuildTransactionsFromRoute`
// - wait for the `wallet.router.sign-transactions` signal
// - sign received hashes using `SignMessage` call or sign on keycard
// - call `SendRouterTransactionsWithSignatures` with the signatures of signed hashes from the previous step
//
// TODO: remove this struct once mobile switches to the new approach
func (api *API) CreateMultiTransaction(ctx context.Context, multiTransactionCommand *transfer.MultiTransactionCommand, data []*pathprocessor.MultipathProcessorTxArgs, password string) (*transfer.MultiTransactionCommandResult, error) {
log.Debug("[WalletAPI:: CreateMultiTransaction] create multi transaction")
cmd, err := api.s.transactionManager.CreateMultiTransactionFromCommand(multiTransactionCommand, data)
if err != nil {
return nil, err
}
if password != "" {
selectedAccount, err := api.getVerifiedWalletAccount(multiTransactionCommand.FromAddress.Hex(), password)
if err != nil {
return nil, err
}
cmdRes, err := api.s.transactionManager.SendTransactions(ctx, cmd, data, api.router.GetPathProcessors(), selectedAccount)
if err != nil {
return nil, err
}
_, err = api.s.transactionManager.InsertMultiTransaction(cmd)
if err != nil {
log.Error("Failed to save multi transaction", "error", err) // not critical
}
return cmdRes, nil
}
return nil, api.s.transactionManager.SendTransactionForSigningToKeycard(ctx, cmd, data, api.router.GetPathProcessors())
}
func updateFields(sd *responses.SendDetails, inputParams requests.RouteInputParams) {
sd.SendType = int(inputParams.SendType)
sd.FromAddress = types.Address(inputParams.AddrFrom)
sd.ToAddress = types.Address(inputParams.AddrTo)
sd.FromToken = inputParams.TokenID
sd.ToToken = inputParams.ToTokenID
if inputParams.AmountIn != nil {
sd.FromAmount = inputParams.AmountIn.String()
}
if inputParams.AmountOut != nil {
sd.ToAmount = inputParams.AmountOut.String()
}
sd.OwnerTokenBeingSent = inputParams.TokenIDIsOwnerToken
sd.Username = inputParams.Username
sd.PublicKey = inputParams.PublicKey
if inputParams.PackID != nil {
sd.PackID = inputParams.PackID.String()
}
}
func (api *API) BuildTransactionsFromRoute(ctx context.Context, buildInputParams *requests.RouterBuildTransactionsParams) {
log.Debug("[WalletAPI::BuildTransactionsFromRoute] builds transactions from the generated best route", "uuid", buildInputParams.Uuid)
go func() {
api.router.StopSuggestedRoutesAsyncCalculation()
var err error
response := &responses.RouterTransactionsForSigning{
SendDetails: &responses.SendDetails{
Uuid: buildInputParams.Uuid,
},
}
defer func() {
if err != nil {
api.s.transactionManager.ClearLocalRouterTransactionsData()
err = statusErrors.CreateErrorResponseFromError(err)
response.SendDetails.ErrorResponse = err.(*statusErrors.ErrorResponse)
}
signal.SendWalletEvent(signal.SignRouterTransactions, response)
}()
route, routeInputParams := api.router.GetBestRouteAndAssociatedInputParams()
if routeInputParams.Uuid != buildInputParams.Uuid {
// should never be here
err = ErrCannotResolveRouteId
return
}
updateFields(response.SendDetails, routeInputParams)
// notify client that sending transactions started (has 3 steps, building txs, signing txs, sending txs)
signal.SendWalletEvent(signal.RouterSendingTransactionsStarted, response.SendDetails)
response.SigningDetails, err = api.s.transactionManager.BuildTransactionsFromRoute(
route,
api.router.GetPathProcessors(),
transfer.BuildRouteExtraParams{
AddressFrom: routeInputParams.AddrFrom,
AddressTo: routeInputParams.AddrTo,
Username: routeInputParams.Username,
PublicKey: routeInputParams.PublicKey,
PackID: routeInputParams.PackID.ToInt(),
SlippagePercentage: buildInputParams.SlippagePercentage,
},
)
}()
}
// Deprecated: `ProceedWithTransactionsSignatures` is the endpoint used in the old way of sending transactions and should not be used anymore.
//
// The flow that should be used instead:
// - call `BuildTransactionsFromRoute`
// - wait for the `wallet.router.sign-transactions` signal
// - sign received hashes using `SignMessage` call or sign on keycard
// - call `SendRouterTransactionsWithSignatures` with the signatures of signed hashes from the previous step
//
// TODO: remove this struct once mobile switches to the new approach
func (api *API) ProceedWithTransactionsSignatures(ctx context.Context, signatures map[string]transfer.SignatureDetails) (*transfer.MultiTransactionCommandResult, error) {
log.Debug("[WalletAPI:: ProceedWithTransactionsSignatures] sign with signatures and send multi transaction")
return api.s.transactionManager.ProceedWithTransactionsSignatures(ctx, signatures)
}
func (api *API) SendRouterTransactionsWithSignatures(ctx context.Context, sendInputParams *requests.RouterSendTransactionsParams) {
log.Debug("[WalletAPI:: SendRouterTransactionsWithSignatures] sign with signatures and send")
go func() {
var (
err error
routeInputParams requests.RouteInputParams
)
response := &responses.RouterSentTransactions{
SendDetails: &responses.SendDetails{
Uuid: sendInputParams.Uuid,
},
}
defer func() {
clearLocalData := true
if routeInputParams.SendType == sendtype.Swap {
// in case of swap don't clear local data if an approval is placed, but swap tx is not sent yet
if api.s.transactionManager.ApprovalRequiredForPath(pathprocessor.ProcessorSwapParaswapName) &&
api.s.transactionManager.ApprovalPlacedForPath(pathprocessor.ProcessorSwapParaswapName) &&
!api.s.transactionManager.TxPlacedForPath(pathprocessor.ProcessorSwapParaswapName) {
clearLocalData = false
}
}
if clearLocalData {
api.s.transactionManager.ClearLocalRouterTransactionsData()
}
if err != nil {
err = statusErrors.CreateErrorResponseFromError(err)
response.SendDetails.ErrorResponse = err.(*statusErrors.ErrorResponse)
}
signal.SendWalletEvent(signal.RouterTransactionsSent, response)
}()
_, routeInputParams = api.router.GetBestRouteAndAssociatedInputParams()
if routeInputParams.Uuid != sendInputParams.Uuid {
err = ErrCannotResolveRouteId
return
}
updateFields(response.SendDetails, routeInputParams)
err = api.s.transactionManager.ValidateAndAddSignaturesToRouterTransactions(sendInputParams.Signatures)
if err != nil {
return
}
//////////////////////////////////////////////////////////////////////////////
// prepare multitx
var mtType transfer.MultiTransactionType = transfer.MultiTransactionSend
if routeInputParams.SendType == sendtype.Bridge {
mtType = transfer.MultiTransactionBridge
} else if routeInputParams.SendType == sendtype.Swap {
mtType = transfer.MultiTransactionSwap
}
multiTx := transfer.NewMultiTransaction(
/* Timestamp: */ uint64(time.Now().Unix()),
/* FromNetworkID: */ 0,
/* ToNetworkID: */ 0,
/* FromTxHash: */ common.Hash{},
/* ToTxHash: */ common.Hash{},
/* FromAddress: */ routeInputParams.AddrFrom,
/* ToAddress: */ routeInputParams.AddrTo,
/* FromAsset: */ routeInputParams.TokenID,
/* ToAsset: */ routeInputParams.ToTokenID,
/* FromAmount: */ routeInputParams.AmountIn,
/* ToAmount: */ routeInputParams.AmountOut,
/* Type: */ mtType,
/* CrossTxID: */ "",
)
_, err = api.s.transactionManager.InsertMultiTransaction(multiTx)
if err != nil {
return
}
//////////////////////////////////////////////////////////////////////////////
response.SentTransactions, err = api.s.transactionManager.SendRouterTransactions(ctx, multiTx)
var (
chainIDs []uint64
addresses []common.Address
)
for _, tx := range response.SentTransactions {
chainIDs = append(chainIDs, tx.FromChain)
addresses = append(addresses, common.Address(tx.FromAddress))
go func(chainId uint64, txHash common.Hash) {
err = api.s.transactionManager.WatchTransaction(context.Background(), chainId, txHash)
if err != nil {
return
}
}(tx.FromChain, common.Hash(tx.Hash))
}
err = api.s.transferController.CheckRecentHistory(chainIDs, addresses)
}()
}
func (api *API) GetMultiTransactions(ctx context.Context, transactionIDs []wcommon.MultiTransactionIDType) ([]*transfer.MultiTransaction, error) {
log.Debug("wallet.api.GetMultiTransactions", "IDs.len", len(transactionIDs))
return api.s.transactionManager.GetMultiTransactions(ctx, transactionIDs)
}
func (api *API) GetCachedCurrencyFormats() (currency.FormatPerSymbol, error) {
log.Debug("call to GetCachedCurrencyFormats")
return api.s.currency.GetCachedCurrencyFormats()
}
func (api *API) FetchAllCurrencyFormats() (currency.FormatPerSymbol, error) {
log.Debug("call to FetchAllCurrencyFormats")
return api.s.currency.FetchAllCurrencyFormats()
}
// @deprecated replaced by session APIs; see #12120
func (api *API) FilterActivityAsync(requestID int32, addresses []common.Address, chainIDs []wcommon.ChainID, filter activity.Filter, offset int, limit int) error {
log.Debug("wallet.api.FilterActivityAsync", "requestID", requestID, "addr.count", len(addresses), "chainIDs.count", len(chainIDs), "offset", offset, "limit", limit)
api.s.activity.FilterActivityAsync(requestID, addresses, chainIDs, filter, offset, limit)
return nil
}
// @deprecated replaced by session APIs; see #12120
func (api *API) CancelActivityFilterTask(requestID int32) error {
log.Debug("wallet.api.CancelActivityFilterTask", "requestID", requestID)
api.s.activity.CancelFilterTask(requestID)
return nil
}
func (api *API) StartActivityFilterSession(addresses []common.Address, chainIDs []wcommon.ChainID, filter activity.Filter, firstPageCount int) (activity.SessionID, error) {
log.Debug("wallet.api.StartActivityFilterSession", "addr.count", len(addresses), "chainIDs.count", len(chainIDs), "firstPageCount", firstPageCount)
return api.s.activity.StartFilterSession(addresses, chainIDs, filter, firstPageCount), nil
}
func (api *API) UpdateActivityFilterForSession(sessionID activity.SessionID, filter activity.Filter, firstPageCount int) error {
log.Debug("wallet.api.UpdateActivityFilterForSession", "sessionID", sessionID, "firstPageCount", firstPageCount)
return api.s.activity.UpdateFilterForSession(sessionID, filter, firstPageCount)
}
func (api *API) ResetActivityFilterSession(id activity.SessionID, firstPageCount int) error {
log.Debug("wallet.api.ResetActivityFilterSession", "id", id, "firstPageCount", firstPageCount)
return api.s.activity.ResetFilterSession(id, firstPageCount)
}
func (api *API) GetMoreForActivityFilterSession(id activity.SessionID, pageCount int) error {
log.Debug("wallet.api.GetMoreForActivityFilterSession", "id", id, "pageCount", pageCount)
return api.s.activity.GetMoreForFilterSession(id, pageCount)
}
func (api *API) StopActivityFilterSession(id activity.SessionID) {
log.Debug("wallet.api.StopActivityFilterSession", "id", id)
api.s.activity.StopFilterSession(id)
}
func (api *API) GetMultiTxDetails(ctx context.Context, multiTxID int) (*activity.EntryDetails, error) {
log.Debug("wallet.api.GetMultiTxDetails", "multiTxID", multiTxID)
return api.s.activity.GetMultiTxDetails(ctx, multiTxID)
}
func (api *API) GetTxDetails(ctx context.Context, id string) (*activity.EntryDetails, error) {
log.Debug("wallet.api.GetTxDetails", "id", id)
return api.s.activity.GetTxDetails(ctx, id)
}
func (api *API) GetRecipientsAsync(requestID int32, chainIDs []wcommon.ChainID, addresses []common.Address, offset int, limit int) (ignored bool, err error) {
log.Debug("wallet.api.GetRecipientsAsync", "addresses.len", len(addresses), "chainIDs.len", len(chainIDs), "offset", offset, "limit", limit)
ignored = api.s.activity.GetRecipientsAsync(requestID, chainIDs, addresses, offset, limit)
return ignored, err
}
func (api *API) GetOldestActivityTimestampAsync(requestID int32, addresses []common.Address) error {
log.Debug("wallet.api.GetOldestActivityTimestamp", "addresses.len", len(addresses))
api.s.activity.GetOldestTimestampAsync(requestID, addresses)
return nil
}
func (api *API) GetActivityCollectiblesAsync(requestID int32, chainIDs []wcommon.ChainID, addresses []common.Address, offset int, limit int) error {
log.Debug("wallet.api.GetActivityCollectiblesAsync", "addresses.len", len(addresses), "chainIDs.len", len(chainIDs), "offset", offset, "limit", limit)
api.s.activity.GetActivityCollectiblesAsync(requestID, chainIDs, addresses, offset, limit)
return nil
}
func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int, error) {
log.Debug("wallet.api.VerifyURL", "rpcURL", rpcURL)
rpcClient, err := gethrpc.Dial(rpcURL)
if err != nil {
return nil, fmt.Errorf("dial upstream server: %s", err)
}
client := ethclient.NewClient(rpcClient)
return client.ChainID(ctx)
}
func (api *API) getVerifiedWalletAccount(address, password string) (*account.SelectedExtKey, error) {
exists, err := api.s.accountsDB.AddressExists(types.HexToAddress(address))
if err != nil {
log.Error("failed to query db for a given address", "address", address, "error", err)
return nil, err
}
if !exists {
log.Error("failed to get a selected account", "err", transactions.ErrInvalidTxSender)
return nil, transactions.ErrAccountDoesntExist
}
keyStoreDir := api.s.Config().KeyStoreDir
key, err := api.s.gethManager.VerifyAccountPassword(keyStoreDir, address, password)
if err != nil {
log.Error("failed to verify account", "account", address, "error", err)
return nil, err
}
return &account.SelectedExtKey{
Address: key.Address,
AccountKey: key,
}, nil
}
// AddWalletConnectSession adds or updates a session wallet connect session
func (api *API) AddWalletConnectSession(ctx context.Context, session_json string) error {
log.Debug("wallet.api.AddWalletConnectSession", "rpcURL", len(session_json))
return walletconnect.AddSession(api.s.db, api.s.config.Networks, session_json)
}
// DisconnectWalletConnectSession removes a wallet connect session
func (api *API) DisconnectWalletConnectSession(ctx context.Context, topic walletconnect.Topic) error {
log.Debug("wallet.api.DisconnectWalletConnectSession", "topic", topic)
return walletconnect.DisconnectSession(api.s.db, topic)
}
// GetWalletConnectActiveSessions returns all active wallet connect sessions
func (api *API) GetWalletConnectActiveSessions(ctx context.Context, validAtTimestamp int64) ([]walletconnect.DBSession, error) {
log.Debug("wallet.api.GetWalletConnectActiveSessions")
return walletconnect.GetActiveSessions(api.s.db, validAtTimestamp)
}
// GetWalletConnectDapps returns all active wallet connect dapps
// Active dApp are those having active sessions (not expired and not disconnected)
func (api *API) GetWalletConnectDapps(ctx context.Context, validAtTimestamp int64, testChains bool) ([]walletconnect.DBDApp, error) {
log.Debug("wallet.api.GetWalletConnectDapps", "validAtTimestamp", validAtTimestamp, "testChains", testChains)
return walletconnect.GetActiveDapps(api.s.db, validAtTimestamp, testChains)
}
// HashMessageEIP191 is used for hashing dApps requests for "personal_sign" and "eth_sign"
// in a safe manner following the EIP-191 version 0x45 for signing on the client side.
func (api *API) HashMessageEIP191(ctx context.Context, message types.HexBytes) types.Hash {
log.Debug("wallet.api.HashMessageEIP191", "len(data)", len(message))
safeMsg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), string(message))
return crypto.Keccak256Hash([]byte(safeMsg))
}
// SignTypedDataV4 dApps use it to execute "eth_signTypedData_v4" requests
// the formatted typed data will be prefixed with \x19\x01 based on the EIP-712
// @deprecated
func (api *API) SignTypedDataV4(typedJson string, address string, password string) (types.HexBytes, error) {
log.Debug("wallet.api.SignTypedDataV4", "len(typedJson)", len(typedJson), "address", address, "len(password)", len(password))
account, err := api.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
var typed signercore.TypedData
err = json.Unmarshal([]byte(typedJson), &typed)
if err != nil {
return types.HexBytes{}, err
}
// This is not used down the line but required by the typeddata.SignTypedDataV4 function call
chain := new(big.Int).SetUint64(api.s.config.NetworkID)
sig, err := typeddata.SignTypedDataV4(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}
// SafeSignTypedDataForDApps is used to execute requests for "eth_signTypedData"
// if legacy is true else "eth_signTypedData_v4"
// the formatted typed data won't be prefixed in case of legacy calls, as the
// old dApps implementation expects
// the chain is validate for both cases
func (api *API) SafeSignTypedDataForDApps(typedJson string, address string, password string, chainID uint64, legacy bool) (types.HexBytes, error) {
log.Debug("wallet.api.SafeSignTypedDataForDApps", "len(typedJson)", len(typedJson), "address", address, "len(password)", len(password), "chainID", chainID, "legacy", legacy)
account, err := api.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
return walletconnect.SafeSignTypedDataForDApps(typedJson, account.AccountKey.PrivateKey, chainID, legacy)
}
func (api *API) RestartWalletReloadTimer(ctx context.Context) error {
return api.s.reader.Restart()
}
func (api *API) IsChecksumValidForAddress(address string) (bool, error) {
log.Debug("wallet.api.isChecksumValidForAddress", "address", address)
return abi_spec.CheckAddressChecksum(address)
}