feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions

View File

@@ -0,0 +1,40 @@
// copy of go-ethereum/internal/ethapi/addrlock.go
package transactions
import (
"sync"
"github.com/status-im/status-go/eth-node/types"
)
// AddrLocker provides locks for addresses
type AddrLocker struct {
mu sync.Mutex
locks map[types.Address]*sync.Mutex
}
// lock returns the lock of the given address.
func (l *AddrLocker) lock(address types.Address) *sync.Mutex {
l.mu.Lock()
defer l.mu.Unlock()
if l.locks == nil {
l.locks = make(map[types.Address]*sync.Mutex)
}
if _, ok := l.locks[address]; !ok {
l.locks[address] = new(sync.Mutex)
}
return l.locks[address]
}
// LockAddr locks an account's mutex. This is used to prevent another tx getting the
// same nonce until the lock is released. The mutex prevents the (an identical nonce) from
// being read again during the time that the first transaction is being signed.
func (l *AddrLocker) LockAddr(address types.Address) {
l.lock(address).Lock()
}
// UnlockAddr unlocks the mutex of the given account.
func (l *AddrLocker) UnlockAddr(address types.Address) {
l.lock(address).Unlock()
}

View File

@@ -0,0 +1,109 @@
package transactions
import (
"context"
"sync"
"time"
)
// TaskFunc defines the task to be run. The context is canceled when Stop is
// called to early stop scheduled task.
type TaskFunc func(ctx context.Context) (done bool)
const (
WorkNotDone = false
WorkDone = true
)
// ConditionalRepeater runs a task at regular intervals until the task returns
// true. It doesn't allow running task in parallel and can be triggered early
// by call to RunUntilDone.
type ConditionalRepeater struct {
interval time.Duration
task TaskFunc
// nil if not running
ctx context.Context
ctxMu sync.Mutex
cancel context.CancelFunc
runNowCh chan bool
runNowMu sync.Mutex
}
func NewConditionalRepeater(interval time.Duration, task TaskFunc) *ConditionalRepeater {
return &ConditionalRepeater{
interval: interval,
task: task,
runNowCh: make(chan bool, 1),
}
}
// RunUntilDone starts the task immediately and continues to run it at the defined
// interval until the task returns true. Can be called multiple times but it
// does not allow multiple concurrent executions of the task.
func (t *ConditionalRepeater) RunUntilDone() {
t.ctxMu.Lock()
defer func() {
t.runNowMu.Lock()
if len(t.runNowCh) == 0 {
t.runNowCh <- true
}
t.runNowMu.Unlock()
t.ctxMu.Unlock()
}()
if t.ctx != nil {
return
}
t.ctx, t.cancel = context.WithCancel(context.Background())
go func() {
defer func() {
t.ctxMu.Lock()
defer t.ctxMu.Unlock()
t.cancel()
t.ctx = nil
}()
ticker := time.NewTicker(t.interval)
defer ticker.Stop()
for {
select {
// Stop was called or task returned true
case <-t.ctx.Done():
return
// Scheduled execution
case <-ticker.C:
if t.task(t.ctx) {
return
}
// Start right away if requested
case <-t.runNowCh:
ticker.Reset(t.interval)
if t.task(t.ctx) {
t.runNowMu.Lock()
if len(t.runNowCh) == 0 {
t.runNowMu.Unlock()
return
}
t.runNowMu.Unlock()
}
}
}
}()
}
// Stop forcefully stops the running task by canceling its context.
func (t *ConditionalRepeater) Stop() {
t.ctxMu.Lock()
defer t.ctxMu.Unlock()
if t.ctx != nil {
t.cancel()
}
}
func (t *ConditionalRepeater) IsRunning() bool {
t.ctxMu.Lock()
defer t.ctxMu.Unlock()
return t.ctx != nil
}

View File

