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
|
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, ×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)
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue