2023-08-01 18:50:30 +00:00
package transactions
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math/big"
"strings"
"time"
2024-10-28 20:54:17 +00:00
"go.uber.org/zap"
2023-08-01 18:50:30 +00:00
eth "github.com/ethereum/go-ethereum/common"
2024-01-08 21:24:30 +00:00
"github.com/ethereum/go-ethereum/core/types"
2023-08-01 18:50:30 +00:00
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/p2p"
ethrpc "github.com/ethereum/go-ethereum/rpc"
2024-10-28 20:54:17 +00:00
"github.com/status-im/status-go/logutils"
2023-08-01 18:50:30 +00:00
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/rpcfilters"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/services/wallet/common"
2024-03-12 09:15:30 +00:00
wallet_common "github.com/status-im/status-go/services/wallet/common"
2023-08-01 18:50:30 +00:00
"github.com/status-im/status-go/services/wallet/walletevent"
)
const (
2024-01-08 21:24:30 +00:00
// EventPendingTransactionUpdate is emitted when a pending transaction is updated (added or deleted). Carries PendingTxUpdatePayload in message
2023-08-01 18:50:30 +00:00
EventPendingTransactionUpdate walletevent . EventType = "pending-transaction-update"
2024-01-08 18:31:33 +00:00
// EventPendingTransactionStatusChanged carries StatusChangedPayload in message
2023-08-01 18:50:30 +00:00
EventPendingTransactionStatusChanged walletevent . EventType = "pending-transaction-status-changed"
2023-08-30 16:14:57 +00:00
PendingCheckInterval = 10 * time . Second
2024-01-08 21:24:30 +00:00
GetTransactionReceiptRPCName = "eth_getTransactionReceipt"
2023-08-01 18:50:30 +00:00
)
var (
ErrStillPending = errors . New ( "transaction is still pending" )
)
type TxStatus = string
// Values for status column in pending_transactions
const (
Pending TxStatus = "Pending"
2024-01-08 21:24:30 +00:00
Success TxStatus = "Success"
Failed TxStatus = "Failed"
2023-08-01 18:50:30 +00:00
)
type AutoDeleteType = bool
const (
AutoDelete AutoDeleteType = true
Keep AutoDeleteType = false
)
2024-01-08 21:24:30 +00:00
type TxIdentity struct {
2023-08-01 18:50:30 +00:00
ChainID common . ChainID ` json:"chainId" `
Hash eth . Hash ` json:"hash" `
2024-01-08 21:24:30 +00:00
}
type PendingTxUpdatePayload struct {
TxIdentity
Deleted bool ` json:"deleted" `
}
type StatusChangedPayload struct {
TxIdentity
Status TxStatus ` json:"status" `
2023-08-01 18:50:30 +00:00
}
2024-01-08 21:24:30 +00:00
// PendingTxTracker implements StatusService in common/status_node_service.go
2023-08-01 18:50:30 +00:00
type PendingTxTracker struct {
db * sql . DB
rpcClient rpc . ClientInterface
rpcFilter * rpcfilters . Service
eventFeed * event . Feed
taskRunner * ConditionalRepeater
2024-10-28 20:54:17 +00:00
logger * zap . Logger
2023-08-01 18:50:30 +00:00
}
2023-08-30 16:14:57 +00:00
func NewPendingTxTracker ( db * sql . DB , rpcClient rpc . ClientInterface , rpcFilter * rpcfilters . Service , eventFeed * event . Feed , checkInterval time . Duration ) * PendingTxTracker {
2023-08-01 18:50:30 +00:00
tm := & PendingTxTracker {
db : db ,
rpcClient : rpcClient ,
eventFeed : eventFeed ,
rpcFilter : rpcFilter ,
2024-10-28 20:54:17 +00:00
logger : logutils . ZapLogger ( ) . Named ( "PendingTxTracker" ) ,
2023-08-01 18:50:30 +00:00
}
2023-08-30 16:14:57 +00:00
tm . taskRunner = NewConditionalRepeater ( checkInterval , func ( ctx context . Context ) bool {
return tm . fetchAndUpdateDB ( ctx )
2023-08-01 18:50:30 +00:00
} )
return tm
}
type txStatusRes struct {
Status TxStatus
hash eth . Hash
}
2023-08-30 16:14:57 +00:00
func ( tm * PendingTxTracker ) fetchAndUpdateDB ( ctx context . Context ) bool {
res := WorkNotDone
2023-08-01 18:50:30 +00:00
txs , err := tm . GetAllPending ( )
if err != nil {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to get pending transactions" , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
return WorkDone
}
2024-10-28 20:54:17 +00:00
tm . logger . Debug ( "Checking for PT status" , zap . Int ( "count" , len ( txs ) ) )
2023-08-01 18:50:30 +00:00
txsMap := make ( map [ common . ChainID ] [ ] eth . Hash )
for _ , tx := range txs {
chainID := tx . ChainID
txsMap [ chainID ] = append ( txsMap [ chainID ] , tx . Hash )
}
2023-08-30 16:14:57 +00:00
doneCount := 0
2023-08-01 18:50:30 +00:00
// Batch request for each chain
for chainID , txs := range txsMap {
2024-10-28 20:54:17 +00:00
tm . logger . Debug ( "Processing PTs" , zap . Stringer ( "chainID" , chainID ) , zap . Int ( "count" , len ( txs ) ) )
batchRes , err := fetchBatchTxStatus ( ctx , tm . rpcClient , chainID , txs , tm . logger )
2023-08-01 18:50:30 +00:00
if err != nil {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to batch fetch pending transactions status for" , zap . Stringer ( "chainID" , chainID ) , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
continue
}
2023-08-30 16:14:57 +00:00
if len ( batchRes ) == 0 {
2024-10-28 20:54:17 +00:00
tm . logger . Debug ( "No change to PTs status" , zap . Stringer ( "chainID" , chainID ) )
2023-08-01 18:50:30 +00:00
continue
}
2024-10-28 20:54:17 +00:00
tm . logger . Debug ( "PTs done" , zap . Stringer ( "chainID" , chainID ) , zap . Int ( "count" , len ( batchRes ) ) )
2023-08-30 16:14:57 +00:00
doneCount += len ( batchRes )
2023-08-01 18:50:30 +00:00
2023-08-30 16:14:57 +00:00
updateRes , err := tm . updateDBStatus ( ctx , chainID , batchRes )
if err != nil {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to update pending transactions status for" , zap . Stringer ( "chainID" , chainID ) , zap . Error ( err ) )
2023-08-30 16:14:57 +00:00
continue
2023-08-01 18:50:30 +00:00
}
2024-10-28 20:54:17 +00:00
tm . logger . Debug ( "Emit notifications for PTs" , zap . Stringer ( "chainID" , chainID ) , zap . Int ( "count" , len ( updateRes ) ) )
2023-08-01 18:50:30 +00:00
tm . emitNotifications ( chainID , updateRes )
}
2023-08-30 16:14:57 +00:00
if len ( txs ) == doneCount {
res = WorkDone
}
2024-10-28 20:54:17 +00:00
tm . logger . Debug ( "Done PTs iteration" , zap . Int ( "count" , doneCount ) , zap . Bool ( "completed" , res ) )
2023-08-30 16:14:57 +00:00
2023-08-01 18:50:30 +00:00
return res
}
2024-01-08 21:24:30 +00:00
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 )
}
2023-08-30 16:14:57 +00:00
// fetchBatchTxStatus returns not pending transactions (confirmed or errored)
// it excludes the still pending or errored request from the result
2024-10-28 20:54:17 +00:00
func fetchBatchTxStatus ( ctx context . Context , rpcClient rpc . ClientInterface , chainID common . ChainID , hashes [ ] eth . Hash , logger * zap . Logger ) ( [ ] txStatusRes , error ) {
2023-08-01 18:50:30 +00:00
chainClient , err := rpcClient . AbstractEthClient ( chainID )
if err != nil {
2024-10-28 20:54:17 +00:00
logger . Error ( "Failed to get chain client" , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
return nil , err
}
reqCtx , cancel := context . WithTimeout ( ctx , 10 * time . Second )
defer cancel ( )
batch := make ( [ ] ethrpc . BatchElem , 0 , len ( hashes ) )
for _ , hash := range hashes {
batch = append ( batch , ethrpc . BatchElem {
2024-01-08 21:24:30 +00:00
Method : GetTransactionReceiptRPCName ,
2023-08-01 18:50:30 +00:00
Args : [ ] interface { } { hash } ,
2024-01-08 21:24:30 +00:00
Result : new ( nullableReceipt ) ,
2023-08-01 18:50:30 +00:00
} )
}
err = chainClient . BatchCallContext ( reqCtx , batch )
if err != nil {
2024-10-28 20:54:17 +00:00
logger . Error ( "Transactions request fail" , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
return nil , err
}
res := make ( [ ] txStatusRes , 0 , len ( batch ) )
for i , b := range batch {
err := b . Error
if err != nil {
2024-10-28 20:54:17 +00:00
logger . Error ( "Failed to get transaction" , zap . Stringer ( "hash" , hashes [ i ] ) , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
continue
}
2024-01-08 21:24:30 +00:00
if b . Result == nil {
2024-10-28 20:54:17 +00:00
logger . Error ( "Transaction not found" , zap . Stringer ( "hash" , hashes [ i ] ) )
2024-01-08 21:24:30 +00:00
continue
}
receiptWrapper , ok := b . Result . ( * nullableReceipt )
if ! ok {
2024-10-28 20:54:17 +00:00
logger . Error ( "Failed to cast transaction receipt" , zap . Stringer ( "hash" , hashes [ i ] ) )
2024-01-08 21:24:30 +00:00
continue
}
if receiptWrapper == nil || receiptWrapper . Receipt == nil {
// the transaction is not available yet
continue
}
receipt := receiptWrapper . Receipt
isPending := receipt != nil && receipt . BlockNumber == nil
2023-08-01 18:50:30 +00:00
if ! isPending {
2024-01-08 21:24:30 +00:00
var status TxStatus
if receipt . Status == types . ReceiptStatusSuccessful {
status = Success
} else {
status = Failed
}
2023-08-01 18:50:30 +00:00
res = append ( res , txStatusRes {
2024-01-08 21:24:30 +00:00
hash : hashes [ i ] ,
Status : status ,
2023-08-01 18:50:30 +00:00
} )
}
}
return res , nil
}
// updateDBStatus returns entries that were updated only
2024-01-08 21:24:30 +00:00
func ( tm * PendingTxTracker ) updateDBStatus ( ctx context . Context , chainID common . ChainID , statuses [ ] txStatusRes ) ( [ ] txStatusRes , error ) {
res := make ( [ ] txStatusRes , 0 , len ( statuses ) )
2023-08-01 18:50:30 +00:00
tx , err := tm . db . BeginTx ( ctx , nil )
if err != nil {
return nil , fmt . Errorf ( "failed to begin transaction: %w" , err )
}
updateStmt , err := tx . PrepareContext ( ctx , ` UPDATE pending_transactions SET status = ? WHERE network_id = ? AND hash = ? ` )
if err != nil {
rollErr := tx . Rollback ( )
if rollErr != nil {
err = fmt . Errorf ( "failed to rollback transaction due to: %w" , err )
}
return nil , fmt . Errorf ( "failed to prepare update statement: %w" , err )
}
checkAutoDelStmt , err := tx . PrepareContext ( ctx , ` SELECT auto_delete FROM pending_transactions WHERE network_id = ? AND hash = ? ` )
if err != nil {
rollErr := tx . Rollback ( )
if rollErr != nil {
err = fmt . Errorf ( "failed to rollback transaction: %w" , err )
}
return nil , fmt . Errorf ( "failed to prepare auto delete statement: %w" , err )
}
notifyFunctions := make ( [ ] func ( ) , 0 , len ( statuses ) )
for _ , br := range statuses {
row := checkAutoDelStmt . QueryRowContext ( ctx , chainID , br . hash )
var autoDel bool
err = row . Scan ( & autoDel )
if err != nil {
if err == sql . ErrNoRows {
2024-10-28 20:54:17 +00:00
tm . logger . Warn ( "Missing entry while checking for auto_delete" , zap . Stringer ( "hash" , br . hash ) )
2023-08-01 18:50:30 +00:00
} else {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to retrieve auto_delete for pending transaction" , zap . Stringer ( "hash" , br . hash ) , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
}
continue
}
if autoDel {
notifyFn , err := tm . DeleteBySQLTx ( tx , chainID , br . hash )
if err != nil && err != ErrStillPending {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to delete pending transaction" , zap . Stringer ( "hash" , br . hash ) , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
continue
}
notifyFunctions = append ( notifyFunctions , notifyFn )
} else {
// If the entry was not deleted, update the status
2024-01-08 21:24:30 +00:00
txStatus := br . Status
2023-08-01 18:50:30 +00:00
res , err := updateStmt . ExecContext ( ctx , txStatus , chainID , br . hash )
if err != nil {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to update pending transaction status" , zap . Stringer ( "hash" , br . hash ) , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
continue
}
affected , err := res . RowsAffected ( )
if err != nil {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to get updated rows" , zap . Stringer ( "hash" , br . hash ) , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
continue
}
if affected == 0 {
2024-10-28 20:54:17 +00:00
tm . logger . Warn ( "Missing entry to update for" , zap . Stringer ( "hash" , br . hash ) )
2023-08-01 18:50:30 +00:00
continue
}
}
2024-01-08 21:24:30 +00:00
res = append ( res , br )
2023-08-01 18:50:30 +00:00
}
err = tx . Commit ( )
if err != nil {
return nil , fmt . Errorf ( "failed to commit transaction: %w" , err )
}
for _ , fn := range notifyFunctions {
fn ( )
}
return res , nil
}
2024-01-08 21:24:30 +00:00
func ( tm * PendingTxTracker ) emitNotifications ( chainID common . ChainID , changes [ ] txStatusRes ) {
2023-08-01 18:50:30 +00:00
if tm . eventFeed != nil {
2024-01-08 21:24:30 +00:00
for _ , change := range changes {
payload := StatusChangedPayload {
TxIdentity : TxIdentity {
ChainID : chainID ,
Hash : change . hash ,
} ,
Status : change . Status ,
2023-08-01 18:50:30 +00:00
}
2024-01-08 21:24:30 +00:00
jsonPayload , err := json . Marshal ( payload )
2023-08-01 18:50:30 +00:00
if err != nil {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to marshal pending transaction status" , zap . Stringer ( "hash" , change . hash ) , zap . Error ( err ) )
2023-08-01 18:50:30 +00:00
continue
}
tm . eventFeed . Send ( walletevent . Event {
Type : EventPendingTransactionStatusChanged ,
ChainID : uint64 ( chainID ) ,
Message : string ( jsonPayload ) ,
} )
}
}
}
// PendingTransaction called with autoDelete = false will keep the transaction in the database until it is confirmed by the caller using Delete
2024-03-14 08:39:06 +00:00
func ( tm * PendingTxTracker ) TrackPendingTransaction ( chainID common . ChainID , hash eth . Hash , from eth . Address , to eth . Address , trType PendingTrxType , autoDelete AutoDeleteType , additionalData string ) error {
2023-08-01 18:50:30 +00:00
err := tm . addPending ( & PendingTransaction {
2024-03-14 08:39:06 +00:00
ChainID : chainID ,
Hash : hash ,
From : from ,
To : to ,
Timestamp : uint64 ( time . Now ( ) . Unix ( ) ) ,
Type : trType ,
AutoDelete : & autoDelete ,
AdditionalData : additionalData ,
2023-08-01 18:50:30 +00:00
} )
if err != nil {
return err
}
tm . taskRunner . RunUntilDone ( )
return nil
}
func ( tm * PendingTxTracker ) Start ( ) error {
tm . taskRunner . RunUntilDone ( )
return nil
}
// APIs returns a list of new APIs.
func ( tm * PendingTxTracker ) APIs ( ) [ ] ethrpc . API {
return [ ] ethrpc . API {
{
Namespace : "pending" ,
Version : "0.1.0" ,
Service : tm ,
Public : true ,
} ,
}
}
// Protocols returns a new protocols list. In this case, there are none.
func ( tm * PendingTxTracker ) Protocols ( ) [ ] p2p . Protocol {
return [ ] p2p . Protocol { }
}
func ( tm * PendingTxTracker ) Stop ( ) error {
tm . taskRunner . Stop ( )
return nil
}
type PendingTrxType string
const (
RegisterENS PendingTrxType = "RegisterENS"
ReleaseENS PendingTrxType = "ReleaseENS"
SetPubKey PendingTrxType = "SetPubKey"
BuyStickerPack PendingTrxType = "BuyStickerPack"
WalletTransfer PendingTrxType = "WalletTransfer"
DeployCommunityToken PendingTrxType = "DeployCommunityToken"
AirdropCommunityToken PendingTrxType = "AirdropCommunityToken"
RemoteDestructCollectible PendingTrxType = "RemoteDestructCollectible"
BurnCommunityToken PendingTrxType = "BurnCommunityToken"
DeployOwnerToken PendingTrxType = "DeployOwnerToken"
2023-08-29 13:17:37 +00:00
SetSignerPublicKey PendingTrxType = "SetSignerPublicKey"
2023-12-01 16:42:08 +00:00
WalletConnectTransfer PendingTrxType = "WalletConnectTransfer"
2023-08-01 18:50:30 +00:00
)
type PendingTransaction struct {
2024-03-12 09:15:30 +00:00
Hash eth . Hash ` json:"hash" `
Timestamp uint64 ` json:"timestamp" `
Value bigint . BigInt ` json:"value" `
From eth . Address ` json:"from" `
To eth . Address ` json:"to" `
Data string ` json:"data" `
Symbol string ` json:"symbol" `
GasPrice bigint . BigInt ` json:"gasPrice" `
GasLimit bigint . BigInt ` json:"gasLimit" `
Type PendingTrxType ` json:"type" `
AdditionalData string ` json:"additionalData" `
ChainID common . ChainID ` json:"network_id" `
MultiTransactionID wallet_common . MultiTransactionIDType ` json:"multi_transaction_id" `
Nonce uint64 ` json:"nonce" `
2023-08-01 18:50:30 +00:00
// nil will insert the default value (Pending) in DB
Status * TxStatus ` json:"status,omitempty" `
// nil will insert the default value (true) in DB
AutoDelete * bool ` json:"autoDelete,omitempty" `
}
const selectFromPending = ` SELECT hash , timestamp , value , from_address , to_address , data ,
symbol , gas_price , gas_limit , type , additional_data ,
2024-03-12 09:15:30 +00:00
network_id , COALESCE ( multi_transaction_id , 0 ) , status , auto_delete , nonce
2023-08-01 18:50:30 +00:00
FROM pending_transactions
`
func rowsToTransactions ( rows * sql . Rows ) ( transactions [ ] * PendingTransaction , err error ) {
for rows . Next ( ) {
transaction := & PendingTransaction {
Value : bigint . BigInt { Int : new ( big . Int ) } ,
GasPrice : bigint . BigInt { Int : new ( big . Int ) } ,
GasLimit : bigint . BigInt { Int : new ( big . Int ) } ,
}
transaction . Status = new ( TxStatus )
transaction . AutoDelete = new ( bool )
err := rows . Scan ( & transaction . Hash ,
& transaction . Timestamp ,
( * bigint . SQLBigIntBytes ) ( transaction . Value . Int ) ,
& transaction . From ,
& transaction . To ,
& transaction . Data ,
& transaction . Symbol ,
( * bigint . SQLBigIntBytes ) ( transaction . GasPrice . Int ) ,
( * bigint . SQLBigIntBytes ) ( transaction . GasLimit . Int ) ,
& transaction . Type ,
& transaction . AdditionalData ,
& transaction . ChainID ,
& transaction . MultiTransactionID ,
transaction . Status ,
transaction . AutoDelete ,
2024-03-12 09:15:30 +00:00
& transaction . Nonce ,
2023-08-01 18:50:30 +00:00
)
if err != nil {
return nil , err
}
transactions = append ( transactions , transaction )
}
return transactions , nil
}
func ( tm * PendingTxTracker ) GetAllPending ( ) ( [ ] * PendingTransaction , error ) {
2024-03-19 00:31:35 +00:00
if tm . db == nil {
return nil , errors . New ( "database is not initialized" )
}
2023-08-01 18:50:30 +00:00
rows , err := tm . db . Query ( selectFromPending + "WHERE status = ?" , Pending )
if err != nil {
return nil , err
}
defer rows . Close ( )
return rowsToTransactions ( rows )
}
func ( tm * PendingTxTracker ) GetPendingByAddress ( chainIDs [ ] uint64 , address eth . Address ) ( [ ] * PendingTransaction , error ) {
if len ( chainIDs ) == 0 {
2023-08-30 16:14:57 +00:00
return nil , errors . New ( "GetPendingByAddress: at least 1 chainID is required" )
2023-08-01 18:50:30 +00:00
}
inVector := strings . Repeat ( "?, " , len ( chainIDs ) - 1 ) + "?"
var parameters [ ] interface { }
for _ , c := range chainIDs {
parameters = append ( parameters , c )
}
parameters = append ( parameters , address )
rows , err := tm . db . Query ( fmt . Sprintf ( selectFromPending + "WHERE network_id in (%s) AND from_address = ?" , inVector ) , parameters ... )
if err != nil {
return nil , err
}
defer rows . Close ( )
return rowsToTransactions ( rows )
}
// GetPendingEntry returns sql.ErrNoRows if no pending transaction is found for the given identity
func ( tm * PendingTxTracker ) GetPendingEntry ( chainID common . ChainID , hash eth . Hash ) ( * PendingTransaction , error ) {
rows , err := tm . db . Query ( selectFromPending + "WHERE network_id = ? AND hash = ?" , chainID , hash )
if err != nil {
return nil , err
}
defer rows . Close ( )
trs , err := rowsToTransactions ( rows )
if err != nil {
return nil , err
}
if len ( trs ) == 0 {
return nil , sql . ErrNoRows
}
return trs [ 0 ] , nil
}
2024-03-22 12:32:19 +00:00
func ( tm * PendingTxTracker ) CountPendingTxsFromNonce ( chainID common . ChainID , address eth . Address , nonce uint64 ) ( pendingTx uint64 , err error ) {
2024-03-12 09:15:30 +00:00
err = tm . db . QueryRow ( `
SELECT
COUNT ( nonce )
FROM
pending_transactions
WHERE
network_id = ?
AND
from_address = ?
AND
nonce >= ? ` ,
chainID , address , nonce ) .
Scan ( & pendingTx )
return
}
2023-08-01 18:50:30 +00:00
// StoreAndTrackPendingTx store the details of a pending transaction and track it until it is mined
func ( tm * PendingTxTracker ) StoreAndTrackPendingTx ( transaction * PendingTransaction ) error {
err := tm . addPending ( transaction )
if err != nil {
return err
}
tm . taskRunner . RunUntilDone ( )
return err
}
func ( tm * PendingTxTracker ) addPending ( transaction * PendingTransaction ) error {
2024-03-12 09:15:30 +00:00
var notifyFn func ( )
tx , err := tm . db . Begin ( )
if err != nil {
return err
}
defer func ( ) {
if err == nil {
err = tx . Commit ( )
if notifyFn != nil {
notifyFn ( )
}
return
}
_ = tx . Rollback ( )
} ( )
exists := true
var hash eth . Hash
err = tx . QueryRow ( `
SELECT hash
FROM
pending_transactions
WHERE
network_id = ?
AND
from_address = ?
AND
nonce = ?
` ,
transaction . ChainID ,
transaction . From ,
transaction . Nonce ) .
Scan ( & hash )
if err != nil {
if err == sql . ErrNoRows {
exists = false
} else {
return err
}
}
if exists {
notifyFn , err = tm . DeleteBySQLTx ( tx , transaction . ChainID , hash )
if err != nil && err != ErrStillPending {
return err
}
}
// TODO: maybe we should think of making (network_id, from_address, nonce) as primary key instead (network_id, hash) ????
2024-03-22 12:32:19 +00:00
var insert * sql . Stmt
insert , err = tx . Prepare ( ` INSERT OR REPLACE INTO pending_transactions
2023-08-01 18:50:30 +00:00
( network_id , hash , timestamp , value , from_address , to_address ,
2024-03-12 09:15:30 +00:00
data , symbol , gas_price , gas_limit , type , additional_data , multi_transaction_id , status ,
auto_delete , nonce )
2023-08-01 18:50:30 +00:00
VALUES
2024-03-12 09:15:30 +00:00
( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) ` )
2023-08-01 18:50:30 +00:00
if err != nil {
return err
}
2024-03-12 09:15:30 +00:00
defer insert . Close ( )
2023-08-01 18:50:30 +00:00
_ , err = insert . Exec (
transaction . ChainID ,
transaction . Hash ,
transaction . Timestamp ,
( * bigint . SQLBigIntBytes ) ( transaction . Value . Int ) ,
transaction . From ,
transaction . To ,
transaction . Data ,
transaction . Symbol ,
( * bigint . SQLBigIntBytes ) ( transaction . GasPrice . Int ) ,
( * bigint . SQLBigIntBytes ) ( transaction . GasLimit . Int ) ,
transaction . Type ,
transaction . AdditionalData ,
transaction . MultiTransactionID ,
transaction . Status ,
transaction . AutoDelete ,
2024-03-12 09:15:30 +00:00
transaction . Nonce ,
2023-08-01 18:50:30 +00:00
)
// Notify listeners of new pending transaction (used in activity history)
if err == nil {
2024-01-08 21:24:30 +00:00
tm . notifyPendingTransactionListeners ( PendingTxUpdatePayload {
TxIdentity : TxIdentity {
ChainID : transaction . ChainID ,
Hash : transaction . Hash ,
} ,
Deleted : false ,
} , [ ] eth . Address { transaction . From , transaction . To } , transaction . Timestamp )
2023-08-01 18:50:30 +00:00
}
if tm . rpcFilter != nil {
tm . rpcFilter . TriggerTransactionSentToUpstreamEvent ( & rpcfilters . PendingTxInfo {
Hash : transaction . Hash ,
Type : string ( transaction . Type ) ,
From : transaction . From ,
ChainID : uint64 ( transaction . ChainID ) ,
} )
}
return err
}
2024-01-08 21:24:30 +00:00
func ( tm * PendingTxTracker ) notifyPendingTransactionListeners ( payload PendingTxUpdatePayload , addresses [ ] eth . Address , timestamp uint64 ) {
jsonPayload , err := json . Marshal ( payload )
if err != nil {
2024-10-28 20:54:17 +00:00
tm . logger . Error ( "Failed to marshal PendingTxUpdatePayload" , zap . Stringer ( "hash" , payload . Hash ) , zap . Error ( err ) )
2024-01-08 21:24:30 +00:00
return
}
2023-08-01 18:50:30 +00:00
if tm . eventFeed != nil {
tm . eventFeed . Send ( walletevent . Event {
Type : EventPendingTransactionUpdate ,
2024-01-08 21:24:30 +00:00
ChainID : uint64 ( payload . ChainID ) ,
2023-08-01 18:50:30 +00:00
Accounts : addresses ,
At : int64 ( timestamp ) ,
2024-01-08 21:24:30 +00:00
Message : string ( jsonPayload ) ,
2023-08-01 18:50:30 +00:00
} )
}
}
// DeleteBySQLTx returns ErrStillPending if the transaction is still pending
func ( tm * PendingTxTracker ) DeleteBySQLTx ( tx * sql . Tx , chainID common . ChainID , hash eth . Hash ) ( notify func ( ) , err error ) {
row := tx . QueryRow ( ` SELECT from_address, to_address, timestamp, status FROM pending_transactions WHERE network_id = ? AND hash = ? ` , chainID , hash )
var from , to eth . Address
var timestamp uint64
var status TxStatus
err = row . Scan ( & from , & to , & timestamp , & status )
if err != nil {
return nil , err
}
_ , err = tx . Exec ( ` DELETE FROM pending_transactions WHERE network_id = ? AND hash = ? ` , chainID , hash )
if err != nil {
return nil , err
}
if err == nil && status == Pending {
err = ErrStillPending
}
return func ( ) {
2024-01-08 21:24:30 +00:00
tm . notifyPendingTransactionListeners ( PendingTxUpdatePayload {
TxIdentity : TxIdentity {
ChainID : chainID ,
Hash : hash ,
} ,
Deleted : true ,
} , [ ] eth . Address { from , to } , timestamp )
2023-08-01 18:50:30 +00:00
} , err
}
2023-09-06 19:15:07 +00:00
// GetOwnedPendingStatus returns sql.ErrNoRows if no pending transaction is found for the given identity
func GetOwnedPendingStatus ( tx * sql . Tx , chainID common . ChainID , hash eth . Hash , ownerAddress eth . Address ) ( txType * PendingTrxType , mTID * int64 , err error ) {
row := tx . QueryRow ( ` SELECT type, multi_transaction_id FROM pending_transactions WHERE network_id = ? AND hash = ? AND from_address = ? ` , chainID , hash , ownerAddress )
2023-08-01 18:50:30 +00:00
txType = new ( PendingTrxType )
2023-09-06 19:15:07 +00:00
mTID = new ( int64 )
err = row . Scan ( txType , mTID )
2023-08-01 18:50:30 +00:00
if err != nil {
return nil , nil , err
}
2023-09-06 19:15:07 +00:00
return txType , mTID , nil
2023-08-01 18:50:30 +00:00
}
// Watch returns sql.ErrNoRows if no pending transaction is found for the given identity
// tx.Status is not nill if err is nil
func ( tm * PendingTxTracker ) Watch ( ctx context . Context , chainID common . ChainID , hash eth . Hash ) ( * TxStatus , error ) {
tx , err := tm . GetPendingEntry ( chainID , hash )
if err != nil {
return nil , err
}
return tx . Status , nil
}
// Delete returns ErrStillPending if the deleted transaction was still pending
// The transactions are suppose to be deleted by the client only after they are confirmed
func ( tm * PendingTxTracker ) Delete ( ctx context . Context , chainID common . ChainID , transactionHash eth . Hash ) error {
tx , err := tm . db . BeginTx ( ctx , nil )
if err != nil {
return fmt . Errorf ( "failed to begin transaction: %w" , err )
}
notifyFn , err := tm . DeleteBySQLTx ( tx , chainID , transactionHash )
if err != nil && err != ErrStillPending {
rollErr := tx . Rollback ( )
if rollErr != nil {
return fmt . Errorf ( "failed to rollback transaction due to error: %w" , err )
}
return err
}
commitErr := tx . Commit ( )
if commitErr != nil {
return fmt . Errorf ( "failed to commit transaction: %w" , commitErr )
}
notifyFn ( )
return err
}