saledjenic 79b1c547d1
Improvements on resolving nonce (#5658)
* chore_: unused `BuildTx` function removed from the processor interface and types that are implement it

Since the `BuildTx` function is not used anywhere, it's removed from the code.

* fix_: resolving nonce improvements

When the app sends more than a single tx from the same account on the same chain, some
chains do not return appropriate nonce (they do not consider pending txs), because of
that we place more tx with the same nonce, where all but the first one fail.

Changes in this PR keep track of nonces being used in the same sending/bridging flow, which means
for the first tx from the multi txs the app asks the chain for the nonce, and every next nonce is resolved
by incrementing the last used nonce by 1.
2024-08-12 08:07:32 -04:00

233 lines
7.3 KiB
Go

package transfer
import (
"database/sql"
"encoding/hex"
"errors"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"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"
wallet_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/router/pathprocessor"
)
func rowsToMultiTransactions(rows *sql.Rows) ([]*MultiTransaction, error) {
var multiTransactions []*MultiTransaction
for rows.Next() {
multiTransaction := &MultiTransaction{}
var fromAmountDB, toAmountDB sql.NullString
var fromTxHash, toTxHash sql.RawBytes
err := rows.Scan(
&multiTransaction.ID,
&multiTransaction.FromNetworkID,
&fromTxHash,
&multiTransaction.FromAddress,
&multiTransaction.FromAsset,
&fromAmountDB,
&multiTransaction.ToNetworkID,
&toTxHash,
&multiTransaction.ToAddress,
&multiTransaction.ToAsset,
&toAmountDB,
&multiTransaction.Type,
&multiTransaction.CrossTxID,
&multiTransaction.Timestamp,
)
if len(fromTxHash) > 0 {
multiTransaction.FromTxHash = common.BytesToHash(fromTxHash)
}
if len(toTxHash) > 0 {
multiTransaction.ToTxHash = common.BytesToHash(toTxHash)
}
if err != nil {
return nil, err
}
if fromAmountDB.Valid {
multiTransaction.FromAmount = new(hexutil.Big)
if _, ok := (*big.Int)(multiTransaction.FromAmount).SetString(fromAmountDB.String, 0); !ok {
return nil, errors.New("failed to convert fromAmountDB.String to big.Int: " + fromAmountDB.String)
}
}
if toAmountDB.Valid {
multiTransaction.ToAmount = new(hexutil.Big)
if _, ok := (*big.Int)(multiTransaction.ToAmount).SetString(toAmountDB.String, 0); !ok {
return nil, errors.New("failed to convert fromAmountDB.String to big.Int: " + toAmountDB.String)
}
}
multiTransactions = append(multiTransactions, multiTransaction)
}
return multiTransactions, nil
}
func addSignaturesToTransactions(transactions map[common.Hash]*TransactionDescription, signatures map[string]SignatureDetails) error {
if len(transactions) == 0 {
return errors.New("no transactions to proceed with")
}
if len(signatures) != len(transactions) {
return errors.New("not all transactions have been signed")
}
// check if all transactions have been signed
for hash, desc := range transactions {
sigDetails, ok := signatures[hash.String()]
if !ok {
return fmt.Errorf("missing signature for transaction %s", hash)
}
rBytes, _ := hex.DecodeString(sigDetails.R)
sBytes, _ := hex.DecodeString(sigDetails.S)
vByte := byte(0)
if sigDetails.V == "01" {
vByte = 1
}
desc.signature = make([]byte, crypto.SignatureLength)
copy(desc.signature[32-len(rBytes):32], rBytes)
copy(desc.signature[64-len(rBytes):64], sBytes)
desc.signature[64] = vByte
}
return nil
}
func multiTransactionFromCommand(command *MultiTransactionCommand) *MultiTransaction {
toAmount := new(hexutil.Big)
if command.ToAmount != nil {
toAmount = command.ToAmount
}
multiTransaction := NewMultiTransaction(
/* Timestamp: */ uint64(time.Now().Unix()),
/* FromNetworkID: */ 0,
/* ToNetworkID: */ 0,
/* FromTxHash: */ common.Hash{},
/* ToTxHash: */ common.Hash{},
/* FromAddress: */ command.FromAddress,
/* ToAddress: */ command.ToAddress,
/* FromAsset: */ command.FromAsset,
/* ToAsset: */ command.ToAsset,
/* FromAmount: */ command.FromAmount,
/* ToAmount: */ toAmount,
/* Type: */ command.Type,
/* CrossTxID: */ "",
)
return multiTransaction
}
func updateDataFromMultiTx(data []*pathprocessor.MultipathProcessorTxArgs, multiTransaction *MultiTransaction) {
for _, tx := range data {
if tx.TransferTx != nil {
tx.TransferTx.MultiTransactionID = multiTransaction.ID
tx.TransferTx.Symbol = multiTransaction.FromAsset
}
if tx.HopTx != nil {
tx.HopTx.MultiTransactionID = multiTransaction.ID
tx.HopTx.Symbol = multiTransaction.FromAsset
}
if tx.CbridgeTx != nil {
tx.CbridgeTx.MultiTransactionID = multiTransaction.ID
tx.CbridgeTx.Symbol = multiTransaction.FromAsset
}
if tx.ERC721TransferTx != nil {
tx.ERC721TransferTx.MultiTransactionID = multiTransaction.ID
tx.ERC721TransferTx.Symbol = multiTransaction.FromAsset
}
if tx.ERC1155TransferTx != nil {
tx.ERC1155TransferTx.MultiTransactionID = multiTransaction.ID
tx.ERC1155TransferTx.Symbol = multiTransaction.FromAsset
}
if tx.SwapTx != nil {
tx.SwapTx.MultiTransactionID = multiTransaction.ID
tx.SwapTx.Symbol = multiTransaction.FromAsset
}
}
}
func sendTransactions(data []*pathprocessor.MultipathProcessorTxArgs, pathProcessors map[string]pathprocessor.PathProcessor, account *account.SelectedExtKey) (
map[uint64][]types.Hash, error) {
hashes := make(map[uint64][]types.Hash)
usedNonces := make(map[uint64]int64)
for _, tx := range data {
lastUsedNonce := int64(-1)
if nonce, ok := usedNonces[tx.ChainID]; ok {
lastUsedNonce = nonce
}
hash, usedNonce, err := pathProcessors[tx.Name].Send(tx, lastUsedNonce, account)
if err != nil {
return nil, err // TODO: One of transfers within transaction could have been sent. Need to notify user about it
}
hashes[tx.ChainID] = append(hashes[tx.ChainID], hash)
usedNonces[tx.ChainID] = int64(usedNonce)
}
return hashes, nil
}
func idFromTimestamp() wallet_common.MultiTransactionIDType {
return wallet_common.MultiTransactionIDType(time.Now().UnixMilli())
}
var multiTransactionIDGenerator func() wallet_common.MultiTransactionIDType = idFromTimestamp
func (tm *TransactionManager) removeMultiTransactionByAddress(address common.Address) error {
// We must not remove those transactions, where from_address and to_address are different and both are stored in accounts DB
// and one of them is equal to the address, as we want to keep the records for the other address
// That is why we don't use cascade delete here with references to transfers table, as we might have 2 records in multi_transactions
// for the same transaction, one for each address
details := NewMultiTxDetails()
details.FromAddress = address
mtxs, err := tm.storage.ReadMultiTransactions(details)
ids := make([]wallet_common.MultiTransactionIDType, 0)
for _, mtx := range mtxs {
// Remove self transactions as well, leave only those where we have the counterparty in accounts DB
if mtx.FromAddress != mtx.ToAddress {
// If both addresses are stored in accounts DB, we don't remove the record
var addressToCheck common.Address
if mtx.FromAddress == address {
addressToCheck = mtx.ToAddress
} else {
addressToCheck = mtx.FromAddress
}
counterpartyExists, err := tm.accountsDB.AddressExists(types.Address(addressToCheck))
if err != nil {
log.Error("Failed to query accounts db for a given address", "address", address, "error", err)
continue
}
// Skip removal if counterparty is in accounts DB and removed address is not sender
if counterpartyExists && address != mtx.FromAddress {
continue
}
}
ids = append(ids, mtx.ID)
}
if len(ids) > 0 {
for _, id := range ids {
err = tm.storage.DeleteMultiTransaction(id)
if err != nil {
log.Error("Failed to delete multi transaction", "id", id, "error", err)
}
}
}
return err
}