diff --git a/services/communitytokens/api.go b/services/communitytokens/api.go index 9bbee3ac9..555276464 100644 --- a/services/communitytokens/api.go +++ b/services/communitytokens/api.go @@ -130,6 +130,7 @@ func (api *API) DeployCollectibles(ctx context.Context, chainID uint64, deployme transactions.DeployCommunityToken, transactions.Keep, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -242,6 +243,7 @@ func (api *API) DeployOwnerToken(ctx context.Context, chainID uint64, transactions.DeployOwnerToken, transactions.Keep, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -305,6 +307,7 @@ func (api *API) DeployAssets(ctx context.Context, chainID uint64, deploymentPara transactions.DeployCommunityToken, transactions.Keep, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -402,6 +405,7 @@ func (api *API) MintTokens(ctx context.Context, chainID uint64, contractAddress transactions.AirdropCommunityToken, transactions.Keep, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -469,6 +473,7 @@ func (api *API) RemoteBurn(ctx context.Context, chainID uint64, contractAddress transactions.RemoteDestructCollectible, transactions.Keep, additionalData, + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -521,6 +526,7 @@ func (api *API) Burn(ctx context.Context, chainID uint64, contractAddress string transactions.BurnCommunityToken, transactions.Keep, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) diff --git a/services/communitytokens/service.go b/services/communitytokens/service.go index 598eaa1fb..27ee3a079 100644 --- a/services/communitytokens/service.go +++ b/services/communitytokens/service.go @@ -526,6 +526,7 @@ func (s *Service) SetSignerPubKey(ctx context.Context, chainID uint64, contractA transactions.SetSignerPublicKey, transactions.Keep, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -711,17 +712,19 @@ func (s *Service) ReTrackOwnerTokenDeploymentTransaction(ctx context.Context, ch transactionType = transactions.DeployOwnerToken } + deployerAddress := common.HexToAddress(communityToken.Deployer) _, err = s.pendingTracker.GetPendingEntry(wcommon.ChainID(chainID), common.HexToHash(hashString)) if errors.Is(err, sql.ErrNoRows) { // start only if no pending transaction in database err = s.pendingTracker.TrackPendingTransaction( wcommon.ChainID(chainID), common.HexToHash(hashString), - common.HexToAddress(communityToken.Deployer), + deployerAddress, common.Address{}, transactionType, transactions.Keep, "", + &deployerAddress, ) log.Debug("retracking pending transaction with hashId ", hashString) } else { diff --git a/services/ens/api.go b/services/ens/api.go index dcd64b06e..a586b3e2f 100644 --- a/services/ens/api.go +++ b/services/ens/api.go @@ -361,6 +361,7 @@ func (api *API) Release(ctx context.Context, chainID uint64, txArgs transactions transactions.ReleaseENS, transactions.AutoDelete, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -490,6 +491,7 @@ func (api *API) Register(ctx context.Context, chainID uint64, txArgs transaction transactions.RegisterENS, transactions.AutoDelete, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) @@ -608,6 +610,7 @@ func (api *API) SetPubKey(ctx context.Context, chainID uint64, txArgs transactio transactions.SetPubKey, transactions.AutoDelete, "", + tx.To(), ) if err != nil { log.Error("TrackPendingTransaction error", "error", err) diff --git a/services/wallet/activity/activity.go b/services/wallet/activity/activity.go index 1c870ec3d..321a8a4fe 100644 --- a/services/wallet/activity/activity.go +++ b/services/wallet/activity/activity.go @@ -50,50 +50,52 @@ const ( ) type Entry struct { - payloadType PayloadType - transaction *transfer.TransactionIdentity - id common.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, ApproveAT - tokenOut *Token // Used for activityType SendAT, SwapAT, BridgeAT - tokenIn *Token // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT, ApproveAT - symbolOut *string - symbolIn *string - sender *eth.Address - recipient *eth.Address - chainIDOut *common.ChainID - chainIDIn *common.ChainID - transferType *TransferType - contractAddress *eth.Address - communityID *string + payloadType PayloadType + transaction *transfer.TransactionIdentity + id common.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, ApproveAT + tokenOut *Token // Used for activityType SendAT, SwapAT, BridgeAT + tokenIn *Token // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT, ApproveAT + symbolOut *string + symbolIn *string + sender *eth.Address + recipient *eth.Address + chainIDOut *common.ChainID + chainIDIn *common.ChainID + transferType *TransferType + deployedContractAddress *eth.Address + communityID *string + interactedContractAddress *eth.Address isNew bool // isNew is used to indicate if the entry is newer than session start (changed state also) } // Only used for JSON marshalling type EntryData struct { - PayloadType PayloadType `json:"payloadType"` - Transaction *transfer.TransactionIdentity `json:"transaction,omitempty"` - ID *common.MultiTransactionIDType `json:"id,omitempty"` - Timestamp *int64 `json:"timestamp,omitempty"` - ActivityType *Type `json:"activityType,omitempty"` - ActivityStatus *Status `json:"activityStatus,omitempty"` - AmountOut *hexutil.Big `json:"amountOut,omitempty"` - AmountIn *hexutil.Big `json:"amountIn,omitempty"` - TokenOut *Token `json:"tokenOut,omitempty"` - TokenIn *Token `json:"tokenIn,omitempty"` - SymbolOut *string `json:"symbolOut,omitempty"` - SymbolIn *string `json:"symbolIn,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"` - CommunityID *string `json:"communityId,omitempty"` + PayloadType PayloadType `json:"payloadType"` + Transaction *transfer.TransactionIdentity `json:"transaction,omitempty"` + ID *common.MultiTransactionIDType `json:"id,omitempty"` + Timestamp *int64 `json:"timestamp,omitempty"` + ActivityType *Type `json:"activityType,omitempty"` + ActivityStatus *Status `json:"activityStatus,omitempty"` + AmountOut *hexutil.Big `json:"amountOut,omitempty"` + AmountIn *hexutil.Big `json:"amountIn,omitempty"` + TokenOut *Token `json:"tokenOut,omitempty"` + TokenIn *Token `json:"tokenIn,omitempty"` + SymbolOut *string `json:"symbolOut,omitempty"` + SymbolIn *string `json:"symbolIn,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"` + DeployedContractAddress *eth.Address `json:"contractAddress,omitempty"` + CommunityID *string `json:"communityId,omitempty"` + InteractedContractAddress *eth.Address `json:"interactedContractAddress,omitempty"` IsNew *bool `json:"isNew,omitempty"` @@ -103,22 +105,23 @@ type EntryData struct { func (e *Entry) MarshalJSON() ([]byte, error) { data := EntryData{ - Timestamp: &e.timestamp, - ActivityType: &e.activityType, - ActivityStatus: &e.activityStatus, - AmountOut: e.amountOut, - AmountIn: e.amountIn, - TokenOut: e.tokenOut, - TokenIn: e.tokenIn, - SymbolOut: e.symbolOut, - SymbolIn: e.symbolIn, - Sender: e.sender, - Recipient: e.recipient, - ChainIDOut: e.chainIDOut, - ChainIDIn: e.chainIDIn, - TransferType: e.transferType, - ContractAddress: e.contractAddress, - CommunityID: e.communityID, + Timestamp: &e.timestamp, + ActivityType: &e.activityType, + ActivityStatus: &e.activityStatus, + AmountOut: e.amountOut, + AmountIn: e.amountIn, + TokenOut: e.tokenOut, + TokenIn: e.tokenIn, + SymbolOut: e.symbolOut, + SymbolIn: e.symbolIn, + Sender: e.sender, + Recipient: e.recipient, + ChainIDOut: e.chainIDOut, + ChainIDIn: e.chainIDIn, + TransferType: e.transferType, + DeployedContractAddress: e.deployedContractAddress, + CommunityID: e.communityID, + InteractedContractAddress: e.interactedContractAddress, } if e.payloadType == MultiTransactionPT { @@ -486,6 +489,7 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses startFilterDisabled, filter.Period.StartTimestamp, endFilterDisabled, filter.Period.EndTimestamp, filterActivityTypeAll, sliceContains(filter.Types, SendAT), sliceContains(filter.Types, ReceiveAT), sliceContains(filter.Types, ContractDeploymentAT), sliceContains(filter.Types, MintAT), + sliceContains(filter.Types, SwapAT), sliceContains(filter.Types, ApproveAT), transfer.MultiTransactionSend, fromTrType, toTrType, allAddresses, filterAllToAddresses, @@ -511,21 +515,36 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses var timestamp int64 var dbMtType, dbTrType sql.NullByte var toAddress, fromAddress eth.Address - var toAddressDB, ownerAddressDB, contractAddressDB, dbTokenID sql.RawBytes - var tokenAddress, contractAddress *eth.Address + var toAddressDB, ownerAddressDB, deployedContractAddressDB, dbTokenID sql.RawBytes + var tokenAddress, deployedContractAddress, interactedContractAddress *eth.Address var aggregatedStatus int var dbTrAmount sql.NullString dbPTrAmount := new(big.Int) - var dbMtFromAmount, dbMtToAmount, contractType sql.NullString + var dbMtFromAmount, dbMtToAmount, dbContractType sql.NullString + var contractType string var tokenCode, fromTokenCode, toTokenCode sql.NullString var methodHash, communityID sql.NullString var transferType *TransferType var communityMintEventDB sql.NullBool var communityMintEvent bool + var dbInteractedContractAddress sql.RawBytes + // Transaction Input parameters + var dbTrInputApprovalSpender sql.RawBytes + var dbTrInputApprovalAmount = new(big.Int) + var dbTrInputFromAsset sql.NullString + var dbTrInputFromAmount = new(big.Int) + var dbTrInputToAsset sql.NullString + var dbTrInputToAmount = new(big.Int) + var dbTrInputSide sql.NullInt64 + var dbTrInputSlippageBps sql.NullInt64 + err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, ×tamp, &dbMtType, &dbTrType, &fromAddress, &toAddressDB, &ownerAddressDB, &dbTrAmount, (*bigint.SQLBigIntBytes)(dbPTrAmount), &dbMtFromAmount, &dbMtToAmount, &aggregatedStatus, &aggregatedCount, - &tokenAddress, &dbTokenID, &tokenCode, &fromTokenCode, &toTokenCode, &outChainIDDB, &inChainIDDB, &contractType, - &contractAddressDB, &methodHash, &communityMintEventDB, &communityID) + &tokenAddress, &dbTokenID, &tokenCode, &fromTokenCode, &toTokenCode, &outChainIDDB, &inChainIDDB, &dbContractType, + &deployedContractAddressDB, &methodHash, &communityMintEventDB, &communityID, + &dbTrInputApprovalSpender, (*bigint.SQLBigIntBytes)(dbTrInputApprovalAmount), &dbTrInputFromAsset, (*bigint.SQLBigIntBytes)(dbTrInputFromAmount), + &dbTrInputToAsset, (*bigint.SQLBigIntBytes)(dbTrInputToAmount), &dbTrInputSide, &dbTrInputSlippageBps, + &dbInteractedContractAddress) if err != nil { return nil, err } @@ -534,24 +553,32 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses toAddress = eth.BytesToAddress(toAddressDB) } - if contractType.Valid { - transferType = contractTypeFromDBType(contractType.String) + if dbContractType.Valid { + contractType = dbContractType.String + transferType = contractTypeFromDBType(contractType) } if communityMintEventDB.Valid { communityMintEvent = communityMintEventDB.Bool } - if len(contractAddressDB) > 0 { - contractAddress = new(eth.Address) - *contractAddress = eth.BytesToAddress(contractAddressDB) + if len(deployedContractAddressDB) > 0 { + deployedContractAddress = new(eth.Address) + *deployedContractAddress = eth.BytesToAddress(deployedContractAddressDB) + } + + if len(dbInteractedContractAddress) > 0 { + interactedContractAddress := new(eth.Address) + *interactedContractAddress = eth.BytesToAddress(dbInteractedContractAddress) } getActivityType := func(trType sql.NullByte) (activityType Type, filteredAddress eth.Address) { if trType.Valid { if trType.Byte == fromTrType { - if toAddress == ZeroAddress && transferType != nil && *transferType == TransferTypeEth && contractAddress != nil && *contractAddress != ZeroAddress { + if toAddress == ZeroAddress && transferType != nil && *transferType == TransferTypeEth && deployedContractAddress != nil && *deployedContractAddress != ZeroAddress { return ContractDeploymentAT, fromAddress + } else if contractType == string(common.Erc20Approval) { + return ApproveAT, fromAddress } return SendAT, fromAddress } else if trType.Byte == toTrType { @@ -624,15 +651,12 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses entry.amountIn = inAmount } else if pendingHash != nil && chainID.Valid { // Process `pending_transactions` row - - // Extract activity type: SendAT/ReceiveAT - activityType, _ := getActivityType(dbTrType) - - inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount, dbPTrAmount) - outChainID = new(common.ChainID) *outChainID = common.ChainID(chainID.Int64) + // Extract activity type: SendAT/ReceiveAT/ApproveAT + activityType, _ := getActivityType(dbTrType) + entry = newActivityEntryWithPendingTransaction( &transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64), Hash: eth.BytesToHash(pendingHash), @@ -640,17 +664,36 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses timestamp, activityType, activityStatus, ) - // Extract tokens - if tokenCode.Valid { - cID := common.ChainID(chainID.Int64) - entry.tokenOut = deps.tokenFromSymbol(&cID, tokenCode.String) + if activityType == ApproveAT { + // Extract Approve token + if dbTrInputFromAsset.Valid { + entry.tokenOut = deps.tokenFromSymbol(outChainID, dbTrInputFromAsset.String) + entry.symbolOut = common.NewAndSet(dbTrInputFromAsset.String) + } + + // Extract Approve amount + if dbTrInputApprovalAmount != nil { + entry.amountOut = (*hexutil.Big)(dbTrInputApprovalAmount) + } + + // Extract Approve spender + if len(dbTrInputApprovalSpender) > 0 { + toAddress = eth.BytesToAddress(dbTrInputApprovalSpender) + } + } else { + inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount, dbPTrAmount) + + // Extract tokens + if tokenCode.Valid { + cID := common.ChainID(chainID.Int64) + entry.tokenOut = deps.tokenFromSymbol(&cID, tokenCode.String) + } + entry.symbolOut, entry.symbolIn = lookupAndFillInTokens(deps, entry.tokenOut, nil) + + // Complete the data + entry.amountOut = outAmount + entry.amountIn = inAmount } - entry.symbolOut, entry.symbolIn = lookupAndFillInTokens(deps, entry.tokenOut, nil) - - // Complete the data - entry.amountOut = outAmount - entry.amountIn = inAmount - } else if multiTxID.Valid { // Process `multi_transactions` row @@ -693,12 +736,12 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses } // Complete common data - entry.recipient = &toAddress entry.sender = &fromAddress entry.recipient = &toAddress entry.chainIDOut = outChainID entry.chainIDIn = inChainID entry.transferType = transferType + entry.interactedContractAddress = interactedContractAddress entries = append(entries, entry) } diff --git a/services/wallet/activity/activity_test.go b/services/wallet/activity/activity_test.go index 03fd9b0fb..7098ecc57 100644 --- a/services/wallet/activity/activity_test.go +++ b/services/wallet/activity/activity_test.go @@ -675,12 +675,14 @@ func TestGetActivityEntriesFilterByType(t *testing.T) { // Add 6 extractable transactions: one MultiTransactionSwap, two MultiTransactionBridge, two MultiTransactionSend and one MultiTransactionApprove multiTxs := make([]transfer.MultiTransaction, 6) trs, fromAddrs, toAddrs := transfer.GenerateTestTransfers(t, deps.db, td.nextIndex, len(multiTxs)*2) + approveTxs, approveFromAddrs := transfer.GenerateTestApproves(t, deps.db, td.nextIndex+len(trs), 2) + multiTxs[0] = transfer.GenerateTestBridgeMultiTransaction(trs[0], trs[1]) multiTxs[1] = transfer.GenerateTestSwapMultiTransaction(trs[2], testutils.SntSymbol, 100) // trs[3] multiTxs[2] = transfer.GenerateTestSendMultiTransaction(trs[4]) // trs[5] multiTxs[3] = transfer.GenerateTestBridgeMultiTransaction(trs[6], trs[7]) - multiTxs[4] = transfer.GenerateTestSendMultiTransaction(trs[8]) // trs[9] - multiTxs[5] = transfer.GenerateTestApproveMultiTransaction(trs[10]) // trs[11] + multiTxs[4] = transfer.GenerateTestSendMultiTransaction(trs[8]) // trs[9] + multiTxs[5] = transfer.GenerateTestApproveMultiTransaction(approveTxs[1]) var lastMT common.MultiTransactionIDType for i := range trs { @@ -695,6 +697,7 @@ func TestGetActivityEntriesFilterByType(t *testing.T) { // Here not to include the modified To and From addresses allAddresses := append(append(append(append(append(tdFromAdds, tdToAddrs...), fromAddrs...), toAddrs...), fromSpecial...), toSpecial...) + allAddresses = append(allAddresses, approveFromAddrs...) // Insert MintAT Collectible trsSpecial[0].From = eth.HexToAddress("0x0") diff --git a/services/wallet/activity/filter.sql b/services/wallet/activity/filter.sql index 900be2144..93240ee76 100644 --- a/services/wallet/activity/filter.sql +++ b/services/wallet/activity/filter.sql @@ -23,6 +23,8 @@ WITH filter_conditions AS ( ? AS filterActivityTypeReceive, ? AS filterActivityTypeContractDeployment, ? AS filterActivityTypeMint, + ? AS filterActivityTypeSwap, + ? AS filterActivityTypeApprove, ? AS mTTypeSend, ? AS fromTrType, ? AS toTrType, @@ -221,12 +223,22 @@ SELECT WHEN transfers.type = 'erc20' THEN (SELECT community_id FROM tokens WHERE transfers.token_address = tokens.address AND transfers.network_id = tokens.network_id) WHEN transfers.type = 'erc721' OR transfers.type = 'erc1155' THEN (SELECT community_id FROM collectible_data_cache WHERE transfers.token_address = collectible_data_cache.contract_address AND transfers.network_id = collectible_data_cache.chain_id) ELSE NULL - END AS community_id + END AS community_id, + transaction_input_data.approval_spender as input_approval_spender, + transaction_input_data.approval_amount as input_approval_amount, + transaction_input_data.from_asset as input_from_asset, + transaction_input_data.from_amount as input_from_amount, + transaction_input_data.to_asset as input_to_asset, + transaction_input_data.to_amount as input_to_amount, + transaction_input_data.side as input_side, + transaction_input_data.slippage_bps as input_slippage_bps, + transfers.transaction_to as interacted_contract_address FROM transfers CROSS JOIN filter_conditions LEFT JOIN filter_addresses from_join ON transfers.tx_from_address = from_join.address LEFT JOIN filter_addresses to_join ON transfers.tx_to_address = to_join.address + LEFT JOIN transaction_input_data ON transfers.network_id = transaction_input_data.chain_id AND transfers.tx_hash = transaction_input_data.tx_hash WHERE transfers.loaded == 1 AND transfers.multi_transaction_id = 0 @@ -273,6 +285,11 @@ WHERE ) ) ) + OR ( + filterActivityTypeApprove + AND tr_type = fromTrType + AND transfers.type = 'Erc20Approval' + ) OR ( filterActivityTypeContractDeployment AND tr_type = fromTrType @@ -391,12 +408,22 @@ SELECT NULL as contract_address, NULL AS method_hash, NULL AS community_mint_event, - NULL AS community_id + NULL AS community_id, + transaction_input_data.approval_spender as input_approval_spender, + transaction_input_data.approval_amount as input_approval_amount, + transaction_input_data.from_asset as input_from_asset, + transaction_input_data.from_amount as input_from_amount, + transaction_input_data.to_asset as input_to_asset, + transaction_input_data.to_amount as input_to_amount, + transaction_input_data.side as input_side, + transaction_input_data.slippage_bps as input_slippage_bps, + pending_transactions.transaction_to as interacted_contract_address FROM pending_transactions CROSS JOIN filter_conditions LEFT JOIN filter_addresses from_join ON pending_transactions.from_address = from_join.address LEFT JOIN filter_addresses to_join ON pending_transactions.to_address = to_join.address + LEFT JOIN transaction_input_data ON pending_transactions.network_id = transaction_input_data.chain_id AND pending_transactions.hash = transaction_input_data.tx_hash WHERE pending_transactions.multi_transaction_id = 0 AND pending_transactions.status = pendingStatus @@ -484,7 +511,16 @@ SELECT NULL as contract_address, NULL AS method_hash, NULL AS community_mint_event, - NULL AS community_id + NULL AS community_id, + NULL as input_approval_spender, + NULL as input_approval_amount, + NULL as input_from_asset, + NULL as input_from_amount, + NULL as input_to_asset, + NULL as input_to_amount, + NULL as input_side, + NULL as input_slippage_bps, + NULL as interacted_contract_address FROM multi_transactions CROSS JOIN filter_conditions diff --git a/services/wallet/activity/service.go b/services/wallet/activity/service.go index e313c31fe..4bf87412d 100644 --- a/services/wallet/activity/service.go +++ b/services/wallet/activity/service.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/multiaccounts/accounts" + protocolCommon "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/services/wallet/async" "github.com/status-im/status-go/services/wallet/collectibles" w_common "github.com/status-im/status-go/services/wallet/common" @@ -74,13 +75,22 @@ type Service struct { debounceDuration time.Duration pendingTracker *transactions.PendingTxTracker + + featureFlags *protocolCommon.FeatureFlags } func (s *Service) nextSessionID() SessionID { return SessionID(s.lastSessionID.Add(1)) } -func NewService(db *sql.DB, accountsDB *accounts.Database, tokenManager token.ManagerInterface, collectibles collectibles.ManagerInterface, eventFeed *event.Feed, pendingTracker *transactions.PendingTxTracker) *Service { +func NewService( + db *sql.DB, + accountsDB *accounts.Database, + tokenManager token.ManagerInterface, + collectibles collectibles.ManagerInterface, + eventFeed *event.Feed, + pendingTracker *transactions.PendingTxTracker, + featureFlags *protocolCommon.FeatureFlags) *Service { return &Service{ db: db, accountsDB: accountsDB, @@ -94,6 +104,8 @@ func NewService(db *sql.DB, accountsDB *accounts.Database, tokenManager token.Ma debounceDuration: 1 * time.Second, pendingTracker: pendingTracker, + + featureFlags: featureFlags, } } diff --git a/services/wallet/activity/service_test.go b/services/wallet/activity/service_test.go index 12ec7edea..504202772 100644 --- a/services/wallet/activity/service_test.go +++ b/services/wallet/activity/service_test.go @@ -468,7 +468,7 @@ func TestService_IncrementalUpdateOnTop(t *testing.T) { ChainID: 5, }, newTx.tokenOut) require.Equal(t, (*Token)(nil), newTx.tokenIn) - require.Equal(t, (*eth.Address)(nil), newTx.contractAddress) + require.Equal(t, (*eth.Address)(nil), newTx.deployedContractAddress) // Check the order of the following transaction data require.Equal(t, SimpleTransactionPT, payload.Activities[1].payloadType) diff --git a/services/wallet/common/log_parser.go b/services/wallet/common/log_parser.go index 4a5dcc1a8..a5b52c03e 100644 --- a/services/wallet/common/log_parser.go +++ b/services/wallet/common/log_parser.go @@ -29,9 +29,11 @@ const ( UniswapV3Swap Type = "uniswapV3Swap" HopBridgeFrom Type = "HopBridgeFrom" HopBridgeTo Type = "HopBridgeTo" + Erc20Approval Type = "Erc20Approval" unknownTransaction Type = "unknown" // Event types + Erc20ApprovalEventType EventType = "erc20ApprovalEvent" WETHDepositEventType EventType = "wethDepositEvent" WETHWithdrawalEventType EventType = "wethWithdrawalEvent" Erc20TransferEventType EventType = "erc20Event" @@ -46,6 +48,10 @@ const ( HopBridgeTransferSentEventType EventType = "hopBridgeTransferSentEvent" UnknownEventType EventType = "unknownEvent" + // ERC20: Approval (index_topic_1 address owner, index_topic_2 address spender, uint256 value) + Erc20_721ApprovalEventSignature = "Approval(address,address,uint256)" + erc20ApprovalEventIndexedParameters = 3 // signature, owner, spender + // Deposit (index_topic_1 address dst, uint256 wad) wethDepositEventSignature = "Deposit(address,uint256)" // Withdrawal (index_topic_1 address src, uint256 wad) @@ -83,6 +89,7 @@ var ( // Detect event type for a cetain item from the Events Log func GetEventType(log *types.Log) EventType { + erc20_721ApprovalEventSignatureHash := GetEventSignatureHash(Erc20_721ApprovalEventSignature) wethDepositEventSignatureHash := GetEventSignatureHash(wethDepositEventSignature) wethWithdrawalEventSignatureHash := GetEventSignatureHash(wethWithdrawalEventSignature) erc20_721TransferEventSignatureHash := GetEventSignatureHash(Erc20_721TransferEventSignature) @@ -97,6 +104,11 @@ func GetEventType(log *types.Log) EventType { if len(log.Topics) > 0 { switch log.Topics[0] { + case erc20_721ApprovalEventSignatureHash: + switch len(log.Topics) { + case erc20ApprovalEventIndexedParameters: + return Erc20ApprovalEventType + } case wethDepositEventSignatureHash: return WETHDepositEventType case wethWithdrawalEventSignatureHash: @@ -132,6 +144,8 @@ func GetEventType(log *types.Log) EventType { func EventTypeToSubtransactionType(eventType EventType) Type { switch eventType { + case Erc20ApprovalEventType: + return Erc20Approval case Erc20TransferEventType: return Erc20Transfer case Erc721TransferEventType: @@ -309,6 +323,35 @@ func getFromToAddresses(ethlog types.Log) (from, to common.Address, err error) { return from, to, fmt.Errorf("unsupported event type to get from/to adddresses %s", eventType) } + +func ParseErc20ApprovalLog(ethlog *types.Log) (owner, spender common.Address, value *big.Int, err error) { + value = new(big.Int) + if len(ethlog.Topics) < erc20ApprovalEventIndexedParameters { + err = fmt.Errorf("not enough topics for erc20 approval %s, %v", "topics", ethlog.Topics) + log.Error("log_parser::ParseErc20ApprovalLog", "err", err) + return + } + + err = checkTopicsLength(*ethlog, 1, erc20ApprovalEventIndexedParameters) + if err != nil { + return + } + + addressIdx := common.HashLength - common.AddressLength + copy(owner[:], ethlog.Topics[1][addressIdx:]) + copy(spender[:], ethlog.Topics[2][addressIdx:]) + + if len(ethlog.Data) != common.HashLength { + err = fmt.Errorf("data is not padded to 32 byts big int %s, %v", "data", ethlog.Data) + log.Error("log_parser::ParseErc20ApprovalLog", "err", err) + return + } + + value.SetBytes(ethlog.Data[:common.HashLength]) + + return +} + func ParseTransferLog(ethlog types.Log) (from, to common.Address, txIDs []common.Hash, tokenIDs, values []*big.Int, err error) { eventType := GetEventType(ðlog) diff --git a/services/wallet/router/pathprocessor/constants.go b/services/wallet/router/pathprocessor/constants.go index 984ac6ac4..2f7638444 100644 --- a/services/wallet/router/pathprocessor/constants.go +++ b/services/wallet/router/pathprocessor/constants.go @@ -11,6 +11,8 @@ var ( ZeroBigIntValue = big.NewInt(0) ) +type SwapSide uint8 + const ( IncreaseEstimatedGasFactor = 1.1 SevenDaysInSeconds = 60 * 60 * 24 * 7 @@ -31,6 +33,9 @@ const ( ProcessorENSReleaseName = "ENSRelease" ProcessorENSPublicKeyName = "ENSPublicKey" ProcessorStickersBuyName = "StickersBuy" + + SwapSideBuy SwapSide = 0 + SwapSideSell SwapSide = 1 ) func IsProcessorBridge(name string) bool { diff --git a/services/wallet/router/pathprocessor/errors.go b/services/wallet/router/pathprocessor/errors.go index 8de34d92f..ebdc4670b 100644 --- a/services/wallet/router/pathprocessor/errors.go +++ b/services/wallet/router/pathprocessor/errors.go @@ -47,6 +47,7 @@ var ( ErrPriceTimeout = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-037"), Details: "price timeout"} ErrNotEnoughLiquidity = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-038"), Details: "not enough liquidity"} ErrPriceImpactTooHigh = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-039"), Details: "price impact too high"} + ErrFromTokenShouldNotBeNative = &errors.ErrorResponse{Code: errors.ErrorCode("WPP-040"), Details: "from token should not be the native token"} ) func createErrorResponse(processorName string, err error) error { diff --git a/services/wallet/router/pathprocessor/mock_pathprocessor/processor.go b/services/wallet/router/pathprocessor/mock_pathprocessor/processor.go index d4d949844..ec53fe96e 100644 --- a/services/wallet/router/pathprocessor/mock_pathprocessor/processor.go +++ b/services/wallet/router/pathprocessor/mock_pathprocessor/processor.go @@ -211,3 +211,18 @@ func (mr *MockPathProcessorClearableMockRecorder) Clear() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockPathProcessorClearable)(nil).Clear)) } + +// GetTransactionInputData mocks base method. +func (m *MockPathProcessor) GetTransactionInputData(sendArgs *pathprocessor.MultipathProcessorTxArgs) (*pathprocessor.TransactionInputData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTransactionInputData", sendArgs) + ret0, _ := ret[0].(*pathprocessor.TransactionInputData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Send indicates an expected call of Send. +func (mr *MockPathProcessorMockRecorder) GetTransactionInputData(sendArgs interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransactionInputData", reflect.TypeOf((*MockPathProcessor)(nil).GetTransactionInputData), sendArgs) +} \ No newline at end of file diff --git a/services/wallet/router/pathprocessor/multipath_processor.go b/services/wallet/router/pathprocessor/multipath_processor.go index f34475d42..57f59cea0 100644 --- a/services/wallet/router/pathprocessor/multipath_processor.go +++ b/services/wallet/router/pathprocessor/multipath_processor.go @@ -11,6 +11,7 @@ type MultipathProcessorTxArgs struct { Name string `json:"bridgeName"` ChainID uint64 TransferTx *transactions.SendTxArgs + ApproveTx *ApproveTxArgs HopTx *HopBridgeTxArgs CbridgeTx *CelerBridgeTxArgs ERC721TransferTx *ERC721TxArgs diff --git a/services/wallet/router/pathprocessor/processor.go b/services/wallet/router/pathprocessor/processor.go index b9e539f91..e3d0b60fe 100644 --- a/services/wallet/router/pathprocessor/processor.go +++ b/services/wallet/router/pathprocessor/processor.go @@ -31,6 +31,8 @@ type PathProcessor interface { GetContractAddress(params ProcessorInputParams) (common.Address, error) // BuildTransaction builds the transaction based on MultipathProcessorTxArgs, returns the transaction and the used nonce (lastUsedNonce is -1 if it's the first tx) BuildTransaction(sendArgs *MultipathProcessorTxArgs, lastUsedNonce int64) (*ethTypes.Transaction, uint64, error) + // GetTransactionInputData returns the input data used to build the transaction + GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) } type PathProcessorClearable interface { diff --git a/services/wallet/router/pathprocessor/processor_approve.go b/services/wallet/router/pathprocessor/processor_approve.go new file mode 100644 index 000000000..0327cdf89 --- /dev/null +++ b/services/wallet/router/pathprocessor/processor_approve.go @@ -0,0 +1,12 @@ +package pathprocessor + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// Placeholder for Approve transaction until handling gets fully done status-go side +type ApproveTxArgs struct { + ApprovalSpender common.Address `json:"approvalSpender"` + ApprovalAmount *hexutil.Big `json:"approvalAmount"` +} diff --git a/services/wallet/router/pathprocessor/processor_bridge_celar.go b/services/wallet/router/pathprocessor/processor_bridge_celar.go index c1dc85201..349142794 100644 --- a/services/wallet/router/pathprocessor/processor_bridge_celar.go +++ b/services/wallet/router/pathprocessor/processor_bridge_celar.go @@ -388,3 +388,7 @@ func (s *CelerBridgeProcessor) CalculateAmountOut(params ProcessorInputParams) ( amountOut, _ := new(big.Int).SetString(amt.EqValueTokenAmt, 10) return amountOut, nil } + +func (s *CelerBridgeProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_bridge_hop.go b/services/wallet/router/pathprocessor/processor_bridge_hop.go index 7fe327b06..2acd293c1 100644 --- a/services/wallet/router/pathprocessor/processor_bridge_hop.go +++ b/services/wallet/router/pathprocessor/processor_bridge_hop.go @@ -651,3 +651,7 @@ func (h *HopBridgeProcessor) sendL2BridgeTx(contractAddress common.Address, ethC return tx, ErrTxForChainNotSupported } + +func (s *HopBridgeProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_ens_public_key.go b/services/wallet/router/pathprocessor/processor_ens_public_key.go index 781727835..adbbb051d 100644 --- a/services/wallet/router/pathprocessor/processor_ens_public_key.go +++ b/services/wallet/router/pathprocessor/processor_ens_public_key.go @@ -125,3 +125,7 @@ func (s *ENSPublicKeyProcessor) GetContractAddress(params ProcessorInputParams) } return *addr, nil } + +func (s *ENSPublicKeyProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_ens_register.go b/services/wallet/router/pathprocessor/processor_ens_register.go index 29ec6bfca..e84051ef3 100644 --- a/services/wallet/router/pathprocessor/processor_ens_register.go +++ b/services/wallet/router/pathprocessor/processor_ens_register.go @@ -154,3 +154,7 @@ func (s *ENSRegisterProcessor) CalculateAmountOut(params ProcessorInputParams) ( func (s *ENSRegisterProcessor) GetContractAddress(params ProcessorInputParams) (common.Address, error) { return snt.ContractAddress(params.FromChain.ChainID) } + +func (s *ENSRegisterProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_ens_release.go b/services/wallet/router/pathprocessor/processor_ens_release.go index 3e9f5943b..f01a913ec 100644 --- a/services/wallet/router/pathprocessor/processor_ens_release.go +++ b/services/wallet/router/pathprocessor/processor_ens_release.go @@ -124,3 +124,7 @@ func (s *ENSReleaseProcessor) GetContractAddress(params ProcessorInputParams) (c } return addr, nil } + +func (s *ENSReleaseProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_erc1155.go b/services/wallet/router/pathprocessor/processor_erc1155.go index 02d50a688..2e3a38a03 100644 --- a/services/wallet/router/pathprocessor/processor_erc1155.go +++ b/services/wallet/router/pathprocessor/processor_erc1155.go @@ -170,3 +170,7 @@ func (s *ERC1155Processor) CalculateAmountOut(params ProcessorInputParams) (*big func (s *ERC1155Processor) GetContractAddress(params ProcessorInputParams) (common.Address, error) { return params.FromToken.Address, nil } + +func (s *ERC1155Processor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_erc721.go b/services/wallet/router/pathprocessor/processor_erc721.go index 35f5814a4..14c6f73e5 100644 --- a/services/wallet/router/pathprocessor/processor_erc721.go +++ b/services/wallet/router/pathprocessor/processor_erc721.go @@ -227,3 +227,7 @@ func (s *ERC721Processor) CalculateAmountOut(params ProcessorInputParams) (*big. func (s *ERC721Processor) GetContractAddress(params ProcessorInputParams) (common.Address, error) { return params.FromToken.Address, nil } + +func (s *ERC721Processor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_stickers_buy.go b/services/wallet/router/pathprocessor/processor_stickers_buy.go index 5db9d9628..3da5a627e 100644 --- a/services/wallet/router/pathprocessor/processor_stickers_buy.go +++ b/services/wallet/router/pathprocessor/processor_stickers_buy.go @@ -146,3 +146,7 @@ func (s *StickersBuyProcessor) CalculateAmountOut(params ProcessorInputParams) ( func (s *StickersBuyProcessor) GetContractAddress(params ProcessorInputParams) (common.Address, error) { return snt.ContractAddress(params.FromChain.ChainID) } + +func (s *StickersBuyProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + return nil, nil +} diff --git a/services/wallet/router/pathprocessor/processor_swap_paraswap.go b/services/wallet/router/pathprocessor/processor_swap_paraswap.go index a9b92a485..9e2e668ea 100644 --- a/services/wallet/router/pathprocessor/processor_swap_paraswap.go +++ b/services/wallet/router/pathprocessor/processor_swap_paraswap.go @@ -208,15 +208,25 @@ func (s *SwapParaswapProcessor) GetContractAddress(params ProcessorInputParams) return priceRoute.TokenTransferProxy, nil } -func (s *SwapParaswapProcessor) prepareTransaction(sendArgs *MultipathProcessorTxArgs) error { - slippageBP := uint(sendArgs.SwapTx.SlippagePercentage * 100) // convert to basis points - +func (s *SwapParaswapProcessor) getPriceRoute(sendArgs *MultipathProcessorTxArgs) (*paraswap.Route, error) { key := makeKey(sendArgs.SwapTx.ChainID, sendArgs.SwapTx.ChainIDTo, sendArgs.SwapTx.TokenIDFrom, sendArgs.SwapTx.TokenIDTo) priceRouteIns, ok := s.priceRoute.Load(key) if !ok { - return ErrPriceRouteNotFound + return nil, ErrPriceRouteNotFound + } + return priceRouteIns.(*paraswap.Route), nil +} + +func percentageToBasisPoints(percentage float32) uint { + return uint(percentage * 100) +} + +func (s *SwapParaswapProcessor) prepareTransaction(sendArgs *MultipathProcessorTxArgs) error { + slippageBP := percentageToBasisPoints(sendArgs.SwapTx.SlippagePercentage) + priceRoute, err := s.getPriceRoute(sendArgs) + if err != nil { + return statusErrors.CreateErrorResponseFromError(err) } - priceRoute := priceRouteIns.(*paraswap.Route) tx, err := s.paraswapClient.BuildTransaction(context.Background(), priceRoute.SrcTokenAddress, priceRoute.SrcTokenDecimals, priceRoute.SrcAmount.Int, priceRoute.DestTokenAddress, priceRoute.DestTokenDecimals, priceRoute.DestAmount.Int, slippageBP, @@ -284,3 +294,31 @@ func (s *SwapParaswapProcessor) CalculateAmountOut(params ProcessorInputParams) return destAmount, nil } + +func paraswapSidetoCommon(side paraswap.SwapSide) SwapSide { + if side == paraswap.BuySide { + return SwapSideBuy + } + return SwapSideSell +} + +func (s *SwapParaswapProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + slippageBP := (uint16)(percentageToBasisPoints(sendArgs.SwapTx.SlippagePercentage)) + priceRoute, err := s.getPriceRoute(sendArgs) + if err != nil { + return nil, err + } + swapSide := paraswapSidetoCommon(priceRoute.Side) + + ret := &TransactionInputData{ + ProcessorName: s.Name(), + FromAsset: &sendArgs.SwapTx.TokenIDFrom, + FromAmount: (*hexutil.Big)(priceRoute.SrcAmount.Int), + ToAsset: &sendArgs.SwapTx.TokenIDTo, + ToAmount: (*hexutil.Big)(priceRoute.DestAmount.Int), + Side: &swapSide, + SlippageBps: &slippageBP, + } + + return ret, nil +} diff --git a/services/wallet/router/pathprocessor/processor_transfer.go b/services/wallet/router/pathprocessor/processor_transfer.go index a6a3e3781..6393bcdcf 100644 --- a/services/wallet/router/pathprocessor/processor_transfer.go +++ b/services/wallet/router/pathprocessor/processor_transfer.go @@ -128,3 +128,18 @@ func (s *TransferProcessor) CalculateAmountOut(params ProcessorInputParams) (*bi func (s *TransferProcessor) GetContractAddress(params ProcessorInputParams) (common.Address, error) { return common.Address{}, nil } + +func (s *TransferProcessor) GetTransactionInputData(sendArgs *MultipathProcessorTxArgs) (*TransactionInputData, error) { + if sendArgs.ApproveTx == nil { + return nil, nil + } + + ret := &TransactionInputData{ + ProcessorName: s.Name(), + FromAsset: &sendArgs.TransferTx.Symbol, + ApprovalAmount: sendArgs.ApproveTx.ApprovalAmount, + ApprovalSpender: &sendArgs.ApproveTx.ApprovalSpender, + } + + return ret, nil +} diff --git a/services/wallet/router/pathprocessor/transaction_input_data.go b/services/wallet/router/pathprocessor/transaction_input_data.go new file mode 100644 index 000000000..6ca7da533 --- /dev/null +++ b/services/wallet/router/pathprocessor/transaction_input_data.go @@ -0,0 +1,28 @@ +package pathprocessor + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type TransactionInputData struct { + ProcessorName string + FromAsset *string + FromAmount *hexutil.Big + ToAsset *string + ToAmount *hexutil.Big + Side *SwapSide + SlippageBps *uint16 + ApprovalAmount *hexutil.Big + ApprovalSpender *common.Address +} + +func NewInputData() *TransactionInputData { + return &TransactionInputData{ + FromAmount: (*hexutil.Big)(new(big.Int)), + ToAmount: (*hexutil.Big)(new(big.Int)), + ApprovalAmount: (*hexutil.Big)(new(big.Int)), + } +} diff --git a/services/wallet/router/pathprocessor/transaction_input_data_db.go b/services/wallet/router/pathprocessor/transaction_input_data_db.go new file mode 100644 index 000000000..eb75d525a --- /dev/null +++ b/services/wallet/router/pathprocessor/transaction_input_data_db.go @@ -0,0 +1,110 @@ +package pathprocessor + +import ( + "database/sql" + + sq "github.com/Masterminds/squirrel" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/services/wallet/bigint" + w_common "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/sqlite" +) + +type TransactionInputDataDB struct { + db *sql.DB +} + +func NewTransactionInputDataDB(db *sql.DB) *TransactionInputDataDB { + return &TransactionInputDataDB{ + db: db, + } +} + +func (iDB *TransactionInputDataDB) UpsertInputData(chainID w_common.ChainID, txHash types.Hash, inputData TransactionInputData) error { + return upsertInputData(iDB.db, chainID, txHash, inputData) +} + +func (iDB *TransactionInputDataDB) ReadInputData(chainID w_common.ChainID, txHash types.Hash) (*TransactionInputData, error) { + return readInputData(iDB.db, chainID, txHash) +} + +func upsertInputData(creator sqlite.StatementCreator, chainID w_common.ChainID, txHash types.Hash, inputData TransactionInputData) error { + q := sq.Replace("transaction_input_data"). + SetMap(sq.Eq{ + "chain_id": chainID, + "tx_hash": txHash.Bytes(), + "processor_name": inputData.ProcessorName, + "from_asset": inputData.FromAsset, + "from_amount": (*bigint.SQLBigIntBytes)(inputData.FromAmount), + "to_asset": inputData.ToAsset, + "to_amount": (*bigint.SQLBigIntBytes)(inputData.ToAmount), + "side": inputData.Side, + "slippage_bps": inputData.SlippageBps, + "approval_amount": (*bigint.SQLBigIntBytes)(inputData.ApprovalAmount), + "approval_spender": inputData.ApprovalSpender, + }) + + query, args, err := q.ToSql() + if err != nil { + return err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(args...) + + return err +} + +func readInputData(creator sqlite.StatementCreator, chainID w_common.ChainID, txHash types.Hash) (*TransactionInputData, error) { + q := sq.Select( + "processor_name", + "from_asset", + "from_amount", + "to_asset", + "to_amount", + "side", + "slippage_bps", + "approval_amount", + "approval_spender", + ). + From("transaction_input_data"). + Where(sq.Eq{"chain_id": chainID, "tx_hash": txHash.Bytes()}) + + query, args, err := q.ToSql() + if err != nil { + return nil, err + } + + stmt, err := creator.Prepare(query) + if err != nil { + return nil, err + } + defer stmt.Close() + + inputData := NewInputData() + err = stmt.QueryRow(args...).Scan( + &inputData.ProcessorName, + &inputData.FromAsset, + (*bigint.SQLBigIntBytes)(inputData.FromAmount), + &inputData.ToAsset, + (*bigint.SQLBigIntBytes)(inputData.ToAmount), + &inputData.Side, + &inputData.SlippageBps, + (*bigint.SQLBigIntBytes)(inputData.ApprovalAmount), + &inputData.ApprovalSpender, + ) + + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + + return inputData, nil +} diff --git a/services/wallet/router/pathprocessor/transaction_input_data_db_test.go b/services/wallet/router/pathprocessor/transaction_input_data_db_test.go new file mode 100644 index 000000000..0352db6fa --- /dev/null +++ b/services/wallet/router/pathprocessor/transaction_input_data_db_test.go @@ -0,0 +1,95 @@ +package pathprocessor + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/google/uuid" + + "github.com/status-im/status-go/eth-node/types" + w_common "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/walletdatabase" + + "github.com/stretchr/testify/require" +) + +func setupTxInputDataDBTest(t *testing.T) (*TransactionInputDataDB, func()) { + db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) + require.NoError(t, err) + return NewTransactionInputDataDB(db), func() { + require.NoError(t, db.Close()) + } +} + +type testTxInputData struct { + ChainID w_common.ChainID + TxHash types.Hash + InputData TransactionInputData +} + +func generateTestTxInputData(offset int, count int) []testTxInputData { + ret := make([]testTxInputData, 0, count) + for i := offset; i < offset+count; i++ { + inputData := NewInputData() + + inputDataIdx := i % 3 + inputData.ProcessorName = fmt.Sprintf("processor_name_%d", inputDataIdx) + + fromAsset := fmt.Sprintf("from_asset_%d", inputDataIdx) + fromAmount := new(big.Int).SetInt64(int64(inputDataIdx)) + toAsset := fmt.Sprintf("to_asset_%d", inputDataIdx) + toAmount := new(big.Int).SetInt64(int64(inputDataIdx * 2)) + side := SwapSide(i % 2) + slippageBps := uint16(i % 100) + approvalAmount := new(big.Int).SetInt64(int64(inputDataIdx * 3)) + approvalSpender := common.HexToAddress(fmt.Sprintf("0x%d", inputDataIdx*4)) + + switch inputDataIdx { + case 0: + inputData.FromAsset = &fromAsset + inputData.FromAmount = (*hexutil.Big)(fromAmount) + case 1: + inputData.FromAsset = &fromAsset + inputData.FromAmount = (*hexutil.Big)(fromAmount) + inputData.ToAsset = &toAsset + inputData.ToAmount = (*hexutil.Big)(toAmount) + inputData.Side = &side + inputData.SlippageBps = &slippageBps + case 2: + inputData.FromAsset = &fromAsset + inputData.FromAmount = (*hexutil.Big)(fromAmount) + inputData.ApprovalAmount = (*hexutil.Big)(approvalAmount) + inputData.ApprovalSpender = &approvalSpender + } + testInputData := testTxInputData{ + ChainID: w_common.ChainID(i % 3), + TxHash: types.HexToHash(uuid.New().String()), + InputData: *inputData, + } + ret = append(ret, testInputData) + } + return ret +} + +func TestUpsertTxInputData(t *testing.T) { + iDB, cleanup := setupTxInputDataDBTest(t) + defer cleanup() + + testData := generateTestTxInputData(0, 15) + for _, data := range testData { + err := iDB.UpsertInputData(data.ChainID, data.TxHash, data.InputData) + require.NoError(t, err) + } + + for _, data := range testData { + readData, err := iDB.ReadInputData(data.ChainID, data.TxHash) + require.NoError(t, err) + require.NotNil(t, readData) + require.Equal(t, data.InputData, *readData) + } +} diff --git a/services/wallet/service.go b/services/wallet/service.go index 9ddeb597b..06c4196ac 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -30,6 +30,7 @@ import ( "github.com/status-im/status-go/services/wallet/history" "github.com/status-im/status-go/services/wallet/market" "github.com/status-im/status-go/services/wallet/onramp" + "github.com/status-im/status-go/services/wallet/router/pathprocessor" "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/status-im/status-go/services/wallet/thirdparty/alchemy" "github.com/status-im/status-go/services/wallet/thirdparty/coingecko" @@ -112,7 +113,16 @@ func NewService( cryptoOnRampManager := onramp.NewManager(cryptoOnRampProviders) savedAddressesManager := &SavedAddressesManager{db: db} - transactionManager := transfer.NewTransactionManager(transfer.NewMultiTransactionDB(db), gethManager, transactor, config, accountsDB, pendingTxManager, feed) + transactionManager := transfer.NewTransactionManager( + transfer.NewMultiTransactionDB(db), + pathprocessor.NewTransactionInputDataDB(db), + gethManager, + transactor, + config, + accountsDB, + pendingTxManager, + feed, + ) blockChainState := blockchainstate.NewBlockChainState() transferController := transfer.NewTransferController(db, accountsDB, rpcClient, accountFeed, feed, transactionManager, pendingTxManager, tokenManager, balanceCacher, blockChainState) @@ -181,12 +191,7 @@ func NewService( ) collectibles := collectibles.NewService(db, feed, accountsDB, accountFeed, settingsFeed, communityManager, rpcClient.NetworkManager, collectiblesManager) - activity := activity.NewService(db, accountsDB, tokenManager, collectiblesManager, feed, pendingTxManager) - - featureFlags := &protocolCommon.FeatureFlags{} - if config.WalletConfig.EnableCelerBridge { - featureFlags.EnableCelerBridge = true - } + activity := activity.NewService(db, accountsDB, tokenManager, collectiblesManager, feed, pendingTxManager, featureFlags) return &Service{ db: db, diff --git a/services/wallet/transfer/commands_sequential_test.go b/services/wallet/transfer/commands_sequential_test.go index 43210092c..2e80b7e6b 100644 --- a/services/wallet/transfer/commands_sequential_test.go +++ b/services/wallet/transfer/commands_sequential_test.go @@ -37,6 +37,7 @@ import ( "github.com/status-im/status-go/services/wallet/balance" "github.com/status-im/status-go/services/wallet/blockchainstate" "github.com/status-im/status-go/services/wallet/community" + "github.com/status-im/status-go/services/wallet/router/pathprocessor" "github.com/status-im/status-go/t/helpers" "github.com/status-im/status-go/t/utils" @@ -1322,7 +1323,7 @@ func TestFetchTransfersForLoadedBlocks(t *testing.T) { db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) require.NoError(t, err) - tm := &TransactionManager{NewMultiTransactionDB(db), nil, nil, nil, nil, nil, nil, nil, nil, nil} + tm := &TransactionManager{NewMultiTransactionDB(db), pathprocessor.NewTransactionInputDataDB(db), nil, nil, nil, nil, nil, nil, nil, nil, nil} mediaServer, err := server.NewMediaServer(appdb, nil, nil, db) require.NoError(t, err) diff --git a/services/wallet/transfer/controller_test.go b/services/wallet/transfer/controller_test.go index c3befd3dc..56528e132 100644 --- a/services/wallet/transfer/controller_test.go +++ b/services/wallet/transfer/controller_test.go @@ -18,6 +18,7 @@ import ( "github.com/status-im/status-go/services/accounts/accountsevent" "github.com/status-im/status-go/services/wallet/blockchainstate" wallet_common "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/router/pathprocessor" "github.com/status-im/status-go/t/helpers" "github.com/status-im/status-go/walletdatabase" ) @@ -36,7 +37,7 @@ func TestController_watchAccountsChanges(t *testing.T) { bcstate := blockchainstate.NewBlockChainState() SetMultiTransactionIDGenerator(StaticIDCounter()) // to have different multi-transaction IDs even with fast execution - transactionManager := NewTransactionManager(NewInMemMultiTransactionStorage(), nil, nil, nil, accountsDB, nil, nil) + transactionManager := NewTransactionManager(NewInMemMultiTransactionStorage(), NewInMemTransactionInputDataStorage(), nil, nil, nil, accountsDB, nil, nil) c := NewTransferController( walletDB, accountsDB, @@ -239,7 +240,7 @@ func TestController_cleanupAccountLeftovers(t *testing.T) { require.NoError(t, err) require.Len(t, storedAccs, 1) - transactionManager := NewTransactionManager(NewMultiTransactionDB(walletDB), nil, nil, nil, accountsDB, nil, nil) + transactionManager := NewTransactionManager(NewMultiTransactionDB(walletDB), pathprocessor.NewTransactionInputDataDB(walletDB), nil, nil, nil, accountsDB, nil, nil) bcstate := blockchainstate.NewBlockChainState() c := NewTransferController( walletDB, diff --git a/services/wallet/transfer/database.go b/services/wallet/transfer/database.go index 69b15b1aa..1536533d5 100644 --- a/services/wallet/transfer/database.go +++ b/services/wallet/transfer/database.go @@ -359,16 +359,33 @@ func updateOrInsertTransfers(chainID uint64, creator statementCreator, transfers var tokenID *big.Int var txFrom *common.Address var txTo *common.Address + var transactionTo *common.Address + var eventLogAddress *common.Address if t.Transaction != nil { if t.Log != nil { - _, tokenAddress, txFrom, txTo = w_common.ExtractTokenTransferData(t.Type, t.Log, t.Transaction) - tokenID = t.TokenID - // Zero tokenID can be used for ERC721 and ERC1155 transfers but when serialzed/deserialized it becomes nil - // as 0 value of big.Int bytes is nil. - if tokenID == nil && (t.Type == w_common.Erc721Transfer || t.Type == w_common.Erc1155Transfer) { - tokenID = big.NewInt(0) + eventType := w_common.GetEventType(t.Log) + switch eventType { + case w_common.Erc20ApprovalEventType: + tokenAddress = &t.Log.Address + owner, spender, value, err := w_common.ParseErc20ApprovalLog(t.Log) + if err != nil { + log.Error("can't parse erc20 approval log", "err", err) + continue + } + txFrom = &owner + txTo = &spender + txValue = value + default: + _, tokenAddress, txFrom, txTo = w_common.ExtractTokenTransferData(t.Type, t.Log, t.Transaction) + tokenID = t.TokenID + // Zero tokenID can be used for ERC721 and ERC1155 transfers but when serialzed/deserialized it becomes nil + // as 0 value of big.Int bytes is nil. + if tokenID == nil && (t.Type == w_common.Erc721Transfer || t.Type == w_common.Erc1155Transfer) { + tokenID = big.NewInt(0) + } + txValue = t.TokenValue } - txValue = t.TokenValue + eventLogAddress = &t.Log.Address } else { txValue = new(big.Int).Set(t.Transaction.Value()) txFrom = &t.From @@ -388,6 +405,7 @@ func updateOrInsertTransfers(chainID uint64, creator statementCreator, transfers *txNonce = t.Transaction.Nonce() txSize = new(uint64) *txSize = t.Transaction.Size() + transactionTo = t.Transaction.To() } dbFields := transferDBFields{ @@ -426,6 +444,8 @@ func updateOrInsertTransfers(chainID uint64, creator statementCreator, transfers tokenID: tokenID, txFrom: txFrom, txTo: txTo, + transactionTo: transactionTo, + eventLogAddress: eventLogAddress, } txsDBFields = append(txsDBFields, dbFields) } @@ -469,15 +489,18 @@ type transferDBFields struct { tokenID *big.Int txFrom *common.Address txTo *common.Address + transactionTo *common.Address + eventLogAddress *common.Address } func updateOrInsertTransfersDBFields(creator statementCreator, transfers []transferDBFields) error { insert, err := creator.Prepare(`INSERT OR REPLACE INTO transfers (network_id, hash, blk_hash, blk_number, timestamp, address, tx, sender, receipt, log, type, loaded, base_gas_fee, multi_transaction_id, status, receipt_type, tx_hash, log_index, block_hash, cumulative_gas_used, contract_address, gas_used, tx_index, - tx_type, protected, gas_limit, gas_price_clamped64, gas_tip_cap_clamped64, gas_fee_cap_clamped64, amount_padded128hex, account_nonce, size, token_address, token_id, tx_from_address, tx_to_address) + tx_type, protected, gas_limit, gas_price_clamped64, gas_tip_cap_clamped64, gas_fee_cap_clamped64, amount_padded128hex, account_nonce, size, token_address, token_id, tx_from_address, tx_to_address, + transaction_to, event_log_address) VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) if err != nil { return err } @@ -489,7 +512,8 @@ func updateOrInsertTransfersDBFields(creator statementCreator, transfers []trans _, err = insert.Exec(t.chainID, t.id, t.blockHash, (*bigint.SQLBigInt)(t.blockNumber), t.timestamp, t.address, &JSONBlob{t.transaction}, t.sender, &JSONBlob{t.receipt}, &JSONBlob{t.log}, t.transferType, t.baseGasFees, t.multiTransactionID, t.receiptStatus, t.receiptType, t.txHash, t.logIndex, t.receiptBlockHash, t.cumulativeGasUsed, t.contractAddress, t.gasUsed, t.transactionIndex, - t.txType, t.txProtected, t.txGas, txGasPrice, txGasTipCap, txGasFeeCap, txValue, t.txNonce, t.txSize, t.tokenAddress, (*bigint.SQLBigIntBytes)(t.tokenID), t.txFrom, t.txTo) + t.txType, t.txProtected, t.txGas, txGasPrice, txGasTipCap, txGasFeeCap, txValue, t.txNonce, t.txSize, t.tokenAddress, (*bigint.SQLBigIntBytes)(t.tokenID), t.txFrom, t.txTo, + t.transactionTo, t.eventLogAddress) if err != nil { log.Error("can't save transfer", "b-hash", t.blockHash, "b-n", t.blockNumber, "a", t.address, "h", t.id) return err diff --git a/services/wallet/transfer/downloader.go b/services/wallet/transfer/downloader.go index f1bb49722..ce5abadeb 100644 --- a/services/wallet/transfer/downloader.go +++ b/services/wallet/transfer/downloader.go @@ -434,7 +434,8 @@ func (d *ETHDownloader) subTransactionsFromTransactionData(address, from common. for _, txLog := range receipt.Logs { eventType := w_common.GetEventType(txLog) switch eventType { - case w_common.UniswapV2SwapEventType, w_common.UniswapV3SwapEventType, + case w_common.Erc20ApprovalEventType, + w_common.UniswapV2SwapEventType, w_common.UniswapV3SwapEventType, w_common.HopBridgeTransferSentToL2EventType, w_common.HopBridgeTransferFromL1CompletedEventType, w_common.HopBridgeWithdrawalBondedEventType, w_common.HopBridgeTransferSentEventType: transfer := Transfer{ diff --git a/services/wallet/transfer/helpers.go b/services/wallet/transfer/helpers.go index 8733bea22..a3ee40719 100644 --- a/services/wallet/transfer/helpers.go +++ b/services/wallet/transfer/helpers.go @@ -154,13 +154,16 @@ func updateDataFromMultiTx(data []*pathprocessor.MultipathProcessorTxArgs, multi } } -func sendTransactions(data []*pathprocessor.MultipathProcessorTxArgs, pathProcessors map[string]pathprocessor.PathProcessor, account *account.SelectedExtKey) ( +func sendTransactions( + data []*pathprocessor.MultipathProcessorTxArgs, + pathProcessors map[string]pathprocessor.PathProcessor, + account *account.SelectedExtKey, + txInputDataStorage TransactionInputDataStorage) ( map[uint64][]types.Hash, error) { hashes := make(map[uint64][]types.Hash) usedNonces := make(map[uint64]int64) for _, tx := range data { - lastUsedNonce := int64(-1) if nonce, ok := usedNonces[tx.ChainID]; ok { lastUsedNonce = nonce @@ -171,6 +174,18 @@ func sendTransactions(data []*pathprocessor.MultipathProcessorTxArgs, pathProces return nil, err // TODO: One of transfers within transaction could have been sent. Need to notify user about it } + // Store transaction input data + txInputData, err := pathProcessors[tx.Name].GetTransactionInputData(tx) + if err != nil { + return nil, err + } + if txInputData != nil { + err = txInputDataStorage.UpsertInputData(wallet_common.ChainID(tx.ChainID), hash, *txInputData) + if err != nil { + return nil, err + } + } + hashes[tx.ChainID] = append(hashes[tx.ChainID], hash) usedNonces[tx.ChainID] = int64(usedNonce) } diff --git a/services/wallet/transfer/testutils.go b/services/wallet/transfer/testutils.go index bb7b5f590..891f22322 100644 --- a/services/wallet/transfer/testutils.go +++ b/services/wallet/transfer/testutils.go @@ -9,9 +9,12 @@ import ( eth_common "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + eth_types "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/common" + wallet_common "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/router/pathprocessor" "github.com/status-im/status-go/services/wallet/testutils" "github.com/status-im/status-go/services/wallet/token" @@ -42,6 +45,13 @@ type TestCollectibleTransfer struct { TestCollectible } +type TestApprove struct { + TestTransaction + Spender eth_common.Address // [address] + Amount int64 + Token *token.Token +} + func SeedToToken(seed int) *token.Token { tokenIndex := seed % len(TestTokens) return TestTokens[tokenIndex] @@ -106,6 +116,17 @@ func generateTestCollectibleTransfer(seed int) TestCollectibleTransfer { return tr } +func generateTestApprove(seed int) TestApprove { + tokenIndex := seed % len(TestTokens) + token := TestTokens[tokenIndex] + return TestApprove{ + TestTransaction: generateTestTransaction(seed), + Spender: eth_common.HexToAddress(fmt.Sprintf("0x3%d", seed)), + Amount: int64(seed), + Token: token, + } +} + func GenerateTestSendMultiTransaction(tr TestTransfer) MultiTransaction { return MultiTransaction{ ID: multiTransactionIDGenerator(), @@ -148,15 +169,15 @@ func GenerateTestBridgeMultiTransaction(fromTr, toTr TestTransfer) MultiTransact } } -func GenerateTestApproveMultiTransaction(tr TestTransfer) MultiTransaction { +func GenerateTestApproveMultiTransaction(tr TestApprove) MultiTransaction { return MultiTransaction{ ID: multiTransactionIDGenerator(), Type: MultiTransactionApprove, FromAddress: tr.From, - ToAddress: tr.To, + ToAddress: tr.Spender, FromAsset: tr.Token.Symbol, ToAsset: tr.Token.Symbol, - FromAmount: (*hexutil.Big)(big.NewInt(tr.Value)), + FromAmount: (*hexutil.Big)(big.NewInt(tr.Amount)), ToAmount: (*hexutil.Big)(big.NewInt(0)), Timestamp: uint64(tr.Timestamp), } @@ -174,6 +195,17 @@ func GenerateTestTransfers(tb testing.TB, db *sql.DB, firstStartIndex int, count return } +// GenerateTesApproves will generate transaction based on the TestTokens index and roll over if there are more than +// len(TestTokens) transactions +func GenerateTestApproves(tb testing.TB, db *sql.DB, firstStartIndex int, count int) (result []TestApprove, fromAddresses []eth_common.Address) { + for i := firstStartIndex; i < (firstStartIndex + count); i++ { + tr := generateTestApprove(i) + fromAddresses = append(fromAddresses, tr.From) + result = append(result, tr) + } + return +} + type TestCollectible struct { TokenAddress eth_common.Address TokenID *big.Int @@ -361,6 +393,14 @@ func InsertTestTransferWithOptions(tb testing.TB, db *sql.DB, address eth_common } } + var transactionTo *eth_common.Address + if opt.Tx != nil { + transactionTo = opt.Tx.To() + } + var eventLogAddress *eth_common.Address + if opt.Receipt != nil { + eventLogAddress = &opt.Receipt.Logs[0].Address + } transfer := transferDBFields{ chainID: uint64(tr.ChainID), id: tr.Hash, @@ -383,6 +423,8 @@ func InsertTestTransferWithOptions(tb testing.TB, db *sql.DB, address eth_common tokenID: opt.TokenID, transaction: opt.Tx, receipt: opt.Receipt, + transactionTo: transactionTo, + eventLogAddress: eventLogAddress, } err = updateOrInsertTransfersDBFields(tx, []transferDBFields{transfer}) require.NoError(tb, err) @@ -391,9 +433,9 @@ func InsertTestTransferWithOptions(tb testing.TB, db *sql.DB, address eth_common func InsertTestPendingTransaction(tb testing.TB, db *sql.DB, tr *TestTransfer) { _, err := db.Exec(` INSERT INTO pending_transactions (network_id, hash, timestamp, from_address, to_address, - symbol, gas_price, gas_limit, value, data, type, additional_data, multi_transaction_id - ) VALUES (?, ?, ?, ?, ?, 'ETH', 0, 0, ?, '', 'eth', '', ?)`, - tr.ChainID, tr.Hash, tr.Timestamp, tr.From, tr.To, (*bigint.SQLBigIntBytes)(big.NewInt(tr.Value)), tr.MultiTransactionID) + symbol, gas_price, gas_limit, value, data, type, additional_data, multi_transaction_id, transaction_to, + ) VALUES (?, ?, ?, ?, ?, 'ETH', 0, 0, ?, '', 'eth', '', ?, ?)`, + tr.ChainID, tr.Hash, tr.Timestamp, tr.From, tr.To, (*bigint.SQLBigIntBytes)(big.NewInt(tr.Value)), tr.MultiTransactionID, tr.Token.Address) require.NoError(tb, err) } @@ -499,3 +541,19 @@ func (s *InMemMultiTransactionStorage) ReadMultiTransactions(details *MultiTxDet } return multiTxs, nil } + +type InMemTransactionInputDataStorage struct { + storage map[string]*pathprocessor.TransactionInputData +} + +func NewInMemTransactionInputDataStorage() *InMemTransactionInputDataStorage { + return &InMemTransactionInputDataStorage{ + storage: make(map[string]*pathprocessor.TransactionInputData), + } +} + +func (s *InMemTransactionInputDataStorage) UpsertInputData(chainID wallet_common.ChainID, txHash eth_types.Hash, inputData pathprocessor.TransactionInputData) error { + key := fmt.Sprintf("%d-%s", chainID, txHash.String()) + s.storage[key] = &inputData + return nil +} diff --git a/services/wallet/transfer/transaction_manager.go b/services/wallet/transfer/transaction_manager.go index c839d4f6c..2e67a4e0f 100644 --- a/services/wallet/transfer/transaction_manager.go +++ b/services/wallet/transfer/transaction_manager.go @@ -34,13 +34,14 @@ type TransactionDescription struct { } type TransactionManager struct { - storage MultiTransactionStorage - gethManager *account.GethManager - transactor transactions.TransactorIface - config *params.NodeConfig - accountsDB accounts.AccountsStorage - pendingTracker *transactions.PendingTxTracker - eventFeed *event.Feed + storage MultiTransactionStorage + txInputDataStorage TransactionInputDataStorage + gethManager *account.GethManager + transactor transactions.TransactorIface + config *params.NodeConfig + accountsDB accounts.AccountsStorage + pendingTracker *transactions.PendingTxTracker + eventFeed *event.Feed multiTransactionForKeycardSigning *MultiTransaction multipathTransactionsData []*pathprocessor.MultipathProcessorTxArgs @@ -54,8 +55,13 @@ type MultiTransactionStorage interface { DeleteMultiTransaction(id wallet_common.MultiTransactionIDType) error } +type TransactionInputDataStorage interface { + UpsertInputData(chainID wallet_common.ChainID, txHash types.Hash, inputData pathprocessor.TransactionInputData) error +} + func NewTransactionManager( storage MultiTransactionStorage, + txInputDataStorage TransactionInputDataStorage, gethManager *account.GethManager, transactor transactions.TransactorIface, config *params.NodeConfig, @@ -64,13 +70,14 @@ func NewTransactionManager( eventFeed *event.Feed, ) *TransactionManager { return &TransactionManager{ - storage: storage, - gethManager: gethManager, - transactor: transactor, - config: config, - accountsDB: accountsDB, - pendingTracker: pendingTxManager, - eventFeed: eventFeed, + storage: storage, + txInputDataStorage: txInputDataStorage, + gethManager: gethManager, + transactor: transactor, + config: config, + accountsDB: accountsDB, + pendingTracker: pendingTxManager, + eventFeed: eventFeed, } } diff --git a/services/wallet/transfer/transaction_manager_multitransaction.go b/services/wallet/transfer/transaction_manager_multitransaction.go index f2685bd36..874b177c3 100644 --- a/services/wallet/transfer/transaction_manager_multitransaction.go +++ b/services/wallet/transfer/transaction_manager_multitransaction.go @@ -90,7 +90,7 @@ func (tm *TransactionManager) SendTransactionForSigningToKeycard(ctx context.Con func (tm *TransactionManager) SendTransactions(ctx context.Context, multiTransaction *MultiTransaction, data []*pathprocessor.MultipathProcessorTxArgs, pathProcessors map[string]pathprocessor.PathProcessor, account *account.SelectedExtKey) (*MultiTransactionCommandResult, error) { updateDataFromMultiTx(data, multiTransaction) - hashes, err := sendTransactions(data, pathProcessors, account) + hashes, err := sendTransactions(data, pathProcessors, account, tm.txInputDataStorage) if err != nil { return nil, err } diff --git a/services/wallet/transfer/transaction_manager_multitransaction_test.go b/services/wallet/transfer/transaction_manager_multitransaction_test.go index fdd789426..0fe60851c 100644 --- a/services/wallet/transfer/transaction_manager_multitransaction_test.go +++ b/services/wallet/transfer/transaction_manager_multitransaction_test.go @@ -57,7 +57,7 @@ func setupTransactionManager(t *testing.T) (*TransactionManager, *mock_transacto // Create a mock transactor transactor := mock_transactor.NewMockTransactorIface(ctrl) // Create a new instance of the TransactionManager - tm := NewTransactionManager(NewInMemMultiTransactionStorage(), nil, transactor, nil, nil, nil, nil) + tm := NewTransactionManager(NewInMemMultiTransactionStorage(), NewInMemTransactionInputDataStorage(), nil, transactor, nil, nil, nil, nil) return tm, transactor, ctrl } diff --git a/transactions/pendingtxtracker.go b/transactions/pendingtxtracker.go index 11bb7415e..74fc62f1d 100644 --- a/transactions/pendingtxtracker.go +++ b/transactions/pendingtxtracker.go @@ -343,7 +343,7 @@ func (tm *PendingTxTracker) emitNotifications(chainID common.ChainID, changes [] } // 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, to eth.Address, trType PendingTrxType, autoDelete AutoDeleteType, additionalData string) error { +func (tm *PendingTxTracker) TrackPendingTransaction(chainID common.ChainID, hash eth.Hash, from eth.Address, to eth.Address, trType PendingTrxType, autoDelete AutoDeleteType, additionalData string, transactionTo *eth.Address) error { err := tm.addPending(&PendingTransaction{ ChainID: chainID, Hash: hash, @@ -353,6 +353,7 @@ func (tm *PendingTxTracker) TrackPendingTransaction(chainID common.ChainID, hash Type: trType, AutoDelete: &autoDelete, AdditionalData: additionalData, + TransactionTo: transactionTo, }) if err != nil { return err @@ -419,6 +420,7 @@ type PendingTransaction struct { GasLimit bigint.BigInt `json:"gasLimit"` Type PendingTrxType `json:"type"` AdditionalData string `json:"additionalData"` + TransactionTo *eth.Address `json:"transactionTo"` ChainID common.ChainID `json:"network_id"` MultiTransactionID wallet_common.MultiTransactionIDType `json:"multi_transaction_id"` Nonce uint64 `json:"nonce"` @@ -430,7 +432,7 @@ type PendingTransaction struct { } const selectFromPending = `SELECT hash, timestamp, value, from_address, to_address, data, - symbol, gas_price, gas_limit, type, additional_data, + symbol, gas_price, gas_limit, type, additional_data, transaction_to, network_id, COALESCE(multi_transaction_id, 0), status, auto_delete, nonce FROM pending_transactions ` @@ -456,6 +458,7 @@ func rowsToTransactions(rows *sql.Rows) (transactions []*PendingTransaction, err (*bigint.SQLBigIntBytes)(transaction.GasLimit.Int), &transaction.Type, &transaction.AdditionalData, + &transaction.TransactionTo, &transaction.ChainID, &transaction.MultiTransactionID, transaction.Status, @@ -608,10 +611,10 @@ func (tm *PendingTxTracker) addPending(transaction *PendingTransaction) error { var insert *sql.Stmt insert, err = tx.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, + data, symbol, gas_price, gas_limit, type, additional_data, transaction_to, multi_transaction_id, status, auto_delete, nonce) VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? , ?, ?)`) + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? , ?, ?, ?)`) if err != nil { return err } @@ -630,6 +633,7 @@ func (tm *PendingTxTracker) addPending(transaction *PendingTransaction) error { (*bigint.SQLBigIntBytes)(transaction.GasLimit.Int), transaction.Type, transaction.AdditionalData, + transaction.TransactionTo, transaction.MultiTransactionID, transaction.Status, transaction.AutoDelete, diff --git a/transactions/pendingtxtracker_test.go b/transactions/pendingtxtracker_test.go index 638b2f23f..9a37efc61 100644 --- a/transactions/pendingtxtracker_test.go +++ b/transactions/pendingtxtracker_test.go @@ -302,7 +302,7 @@ func TestPendingTxTracker_MultipleClients(t *testing.T) { sub := eventFeed.Subscribe(eventChan) for i := range txs { - err := m.TrackPendingTransaction(txs[i].ChainID, txs[i].Hash, txs[i].From, txs[i].To, txs[i].Type, AutoDelete, "") + err := m.TrackPendingTransaction(txs[i].ChainID, txs[i].Hash, txs[i].From, txs[i].To, txs[i].Type, AutoDelete, "", txs[i].TransactionTo) require.NoError(t, err) } @@ -377,7 +377,7 @@ func TestPendingTxTracker_Watch(t *testing.T) { sub := eventFeed.Subscribe(eventChan) // Track the first transaction - err := m.TrackPendingTransaction(txs[1].ChainID, txs[1].Hash, txs[1].From, txs[1].To, txs[1].Type, Keep, "") + err := m.TrackPendingTransaction(txs[1].ChainID, txs[1].Hash, txs[1].From, txs[1].To, txs[1].Type, Keep, "", txs[1].TransactionTo) require.NoError(t, err) // Store the confirmed already @@ -476,7 +476,7 @@ func TestPendingTxTracker_Watch_StatusChangeIncrementally(t *testing.T) { for i := range txs { // Track the first transaction - err := m.TrackPendingTransaction(txs[i].ChainID, txs[i].Hash, txs[i].From, txs[i].To, txs[i].Type, Keep, "") + err := m.TrackPendingTransaction(txs[i].ChainID, txs[i].Hash, txs[i].From, txs[i].To, txs[i].Type, Keep, "", txs[i].TransactionTo) require.NoError(t, err) } diff --git a/transactions/testhelpers.go b/transactions/testhelpers.go index b908ce5ff..25b9aaca3 100644 --- a/transactions/testhelpers.go +++ b/transactions/testhelpers.go @@ -67,12 +67,14 @@ func GenerateTestPendingTransactions(start int, count int) []PendingTransaction txs := make([]PendingTransaction, count) for i := start; i < count; i++ { + transactionTo := eth.HexToAddress(fmt.Sprintf("0x4%d", 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", + TransactionTo: &transactionTo, Value: bigint.BigInt{Int: big.NewInt(int64(i))}, GasLimit: bigint.BigInt{Int: big.NewInt(21000)}, GasPrice: bigint.BigInt{Int: big.NewInt(int64(i))}, diff --git a/transactions/transactor.go b/transactions/transactor.go index 1dee6d799..78178ffa6 100644 --- a/transactions/transactor.go +++ b/transactions/transactor.go @@ -197,6 +197,7 @@ func createPendingTransaction(from common.Address, symbol string, chainID uint64 MultiTransactionID: multiTransactionID, Symbol: symbol, AutoDelete: new(bool), + TransactionTo: tx.To(), } // Transaction downloader will delete pending transaction as soon as it is confirmed *pTx.AutoDelete = false diff --git a/walletdatabase/database.go b/walletdatabase/database.go index 5f178652c..04f3f07d5 100644 --- a/walletdatabase/database.go +++ b/walletdatabase/database.go @@ -2,11 +2,19 @@ package walletdatabase import ( "database/sql" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/status-im/status-go/sqlite" "github.com/status-im/status-go/walletdatabase/migrations" ) +const ( + batchSize = 1000 +) + type DbInitializer struct { } @@ -14,7 +22,9 @@ func (a DbInitializer) Initialize(path, password string, kdfIterationsNumber int return InitializeDB(path, password, kdfIterationsNumber) } -var walletCustomSteps = []*sqlite.PostStep{} +var walletCustomSteps = []*sqlite.PostStep{ + {Version: 1721166023, CustomMigration: migrateWalletTransactionToAndEventLogAddress, RollBackVersion: 1720206965}, +} func doMigration(db *sql.DB) error { // Run all the new migrations @@ -35,3 +45,105 @@ func InitializeDB(path, password string, kdfIterationsNumber int) (*sql.DB, erro return db, nil } + +func migrateWalletTransactionToAndEventLogAddress(sqlTx *sql.Tx) error { + var batchEntries [][]interface{} + + // Extract Transaction To addresses and Event Log Address and + // add the information into the new columns + newColumnsAndIndexSetup := ` + ALTER TABLE transfers ADD COLUMN transaction_to BLOB; + ALTER TABLE transfers ADD COLUMN event_log_address BLOB;` + + rowIndex := 0 + mightHaveRows := true + + _, err := sqlTx.Exec(newColumnsAndIndexSetup) + if err != nil { + return err + } + + for mightHaveRows { + var chainID uint64 + var hash common.Hash + var address common.Address + + rows, err := sqlTx.Query(`SELECT hash, address, network_id, tx, log FROM transfers WHERE tx IS NOT NULL LIMIT ? OFFSET ?`, batchSize, rowIndex) + if err != nil { + return err + } + + curProcessed := 0 + for rows.Next() { + tx := &types.Transaction{} + l := &types.Log{} + + // Scan row data into the transaction and log objects + nullableTx := sqlite.JSONBlob{Data: tx} + nullableL := sqlite.JSONBlob{Data: l} + err = rows.Scan(&hash, &address, &chainID, &nullableTx, &nullableL) + if err != nil { + if strings.Contains(err.Error(), "missing required field") { + // Some Arb and Opt transaction types don't contain all required fields + continue + } + rows.Close() + return err + } + + var currentRow []interface{} + + var transactionTo *common.Address + var eventLogAddress *common.Address + + if nullableTx.Valid { + transactionTo = tx.To() + currentRow = append(currentRow, transactionTo) + } else { + currentRow = append(currentRow, nil) + } + + if nullableL.Valid { + eventLogAddress = &l.Address + currentRow = append(currentRow, eventLogAddress) + } else { + currentRow = append(currentRow, nil) + } + + currentRow = append(currentRow, hash, address, chainID) + batchEntries = append(batchEntries, currentRow) + + curProcessed++ + } + rowIndex += curProcessed + + // Check if there was an error in the last rows.Next() + rows.Close() + if err = rows.Err(); err != nil { + return err + } + mightHaveRows = (curProcessed == batchSize) + + // insert extracted data into the new columns + if len(batchEntries) > 0 { + var stmt *sql.Stmt + stmt, err = sqlTx.Prepare(`UPDATE transfers SET transaction_to = ?, event_log_address = ? + WHERE hash = ? AND address = ? AND network_id = ?`) + if err != nil { + return err + } + + for _, dataEntry := range batchEntries { + _, err = stmt.Exec(dataEntry...) + if err != nil { + return err + } + } + + // Reset placeHolders and batchEntries for the next batch + batchEntries = [][]interface{}{} + } + } + + return nil +} diff --git a/walletdatabase/database_test.go b/walletdatabase/database_test.go new file mode 100644 index 000000000..a21d24f5b --- /dev/null +++ b/walletdatabase/database_test.go @@ -0,0 +1,258 @@ +package walletdatabase + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/status-im/status-go/common/dbsetup" + w_common "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/sqlite" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/walletdatabase/migrations" +) + +const ( + erc20ReceiptTestDataTemplate = `{"type":"0x2","root":"0x","status":"0x%d","cumulativeGasUsed":"0x10f8d2c","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000001008000000000000000000000000000000000000002000000000020000000000000000000800000000000000000000000010000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000800000000000000000000","logs":[{"address":"0x98339d8c260052b7ad81c28c16c0b98420f2b46a","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000e2d622c817878da5143bbe06866ca8e35273ba8a"],"data":"0x0000000000000000000000000000000000000000000000000000000000989680","blockNumber":"0x825527","transactionHash":"0xdcaa0fc7fe2e0d1f1343d1f36807344bb4fd26cda62ad8f9d8700e2c458cc79a","transactionIndex":"0x6c","blockHash":"0x69e0f829a557052c134cd7e21c220507d91bc35c316d3c47217e9bd362270274","logIndex":"0xcd","removed":false}],"transactionHash":"0xdcaa0fc7fe2e0d1f1343d1f36807344bb4fd26cda62ad8f9d8700e2c458cc79a","contractAddress":"0x0000000000000000000000000000000000000000","gasUsed":"0x8623","blockHash":"0x69e0f829a557052c134cd7e21c220507d91bc35c316d3c47217e9bd362270274","blockNumber":"0x825527","transactionIndex":"0x6c"}` + erc20TxTestData = `{"type":"0x2","nonce":"0x3d","gasPrice":"0x0","maxPriorityFeePerGas":"0x8c347c90","maxFeePerGas":"0x45964d43a4","gas":"0x8623","value":"0x0","input":"0x40c10f19000000000000000000000000e2d622c817878da5143bbe06866ca8e35273ba8a0000000000000000000000000000000000000000000000000000000000989680","v":"0x0","r":"0xbcac4bb290d48b467bb18ac67e98050b5f316d2c66b2f75dcc1d63a45c905d21","s":"0x10c15517ea9cabd7fe134b270daabf5d2e8335e935d3e021f54a4efaffb37cd2","to":"0x98339d8c260052b7ad81c28c16c0b98420f2b46a","chainId":"0x5","accessList":[],"hash":"0xdcaa0fc7fe2e0d1f1343d1f36807344bb4fd26cda62ad8f9d8700e2c458cc79a"}` + erc20LogTestData = `{"address":"0x98339d8c260052b7ad81c28c16c0b98420f2b46a","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000e2d622c817878da5143bbe06866ca8e35273ba8a"],"data":"0x0000000000000000000000000000000000000000000000000000000000989680","blockNumber":"0x825527","transactionHash":"0xdcaa0fc7fe2e0d1f1343d1f36807344bb4fd26cda62ad8f9d8700e2c458cc79a","transactionIndex":"0x6c","blockHash":"0x69e0f829a557052c134cd7e21c220507d91bc35c316d3c47217e9bd362270274","logIndex":"0xcd","removed":false}` + + ethReceiptTestData = `{ + "type": "0x2", + "root": "0x", + "status": "0x1", + "cumulativeGasUsed": "0x2b461", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logs": [], + "transactionHash": "0x4ac700ee2a1702f82b3cfdc88fd4d91f767b87fea9b929bd6223c6471a5e05b4", + "contractAddress": "0x0000000000000000000000000000000000000000", + "gasUsed": "0x5208", + "blockHash": "0x25fe164361c1cb4ed1b46996f7b5236d3118144529b31fca037fcda1d8ee684d", + "blockNumber": "0x5e3294", + "transactionIndex": "0x3" + }` + ethTxTestData = `{ + "type": "0x2", + "nonce": "0x1", + "gasPrice": "0x0", + "maxPriorityFeePerGas": "0x33", + "maxFeePerGas": "0x3b9aca00", + "gas": "0x55f0", + "value": "0x%s", + "input": "0x", + "v": "0x0", + "r": "0xacc277ce156382d6f333cc8d75a56250778b17f1c6d1676af63cf68d53713986", + "s": "0x32417261484e9796390abb8db13f993965d917836be5cd96df25b9b581de91ec", + "to": "0xbd54a96c0ae19a220c8e1234f54c940dfab34639", + "chainId": "0x1a4", + "accessList": [], + "hash": "0x4ac700ee2a1702f82b3cfdc88fd4d91f767b87fea9b929bd6223c6471a5e05b4" + }` +) + +func TestMigrateWalletTransactionToAndEventLogAddress(t *testing.T) { + openDB := func() (*sql.DB, error) { + return sqlite.OpenDB(sqlite.InMemoryPath, "1234567890", dbsetup.ReducedKDFIterationsNumber) + } + db, err := openDB() + require.NoError(t, err) + + // Migrate until before custom step + err = migrations.MigrateTo(db, walletCustomSteps, 1720206965) + require.NoError(t, err) + + // Validate that transfers table has no status column + exists, err := helpers.ColumnExists(db, "transfers", "transaction_to") + require.NoError(t, err) + require.False(t, exists) + + exists, err = helpers.ColumnExists(db, "transfers", "event_log_address") + require.NoError(t, err) + require.False(t, exists) + + insertTestTransaction := func(index int, txBlob string, receiptBlob string, logBlob string, ethType bool) error { + indexStr := strconv.Itoa(index) + senderStr := strconv.Itoa(index + 1) + + var txValue *string + if txBlob != "" { + txValue = &txBlob + } + var receiptValue *string + if receiptBlob != "" { + receiptValue = &receiptBlob + } + var logValue *string + if logBlob != "" { + logValue = &logBlob + } + entryType := "eth" + if !ethType { + entryType = "erc20" + } + _, err = db.Exec(`INSERT OR IGNORE INTO blocks(network_id, address, blk_number, blk_hash) VALUES (?, ?, ?, ?); + INSERT INTO transfers (hash, address, sender, network_id, tx, receipt, log, blk_hash, type, blk_number, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + index, common.HexToAddress(indexStr), index, common.HexToHash(indexStr), + common.HexToHash(indexStr), common.HexToAddress(indexStr), common.HexToAddress(senderStr), index, txValue, receiptValue, logValue, common.HexToHash(indexStr), entryType, index, index) + return err + } + + // Empty transaction, found the usecase in the test DB + erc20FailReceiptJSON := fmt.Sprintf(erc20ReceiptTestDataTemplate, 0) + erc20SuccessReceiptJSON := fmt.Sprintf(erc20ReceiptTestDataTemplate, 1) + err = insertTestTransaction(2, erc20TxTestData, erc20FailReceiptJSON, erc20LogTestData, false) + require.NoError(t, err) + + err = insertTestTransaction(3, erc20TxTestData, erc20SuccessReceiptJSON, erc20LogTestData, false) + require.NoError(t, err) + + ethZeroValueTxTestData := fmt.Sprintf(ethTxTestData, "0") + ethVeryBigValueTxTestData := fmt.Sprintf(ethTxTestData, "12345678901234567890") + ethOriginalTxTestData := fmt.Sprintf(ethTxTestData, "2386f26fc10000") + + err = insertTestTransaction(4, ethZeroValueTxTestData, ethReceiptTestData, "", true) + require.NoError(t, err) + err = insertTestTransaction(5, ethVeryBigValueTxTestData, "", "", true) + require.NoError(t, err) + err = insertTestTransaction(6, ethOriginalTxTestData, ethReceiptTestData, "", true) + require.NoError(t, err) + + failMigrationSteps := []*sqlite.PostStep{ + { + Version: walletCustomSteps[0].Version, + CustomMigration: func(sqlTx *sql.Tx) error { + return errors.New("failed to run custom migration") + }, + RollBackVersion: walletCustomSteps[0].RollBackVersion, + }, + } + + // Attempt to run test migration 1721166023 and fail in custom step + err = migrations.MigrateTo(db, failMigrationSteps, walletCustomSteps[0].Version) + require.Error(t, err) + + exists, err = helpers.ColumnExists(db, "transfers", "transaction_to") + require.NoError(t, err) + require.False(t, exists) + + // Run test migration 1721166023_extract_tx_to_and_event_log_address..sql + err = migrations.MigrateTo(db, walletCustomSteps, walletCustomSteps[0].Version) + require.NoError(t, err) + + // Validate that the migration was run and transfers table has now new columns + exists, err = helpers.ColumnExists(db, "transfers", "transaction_to") + require.NoError(t, err) + require.True(t, exists) + + exists, err = helpers.ColumnExists(db, "transfers", "event_log_address") + require.NoError(t, err) + require.True(t, exists) + + var ( + transactionTo sql.RawBytes + eventLogAddress sql.RawBytes + ) + + rows, err := db.Query(`SELECT transaction_to, event_log_address + FROM transfers ORDER BY timestamp ASC`) + require.NoError(t, err) + + scanNextData := func() error { + rows.Next() + if rows.Err() != nil { + return rows.Err() + } + err := rows.Scan(&transactionTo, &eventLogAddress) + if err != nil { + return err + } + return nil + } + + validateTransaction := func(tt *types.Transaction, expectedEntryType w_common.Type, tl *types.Log) { + if tt == nil { + require.Empty(t, transactionTo) + require.Empty(t, eventLogAddress) + } else { + if expectedEntryType == w_common.EthTransfer { + require.NotEmpty(t, transactionTo) + parsedTransactionTo := common.BytesToAddress(transactionTo) + require.Equal(t, *tt.To(), parsedTransactionTo) + } else { + require.NotEmpty(t, transactionTo) + parsedTransactionTo := common.BytesToAddress(transactionTo) + require.Equal(t, *tt.To(), parsedTransactionTo) + + require.NotEmpty(t, eventLogAddress) + parsedEventLogAddress := common.BytesToAddress(eventLogAddress) + require.Equal(t, tl.Address, parsedEventLogAddress) + } + } + } + + var successReceipt types.Receipt + err = json.Unmarshal([]byte(erc20SuccessReceiptJSON), &successReceipt) + require.NoError(t, err) + + var failReceipt types.Receipt + err = json.Unmarshal([]byte(erc20FailReceiptJSON), &failReceipt) + require.NoError(t, err) + + var erc20Log types.Log + err = json.Unmarshal([]byte(erc20LogTestData), &erc20Log) + require.NoError(t, err) + + var erc20Tx types.Transaction + err = json.Unmarshal([]byte(erc20TxTestData), &erc20Tx) + require.NoError(t, err) + + err = scanNextData() + require.NoError(t, err) + validateTransaction(&erc20Tx, w_common.Erc20Transfer, &erc20Log) + + err = scanNextData() + require.NoError(t, err) + validateTransaction(&erc20Tx, w_common.Erc20Transfer, &erc20Log) + + var zeroTestTx types.Transaction + err = json.Unmarshal([]byte(ethZeroValueTxTestData), &zeroTestTx) + require.NoError(t, err) + + var ethReceipt types.Receipt + err = json.Unmarshal([]byte(ethReceiptTestData), ðReceipt) + require.NoError(t, err) + + err = scanNextData() + require.NoError(t, err) + validateTransaction(&zeroTestTx, w_common.EthTransfer, nil) + + var bigTestTx types.Transaction + err = json.Unmarshal([]byte(ethVeryBigValueTxTestData), &bigTestTx) + require.NoError(t, err) + + err = scanNextData() + require.NoError(t, err) + validateTransaction(&bigTestTx, w_common.EthTransfer, nil) + + var ethOriginalTestTx types.Transaction + err = json.Unmarshal([]byte(ethOriginalTxTestData), ðOriginalTestTx) + require.NoError(t, err) + + err = scanNextData() + require.NoError(t, err) + validateTransaction(ðOriginalTestTx, w_common.EthTransfer, nil) + + err = scanNextData() + // Validate that we processed all data (no more rows expected) + require.Error(t, err) + + db.Close() +} diff --git a/walletdatabase/migrations/sql/1720206965_add_transaction_input_data_extend_transfers.up.sql b/walletdatabase/migrations/sql/1720206965_add_transaction_input_data_extend_transfers.up.sql new file mode 100644 index 000000000..b5c35f6e1 --- /dev/null +++ b/walletdatabase/migrations/sql/1720206965_add_transaction_input_data_extend_transfers.up.sql @@ -0,0 +1,19 @@ +-- transaction_input_data table stores data for each transaction generated by the wallet +CREATE TABLE IF NOT EXISTS transaction_input_data ( + chain_id UNSIGNED BIGINT NOT NULL, + tx_hash BLOB NOT NULL, + processor_name TEXT NOT NULL, + from_asset TEXT, + from_amount BLOB, + to_asset TEXT, + to_amount BLOB, + side INTEGER, + slippage_bps INTEGER, + approval_amount BLOB, + approval_spender BLOB +); + +CREATE UNIQUE INDEX IF NOT EXISTS transaction_input_data_identify_entry ON transaction_input_data (chain_id, tx_hash); + +-- ALTER TABLE transfers ADD COLUMN approval_amount BLOB; +-- ALTER TABLE transfers ADD COLUMN approval_spender BLOB; diff --git a/walletdatabase/migrations/sql/1721166023_extract_tx_to_and_event_log_address.down.sql b/walletdatabase/migrations/sql/1721166023_extract_tx_to_and_event_log_address.down.sql new file mode 100644 index 000000000..967192b87 --- /dev/null +++ b/walletdatabase/migrations/sql/1721166023_extract_tx_to_and_event_log_address.down.sql @@ -0,0 +1 @@ +-- This migration is in GO code. If GO migration fails this entry serves as rollback to the previous one \ No newline at end of file diff --git a/walletdatabase/migrations/sql/1721166023_extract_tx_to_and_event_log_address.up.sql b/walletdatabase/migrations/sql/1721166023_extract_tx_to_and_event_log_address.up.sql new file mode 100644 index 000000000..6d7274c82 --- /dev/null +++ b/walletdatabase/migrations/sql/1721166023_extract_tx_to_and_event_log_address.up.sql @@ -0,0 +1,13 @@ +-- This migration is done in GO code as a custom step. +-- This file serves as an anchor for the migration system +-- Check migrateWalletTransactionToAndEventLogAddress from walletdatabase/database.go + +-- The following steps are done in GO code: + +-- ALTER TABLE transfers ADD COLUMN transaction_to BLOB; +-- ALTER TABLE transfers ADD COLUMN event_log_address BLOB; + +-- Extract the following: +-- 1) Transaction To field (Receiver address for ETH transfers or +-- address of the contract interacted with) +-- 2) Event Log Address (Address of the contract that emitted the event) diff --git a/walletdatabase/migrations/sql/1721181249_add_transaction_to_to_pending_transactions.up.sql b/walletdatabase/migrations/sql/1721181249_add_transaction_to_to_pending_transactions.up.sql new file mode 100644 index 000000000..7f543438d --- /dev/null +++ b/walletdatabase/migrations/sql/1721181249_add_transaction_to_to_pending_transactions.up.sql @@ -0,0 +1 @@ +ALTER TABLE pending_transactions ADD COLUMN transaction_to BLOB; \ No newline at end of file