feat: Wallet activity collectibles model (#4074)
This commit is contained in:
parent
cff96f99e0
commit
ecc8b4cb55
|
@ -11,11 +11,15 @@ import (
|
|||
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/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/transactions"
|
||||
)
|
||||
|
||||
const NoLimitTimestampForPeriod = 0
|
||||
|
||||
//go:embed get_collectibles.sql
|
||||
var getCollectiblesQueryFormatString string
|
||||
|
||||
//go:embed oldest_timestamp.sql
|
||||
var oldestTimestampQueryFormatString string
|
||||
|
||||
|
@ -158,3 +162,27 @@ func GetOldestTimestamp(ctx context.Context, db *sql.DB, addresses []eth.Address
|
|||
|
||||
return timestamp, nil
|
||||
}
|
||||
|
||||
func GetActivityCollectibles(ctx context.Context, db *sql.DB, chainIDs []common.ChainID, owners []eth.Address, offset int, limit int) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
filterAllAddresses := len(owners) == 0
|
||||
involvedAddresses := noEntriesInTmpTableSQLValues
|
||||
if !filterAllAddresses {
|
||||
involvedAddresses = joinAddresses(owners)
|
||||
}
|
||||
|
||||
includeAllNetworks := len(chainIDs) == 0
|
||||
networks := noEntriesInTmpTableSQLValues
|
||||
if !includeAllNetworks {
|
||||
networks = joinItems(chainIDs, nil)
|
||||
}
|
||||
|
||||
queryString := fmt.Sprintf(getCollectiblesQueryFormatString, involvedAddresses, networks)
|
||||
|
||||
rows, err := db.QueryContext(ctx, queryString, filterAllAddresses, includeAllNetworks, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return thirdparty.RowsToCollectibles(rows)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@ import (
|
|||
|
||||
eth "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
"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/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
"github.com/status-im/status-go/t/helpers"
|
||||
"github.com/status-im/status-go/walletdatabase"
|
||||
|
@ -29,7 +31,7 @@ func setupTestFilterDB(t *testing.T) (db *sql.DB, close func()) {
|
|||
// insertTestData inserts 6 extractable activity entries: 2 transfers, 2 pending transactions and 2 multi transactions
|
||||
func insertTestData(t *testing.T, db *sql.DB, nullifyToForIndexes []int) (trs []transfer.TestTransfer, toTrs []eth.Address, multiTxs []transfer.TestMultiTransaction) {
|
||||
// Add 6 extractable transactions
|
||||
trs, _, toTrs = transfer.GenerateTestTransfers(t, db, 0, 7)
|
||||
trs, _, toTrs = transfer.GenerateTestTransfers(t, db, 0, 10)
|
||||
multiTxs = []transfer.TestMultiTransaction{
|
||||
transfer.GenerateTestBridgeMultiTransaction(trs[0], trs[1]),
|
||||
transfer.GenerateTestSwapMultiTransaction(trs[2], testutils.SntSymbol, 100),
|
||||
|
@ -58,6 +60,13 @@ func insertTestData(t *testing.T, db *sql.DB, nullifyToForIndexes []int) (trs []
|
|||
transfer.InsertTestTransferWithOptions(t, db, trs[i].To, &trs[i], &transfer.TestTransferOptions{
|
||||
NullifyAddresses: nullifyAddresses,
|
||||
})
|
||||
} else if i >= 7 && i < 10 {
|
||||
ci := i - 7
|
||||
trs[i].ChainID = transfer.TestCollectibles[ci].ChainID
|
||||
transfer.InsertTestTransferWithOptions(t, db, trs[i].To, &trs[i], &transfer.TestTransferOptions{
|
||||
TokenID: transfer.TestCollectibles[ci].TokenID,
|
||||
TokenAddress: transfer.TestCollectibles[ci].TokenAddress,
|
||||
})
|
||||
} else {
|
||||
for j := range nullifyToForIndexes {
|
||||
if i == nullifyToForIndexes[j] {
|
||||
|
@ -97,7 +106,7 @@ func TestGetRecipients(t *testing.T) {
|
|||
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))
|
||||
require.Equal(t, 9, len(entries))
|
||||
for i := range entries {
|
||||
found := false
|
||||
for j := range toTrs {
|
||||
|
@ -141,7 +150,7 @@ func TestGetRecipients_NullAddresses(t *testing.T) {
|
|||
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))
|
||||
require.Equal(t, 6, len(entries))
|
||||
}
|
||||
|
||||
func TestGetOldestTimestampEmptyDB(t *testing.T) {
|
||||
|
@ -226,3 +235,67 @@ func TestGetOldestTimestamp_NullAddresses(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), timestamp)
|
||||
}
|
||||
|
||||
func TestGetActivityCollectiblesEmptyDB(t *testing.T) {
|
||||
db, close := setupTestFilterDB(t)
|
||||
defer close()
|
||||
|
||||
collectibles, err := GetActivityCollectibles(context.Background(), db, []common.ChainID{}, []eth.Address{}, 0, 10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(collectibles))
|
||||
}
|
||||
|
||||
func TestGetActivityCollectibles(t *testing.T) {
|
||||
db, close := setupTestFilterDB(t)
|
||||
defer close()
|
||||
|
||||
trs, _, _ := insertTestData(t, db, nil)
|
||||
|
||||
// Extract all collectibles
|
||||
collectibles, err := GetActivityCollectibles(context.Background(), db, []common.ChainID{}, []eth.Address{}, 0, 10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(collectibles))
|
||||
|
||||
// Extract collectibles for a specific chain
|
||||
collectibles, err = GetActivityCollectibles(context.Background(), db, []common.ChainID{1}, []eth.Address{}, 0, 10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(collectibles))
|
||||
require.Equal(t, thirdparty.CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: transfer.TestCollectibles[0].TokenID},
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: transfer.TestCollectibles[1].ChainID,
|
||||
Address: transfer.TestCollectibles[1].TokenAddress,
|
||||
},
|
||||
}, collectibles[0])
|
||||
require.Equal(t, thirdparty.CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: transfer.TestCollectibles[1].TokenID},
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: transfer.TestCollectibles[0].ChainID,
|
||||
Address: transfer.TestCollectibles[0].TokenAddress,
|
||||
},
|
||||
}, collectibles[1])
|
||||
|
||||
// Extract collectibles for a specific sender addresses
|
||||
collectibles, err = GetActivityCollectibles(context.Background(), db, []common.ChainID{}, []eth.Address{trs[8].From}, 0, 10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(collectibles))
|
||||
require.Equal(t, thirdparty.CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: transfer.TestCollectibles[1].TokenID},
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: transfer.TestCollectibles[1].ChainID,
|
||||
Address: transfer.TestCollectibles[1].TokenAddress,
|
||||
},
|
||||
}, collectibles[0])
|
||||
|
||||
// Extract collectibles for a specific recipient addresses
|
||||
collectibles, err = GetActivityCollectibles(context.Background(), db, []common.ChainID{}, []eth.Address{trs[7].To}, 0, 10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(collectibles))
|
||||
require.Equal(t, thirdparty.CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: transfer.TestCollectibles[0].TokenID},
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: transfer.TestCollectibles[0].ChainID,
|
||||
Address: transfer.TestCollectibles[0].TokenAddress,
|
||||
},
|
||||
}, collectibles[0])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
-- Query for getting collectibles from transfers
|
||||
-- It can be filtered by owner addresses and networks
|
||||
WITH filter_conditions AS (
|
||||
SELECT
|
||||
? AS filterAllAddresses,
|
||||
? AS includeAllNetworks
|
||||
),
|
||||
owner_addresses(address) AS (
|
||||
VALUES
|
||||
%s
|
||||
),
|
||||
filter_networks(network_id) AS (
|
||||
VALUES
|
||||
%s
|
||||
)
|
||||
SELECT network_id, token_address, token_id
|
||||
FROM
|
||||
transfers, filter_conditions
|
||||
WHERE
|
||||
token_id IS NOT NULL
|
||||
AND token_address IS NOT NULL
|
||||
AND (
|
||||
filterAllAddresses
|
||||
OR tx_from_address IN owner_addresses
|
||||
OR tx_to_address IN owner_addresses
|
||||
)
|
||||
AND (
|
||||
includeAllNetworks
|
||||
OR network_id IN filter_networks
|
||||
)
|
||||
GROUP BY
|
||||
token_id, token_address
|
||||
LIMIT ? OFFSET ?
|
|
@ -26,6 +26,7 @@ const (
|
|||
EventActivityFilteringUpdate walletevent.EventType = "wallet-activity-filtering-entries-updated"
|
||||
EventActivityGetRecipientsDone walletevent.EventType = "wallet-activity-get-recipients-result"
|
||||
EventActivityGetOldestTimestampDone walletevent.EventType = "wallet-activity-get-oldest-timestamp-result"
|
||||
EventActivityGetCollectibles walletevent.EventType = "wallet-activity-get-collectibles"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -41,6 +42,10 @@ var (
|
|||
ID: 3,
|
||||
Policy: async.ReplacementPolicyCancelOld,
|
||||
}
|
||||
getCollectiblesTask = async.TaskType{
|
||||
ID: 4,
|
||||
Policy: async.ReplacementPolicyCancelOld,
|
||||
}
|
||||
)
|
||||
|
||||
// Service provides an async interface, ensuring only one filter request, of each type, is running at a time. It also provides lazy load of NFT info and token mapping
|
||||
|
@ -113,6 +118,64 @@ func (s *Service) FilterActivityAsync(requestID int32, addresses []common.Addres
|
|||
})
|
||||
}
|
||||
|
||||
type CollectibleHeader struct {
|
||||
ID thirdparty.CollectibleUniqueID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ImageURL string `json:"image_url"`
|
||||
}
|
||||
|
||||
type GetollectiblesResponse struct {
|
||||
Collectibles []CollectibleHeader `json:"collectibles"`
|
||||
Offset int `json:"offset"`
|
||||
// Used to indicate that there might be more collectibles that were not returned
|
||||
// based on a simple heuristic
|
||||
HasMore bool `json:"hasMore"`
|
||||
ErrorCode ErrorCode `json:"errorCode"`
|
||||
}
|
||||
|
||||
func (s *Service) GetActivityCollectiblesAsync(requestID int32, chainIDs []w_common.ChainID, addresses []common.Address, offset int, limit int) {
|
||||
s.scheduler.Enqueue(requestID, getCollectiblesTask, func(ctx context.Context) (interface{}, error) {
|
||||
collectibles, err := GetActivityCollectibles(ctx, s.db, chainIDs, addresses, offset, limit)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := s.collectibles.FetchAssetsByCollectibleUniqueID(collectibles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]CollectibleHeader, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
res = append(res, CollectibleHeader{
|
||||
ID: c.CollectibleData.ID,
|
||||
Name: c.CollectibleData.Name,
|
||||
ImageURL: c.CollectibleData.ImageURL,
|
||||
})
|
||||
}
|
||||
|
||||
return res, err
|
||||
}, func(result interface{}, taskType async.TaskType, err error) {
|
||||
res := GetollectiblesResponse{
|
||||
ErrorCode: ErrorCodeFailed,
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, async.ErrTaskOverwritten) {
|
||||
res.ErrorCode = ErrorCodeTaskCanceled
|
||||
} else if err == nil {
|
||||
collectibles := result.([]CollectibleHeader)
|
||||
res.Collectibles = collectibles
|
||||
res.Offset = offset
|
||||
res.HasMore = len(collectibles) == limit
|
||||
res.ErrorCode = ErrorCodeSuccess
|
||||
}
|
||||
|
||||
s.sendResponseEvent(&requestID, EventActivityGetCollectibles, res, err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) GetMultiTxDetails(ctx context.Context, multiTxID int) (*EntryDetails, error) {
|
||||
return getMultiTxDetails(ctx, s.db, multiTxID)
|
||||
}
|
||||
|
|
|
@ -617,6 +617,14 @@ func (api *API) GetOldestActivityTimestampAsync(requestID int32, addresses []com
|
|||
return nil
|
||||
}
|
||||
|
||||
func (api *API) GetActivityCollectiblesAsync(requestID int32, chainIDs []wcommon.ChainID, addresses []common.Address, offset int, limit int) error {
|
||||
log.Debug("wallet.api.GetActivityCollectiblesAsync", "addresses.len", len(addresses), "chainIDs.len", len(chainIDs), "offset", offset, "limit", limit)
|
||||
|
||||
api.s.activity.GetActivityCollectiblesAsync(requestID, chainIDs, addresses, offset, limit)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) FetchChainIDForURL(ctx context.Context, rpcURL string) (*big.Int, error) {
|
||||
log.Debug("wallet.api.VerifyURL", rpcURL)
|
||||
|
||||
|
|
|
@ -111,27 +111,6 @@ func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Addre
|
|||
return
|
||||
}
|
||||
|
||||
func rowsToCollectibles(rows *sql.Rows) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
var ids []thirdparty.CollectibleUniqueID
|
||||
for rows.Next() {
|
||||
id := thirdparty.CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(0)},
|
||||
}
|
||||
err := rows.Scan(
|
||||
&id.ContractID.ChainID,
|
||||
&id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetOwnedCollectibles(chainIDs []w_common.ChainID, ownerAddresses []common.Address, offset int, limit int) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
query, args, err := sqlx.In(fmt.Sprintf(`SELECT %s
|
||||
FROM collectibles_ownership_cache
|
||||
|
@ -153,7 +132,7 @@ func (o *OwnershipDB) GetOwnedCollectibles(chainIDs []w_common.ChainID, ownerAdd
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
return rowsToCollectibles(rows)
|
||||
return thirdparty.RowsToCollectibles(rows)
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetOwnedCollectible(chainID w_common.ChainID, ownerAddresses common.Address, contractAddress common.Address, tokenID *big.Int) (*thirdparty.CollectibleUniqueID, error) {
|
||||
|
@ -173,7 +152,7 @@ func (o *OwnershipDB) GetOwnedCollectible(chainID w_common.ChainID, ownerAddress
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
ids, err := rowsToCollectibles(rows)
|
||||
ids, err := thirdparty.RowsToCollectibles(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package thirdparty
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
|
@ -45,6 +47,27 @@ func (k *CollectibleUniqueID) Same(other *CollectibleUniqueID) bool {
|
|||
return k.ContractID.ChainID == other.ContractID.ChainID && k.ContractID.Address == other.ContractID.Address && k.TokenID.Cmp(other.TokenID.Int) == 0
|
||||
}
|
||||
|
||||
func RowsToCollectibles(rows *sql.Rows) ([]CollectibleUniqueID, error) {
|
||||
var ids []CollectibleUniqueID
|
||||
for rows.Next() {
|
||||
id := CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(0)},
|
||||
}
|
||||
err := rows.Scan(
|
||||
&id.ContractID.ChainID,
|
||||
&id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func GroupCollectibleUIDsByChainID(uids []CollectibleUniqueID) map[w_common.ChainID][]CollectibleUniqueID {
|
||||
ret := make(map[w_common.ChainID][]CollectibleUniqueID)
|
||||
|
||||
|
|
Loading…
Reference in New Issue