diff --git a/services/wallet/activity/activity.go b/services/wallet/activity/activity.go index 627a8ebb3..cc1fcf415 100644 --- a/services/wallet/activity/activity.go +++ b/services/wallet/activity/activity.go @@ -48,61 +48,58 @@ const ( ) type Entry struct { - payloadType PayloadType - transaction *transfer.TransactionIdentity - id transfer.MultiTransactionIDType - timestamp int64 - activityType Type - activityStatus Status - amountOut *hexutil.Big // Used for activityType SendAT, SwapAT, BridgeAT - amountIn *hexutil.Big // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT - tokenOut *Token // Used for activityType SendAT, SwapAT, BridgeAT - tokenIn *Token // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT - sender *eth.Address - recipient *eth.Address - chainIDOut *common.ChainID - chainIDIn *common.ChainID - transferType *TransferType - contractAddress *eth.Address + payloadType PayloadType + transaction *transfer.TransactionIdentity + id transfer.MultiTransactionIDType + timestamp int64 + activityType Type + activityStatus Status + amountOut *hexutil.Big // Used for activityType SendAT, SwapAT, BridgeAT + amountIn *hexutil.Big // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT + tokenOut *Token // Used for activityType SendAT, SwapAT, BridgeAT + tokenIn *Token // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT + sender *eth.Address + recipient *eth.Address + chainIDOut *common.ChainID + chainIDIn *common.ChainID + transferType *TransferType } type jsonSerializationTemplate struct { - PayloadType PayloadType `json:"payloadType"` - Transaction *transfer.TransactionIdentity `json:"transaction"` - ID transfer.MultiTransactionIDType `json:"id"` - Timestamp int64 `json:"timestamp"` - ActivityType Type `json:"activityType"` - ActivityStatus Status `json:"activityStatus"` - AmountOut *hexutil.Big `json:"amountOut"` - AmountIn *hexutil.Big `json:"amountIn"` - TokenOut *Token `json:"tokenOut,omitempty"` - TokenIn *Token `json:"tokenIn,omitempty"` - Sender *eth.Address `json:"sender,omitempty"` - Recipient *eth.Address `json:"recipient,omitempty"` - ChainIDOut *common.ChainID `json:"chainIdOut,omitempty"` - ChainIDIn *common.ChainID `json:"chainIdIn,omitempty"` - TransferType *TransferType `json:"transferType,omitempty"` - ContractAddress *eth.Address `json:"contractAddress,omitempty"` + PayloadType PayloadType `json:"payloadType"` + Transaction *transfer.TransactionIdentity `json:"transaction"` + ID transfer.MultiTransactionIDType `json:"id"` + Timestamp int64 `json:"timestamp"` + ActivityType Type `json:"activityType"` + ActivityStatus Status `json:"activityStatus"` + AmountOut *hexutil.Big `json:"amountOut"` + AmountIn *hexutil.Big `json:"amountIn"` + TokenOut *Token `json:"tokenOut,omitempty"` + TokenIn *Token `json:"tokenIn,omitempty"` + Sender *eth.Address `json:"sender,omitempty"` + Recipient *eth.Address `json:"recipient,omitempty"` + ChainIDOut *common.ChainID `json:"chainIdOut,omitempty"` + ChainIDIn *common.ChainID `json:"chainIdIn,omitempty"` + TransferType *TransferType `json:"transferType,omitempty"` } func (e *Entry) MarshalJSON() ([]byte, error) { return json.Marshal(jsonSerializationTemplate{ - PayloadType: e.payloadType, - Transaction: e.transaction, - ID: e.id, - Timestamp: e.timestamp, - ActivityType: e.activityType, - ActivityStatus: e.activityStatus, - AmountOut: e.amountOut, - AmountIn: e.amountIn, - TokenOut: e.tokenOut, - TokenIn: e.tokenIn, - Sender: e.sender, - Recipient: e.recipient, - ChainIDOut: e.chainIDOut, - ChainIDIn: e.chainIDIn, - TransferType: e.transferType, - ContractAddress: e.contractAddress, + PayloadType: e.payloadType, + Transaction: e.transaction, + ID: e.id, + Timestamp: e.timestamp, + ActivityType: e.activityType, + ActivityStatus: e.activityStatus, + AmountOut: e.amountOut, + AmountIn: e.amountIn, + TokenOut: e.tokenOut, + TokenIn: e.tokenIn, + Sender: e.sender, + Recipient: e.recipient, + ChainIDOut: e.chainIDOut, + ChainIDIn: e.chainIDIn, + TransferType: e.transferType, }) } @@ -128,7 +125,6 @@ func (e *Entry) UnmarshalJSON(data []byte) error { e.chainIDOut = aux.ChainIDOut e.chainIDIn = aux.ChainIDIn e.transferType = aux.TransferType - e.contractAddress = aux.ContractAddress return nil } @@ -847,7 +843,6 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses entry.chainIDOut = outChainID entry.chainIDIn = inChainID entry.transferType = transferType - entry.contractAddress = contractAddress entries = append(entries, entry) } diff --git a/services/wallet/activity/activity_test.go b/services/wallet/activity/activity_test.go index b43b1740e..69e26ffbb 100644 --- a/services/wallet/activity/activity_test.go +++ b/services/wallet/activity/activity_test.go @@ -1115,6 +1115,46 @@ func TestGetActivityEntriesNullAddresses(t *testing.T) { require.Equal(t, 3, len(activities)) } +func TestGetTxDetails(t *testing.T) { + deps, close := setupTestActivityDB(t) + defer close() + + // Adds 4 extractable transactions 2 transactions (ETH/Goerli, ETH/Optimism), one MT USDC to DAI and another MT USDC to SNT + td, _, _ := fillTestData(t, deps.db) + + _, err := getTxDetails(context.Background(), deps.db, "") + require.EqualError(t, err, "invalid tx id") + + details, err := getTxDetails(context.Background(), deps.db, td.tr1.Hash.String()) + require.NoError(t, err) + + require.Equal(t, td.tr1.Hash.String(), details.ID) + require.Equal(t, 0, details.MultiTxID) + require.Equal(t, td.tr1.Nonce, details.Nonce) + require.Equal(t, td.tr1.BlkNumber, details.BlockNumber) + require.Equal(t, td.tr1.Contract, *details.Contract) +} + +func TestGetMultiTxDetails(t *testing.T) { + deps, close := setupTestActivityDB(t) + defer close() + + // Adds 4 extractable transactions 2 transactions (ETH/Goerli, ETH/Optimism), one MT USDC to DAI and another MT USDC to SNT + td, _, _ := fillTestData(t, deps.db) + + _, err := getMultiTxDetails(context.Background(), deps.db, 0) + require.EqualError(t, err, "invalid tx id") + + details, err := getMultiTxDetails(context.Background(), deps.db, int(td.multiTx1.MultiTransactionID)) + require.NoError(t, err) + + require.Equal(t, "", details.ID) + require.Equal(t, int(td.multiTx1.MultiTransactionID), details.MultiTxID) + require.Equal(t, td.multiTx1Tr2.Nonce, details.Nonce) + require.Equal(t, td.multiTx1Tr2.BlkNumber, details.BlockNumber) + require.Equal(t, td.multiTx1Tr1.Contract, *details.Contract) +} + func setupBenchmark(b *testing.B, inMemory bool, resultCount int) (deps FilterDependencies, close func(), accounts []eth.Address) { deps, close = setupTestActivityDBStorageChoice(b, inMemory) diff --git a/services/wallet/activity/details.go b/services/wallet/activity/details.go new file mode 100644 index 000000000..e694df0cf --- /dev/null +++ b/services/wallet/activity/details.go @@ -0,0 +1,195 @@ +package activity + +import ( + "context" + "database/sql" + "encoding/hex" + "errors" + + eth "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/sqlite" +) + +type ProtocolType = int + +const ( + ProtocolHop ProtocolType = iota + 1 + ProtocolUniswap +) + +type EntryDetails struct { + ID string `json:"id"` + MultiTxID int `json:"multiTxId"` + Nonce uint64 `json:"nonce"` + BlockNumber int64 `json:"blockNumber"` + Input string `json:"input"` + ProtocolType *ProtocolType `json:"protocolType,omitempty"` + Hash *eth.Hash `json:"hash,omitempty"` + Contract *eth.Address `json:"contractAddress,omitempty"` + MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` + GasLimit hexutil.Uint64 `json:"gasLimit"` +} + +func protocolTypeFromDBType(dbType string) (protocolType *ProtocolType) { + protocolType = new(ProtocolType) + switch common.Type(dbType) { + case common.UniswapV2Swap: + fallthrough + case common.UniswapV3Swap: + *protocolType = ProtocolUniswap + case common.HopBridgeFrom: + fallthrough + case common.HopBridgeTo: + *protocolType = ProtocolHop + default: + return nil + } + return protocolType +} + +func getMultiTxDetails(ctx context.Context, db *sql.DB, multiTxID int) (*EntryDetails, error) { + if multiTxID <= 0 { + return nil, errors.New("invalid tx id") + } + rows, err := db.QueryContext(ctx, ` + SELECT + tx_hash, + blk_number, + type, + account_nonce, + tx, + contract_address + FROM + transfers + WHERE + multi_transaction_id = ?;`, multiTxID) + if err != nil { + return nil, err + } + defer rows.Close() + + var maxFeePerGas *hexutil.Big + var gasLimit hexutil.Uint64 + var input string + var protocolType *ProtocolType + var transferHash *eth.Hash + var contractAddress *eth.Address + var blockNumber int64 + var nonce uint64 + for rows.Next() { + var contractTypeDB sql.NullString + var transferHashDB, contractAddressDB sql.RawBytes + var blockNumberDB int64 + var nonceDB uint64 + tx := &types.Transaction{} + nullableTx := sqlite.JSONBlob{Data: tx} + err := rows.Scan(&transferHashDB, &blockNumberDB, &contractTypeDB, &nonceDB, &nullableTx, &contractAddressDB) + if err != nil { + return nil, err + } + if len(transferHashDB) > 0 { + transferHash = new(eth.Hash) + *transferHash = eth.BytesToHash(transferHashDB) + } + if contractTypeDB.Valid && protocolType == nil { + protocolType = protocolTypeFromDBType(contractTypeDB.String) + } + + if blockNumberDB > 0 { + blockNumber = blockNumberDB + } + if nonceDB > 0 { + nonce = nonceDB + } + if len(input) == 0 && nullableTx.Valid { + if len(input) == 0 { + input = "0x" + hex.EncodeToString(tx.Data()) + } + if maxFeePerGas == nil { + maxFeePerGas = (*hexutil.Big)(tx.GasFeeCap()) + gasLimit = hexutil.Uint64(tx.Gas()) + } + } + + if contractAddress == nil && len(contractAddressDB) > 0 { + contractAddress = new(eth.Address) + *contractAddress = eth.BytesToAddress(contractAddressDB) + } + } + if err = rows.Err(); err != nil { + return nil, err + } + return &EntryDetails{ + MultiTxID: multiTxID, + Nonce: nonce, + BlockNumber: blockNumber, + Hash: transferHash, + ProtocolType: protocolType, + Input: input, + Contract: contractAddress, + MaxFeePerGas: maxFeePerGas, + GasLimit: gasLimit, + }, nil +} + +func getTxDetails(ctx context.Context, db *sql.DB, id string) (*EntryDetails, error) { + if len(id) == 0 { + return nil, errors.New("invalid tx id") + } + rows, err := db.QueryContext(ctx, ` + SELECT + tx_hash, + blk_number, + account_nonce, + tx, + contract_address + FROM + transfers + WHERE + hash = ?;`, eth.HexToHash(id)) + if err != nil { + return nil, err + } + defer rows.Close() + + if !rows.Next() { + return nil, errors.New("Entry not found") + } + + tx := &types.Transaction{} + nullableTx := sqlite.JSONBlob{Data: tx} + var transferHashDB, contractAddressDB sql.RawBytes + var blockNumber int64 + var nonce uint64 + err = rows.Scan(&transferHashDB, &blockNumber, &nonce, &nullableTx, &contractAddressDB) + if err != nil { + return nil, err + } + + details := &EntryDetails{ + ID: id, + Nonce: nonce, + BlockNumber: blockNumber, + } + + if len(transferHashDB) > 0 { + details.Hash = new(eth.Hash) + *details.Hash = eth.BytesToHash(transferHashDB) + } + + if len(contractAddressDB) > 0 { + details.Contract = new(eth.Address) + *details.Contract = eth.BytesToAddress(contractAddressDB) + } + + if nullableTx.Valid { + details.Input = "0x" + hex.EncodeToString(tx.Data()) + details.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap()) + details.GasLimit = hexutil.Uint64(tx.Gas()) + } + + return details, nil +} diff --git a/services/wallet/activity/service.go b/services/wallet/activity/service.go index 8c2800abb..db5a39881 100644 --- a/services/wallet/activity/service.go +++ b/services/wallet/activity/service.go @@ -101,6 +101,14 @@ func (s *Service) FilterActivityAsync(requestID int32, addresses []common.Addres }) } +func (s *Service) GetMultiTxDetails(ctx context.Context, multiTxID int) (*EntryDetails, error) { + return getMultiTxDetails(ctx, s.db, multiTxID) +} + +func (s *Service) GetTxDetails(ctx context.Context, id string) (*EntryDetails, error) { + return getTxDetails(ctx, s.db, id) +} + type GetRecipientsResponse struct { Addresses []common.Address `json:"addresses"` Offset int `json:"offset"` diff --git a/services/wallet/api.go b/services/wallet/api.go index 612c238e8..aab872a45 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -547,6 +547,18 @@ func (api *API) FilterActivityAsync(requestID int32, addresses []common.Address, return nil } +func (api *API) GetMultiTxDetails(ctx context.Context, multiTxID int) (*activity.EntryDetails, error) { + log.Debug("wallet.api.GetMultiTxDetails", "multiTxID", multiTxID) + + return api.s.activity.GetMultiTxDetails(ctx, multiTxID) +} + +func (api *API) GetTxDetails(ctx context.Context, id string) (*activity.EntryDetails, error) { + log.Debug("wallet.api.GetTxDetails", "id", id) + + return api.s.activity.GetTxDetails(ctx, id) +} + func (api *API) GetRecipientsAsync(requestID int32, offset int, limit int) (ignored bool, err error) { log.Debug("wallet.api.GetRecipientsAsync", "offset", offset, "limit", limit) diff --git a/services/wallet/transfer/testutils.go b/services/wallet/transfer/testutils.go index de46c5211..655206c63 100644 --- a/services/wallet/transfer/testutils.go +++ b/services/wallet/transfer/testutils.go @@ -23,6 +23,8 @@ type TestTransaction struct { Timestamp int64 BlkNumber int64 Success bool + Nonce uint64 + Contract eth_common.Address MultiTransactionID MultiTransactionIDType } @@ -69,6 +71,8 @@ func generateTestTransaction(seed int) TestTransaction { Timestamp: int64(seed), BlkNumber: int64(seed), Success: true, + Nonce: uint64(seed), + Contract: eth_common.HexToAddress(fmt.Sprintf("0x2%d", seed)), MultiTransactionID: NoMultiTransactionID, } } @@ -281,7 +285,9 @@ func InsertTestTransferWithOptions(tb testing.TB, db *sql.DB, address eth_common txValue: big.NewInt(tr.Value), txFrom: txFrom, txTo: txTo, + txNonce: &tr.Nonce, tokenAddress: &opt.TokenAddress, + contractAddress: &tr.Contract, } err = updateOrInsertTransfersDBFields(tx, []transferDBFields{transfer}) require.NoError(tb, err)