2024-01-08 18:31:33 +00:00
|
|
|
package transactions
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"math/big"
|
2024-01-08 21:24:30 +00:00
|
|
|
"testing"
|
2024-01-08 18:31:33 +00:00
|
|
|
|
|
|
|
eth "github.com/ethereum/go-ethereum/common"
|
2024-01-08 21:24:30 +00:00
|
|
|
"github.com/ethereum/go-ethereum/core/types"
|
2024-01-08 18:31:33 +00:00
|
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
|
|
"github.com/status-im/status-go/rpc/chain"
|
|
|
|
"github.com/status-im/status-go/services/wallet/bigint"
|
|
|
|
"github.com/status-im/status-go/services/wallet/common"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/mock"
|
2024-01-08 21:24:30 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2024-01-08 18:31:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type MockETHClient struct {
|
|
|
|
mock.Mock
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MockETHClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
|
|
|
|
args := m.Called(ctx, b)
|
|
|
|
return args.Error(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
type MockChainClient struct {
|
|
|
|
mock.Mock
|
|
|
|
|
|
|
|
Clients map[common.ChainID]*MockETHClient
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewMockChainClient() *MockChainClient {
|
|
|
|
return &MockChainClient{
|
|
|
|
Clients: make(map[common.ChainID]*MockETHClient),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MockChainClient) SetAvailableClients(chainIDs []common.ChainID) *MockChainClient {
|
|
|
|
for _, chainID := range chainIDs {
|
|
|
|
if _, ok := m.Clients[chainID]; !ok {
|
|
|
|
m.Clients[chainID] = new(MockETHClient)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MockChainClient) AbstractEthClient(chainID common.ChainID) (chain.BatchCallClient, error) {
|
|
|
|
if _, ok := m.Clients[chainID]; !ok {
|
|
|
|
panic(fmt.Sprintf("no mock client for chainID %d", chainID))
|
|
|
|
}
|
|
|
|
return m.Clients[chainID], nil
|
|
|
|
}
|
|
|
|
|
2024-01-26 04:31:18 +00:00
|
|
|
func GenerateTestPendingTransactions(start int, count int) []PendingTransaction {
|
2024-01-08 18:31:33 +00:00
|
|
|
if count > 127 {
|
|
|
|
panic("can't generate more than 127 distinct transactions")
|
|
|
|
}
|
|
|
|
|
|
|
|
txs := make([]PendingTransaction, count)
|
2024-01-26 04:31:18 +00:00
|
|
|
for i := start; i < count; i++ {
|
2024-01-08 18:31:33 +00:00
|
|
|
txs[i] = PendingTransaction{
|
2024-01-26 04:31:18 +00:00
|
|
|
Hash: eth.HexToHash(fmt.Sprintf("0x1%d", i)),
|
|
|
|
From: eth.HexToAddress(fmt.Sprintf("0x2%d", i)),
|
|
|
|
To: eth.HexToAddress(fmt.Sprintf("0x3%d", i)),
|
2024-01-08 18:31:33 +00:00
|
|
|
Type: RegisterENS,
|
|
|
|
AdditionalData: "someuser.stateofus.eth",
|
2024-01-26 04:31:18 +00:00
|
|
|
Value: bigint.BigInt{Int: big.NewInt(int64(i))},
|
2024-01-08 18:31:33 +00:00
|
|
|
GasLimit: bigint.BigInt{Int: big.NewInt(21000)},
|
2024-01-26 04:31:18 +00:00
|
|
|
GasPrice: bigint.BigInt{Int: big.NewInt(int64(i))},
|
2024-01-08 18:31:33 +00:00
|
|
|
ChainID: 777,
|
|
|
|
Status: new(TxStatus),
|
|
|
|
AutoDelete: new(bool),
|
2024-01-26 04:31:18 +00:00
|
|
|
Symbol: "ETH",
|
|
|
|
Timestamp: uint64(i),
|
2024-01-08 18:31:33 +00:00
|
|
|
}
|
|
|
|
*txs[i].Status = Pending // set to pending by default
|
|
|
|
*txs[i].AutoDelete = true // set to true by default
|
|
|
|
}
|
|
|
|
return txs
|
|
|
|
}
|
2024-01-08 21:24:30 +00:00
|
|
|
|
2024-02-09 01:55:33 +00:00
|
|
|
// groupSliceInMap groups a slice of S into a map[K][]N using the getKeyValue function to extract the key and new value for each entry
|
|
|
|
func groupSliceInMap[S any, K comparable, N any](s []S, getKeyValue func(entry S, i int) (K, N)) map[K][]N {
|
|
|
|
m := make(map[K][]N)
|
|
|
|
for i, x := range s {
|
|
|
|
k, v := getKeyValue(x, i)
|
|
|
|
m[k] = append(m[k], v)
|
|
|
|
}
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
func keysInMap[K comparable, V any](m map[K]V) (res []K) {
|
|
|
|
if len(m) > 0 {
|
|
|
|
res = make([]K, 0, len(m))
|
|
|
|
}
|
|
|
|
|
|
|
|
for k := range m {
|
|
|
|
res = append(res, k)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-08 21:24:30 +00:00
|
|
|
type TestTxSummary struct {
|
|
|
|
failStatus bool
|
2024-01-26 04:31:18 +00:00
|
|
|
DontConfirm bool
|
|
|
|
// Timestamp will be used to mock the Timestamp if greater than 0
|
|
|
|
Timestamp int
|
2024-01-08 21:24:30 +00:00
|
|
|
}
|
|
|
|
|
2024-02-09 01:55:33 +00:00
|
|
|
type summaryTxPair struct {
|
|
|
|
summary TestTxSummary
|
|
|
|
tx PendingTransaction
|
|
|
|
answered bool
|
|
|
|
}
|
|
|
|
|
2024-01-08 21:24:30 +00:00
|
|
|
func MockTestTransactions(t *testing.T, chainClient *MockChainClient, testTxs []TestTxSummary) []PendingTransaction {
|
2024-02-09 01:55:33 +00:00
|
|
|
genTxs := GenerateTestPendingTransactions(0, len(testTxs))
|
|
|
|
for i, tx := range testTxs {
|
|
|
|
if tx.Timestamp > 0 {
|
|
|
|
genTxs[i].Timestamp = uint64(tx.Timestamp)
|
|
|
|
}
|
|
|
|
}
|
2024-01-26 04:31:18 +00:00
|
|
|
|
2024-02-09 01:55:33 +00:00
|
|
|
grouped := groupSliceInMap(genTxs, func(tx PendingTransaction, i int) (common.ChainID, summaryTxPair) {
|
|
|
|
return tx.ChainID, summaryTxPair{
|
|
|
|
summary: testTxs[i],
|
|
|
|
tx: tx,
|
2024-01-08 21:24:30 +00:00
|
|
|
}
|
2024-02-09 01:55:33 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
chains := keysInMap(grouped)
|
|
|
|
chainClient.SetAvailableClients(chains)
|
2024-01-08 21:24:30 +00:00
|
|
|
|
2024-02-09 01:55:33 +00:00
|
|
|
for chainID, chainSummaries := range grouped {
|
|
|
|
// Mock the one call to getTransactionReceipt
|
|
|
|
// It is expected that pending transactions manager will call out of order, therefore match based on hash
|
|
|
|
cl := chainClient.Clients[chainID]
|
2024-01-26 04:31:18 +00:00
|
|
|
call := cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
|
2024-02-09 01:55:33 +00:00
|
|
|
if len(b) > len(chainSummaries) {
|
|
|
|
return false
|
|
|
|
}
|
2024-01-26 04:31:18 +00:00
|
|
|
for i := range b {
|
2024-04-18 16:48:02 +00:00
|
|
|
for _, localSummary := range chainSummaries {
|
|
|
|
// to satisfy gosec: C601 checks
|
|
|
|
sum := localSummary
|
2024-02-09 01:55:33 +00:00
|
|
|
tx := &sum.tx
|
|
|
|
if sum.answered {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
require.Equal(t, GetTransactionReceiptRPCName, b[i].Method)
|
|
|
|
if tx.Hash == b[i].Args[0].(eth.Hash) {
|
|
|
|
sum.answered = true
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2024-01-08 21:24:30 +00:00
|
|
|
}
|
2024-02-09 01:55:33 +00:00
|
|
|
return false
|
2024-01-26 04:31:18 +00:00
|
|
|
})).Return(nil)
|
|
|
|
|
|
|
|
call.Run(func(args mock.Arguments) {
|
|
|
|
elems := args.Get(1).([]rpc.BatchElem)
|
|
|
|
for i := range elems {
|
|
|
|
receiptWrapper, ok := elems[i].Result.(*nullableReceipt)
|
|
|
|
require.True(t, ok)
|
|
|
|
require.NotNil(t, receiptWrapper)
|
|
|
|
// Simulate parsing of eth_getTransactionReceipt response
|
2024-04-18 16:48:02 +00:00
|
|
|
for _, localSum := range chainSummaries {
|
|
|
|
// to satisfy gosec: C601 checks
|
|
|
|
sum := localSum
|
2024-02-09 01:55:33 +00:00
|
|
|
tx := &sum.tx
|
|
|
|
if tx.Hash == elems[i].Args[0].(eth.Hash) {
|
|
|
|
if !sum.summary.DontConfirm {
|
|
|
|
status := types.ReceiptStatusSuccessful
|
|
|
|
if sum.summary.failStatus {
|
|
|
|
status = types.ReceiptStatusFailed
|
|
|
|
}
|
|
|
|
|
|
|
|
receiptWrapper.Receipt = &types.Receipt{
|
|
|
|
BlockNumber: new(big.Int).SetUint64(1),
|
|
|
|
Status: status,
|
|
|
|
}
|
|
|
|
}
|
2024-01-26 04:31:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-02-09 01:55:33 +00:00
|
|
|
return genTxs
|
2024-01-08 21:24:30 +00:00
|
|
|
}
|