feat(wallet): propagate status with the pending tx status changed

Replace usage of `eth_getTransactionByHash` with `eth_getTransactionReceipt`
when polling for changes. `eth_getTransactionReceipt` delivers also the
status of the transaction.

Update tests to account for the new changes
Propagate status with the update event

Refactoring of the `pendingtxtracker.go` file to emit notifications with
a new payload structure that includes transaction identity and deletion status.

Closes status-desktop [#12120](https://github.com/status-im/status-desktop/issues/13124)

ghstack-source-id: 936bff5a41
Pull Request resolved: https://github.com/status-im/status-go/pull/4523
This commit is contained in:
Stefan 2024-01-08 16:24:30 -05:00 committed by Stefan Dunca
parent e088e1b3bd
commit 3c4fcaa2ed
3 changed files with 234 additions and 82 deletions

View File

@ -11,6 +11,7 @@ import (
"time"
eth "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p"
@ -24,12 +25,14 @@ import (
)
const (
// EventPendingTransactionUpdate is emitted when a pending transaction is updated (added or deleted). Carries StatusChangedPayload in message
// EventPendingTransactionUpdate is emitted when a pending transaction is updated (added or deleted). Carries PendingTxUpdatePayload in message
EventPendingTransactionUpdate walletevent.EventType = "pending-transaction-update"
// EventPendingTransactionStatusChanged carries StatusChangedPayload in message
EventPendingTransactionStatusChanged walletevent.EventType = "pending-transaction-status-changed"
PendingCheckInterval = 10 * time.Second
GetTransactionReceiptRPCName = "eth_getTransactionReceipt"
)
var (
@ -41,7 +44,8 @@ type TxStatus = string
// Values for status column in pending_transactions
const (
Pending TxStatus = "Pending"
Done TxStatus = "Done"
Success TxStatus = "Success"
Failed TxStatus = "Failed"
)
type AutoDeleteType = bool
@ -51,10 +55,19 @@ const (
Keep AutoDeleteType = false
)
type StatusChangedPayload struct {
type TxIdentity struct {
ChainID common.ChainID `json:"chainId"`
Hash eth.Hash `json:"hash"`
Status *TxStatus `json:"status,omitempty"`
}
type PendingTxUpdatePayload struct {
TxIdentity
Deleted bool `json:"deleted"`
}
type StatusChangedPayload struct {
TxIdentity
Status TxStatus `json:"status"`
}
type PendingTxTracker struct {
@ -83,7 +96,6 @@ func NewPendingTxTracker(db *sql.DB, rpcClient rpc.ClientInterface, rpcFilter *r
}
type txStatusRes struct {
// TODO - 11861: propagate real status
Status TxStatus
hash eth.Hash
}
@ -139,6 +151,18 @@ func (tm *PendingTxTracker) fetchAndUpdateDB(ctx context.Context) bool {
return res
}
type nullableReceipt struct {
*types.Receipt
}
func (nr *nullableReceipt) UnmarshalJSON(data []byte) error {
transactionNotAvailable := (string(data) == "null")
if transactionNotAvailable {
return nil
}
return json.Unmarshal(data, &nr.Receipt)
}
// fetchBatchTxStatus returns not pending transactions (confirmed or errored)
// it excludes the still pending or errored request from the result
func fetchBatchTxStatus(ctx context.Context, rpcClient rpc.ClientInterface, chainID common.ChainID, hashes []eth.Hash, log log.Logger) ([]txStatusRes, error) {
@ -153,11 +177,10 @@ func fetchBatchTxStatus(ctx context.Context, rpcClient rpc.ClientInterface, chai
batch := make([]ethrpc.BatchElem, 0, len(hashes))
for _, hash := range hashes {
jsonRes := make(map[string]interface{})
batch = append(batch, ethrpc.BatchElem{
Method: "eth_getTransactionByHash",
Method: GetTransactionReceiptRPCName,
Args: []interface{}{hash},
Result: &jsonRes,
Result: new(nullableReceipt),
})
}
@ -169,23 +192,40 @@ func fetchBatchTxStatus(ctx context.Context, rpcClient rpc.ClientInterface, chai
res := make([]txStatusRes, 0, len(batch))
for i, b := range batch {
isPending := true
err := b.Error
if err != nil {
log.Error("Failed to get transaction", "error", err, "hash", hashes[i])
continue
} else {
jsonRes := *(b.Result.(*map[string]interface{}))
if jsonRes != nil {
if blNo, ok := jsonRes["blockNumber"]; ok {
isPending = blNo == nil
}
}
}
if b.Result == nil {
log.Error("Transaction not found", "hash", hashes[i])
continue
}
receiptWrapper, ok := b.Result.(*nullableReceipt)
if !ok {
log.Error("Failed to cast transaction receipt", "hash", hashes[i])
continue
}
if receiptWrapper == nil || receiptWrapper.Receipt == nil {
// the transaction is not available yet
continue
}
receipt := receiptWrapper.Receipt
isPending := receipt != nil && receipt.BlockNumber == nil
if !isPending {
var status TxStatus
if receipt.Status == types.ReceiptStatusSuccessful {
status = Success
} else {
status = Failed
}
res = append(res, txStatusRes{
hash: hashes[i],
hash: hashes[i],
Status: status,
})
}
}
@ -193,8 +233,8 @@ func fetchBatchTxStatus(ctx context.Context, rpcClient rpc.ClientInterface, chai
}
// updateDBStatus returns entries that were updated only
func (tm *PendingTxTracker) updateDBStatus(ctx context.Context, chainID common.ChainID, statuses []txStatusRes) ([]eth.Hash, error) {
res := make([]eth.Hash, 0, len(statuses))
func (tm *PendingTxTracker) updateDBStatus(ctx context.Context, chainID common.ChainID, statuses []txStatusRes) ([]txStatusRes, error) {
res := make([]txStatusRes, 0, len(statuses))
tx, err := tm.db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
@ -241,8 +281,7 @@ func (tm *PendingTxTracker) updateDBStatus(ctx context.Context, chainID common.C
notifyFunctions = append(notifyFunctions, notifyFn)
} else {
// If the entry was not deleted, update the status
// TODO - #11861: fix status - `br.status`
txStatus := Done
txStatus := br.Status
res, err := updateStmt.ExecContext(ctx, txStatus, chainID, br.hash)
if err != nil {
@ -261,7 +300,7 @@ func (tm *PendingTxTracker) updateDBStatus(ctx context.Context, chainID common.C
}
}
res = append(res, br.hash)
res = append(res, br)
}
err = tx.Commit()
@ -276,18 +315,20 @@ func (tm *PendingTxTracker) updateDBStatus(ctx context.Context, chainID common.C
return res, nil
}
func (tm *PendingTxTracker) emitNotifications(chainID common.ChainID, changes []eth.Hash) {
func (tm *PendingTxTracker) emitNotifications(chainID common.ChainID, changes []txStatusRes) {
if tm.eventFeed != nil {
for _, hash := range changes {
status := StatusChangedPayload{
ChainID: chainID,
Hash: hash,
// TODO - #11861: status
for _, change := range changes {
payload := StatusChangedPayload{
TxIdentity: TxIdentity{
ChainID: chainID,
Hash: change.hash,
},
Status: change.Status,
}
jsonPayload, err := json.Marshal(status)
jsonPayload, err := json.Marshal(payload)
if err != nil {
tm.log.Error("Failed to marshal pending transaction status", "error", err, "hash", hash)
tm.log.Error("Failed to marshal pending transaction status", "error", err, "hash", change.hash)
continue
}
tm.eventFeed.Send(walletevent.Event{
@ -515,7 +556,13 @@ func (tm *PendingTxTracker) addPending(transaction *PendingTransaction) error {
)
// Notify listeners of new pending transaction (used in activity history)
if err == nil {
tm.notifyPendingTransactionListeners(transaction.ChainID, []eth.Address{transaction.From, transaction.To}, transaction.Timestamp)
tm.notifyPendingTransactionListeners(PendingTxUpdatePayload{
TxIdentity: TxIdentity{
ChainID: transaction.ChainID,
Hash: transaction.Hash,
},
Deleted: false,
}, []eth.Address{transaction.From, transaction.To}, transaction.Timestamp)
}
if tm.rpcFilter != nil {
tm.rpcFilter.TriggerTransactionSentToUpstreamEvent(&rpcfilters.PendingTxInfo{
@ -528,13 +575,20 @@ func (tm *PendingTxTracker) addPending(transaction *PendingTransaction) error {
return err
}
func (tm *PendingTxTracker) notifyPendingTransactionListeners(chainID common.ChainID, addresses []eth.Address, timestamp uint64) {
func (tm *PendingTxTracker) notifyPendingTransactionListeners(payload PendingTxUpdatePayload, addresses []eth.Address, timestamp uint64) {
jsonPayload, err := json.Marshal(payload)
if err != nil {
tm.log.Error("Failed to marshal PendingTxUpdatePayload", "error", err, "hash", payload.Hash)
return
}
if tm.eventFeed != nil {
tm.eventFeed.Send(walletevent.Event{
Type: EventPendingTransactionUpdate,
ChainID: uint64(chainID),
ChainID: uint64(payload.ChainID),
Accounts: addresses,
At: int64(timestamp),
Message: string(jsonPayload),
})
}
}
@ -559,7 +613,13 @@ func (tm *PendingTxTracker) DeleteBySQLTx(tx *sql.Tx, chainID common.ChainID, ha
err = ErrStillPending
}
return func() {
tm.notifyPendingTransactionListeners(chainID, []eth.Address{from, to}, timestamp)
tm.notifyPendingTransactionListeners(PendingTxUpdatePayload{
TxIdentity: TxIdentity{
ChainID: chainID,
Hash: hash,
},
Deleted: true,
}, []eth.Address{from, to}, timestamp)
}, err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"encoding/json"
"math/big"
"sync"
"testing"
"time"
@ -12,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
eth "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/rpc"
@ -43,22 +45,11 @@ func waitForTaskToStop(pt *PendingTxTracker) {
}
}
func TestPendingTxTracker_ValidateConfirmed(t *testing.T) {
func TestPendingTxTracker_ValidateConfirmedWithSuccessStatus(t *testing.T) {
m, stop, chainClient, eventFeed := setupTestTransactionDB(t, nil)
defer stop()
txs := GenerateTestPendingTransactions(1)
// Mock the first call to getTransactionByHash
chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID})
cl := chainClient.Clients[txs[0].ChainID]
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return len(b) == 1 && b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[0].Hash
})).Return(nil).Once().Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
res := elems[0].Result.(*map[string]interface{})
(*res)["blockNumber"] = TransactionBlockNo
})
txs := MockTestTransactions(t, chainClient, []TestTxSummary{{}})
eventChan := make(chan walletevent.Event, 3)
sub := eventFeed.Subscribe(eventChan)
@ -78,7 +69,51 @@ func TestPendingTxTracker_ValidateConfirmed(t *testing.T) {
err = json.Unmarshal([]byte(we.Message), &p)
require.NoError(t, err)
require.Equal(t, txs[0].Hash, p.Hash)
require.Nil(t, p.Status)
require.Equal(t, Success, p.Status)
}
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for event")
}
}
// Wait for the answer to be processed
err = m.Stop()
require.NoError(t, err)
waitForTaskToStop(m)
res, err := m.GetAllPending()
require.NoError(t, err)
require.Equal(t, 0, len(res))
sub.Unsubscribe()
}
func TestPendingTxTracker_ValidateConfirmedWithFailedStatus(t *testing.T) {
m, stop, chainClient, eventFeed := setupTestTransactionDB(t, nil)
defer stop()
txs := MockTestTransactions(t, chainClient, []TestTxSummary{{failStatus: true}})
eventChan := make(chan walletevent.Event, 3)
sub := eventFeed.Subscribe(eventChan)
err := m.StoreAndTrackPendingTx(&txs[0])
require.NoError(t, err)
for i := 0; i < 3; i++ {
select {
case we := <-eventChan:
if i == 0 || i == 1 {
// Check add and delete
require.Equal(t, EventPendingTransactionUpdate, we.Type)
} else {
require.Equal(t, EventPendingTransactionStatusChanged, we.Type)
var p StatusChangedPayload
err = json.Unmarshal([]byte(we.Message), &p)
require.NoError(t, err)
require.Equal(t, txs[0].Hash, p.Hash)
require.Equal(t, Failed, p.Status)
}
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for event")
@ -108,14 +143,18 @@ func TestPendingTxTracker_InterruptWatching(t *testing.T) {
chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID})
cl := chainClient.Clients[txs[0].ChainID]
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return (len(b) == 2 && b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[0].Hash && b[1].Method == TransactionByHashRPCName && b[1].Args[0] == txs[1].Hash)
return (len(b) == 2 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash && b[1].Method == GetTransactionReceiptRPCName && b[1].Args[0] == txs[1].Hash)
})).Return(nil).Once().Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
// Simulate still pending by excluding "blockNumber" in elems[0]
// Simulate still pending due to "null" return from eth_getTransactionReceipt
elems[0].Result.(*nullableReceipt).Receipt = nil
res := elems[1].Result.(*map[string]interface{})
(*res)["blockNumber"] = TransactionBlockNo
// Simulate parsing of eth_getTransactionReceipt response
elems[1].Result.(*nullableReceipt).Receipt = &types.Receipt{
BlockNumber: new(big.Int).SetUint64(1),
Status: 1,
}
})
eventChan := make(chan walletevent.Event, 2)
@ -151,7 +190,7 @@ func TestPendingTxTracker_InterruptWatching(t *testing.T) {
require.NoError(t, err)
require.Equal(t, txs[1].Hash, p.Hash)
require.Equal(t, txs[1].ChainID, p.ChainID)
require.Nil(t, p.Status)
require.Equal(t, Success, p.Status)
}
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for event")
@ -171,11 +210,14 @@ func TestPendingTxTracker_InterruptWatching(t *testing.T) {
// Restart the tracker to process leftovers
//
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return (len(b) == 1 && b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[0].Hash)
return (len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash)
})).Return(nil).Once().Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
res := elems[0].Result.(*map[string]interface{})
(*res)["blockNumber"] = TransactionBlockNo
// Simulate parsing of eth_getTransactionReceipt response
elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{
BlockNumber: new(big.Int).SetUint64(1),
Status: 1,
}
})
err = m.Start()
@ -193,7 +235,7 @@ func TestPendingTxTracker_InterruptWatching(t *testing.T) {
require.NoError(t, err)
require.Equal(t, txs[0].ChainID, p.ChainID)
require.Equal(t, txs[0].Hash, p.Hash)
require.Nil(t, p.Status)
require.Equal(t, Success, p.Status)
}
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for event")
@ -223,19 +265,25 @@ func TestPendingTxTracker_MultipleClients(t *testing.T) {
chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID, txs[1].ChainID})
cl := chainClient.Clients[txs[0].ChainID]
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return (len(b) == 1 && b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[0].Hash)
return (len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash)
})).Return(nil).Once().Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
res := elems[0].Result.(*map[string]interface{})
(*res)["blockNumber"] = TransactionBlockNo
// Simulate parsing of eth_getTransactionReceipt response
elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{
BlockNumber: new(big.Int).SetUint64(1),
Status: 1,
}
})
cl = chainClient.Clients[txs[1].ChainID]
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return (len(b) == 1 && b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[1].Hash)
return (len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[1].Hash)
})).Return(nil).Once().Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
res := elems[0].Result.(*map[string]interface{})
(*res)["blockNumber"] = TransactionBlockNo
// Simulate parsing of eth_getTransactionReceipt response
elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{
BlockNumber: new(big.Int).SetUint64(1),
Status: 1,
}
})
eventChan := make(chan walletevent.Event, 6)
@ -261,7 +309,7 @@ func TestPendingTxTracker_MultipleClients(t *testing.T) {
var p StatusChangedPayload
err := json.Unmarshal([]byte(we.Message), &p)
require.NoError(t, err)
require.Nil(t, p.Status)
require.Equal(t, Success, p.Status)
}
}
@ -297,17 +345,20 @@ func TestPendingTxTracker_Watch(t *testing.T) {
txs := GenerateTestPendingTransactions(2)
// Make the second already confirmed
*txs[0].Status = Done
*txs[0].Status = Success
// Mock the first call to getTransactionByHash
chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID})
cl := chainClient.Clients[txs[0].ChainID]
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return len(b) == 1 && b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[1].Hash
return len(b) == 1 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[1].Hash
})).Return(nil).Once().Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
res := elems[0].Result.(*map[string]interface{})
(*res)["blockNumber"] = TransactionBlockNo
// Simulate parsing of eth_getTransactionReceipt response
elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{
BlockNumber: new(big.Int).SetUint64(1),
Status: 1,
}
})
eventChan := make(chan walletevent.Event, 3)
@ -335,7 +386,7 @@ func TestPendingTxTracker_Watch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, txs[1].ChainID, p.ChainID)
require.Equal(t, txs[1].Hash, p.Hash)
require.Nil(t, p.Status)
require.Equal(t, Success, p.Status)
}
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for the status update event")
@ -386,23 +437,26 @@ func TestPendingTxTracker_Watch_StatusChangeIncrementally(t *testing.T) {
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
if len(cl.Calls) == 0 {
res := len(b) > 0 && b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[0].Hash
res := len(b) > 0 && b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[0].Hash
// If the first processing call picked up the second validate this case also
if len(b) == 2 {
res = res && b[1].Method == TransactionByHashRPCName && b[1].Args[0] == txs[1].Hash
res = res && b[1].Method == GetTransactionReceiptRPCName && b[1].Args[0] == txs[1].Hash
}
return res
}
// Second call we expect only one left
return len(b) == 1 && (b[0].Method == TransactionByHashRPCName && b[0].Args[0] == txs[1].Hash)
return len(b) == 1 && (b[0].Method == GetTransactionReceiptRPCName && b[0].Args[0] == txs[1].Hash)
})).Return(nil).Twice().Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
if len(cl.Calls) == 2 {
firsDoneWG.Wait()
}
// Only first item is processed, second is left pending
res := elems[0].Result.(*map[string]interface{})
(*res)["blockNumber"] = TransactionBlockNo
// Simulate parsing of eth_getTransactionReceipt response
elems[0].Result.(*nullableReceipt).Receipt = &types.Receipt{
BlockNumber: new(big.Int).SetUint64(1),
Status: 1,
}
})
eventChan := make(chan walletevent.Event, 6)
@ -425,11 +479,11 @@ func TestPendingTxTracker_Watch_StatusChangeIncrementally(t *testing.T) {
if statusEventCount == 0 {
require.Equal(t, txs[0].ChainID, p.ChainID)
require.Equal(t, txs[0].Hash, p.Hash)
require.Nil(t, p.Status)
require.Equal(t, Success, p.Status)
status, err := m.Watch(context.Background(), txs[0].ChainID, txs[0].Hash)
require.NoError(t, err)
require.Equal(t, Done, *status)
require.Equal(t, Success, *status)
err = m.Delete(context.Background(), txs[0].ChainID, txs[0].Hash)
require.NoError(t, err)
@ -443,7 +497,7 @@ func TestPendingTxTracker_Watch_StatusChangeIncrementally(t *testing.T) {
status, err := m.Watch(context.Background(), txs[1].ChainID, txs[1].Hash)
require.NoError(t, err)
require.Equal(t, Done, *status)
require.Equal(t, Success, *status)
err = m.Delete(context.Background(), txs[1].ChainID, txs[1].Hash)
require.NoError(t, err)
}

View File

@ -4,14 +4,17 @@ import (
"context"
"fmt"
"math/big"
"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/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 {
@ -23,11 +26,6 @@ func (m *MockETHClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem)
return args.Error(0)
}
const (
TransactionBlockNo = "0x1"
TransactionByHashRPCName = "eth_getTransactionByHash"
)
type MockChainClient struct {
mock.Mock
@ -81,3 +79,43 @@ func GenerateTestPendingTransactions(count int) []PendingTransaction {
}
return txs
}
type TestTxSummary struct {
failStatus bool
dontConfirm bool
}
func MockTestTransactions(t *testing.T, chainClient *MockChainClient, testTxs []TestTxSummary) []PendingTransaction {
txs := GenerateTestPendingTransactions(len(testTxs))
// Mock the first call to getTransactionByHash
chainClient.SetAvailableClients([]common.ChainID{txs[0].ChainID})
cl := chainClient.Clients[txs[0].ChainID]
cl.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
ok := len(b) == len(testTxs)
for i := range b {
ok = ok && b[i].Method == GetTransactionReceiptRPCName && b[i].Args[0] == txs[0].Hash
}
return ok
})).Return(nil).Once().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
if !testTxs[i].dontConfirm {
status := types.ReceiptStatusSuccessful
if testTxs[i].failStatus {
status = types.ReceiptStatusFailed
}
receiptWrapper.Receipt = &types.Receipt{
BlockNumber: new(big.Int).SetUint64(1),
Status: status,
}
}
}
})
return txs
}