diff --git a/services/wallet/activity/filter.go b/services/wallet/activity/filter.go index 77f2c3d53..400bdf701 100644 --- a/services/wallet/activity/filter.go +++ b/services/wallet/activity/filter.go @@ -5,13 +5,23 @@ import ( "database/sql" "fmt" + // used for embedding the sql query in the binary + _ "embed" + eth "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/transactions" ) const NoLimitTimestampForPeriod = 0 +//go:embed oldest_timestamp.sql +var oldestTimestampQueryFormatString string + +//go:embed recipients.sql +var recipientsQueryFormatString string + type Period struct { StartTimestamp int64 `json:"startTimestamp"` EndTimestamp int64 `json:"endTimestamp"` @@ -85,49 +95,22 @@ type Filter struct { FilterOutCollectibles bool `json:"filterOutCollectibles"` } -func GetRecipients(ctx context.Context, db *sql.DB, offset int, limit int) (addresses []eth.Address, hasMore bool, err error) { - rows, err := db.QueryContext(ctx, ` - SELECT - to_address, - MIN(timestamp) AS min_timestamp - FROM ( - SELECT - transfers.tx_to_address as to_address, - MIN(transfers.timestamp) AS timestamp - FROM - transfers - WHERE - transfers.multi_transaction_id = 0 AND transfers.tx_to_address NOT NULL - GROUP BY - transfers.tx_to_address +func GetRecipients(ctx context.Context, db *sql.DB, chainIDs []common.ChainID, addresses []eth.Address, offset int, limit int) (recipients []eth.Address, hasMore bool, err error) { + filterAllAddresses := len(addresses) == 0 + involvedAddresses := noEntriesInTmpTableSQLValues + if !filterAllAddresses { + involvedAddresses = joinAddresses(addresses) + } - UNION + includeAllNetworks := len(chainIDs) == 0 + networks := noEntriesInTmpTableSQLValues + if !includeAllNetworks { + networks = joinItems(chainIDs, nil) + } - SELECT - pending_transactions.to_address AS to_address, - MIN(pending_transactions.timestamp) AS timestamp - FROM - pending_transactions - WHERE - pending_transactions.multi_transaction_id = 0 AND pending_transactions.to_address NOT NULL - GROUP BY - pending_transactions.to_address + queryString := fmt.Sprintf(recipientsQueryFormatString, involvedAddresses, networks) - UNION - - SELECT - multi_transactions.to_address AS to_address, - MIN(multi_transactions.timestamp) AS timestamp - FROM - multi_transactions - GROUP BY - multi_transactions.to_address - ) AS combined_result - GROUP BY - to_address - ORDER BY - min_timestamp DESC - LIMIT ? OFFSET ?;`, limit, offset) + rows, err := db.QueryContext(ctx, queryString, filterAllAddresses, includeAllNetworks, transactions.Pending, limit, offset) if err != nil { return nil, false, err } @@ -154,47 +137,13 @@ func GetRecipients(ctx context.Context, db *sql.DB, offset int, limit int) (addr } func GetOldestTimestamp(ctx context.Context, db *sql.DB, addresses []eth.Address) (timestamp int64, err error) { - queryFormatString := ` - WITH filter_conditions AS (SELECT ? AS filterAllAddresses), - filter_addresses(address) AS ( - SELECT * FROM (VALUES %s) WHERE (SELECT filterAllAddresses FROM filter_conditions) = 0 - ) - - SELECT - transfers.tx_from_address AS from_address, - transfers.tx_to_address AS to_address, - transfers.timestamp AS timestamp - FROM transfers, filter_conditions - WHERE transfers.multi_transaction_id = 0 - AND (filterAllAddresses OR from_address IN filter_addresses OR to_address IN filter_addresses) - - UNION ALL - - SELECT - pending_transactions.from_address AS from_address, - pending_transactions.to_address AS to_address, - pending_transactions.timestamp AS timestamp - FROM pending_transactions, filter_conditions - WHERE pending_transactions.multi_transaction_id = 0 - AND (filterAllAddresses OR from_address IN filter_addresses OR to_address IN filter_addresses) - - UNION ALL - - SELECT - multi_transactions.from_address AS from_address, - multi_transactions.to_address AS to_address, - multi_transactions.timestamp AS timestamp - FROM multi_transactions, filter_conditions - WHERE filterAllAddresses OR from_address IN filter_addresses OR to_address IN filter_addresses - ORDER BY timestamp ASC - LIMIT 1` - filterAllAddresses := len(addresses) == 0 involvedAddresses := noEntriesInTmpTableSQLValues if !filterAllAddresses { involvedAddresses = joinAddresses(addresses) } - queryString := fmt.Sprintf(queryFormatString, involvedAddresses) + + queryString := fmt.Sprintf(oldestTimestampQueryFormatString, involvedAddresses) row := db.QueryRowContext(ctx, queryString, filterAllAddresses) var fromAddress, toAddress sql.NullString diff --git a/services/wallet/activity/filter_test.go b/services/wallet/activity/filter_test.go index 2b1311d39..e9f765b89 100644 --- a/services/wallet/activity/filter_test.go +++ b/services/wallet/activity/filter_test.go @@ -8,6 +8,7 @@ import ( eth "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/services/wallet/transfer" "github.com/status-im/status-go/t/helpers" @@ -69,16 +70,6 @@ func insertTestData(t *testing.T, db *sql.DB, nullifyToForIndexes []int) (trs [] return } -func TestGetRecipientsEmptyDB(t *testing.T) { - db, close := setupTestFilterDB(t) - defer close() - - entries, hasMore, err := GetRecipients(context.Background(), db, 0, 15) - require.NoError(t, err) - require.Equal(t, 0, len(entries)) - require.False(t, hasMore) -} - func TestGetRecipients(t *testing.T) { db, close := setupTestFilterDB(t) defer close() @@ -103,7 +94,7 @@ func TestGetRecipients(t *testing.T) { dupTrs[3].To = trs[5].To transfer.InsertTestPendingTransaction(t, db, &dupTrs[3]) - entries, hasMore, err := GetRecipients(context.Background(), db, 0, 15) + entries, hasMore, err := GetRecipients(context.Background(), db, []common.ChainID{}, []eth.Address{}, 0, 15) require.NoError(t, err) require.False(t, hasMore) require.Equal(t, 6, len(entries)) @@ -118,10 +109,27 @@ func TestGetRecipients(t *testing.T) { require.True(t, found, fmt.Sprintf("recipient %s not found in toTrs", entries[i].Hex())) } - entries, hasMore, err = GetRecipients(context.Background(), db, 0, 2) + entries, hasMore, err = GetRecipients(context.Background(), db, []common.ChainID{}, []eth.Address{}, 0, 2) require.NoError(t, err) require.Equal(t, 2, len(entries)) require.True(t, hasMore) + + // Get Recipients from specific chains + entries, hasMore, err = GetRecipients(context.Background(), db, []common.ChainID{10}, []eth.Address{}, 0, 15) + + require.NoError(t, err) + require.Equal(t, 2, len(entries)) + require.False(t, hasMore) + require.Equal(t, trs[5].To, entries[0]) + require.Equal(t, trs[2].To, entries[1]) + + // Get Recipients from specific addresses + entries, hasMore, err = GetRecipients(context.Background(), db, []common.ChainID{}, []eth.Address{trs[0].From}, 0, 15) + + require.NoError(t, err) + require.Equal(t, 1, len(entries)) + require.False(t, hasMore) + require.Equal(t, trs[1].To, entries[0]) } func TestGetRecipients_NullAddresses(t *testing.T) { @@ -130,7 +138,7 @@ func TestGetRecipients_NullAddresses(t *testing.T) { insertTestData(t, db, []int{1, 2, 3, 5}) - entries, hasMore, err := GetRecipients(context.Background(), db, 0, 15) + entries, hasMore, err := GetRecipients(context.Background(), db, []common.ChainID{}, []eth.Address{}, 0, 15) require.NoError(t, err) require.False(t, hasMore) require.Equal(t, 3, len(entries)) diff --git a/services/wallet/activity/oldest_timestamp.sql b/services/wallet/activity/oldest_timestamp.sql new file mode 100644 index 000000000..8605b2065 --- /dev/null +++ b/services/wallet/activity/oldest_timestamp.sql @@ -0,0 +1,35 @@ +-- Get oldest timestamp query + +WITH filter_conditions AS + (SELECT ? AS filterAllAddresses), + filter_addresses(address) AS ( + SELECT * FROM (VALUES %s) WHERE (SELECT filterAllAddresses FROM filter_conditions) = 0 + ) +SELECT + transfers.tx_from_address AS from_address, + transfers.tx_to_address AS to_address, + transfers.timestamp AS timestamp +FROM transfers, filter_conditions +WHERE transfers.multi_transaction_id = 0 + AND (filterAllAddresses OR from_address IN filter_addresses OR to_address IN filter_addresses) + +UNION ALL + +SELECT + pending_transactions.from_address AS from_address, + pending_transactions.to_address AS to_address, + pending_transactions.timestamp AS timestamp +FROM pending_transactions, filter_conditions +WHERE pending_transactions.multi_transaction_id = 0 + AND (filterAllAddresses OR from_address IN filter_addresses OR to_address IN filter_addresses) + +UNION ALL + +SELECT + multi_transactions.from_address AS from_address, + multi_transactions.to_address AS to_address, + multi_transactions.timestamp AS timestamp +FROM multi_transactions, filter_conditions +WHERE filterAllAddresses OR from_address IN filter_addresses OR to_address IN filter_addresses +ORDER BY timestamp ASC +LIMIT 1 \ No newline at end of file diff --git a/services/wallet/activity/recipients.sql b/services/wallet/activity/recipients.sql new file mode 100644 index 000000000..6c35924f5 --- /dev/null +++ b/services/wallet/activity/recipients.sql @@ -0,0 +1,114 @@ +-- Query to retrive all recipients for selected addresses and networks +WITH filter_conditions AS ( + SELECT + ? AS filterAllAddresses, + ? AS includeAllNetworks, + ? AS pendingStatus +), +filter_addresses(address) AS ( + VALUES + %s +), +filter_networks(network_id) AS ( + VALUES + %s +), +pending_network_ids AS ( + SELECT + multi_transaction_id + FROM + pending_transactions, + filter_conditions + WHERE + pending_transactions.multi_transaction_id != 0 + AND pending_transactions.status = pendingStatus + AND pending_transactions.network_id IN filter_networks + GROUP BY + pending_transactions.multi_transaction_id +), +tr_network_ids AS ( + SELECT + multi_transaction_id + FROM + transfers + WHERE + transfers.loaded == 1 + AND transfers.multi_transaction_id != 0 + AND network_id IN filter_networks + GROUP BY + transfers.multi_transaction_id +) +SELECT + to_address, + MIN(timestamp) AS min_timestamp +FROM ( + SELECT + transfers.tx_to_address as to_address, + MIN(transfers.timestamp) AS timestamp + FROM + transfers, filter_conditions + WHERE + transfers.multi_transaction_id = 0 AND transfers.tx_to_address NOT NULL + AND (filterAllAddresses OR transfers.address IN filter_addresses) + AND (includeAllNetworks OR transfers.network_id IN filter_networks) + GROUP BY + transfers.tx_to_address + + UNION + + SELECT + pending_transactions.to_address AS to_address, + MIN(pending_transactions.timestamp) AS timestamp + FROM + pending_transactions, filter_conditions + WHERE + pending_transactions.multi_transaction_id = 0 AND pending_transactions.to_address NOT NULL + AND (filterAllAddresses OR pending_transactions.from_address IN filter_addresses) + AND (includeAllNetworks OR pending_transactions.network_id IN filter_networks) + GROUP BY + pending_transactions.to_address + + UNION + + SELECT + multi_transactions.to_address AS to_address, + MIN(multi_transactions.timestamp) AS timestamp + FROM + multi_transactions, filter_conditions + WHERE + (filterAllAddresses OR multi_transactions.from_address IN filter_addresses) + AND ( + includeAllNetworks + OR (multi_transactions.from_network_id IN filter_networks) + OR (multi_transactions.to_network_id IN filter_networks) + OR ( + COALESCE(multi_transactions.from_network_id, 0) = 0 + AND COALESCE(multi_transactions.to_network_id, 0) = 0 + AND ( + EXISTS ( + SELECT + 1 + FROM + tr_network_ids + WHERE + multi_transactions.ROWID = tr_network_ids.multi_transaction_id + ) + OR EXISTS ( + SELECT + 1 + FROM + pending_network_ids + WHERE + multi_transactions.ROWID = pending_network_ids.multi_transaction_id + ) + ) + ) + ) + GROUP BY + multi_transactions.to_address +) AS combined_result +GROUP BY + to_address +ORDER BY + min_timestamp DESC +LIMIT ? OFFSET ?; \ No newline at end of file diff --git a/services/wallet/activity/service.go b/services/wallet/activity/service.go index 53749997b..df8db05eb 100644 --- a/services/wallet/activity/service.go +++ b/services/wallet/activity/service.go @@ -190,14 +190,14 @@ type GetRecipientsResponse struct { // GetRecipientsAsync returns true if a task is already running or scheduled due to a previous call; meaning that // this call won't receive an answer but client should rely on the answer from the previous call. // If no task is already scheduled false will be returned -func (s *Service) GetRecipientsAsync(requestID int32, offset int, limit int) bool { +func (s *Service) GetRecipientsAsync(requestID int32, chainIDs []w_common.ChainID, addresses []common.Address, offset int, limit int) bool { return s.scheduler.Enqueue(requestID, getRecipientsTask, func(ctx context.Context) (interface{}, error) { var err error result := &GetRecipientsResponse{ Offset: offset, ErrorCode: ErrorCodeSuccess, } - result.Addresses, result.HasMore, err = GetRecipients(ctx, s.db, offset, limit) + result.Addresses, result.HasMore, err = GetRecipients(ctx, s.db, chainIDs, addresses, offset, limit) return result, err }, func(result interface{}, taskType async.TaskType, err error) { res := result.(*GetRecipientsResponse) diff --git a/services/wallet/api.go b/services/wallet/api.go index cc2332f9d..2a8e9a35a 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -603,10 +603,10 @@ func (api *API) GetTxDetails(ctx context.Context, id string) (*activity.EntryDet return api.s.activity.GetTxDetails(ctx, id) } -func (api *API) GetRecipientsAsync(requestID int32, offset int, limit int) (ignored bool, err error) { - log.Debug("wallet.api.GetRecipientsAsync", "offset", offset, "limit", limit) +func (api *API) GetRecipientsAsync(requestID int32, chainIDs []wcommon.ChainID, addresses []common.Address, offset int, limit int) (ignored bool, err error) { + log.Debug("wallet.api.GetRecipientsAsync", "addresses.len", len(addresses), "chainIDs.len", len(chainIDs), "offset", offset, "limit", limit) - ignored = api.s.activity.GetRecipientsAsync(requestID, offset, limit) + ignored = api.s.activity.GetRecipientsAsync(requestID, chainIDs, addresses, offset, limit) return ignored, err }