560 lines
16 KiB
Go
560 lines
16 KiB
Go
package transfer
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"math/big"
|
|
"testing"
|
|
|
|
eth_common "github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
eth_types "github.com/status-im/status-go/eth-node/types"
|
|
|
|
"github.com/status-im/status-go/services/wallet/bigint"
|
|
"github.com/status-im/status-go/services/wallet/common"
|
|
wallet_common "github.com/status-im/status-go/services/wallet/common"
|
|
"github.com/status-im/status-go/services/wallet/router/pathprocessor"
|
|
"github.com/status-im/status-go/services/wallet/testutils"
|
|
"github.com/status-im/status-go/services/wallet/token"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type TestTransaction struct {
|
|
Hash eth_common.Hash
|
|
ChainID common.ChainID
|
|
From eth_common.Address // [sender]
|
|
Timestamp int64
|
|
BlkNumber int64
|
|
Success bool
|
|
Nonce uint64
|
|
Contract eth_common.Address
|
|
MultiTransactionID common.MultiTransactionIDType
|
|
}
|
|
|
|
type TestTransfer struct {
|
|
TestTransaction
|
|
To eth_common.Address // [address]
|
|
Value int64
|
|
Token *token.Token
|
|
}
|
|
|
|
type TestCollectibleTransfer struct {
|
|
TestTransfer
|
|
TestCollectible
|
|
}
|
|
|
|
type TestApprove struct {
|
|
TestTransaction
|
|
Spender eth_common.Address // [address]
|
|
Amount int64
|
|
Token *token.Token
|
|
}
|
|
|
|
func SeedToToken(seed int) *token.Token {
|
|
tokenIndex := seed % len(TestTokens)
|
|
return TestTokens[tokenIndex]
|
|
}
|
|
|
|
func TestTrToToken(t *testing.T, tt *TestTransaction) (token *token.Token, isNative bool) {
|
|
// Sanity check that none of the markers changed and they should be equal to seed
|
|
require.Equal(t, tt.Timestamp, tt.BlkNumber)
|
|
|
|
tokenIndex := int(tt.Timestamp) % len(TestTokens)
|
|
isNative = testutils.SliceContains(NativeTokenIndices, tokenIndex)
|
|
|
|
return TestTokens[tokenIndex], isNative
|
|
}
|
|
|
|
func generateTestTransaction(seed int) TestTransaction {
|
|
token := SeedToToken(seed)
|
|
return TestTransaction{
|
|
Hash: eth_common.HexToHash(fmt.Sprintf("0x1%d", seed)),
|
|
ChainID: common.ChainID(token.ChainID),
|
|
From: eth_common.HexToAddress(fmt.Sprintf("0x2%d", seed)),
|
|
Timestamp: int64(seed),
|
|
BlkNumber: int64(seed),
|
|
Success: true,
|
|
Nonce: uint64(seed),
|
|
// In practice this is last20Bytes(Keccak256(RLP(From, nonce)))
|
|
Contract: eth_common.HexToAddress(fmt.Sprintf("0x4%d", seed)),
|
|
MultiTransactionID: common.NoMultiTransactionID,
|
|
}
|
|
}
|
|
|
|
func generateTestTransfer(seed int) TestTransfer {
|
|
tokenIndex := seed % len(TestTokens)
|
|
token := TestTokens[tokenIndex]
|
|
return TestTransfer{
|
|
TestTransaction: generateTestTransaction(seed),
|
|
To: eth_common.HexToAddress(fmt.Sprintf("0x3%d", seed)),
|
|
Value: int64(seed),
|
|
Token: token,
|
|
}
|
|
}
|
|
|
|
// Will be used in tests to generate a collectible transfer
|
|
// nolint:unused
|
|
func generateTestCollectibleTransfer(seed int) TestCollectibleTransfer {
|
|
collectibleIndex := seed % len(TestCollectibles)
|
|
collectible := TestCollectibles[collectibleIndex]
|
|
tr := TestCollectibleTransfer{
|
|
TestTransfer: TestTransfer{
|
|
TestTransaction: generateTestTransaction(seed),
|
|
To: eth_common.HexToAddress(fmt.Sprintf("0x3%d", seed)),
|
|
Value: int64(seed),
|
|
Token: &token.Token{
|
|
Address: collectible.TokenAddress,
|
|
Name: "Collectible",
|
|
ChainID: uint64(collectible.ChainID),
|
|
},
|
|
},
|
|
TestCollectible: collectible,
|
|
}
|
|
tr.TestTransaction.ChainID = collectible.ChainID
|
|
return tr
|
|
}
|
|
|
|
func generateTestApprove(seed int) TestApprove {
|
|
tokenIndex := seed % len(TestTokens)
|
|
token := TestTokens[tokenIndex]
|
|
return TestApprove{
|
|
TestTransaction: generateTestTransaction(seed),
|
|
Spender: eth_common.HexToAddress(fmt.Sprintf("0x3%d", seed)),
|
|
Amount: int64(seed),
|
|
Token: token,
|
|
}
|
|
}
|
|
|
|
func GenerateTestSendMultiTransaction(tr TestTransfer) MultiTransaction {
|
|
return MultiTransaction{
|
|
ID: multiTransactionIDGenerator(),
|
|
Type: MultiTransactionSend,
|
|
FromAddress: tr.From,
|
|
ToAddress: tr.To,
|
|
FromAsset: tr.Token.Symbol,
|
|
ToAsset: tr.Token.Symbol,
|
|
FromAmount: (*hexutil.Big)(big.NewInt(tr.Value)),
|
|
ToAmount: (*hexutil.Big)(big.NewInt(0)),
|
|
Timestamp: uint64(tr.Timestamp),
|
|
}
|
|
}
|
|
|
|
func GenerateTestSwapMultiTransaction(tr TestTransfer, toToken string, toAmount int64) MultiTransaction {
|
|
return MultiTransaction{
|
|
ID: multiTransactionIDGenerator(),
|
|
Type: MultiTransactionSwap,
|
|
FromAddress: tr.From,
|
|
ToAddress: tr.To,
|
|
FromAsset: tr.Token.Symbol,
|
|
ToAsset: toToken,
|
|
FromAmount: (*hexutil.Big)(big.NewInt(tr.Value)),
|
|
ToAmount: (*hexutil.Big)(big.NewInt(toAmount)),
|
|
Timestamp: uint64(tr.Timestamp),
|
|
}
|
|
}
|
|
|
|
func GenerateTestBridgeMultiTransaction(fromTr, toTr TestTransfer) MultiTransaction {
|
|
return MultiTransaction{
|
|
ID: multiTransactionIDGenerator(),
|
|
Type: MultiTransactionBridge,
|
|
FromAddress: fromTr.From,
|
|
ToAddress: toTr.To,
|
|
FromAsset: fromTr.Token.Symbol,
|
|
ToAsset: toTr.Token.Symbol,
|
|
FromAmount: (*hexutil.Big)(big.NewInt(fromTr.Value)),
|
|
ToAmount: (*hexutil.Big)(big.NewInt(toTr.Value)),
|
|
Timestamp: uint64(fromTr.Timestamp),
|
|
}
|
|
}
|
|
|
|
func GenerateTestApproveMultiTransaction(tr TestApprove) MultiTransaction {
|
|
return MultiTransaction{
|
|
ID: multiTransactionIDGenerator(),
|
|
Type: MultiTransactionApprove,
|
|
FromAddress: tr.From,
|
|
ToAddress: tr.Spender,
|
|
FromAsset: tr.Token.Symbol,
|
|
ToAsset: tr.Token.Symbol,
|
|
FromAmount: (*hexutil.Big)(big.NewInt(tr.Amount)),
|
|
ToAmount: (*hexutil.Big)(big.NewInt(0)),
|
|
Timestamp: uint64(tr.Timestamp),
|
|
}
|
|
}
|
|
|
|
// GenerateTestTransfers will generate transaction based on the TestTokens index and roll over if there are more than
|
|
// len(TestTokens) transactions
|
|
func GenerateTestTransfers(tb testing.TB, db *sql.DB, firstStartIndex int, count int) (result []TestTransfer, fromAddresses, toAddresses []eth_common.Address) {
|
|
for i := firstStartIndex; i < (firstStartIndex + count); i++ {
|
|
tr := generateTestTransfer(i)
|
|
fromAddresses = append(fromAddresses, tr.From)
|
|
toAddresses = append(toAddresses, tr.To)
|
|
result = append(result, tr)
|
|
}
|
|
return
|
|
}
|
|
|
|
// GenerateTesApproves will generate transaction based on the TestTokens index and roll over if there are more than
|
|
// len(TestTokens) transactions
|
|
func GenerateTestApproves(tb testing.TB, db *sql.DB, firstStartIndex int, count int) (result []TestApprove, fromAddresses []eth_common.Address) {
|
|
for i := firstStartIndex; i < (firstStartIndex + count); i++ {
|
|
tr := generateTestApprove(i)
|
|
fromAddresses = append(fromAddresses, tr.From)
|
|
result = append(result, tr)
|
|
}
|
|
return
|
|
}
|
|
|
|
type TestCollectible struct {
|
|
TokenAddress eth_common.Address
|
|
TokenID *big.Int
|
|
ChainID common.ChainID
|
|
}
|
|
|
|
var TestCollectibles = []TestCollectible{
|
|
TestCollectible{
|
|
TokenAddress: eth_common.HexToAddress("0x97a04fda4d97c6e3547d66b572e29f4a4ff40392"),
|
|
TokenID: big.NewInt(1),
|
|
ChainID: 1,
|
|
},
|
|
TestCollectible{ // Same token ID as above but different address
|
|
TokenAddress: eth_common.HexToAddress("0x2cec8879915cdbd80c88d8b1416aa9413a24ddfa"),
|
|
TokenID: big.NewInt(1),
|
|
ChainID: 1,
|
|
},
|
|
TestCollectible{ // TokenID (big.Int) value 0 might be problematic if not handled properly
|
|
TokenAddress: eth_common.HexToAddress("0x97a04fda4d97c6e3547d66b572e29f4a4ff4ABCD"),
|
|
TokenID: big.NewInt(0),
|
|
ChainID: 420,
|
|
},
|
|
TestCollectible{
|
|
TokenAddress: eth_common.HexToAddress("0x1dea7a3e04849840c0eb15fd26a55f6c40c4a69b"),
|
|
TokenID: big.NewInt(11),
|
|
ChainID: 5,
|
|
},
|
|
TestCollectible{ // Same address as above but different token ID
|
|
TokenAddress: eth_common.HexToAddress("0x1dea7a3e04849840c0eb15fd26a55f6c40c4a69b"),
|
|
TokenID: big.NewInt(12),
|
|
ChainID: 5,
|
|
},
|
|
}
|
|
|
|
var EthMainnet = token.Token{
|
|
Address: eth_common.HexToAddress("0x"),
|
|
Name: "Ether",
|
|
Symbol: "ETH",
|
|
ChainID: 1,
|
|
}
|
|
|
|
var EthGoerli = token.Token{
|
|
Address: eth_common.HexToAddress("0x"),
|
|
Name: "Ether",
|
|
Symbol: "ETH",
|
|
ChainID: 5,
|
|
}
|
|
|
|
var EthOptimism = token.Token{
|
|
Address: eth_common.HexToAddress("0x"),
|
|
Name: "Ether",
|
|
Symbol: "ETH",
|
|
ChainID: 10,
|
|
}
|
|
|
|
var UsdcMainnet = token.Token{
|
|
Address: eth_common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
|
|
Name: "USD Coin",
|
|
Symbol: "USDC",
|
|
ChainID: 1,
|
|
}
|
|
|
|
var UsdcGoerli = token.Token{
|
|
Address: eth_common.HexToAddress("0x98339d8c260052b7ad81c28c16c0b98420f2b46a"),
|
|
Name: "USD Coin",
|
|
Symbol: "USDC",
|
|
ChainID: 5,
|
|
}
|
|
|
|
var UsdcOptimism = token.Token{
|
|
Address: eth_common.HexToAddress("0x7f5c764cbc14f9669b88837ca1490cca17c31607"),
|
|
Name: "USD Coin",
|
|
Symbol: "USDC",
|
|
ChainID: 10,
|
|
}
|
|
|
|
var SntMainnet = token.Token{
|
|
Address: eth_common.HexToAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e"),
|
|
Name: "Status Network Token",
|
|
Symbol: "SNT",
|
|
ChainID: 1,
|
|
}
|
|
|
|
var DaiMainnet = token.Token{
|
|
Address: eth_common.HexToAddress("0xf2edF1c091f683E3fb452497d9a98A49cBA84666"),
|
|
Name: "DAI Stablecoin",
|
|
Symbol: "DAI",
|
|
ChainID: 5,
|
|
}
|
|
|
|
var DaiGoerli = token.Token{
|
|
Address: eth_common.HexToAddress("0xf2edF1c091f683E3fb452497d9a98A49cBA84666"),
|
|
Name: "DAI Stablecoin",
|
|
Symbol: "DAI",
|
|
ChainID: 5,
|
|
}
|
|
|
|
// TestTokens contains ETH/Mainnet, ETH/Goerli, ETH/Optimism, USDC/Mainnet, USDC/Goerli, USDC/Optimism, SNT/Mainnet, DAI/Mainnet, DAI/Goerli
|
|
var TestTokens = []*token.Token{
|
|
&EthMainnet, &EthGoerli, &EthOptimism, &UsdcMainnet, &UsdcGoerli, &UsdcOptimism, &SntMainnet, &DaiMainnet, &DaiGoerli,
|
|
}
|
|
|
|
func LookupTokenIdentity(chainID uint64, address eth_common.Address, native bool) *token.Token {
|
|
for _, token := range TestTokens {
|
|
if token.ChainID == chainID && token.Address == address && token.IsNative() == native {
|
|
return token
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var NativeTokenIndices = []int{0, 1, 2}
|
|
|
|
func InsertTestTransfer(tb testing.TB, db *sql.DB, address eth_common.Address, tr *TestTransfer) {
|
|
token := TestTokens[int(tr.Timestamp)%len(TestTokens)]
|
|
InsertTestTransferWithOptions(tb, db, address, tr, &TestTransferOptions{
|
|
TokenAddress: token.Address,
|
|
})
|
|
}
|
|
|
|
type TestTransferOptions struct {
|
|
TokenAddress eth_common.Address
|
|
TokenID *big.Int
|
|
NullifyAddresses []eth_common.Address
|
|
Tx *types.Transaction
|
|
Receipt *types.Receipt
|
|
}
|
|
|
|
func GenerateTxField(data []byte) *types.Transaction {
|
|
return types.NewTx(&types.DynamicFeeTx{
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
func InsertTestTransferWithOptions(tb testing.TB, db *sql.DB, address eth_common.Address, tr *TestTransfer, opt *TestTransferOptions) {
|
|
var (
|
|
tx *sql.Tx
|
|
)
|
|
tx, err := db.Begin()
|
|
require.NoError(tb, err)
|
|
defer func() {
|
|
if err == nil {
|
|
err = tx.Commit()
|
|
return
|
|
}
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
blkHash := eth_common.HexToHash("4")
|
|
|
|
block := blockDBFields{
|
|
chainID: uint64(tr.ChainID),
|
|
account: address,
|
|
blockNumber: big.NewInt(tr.BlkNumber),
|
|
blockHash: blkHash,
|
|
}
|
|
|
|
// Respect `FOREIGN KEY(network_id,address,blk_hash)` of `transfers` table
|
|
err = insertBlockDBFields(tx, block)
|
|
require.NoError(tb, err)
|
|
|
|
receiptStatus := uint64(0)
|
|
if tr.Success {
|
|
receiptStatus = 1
|
|
}
|
|
|
|
tokenType := "eth"
|
|
if (opt.TokenAddress != eth_common.Address{}) {
|
|
if opt.TokenID == nil {
|
|
tokenType = "erc20"
|
|
} else {
|
|
tokenType = "erc721"
|
|
}
|
|
}
|
|
|
|
// Workaround to simulate writing of NULL values for addresses
|
|
txTo := &tr.To
|
|
txFrom := &tr.From
|
|
for i := 0; i < len(opt.NullifyAddresses); i++ {
|
|
if opt.NullifyAddresses[i] == tr.To {
|
|
txTo = nil
|
|
}
|
|
if opt.NullifyAddresses[i] == tr.From {
|
|
txFrom = nil
|
|
}
|
|
}
|
|
|
|
var transactionTo *eth_common.Address
|
|
if opt.Tx != nil {
|
|
transactionTo = opt.Tx.To()
|
|
}
|
|
var eventLogAddress *eth_common.Address
|
|
if opt.Receipt != nil {
|
|
eventLogAddress = &opt.Receipt.Logs[0].Address
|
|
}
|
|
transfer := transferDBFields{
|
|
chainID: uint64(tr.ChainID),
|
|
id: tr.Hash,
|
|
txHash: &tr.Hash,
|
|
address: address,
|
|
blockHash: blkHash,
|
|
blockNumber: big.NewInt(tr.BlkNumber),
|
|
sender: tr.From,
|
|
transferType: common.Type(tokenType),
|
|
timestamp: uint64(tr.Timestamp),
|
|
multiTransactionID: tr.MultiTransactionID,
|
|
baseGasFees: "0x0",
|
|
receiptStatus: &receiptStatus,
|
|
txValue: big.NewInt(tr.Value),
|
|
txFrom: txFrom,
|
|
txTo: txTo,
|
|
txNonce: &tr.Nonce,
|
|
tokenAddress: &opt.TokenAddress,
|
|
contractAddress: &tr.Contract,
|
|
tokenID: opt.TokenID,
|
|
transaction: opt.Tx,
|
|
receipt: opt.Receipt,
|
|
transactionTo: transactionTo,
|
|
eventLogAddress: eventLogAddress,
|
|
}
|
|
err = updateOrInsertTransfersDBFields(tx, []transferDBFields{transfer})
|
|
require.NoError(tb, err)
|
|
}
|
|
|
|
func InsertTestPendingTransaction(tb testing.TB, db *sql.DB, tr *TestTransfer) {
|
|
_, err := db.Exec(`
|
|
INSERT INTO pending_transactions (network_id, hash, timestamp, from_address, to_address,
|
|
symbol, gas_price, gas_limit, value, data, type, additional_data, multi_transaction_id, transaction_to,
|
|
) VALUES (?, ?, ?, ?, ?, 'ETH', 0, 0, ?, '', 'eth', '', ?, ?)`,
|
|
tr.ChainID, tr.Hash, tr.Timestamp, tr.From, tr.To, (*bigint.SQLBigIntBytes)(big.NewInt(tr.Value)), tr.MultiTransactionID, tr.Token.Address)
|
|
require.NoError(tb, err)
|
|
}
|
|
|
|
func InsertTestMultiTransaction(tb testing.TB, db *sql.DB, tr *MultiTransaction) common.MultiTransactionIDType {
|
|
if tr.FromAsset == "" {
|
|
tr.FromAsset = testutils.EthSymbol
|
|
}
|
|
if tr.ToAsset == "" {
|
|
tr.ToAsset = testutils.EthSymbol
|
|
}
|
|
|
|
tr.ID = multiTransactionIDGenerator()
|
|
multiTxDB := NewMultiTransactionDB(db)
|
|
err := multiTxDB.CreateMultiTransaction(tr)
|
|
require.NoError(tb, err)
|
|
return tr.ID
|
|
}
|
|
|
|
// For using in tests only outside the package
|
|
func SaveTransfersMarkBlocksLoaded(database *Database, chainID uint64, address eth_common.Address, transfers []Transfer, blocks []*big.Int) error {
|
|
return saveTransfersMarkBlocksLoaded(database.client, chainID, address, transfers, blocks)
|
|
}
|
|
|
|
func SetMultiTransactionIDGenerator(f func() common.MultiTransactionIDType) {
|
|
multiTransactionIDGenerator = f
|
|
}
|
|
|
|
func StaticIDCounter() (f func() common.MultiTransactionIDType) {
|
|
var i int
|
|
f = func() common.MultiTransactionIDType {
|
|
i++
|
|
return common.MultiTransactionIDType(i)
|
|
}
|
|
return
|
|
}
|
|
|
|
type InMemMultiTransactionStorage struct {
|
|
storage map[common.MultiTransactionIDType]*MultiTransaction
|
|
}
|
|
|
|
func NewInMemMultiTransactionStorage() *InMemMultiTransactionStorage {
|
|
return &InMemMultiTransactionStorage{
|
|
storage: make(map[common.MultiTransactionIDType]*MultiTransaction),
|
|
}
|
|
}
|
|
|
|
func (s *InMemMultiTransactionStorage) CreateMultiTransaction(multiTx *MultiTransaction) error {
|
|
s.storage[multiTx.ID] = multiTx
|
|
return nil
|
|
}
|
|
|
|
func (s *InMemMultiTransactionStorage) GetMultiTransaction(id common.MultiTransactionIDType) (*MultiTransaction, error) {
|
|
multiTx, ok := s.storage[id]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return multiTx, nil
|
|
}
|
|
|
|
func (s *InMemMultiTransactionStorage) UpdateMultiTransaction(multiTx *MultiTransaction) error {
|
|
s.storage[multiTx.ID] = multiTx
|
|
return nil
|
|
}
|
|
|
|
func (s *InMemMultiTransactionStorage) DeleteMultiTransaction(id common.MultiTransactionIDType) error {
|
|
delete(s.storage, id)
|
|
return nil
|
|
}
|
|
|
|
func (s *InMemMultiTransactionStorage) ReadMultiTransactions(details *MultiTxDetails) ([]*MultiTransaction, error) {
|
|
var multiTxs []*MultiTransaction
|
|
for _, multiTx := range s.storage {
|
|
if len(details.IDs) > 0 && !testutils.SliceContains(details.IDs, multiTx.ID) {
|
|
continue
|
|
}
|
|
|
|
if (details.AnyAddress != eth_common.Address{}) &&
|
|
(multiTx.FromAddress != details.AnyAddress && multiTx.ToAddress != details.AnyAddress) {
|
|
continue
|
|
}
|
|
|
|
if (details.FromAddress != eth_common.Address{}) && multiTx.FromAddress != details.FromAddress {
|
|
continue
|
|
}
|
|
|
|
if (details.ToAddress != eth_common.Address{}) && multiTx.ToAddress != details.ToAddress {
|
|
continue
|
|
}
|
|
|
|
if details.ToChainID != 0 && multiTx.ToNetworkID != details.ToChainID {
|
|
continue
|
|
}
|
|
|
|
if details.Type != MultiTransactionDBTypeInvalid && multiTx.Type != mtDBTypeToMTType(details.Type) {
|
|
continue
|
|
}
|
|
|
|
if details.CrossTxID != "" && multiTx.CrossTxID != details.CrossTxID {
|
|
continue
|
|
}
|
|
|
|
multiTxs = append(multiTxs, multiTx)
|
|
}
|
|
return multiTxs, nil
|
|
}
|
|
|
|
type InMemTransactionInputDataStorage struct {
|
|
storage map[string]*pathprocessor.TransactionInputData
|
|
}
|
|
|
|
func NewInMemTransactionInputDataStorage() *InMemTransactionInputDataStorage {
|
|
return &InMemTransactionInputDataStorage{
|
|
storage: make(map[string]*pathprocessor.TransactionInputData),
|
|
}
|
|
}
|
|
|
|
func (s *InMemTransactionInputDataStorage) UpsertInputData(chainID wallet_common.ChainID, txHash eth_types.Hash, inputData pathprocessor.TransactionInputData) error {
|
|
key := fmt.Sprintf("%d-%s", chainID, txHash.String())
|
|
s.storage[key] = &inputData
|
|
return nil
|
|
}
|