@@ -0,0 +1,674 @@
package transactions
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math/big"
"strings"
"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"
ethrpc "github.com/ethereum/go-ethereum/rpc"
"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"
"github.com/status-im/status-go/services/wallet/walletevent"
)
const (
// 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 (
ErrStillPending = errors.New("transaction is still pending")
)
type TxStatus = string
// Values for status column in pending_transactions
const (
Pending TxStatus = "Pending"
Success TxStatus = "Success"
Failed TxStatus = "Failed"
)
type AutoDeleteType = bool
const (
AutoDelete AutoDeleteType = true
Keep AutoDeleteType = false
)
// TODO #12120: unify it with TransactionIdentity
type TxIdentity struct {
ChainID common.ChainID `json:"chainId"`
Hash eth.Hash `json:"hash"`
}
type PendingTxUpdatePayload struct {
TxIdentity
Deleted bool `json:"deleted"`
}
type StatusChangedPayload struct {
TxIdentity
Status TxStatus `json:"status"`
}
// PendingTxTracker implements StatusService in common/status_node_service.go
type PendingTxTracker struct {
db *sql.DB
rpcClient rpc.ClientInterface
rpcFilter *rpcfilters.Service
eventFeed *event.Feed
taskRunner *ConditionalRepeater
log log.Logger
}
func NewPendingTxTracker(db *sql.DB, rpcClient rpc.ClientInterface, rpcFilter *rpcfilters.Service, eventFeed *event.Feed, checkInterval time.Duration) *PendingTxTracker {
tm := &PendingTxTracker{
db: db,
rpcClient: rpcClient,
eventFeed: eventFeed,
rpcFilter: rpcFilter,
log: log.New("package", "status-go/transactions.PendingTxTracker"),
}
tm.taskRunner = NewConditionalRepeater(checkInterval, func(ctx context.Context) bool {
return tm.fetchAndUpdateDB(ctx)
})
return tm
}
type txStatusRes struct {
Status TxStatus
hash eth.Hash
}
func (tm *PendingTxTracker) fetchAndUpdateDB(ctx context.Context) bool {
res := WorkNotDone
txs, err := tm.GetAllPending()
if err != nil {
tm.log.Error("Failed to get pending transactions", "error", err)
return WorkDone
}
tm.log.Debug("Checking for PT status", "count", len(txs))
txsMap := make(map[common.ChainID][]eth.Hash)
for _, tx := range txs {
chainID := tx.ChainID
txsMap[chainID] = append(txsMap[chainID], tx.Hash)
}
doneCount := 0
// Batch request for each chain
for chainID, txs := range txsMap {
tm.log.Debug("Processing PTs", "chainID", chainID, "count", len(txs))
batchRes, err := fetchBatchTxStatus(ctx, tm.rpcClient, chainID, txs, tm.log)
if err != nil {
tm.log.Error("Failed to batch fetch pending transactions status for", "chainID", chainID, "error", err)
continue
}
if len(batchRes) == 0 {
tm.log.Debug("No change to PTs status", "chainID", chainID)
continue
}
tm.log.Debug("PTs done", "chainID", chainID, "count", len(batchRes))
doneCount += len(batchRes)
updateRes, err := tm.updateDBStatus(ctx, chainID, batchRes)
if err != nil {
tm.log.Error("Failed to update pending transactions status for", "chainID", chainID, "error", err)
continue
}
tm.log.Debug("Emit notifications for PTs", "chainID", chainID, "count", len(updateRes))
tm.emitNotifications(chainID, updateRes)
}
if len(txs) == doneCount {
res = WorkDone
}
tm.log.Debug("Done PTs iteration", "count", doneCount, "completed", res)
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) {
chainClient, err := rpcClient.AbstractEthClient(chainID)
if err != nil {
log.Error("Failed to get chain client", "error", err)
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{
Method: GetTransactionReceiptRPCName,
Args: []interface{}{hash},
Result: new(nullableReceipt),
})
}
err = chainClient.BatchCallContext(reqCtx, batch)
if err != nil {
log.Error("Transactions request fail", "error", err)
return nil, err
}
res := make([]txStatusRes, 0, len(batch))
for i, b := range batch {
err := b.Error
if err != nil {
log.Error("Failed to get transaction", "error", err, "hash", hashes[i])
continue
}
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],
Status: status,
})
}
}
return res, nil
}
// updateDBStatus returns entries that were updated only
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)
}
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 {
tm.log.Warn("Missing entry while checking for auto_delete", "hash", br.hash)
} else {
tm.log.Error("Failed to retrieve auto_delete for pending transaction", "error", err, "hash", br.hash)
}
continue
}
if autoDel {
notifyFn, err := tm.DeleteBySQLTx(tx, chainID, br.hash)
if err != nil && err != ErrStillPending {
tm.log.Error("Failed to delete pending transaction", "error", err, "hash", br.hash)
continue
}
notifyFunctions = append(notifyFunctions, notifyFn)
} else {
// If the entry was not deleted, update the status
txStatus := br.Status
res, err := updateStmt.ExecContext(ctx, txStatus, chainID, br.hash)
if err != nil {
tm.log.Error("Failed to update pending transaction status", "error", err, "hash", br.hash)
continue
}
affected, err := res.RowsAffected()
if err != nil {
tm.log.Error("Failed to get updated rows", "error", err, "hash", br.hash)
continue
}
if affected == 0 {
tm.log.Warn("Missing entry to update for", "hash", br.hash)
continue
}
}
res = append(res, br)
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
for _, fn := range notifyFunctions {
fn()
}
return res, nil
}
func (tm *PendingTxTracker) emitNotifications(chainID common.ChainID, changes []txStatusRes) {
if tm.eventFeed != nil {
for _, change := range changes {
payload := StatusChangedPayload{
TxIdentity: TxIdentity{
ChainID: chainID,
Hash: change.hash,
},
Status: change.Status,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
tm.log.Error("Failed to marshal pending transaction status", "error", err, "hash", change.hash)
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
func (tm *PendingTxTracker) TrackPendingTransaction(chainID common.ChainID, hash eth.Hash, from eth.Address, trType PendingTrxType, autoDelete AutoDeleteType) error {
err := tm.addPending(&PendingTransaction{
ChainID: chainID,
Hash: hash,
From: from,
Timestamp: uint64(time.Now().Unix()),
Type: trType,
AutoDelete: &autoDelete,
})
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"
SetSignerPublicKey PendingTrxType = "SetSignerPublicKey"
WalletConnectTransfer PendingTrxType = "WalletConnectTransfer"
)
type PendingTransaction struct {
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 int64 `json:"multi_transaction_id"`
// 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,
network_id, COALESCE(multi_transaction_id, 0), status, auto_delete
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,
)
if err != nil {
return nil, err
}
transactions = append(transactions, transaction)
}
return transactions, nil
}
func (tm *PendingTxTracker) GetAllPending() ([]*PendingTransaction, error) {
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 {
return nil, errors.New("GetPendingByAddress: at least 1 chainID is required")
}
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
}
// 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 {
insert, err := tm.db.Prepare(`INSERT OR REPLACE INTO pending_transactions
(network_id, hash, timestamp, value, from_address, to_address,
data, symbol, gas_price, gas_limit, type, additional_data, multi_transaction_id, status, auto_delete)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? , ?)`)
if err != nil {
return err
}
_, 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,
)
// Notify listeners of new pending transaction (used in activity history)
if err == nil {
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{
Hash: transaction.Hash,
Type: string(transaction.Type),
From: transaction.From,
ChainID: uint64(transaction.ChainID),
})
}
return err
}
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(payload.ChainID),
Accounts: addresses,
At: int64(timestamp),
Message: string(jsonPayload),
})
}
}
// 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() {
tm.notifyPendingTransactionListeners(PendingTxUpdatePayload{
TxIdentity: TxIdentity{
ChainID: chainID,
Hash: hash,
},
Deleted: true,
}, []eth.Address{from, to}, timestamp)
}, err
}
// 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)
txType = new(PendingTrxType)
mTID = new(int64)
err = row.Scan(txType, mTID)
if err != nil {
return nil, nil, err
}
return txType, mTID, nil
}
// 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
}

