feat: Simple transaction router (#2686)

This commit is contained in:
Anthony Laibe 2022-06-09 15:09:56 +02:00 committed by GitHub
parent 9430f494be
commit 8c0f230644
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 255 additions and 49 deletions

View file

@ -41,6 +41,7 @@ type StatusBackend interface {
HashTypedDataV4(typed signercore.TypedData) (types.Hash, error)
ResetChainData() error
SendTransaction(sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error)
SendTransactionWithChainID(chainID uint64, sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error)
SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash types.Hash, err error)
SignHash(hexEncodedHash string) (string, error)
SignMessage(rpcParams personal.SignParams) (types.HexBytes, error)

View file

@ -875,6 +875,22 @@ func (b *GethStatusBackend) SendTransaction(sendArgs transactions.SendTxArgs, pa
return
}
func (b *GethStatusBackend) SendTransactionWithChainID(chainID uint64, sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) {
verifiedAccount, err := b.getVerifiedWalletAccount(sendArgs.From.String(), password)
if err != nil {
return hash, err
}
hash, err = b.transactor.SendTransactionWithChainID(chainID, sendArgs, verifiedAccount)
if err != nil {
return
}
go b.statusNode.RPCFiltersService().TriggerTransactionSentToUpstreamEvent(hash)
return
}
func (b *GethStatusBackend) SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash types.Hash, err error) {
hash, err = b.transactor.SendTransactionWithSignature(sendArgs, sig)
if err != nil {

View file

@ -482,6 +482,21 @@ func Recover(rpcParams string) string {
return prepareJSONResponse(addr.String(), err)
}
// SendTransactionWithChainID converts RPC args and calls backend.SendTransactionWithChainID.
func SendTransactionWithChainID(chainID int, txArgsJSON, password string) string {
var params transactions.SendTxArgs
err := json.Unmarshal([]byte(txArgsJSON), &params)
if err != nil {
return prepareJSONResponseWithCode(nil, err, codeFailedParseParams)
}
hash, err := statusBackend.SendTransactionWithChainID(uint64(chainID), params, password)
code := codeUnknown
if c, ok := errToCodeMap[err]; ok {
code = c
}
return prepareJSONResponseWithCode(hash.String(), err, code)
}
// SendTransaction converts RPC args and calls backend.SendTransaction.
func SendTransaction(txArgsJSON, password string) string {
var params transactions.SendTxArgs

View file

@ -553,3 +553,11 @@ func (db *Database) TestNetworksEnabled() (rst bool, err error) {
}
return
}
func (db *Database) GetTestNetworksEnabled() (result bool, err error) {
err = db.makeSelectRow(TestNetworksEnabled).Scan(&result)
if err == sql.ErrNoRows {
return result, nil
}
return result, err
}

View file

@ -17,22 +17,24 @@ import (
)
func NewAPI(s *Service) *API {
r := NewReader(s)
return &API{s, r}
reader := NewReader(s)
router := NewRouter(s)
return &API{s, reader, router}
}
// API is class with methods available over RPC.
type API struct {
s *Service
r *Reader
s *Service
reader *Reader
router *Router
}
func (api *API) StartWallet(ctx context.Context, chainIDs []uint64) error {
return api.r.Start(ctx, chainIDs)
return api.reader.Start(ctx, chainIDs)
}
func (api *API) GetWallet(ctx context.Context, chainIDs []uint64) (*Wallet, error) {
return api.r.GetWallet(ctx, chainIDs)
return api.reader.GetWallet(ctx, chainIDs)
}
type DerivedAddress struct {
@ -316,6 +318,11 @@ func (api *API) GetSuggestedFees(ctx context.Context, chainID uint64) (*Suggeste
return api.s.feesManager.suggestedFees(ctx, chainID)
}
func (api *API) GetSuggestedRoutes(ctx context.Context, account common.Address, amount float64, tokenSymbol string) (*SuggestedRoutes, error) {
log.Debug("call to GetSuggestedRoutes")
return api.router.suggestedRoutes(ctx, account, amount, tokenSymbol)
}
func (api *API) GetDerivedAddressesForPath(ctx context.Context, password string, derivedFrom string, path string, pageSize int, pageNumber int) ([]*DerivedAddress, error) {
info, err := api.s.gethManager.AccountsGenerator().LoadAccount(derivedFrom, password)
if err != nil {

View file

@ -18,6 +18,7 @@ type SuggestedFees struct {
MaxFeePerGasLow *big.Float `json:"maxFeePerGasLow"`
MaxFeePerGasMedium *big.Float `json:"maxFeePerGasMedium"`
MaxFeePerGasHigh *big.Float `json:"maxFeePerGasHigh"`
EIP1559Enabled bool `json:"eip1559Enabled"`
}
type FeeHistory struct {
@ -43,21 +44,29 @@ func (f *FeeManager) suggestedFees(ctx context.Context, chainID uint64) (*Sugges
if err != nil {
return nil, err
}
gasPrice, err := backend.SuggestGasPrice(ctx)
if err != nil {
return nil, err
}
maxPriorityFeePerGas, err := backend.SuggestGasTipCap(ctx)
if err != nil {
return nil, err
}
block, err := backend.BlockByNumber(ctx, nil)
if err != nil {
return nil, err
}
maxPriorityFeePerGas, err := backend.SuggestGasTipCap(ctx)
if err != nil {
return &SuggestedFees{
GasPrice: weiToGwei(gasPrice),
BaseFee: big.NewFloat(0),
MaxPriorityFeePerGas: big.NewFloat(0),
MaxFeePerGasLow: big.NewFloat(0),
MaxFeePerGasMedium: big.NewFloat(0),
MaxFeePerGasHigh: big.NewFloat(0),
EIP1559Enabled: false,
}, nil
}
config := params.MainnetChainConfig
baseFee := misc.CalcBaseFee(config, block.Header())
@ -103,5 +112,6 @@ func (f *FeeManager) suggestedFees(ctx context.Context, chainID uint64) (*Sugges
MaxFeePerGasLow: weiToGwei(perc10),
MaxFeePerGasMedium: weiToGwei(maxFeePerGasMedium),
MaxFeePerGasHigh: weiToGwei(maxFeePerGasHigh),
EIP1559Enabled: true,
}, nil
}

122
services/wallet/router.go Normal file
View file

@ -0,0 +1,122 @@
package wallet
import (
"context"
"math"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/services/wallet/async"
"github.com/status-im/status-go/services/wallet/chain"
)
type SuggestedRoutes struct {
Networks []params.Network `json:"networks"`
}
func NewRouter(s *Service) *Router {
return &Router{s}
}
type Router struct {
s *Service
}
func (r *Router) suitableTokenExists(ctx context.Context, network *params.Network, tokens []*Token, account common.Address, amount float64, tokenSymbol string) (bool, error) {
for _, token := range tokens {
if token.Symbol != tokenSymbol {
continue
}
clients, err := chain.NewClients(r.s.rpcClient, []uint64{network.ChainID})
if err != nil {
return false, err
}
balance, err := r.s.tokenManager.getBalance(ctx, clients[0], account, token.Address)
if err != nil {
return false, err
}
amountForToken, _ := new(big.Float).Mul(big.NewFloat(amount), big.NewFloat(math.Pow10(int(token.Decimals)))).Int(nil)
if balance.Cmp(amountForToken) >= 0 {
return true, nil
}
}
return false, nil
}
func (r *Router) suggestedRoutes(ctx context.Context, account common.Address, amount float64, tokenSymbol string) (*SuggestedRoutes, error) {
areTestNetworksEnabled, err := r.s.accountsDB.GetTestNetworksEnabled()
if err != nil {
return nil, err
}
networks, err := r.s.rpcClient.NetworkManager.Get(false)
if err != nil {
return nil, err
}
var (
group = async.NewAtomicGroup(ctx)
mu sync.Mutex
candidates = make([]params.Network, 0)
)
for networkIdx := range networks {
network := networks[networkIdx]
if network.IsTest != areTestNetworksEnabled {
continue
}
group.Add(func(c context.Context) error {
if tokenSymbol == network.NativeCurrencySymbol {
tokens := []*Token{&Token{
Address: common.HexToAddress("0x"),
Symbol: network.NativeCurrencySymbol,
Decimals: uint(network.NativeCurrencyDecimals),
Name: network.NativeCurrencyName,
}}
ok, _ := r.suitableTokenExists(c, network, tokens, account, amount, tokenSymbol)
if ok {
mu.Lock()
candidates = append(candidates, *network)
mu.Unlock()
return nil
}
}
tokens, err := r.s.tokenManager.getTokens(network.ChainID)
if err == nil {
ok, _ := r.suitableTokenExists(c, network, tokens, account, amount, tokenSymbol)
if ok {
mu.Lock()
candidates = append(candidates, *network)
mu.Unlock()
return nil
}
}
customTokens, err := r.s.tokenManager.getCustomsByChainID(network.ChainID)
if err == nil {
ok, _ := r.suitableTokenExists(c, network, customTokens, account, amount, tokenSymbol)
if ok {
mu.Lock()
candidates = append(candidates, *network)
mu.Unlock()
}
}
return nil
})
}
group.Wait()
return &SuggestedRoutes{
Networks: candidates,
}, nil
}

View file

@ -129,6 +129,27 @@ func (tm *TokenManager) getCustoms() ([]*Token, error) {
return rst, nil
}
func (tm *TokenManager) getCustomsByChainID(chainID uint64) ([]*Token, error) {
rows, err := tm.db.Query("SELECT address, name, symbol, decimals, color, network_id FROM tokens where network_id=?", chainID)
if err != nil {
return nil, err
}
defer rows.Close()
var rst []*Token
for rows.Next() {
token := &Token{}
err := rows.Scan(&token.Address, &token.Name, &token.Symbol, &token.Decimals, &token.Color, &token.ChainID)
if err != nil {
return nil, err
}
rst = append(rst, token)
}
return rst, nil
}
func (tm *TokenManager) isTokenVisible(chainID uint64, address common.Address) (bool, error) {
rows, err := tm.db.Query("SELECT chain_id, address FROM visible_tokens WHERE chain_id = ? AND address = ?", chainID, address)
if err != nil {
@ -257,6 +278,14 @@ func (tm *TokenManager) getChainBalance(ctx context.Context, client *chain.Clien
return client.BalanceAt(ctx, account, nil)
}
func (tm *TokenManager) getBalance(ctx context.Context, client *chain.Client, account common.Address, token common.Address) (*big.Int, error) {
if token == common.HexToAddress("0x") {
return tm.getChainBalance(ctx, client, account)
}
return tm.getTokenBalance(ctx, client, account, token)
}
func (tm *TokenManager) getBalances(parent context.Context, clients []*chain.Client, accounts, tokens []common.Address) (map[common.Address]map[common.Address]*hexutil.Big, error) {
var (
group = async.NewAtomicGroup(parent)
@ -273,13 +302,7 @@ func (tm *TokenManager) getBalances(parent context.Context, clients []*chain.Cli
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
var balance *big.Int
var err error
if token == common.HexToAddress("0x") {
balance, err = tm.getChainBalance(ctx, client, account)
} else {
balance, err = tm.getTokenBalance(ctx, client, account, token)
}
balance, err := tm.getBalance(ctx, client, account, token)
// We don't want to return an error here and prevent
// the rest from completing

View file

@ -16,18 +16,19 @@ import (
// rpcWrapper wraps provides convenient interface for ethereum RPC APIs we need for sending transactions
type rpcWrapper struct {
rpcClient *rpc.Client
RPCClient *rpc.Client
chainID uint64
}
func newRPCWrapper(client *rpc.Client) *rpcWrapper {
return &rpcWrapper{rpcClient: client}
func newRPCWrapper(client *rpc.Client, chainID uint64) *rpcWrapper {
return &rpcWrapper{RPCClient: client, chainID: chainID}
}
// PendingNonceAt returns the account nonce of the given account in the pending state.
// This is the nonce that should be used for the next transaction.
func (w *rpcWrapper) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
var result hexutil.Uint64
err := w.rpcClient.CallContext(ctx, &result, w.rpcClient.UpstreamChainID, "eth_getTransactionCount", account, "pending")
err := w.RPCClient.CallContext(ctx, &result, w.chainID, "eth_getTransactionCount", account, "pending")
return uint64(result), err
}
@ -35,7 +36,7 @@ func (w *rpcWrapper) PendingNonceAt(ctx context.Context, account common.Address)
// execution of a transaction.
func (w *rpcWrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
var hex hexutil.Big
if err := w.rpcClient.CallContext(ctx, &hex, w.rpcClient.UpstreamChainID, "eth_gasPrice"); err != nil {
if err := w.RPCClient.CallContext(ctx, &hex, w.chainID, "eth_gasPrice"); err != nil {
return nil, err
}
return (*big.Int)(&hex), nil
@ -47,7 +48,7 @@ func (w *rpcWrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
// but it should provide a basis for setting a reasonable default.
func (w *rpcWrapper) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
var hex hexutil.Uint64
err := w.rpcClient.CallContext(ctx, &hex, w.rpcClient.UpstreamChainID, "eth_estimateGas", toCallArg(msg))
err := w.RPCClient.CallContext(ctx, &hex, w.chainID, "eth_estimateGas", toCallArg(msg))
if err != nil {
return 0, err
}
@ -63,7 +64,7 @@ func (w *rpcWrapper) SendTransaction(ctx context.Context, tx *gethtypes.Transact
if err != nil {
return err
}
return w.rpcClient.CallContext(ctx, nil, w.rpcClient.UpstreamChainID, "eth_sendRawTransaction", types.EncodeHex(data))
return w.RPCClient.CallContext(ctx, nil, w.chainID, "eth_sendRawTransaction", types.EncodeHex(data))
}
func toCallArg(msg ethereum.CallMsg) interface{} {

View file

@ -45,12 +45,10 @@ func (e *ErrBadNonce) Error() string {
// Transactor validates, signs transactions.
// It uses upstream to propagate transactions to the Ethereum network.
type Transactor struct {
sender ethereum.TransactionSender
pendingNonceProvider PendingNonceProvider
gasCalculator GasCalculator
sendTxTimeout time.Duration
rpcCallTimeout time.Duration
networkID uint64
rpcWrapper *rpcWrapper
sendTxTimeout time.Duration
rpcCallTimeout time.Duration
networkID uint64
addrLock *AddrLocker
localNonce sync.Map
@ -74,16 +72,19 @@ func (t *Transactor) SetNetworkID(networkID uint64) {
// SetRPC sets RPC params, a client and a timeout
func (t *Transactor) SetRPC(rpcClient *rpc.Client, timeout time.Duration) {
rpcWrapper := newRPCWrapper(rpcClient)
t.sender = rpcWrapper
t.pendingNonceProvider = rpcWrapper
t.gasCalculator = rpcWrapper
t.rpcWrapper = newRPCWrapper(rpcClient, rpcClient.UpstreamChainID)
t.rpcCallTimeout = timeout
}
// SendTransaction is an implementation of eth_sendTransaction. It queues the tx to the sign queue.
func (t *Transactor) SendTransaction(sendArgs SendTxArgs, verifiedAccount *account.SelectedExtKey) (hash types.Hash, err error) {
hash, err = t.validateAndPropagate(verifiedAccount, sendArgs)
hash, err = t.validateAndPropagate(t.rpcWrapper, verifiedAccount, sendArgs)
return
}
func (t *Transactor) SendTransactionWithChainID(chainID uint64, sendArgs SendTxArgs, verifiedAccount *account.SelectedExtKey) (hash types.Hash, err error) {
wrapper := newRPCWrapper(t.rpcWrapper.RPCClient, chainID)
hash, err = t.validateAndPropagate(wrapper, verifiedAccount, sendArgs)
return
}
@ -130,7 +131,7 @@ func (t *Transactor) SendTransactionWithSignature(args SendTxArgs, sig []byte) (
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
if err := t.sender.SendTransaction(ctx, signedTx); err != nil {
if err := t.rpcWrapper.SendTransaction(ctx, signedTx); err != nil {
return hash, err
}
@ -160,7 +161,7 @@ func (t *Transactor) HashTransaction(args SendTxArgs) (validatedArgs SendTxArgs,
if args.GasPrice == nil && args.MaxFeePerGas == nil {
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
gasPrice, err = t.gasCalculator.SuggestGasPrice(ctx)
gasPrice, err = t.rpcWrapper.SuggestGasPrice(ctx)
if err != nil {
return validatedArgs, hash, err
}
@ -183,7 +184,7 @@ func (t *Transactor) HashTransaction(args SendTxArgs) (validatedArgs SendTxArgs,
gethToPtr = &gethTo
}
if args.GasPrice == nil {
gas, err = t.gasCalculator.EstimateGas(ctx, ethereum.CallMsg{
gas, err = t.rpcWrapper.EstimateGas(ctx, ethereum.CallMsg{
From: common.Address(args.From),
To: gethToPtr,
GasFeeCap: gasFeeCap,
@ -192,7 +193,7 @@ func (t *Transactor) HashTransaction(args SendTxArgs) (validatedArgs SendTxArgs,
Data: args.GetInput(),
})
} else {
gas, err = t.gasCalculator.EstimateGas(ctx, ethereum.CallMsg{
gas, err = t.rpcWrapper.EstimateGas(ctx, ethereum.CallMsg{
From: common.Address(args.From),
To: gethToPtr,
GasPrice: gasPrice,
@ -241,7 +242,7 @@ func (t *Transactor) validateAccount(args SendTxArgs, selectedAccount *account.S
return nil
}
func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKey, args SendTxArgs) (hash types.Hash, err error) {
func (t *Transactor) validateAndPropagate(rpcWrapper *rpcWrapper, selectedAccount *account.SelectedExtKey, args SendTxArgs) (hash types.Hash, err error) {
if err = t.validateAccount(args, selectedAccount); err != nil {
return hash, err
}
@ -269,7 +270,7 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe
if args.Nonce == nil {
nonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, common.Address(args.From))
nonce, err = rpcWrapper.PendingNonceAt(ctx, common.Address(args.From))
if err != nil {
return hash, err
}
@ -285,13 +286,13 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe
if !args.IsDynamicFeeTx() && args.GasPrice == nil {
ctx, cancel = context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
gasPrice, err = t.gasCalculator.SuggestGasPrice(ctx)
gasPrice, err = rpcWrapper.SuggestGasPrice(ctx)
if err != nil {
return hash, err
}
}
chainID := big.NewInt(int64(t.networkID))
chainID := big.NewInt(int64(rpcWrapper.chainID))
value := (*big.Int)(args.Value)
var gas uint64
@ -309,7 +310,7 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe
gethTo = common.Address(*args.To)
gethToPtr = &gethTo
}
gas, err = t.gasCalculator.EstimateGas(ctx, ethereum.CallMsg{
gas, err = rpcWrapper.EstimateGas(ctx, ethereum.CallMsg{
From: common.Address(args.From),
To: gethToPtr,
GasPrice: gasPrice,
@ -334,7 +335,7 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe
ctx, cancel = context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
if err := t.sender.SendTransaction(ctx, signedTx); err != nil {
if err := rpcWrapper.SendTransaction(ctx, signedTx); err != nil {
return hash, err
}
return types.Hash(signedTx.Hash()), nil
@ -418,7 +419,7 @@ func (t *Transactor) getTransactionNonce(args SendTxArgs) (newNonce uint64, err
// get the remote nonce
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
remoteNonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, common.Address(args.From))
remoteNonce, err = t.rpcWrapper.PendingNonceAt(ctx, common.Address(args.From))
if err != nil {
return newNonce, err
}

View file

@ -50,6 +50,7 @@ func (s *TransactorSuite) SetupTest() {
s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
s.client = gethrpc.DialInProc(s.server)
rpcClient, _ := rpc.NewClient(s.client, 1, params.UpstreamRPCConfig{}, nil, nil)
rpcClient.UpstreamChainID = 1
// expected by simulated backend
chainID := gethparams.AllEthashProtocolChanges.ChainID.Uint64()
nodeConfig, err := utils.MakeTestNodeConfigWithDataDir("", "/tmp", chainID)
@ -128,7 +129,8 @@ func (s *TransactorSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfig
}
newTx := gethtypes.NewTx(txData)
chainID := big.NewInt(int64(config.NetworkID))
chainID := big.NewInt(int64(1))
signedTx, err := gethtypes.SignTx(newTx, gethtypes.NewLondonSigner(chainID), account.AccountKey.PrivateKey)
s.NoError(err)
data, err := signedTx.MarshalBinary()