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:
parent
e088e1b3bd
commit
3c4fcaa2ed
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue