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:
parent
a2b1640ad7
commit
6f2c338f72
|
@ -1,7 +1,10 @@
|
|||
package activity
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
@ -53,21 +56,21 @@ type TokenCode string
|
|||
// nil means all
|
||||
// see allTokensFilter and noTokensFilter
|
||||
type Tokens struct {
|
||||
Assets []TokenCode `json:"assets"`
|
||||
Collectibles []eth_common.Address `json:"collectibles"`
|
||||
EnabledTypes []TokenType `json:"enabledTypes"`
|
||||
Assets []TokenCode `json:"assets"`
|
||||
Collectibles []eth.Address `json:"collectibles"`
|
||||
EnabledTypes []TokenType `json:"enabledTypes"`
|
||||
}
|
||||
|
||||
func noAssetsFilter() Tokens {
|
||||
return Tokens{[]TokenCode{}, []eth_common.Address{}, []TokenType{CollectiblesTT}}
|
||||
return Tokens{[]TokenCode{}, []eth.Address{}, []TokenType{CollectiblesTT}}
|
||||
}
|
||||
|
||||
func allTokensFilter() Tokens {
|
||||
return Tokens{}
|
||||
}
|
||||
|
||||
func allAddressesFilter() []eth_common.Address {
|
||||
return []eth_common.Address{}
|
||||
func allAddressesFilter() []eth.Address {
|
||||
return []eth.Address{}
|
||||
}
|
||||
|
||||
func allNetworksFilter() []common.ChainID {
|
||||
|
@ -75,9 +78,59 @@ func allNetworksFilter() []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"`
|
||||
Period Period `json:"period"`
|
||||
Types []Type `json:"types"`
|
||||
Statuses []Status `json:"statuses"`
|
||||
Tokens Tokens `json:"tokens"`
|
||||
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, ×tamp)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue