From e78a73bd9fa77e925b403bfc48da96995c1cc1ae Mon Sep 17 00:00:00 2001 From: Stefan Date: Thu, 11 May 2023 10:50:07 +0300 Subject: [PATCH] feat(Wallet) complete the filter API It uses the current data only and doesn't extend with new types or include new features in activity sources DBs. Major changes: - Partially filter by chain IDs - Partially filter by Status if it is the case - Partially filter by token types - Filter by counterparty addresses - Use wallet accounts for TO/FROM instead of filters Closes: #10634 --- multiaccounts/accounts/testutils.go | 19 + services/wallet/activity/activity.go | 364 +++++++++++---- services/wallet/activity/activity_test.go | 538 ++++++++++++++++------ services/wallet/activity/filter.go | 65 ++- services/wallet/api.go | 6 +- services/wallet/testutils/helpers.go | 4 + services/wallet/transfer/database.go | 2 +- services/wallet/transfer/database_test.go | 4 +- services/wallet/transfer/query.go | 1 + services/wallet/transfer/testutils.go | 54 ++- services/wallet/transfer/transaction.go | 7 +- 11 files changed, 797 insertions(+), 267 deletions(-) create mode 100644 multiaccounts/accounts/testutils.go diff --git a/multiaccounts/accounts/testutils.go b/multiaccounts/accounts/testutils.go new file mode 100644 index 000000000..1444ceb20 --- /dev/null +++ b/multiaccounts/accounts/testutils.go @@ -0,0 +1,19 @@ +package accounts + +import ( + "database/sql" + "testing" + + "github.com/stretchr/testify/require" +) + +func AddTestAccounts(t *testing.T, db *sql.DB, accounts []*Account) { + d, err := NewDB(db) + require.NoError(t, err) + + err = d.SaveAccounts(accounts) + require.NoError(t, err) + res, err := d.GetAccounts() + require.NoError(t, err) + require.Equal(t, accounts, res) +} diff --git a/services/wallet/activity/activity.go b/services/wallet/activity/activity.go index 3896d6886..8183afc1d 100644 --- a/services/wallet/activity/activity.go +++ b/services/wallet/activity/activity.go @@ -9,12 +9,19 @@ import ( "strconv" "strings" - "github.com/ethereum/go-ethereum/common" + eth "github.com/ethereum/go-ethereum/common" + + "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/transfer" + + "golang.org/x/exp/constraints" ) type PayloadType = int +// Beware if adding/removing please check if affected and update the functions below +// - NewActivityEntryWithTransaction +// - multiTransactionTypeToActivityType const ( MultiTransactionPT PayloadType = iota + 1 SimpleTransactionPT @@ -22,29 +29,34 @@ const ( ) type Entry struct { - // TODO: rename in payloadType - transactionType PayloadType - transaction *transfer.TransactionIdentity - id transfer.MultiTransactionIDType - timestamp int64 - activityType Type + payloadType PayloadType + transaction *transfer.TransactionIdentity + id transfer.MultiTransactionIDType + timestamp int64 + activityType Type + activityStatus Status + tokenType TokenType } type jsonSerializationTemplate struct { - TransactionType PayloadType `json:"transactionType"` - Transaction *transfer.TransactionIdentity `json:"transaction"` - ID transfer.MultiTransactionIDType `json:"id"` - Timestamp int64 `json:"timestamp"` - ActivityType Type `json:"activityType"` + PayloadType PayloadType `json:"payloadType"` + Transaction *transfer.TransactionIdentity `json:"transaction"` + ID transfer.MultiTransactionIDType `json:"id"` + Timestamp int64 `json:"timestamp"` + ActivityType Type `json:"activityType"` + ActivityStatus Status `json:"activityStatus"` + TokenType TokenType `json:"tokenType"` } func (e *Entry) MarshalJSON() ([]byte, error) { return json.Marshal(jsonSerializationTemplate{ - TransactionType: e.transactionType, - Transaction: e.transaction, - ID: e.id, - Timestamp: e.timestamp, - ActivityType: e.activityType, + PayloadType: e.payloadType, + Transaction: e.transaction, + ID: e.id, + Timestamp: e.timestamp, + ActivityType: e.activityType, + ActivityStatus: e.activityStatus, + TokenType: e.tokenType, }) } @@ -55,7 +67,7 @@ func (e *Entry) UnmarshalJSON(data []byte) error { return err } - e.transactionType = aux.TransactionType + e.payloadType = aux.PayloadType e.transaction = aux.Transaction e.id = aux.ID e.timestamp = aux.Timestamp @@ -63,31 +75,35 @@ func (e *Entry) UnmarshalJSON(data []byte) error { return nil } -func NewActivityEntryWithTransaction(transactionType PayloadType, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type) Entry { - if transactionType != SimpleTransactionPT && transactionType != PendingTransactionPT { +func NewActivityEntryWithTransaction(payloadType PayloadType, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry { + if payloadType != SimpleTransactionPT && payloadType != PendingTransactionPT { panic("invalid transaction type") } return Entry{ - transactionType: transactionType, - transaction: transaction, - id: 0, - timestamp: timestamp, - activityType: activityType, + payloadType: payloadType, + transaction: transaction, + id: 0, + timestamp: timestamp, + activityType: activityType, + activityStatus: activityStatus, + tokenType: AssetTT, } } -func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type) Entry { +func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry { return Entry{ - transactionType: MultiTransactionPT, - id: id, - timestamp: timestamp, - activityType: activityType, + payloadType: MultiTransactionPT, + id: id, + timestamp: timestamp, + activityType: activityType, + activityStatus: activityStatus, + tokenType: AssetTT, } } -func (e *Entry) TransactionType() PayloadType { - return e.transactionType +func (e *Entry) PayloadType() PayloadType { + return e.payloadType } func multiTransactionTypeToActivityType(mtType transfer.MultiTransactionType) Type { @@ -101,7 +117,7 @@ func multiTransactionTypeToActivityType(mtType transfer.MultiTransactionType) Ty panic("unknown multi transaction type") } -func typesContain(slice []Type, item Type) bool { +func sliceContains[T constraints.Ordered](slice []T, item T) bool { for _, a := range slice { if a == item { return true @@ -110,31 +126,33 @@ func typesContain(slice []Type, item Type) bool { return false } -func joinMTTypes(types []transfer.MultiTransactionType) string { - var sb strings.Builder - for i, val := range types { - if i > 0 { - sb.WriteString(",") - } - sb.WriteString(strconv.Itoa(int(val))) +func joinItems[T interface{}](items []T, itemConversion func(T) string) string { + if len(items) == 0 { + return "" } + var sb strings.Builder + if itemConversion == nil { + itemConversion = func(item T) string { + return fmt.Sprintf("%v", item) + } + } + for i, item := range items { + if i == 0 { + sb.WriteString("(") + } else { + sb.WriteString("),(") + } + sb.WriteString(itemConversion(item)) + } + sb.WriteString(")") return sb.String() } -func joinAddresses(addresses []common.Address) string { - var sb strings.Builder - for i, address := range addresses { - if i == 0 { - sb.WriteString("('") - } else { - sb.WriteString("'),('") - } - sb.WriteString(strings.ToUpper(hex.EncodeToString(address[:]))) - } - sb.WriteString("')") - - return sb.String() +func joinAddresses(addresses []eth.Address) string { + return joinItems(addresses, func(a eth.Address) string { + return fmt.Sprintf("'%s'", strings.ToUpper(hex.EncodeToString(a[:]))) + }) } func activityTypesToMultiTransactionTypes(trTypes []Type) []transfer.MultiTransactionType { @@ -155,11 +173,25 @@ func activityTypesToMultiTransactionTypes(trTypes []Type) []transfer.MultiTransa return mtTypes } -// TODO: extend with SEND/RECEIVE for transfers and pending_transactions -// TODO: clarify if we include sender and receiver in pending_transactions as we do for transfers -// TODO optimization: consider implementing nullable []byte instead of using strings for addresses -// Query includes duplicates, will return multiple rows for the same transaction -const queryFormatString = ` +const ( + fromTrType = byte(1) + //toTrType = byte(2) + + // TODO: Multi-transaction network information is missing in filtering + // TODO: extract token code for non transfer type eth + // TODO optimization: consider implementing nullable []byte instead of using strings for addresses + // + // Query includes duplicates, will return multiple rows for the same transaction if both to and from addresses + // are in the address list. + // + // The addresses list will have priority in deciding the source of the duplicate transaction. However, if the + // if the addresses list is empty, and all addresses should be included, the accounts table will be used + // see filter_addresses temp table is used + // The switch for tr_type is used to de-conflict the source for the two entries for the same transaction + // + // UNION ALL is used to avoid the overhead of DISTINCT given that we don't expect to have duplicate entries outside + // the sender and receiver addresses being in the list which is handled separately + queryFormatString = ` WITH filter_conditions AS ( SELECT ? AS startFilterDisabled, @@ -171,9 +203,26 @@ const queryFormatString = ` ? AS filterActivityTypeSend, ? AS filterActivityTypeReceive, - ? AS filterAllAddresses + ? AS filterAllAddresses, + ? AS filterAllToAddresses, + ? AS filterAllActivityStatus, + ? AS includeAllTokenTypeAssets, + ? AS statusIsPending, + + ? AS includeAllNetworks ), filter_addresses(address) AS ( + SELECT HEX(address) FROM accounts WHERE (SELECT filterAllAddresses FROM filter_conditions) != 0 + UNION ALL + SELECT * FROM (VALUES %s) WHERE (SELECT filterAllAddresses FROM filter_conditions) = 0 + ), + filter_to_addresses(address) AS ( + VALUES %s + ), + filter_assets(token_code) AS ( + VALUES %s + ), + filter_networks(network_id) AS ( VALUES %s ) SELECT @@ -183,12 +232,48 @@ const queryFormatString = ` 0 AS multi_tx_id, transfers.timestamp AS timestamp, NULL AS mt_type, - HEX(transfers.address) AS owner_address + + CASE + WHEN from_join.address IS NOT NULL AND to_join.address IS NULL THEN 1 + WHEN to_join.address IS NOT NULL AND from_join.address IS NULL THEN 2 + WHEN from_join.address IS NOT NULL AND to_join.address IS NOT NULL THEN + CASE + WHEN from_join.address < to_join.address THEN 1 + ELSE 2 + END + ELSE NULL + END as tr_type, + + transfers.sender AS from_address, + transfers.address AS to_address FROM transfers, filter_conditions + LEFT JOIN + filter_addresses from_join ON HEX(transfers.sender) = from_join.address + LEFT JOIN + filter_addresses to_join ON HEX(transfers.address) = to_join.address WHERE transfers.multi_transaction_id = 0 - AND ((startFilterDisabled OR timestamp >= startTimestamp) AND (endFilterDisabled OR timestamp <= endTimestamp)) - AND (filterActivityTypeAll OR (filterActivityTypeSend AND (filterAllAddresses OR (HEX(transfers.sender) IN filter_addresses))) OR (filterActivityTypeReceive AND (filterAllAddresses OR (HEX(transfers.address) IN filter_addresses)))) - AND (filterAllAddresses OR (HEX(transfers.sender) IN filter_addresses) OR (HEX(transfers.address) IN filter_addresses)) + AND ((startFilterDisabled OR timestamp >= startTimestamp) + AND (endFilterDisabled OR timestamp <= endTimestamp) + ) + AND (filterActivityTypeAll + OR (filterActivityTypeSend + AND (filterAllAddresses + OR (HEX(transfers.sender) IN filter_addresses) + ) + ) + OR (filterActivityTypeReceive + AND (filterAllAddresses OR (HEX(transfers.address) IN filter_addresses)) + ) + ) + AND (filterAllAddresses + OR (HEX(transfers.sender) IN filter_addresses) + OR (HEX(transfers.address) IN filter_addresses) + ) + AND (filterAllToAddresses + OR (HEX(transfers.address) IN filter_to_addresses) + ) + AND (includeAllTokenTypeAssets OR (transfers.type = "eth" AND ("ETH" IN filter_assets))) + AND (includeAllNetworks OR (transfers.network_id IN filter_networks)) UNION ALL @@ -199,12 +284,40 @@ const queryFormatString = ` 0 AS multi_tx_id, pending_transactions.timestamp AS timestamp, NULL AS mt_type, - NULL AS owner_address + + CASE + WHEN from_join.address IS NOT NULL AND to_join.address IS NULL THEN 1 + WHEN to_join.address IS NOT NULL AND from_join.address IS NULL THEN 2 + WHEN from_join.address IS NOT NULL AND to_join.address IS NOT NULL THEN + CASE + WHEN from_join.address < to_join.address THEN 1 + ELSE 2 + END + ELSE NULL + END as tr_type, + + pending_transactions.from_address AS from_address, + pending_transactions.to_address AS to_address FROM pending_transactions, filter_conditions + LEFT JOIN + filter_addresses from_join ON HEX(pending_transactions.from_address) = from_join.address + LEFT JOIN + filter_addresses to_join ON HEX(pending_transactions.to_address) = to_join.address WHERE pending_transactions.multi_transaction_id = 0 - AND ((startFilterDisabled OR timestamp >= startTimestamp) AND (endFilterDisabled OR timestamp <= endTimestamp)) + AND (filterAllActivityStatus OR statusIsPending) + AND ((startFilterDisabled OR timestamp >= startTimestamp) + AND (endFilterDisabled OR timestamp <= endTimestamp) + ) AND (filterActivityTypeAll OR filterActivityTypeSend) - AND (filterAllAddresses OR (HEX(pending_transactions.from_address) IN filter_addresses) OR (HEX(pending_transactions.to_address) IN filter_addresses)) + AND (filterAllAddresses + OR (HEX(pending_transactions.from_address) IN filter_addresses) + OR (HEX(pending_transactions.to_address) IN filter_addresses) + ) + AND (filterAllToAddresses + OR (HEX(pending_transactions.to_address) IN filter_to_addresses) + ) + AND (includeAllTokenTypeAssets OR (UPPER(pending_transactions.symbol) IN filter_assets)) + AND (includeAllNetworks OR (pending_transactions.network_id IN filter_networks)) UNION ALL @@ -215,41 +328,89 @@ const queryFormatString = ` multi_transactions.ROWID AS multi_tx_id, multi_transactions.timestamp AS timestamp, multi_transactions.type AS mt_type, - NULL AS owner_address + NULL as tr_type, + multi_transactions.from_address AS from_address, + multi_transactions.to_address AS to_address FROM multi_transactions, filter_conditions - WHERE ((startFilterDisabled OR timestamp >= startTimestamp) AND (endFilterDisabled OR timestamp <= endTimestamp)) + WHERE ((startFilterDisabled OR timestamp >= startTimestamp) + AND (endFilterDisabled OR timestamp <= endTimestamp) + ) AND (filterActivityTypeAll OR (multi_transactions.type IN (%s))) - AND (filterAllAddresses OR (HEX(multi_transactions.from_address) IN filter_addresses) OR (HEX(multi_transactions.to_address) IN filter_addresses)) + AND (filterAllAddresses + OR (HEX(multi_transactions.from_address) IN filter_addresses) + OR (HEX(multi_transactions.to_address) IN filter_addresses) + ) + AND (filterAllToAddresses + OR (HEX(multi_transactions.to_address) IN filter_to_addresses) + ) + AND (includeAllTokenTypeAssets OR (UPPER(multi_transactions.from_asset) IN filter_assets) OR (UPPER(multi_transactions.to_asset) IN filter_assets)) ORDER BY timestamp DESC LIMIT ? OFFSET ?` -func GetActivityEntries(db *sql.DB, addresses []common.Address, chainIDs []uint64, filter Filter, offset int, limit int) ([]Entry, error) { - // Query the transfers, pending_transactions, and multi_transactions tables ordered by timestamp column + noEntriesInTmpTableSQLValues = "(NULL)" +) + +// GetActivityEntries returns query the transfers, pending_transactions, and multi_transactions tables +// based on filter parameters and arguments +// it returns metadata for all entries ordered by timestamp column +// +// Adding a no-limit option was never considered or required. +func GetActivityEntries(db *sql.DB, addresses []eth.Address, chainIDs []common.ChainID, filter Filter, offset int, limit int) ([]Entry, error) { + // TODO: filter collectibles after they are added to multi_transactions table + if len(filter.Tokens.EnabledTypes) > 0 && !sliceContains(filter.Tokens.EnabledTypes, AssetTT) { + // For now we deal only with assets so return empty result + return []Entry{}, nil + } + + includeAllTokenTypeAssets := (len(filter.Tokens.EnabledTypes) == 0 || + sliceContains(filter.Tokens.EnabledTypes, AssetTT)) && len(filter.Tokens.Assets) == 0 + + assets := noEntriesInTmpTableSQLValues + if !includeAllTokenTypeAssets { + assets = joinItems(filter.Tokens.Assets, func(item TokenCode) string { return fmt.Sprintf("'%v'", item) }) + } + + includeAllNetworks := len(chainIDs) == 0 + networks := noEntriesInTmpTableSQLValues + if !includeAllNetworks { + networks = joinItems(chainIDs, nil) + } - // TODO: finish filter: chainIDs, statuses, tokenTypes, counterpartyAddresses - // TODO: use all accounts list for detecting SEND/RECEIVE instead of the current addresses list; also change activityType detection in transfer part startFilterDisabled := !(filter.Period.StartTimestamp > 0) endFilterDisabled := !(filter.Period.EndTimestamp > 0) - filterActivityTypeAll := typesContain(filter.Types, AllAT) || len(filter.Types) == 0 + filterActivityTypeAll := len(filter.Types) == 0 filterAllAddresses := len(addresses) == 0 + filterAllToAddresses := len(filter.CounterpartyAddresses) == 0 + includeAllStatuses := len(filter.Statuses) == 0 - //fmt.Println("@dd filter: timeEnabled", filter.Period.StartTimestamp, filter.Period.EndTimestamp, "; type", filter.Types, "offset", offset, "limit", limit) + statusIsPending := false + if !includeAllStatuses { + statusIsPending = sliceContains(filter.Statuses, PendingAS) + } - joinedAddresses := "(NULL)" + involvedAddresses := noEntriesInTmpTableSQLValues if !filterAllAddresses { - joinedAddresses = joinAddresses(addresses) + involvedAddresses = joinAddresses(addresses) + } + toAddresses := noEntriesInTmpTableSQLValues + if !filterAllToAddresses { + toAddresses = joinAddresses(filter.CounterpartyAddresses) } mtTypes := activityTypesToMultiTransactionTypes(filter.Types) - joinedMTTypes := joinMTTypes(mtTypes) + joinedMTTypes := joinItems(mtTypes, func(t transfer.MultiTransactionType) string { + return strconv.Itoa(int(t)) + }) - queryString := fmt.Sprintf(queryFormatString, joinedAddresses, joinedMTTypes) + queryString := fmt.Sprintf(queryFormatString, involvedAddresses, toAddresses, assets, networks, + joinedMTTypes) rows, err := db.Query(queryString, startFilterDisabled, filter.Period.StartTimestamp, endFilterDisabled, filter.Period.EndTimestamp, - filterActivityTypeAll, typesContain(filter.Types, SendAT), typesContain(filter.Types, ReceiveAT), - filterAllAddresses, + filterActivityTypeAll, sliceContains(filter.Types, SendAT), sliceContains(filter.Types, ReceiveAT), + filterAllAddresses, filterAllToAddresses, includeAllStatuses, includeAllTokenTypeAssets, statusIsPending, + includeAllNetworks, limit, offset) if err != nil { return nil, err @@ -261,30 +422,41 @@ func GetActivityEntries(db *sql.DB, addresses []common.Address, chainIDs []uint6 var transferHash, pendingHash []byte var chainID, multiTxID sql.NullInt64 var timestamp int64 - var dbActivityType sql.NullByte - var dbAddress sql.NullString - err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, ×tamp, &dbActivityType, &dbAddress) + var dbMtType, dbTrType sql.NullByte + var toAddress, fromAddress eth.Address + err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, ×tamp, &dbMtType, &dbTrType, &fromAddress, &toAddress) if err != nil { return nil, err } + getActivityType := func(trType sql.NullByte) (activityType Type, filteredAddress eth.Address) { + if trType.Valid && trType.Byte == fromTrType { + return SendAT, fromAddress + } + // Don't expect this to happen due to trType = NULL outside of tests + return ReceiveAT, toAddress + } + var entry Entry if transferHash != nil && chainID.Valid { - var activityType Type = SendAT - thisAddress := common.HexToAddress(dbAddress.String) - for _, address := range addresses { - if address == thisAddress { - activityType = ReceiveAT - } - } - entry = NewActivityEntryWithTransaction(SimpleTransactionPT, &transfer.TransactionIdentity{ChainID: uint64(chainID.Int64), Hash: common.BytesToHash(transferHash), Address: thisAddress}, timestamp, activityType) + // TODO: extend DB with status in order to filter by status. The status has to be extracted from the receipt upon downloading + activityStatus := FinalizedAS + activityType, filteredAddress := getActivityType(dbTrType) + entry = NewActivityEntryWithTransaction(SimpleTransactionPT, + &transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64), Hash: eth.BytesToHash(transferHash), Address: filteredAddress}, + timestamp, activityType, activityStatus) } else if pendingHash != nil && chainID.Valid { - var activityType Type = SendAT - entry = NewActivityEntryWithTransaction(PendingTransactionPT, &transfer.TransactionIdentity{ChainID: uint64(chainID.Int64), Hash: common.BytesToHash(pendingHash)}, timestamp, activityType) + activityStatus := PendingAS + activityType, _ := getActivityType(dbTrType) + entry = NewActivityEntryWithTransaction(PendingTransactionPT, + &transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64), Hash: eth.BytesToHash(pendingHash)}, + timestamp, activityType, activityStatus) } else if multiTxID.Valid { - activityType := multiTransactionTypeToActivityType(transfer.MultiTransactionType(dbActivityType.Byte)) + activityType := multiTransactionTypeToActivityType(transfer.MultiTransactionType(dbMtType.Byte)) + // TODO: aggregate status from all sub-transactions + activityStatus := FinalizedAS entry = NewActivityEntryWithMultiTransaction(transfer.MultiTransactionIDType(multiTxID.Int64), - timestamp, activityType) + timestamp, activityType, activityStatus) } 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 92bea450e..862df8b70 100644 --- a/services/wallet/activity/activity_test.go +++ b/services/wallet/activity/activity_test.go @@ -5,10 +5,14 @@ import ( "testing" "github.com/status-im/status-go/appdatabase" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/multiaccounts/accounts" + "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/testutils" "github.com/status-im/status-go/services/wallet/transfer" - "github.com/ethereum/go-ethereum/common" + eth "github.com/ethereum/go-ethereum/common" + eth_common "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -22,15 +26,6 @@ func setupTestActivityDB(t *testing.T) (db *sql.DB, close func()) { } } -func insertTestPendingTransaction(t *testing.T, db *sql.DB, tr *transfer.TestTransaction) { - _, 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, ?, '', 'test', '', ?)`, - tr.ChainID, tr.Hash, tr.Timestamp, tr.From, tr.To, tr.Value, tr.MultiTransactionID) - require.NoError(t, err) -} - type testData struct { tr1 transfer.TestTransaction // index 1 pendingTr transfer.TestTransaction // index 2 @@ -51,12 +46,15 @@ func fillTestData(t *testing.T, db *sql.DB) (td testData) { transfer.InsertTestTransfer(t, db, &td.tr1) td.pendingTr = trs[1] - insertTestPendingTransaction(t, db, &td.pendingTr) + transfer.InsertTestPendingTransaction(t, db, &td.pendingTr) td.singletonMTr = trs[2] + td.singletonMTr.FromToken = testutils.SntSymbol + td.singletonMTr.ToToken = testutils.DaiSymbol td.singletonMTID = transfer.InsertTestMultiTransaction(t, db, &td.singletonMTr) td.mTr = trs[3] + td.mTr.ToToken = testutils.SntSymbol td.mTrID = transfer.InsertTestMultiTransaction(t, db, &td.mTr) td.subTr = trs[4] @@ -65,7 +63,7 @@ func fillTestData(t *testing.T, db *sql.DB) (td testData) { td.subPendingTr = trs[5] td.subPendingTr.MultiTransactionID = td.mTrID - insertTestPendingTransaction(t, db, &td.subPendingTr) + transfer.InsertTestPendingTransaction(t, db, &td.subPendingTr) return } @@ -76,7 +74,7 @@ func TestGetActivityEntriesAll(t *testing.T) { td := fillTestData(t, db) var filter Filter - entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 10) + entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 10) require.NoError(t, err) require.Equal(t, 4, len(entries)) @@ -88,48 +86,60 @@ func TestGetActivityEntriesAll(t *testing.T) { } require.True(t, testutils.StructExistsInSlice(Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To}, - id: td.tr1.MultiTransactionID, - timestamp: td.tr1.Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To}, + id: td.tr1.MultiTransactionID, + timestamp: td.tr1.Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries)) require.True(t, testutils.StructExistsInSlice(Entry{ - transactionType: PendingTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: td.pendingTr.ChainID, Hash: td.pendingTr.Hash}, - id: td.pendingTr.MultiTransactionID, - timestamp: td.pendingTr.Timestamp, - activityType: SendAT, + payloadType: PendingTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: td.pendingTr.ChainID, Hash: td.pendingTr.Hash}, + id: td.pendingTr.MultiTransactionID, + timestamp: td.pendingTr.Timestamp, + activityType: ReceiveAT, + activityStatus: PendingAS, + tokenType: AssetTT, }, entries)) require.True(t, testutils.StructExistsInSlice(Entry{ - transactionType: MultiTransactionPT, - transaction: nil, - id: td.singletonMTID, - timestamp: td.singletonMTr.Timestamp, - activityType: SendAT, + payloadType: MultiTransactionPT, + transaction: nil, + id: td.singletonMTID, + timestamp: td.singletonMTr.Timestamp, + activityType: SendAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries)) require.True(t, testutils.StructExistsInSlice(Entry{ - transactionType: MultiTransactionPT, - transaction: nil, - id: td.mTrID, - timestamp: td.mTr.Timestamp, - activityType: SendAT, + payloadType: MultiTransactionPT, + transaction: nil, + id: td.mTrID, + timestamp: td.mTr.Timestamp, + activityType: SendAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries)) // Ensure the sub-transactions of the multi-transactions are not returned require.False(t, testutils.StructExistsInSlice(Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: td.subTr.ChainID, Hash: td.subTr.Hash, Address: td.subTr.To}, - id: td.subTr.MultiTransactionID, - timestamp: td.subTr.Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: td.subTr.ChainID, Hash: td.subTr.Hash, Address: td.subTr.To}, + id: td.subTr.MultiTransactionID, + timestamp: td.subTr.Timestamp, + activityType: SendAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries)) require.False(t, testutils.StructExistsInSlice(Entry{ - transactionType: PendingTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: td.subPendingTr.ChainID, Hash: td.subPendingTr.Hash}, - id: td.subPendingTr.MultiTransactionID, - timestamp: td.subPendingTr.Timestamp, - activityType: SendAT, + payloadType: PendingTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: td.subPendingTr.ChainID, Hash: td.subPendingTr.Hash}, + id: td.subPendingTr.MultiTransactionID, + timestamp: td.subPendingTr.Timestamp, + activityType: SendAT, + activityStatus: PendingAS, + tokenType: AssetTT, }, entries)) } @@ -146,14 +156,40 @@ func TestGetActivityEntriesWithSameTransactionForSenderAndReceiverInDB(t *testin prevTo := receiverTr.To receiverTr.To = td.tr1.From receiverTr.From = prevTo + + // TODO: test also when there is a transaction in the other direction + + // Ensure they are the oldest transactions (last in the list) and we have a consistent order + receiverTr.Timestamp-- transfer.InsertTestTransfer(t, db, &receiverTr) var filter Filter - entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 10) + entries, err := GetActivityEntries(db, []eth.Address{td.tr1.From, receiverTr.From}, []common.ChainID{}, filter, 0, 10) + require.NoError(t, err) + require.Equal(t, 2, len(entries)) + + // Check that the transaction are labeled alternatively as send and receive + require.Equal(t, ReceiveAT, entries[1].activityType) + require.NotEqual(t, eth.Address{}, entries[1].transaction.Address) + require.Equal(t, receiverTr.To, entries[1].transaction.Address) + + require.Equal(t, SendAT, entries[0].activityType) + require.NotEqual(t, eth.Address{}, entries[0].transaction.Address) + require.Equal(t, td.tr1.From, entries[0].transaction.Address) + + // add accounts to DB for proper detection of sender/receiver in all cases + accounts.AddTestAccounts(t, db, []*accounts.Account{ + {Address: types.Address(td.tr1.From), Chat: false, Wallet: true}, + {Address: types.Address(receiverTr.From)}, + }) + + entries, err = GetActivityEntries(db, []eth.Address{}, []common.ChainID{}, filter, 0, 10) require.NoError(t, err) - // TODO: decide how should we handle this case filter out or include it in the result - // For now we include both. Can be changed by using UNION instead of UNION ALL in the query or by filtering out require.Equal(t, 5, len(entries)) + + // Check that the transaction are labeled alternatively as send and receive + require.Equal(t, ReceiveAT, entries[4].activityType) + require.Equal(t, SendAT, entries[3].activityType) } func TestGetActivityEntriesFilterByTime(t *testing.T) { @@ -170,65 +206,78 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) { // Test start only var filter Filter filter.Period.StartTimestamp = td.singletonMTr.Timestamp - entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15) + filter.Period.EndTimestamp = NoLimitTimestampForPeriod + entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) require.NoError(t, err) require.Equal(t, 8, len(entries)) // Check start and end content require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[5].ChainID, Hash: trs[5].Hash, Address: trs[5].To}, - id: 0, - timestamp: trs[5].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[5].ChainID, Hash: trs[5].Hash, Address: trs[5].To}, + id: 0, + timestamp: trs[5].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[0]) require.Equal(t, Entry{ - transactionType: MultiTransactionPT, - transaction: nil, - id: td.singletonMTID, - timestamp: td.singletonMTr.Timestamp, - activityType: SendAT, + payloadType: MultiTransactionPT, + transaction: nil, + id: td.singletonMTID, + timestamp: td.singletonMTr.Timestamp, + activityType: SendAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[7]) // Test complete interval filter.Period.EndTimestamp = trs[2].Timestamp - entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15) + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) require.NoError(t, err) require.Equal(t, 5, len(entries)) // Check start and end content require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To}, - id: 0, - timestamp: trs[2].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To}, + id: 0, + timestamp: trs[2].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[0]) require.Equal(t, Entry{ - transactionType: MultiTransactionPT, - transaction: nil, - id: td.singletonMTID, - timestamp: td.singletonMTr.Timestamp, - activityType: SendAT, + payloadType: MultiTransactionPT, + transaction: nil, + id: td.singletonMTID, + timestamp: td.singletonMTr.Timestamp, + activityType: SendAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[4]) // Test end only - filter.Period.StartTimestamp = 0 - entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15) + filter.Period.StartTimestamp = NoLimitTimestampForPeriod + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) require.NoError(t, err) require.Equal(t, 7, len(entries)) // Check start and end content require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To}, - id: 0, - timestamp: trs[2].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To}, + id: 0, + timestamp: trs[2].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[0]) require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To}, - id: 0, - timestamp: td.tr1.Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To}, + id: 0, + timestamp: td.tr1.Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[6]) } @@ -244,63 +293,73 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) { var filter Filter // Get all - entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 5) + entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 5) require.NoError(t, err) require.Equal(t, 5, len(entries)) // Get time based interval filter.Period.StartTimestamp = trs[2].Timestamp filter.Period.EndTimestamp = trs[8].Timestamp - entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 3) + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 3) require.NoError(t, err) require.Equal(t, 3, len(entries)) // Check start and end content require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[8].ChainID, Hash: trs[8].Hash, Address: trs[8].To}, - id: 0, - timestamp: trs[8].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[8].ChainID, Hash: trs[8].Hash, Address: trs[8].To}, + id: 0, + timestamp: trs[8].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[0]) require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To}, - id: 0, - timestamp: trs[6].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To}, + id: 0, + timestamp: trs[6].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[2]) // Move window 2 entries forward - entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 2, 3) + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 2, 3) require.NoError(t, err) require.Equal(t, 3, len(entries)) // Check start and end content require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To}, - id: 0, - timestamp: trs[6].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To}, + id: 0, + timestamp: trs[6].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[0]) require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To}, - id: 0, - timestamp: trs[4].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To}, + id: 0, + timestamp: trs[4].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[2]) // Move window 4 more entries to test filter cap - entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 6, 3) + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 6, 3) require.NoError(t, err) require.Equal(t, 1, len(entries)) // Check start and end content require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To}, - id: 0, - timestamp: trs[2].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To}, + id: 0, + timestamp: trs[2].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[0]) } @@ -310,7 +369,7 @@ func TestGetActivityEntriesFilterByType(t *testing.T) { // Adds 4 extractable transactions fillTestData(t, db) - // Add 6 extractable transactions: one MultiTransactionSwap, two MultiTransactionBridge rest Send + // Add 6 extractable transactions: one MultiTransactionSwap, two MultiTransactionBridge rest MultiTransactionSend trs := transfer.GenerateTestTransactions(t, db, 6, 6) trs[1].MultiTransactionType = transfer.MultiTransactionBridge trs[3].MultiTransactionType = transfer.MultiTransactionSwap @@ -326,33 +385,43 @@ func TestGetActivityEntriesFilterByType(t *testing.T) { // Test filtering out without address involved var filter Filter - // TODO: add more types to cover all cases + + filter.Types = allActivityTypesFilter() + entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + filter.Types = []Type{SendAT, SwapAT} - entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15) + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) require.NoError(t, err) require.Equal(t, 8, len(entries)) swapCount := 0 sendCount := 0 + receiveCount := 0 for _, entry := range entries { if entry.activityType == SendAT { sendCount++ } + if entry.activityType == ReceiveAT { + receiveCount++ + } if entry.activityType == SwapAT { swapCount++ } } - require.Equal(t, 7, sendCount) + require.Equal(t, 2, sendCount) + require.Equal(t, 5, receiveCount) require.Equal(t, 1, swapCount) // Test filtering out with address involved filter.Types = []Type{BridgeAT, ReceiveAT} // Include one "to" from transfers to be detected as receive - addresses := []common.Address{trs[0].To, trs[1].To, trs[2].From, trs[3].From, trs[5].From} - entries, err = GetActivityEntries(db, addresses, []uint64{}, filter, 0, 15) + addresses := []eth_common.Address{trs[0].To, trs[1].To, trs[2].From, trs[3].From, trs[5].From} + entries, err = GetActivityEntries(db, addresses, []common.ChainID{}, filter, 0, 15) require.NoError(t, err) require.Equal(t, 3, len(entries)) bridgeCount := 0 - receiveCount := 0 + receiveCount = 0 for _, entry := range entries { if entry.activityType == BridgeAT { bridgeCount++ @@ -365,42 +434,241 @@ func TestGetActivityEntriesFilterByType(t *testing.T) { require.Equal(t, 1, receiveCount) } -func TestGetActivityEntriesFilterByAddress(t *testing.T) { +func TestGetActivityEntriesFilterByAddresses(t *testing.T) { db, close := setupTestActivityDB(t) defer close() // Adds 4 extractable transactions td := fillTestData(t, db) - // Add 6 extractable transactions: one MultiTransactionSwap, two MultiTransactionBridge rest Send trs := transfer.GenerateTestTransactions(t, db, 7, 6) for i := range trs { transfer.InsertTestTransfer(t, db, &trs[i]) } var filter Filter - addressesFilter := []common.Address{td.mTr.To, trs[1].From, trs[4].To} - entries, err := GetActivityEntries(db, addressesFilter, []uint64{}, filter, 0, 15) + + addressesFilter := allAddressesFilter() + entries, err := GetActivityEntries(db, addressesFilter, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + + addressesFilter = []eth_common.Address{td.mTr.To, trs[1].From, trs[4].To} + entries, err = GetActivityEntries(db, addressesFilter, []common.ChainID{}, filter, 0, 15) require.NoError(t, err) require.Equal(t, 3, len(entries)) require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To}, - id: 0, - timestamp: trs[4].Timestamp, - activityType: ReceiveAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To}, + id: 0, + timestamp: trs[4].Timestamp, + activityType: ReceiveAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[0]) require.Equal(t, Entry{ - transactionType: SimpleTransactionPT, - transaction: &transfer.TransactionIdentity{ChainID: trs[1].ChainID, Hash: trs[1].Hash, Address: trs[1].To}, - id: 0, - timestamp: trs[1].Timestamp, - activityType: SendAT, + payloadType: SimpleTransactionPT, + transaction: &transfer.TransactionIdentity{ChainID: trs[1].ChainID, Hash: trs[1].Hash, Address: trs[1].From}, + id: 0, + timestamp: trs[1].Timestamp, + activityType: SendAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[1]) require.Equal(t, Entry{ - transactionType: MultiTransactionPT, - transaction: nil, - id: td.mTrID, - timestamp: td.mTr.Timestamp, - activityType: SendAT, + payloadType: MultiTransactionPT, + transaction: nil, + id: td.mTrID, + timestamp: td.mTr.Timestamp, + activityType: SendAT, + activityStatus: FinalizedAS, + tokenType: AssetTT, }, entries[2]) } + +func TestGetActivityEntriesFilterByStatus(t *testing.T) { + db, close := setupTestActivityDB(t) + defer close() + + // Adds 4 extractable transactions + fillTestData(t, db) + // Add 6 extractable transactions + trs := transfer.GenerateTestTransactions(t, db, 7, 6) + for i := range trs { + transfer.InsertTestTransfer(t, db, &trs[i]) + } + + var filter Filter + filter.Statuses = []Status{} + entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + + filter.Statuses = allActivityStatusesFilter() + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + + // TODO: enabled and finish tests after extending DB with transaction status + // + // filter.Statuses = []Status{PendingAS} + // entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + // require.NoError(t, err) + // require.Equal(t, 1, len(entries)) + + // filter.Statuses = []Status{FailedAS, CompleteAS} + // entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + // require.NoError(t, err) + // require.Equal(t, 9, len(entries)) +} + +func TestGetActivityEntriesFilterByTokenType(t *testing.T) { + db, close := setupTestActivityDB(t) + defer close() + + // Adds 4 extractable transactions 2 transactions ETH, one MT SNT to DAI and another MT ETH to SNT + fillTestData(t, db) + // Add 6 extractable transactions with USDC (only erc20 as type in DB) + trs := transfer.GenerateTestTransactions(t, db, 7, 6) + for i := range trs { + trs[i].FromToken = "USDC" + transfer.InsertTestTransfer(t, db, &trs[i]) + } + + var filter Filter + filter.Tokens = noAssetsFilter() + entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 0, len(entries)) + + filter.Tokens = allTokensFilter() + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + + // Regression when collectibles is nil + filter.Tokens = Tokens{[]TokenCode{}, nil, []TokenType{}} + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + + filter.Tokens = Tokens{Assets: []TokenCode{"ETH"}, EnabledTypes: []TokenType{AssetTT}} + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 3, len(entries)) + + // TODO: update tests after adding token type to transfers + filter.Tokens = Tokens{Assets: []TokenCode{"USDC", "DAI"}, EnabledTypes: []TokenType{AssetTT}} + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 1, len(entries)) + + // Regression when EnabledTypes ar empty + filter.Tokens = Tokens{Assets: []TokenCode{"USDC", "DAI"}, EnabledTypes: []TokenType{}} + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 1, len(entries)) +} + +func TestGetActivityEntriesFilterByToAddresses(t *testing.T) { + db, close := setupTestActivityDB(t) + defer close() + + // Adds 4 extractable transactions + td := fillTestData(t, db) + // Add 6 extractable transactions + trs := transfer.GenerateTestTransactions(t, db, 7, 6) + for i := range trs { + transfer.InsertTestTransfer(t, db, &trs[i]) + } + + var filter Filter + filter.CounterpartyAddresses = allAddressesFilter() + entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + + filter.CounterpartyAddresses = []eth_common.Address{eth_common.HexToAddress("0x567890")} + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 0, len(entries)) + + filter.CounterpartyAddresses = []eth_common.Address{td.pendingTr.To, td.mTr.To, trs[3].To} + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 3, len(entries)) + + filter.CounterpartyAddresses = []eth_common.Address{td.tr1.To, td.pendingTr.From, trs[3].From, trs[5].To} + entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 2, len(entries)) +} +func TestGetActivityEntriesFilterByNetworks(t *testing.T) { + db, close := setupTestActivityDB(t) + defer close() + + // Adds 4 extractable transactions + td := fillTestData(t, db) + // Add 6 extractable transactions + trs := transfer.GenerateTestTransactions(t, db, 7, 6) + for i := range trs { + transfer.InsertTestTransfer(t, db, &trs[i]) + } + + var filter Filter + chainIDs := allNetworksFilter() + entries, err := GetActivityEntries(db, []eth_common.Address{}, chainIDs, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 10, len(entries)) + + chainIDs = []common.ChainID{5674839210} + entries, err = GetActivityEntries(db, []eth_common.Address{}, chainIDs, filter, 0, 15) + require.NoError(t, err) + // TODO: update after multi-transactions are filterable by ChainID + require.Equal(t, 2 /*0*/, len(entries)) + + chainIDs = []common.ChainID{td.pendingTr.ChainID, td.mTr.ChainID, trs[3].ChainID} + entries, err = GetActivityEntries(db, []eth_common.Address{}, chainIDs, filter, 0, 15) + require.NoError(t, err) + // TODO: update after multi-transactions are filterable by ChainID + require.Equal(t, 4 /*3*/, len(entries)) +} + +func TestGetActivityEntriesCheckToAndFrom(t *testing.T) { + db, close := setupTestActivityDB(t) + defer close() + + // Adds 6 transactions from which 4 are filered out + td := fillTestData(t, db) + + // Add extra transactions to test To address + trs := transfer.GenerateTestTransactions(t, db, 7, 2) + transfer.InsertTestTransfer(t, db, &trs[0]) + transfer.InsertTestPendingTransaction(t, db, &trs[1]) + + addresses := []eth_common.Address{td.tr1.From, td.pendingTr.From, + td.singletonMTr.From, td.mTr.To, trs[0].To, trs[1].To} + + var filter Filter + entries, err := GetActivityEntries(db, addresses, []common.ChainID{}, filter, 0, 15) + require.NoError(t, err) + require.Equal(t, 6, len(entries)) + + require.Equal(t, SendAT, entries[5].activityType) // td.tr1 + require.NotEqual(t, eth.Address{}, entries[5].transaction.Address) // td.tr1 + require.Equal(t, td.tr1.From, entries[5].transaction.Address) // td.tr1 + + require.Equal(t, SendAT, entries[4].activityType) // td.pendingTr + + // Multi-transactions are always considered as SendAT + require.Equal(t, SendAT, entries[3].activityType) // td.singletonMTr + require.Equal(t, SendAT, entries[2].activityType) // td.mTr + + require.Equal(t, ReceiveAT, entries[1].activityType) // trs[0] + require.NotEqual(t, eth.Address{}, entries[1].transaction.Address) // trs[0] + require.Equal(t, trs[0].To, entries[1].transaction.Address) // trs[0] + + require.Equal(t, ReceiveAT, entries[0].activityType) // trs[1] (pending) + + // TODO: add accounts to DB for proper detection of sender/receiver + // TODO: Test with all addresses +} diff --git a/services/wallet/activity/filter.go b/services/wallet/activity/filter.go index 156ec00e0..1180ad4d8 100644 --- a/services/wallet/activity/filter.go +++ b/services/wallet/activity/filter.go @@ -1,9 +1,13 @@ package activity -import "github.com/ethereum/go-ethereum/common" +import ( + eth_common "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/services/wallet/common" +) + +const NoLimitTimestampForPeriod = 0 type Period struct { - // 0 means no limit StartTimestamp int64 `json:"startTimestamp"` EndTimestamp int64 `json:"endTimestamp"` } @@ -11,36 +15,69 @@ type Period struct { type Type int const ( - AllAT Type = iota - SendAT + SendAT Type = iota ReceiveAT BuyAT SwapAT BridgeAT ) +func allActivityTypesFilter() []Type { + return []Type{} +} + type Status int const ( - AllAS Status = iota - FailedAS + FailedAS Status = iota PendingAS CompleteAS FinalizedAS ) +func allActivityStatusesFilter() []Status { + return []Status{} +} + type TokenType int const ( - AllTT TokenType = iota - AssetTT + AssetTT TokenType = iota CollectiblesTT ) -type Filter struct { - Period Period `json:"period"` - Types []Type `json:"types"` - Statuses []Status `json:"statuses"` - TokenTypes []TokenType `json:"tokenTypes"` - CounterpartyAddresses []common.Address `json:"counterpartyAddresses"` +type TokenCode string + +// Tokens the following rules apply for its members: +// empty member: none is selected +// nil means all +// see allTokensFilter and noTokensFilter +type Tokens struct { + Assets []TokenCode `json:"assets"` + Collectibles []eth_common.Address `json:"collectibles"` + EnabledTypes []TokenType `json:"enabledTypes"` +} + +func noAssetsFilter() Tokens { + return Tokens{[]TokenCode{}, []eth_common.Address{}, []TokenType{CollectiblesTT}} +} + +func allTokensFilter() Tokens { + return Tokens{} +} + +func allAddressesFilter() []eth_common.Address { + return []eth_common.Address{} +} + +func allNetworksFilter() []common.ChainID { + return []common.ChainID{} +} + +type Filter struct { + Period Period `json:"period"` + Types []Type `json:"types"` + Statuses []Status `json:"statuses"` + Tokens Tokens `json:"tokens"` + CounterpartyAddresses []eth_common.Address `json:"counterpartyAddresses"` } diff --git a/services/wallet/api.go b/services/wallet/api.go index fdd36dfb2..b37169685 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -19,6 +19,8 @@ import ( "github.com/status-im/status-go/services/wallet/thirdparty/opensea" "github.com/status-im/status-go/services/wallet/token" "github.com/status-im/status-go/services/wallet/transfer" + + wallet_common "github.com/status-im/status-go/services/wallet/common" ) func NewAPI(s *Service) *API { @@ -247,7 +249,7 @@ func (api *API) GetPendingTransactionsForIdentities(ctx context.Context, identit result = make([]*transfer.PendingTransaction, 0, len(identities)) var pt *transfer.PendingTransaction for _, identity := range identities { - pt, err = api.s.transactionManager.GetPendingEntry(identity.ChainID, identity.Hash) + pt, err = api.s.transactionManager.GetPendingEntry(uint64(identity.ChainID), identity.Hash) result = append(result, pt) } @@ -525,7 +527,7 @@ func (api *API) FetchAllCurrencyFormats() (currency.FormatPerSymbol, error) { return api.s.currency.FetchAllCurrencyFormats() } -func (api *API) GetActivityEntries(addresses []common.Address, chainIDs []uint64, filter activity.Filter, offset int, limit int) ([]activity.Entry, error) { +func (api *API) GetActivityEntries(addresses []common.Address, chainIDs []wallet_common.ChainID, filter activity.Filter, offset int, limit int) ([]activity.Entry, error) { log.Debug("call to GetActivityEntries") return activity.GetActivityEntries(api.s.db, addresses, chainIDs, filter, offset, limit) } diff --git a/services/wallet/testutils/helpers.go b/services/wallet/testutils/helpers.go index 2af38c77a..e356d8fa0 100644 --- a/services/wallet/testutils/helpers.go +++ b/services/wallet/testutils/helpers.go @@ -2,6 +2,10 @@ package testutils import "reflect" +const EthSymbol = "ETH" +const SntSymbol = "SNT" +const DaiSymbol = "DAI" + func StructExistsInSlice[T any](target T, slice []T) bool { for _, item := range slice { if reflect.DeepEqual(target, item) { diff --git a/services/wallet/transfer/database.go b/services/wallet/transfer/database.go index 34870340e..d7df98fa8 100644 --- a/services/wallet/transfer/database.go +++ b/services/wallet/transfer/database.go @@ -279,7 +279,7 @@ func (db *Database) GetTransfersForIdentities(ctx context.Context, identities [] for _, identity := range identities { subQuery := newSubQuery() // TODO optimization: consider using tuples in sqlite and IN operator - subQuery = subQuery.FilterNetwork(identity.ChainID).FilterTransactionHash(identity.Hash).FilterAddress(identity.Address) + subQuery = subQuery.FilterNetwork(uint64(identity.ChainID)).FilterTransactionHash(identity.Hash).FilterAddress(identity.Address) query.addSubQuery(subQuery, OrSeparator) } rows, err := db.client.QueryContext(ctx, query.String(), query.Args()...) diff --git a/services/wallet/transfer/database_test.go b/services/wallet/transfer/database_test.go index a5ba5b89a..2b8ae0cd3 100644 --- a/services/wallet/transfer/database_test.go +++ b/services/wallet/transfer/database_test.go @@ -223,8 +223,8 @@ func TestGetTransfersForIdentities(t *testing.T) { require.Equal(t, big.NewInt(trs[3].BlkNumber), entries[1].BlockNumber) require.Equal(t, uint64(trs[1].Timestamp), entries[0].Timestamp) require.Equal(t, uint64(trs[3].Timestamp), entries[1].Timestamp) - require.Equal(t, trs[1].ChainID, entries[0].NetworkID) - require.Equal(t, trs[3].ChainID, entries[1].NetworkID) + require.Equal(t, uint64(trs[1].ChainID), entries[0].NetworkID) + require.Equal(t, uint64(trs[3].ChainID), entries[1].NetworkID) require.Equal(t, MultiTransactionIDType(0), entries[0].MultiTransactionID) require.Equal(t, MultiTransactionIDType(0), entries[1].MultiTransactionID) } diff --git a/services/wallet/transfer/query.go b/services/wallet/transfer/query.go index b6c5f6aea..ce4a07d95 100644 --- a/services/wallet/transfer/query.go +++ b/services/wallet/transfer/query.go @@ -53,6 +53,7 @@ func (q *transfersQuery) addWhereSeparator(separator SeparatorType) { type SeparatorType int +// Beware if changing this enum please update addWhereSeparator as well const ( NoSeparator SeparatorType = iota + 1 OrSeparator diff --git a/services/wallet/transfer/testutils.go b/services/wallet/transfer/testutils.go index 923be1492..0b808fdab 100644 --- a/services/wallet/transfer/testutils.go +++ b/services/wallet/transfer/testutils.go @@ -3,18 +3,23 @@ package transfer import ( "database/sql" "fmt" + "strings" "testing" - "github.com/ethereum/go-ethereum/common" + 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/stretchr/testify/require" ) type TestTransaction struct { - Hash common.Hash - ChainID uint64 - From common.Address // [sender] - To common.Address // [address] + Hash eth_common.Hash + ChainID common.ChainID + From eth_common.Address // [sender] + To eth_common.Address // [address] + FromToken string // used to detect type in transfers table + ToToken string // only used in multi_transactions table Timestamp int64 Value int64 BlkNumber int64 @@ -25,10 +30,10 @@ type TestTransaction struct { func GenerateTestTransactions(t *testing.T, db *sql.DB, firstStartIndex int, count int) (result []TestTransaction) { for i := firstStartIndex; i < (firstStartIndex + count); i++ { tr := TestTransaction{ - Hash: common.HexToHash(fmt.Sprintf("0x1%d", i)), - ChainID: uint64(i), - From: common.HexToAddress(fmt.Sprintf("0x2%d", i)), - To: common.HexToAddress(fmt.Sprintf("0x3%d", i)), + Hash: eth_common.HexToHash(fmt.Sprintf("0x1%d", i)), + ChainID: common.ChainID(i), + From: eth_common.HexToAddress(fmt.Sprintf("0x2%d", i)), + To: eth_common.HexToAddress(fmt.Sprintf("0x3%d", i)), Timestamp: int64(i), Value: int64(i), BlkNumber: int64(i), @@ -42,7 +47,11 @@ func GenerateTestTransactions(t *testing.T, db *sql.DB, firstStartIndex int, cou func InsertTestTransfer(t *testing.T, db *sql.DB, tr *TestTransaction) { // Respect `FOREIGN KEY(network_id,address,blk_hash)` of `transfers` table - blkHash := common.HexToHash("4") + tokenType := "eth" + if tr.FromToken != "" && strings.ToUpper(tr.FromToken) != testutils.EthSymbol { + tokenType = "erc20" + } + blkHash := eth_common.HexToHash("4") _, err := db.Exec(` INSERT OR IGNORE INTO blocks( network_id, address, blk_number, blk_hash @@ -50,17 +59,34 @@ func InsertTestTransfer(t *testing.T, db *sql.DB, tr *TestTransaction) { INSERT INTO transfers (network_id, hash, address, blk_hash, tx, sender, receipt, log, type, blk_number, timestamp, loaded, multi_transaction_id, base_gas_fee - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "test", ?, ?, 0, ?, 0)`, + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0)`, tr.ChainID, tr.To, tr.BlkNumber, blkHash, - tr.ChainID, tr.Hash, tr.To, blkHash, &JSONBlob{}, tr.From, &JSONBlob{}, &JSONBlob{}, tr.BlkNumber, tr.Timestamp, tr.MultiTransactionID) + tr.ChainID, tr.Hash, tr.To, blkHash, &JSONBlob{}, tr.From, &JSONBlob{}, &JSONBlob{}, tokenType, tr.BlkNumber, tr.Timestamp, tr.MultiTransactionID) + require.NoError(t, err) +} + +func InsertTestPendingTransaction(t *testing.T, db *sql.DB, tr *TestTransaction) { + _, 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, ?, '', 'test', '', ?)`, + tr.ChainID, tr.Hash, tr.Timestamp, tr.From, tr.To, tr.Value, tr.MultiTransactionID) require.NoError(t, err) } func InsertTestMultiTransaction(t *testing.T, db *sql.DB, tr *TestTransaction) MultiTransactionIDType { + fromTokenType := tr.FromToken + if tr.FromToken == "" { + fromTokenType = testutils.EthSymbol + } + toTokenType := tr.ToToken + if tr.ToToken == "" { + toTokenType = testutils.EthSymbol + } result, err := db.Exec(` INSERT INTO multi_transactions (from_address, from_asset, from_amount, to_address, to_asset, type, timestamp - ) VALUES (?, 'ETH', 0, ?, 'SNT', ?, ?)`, - tr.From, tr.To, tr.MultiTransactionType, tr.Timestamp) + ) VALUES (?, ?, 0, ?, ?, ?, ?)`, + tr.From, fromTokenType, tr.To, toTokenType, tr.MultiTransactionType, tr.Timestamp) require.NoError(t, err) rowID, err := result.LastInsertId() require.NoError(t, err) diff --git a/services/wallet/transfer/transaction.go b/services/wallet/transfer/transaction.go index 636a5e22e..dea470dca 100644 --- a/services/wallet/transfer/transaction.go +++ b/services/wallet/transfer/transaction.go @@ -20,6 +20,7 @@ import ( "github.com/status-im/status-go/services/wallet/async" "github.com/status-im/status-go/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/bridge" + wallet_common "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/transactions" ) @@ -96,9 +97,9 @@ type PendingTransaction struct { } type TransactionIdentity struct { - ChainID uint64 `json:"chainId"` - Hash common.Hash `json:"hash"` - Address common.Address `json:"address"` + ChainID wallet_common.ChainID `json:"chainId"` + Hash common.Hash `json:"hash"` + Address common.Address `json:"address"` } const selectFromPending = `SELECT hash, timestamp, value, from_address, to_address, data,