View File

@@ -0,0 +1,93 @@
package transactions
import (
"context"
"math/big"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/rpc"
)
// rpcWrapper wraps provides convenient interface for ethereum RPC APIs we need for sending transactions
type rpcWrapper struct {
RPCClient *rpc.Client
chainID uint64
}
func newRPCWrapper(client *rpc.Client, chainID uint64) *rpcWrapper {
return &rpcWrapper{RPCClient: client, chainID: chainID}
}
// PendingNonceAt returns the account nonce of the given account in the pending state.
// This is the nonce that should be used for the next transaction.
func (w *rpcWrapper) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
var result hexutil.Uint64
err := w.RPCClient.CallContext(ctx, &result, w.chainID, "eth_getTransactionCount", account, "pending")
return uint64(result), err
}
// SuggestGasPrice retrieves the currently suggested gas price to allow a timely
// execution of a transaction.
func (w *rpcWrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
var hex hexutil.Big
if err := w.RPCClient.CallContext(ctx, &hex, w.chainID, "eth_gasPrice"); err != nil {
return nil, err
}
return (*big.Int)(&hex), nil
}
// EstimateGas tries to estimate the gas needed to execute a specific transaction based on
// the current pending state of the backend blockchain. There is no guarantee that this is
// the true gas limit requirement as other transactions may be added or removed by miners,
// but it should provide a basis for setting a reasonable default.
func (w *rpcWrapper) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
var hex hexutil.Uint64
err := w.RPCClient.CallContext(ctx, &hex, w.chainID, "eth_estimateGas", toCallArg(msg))
if err != nil {
return 0, err
}
return uint64(hex), nil
}
// Does the `eth_sendRawTransaction` call with the given raw transaction hex string.
func (w *rpcWrapper) SendRawTransaction(ctx context.Context, rawTx string) error {
return w.RPCClient.CallContext(ctx, nil, w.chainID, "eth_sendRawTransaction", rawTx)
}
// SendTransaction injects a signed transaction into the pending pool for execution.
//
// If the transaction was a contract creation use the TransactionReceipt method to get the
// contract address after the transaction has been mined.
func (w *rpcWrapper) SendTransaction(ctx context.Context, tx *gethtypes.Transaction) error {
data, err := tx.MarshalBinary()
if err != nil {
return err
}
return w.SendRawTransaction(ctx, types.EncodeHex(data))
}
func toCallArg(msg ethereum.CallMsg) interface{} {
arg := map[string]interface{}{
"from": msg.From,
"to": msg.To,
}
if len(msg.Data) > 0 {
arg["data"] = types.HexBytes(msg.Data)
}
if msg.Value != nil {
arg["value"] = (*hexutil.Big)(msg.Value)
}
if msg.Gas != 0 {
arg["gas"] = hexutil.Uint64(msg.Gas)
}
if msg.GasPrice != nil {
arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice)
}
return arg
}

