package transactions import ( "context" "fmt" "math/big" "sync" "testing" eth "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" "github.com/status-im/status-go/rpc/chain" "github.com/status-im/status-go/rpc/chain/ethclient" mock_client "github.com/status-im/status-go/rpc/chain/mock/client" "github.com/status-im/status-go/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/common" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) type MockETHClient struct { mock.Mock } var _ ethclient.BatchCallClient = (*MockETHClient)(nil) 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 mock_client.MockClientInterface Clients map[common.ChainID]*MockETHClient mu sync.RWMutex } var _ chain.ClientInterface = (*MockChainClient)(nil) func NewMockChainClient() *MockChainClient { return &MockChainClient{ Clients: make(map[common.ChainID]*MockETHClient), } } func (m *MockChainClient) SetAvailableClients(chainIDs []common.ChainID) *MockChainClient { m.mu.Lock() defer m.mu.Unlock() for _, chainID := range chainIDs { if _, ok := m.Clients[chainID]; !ok { m.Clients[chainID] = new(MockETHClient) } } return m } func (m *MockChainClient) AbstractEthClient(chainID common.ChainID) (ethclient.BatchCallClient, error) { m.mu.RLock() defer m.mu.RUnlock() if _, ok := m.Clients[chainID]; !ok { panic(fmt.Sprintf("no mock client for chainID %d", chainID)) } return m.Clients[chainID], nil } func GenerateTestPendingTransactions(start int, count int) []PendingTransaction { if count > 127 { panic("can't generate more than 127 distinct transactions") } txs := make([]PendingTransaction, count) for i := start; i < count; i++ { txs[i] = PendingTransaction{ Hash: eth.HexToHash(fmt.Sprintf("0x1%d", i)), From: eth.HexToAddress(fmt.Sprintf("0x2%d", i)), To: eth.HexToAddress(fmt.Sprintf("0x3%d", i)), Type: RegisterENS, AdditionalData: "someuser.stateofus.eth", Value: bigint.BigInt{Int: big.NewInt(int64(i))}, GasLimit: bigint.BigInt{Int: big.NewInt(21000)}, GasPrice: bigint.BigInt{Int: big.NewInt(int64(i))}, ChainID: 777, Status: new(TxStatus), AutoDelete: new(bool), Symbol: "ETH", Timestamp: uint64(i), } *txs[i].Status = Pending // set to pending by default *txs[i].AutoDelete = true // set to true by default } return txs } // 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 } type TestTxSummary struct { failStatus bool DontConfirm bool // Timestamp will be used to mock the Timestamp if greater than 0 Timestamp int } type summaryTxPair struct { summary TestTxSummary tx PendingTransaction answered bool } func MockTestTransactions(t *testing.T, chainClient *MockChainClient, testTxs []TestTxSummary) []PendingTransaction { genTxs := GenerateTestPendingTransactions(0, len(testTxs)) for i, tx := range testTxs { if tx.Timestamp > 0 { genTxs[i].Timestamp = uint64(tx.Timestamp) } } grouped := groupSliceInMap(genTxs, func(tx PendingTransaction, i int) (common.ChainID, summaryTxPair) { return tx.ChainID, summaryTxPair{ summary: testTxs[i], tx: tx, } }) chains := keysInMap(grouped) chainClient.SetAvailableClients(chains) 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] call := cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool { if len(b) > len(chainSummaries) { return false } for i := range b { for _, localSummary := range chainSummaries { // to satisfy gosec: C601 checks sum := localSummary 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 } } } return false })).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 for _, localSum := range chainSummaries { // to satisfy gosec: C601 checks sum := localSum 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, } } } } } }) } return genTxs }