package transfer import ( "context" "database/sql" "errors" "fmt" "math/big" "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/account" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/params" "github.com/status-im/status-go/rpc/chain" "github.com/status-im/status-go/services/wallet/async" "github.com/status-im/status-go/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/bridge" wallet_common "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/walletevent" "github.com/status-im/status-go/transactions" ) const ( // PendingTransactionUpdate is emitted when a pending transaction is updated (added or deleted) EventPendingTransactionUpdate walletevent.EventType = "pending-transaction-update" ) type TransactionManager struct { db *sql.DB gethManager *account.GethManager transactor *transactions.Transactor config *params.NodeConfig accountsDB *accounts.Database eventFeed *event.Feed } func NewTransactionManager(db *sql.DB, gethManager *account.GethManager, transactor *transactions.Transactor, config *params.NodeConfig, accountsDB *accounts.Database, eventFeed *event.Feed) *TransactionManager { return &TransactionManager{ db: db, gethManager: gethManager, transactor: transactor, config: config, accountsDB: accountsDB, eventFeed: eventFeed, } } var ( emptyHash = common.Hash{} ) type MultiTransactionType uint8 const ( MultiTransactionSend = iota MultiTransactionSwap MultiTransactionBridge ) type MultiTransaction struct { ID uint `json:"id"` Timestamp uint64 `json:"timestamp"` FromNetworkID uint64 `json:"fromNetworkID"` ToNetworkID uint64 `json:"toNetworkID"` FromTxHash common.Hash `json:"fromTxHash"` ToTxHash common.Hash `json:"toTxHash"` FromAddress common.Address `json:"fromAddress"` ToAddress common.Address `json:"toAddress"` FromAsset string `json:"fromAsset"` ToAsset string `json:"toAsset"` FromAmount *hexutil.Big `json:"fromAmount"` ToAmount *hexutil.Big `json:"toAmount"` Type MultiTransactionType `json:"type"` CrossTxID string } type MultiTransactionCommand struct { FromAddress common.Address `json:"fromAddress"` ToAddress common.Address `json:"toAddress"` FromAsset string `json:"fromAsset"` ToAsset string `json:"toAsset"` FromAmount *hexutil.Big `json:"fromAmount"` Type MultiTransactionType `json:"type"` } type MultiTransactionCommandResult struct { ID int64 `json:"id"` Hashes map[uint64][]types.Hash `json:"hashes"` } type PendingTrxType string const ( RegisterENS PendingTrxType = "RegisterENS" ReleaseENS PendingTrxType = "ReleaseENS" SetPubKey PendingTrxType = "SetPubKey" BuyStickerPack PendingTrxType = "BuyStickerPack" WalletTransfer PendingTrxType = "WalletTransfer" CollectibleDeployment PendingTrxType = "CollectibleDeployment" CollectibleAirdrop PendingTrxType = "CollectibleAirdrop" CollectibleRemoteSelfDestruct PendingTrxType = "CollectibleRemoteSelfDestruct" CollectibleBurn PendingTrxType = "CollectibleBurn" ) type PendingTransaction struct { Hash common.Hash `json:"hash"` Timestamp uint64 `json:"timestamp"` Value bigint.BigInt `json:"value"` From common.Address `json:"from"` To common.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 uint64 `json:"network_id"` MultiTransactionID MultiTransactionIDType `json:"multi_transaction_id"` } type TransactionIdentity struct { ChainID wallet_common.ChainID `json:"chainId"` Hash common.Hash `json:"hash"` Address common.Address `json:"address"` } 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) 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)}, } 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, ) if err != nil { return nil, err } transactions = append(transactions, transaction) } return transactions, nil } func (tm *TransactionManager) GetAllPending(chainIDs []uint64) ([]*PendingTransaction, error) { if len(chainIDs) == 0 { return nil, errors.New("at least 1 chainID is required") } inVector := strings.Repeat("?, ", len(chainIDs)-1) + "?" var parameters []interface{} for _, c := range chainIDs { parameters = append(parameters, c) } rows, err := tm.db.Query(fmt.Sprintf(selectFromPending+"WHERE network_id in (%s)", inVector), parameters...) if err != nil { return nil, err } defer rows.Close() return rowsToTransactions(rows) } func (tm *TransactionManager) GetPendingByAddress(chainIDs []uint64, address common.Address) ([]*PendingTransaction, error) { if len(chainIDs) == 0 { return nil, errors.New("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 // TODO: consider using address also in case we expect to have also for the receiver func (tm *TransactionManager) GetPendingEntry(chainID uint64, hash common.Hash) (*PendingTransaction, error) { row := tm.db.QueryRow(`SELECT timestamp, value, from_address, to_address, data, symbol, gas_price, gas_limit, type, additional_data, network_id, COALESCE(multi_transaction_id, 0) FROM pending_transactions WHERE network_id = ? AND hash = ?`, chainID, hash) transaction := &PendingTransaction{ Hash: hash, Value: bigint.BigInt{Int: new(big.Int)}, GasPrice: bigint.BigInt{Int: new(big.Int)}, GasLimit: bigint.BigInt{Int: new(big.Int)}, ChainID: chainID, } err := row.Scan( &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, ) if err != nil { return nil, err } return transaction, nil } func (tm *TransactionManager) 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) 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, ) // Notify listeners of new pending transaction (used in activity history) if err == nil { tm.notifyPendingTransactionListeners(transaction.ChainID, []common.Address{transaction.From, transaction.To}, transaction.Timestamp) } return err } func (tm *TransactionManager) notifyPendingTransactionListeners(chainID uint64, addresses []common.Address, timestamp uint64) { if tm.eventFeed != nil { tm.eventFeed.Send(walletevent.Event{ Type: EventPendingTransactionUpdate, ChainID: chainID, Accounts: addresses, At: int64(timestamp), }) } } func (tm *TransactionManager) DeletePending(chainID uint64, hash common.Hash) error { tx, err := tm.db.BeginTx(context.Background(), nil) if err != nil { return err } defer func() { if err != nil { _ = tx.Rollback() } }() row := tx.QueryRow(`SELECT from_address, to_address, timestamp FROM pending_transactions WHERE network_id = ? AND hash = ?`, chainID, hash) var from, to common.Address var timestamp uint64 err = row.Scan(&from, &to, ×tamp) if err != nil { return err } _, err = tx.Exec(`DELETE FROM pending_transactions WHERE network_id = ? AND hash = ?`, chainID, hash) if err != nil { return err } err = tx.Commit() if err == nil { tm.notifyPendingTransactionListeners(chainID, []common.Address{from, to}, timestamp) } return err } func (tm *TransactionManager) Watch(ctx context.Context, transactionHash common.Hash, client *chain.ClientWithFallback) error { watchTxCommand := &watchTransactionCommand{ hash: transactionHash, client: client, } commandContext, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() return watchTxCommand.Command()(commandContext) } const multiTransactionColumns = "from_network_id, from_tx_hash, from_address, from_asset, from_amount, to_network_id, to_tx_hash, to_address, to_asset, to_amount, type, cross_tx_id, timestamp" func rowsToMultiTransactions(rows *sql.Rows) ([]*MultiTransaction, error) { var multiTransactions []*MultiTransaction for rows.Next() { multiTransaction := &MultiTransaction{} var fromAmountDB, toAmountDB sql.NullString err := rows.Scan( &multiTransaction.ID, &multiTransaction.FromNetworkID, &multiTransaction.FromTxHash, &multiTransaction.FromAddress, &multiTransaction.FromAsset, &fromAmountDB, &multiTransaction.ToNetworkID, &multiTransaction.ToTxHash, &multiTransaction.ToAddress, &multiTransaction.ToAsset, &toAmountDB, &multiTransaction.Type, &multiTransaction.CrossTxID, &multiTransaction.Timestamp, ) if err != nil { return nil, err } if fromAmountDB.Valid { multiTransaction.FromAmount = new(hexutil.Big) if _, ok := (*big.Int)(multiTransaction.FromAmount).SetString(fromAmountDB.String, 0); !ok { return nil, errors.New("failed to convert fromAmountDB.String to big.Int: " + fromAmountDB.String) } } if toAmountDB.Valid { multiTransaction.ToAmount = new(hexutil.Big) if _, ok := (*big.Int)(multiTransaction.ToAmount).SetString(toAmountDB.String, 0); !ok { return nil, errors.New("failed to convert fromAmountDB.String to big.Int: " + toAmountDB.String) } } multiTransactions = append(multiTransactions, multiTransaction) } return multiTransactions, nil } func insertMultiTransaction(db *sql.DB, multiTransaction *MultiTransaction) (MultiTransactionIDType, error) { insert, err := db.Prepare(fmt.Sprintf(`INSERT INTO multi_transactions (%s) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, multiTransactionColumns)) if err != nil { return 0, err } timestamp := time.Now().Unix() result, err := insert.Exec( multiTransaction.FromNetworkID, multiTransaction.FromTxHash, multiTransaction.FromAddress, multiTransaction.FromAsset, multiTransaction.FromAmount.String(), multiTransaction.ToNetworkID, multiTransaction.ToTxHash, multiTransaction.ToAddress, multiTransaction.ToAsset, multiTransaction.ToAmount.String(), multiTransaction.Type, multiTransaction.CrossTxID, timestamp, ) if err != nil { return 0, err } defer insert.Close() multiTransactionID, err := result.LastInsertId() multiTransaction.Timestamp = uint64(timestamp) multiTransaction.ID = uint(multiTransactionID) return MultiTransactionIDType(multiTransactionID), err } func (tm *TransactionManager) InsertMultiTransaction(multiTransaction *MultiTransaction) (MultiTransactionIDType, error) { return insertMultiTransaction(tm.db, multiTransaction) } func updateMultiTransaction(db *sql.DB, multiTransaction *MultiTransaction) error { if MultiTransactionIDType(multiTransaction.ID) == NoMultiTransactionID { return fmt.Errorf("no multitransaction ID") } update, err := db.Prepare(fmt.Sprintf(`REPLACE INTO multi_transactions (rowid, %s) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, multiTransactionColumns)) if err != nil { return err } _, err = update.Exec( multiTransaction.ID, multiTransaction.FromNetworkID, multiTransaction.FromTxHash, multiTransaction.FromAddress, multiTransaction.FromAsset, multiTransaction.FromAmount.String(), multiTransaction.ToNetworkID, multiTransaction.ToTxHash, multiTransaction.ToAddress, multiTransaction.ToAsset, multiTransaction.ToAmount.String(), multiTransaction.Type, multiTransaction.CrossTxID, time.Now().Unix(), ) if err != nil { return err } return update.Close() } func (tm *TransactionManager) UpdateMultiTransaction(multiTransaction *MultiTransaction) error { return updateMultiTransaction(tm.db, multiTransaction) } func (tm *TransactionManager) CreateMultiTransactionFromCommand(ctx context.Context, command *MultiTransactionCommand, data []*bridge.TransactionBridge, bridges map[string]bridge.Bridge, password string) (*MultiTransactionCommandResult, error) { multiTransaction := &MultiTransaction{ FromAddress: command.FromAddress, ToAddress: command.ToAddress, FromAsset: command.FromAsset, ToAsset: command.ToAsset, FromAmount: command.FromAmount, ToAmount: new(hexutil.Big), Type: command.Type, } selectedAccount, err := tm.getVerifiedWalletAccount(multiTransaction.FromAddress.Hex(), password) if err != nil { return nil, err } multiTransactionID, err := insertMultiTransaction(tm.db, multiTransaction) if err != nil { return nil, err } hashes := make(map[uint64][]types.Hash) for _, tx := range data { hash, err := bridges[tx.BridgeName].Send(tx, selectedAccount) if err != nil { return nil, err } pendingTransaction := PendingTransaction{ Hash: common.Hash(hash), Timestamp: uint64(time.Now().Unix()), Value: bigint.BigInt{Int: multiTransaction.FromAmount.ToInt()}, From: common.Address(tx.From()), To: common.Address(tx.To()), Data: tx.Data().String(), Type: WalletTransfer, ChainID: tx.ChainID, MultiTransactionID: multiTransactionID, Symbol: multiTransaction.FromAsset, } err = tm.AddPending(pendingTransaction) if err != nil { return nil, err } hashes[tx.ChainID] = append(hashes[tx.ChainID], hash) } return &MultiTransactionCommandResult{ ID: int64(multiTransactionID), Hashes: hashes, }, nil } func (tm *TransactionManager) GetMultiTransactions(ctx context.Context, ids []MultiTransactionIDType) ([]*MultiTransaction, error) { placeholders := make([]string, len(ids)) args := make([]interface{}, len(ids)) for i, v := range ids { placeholders[i] = "?" args[i] = v } stmt, err := tm.db.Prepare(fmt.Sprintf(`SELECT rowid, %s FROM multi_transactions WHERE rowid in (%s)`, multiTransactionColumns, strings.Join(placeholders, ","))) if err != nil { return nil, err } defer stmt.Close() rows, err := stmt.Query(args...) if err != nil { return nil, err } defer rows.Close() return rowsToMultiTransactions(rows) } func (tm *TransactionManager) getBridgeMultiTransactions(ctx context.Context, toChainID uint64, crossTxID string) ([]*MultiTransaction, error) { stmt, err := tm.db.Prepare(fmt.Sprintf(`SELECT rowid, %s FROM multi_transactions WHERE type=? AND to_network_id=? AND cross_tx_id=?`, multiTransactionColumns)) if err != nil { return nil, err } defer stmt.Close() rows, err := stmt.Query(MultiTransactionBridge, toChainID, crossTxID) if err != nil { return nil, err } defer rows.Close() return rowsToMultiTransactions(rows) } func (tm *TransactionManager) GetBridgeOriginMultiTransaction(ctx context.Context, toChainID uint64, crossTxID string) (*MultiTransaction, error) { multiTxs, err := tm.getBridgeMultiTransactions(ctx, toChainID, crossTxID) if err != nil { return nil, err } for _, multiTx := range multiTxs { // Origin MultiTxs will have a missing "ToTxHash" if multiTx.ToTxHash == emptyHash { return multiTx, nil } } return nil, nil } func (tm *TransactionManager) GetBridgeDestinationMultiTransaction(ctx context.Context, toChainID uint64, crossTxID string) (*MultiTransaction, error) { multiTxs, err := tm.getBridgeMultiTransactions(ctx, toChainID, crossTxID) if err != nil { return nil, err } for _, multiTx := range multiTxs { // Destination MultiTxs will have a missing "FromTxHash" if multiTx.FromTxHash == emptyHash { return multiTx, nil } } return nil, nil } func (tm *TransactionManager) getVerifiedWalletAccount(address, password string) (*account.SelectedExtKey, error) { exists, err := tm.accountsDB.AddressExists(types.HexToAddress(address)) if err != nil { log.Error("failed to query db for a given address", "address", address, "error", err) return nil, err } if !exists { log.Error("failed to get a selected account", "err", transactions.ErrInvalidTxSender) return nil, transactions.ErrAccountDoesntExist } key, err := tm.gethManager.VerifyAccountPassword(tm.config.KeyStoreDir, address, password) if err != nil { log.Error("failed to verify account", "account", address, "error", err) return nil, err } return &account.SelectedExtKey{ Address: key.Address, AccountKey: key, }, nil } type watchTransactionCommand struct { client *chain.ClientWithFallback hash common.Hash } func (c *watchTransactionCommand) Command() async.Command { return async.FiniteCommand{ Interval: 10 * time.Second, Runable: c.Run, }.Run } func (c *watchTransactionCommand) Run(ctx context.Context) error { requestContext, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() _, isPending, err := c.client.TransactionByHash(requestContext, c.hash) if err != nil { log.Error("Watching transaction error", "error", err) return err } if isPending { return errors.New("transaction is pending") } return nil }