diff --git a/services/wallet/activity/activity.go b/services/wallet/activity/activity.go index 03766973d..22849642b 100644 --- a/services/wallet/activity/activity.go +++ b/services/wallet/activity/activity.go @@ -7,10 +7,12 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "strconv" "strings" eth "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/services/wallet/common" @@ -36,6 +38,8 @@ type Entry struct { activityType Type activityStatus Status tokenType TokenType + amountOut *hexutil.Big // Used for activityType SendAT, SwapAT, BridgeAT + amountIn *hexutil.Big // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT } type jsonSerializationTemplate struct { @@ -46,6 +50,8 @@ type jsonSerializationTemplate struct { ActivityType Type `json:"activityType"` ActivityStatus Status `json:"activityStatus"` TokenType TokenType `json:"tokenType"` + AmountOut *hexutil.Big `json:"amountOut"` + AmountIn *hexutil.Big `json:"amountIn"` } func (e *Entry) MarshalJSON() ([]byte, error) { @@ -57,6 +63,8 @@ func (e *Entry) MarshalJSON() ([]byte, error) { ActivityType: e.activityType, ActivityStatus: e.activityStatus, TokenType: e.tokenType, + AmountOut: e.amountOut, + AmountIn: e.amountIn, }) } @@ -72,18 +80,20 @@ func (e *Entry) UnmarshalJSON(data []byte) error { e.id = aux.ID e.timestamp = aux.Timestamp e.activityType = aux.ActivityType + e.amountOut = aux.AmountOut + e.amountIn = aux.AmountIn return nil } -func newActivityEntryWithPendingTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry { - return newActivityEntryWithTransaction(true, transaction, timestamp, activityType, activityStatus) +func newActivityEntryWithPendingTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status, amountIn *hexutil.Big, amountOut *hexutil.Big) Entry { + return newActivityEntryWithTransaction(true, transaction, timestamp, activityType, activityStatus, amountIn, amountOut) } -func newActivityEntryWithSimpleTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry { - return newActivityEntryWithTransaction(false, transaction, timestamp, activityType, activityStatus) +func newActivityEntryWithSimpleTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status, amountIn *hexutil.Big, amountOut *hexutil.Big) Entry { + return newActivityEntryWithTransaction(false, transaction, timestamp, activityType, activityStatus, amountIn, amountOut) } -func newActivityEntryWithTransaction(pending bool, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry { +func newActivityEntryWithTransaction(pending bool, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status, amountIn *hexutil.Big, amountOut *hexutil.Big) Entry { payloadType := SimpleTransactionPT if pending { payloadType = PendingTransactionPT @@ -97,10 +107,12 @@ func newActivityEntryWithTransaction(pending bool, transaction *transfer.Transac activityType: activityType, activityStatus: activityStatus, tokenType: AssetTT, + amountIn: amountIn, + amountOut: amountOut, } } -func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry { +func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status, amountIn *hexutil.Big, amountOut *hexutil.Big) Entry { return Entry{ payloadType: MultiTransactionPT, id: id, @@ -108,6 +120,8 @@ func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, ti activityType: activityType, activityStatus: activityStatus, tokenType: AssetTT, + amountIn: amountIn, + amountOut: amountOut, } } @@ -290,6 +304,9 @@ const ( transfers.sender AS from_address, transfers.address AS to_address, + transfers.amount_padded128hex AS tr_amount, + NULL AS mt_from_amount, + NULL AS mt_to_amount, CASE WHEN transfers.status IS 1 THEN statusSuccess @@ -352,6 +369,10 @@ const ( pending_transactions.from_address AS from_address, pending_transactions.to_address AS to_address, + pending_transactions.value AS tr_amount, + NULL AS mt_from_amount, + NULL AS mt_to_amount, + statusPending AS agg_status, 1 AS agg_count FROM pending_transactions, filter_conditions @@ -387,6 +408,9 @@ const ( NULL as tr_type, multi_transactions.from_address AS from_address, multi_transactions.to_address AS to_address, + NULL AS tr_amount, + multi_transactions.from_amount AS mt_from_amount, + multi_transactions.to_amount AS mt_to_amount, CASE WHEN tr_status.min_status = 1 AND pending_status.count IS NULL THEN statusSuccess @@ -423,6 +447,51 @@ const ( noEntriesInTmpTableSQLValues = "(NULL)" ) +func getTrInAndOutAmounts(activityType Type, trAmount sql.NullString) (inAmount *hexutil.Big, outAmount *hexutil.Big) { + if trAmount.Valid { + amount, ok := new(big.Int).SetString(trAmount.String, 16) + if ok { + switch activityType { + case SendAT: + inAmount = (*hexutil.Big)(big.NewInt(0)) + outAmount = (*hexutil.Big)(amount) + return + case ReceiveAT: + inAmount = (*hexutil.Big)(amount) + outAmount = (*hexutil.Big)(big.NewInt(0)) + return + default: + log.Warn(fmt.Sprintf("unexpected activity type %d", activityType)) + } + } else { + log.Warn(fmt.Sprintf("could not parse amount %s", trAmount.String)) + } + } else { + log.Warn(fmt.Sprintf("invalid transaction amount for type %d", activityType)) + } + inAmount = (*hexutil.Big)(big.NewInt(0)) + outAmount = (*hexutil.Big)(big.NewInt(0)) + return +} + +func getMtInAndOutAmounts(dbFromAmount sql.NullString, dbToAmount sql.NullString) (inAmount *hexutil.Big, outAmount *hexutil.Big) { + if dbFromAmount.Valid && dbToAmount.Valid { + fromAmount, frOk := new(big.Int).SetString(dbFromAmount.String, 16) + toAmount, toOk := new(big.Int).SetString(dbToAmount.String, 16) + if frOk && toOk { + inAmount = (*hexutil.Big)(toAmount) + outAmount = (*hexutil.Big)(fromAmount) + return + } + log.Warn(fmt.Sprintf("could not parse amounts %s %s", dbFromAmount.String, dbToAmount.String)) + } else { + log.Warn("invalid transaction amounts") + } + inAmount = (*hexutil.Big)(big.NewInt(0)) + outAmount = (*hexutil.Big)(big.NewInt(0)) + return +} + // getActivityEntries queries the transfers, pending_transactions, and multi_transactions tables // based on filter parameters and arguments // it returns metadata for all entries ordered by timestamp column @@ -507,7 +576,9 @@ func getActivityEntries(ctx context.Context, db *sql.DB, addresses []eth.Address var dbMtType, dbTrType sql.NullByte var toAddress, fromAddress eth.Address var aggregatedStatus int - err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, ×tamp, &dbMtType, &dbTrType, &fromAddress, &toAddress, &aggregatedStatus, &aggregatedCount) + var dbTrAmount sql.NullString + var dbMtFromAmount, dbMtToAmount sql.NullString + err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, ×tamp, &dbMtType, &dbTrType, &fromAddress, &toAddress, &dbTrAmount, &dbMtFromAmount, &dbMtToAmount, &aggregatedStatus, &aggregatedCount) if err != nil { return nil, err } @@ -530,17 +601,20 @@ func getActivityEntries(ctx context.Context, db *sql.DB, addresses []eth.Address var entry Entry if transferHash != nil && chainID.Valid { activityType, filteredAddress := getActivityType(dbTrType) + inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount) entry = newActivityEntryWithSimpleTransaction( &transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64), Hash: eth.BytesToHash(transferHash), Address: filteredAddress}, - timestamp, activityType, activityStatus) + timestamp, activityType, activityStatus, inAmount, outAmount) } else if pendingHash != nil && chainID.Valid { activityType, _ := getActivityType(dbTrType) + inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount) entry = newActivityEntryWithPendingTransaction(&transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64), Hash: eth.BytesToHash(pendingHash)}, - timestamp, activityType, activityStatus) + timestamp, activityType, activityStatus, inAmount, outAmount) } else if multiTxID.Valid { + mtInAmount, mtOutAmount := getMtInAndOutAmounts(dbMtFromAmount, dbMtToAmount) activityType := multiTransactionTypeToActivityType(transfer.MultiTransactionType(dbMtType.Byte)) entry = NewActivityEntryWithMultiTransaction(transfer.MultiTransactionIDType(multiTxID.Int64), - timestamp, activityType, activityStatus) + timestamp, activityType, activityStatus, mtInAmount, mtOutAmount) } else { return nil, errors.New("invalid row data") } diff --git a/services/wallet/activity/activity_test.go b/services/wallet/activity/activity_test.go index 64c9a1bca..ffcd71e81 100644 --- a/services/wallet/activity/activity_test.go +++ b/services/wallet/activity/activity_test.go @@ -3,6 +3,7 @@ package activity import ( "context" "database/sql" + "math/big" "testing" "github.com/status-im/status-go/appdatabase" @@ -14,6 +15,7 @@ import ( eth "github.com/ethereum/go-ethereum/common" eth_common "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" ) @@ -133,6 +135,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.tr1.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries)) require.True(t, testutils.StructExistsInSlice(Entry{ payloadType: PendingTransactionPT, @@ -142,6 +146,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: PendingAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.pendingTr.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries)) require.True(t, testutils.StructExistsInSlice(Entry{ payloadType: MultiTransactionPT, @@ -151,6 +157,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1.FromAmount)), + amountIn: (*hexutil.Big)(big.NewInt(td.multiTx1.ToAmount)), }, entries)) require.True(t, testutils.StructExistsInSlice(Entry{ payloadType: MultiTransactionPT, @@ -160,6 +168,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: PendingAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx2.FromAmount)), + amountIn: (*hexutil.Big)(big.NewInt(td.multiTx2.ToAmount)), }, entries)) // Ensure the sub-transactions of the multi-transactions are not returned @@ -171,6 +181,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1Tr1.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries)) require.False(t, testutils.StructExistsInSlice(Entry{ payloadType: SimpleTransactionPT, @@ -180,6 +192,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1Tr2.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries)) require.False(t, testutils.StructExistsInSlice(Entry{ payloadType: SimpleTransactionPT, @@ -189,6 +203,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx2Tr1.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries)) require.False(t, testutils.StructExistsInSlice(Entry{ payloadType: SimpleTransactionPT, @@ -198,6 +214,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx2Tr2.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries)) require.False(t, testutils.StructExistsInSlice(Entry{ payloadType: PendingTransactionPT, @@ -207,6 +225,8 @@ func TestGetActivityEntriesAll(t *testing.T) { activityType: SendAT, activityStatus: PendingAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx2PendingTr.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries)) } @@ -286,6 +306,8 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[5].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[0]) require.Equal(t, Entry{ payloadType: MultiTransactionPT, @@ -295,6 +317,8 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1.FromAmount)), + amountIn: (*hexutil.Big)(big.NewInt(td.multiTx1.ToAmount)), }, entries[7]) // Test complete interval @@ -311,6 +335,8 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[2].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[0]) require.Equal(t, Entry{ payloadType: MultiTransactionPT, @@ -320,6 +346,8 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx1.FromAmount)), + amountIn: (*hexutil.Big)(big.NewInt(td.multiTx1.ToAmount)), }, entries[4]) // Test end only @@ -336,6 +364,8 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[2].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[0]) require.Equal(t, Entry{ payloadType: SimpleTransactionPT, @@ -345,6 +375,8 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.tr1.Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[6]) } @@ -381,6 +413,8 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[8].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[0]) require.Equal(t, Entry{ payloadType: SimpleTransactionPT, @@ -390,6 +424,8 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[6].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[2]) // Move window 2 entries forward @@ -405,6 +441,8 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[6].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[0]) require.Equal(t, Entry{ payloadType: SimpleTransactionPT, @@ -414,6 +452,8 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[4].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[2]) // Move window 4 more entries to test filter cap @@ -429,6 +469,8 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[2].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[0]) } @@ -541,6 +583,8 @@ func TestGetActivityEntriesFilterByAddresses(t *testing.T) { activityType: ReceiveAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(0)), + amountIn: (*hexutil.Big)(big.NewInt(trs[4].Value)), }, entries[0]) require.Equal(t, Entry{ payloadType: SimpleTransactionPT, @@ -550,6 +594,8 @@ func TestGetActivityEntriesFilterByAddresses(t *testing.T) { activityType: SendAT, activityStatus: CompleteAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(trs[1].Value)), + amountIn: (*hexutil.Big)(big.NewInt(0)), }, entries[1]) require.Equal(t, Entry{ payloadType: MultiTransactionPT, @@ -559,6 +605,8 @@ func TestGetActivityEntriesFilterByAddresses(t *testing.T) { activityType: SendAT, activityStatus: PendingAS, tokenType: AssetTT, + amountOut: (*hexutil.Big)(big.NewInt(td.multiTx2.FromAmount)), + amountIn: (*hexutil.Big)(big.NewInt(td.multiTx2.ToAmount)), }, entries[2]) } diff --git a/services/wallet/transfer/testutils.go b/services/wallet/transfer/testutils.go index 3fa79fb50..fd9399fb7 100644 --- a/services/wallet/transfer/testutils.go +++ b/services/wallet/transfer/testutils.go @@ -9,6 +9,7 @@ import ( eth_common "github.com/ethereum/go-ethereum/common" "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/testutils" + "github.com/status-im/status-go/sqlite" "github.com/stretchr/testify/require" ) @@ -118,16 +119,18 @@ func InsertTestTransfer(t *testing.T, db *sql.DB, tr *TestTransfer) { tokenType = "erc20" } blkHash := eth_common.HexToHash("4") + value := sqlite.Int64ToPadded128BitsStr(tr.Value) + _, err := db.Exec(` INSERT OR IGNORE INTO blocks( network_id, address, blk_number, blk_hash ) VALUES (?, ?, ?, ?); INSERT INTO transfers (network_id, hash, address, blk_hash, tx, sender, receipt, log, type, blk_number, timestamp, loaded, - multi_transaction_id, base_gas_fee, status - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?)`, + multi_transaction_id, base_gas_fee, status, amount_padded128hex + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?)`, tr.ChainID, tr.To, tr.BlkNumber, blkHash, - tr.ChainID, tr.Hash, tr.To, blkHash, &JSONBlob{}, tr.From, &JSONBlob{}, &JSONBlob{}, tokenType, tr.BlkNumber, tr.Timestamp, tr.MultiTransactionID, tr.Success) + tr.ChainID, tr.Hash, tr.To, blkHash, &JSONBlob{}, tr.From, &JSONBlob{}, &JSONBlob{}, tokenType, tr.BlkNumber, tr.Timestamp, tr.MultiTransactionID, tr.Success, value) require.NoError(t, err) } diff --git a/sqlite/fields.go b/sqlite/fields.go index 19ddb20c2..5e71406b0 100644 --- a/sqlite/fields.go +++ b/sqlite/fields.go @@ -87,3 +87,8 @@ func BigIntToPadded128BitsStr(val *big.Int) *string { *res = fmt.Sprintf("%032s", hexStr) return res } + +func Int64ToPadded128BitsStr(val int64) *string { + res := fmt.Sprintf("%032x", val) + return &res +} diff --git a/sqlite/fields_test.go b/sqlite/fields_test.go index 5da203ce1..a259deac9 100644 --- a/sqlite/fields_test.go +++ b/sqlite/fields_test.go @@ -57,3 +57,35 @@ func TestBigIntToPadded128BitsStr(t *testing.T) { }) } } + +func TestInt64ToPadded128BitsStr(t *testing.T) { + testCases := []struct { + name string + input int64 + expected *string + }{ + { + name: "case nonzero", + input: 123456, + expected: strToPtr("0000000000000000000000000001e240"), + }, + { + name: "case zero", + input: 0, + expected: strToPtr("00000000000000000000000000000000"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := Int64ToPadded128BitsStr(tc.input) + if result != nil && tc.expected != nil { + if *result != *tc.expected { + t.Errorf("expected %s, got %s", *tc.expected, *result) + } + } else if result != nil || tc.expected != nil { + t.Errorf("expected %v, got %v", tc.expected, result) + } + }) + } +}