View File

@@ -0,0 +1,139 @@
package transactions
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 {
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
}
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
}
type TestTxSummary struct {
failStatus bool
DontConfirm bool
// Timestamp will be used to mock the Timestamp if greater than 0
Timestamp int
}
func MockTestTransactions(t *testing.T, chainClient *MockChainClient, testTxs []TestTxSummary) []PendingTransaction {
txs := GenerateTestPendingTransactions(0, len(testTxs))
for txIdx := range txs {
tx := &txs[txIdx]
if testTxs[txIdx].Timestamp > 0 {
tx.Timestamp = uint64(testTxs[txIdx].Timestamp)
}
// Mock the first call to getTransactionByHash
chainClient.SetAvailableClients([]common.ChainID{tx.ChainID})
cl := chainClient.Clients[tx.ChainID]
call := 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] == tx.Hash
}
return ok
})).Return(nil)
if testTxs[txIdx].DontConfirm {
call = call.Times(0)
} else {
call = call.Once()
}
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
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
}

View File

@@ -0,0 +1,486 @@
package transactions
import (
"bytes"
"context"
"errors"
"fmt"
"math/big"
"time"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
)
const (
// sendTxTimeout defines how many seconds to wait before returning result in sentTransaction().
sendTxTimeout = 300 * time.Second
defaultGas = 90000
ValidSignatureSize = 65
)
// ErrInvalidSignatureSize is returned if a signature is not 65 bytes to avoid panic from go-ethereum
var ErrInvalidSignatureSize = errors.New("signature size must be 65")
type ErrBadNonce struct {
nonce uint64
expectedNonce uint64
}
func (e *ErrBadNonce) Error() string {
return fmt.Sprintf("bad nonce. expected %d, got %d", e.expectedNonce, e.nonce)
}
// Transactor validates, signs transactions.
// It uses upstream to propagate transactions to the Ethereum network.
type Transactor struct {
rpcWrapper *rpcWrapper
sendTxTimeout time.Duration
rpcCallTimeout time.Duration
networkID uint64
log log.Logger
}
// NewTransactor returns a new Manager.
func NewTransactor() *Transactor {
return &Transactor{
sendTxTimeout: sendTxTimeout,
log: log.New("package", "status-go/transactions.Manager"),
}
}
// SetNetworkID selects a correct network.
func (t *Transactor) SetNetworkID(networkID uint64) {
t.networkID = networkID
}
func (t *Transactor) NetworkID() uint64 {
return t.networkID
}
// SetRPC sets RPC params, a client and a timeout
func (t *Transactor) SetRPC(rpcClient *rpc.Client, timeout time.Duration) {
t.rpcWrapper = newRPCWrapper(rpcClient, rpcClient.UpstreamChainID)
t.rpcCallTimeout = timeout
}
func (t *Transactor) NextNonce(rpcClient *rpc.Client, chainID uint64, from types.Address) (uint64, error) {
wrapper := newRPCWrapper(rpcClient, chainID)
ctx := context.Background()
return wrapper.PendingNonceAt(ctx, common.Address(from))
}
func (t *Transactor) EstimateGas(network *params.Network, from common.Address, to common.Address, value *big.Int, input []byte) (uint64, error) {
rpcWrapper := newRPCWrapper(t.rpcWrapper.RPCClient, network.ChainID)
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
msg := ethereum.CallMsg{
From: from,
To: &to,
Value: value,
Data: input,
}
return rpcWrapper.EstimateGas(ctx, msg)
}
// SendTransaction is an implementation of eth_sendTransaction. It queues the tx to the sign queue.
func (t *Transactor) SendTransaction(sendArgs SendTxArgs, verifiedAccount *account.SelectedExtKey) (hash types.Hash, err error) {
hash, err = t.validateAndPropagate(t.rpcWrapper, verifiedAccount, sendArgs)
return
}
func (t *Transactor) SendTransactionWithChainID(chainID uint64, sendArgs SendTxArgs, verifiedAccount *account.SelectedExtKey) (hash types.Hash, err error) {
wrapper := newRPCWrapper(t.rpcWrapper.RPCClient, chainID)
hash, err = t.validateAndPropagate(wrapper, verifiedAccount, sendArgs)
return
}
func (t *Transactor) ValidateAndBuildTransaction(chainID uint64, sendArgs SendTxArgs) (tx *gethtypes.Transaction, err error) {
wrapper := newRPCWrapper(t.rpcWrapper.RPCClient, chainID)
tx, err = t.validateAndBuildTransaction(wrapper, sendArgs)
return
}
func (t *Transactor) AddSignatureToTransaction(chainID uint64, tx *gethtypes.Transaction, sig []byte) (*gethtypes.Transaction, error) {
if len(sig) != ValidSignatureSize {
return nil, ErrInvalidSignatureSize
}
rpcWrapper := newRPCWrapper(t.rpcWrapper.RPCClient, chainID)
chID := big.NewInt(int64(rpcWrapper.chainID))
signer := gethtypes.NewLondonSigner(chID)
txWithSignature, err := tx.WithSignature(signer, sig)
if err != nil {
return nil, err
}
return txWithSignature, nil
}
func (t *Transactor) SendRawTransaction(chainID uint64, rawTx string) error {
rpcWrapper := newRPCWrapper(t.rpcWrapper.RPCClient, chainID)
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
return rpcWrapper.SendRawTransaction(ctx, rawTx)
}
func (t *Transactor) SendTransactionWithSignature(tx *gethtypes.Transaction) (hash types.Hash, err error) {
rpcWrapper := newRPCWrapper(t.rpcWrapper.RPCClient, tx.ChainId().Uint64())
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
if err := rpcWrapper.SendTransaction(ctx, tx); err != nil {
return hash, err
}
return types.Hash(tx.Hash()), nil
}
func (t *Transactor) AddSignatureToTransactionAndSend(chainID uint64, tx *gethtypes.Transaction, sig []byte) (hash types.Hash, err error) {
txWithSignature, err := t.AddSignatureToTransaction(chainID, tx, sig)
if err != nil {
return hash, err
}
return t.SendTransactionWithSignature(txWithSignature)
}
// BuildTransactionAndSendWithSignature receive a transaction and a signature, serialize them together and propage it to the network.
// It's different from eth_sendRawTransaction because it receives a signature and not a serialized transaction with signature.
// Since the transactions is already signed, we assume it was validated and used the right nonce.
func (t *Transactor) BuildTransactionAndSendWithSignature(chainID uint64, args SendTxArgs, sig []byte) (hash types.Hash, err error) {
txWithSignature, err := t.BuildTransactionWithSignature(chainID, args, sig)
if err != nil {
return hash, err
}
hash, err = t.SendTransactionWithSignature(txWithSignature)
return hash, err
}
func (t *Transactor) BuildTransactionWithSignature(chainID uint64, args SendTxArgs, sig []byte) (*gethtypes.Transaction, error) {
if !args.Valid() {
return nil, ErrInvalidSendTxArgs
}
if len(sig) != ValidSignatureSize {
return nil, ErrInvalidSignatureSize
}
tx := t.buildTransaction(args)
expectedNonce, err := t.NextNonce(t.rpcWrapper.RPCClient, chainID, args.From)
if err != nil {
return nil, err
}
if tx.Nonce() != expectedNonce {
return nil, &ErrBadNonce{tx.Nonce(), expectedNonce}
}
txWithSignature, err := t.AddSignatureToTransaction(chainID, tx, sig)
if err != nil {
return nil, err
}
return txWithSignature, nil
}
func (t *Transactor) HashTransaction(args SendTxArgs) (validatedArgs SendTxArgs, hash types.Hash, err error) {
if !args.Valid() {
return validatedArgs, hash, ErrInvalidSendTxArgs
}
validatedArgs = args
nonce, err := t.NextNonce(t.rpcWrapper.RPCClient, t.rpcWrapper.chainID, args.From)
if err != nil {
return validatedArgs, hash, err
}
gasPrice := (*big.Int)(args.GasPrice)
gasFeeCap := (*big.Int)(args.MaxFeePerGas)
gasTipCap := (*big.Int)(args.MaxPriorityFeePerGas)
if args.GasPrice == nil && args.MaxFeePerGas == nil {
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
gasPrice, err = t.rpcWrapper.SuggestGasPrice(ctx)
if err != nil {
return validatedArgs, hash, err
}
}
chainID := big.NewInt(int64(t.networkID))
value := (*big.Int)(args.Value)
var gas uint64
if args.Gas == nil {
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
var (
gethTo common.Address
gethToPtr *common.Address
)
if args.To != nil {
gethTo = common.Address(*args.To)
gethToPtr = &gethTo
}
if args.GasPrice == nil {
gas, err = t.rpcWrapper.EstimateGas(ctx, ethereum.CallMsg{
From: common.Address(args.From),
To: gethToPtr,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Value: value,
Data: args.GetInput(),
})
} else {
gas, err = t.rpcWrapper.EstimateGas(ctx, ethereum.CallMsg{
From: common.Address(args.From),
To: gethToPtr,
GasPrice: gasPrice,
Value: value,
Data: args.GetInput(),
})
}
if err != nil {
return validatedArgs, hash, err
}
if gas < defaultGas {
t.log.Info("default gas will be used because estimated is lower", "estimated", gas, "default", defaultGas)
gas = defaultGas
}
} else {
gas = uint64(*args.Gas)
}
newNonce := hexutil.Uint64(nonce)
newGas := hexutil.Uint64(gas)
validatedArgs.Nonce = &newNonce
if args.GasPrice != nil {
validatedArgs.GasPrice = (*hexutil.Big)(gasPrice)
} else {
validatedArgs.MaxPriorityFeePerGas = (*hexutil.Big)(gasTipCap)
validatedArgs.MaxPriorityFeePerGas = (*hexutil.Big)(gasFeeCap)
}
validatedArgs.Gas = &newGas
tx := t.buildTransaction(validatedArgs)
hash = types.Hash(gethtypes.NewLondonSigner(chainID).Hash(tx))
return validatedArgs, hash, nil
}
// make sure that only account which created the tx can complete it
func (t *Transactor) validateAccount(args SendTxArgs, selectedAccount *account.SelectedExtKey) error {
if selectedAccount == nil {
return account.ErrNoAccountSelected
}
if !bytes.Equal(args.From.Bytes(), selectedAccount.Address.Bytes()) {
return ErrInvalidTxSender
}
return nil
}
func (t *Transactor) validateAndBuildTransaction(rpcWrapper *rpcWrapper, args SendTxArgs) (tx *gethtypes.Transaction, err error) {
if !args.Valid() {
return tx, ErrInvalidSendTxArgs
}
var nonce uint64
if args.Nonce != nil {
nonce = uint64(*args.Nonce)
} else {
nonce, err = t.NextNonce(rpcWrapper.RPCClient, rpcWrapper.chainID, args.From)
if err != nil {
return tx, err
}
}
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
gasPrice := (*big.Int)(args.GasPrice)
if !args.IsDynamicFeeTx() && args.GasPrice == nil {
gasPrice, err = rpcWrapper.SuggestGasPrice(ctx)
if err != nil {
return tx, err
}
}
value := (*big.Int)(args.Value)
var gas uint64
if args.Gas != nil {
gas = uint64(*args.Gas)
} else if args.Gas == nil && !args.IsDynamicFeeTx() {
ctx, cancel = context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
var (
gethTo common.Address
gethToPtr *common.Address
)
if args.To != nil {
gethTo = common.Address(*args.To)
gethToPtr = &gethTo
}
gas, err = rpcWrapper.EstimateGas(ctx, ethereum.CallMsg{
From: common.Address(args.From),
To: gethToPtr,
GasPrice: gasPrice,
Value: value,
Data: args.GetInput(),
})
if err != nil {
return tx, err
}
if gas < defaultGas {
t.log.Info("default gas will be used because estimated is lower", "estimated", gas, "default", defaultGas)
gas = defaultGas
}
}
tx = t.buildTransactionWithOverrides(nonce, value, gas, gasPrice, args)
return tx, nil
}
func (t *Transactor) validateAndPropagate(rpcWrapper *rpcWrapper, selectedAccount *account.SelectedExtKey, args SendTxArgs) (hash types.Hash, err error) {
if err = t.validateAccount(args, selectedAccount); err != nil {
return hash, err
}
tx, err := t.validateAndBuildTransaction(rpcWrapper, args)
if err != nil {
return hash, err
}
chainID := big.NewInt(int64(rpcWrapper.chainID))
signedTx, err := gethtypes.SignTx(tx, gethtypes.NewLondonSigner(chainID), selectedAccount.AccountKey.PrivateKey)
if err != nil {
return hash, err
}
ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout)
defer cancel()
if err := rpcWrapper.SendTransaction(ctx, signedTx); err != nil {
return hash, err
}
return types.Hash(signedTx.Hash()), nil
}
func (t *Transactor) buildTransaction(args SendTxArgs) *gethtypes.Transaction {
var (
nonce uint64
value *big.Int
gas uint64
gasPrice *big.Int
)
if args.Nonce != nil {
nonce = uint64(*args.Nonce)
}
if args.Value != nil {
value = (*big.Int)(args.Value)
}
if args.Gas != nil {
gas = uint64(*args.Gas)
}
if args.GasPrice != nil {
gasPrice = (*big.Int)(args.GasPrice)
}
return t.buildTransactionWithOverrides(nonce, value, gas, gasPrice, args)
}
func (t *Transactor) buildTransactionWithOverrides(nonce uint64, value *big.Int, gas uint64, gasPrice *big.Int, args SendTxArgs) *gethtypes.Transaction {
var tx *gethtypes.Transaction
if args.To != nil {
to := common.Address(*args.To)
var txData gethtypes.TxData
if args.IsDynamicFeeTx() {
gasTipCap := (*big.Int)(args.MaxPriorityFeePerGas)
gasFeeCap := (*big.Int)(args.MaxFeePerGas)
txData = &gethtypes.DynamicFeeTx{
Nonce: nonce,
Gas: gas,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
To: &to,
Value: value,
Data: args.GetInput(),
}
} else {
txData = &gethtypes.LegacyTx{
Nonce: nonce,
GasPrice: gasPrice,
Gas: gas,
To: &to,
Value: value,
Data: args.GetInput(),
}
}
tx = gethtypes.NewTx(txData)
t.logNewTx(args, gas, gasPrice, value)
} else {
if args.IsDynamicFeeTx() {
gasTipCap := (*big.Int)(args.MaxPriorityFeePerGas)
gasFeeCap := (*big.Int)(args.MaxFeePerGas)
txData := &gethtypes.DynamicFeeTx{
Nonce: nonce,
Value: value,
Gas: gas,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: args.GetInput(),
}
tx = gethtypes.NewTx(txData)
} else {
tx = gethtypes.NewContractCreation(nonce, value, gas, gasPrice, args.GetInput())
}
t.logNewContract(args, gas, gasPrice, value, nonce)
}
return tx
}
func (t *Transactor) logNewTx(args SendTxArgs, gas uint64, gasPrice *big.Int, value *big.Int) {
t.log.Info("New transaction",
"From", args.From,
"To", *args.To,
"Gas", gas,
"GasPrice", gasPrice,
"Value", value,
)
}
func (t *Transactor) logNewContract(args SendTxArgs, gas uint64, gasPrice *big.Int, value *big.Int, nonce uint64) {
t.log.Info("New contract",
"From", args.From,
"Gas", gas,
"GasPrice", gasPrice,
"Value", value,
"Contract address", crypto.CreateAddress(args.From, nonce),
)
}

