40
vendor/github.com/status-im/status-go/transactions/addrlock.go
generated
vendored
Normal file
40
vendor/github.com/status-im/status-go/transactions/addrlock.go
generated
vendored
Normal 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()
|
||||
}
|
||||
109
vendor/github.com/status-im/status-go/transactions/conditionalrepeater.go
generated
vendored
Normal file
109
vendor/github.com/status-im/status-go/transactions/conditionalrepeater.go
generated
vendored
Normal 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
|
||||
}
|
||||
674
vendor/github.com/status-im/status-go/transactions/pendingtxtracker.go
generated
vendored
Normal file
674
vendor/github.com/status-im/status-go/transactions/pendingtxtracker.go
generated
vendored
Normal 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, ×tamp, &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
|
||||
}
|
||||
93
vendor/github.com/status-im/status-go/transactions/rpc_wrapper.go
generated
vendored
Normal file
93
vendor/github.com/status-im/status-go/transactions/rpc_wrapper.go
generated
vendored
Normal 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
|
||||
}
|
||||
139
vendor/github.com/status-im/status-go/transactions/testhelpers.go
generated
vendored
Normal file
139
vendor/github.com/status-im/status-go/transactions/testhelpers.go
generated
vendored
Normal 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
|
||||
}
|
||||
486
vendor/github.com/status-im/status-go/transactions/transactor.go
generated
vendored
Normal file
486
vendor/github.com/status-im/status-go/transactions/transactor.go
generated
vendored
Normal 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),
|
||||
)
|
||||
}
|
||||
128
vendor/github.com/status-im/status-go/transactions/types.go
generated
vendored
Normal file
128
vendor/github.com/status-im/status-go/transactions/types.go
generated
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user