feat(wallet) add filter api to retrieve recipients of a wallet

The new API returns all known recipients of a wallet, by
sourcing transfers, pending_transactions and multi_transactions tables
The API is synchronous. Future work will be to make it async.
In some corner cases, when watching a famous wallet, it can
be that there are too many recipients to be returned in one go. Offset
and limit can be used to paginate through the results.

Updates status-desktop #10025
This commit is contained in:
Stefan 2023-06-15 19:00:40 +02:00 committed by dlipicar
parent a2b1640ad7
commit 6f2c338f72
3 changed files with 139 additions and 12 deletions

View File

@ -1,7 +1,10 @@
package activity package activity
import ( import (
eth_common "github.com/ethereum/go-ethereum/common" "context"
"database/sql"
eth "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/common"
) )
@ -54,20 +57,20 @@ type TokenCode string
// see allTokensFilter and noTokensFilter // see allTokensFilter and noTokensFilter
type Tokens struct { type Tokens struct {
Assets []TokenCode `json:"assets"` Assets []TokenCode `json:"assets"`
Collectibles []eth_common.Address `json:"collectibles"` Collectibles []eth.Address `json:"collectibles"`
EnabledTypes []TokenType `json:"enabledTypes"` EnabledTypes []TokenType `json:"enabledTypes"`
} }
func noAssetsFilter() Tokens { func noAssetsFilter() Tokens {
return Tokens{[]TokenCode{}, []eth_common.Address{}, []TokenType{CollectiblesTT}} return Tokens{[]TokenCode{}, []eth.Address{}, []TokenType{CollectiblesTT}}
} }
func allTokensFilter() Tokens { func allTokensFilter() Tokens {
return Tokens{} return Tokens{}
} }
func allAddressesFilter() []eth_common.Address { func allAddressesFilter() []eth.Address {
return []eth_common.Address{} return []eth.Address{}
} }
func allNetworksFilter() []common.ChainID { func allNetworksFilter() []common.ChainID {
@ -79,5 +82,55 @@ type Filter struct {
Types []Type `json:"types"` Types []Type `json:"types"`
Statuses []Status `json:"statuses"` Statuses []Status `json:"statuses"`
Tokens Tokens `json:"tokens"` Tokens Tokens `json:"tokens"`
CounterpartyAddresses []eth_common.Address `json:"counterpartyAddresses"` CounterpartyAddresses []eth.Address `json:"counterpartyAddresses"`
}
// TODO: consider sorting by saved address and contacts to offload the client from doing it at runtime
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
transfers.address as to_address,
transfers.timestamp AS timestamp
FROM transfers
WHERE transfers.multi_transaction_id = 0
UNION ALL
SELECT
pending_transactions.to_address AS to_address,
pending_transactions.timestamp AS timestamp
FROM pending_transactions
WHERE pending_transactions.multi_transaction_id = 0
UNION ALL
SELECT
multi_transactions.to_address AS to_address,
multi_transactions.timestamp AS timestamp
FROM multi_transactions
ORDER BY timestamp DESC
LIMIT ? OFFSET ?`, limit, offset)
if err != nil {
return nil, false, err
}
defer rows.Close()
var entries []eth.Address
for rows.Next() {
var toAddress eth.Address
var timestamp int64
err := rows.Scan(&toAddress, &timestamp)
if err != nil {
return nil, false, err
}
entries = append(entries, toAddress)
}
if err = rows.Err(); err != nil {
return nil, false, err
}
hasMore = len(entries) == limit
return entries, hasMore, nil
} }

View File

@ -0,0 +1,63 @@
package activity
import (
"context"
"database/sql"
"fmt"
"testing"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/stretchr/testify/require"
)
func setupTestFilterDB(t *testing.T) (db *sql.DB, close func()) {
db, err := appdatabase.SetupTestMemorySQLDB("wallet-activity-tests-filter")
require.NoError(t, err)
return db, func() {
require.NoError(t, db.Close())
}
}
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()
// Add 6 extractable transactions
trs, _, toTrs := transfer.GenerateTestTransactions(t, db, 0, 6)
for i := range trs {
transfer.InsertTestTransfer(t, db, &trs[i])
}
entries, hasMore, err := GetRecipients(context.Background(), db, 0, 15)
require.NoError(t, err)
require.False(t, hasMore)
require.Equal(t, 6, len(entries))
for i := range entries {
found := false
for j := range toTrs {
if entries[i] == toTrs[j] {
found = true
break
}
}
require.True(t, found, fmt.Sprintf("recipient %s not found in toTrs", entries[i].Hex()))
}
entries, hasMore, err = GetRecipients(context.Background(), db, 0, 4)
require.NoError(t, err)
require.Equal(t, 4, len(entries))
require.True(t, hasMore)
}

View File

@ -532,3 +532,14 @@ func (api *API) FilterActivityAsync(ctx context.Context, addresses []common.Addr
log.Debug("[WalletAPI:: FilterActivityAsync] addr.count", len(addresses), "chainIDs.count", len(chainIDs), "filter", filter, "offset", offset, "limit", limit) log.Debug("[WalletAPI:: FilterActivityAsync] addr.count", len(addresses), "chainIDs.count", len(chainIDs), "filter", filter, "offset", offset, "limit", limit)
return api.s.activity.FilterActivityAsync(ctx, addresses, chainIDs, filter, offset, limit) return api.s.activity.FilterActivityAsync(ctx, addresses, chainIDs, filter, offset, limit)
} }
type GetAllRecipientsResponse struct {
Addresses []common.Address `json:"addresses"`
HasMore bool `json:"hasMore"`
}
func (api *API) GetAllRecipients(ctx context.Context, offset int, limit int) (result *GetAllRecipientsResponse, err error) {
result = &GetAllRecipientsResponse{}
result.Addresses, result.HasMore, err = activity.GetRecipients(ctx, api.s.db, offset, limit)
return result, err
}