View File

@@ -0,0 +1,128 @@
package transactions
import (
"bytes"
"context"
"errors"
"math/big"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/eth-node/types"
)
var (
// ErrInvalidSendTxArgs is returned when the structure of SendTxArgs is ambigious.
ErrInvalidSendTxArgs = errors.New("transaction arguments are invalid")
// ErrUnexpectedArgs is returned when args are of unexpected length.
ErrUnexpectedArgs = errors.New("unexpected args")
//ErrInvalidTxSender is returned when selected account is different than From field.
ErrInvalidTxSender = errors.New("transaction can only be send by its creator")
//ErrAccountDoesntExist is sent when provided sub-account is not stored in database.
ErrAccountDoesntExist = errors.New("account doesn't exist")
)
// PendingNonceProvider provides information about nonces.
type PendingNonceProvider interface {
PendingNonceAt(ctx context.Context, account common.Address) (uint64, error)
}
// GasCalculator provides methods for estimating and pricing gas.
type GasCalculator interface {
ethereum.GasEstimator
ethereum.GasPricer
}
// SendTxArgs represents the arguments to submit a new transaction into the transaction pool.
// This struct is based on go-ethereum's type in internal/ethapi/api.go, but we have freedom
// over the exact layout of this struct.
type SendTxArgs struct {
From types.Address `json:"from"`
To *types.Address `json:"to"`
Gas *hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
Value *hexutil.Big `json:"value"`
Nonce *hexutil.Uint64 `json:"nonce"`
MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"`
MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"`
// We keep both "input" and "data" for backward compatibility.
// "input" is a preferred field.
// see `vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1107`
Input types.HexBytes `json:"input"`
Data types.HexBytes `json:"data"`
}
// Valid checks whether this structure is filled in correctly.
func (args SendTxArgs) Valid() bool {
// if at least one of the fields is empty, it is a valid struct
if isNilOrEmpty(args.Input) || isNilOrEmpty(args.Data) {
return true
}
// we only allow both fields to present if they have the same data
return bytes.Equal(args.Input, args.Data)
}
// IsDynamicFeeTx checks whether dynamic fee parameters are set for the tx
func (args SendTxArgs) IsDynamicFeeTx() bool {
return args.MaxFeePerGas != nil && args.MaxPriorityFeePerGas != nil
}
// GetInput returns either Input or Data field's value dependent on what is filled.
func (args SendTxArgs) GetInput() types.HexBytes {
if !isNilOrEmpty(args.Input) {
return args.Input
}
return args.Data
}
func (args SendTxArgs) ToTransactOpts(signerFn bind.SignerFn) *bind.TransactOpts {
var gasFeeCap *big.Int
if args.MaxFeePerGas != nil {
gasFeeCap = (*big.Int)(args.MaxFeePerGas)
}
var gasTipCap *big.Int
if args.MaxPriorityFeePerGas != nil {
gasTipCap = (*big.Int)(args.MaxPriorityFeePerGas)
}
var nonce *big.Int
if args.Nonce != nil {
nonce = new(big.Int).SetUint64((uint64)(*args.Nonce))
}
var gasPrice *big.Int
if args.GasPrice != nil {
gasPrice = (*big.Int)(args.GasPrice)
}
var gasLimit uint64
if args.Gas != nil {
gasLimit = uint64(*args.Gas)
}
var noSign = false
if signerFn == nil {
noSign = true
}
return &bind.TransactOpts{
From: common.Address(args.From),
Signer: signerFn,
GasPrice: gasPrice,
GasLimit: gasLimit,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Nonce: nonce,
NoSign: noSign,
}
}
func isNilOrEmpty(bytes types.HexBytes) bool {
return len(bytes) == 0
}