feat(Wallet) complete the filter API

It uses the current data only and doesn't extend with new types or
include new features in activity sources DBs.

Major changes:
- Partially filter by chain IDs
- Partially filter by Status if it is the case
- Partially filter by token types
- Filter by counterparty addresses
- Use wallet accounts for TO/FROM instead of filters

Closes: #10634
This commit is contained in:
Stefan 2023-05-11 10:50:07 +03:00 committed by Stefan Dunca
parent 5777bb429a
commit e78a73bd9f
11 changed files with 797 additions and 267 deletions

View File

@ -0,0 +1,19 @@
package accounts
import (
"database/sql"
"testing"
"github.com/stretchr/testify/require"
)
func AddTestAccounts(t *testing.T, db *sql.DB, accounts []*Account) {
d, err := NewDB(db)
require.NoError(t, err)
err = d.SaveAccounts(accounts)
require.NoError(t, err)
res, err := d.GetAccounts()
require.NoError(t, err)
require.Equal(t, accounts, res)
}

View File

@ -9,12 +9,19 @@ import (
"strconv"
"strings"
"github.com/ethereum/go-ethereum/common"
eth "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/transfer"
"golang.org/x/exp/constraints"
)
type PayloadType = int
// Beware if adding/removing please check if affected and update the functions below
// - NewActivityEntryWithTransaction
// - multiTransactionTypeToActivityType
const (
MultiTransactionPT PayloadType = iota + 1
SimpleTransactionPT
@ -22,29 +29,34 @@ const (
)
type Entry struct {
// TODO: rename in payloadType
transactionType PayloadType
transaction *transfer.TransactionIdentity
id transfer.MultiTransactionIDType
timestamp int64
activityType Type
payloadType PayloadType
transaction *transfer.TransactionIdentity
id transfer.MultiTransactionIDType
timestamp int64
activityType Type
activityStatus Status
tokenType TokenType
}
type jsonSerializationTemplate struct {
TransactionType PayloadType `json:"transactionType"`
Transaction *transfer.TransactionIdentity `json:"transaction"`
ID transfer.MultiTransactionIDType `json:"id"`
Timestamp int64 `json:"timestamp"`
ActivityType Type `json:"activityType"`
PayloadType PayloadType `json:"payloadType"`
Transaction *transfer.TransactionIdentity `json:"transaction"`
ID transfer.MultiTransactionIDType `json:"id"`
Timestamp int64 `json:"timestamp"`
ActivityType Type `json:"activityType"`
ActivityStatus Status `json:"activityStatus"`
TokenType TokenType `json:"tokenType"`
}
func (e *Entry) MarshalJSON() ([]byte, error) {
return json.Marshal(jsonSerializationTemplate{
TransactionType: e.transactionType,
Transaction: e.transaction,
ID: e.id,
Timestamp: e.timestamp,
ActivityType: e.activityType,
PayloadType: e.payloadType,
Transaction: e.transaction,
ID: e.id,
Timestamp: e.timestamp,
ActivityType: e.activityType,
ActivityStatus: e.activityStatus,
TokenType: e.tokenType,
})
}
@ -55,7 +67,7 @@ func (e *Entry) UnmarshalJSON(data []byte) error {
return err
}
e.transactionType = aux.TransactionType
e.payloadType = aux.PayloadType
e.transaction = aux.Transaction
e.id = aux.ID
e.timestamp = aux.Timestamp
@ -63,31 +75,35 @@ func (e *Entry) UnmarshalJSON(data []byte) error {
return nil
}
func NewActivityEntryWithTransaction(transactionType PayloadType, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type) Entry {
if transactionType != SimpleTransactionPT && transactionType != PendingTransactionPT {
func NewActivityEntryWithTransaction(payloadType PayloadType, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry {
if payloadType != SimpleTransactionPT && payloadType != PendingTransactionPT {
panic("invalid transaction type")
}
return Entry{
transactionType: transactionType,
transaction: transaction,
id: 0,
timestamp: timestamp,
activityType: activityType,
payloadType: payloadType,
transaction: transaction,
id: 0,
timestamp: timestamp,
activityType: activityType,
activityStatus: activityStatus,
tokenType: AssetTT,
}
}
func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type) Entry {
func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry {
return Entry{
transactionType: MultiTransactionPT,
id: id,
timestamp: timestamp,
activityType: activityType,
payloadType: MultiTransactionPT,
id: id,
timestamp: timestamp,
activityType: activityType,
activityStatus: activityStatus,
tokenType: AssetTT,
}
}
func (e *Entry) TransactionType() PayloadType {
return e.transactionType
func (e *Entry) PayloadType() PayloadType {
return e.payloadType
}
func multiTransactionTypeToActivityType(mtType transfer.MultiTransactionType) Type {
@ -101,7 +117,7 @@ func multiTransactionTypeToActivityType(mtType transfer.MultiTransactionType) Ty
panic("unknown multi transaction type")
}
func typesContain(slice []Type, item Type) bool {
func sliceContains[T constraints.Ordered](slice []T, item T) bool {
for _, a := range slice {
if a == item {
return true
@ -110,31 +126,33 @@ func typesContain(slice []Type, item Type) bool {
return false
}
func joinMTTypes(types []transfer.MultiTransactionType) string {
var sb strings.Builder
for i, val := range types {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(strconv.Itoa(int(val)))
func joinItems[T interface{}](items []T, itemConversion func(T) string) string {
if len(items) == 0 {
return ""
}
var sb strings.Builder
if itemConversion == nil {
itemConversion = func(item T) string {
return fmt.Sprintf("%v", item)
}
}
for i, item := range items {
if i == 0 {
sb.WriteString("(")
} else {
sb.WriteString("),(")
}
sb.WriteString(itemConversion(item))
}
sb.WriteString(")")
return sb.String()
}
func joinAddresses(addresses []common.Address) string {
var sb strings.Builder
for i, address := range addresses {
if i == 0 {
sb.WriteString("('")
} else {
sb.WriteString("'),('")
}
sb.WriteString(strings.ToUpper(hex.EncodeToString(address[:])))
}
sb.WriteString("')")
return sb.String()
func joinAddresses(addresses []eth.Address) string {
return joinItems(addresses, func(a eth.Address) string {
return fmt.Sprintf("'%s'", strings.ToUpper(hex.EncodeToString(a[:])))
})
}
func activityTypesToMultiTransactionTypes(trTypes []Type) []transfer.MultiTransactionType {
@ -155,11 +173,25 @@ func activityTypesToMultiTransactionTypes(trTypes []Type) []transfer.MultiTransa
return mtTypes
}
// TODO: extend with SEND/RECEIVE for transfers and pending_transactions
// TODO: clarify if we include sender and receiver in pending_transactions as we do for transfers
// TODO optimization: consider implementing nullable []byte instead of using strings for addresses
// Query includes duplicates, will return multiple rows for the same transaction
const queryFormatString = `
const (
fromTrType = byte(1)
//toTrType = byte(2)
// TODO: Multi-transaction network information is missing in filtering
// TODO: extract token code for non transfer type eth
// TODO optimization: consider implementing nullable []byte instead of using strings for addresses
//
// Query includes duplicates, will return multiple rows for the same transaction if both to and from addresses
// are in the address list.
//
// The addresses list will have priority in deciding the source of the duplicate transaction. However, if the
// if the addresses list is empty, and all addresses should be included, the accounts table will be used
// see filter_addresses temp table is used
// The switch for tr_type is used to de-conflict the source for the two entries for the same transaction
//
// UNION ALL is used to avoid the overhead of DISTINCT given that we don't expect to have duplicate entries outside
// the sender and receiver addresses being in the list which is handled separately
queryFormatString = `
WITH filter_conditions AS (
SELECT
? AS startFilterDisabled,
@ -171,9 +203,26 @@ const queryFormatString = `
? AS filterActivityTypeSend,
? AS filterActivityTypeReceive,
? AS filterAllAddresses
? AS filterAllAddresses,
? AS filterAllToAddresses,
? AS filterAllActivityStatus,
? AS includeAllTokenTypeAssets,
? AS statusIsPending,
? AS includeAllNetworks
),
filter_addresses(address) AS (
SELECT HEX(address) FROM accounts WHERE (SELECT filterAllAddresses FROM filter_conditions) != 0
UNION ALL
SELECT * FROM (VALUES %s) WHERE (SELECT filterAllAddresses FROM filter_conditions) = 0
),
filter_to_addresses(address) AS (
VALUES %s
),
filter_assets(token_code) AS (
VALUES %s
),
filter_networks(network_id) AS (
VALUES %s
)
SELECT
@ -183,12 +232,48 @@ const queryFormatString = `
0 AS multi_tx_id,
transfers.timestamp AS timestamp,
NULL AS mt_type,
HEX(transfers.address) AS owner_address
CASE
WHEN from_join.address IS NOT NULL AND to_join.address IS NULL THEN 1
WHEN to_join.address IS NOT NULL AND from_join.address IS NULL THEN 2
WHEN from_join.address IS NOT NULL AND to_join.address IS NOT NULL THEN
CASE
WHEN from_join.address < to_join.address THEN 1
ELSE 2
END
ELSE NULL
END as tr_type,
transfers.sender AS from_address,
transfers.address AS to_address
FROM transfers, filter_conditions
LEFT JOIN
filter_addresses from_join ON HEX(transfers.sender) = from_join.address
LEFT JOIN
filter_addresses to_join ON HEX(transfers.address) = to_join.address
WHERE transfers.multi_transaction_id = 0
AND ((startFilterDisabled OR timestamp >= startTimestamp) AND (endFilterDisabled OR timestamp <= endTimestamp))
AND (filterActivityTypeAll OR (filterActivityTypeSend AND (filterAllAddresses OR (HEX(transfers.sender) IN filter_addresses))) OR (filterActivityTypeReceive AND (filterAllAddresses OR (HEX(transfers.address) IN filter_addresses))))
AND (filterAllAddresses OR (HEX(transfers.sender) IN filter_addresses) OR (HEX(transfers.address) IN filter_addresses))
AND ((startFilterDisabled OR timestamp >= startTimestamp)
AND (endFilterDisabled OR timestamp <= endTimestamp)
)
AND (filterActivityTypeAll
OR (filterActivityTypeSend
AND (filterAllAddresses
OR (HEX(transfers.sender) IN filter_addresses)
)
)
OR (filterActivityTypeReceive
AND (filterAllAddresses OR (HEX(transfers.address) IN filter_addresses))
)
)
AND (filterAllAddresses
OR (HEX(transfers.sender) IN filter_addresses)
OR (HEX(transfers.address) IN filter_addresses)
)
AND (filterAllToAddresses
OR (HEX(transfers.address) IN filter_to_addresses)
)
AND (includeAllTokenTypeAssets OR (transfers.type = "eth" AND ("ETH" IN filter_assets)))
AND (includeAllNetworks OR (transfers.network_id IN filter_networks))
UNION ALL
@ -199,12 +284,40 @@ const queryFormatString = `
0 AS multi_tx_id,
pending_transactions.timestamp AS timestamp,
NULL AS mt_type,
NULL AS owner_address
CASE
WHEN from_join.address IS NOT NULL AND to_join.address IS NULL THEN 1
WHEN to_join.address IS NOT NULL AND from_join.address IS NULL THEN 2
WHEN from_join.address IS NOT NULL AND to_join.address IS NOT NULL THEN
CASE
WHEN from_join.address < to_join.address THEN 1
ELSE 2
END
ELSE NULL
END as tr_type,
pending_transactions.from_address AS from_address,
pending_transactions.to_address AS to_address
FROM pending_transactions, filter_conditions
LEFT JOIN
filter_addresses from_join ON HEX(pending_transactions.from_address) = from_join.address
LEFT JOIN
filter_addresses to_join ON HEX(pending_transactions.to_address) = to_join.address
WHERE pending_transactions.multi_transaction_id = 0
AND ((startFilterDisabled OR timestamp >= startTimestamp) AND (endFilterDisabled OR timestamp <= endTimestamp))
AND (filterAllActivityStatus OR statusIsPending)
AND ((startFilterDisabled OR timestamp >= startTimestamp)
AND (endFilterDisabled OR timestamp <= endTimestamp)
)
AND (filterActivityTypeAll OR filterActivityTypeSend)
AND (filterAllAddresses OR (HEX(pending_transactions.from_address) IN filter_addresses) OR (HEX(pending_transactions.to_address) IN filter_addresses))
AND (filterAllAddresses
OR (HEX(pending_transactions.from_address) IN filter_addresses)
OR (HEX(pending_transactions.to_address) IN filter_addresses)
)
AND (filterAllToAddresses
OR (HEX(pending_transactions.to_address) IN filter_to_addresses)
)
AND (includeAllTokenTypeAssets OR (UPPER(pending_transactions.symbol) IN filter_assets))
AND (includeAllNetworks OR (pending_transactions.network_id IN filter_networks))
UNION ALL
@ -215,41 +328,89 @@ const queryFormatString = `
multi_transactions.ROWID AS multi_tx_id,
multi_transactions.timestamp AS timestamp,
multi_transactions.type AS mt_type,
NULL AS owner_address
NULL as tr_type,
multi_transactions.from_address AS from_address,
multi_transactions.to_address AS to_address
FROM multi_transactions, filter_conditions
WHERE ((startFilterDisabled OR timestamp >= startTimestamp) AND (endFilterDisabled OR timestamp <= endTimestamp))
WHERE ((startFilterDisabled OR timestamp >= startTimestamp)
AND (endFilterDisabled OR timestamp <= endTimestamp)
)
AND (filterActivityTypeAll OR (multi_transactions.type IN (%s)))
AND (filterAllAddresses OR (HEX(multi_transactions.from_address) IN filter_addresses) OR (HEX(multi_transactions.to_address) IN filter_addresses))
AND (filterAllAddresses
OR (HEX(multi_transactions.from_address) IN filter_addresses)
OR (HEX(multi_transactions.to_address) IN filter_addresses)
)
AND (filterAllToAddresses
OR (HEX(multi_transactions.to_address) IN filter_to_addresses)
)
AND (includeAllTokenTypeAssets OR (UPPER(multi_transactions.from_asset) IN filter_assets) OR (UPPER(multi_transactions.to_asset) IN filter_assets))
ORDER BY timestamp DESC
LIMIT ? OFFSET ?`
func GetActivityEntries(db *sql.DB, addresses []common.Address, chainIDs []uint64, filter Filter, offset int, limit int) ([]Entry, error) {
// Query the transfers, pending_transactions, and multi_transactions tables ordered by timestamp column
noEntriesInTmpTableSQLValues = "(NULL)"
)
// GetActivityEntries returns query the transfers, pending_transactions, and multi_transactions tables
// based on filter parameters and arguments
// it returns metadata for all entries ordered by timestamp column
//
// Adding a no-limit option was never considered or required.
func GetActivityEntries(db *sql.DB, addresses []eth.Address, chainIDs []common.ChainID, filter Filter, offset int, limit int) ([]Entry, error) {
// TODO: filter collectibles after they are added to multi_transactions table
if len(filter.Tokens.EnabledTypes) > 0 && !sliceContains(filter.Tokens.EnabledTypes, AssetTT) {
// For now we deal only with assets so return empty result
return []Entry{}, nil
}
includeAllTokenTypeAssets := (len(filter.Tokens.EnabledTypes) == 0 ||
sliceContains(filter.Tokens.EnabledTypes, AssetTT)) && len(filter.Tokens.Assets) == 0
assets := noEntriesInTmpTableSQLValues
if !includeAllTokenTypeAssets {
assets = joinItems(filter.Tokens.Assets, func(item TokenCode) string { return fmt.Sprintf("'%v'", item) })
}
includeAllNetworks := len(chainIDs) == 0
networks := noEntriesInTmpTableSQLValues
if !includeAllNetworks {
networks = joinItems(chainIDs, nil)
}
// TODO: finish filter: chainIDs, statuses, tokenTypes, counterpartyAddresses
// TODO: use all accounts list for detecting SEND/RECEIVE instead of the current addresses list; also change activityType detection in transfer part
startFilterDisabled := !(filter.Period.StartTimestamp > 0)
endFilterDisabled := !(filter.Period.EndTimestamp > 0)
filterActivityTypeAll := typesContain(filter.Types, AllAT) || len(filter.Types) == 0
filterActivityTypeAll := len(filter.Types) == 0
filterAllAddresses := len(addresses) == 0
filterAllToAddresses := len(filter.CounterpartyAddresses) == 0
includeAllStatuses := len(filter.Statuses) == 0
//fmt.Println("@dd filter: timeEnabled", filter.Period.StartTimestamp, filter.Period.EndTimestamp, "; type", filter.Types, "offset", offset, "limit", limit)
statusIsPending := false
if !includeAllStatuses {
statusIsPending = sliceContains(filter.Statuses, PendingAS)
}
joinedAddresses := "(NULL)"
involvedAddresses := noEntriesInTmpTableSQLValues
if !filterAllAddresses {
joinedAddresses = joinAddresses(addresses)
involvedAddresses = joinAddresses(addresses)
}
toAddresses := noEntriesInTmpTableSQLValues
if !filterAllToAddresses {
toAddresses = joinAddresses(filter.CounterpartyAddresses)
}
mtTypes := activityTypesToMultiTransactionTypes(filter.Types)
joinedMTTypes := joinMTTypes(mtTypes)
joinedMTTypes := joinItems(mtTypes, func(t transfer.MultiTransactionType) string {
return strconv.Itoa(int(t))
})
queryString := fmt.Sprintf(queryFormatString, joinedAddresses, joinedMTTypes)
queryString := fmt.Sprintf(queryFormatString, involvedAddresses, toAddresses, assets, networks,
joinedMTTypes)
rows, err := db.Query(queryString,
startFilterDisabled, filter.Period.StartTimestamp, endFilterDisabled, filter.Period.EndTimestamp,
filterActivityTypeAll, typesContain(filter.Types, SendAT), typesContain(filter.Types, ReceiveAT),
filterAllAddresses,
filterActivityTypeAll, sliceContains(filter.Types, SendAT), sliceContains(filter.Types, ReceiveAT),
filterAllAddresses, filterAllToAddresses, includeAllStatuses, includeAllTokenTypeAssets, statusIsPending,
includeAllNetworks,
limit, offset)
if err != nil {
return nil, err
@ -261,30 +422,41 @@ func GetActivityEntries(db *sql.DB, addresses []common.Address, chainIDs []uint6
var transferHash, pendingHash []byte
var chainID, multiTxID sql.NullInt64
var timestamp int64
var dbActivityType sql.NullByte
var dbAddress sql.NullString
err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, &timestamp, &dbActivityType, &dbAddress)
var dbMtType, dbTrType sql.NullByte
var toAddress, fromAddress eth.Address
err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, &timestamp, &dbMtType, &dbTrType, &fromAddress, &toAddress)
if err != nil {
return nil, err
}
getActivityType := func(trType sql.NullByte) (activityType Type, filteredAddress eth.Address) {
if trType.Valid && trType.Byte == fromTrType {
return SendAT, fromAddress
}
// Don't expect this to happen due to trType = NULL outside of tests
return ReceiveAT, toAddress
}
var entry Entry
if transferHash != nil && chainID.Valid {
var activityType Type = SendAT
thisAddress := common.HexToAddress(dbAddress.String)
for _, address := range addresses {
if address == thisAddress {
activityType = ReceiveAT
}
}
entry = NewActivityEntryWithTransaction(SimpleTransactionPT, &transfer.TransactionIdentity{ChainID: uint64(chainID.Int64), Hash: common.BytesToHash(transferHash), Address: thisAddress}, timestamp, activityType)
// TODO: extend DB with status in order to filter by status. The status has to be extracted from the receipt upon downloading
activityStatus := FinalizedAS
activityType, filteredAddress := getActivityType(dbTrType)
entry = NewActivityEntryWithTransaction(SimpleTransactionPT,
&transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64), Hash: eth.BytesToHash(transferHash), Address: filteredAddress},
timestamp, activityType, activityStatus)
} else if pendingHash != nil && chainID.Valid {
var activityType Type = SendAT
entry = NewActivityEntryWithTransaction(PendingTransactionPT, &transfer.TransactionIdentity{ChainID: uint64(chainID.Int64), Hash: common.BytesToHash(pendingHash)}, timestamp, activityType)
activityStatus := PendingAS
activityType, _ := getActivityType(dbTrType)
entry = NewActivityEntryWithTransaction(PendingTransactionPT,
&transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64), Hash: eth.BytesToHash(pendingHash)},
timestamp, activityType, activityStatus)
} else if multiTxID.Valid {
activityType := multiTransactionTypeToActivityType(transfer.MultiTransactionType(dbActivityType.Byte))
activityType := multiTransactionTypeToActivityType(transfer.MultiTransactionType(dbMtType.Byte))
// TODO: aggregate status from all sub-transactions
activityStatus := FinalizedAS
entry = NewActivityEntryWithMultiTransaction(transfer.MultiTransactionIDType(multiTxID.Int64),
timestamp, activityType)
timestamp, activityType, activityStatus)
} else {
return nil, errors.New("invalid row data")
}

View File

@ -5,10 +5,14 @@ import (
"testing"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"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/ethereum/go-ethereum/common"
eth "github.com/ethereum/go-ethereum/common"
eth_common "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
@ -22,15 +26,6 @@ func setupTestActivityDB(t *testing.T) (db *sql.DB, close func()) {
}
}
func insertTestPendingTransaction(t *testing.T, db *sql.DB, tr *transfer.TestTransaction) {
_, err := db.Exec(`
INSERT INTO pending_transactions (network_id, hash, timestamp, from_address, to_address,
symbol, gas_price, gas_limit, value, data, type, additional_data, multi_transaction_id
) VALUES (?, ?, ?, ?, ?, 'ETH', 0, 0, ?, '', 'test', '', ?)`,
tr.ChainID, tr.Hash, tr.Timestamp, tr.From, tr.To, tr.Value, tr.MultiTransactionID)
require.NoError(t, err)
}
type testData struct {
tr1 transfer.TestTransaction // index 1
pendingTr transfer.TestTransaction // index 2
@ -51,12 +46,15 @@ func fillTestData(t *testing.T, db *sql.DB) (td testData) {
transfer.InsertTestTransfer(t, db, &td.tr1)
td.pendingTr = trs[1]
insertTestPendingTransaction(t, db, &td.pendingTr)
transfer.InsertTestPendingTransaction(t, db, &td.pendingTr)
td.singletonMTr = trs[2]
td.singletonMTr.FromToken = testutils.SntSymbol
td.singletonMTr.ToToken = testutils.DaiSymbol
td.singletonMTID = transfer.InsertTestMultiTransaction(t, db, &td.singletonMTr)
td.mTr = trs[3]
td.mTr.ToToken = testutils.SntSymbol
td.mTrID = transfer.InsertTestMultiTransaction(t, db, &td.mTr)
td.subTr = trs[4]
@ -65,7 +63,7 @@ func fillTestData(t *testing.T, db *sql.DB) (td testData) {
td.subPendingTr = trs[5]
td.subPendingTr.MultiTransactionID = td.mTrID
insertTestPendingTransaction(t, db, &td.subPendingTr)
transfer.InsertTestPendingTransaction(t, db, &td.subPendingTr)
return
}
@ -76,7 +74,7 @@ func TestGetActivityEntriesAll(t *testing.T) {
td := fillTestData(t, db)
var filter Filter
entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 10)
entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 10)
require.NoError(t, err)
require.Equal(t, 4, len(entries))
@ -88,48 +86,60 @@ func TestGetActivityEntriesAll(t *testing.T) {
}
require.True(t, testutils.StructExistsInSlice(Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To},
id: td.tr1.MultiTransactionID,
timestamp: td.tr1.Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To},
id: td.tr1.MultiTransactionID,
timestamp: td.tr1.Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries))
require.True(t, testutils.StructExistsInSlice(Entry{
transactionType: PendingTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.pendingTr.ChainID, Hash: td.pendingTr.Hash},
id: td.pendingTr.MultiTransactionID,
timestamp: td.pendingTr.Timestamp,
activityType: SendAT,
payloadType: PendingTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.pendingTr.ChainID, Hash: td.pendingTr.Hash},
id: td.pendingTr.MultiTransactionID,
timestamp: td.pendingTr.Timestamp,
activityType: ReceiveAT,
activityStatus: PendingAS,
tokenType: AssetTT,
}, entries))
require.True(t, testutils.StructExistsInSlice(Entry{
transactionType: MultiTransactionPT,
transaction: nil,
id: td.singletonMTID,
timestamp: td.singletonMTr.Timestamp,
activityType: SendAT,
payloadType: MultiTransactionPT,
transaction: nil,
id: td.singletonMTID,
timestamp: td.singletonMTr.Timestamp,
activityType: SendAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries))
require.True(t, testutils.StructExistsInSlice(Entry{
transactionType: MultiTransactionPT,
transaction: nil,
id: td.mTrID,
timestamp: td.mTr.Timestamp,
activityType: SendAT,
payloadType: MultiTransactionPT,
transaction: nil,
id: td.mTrID,
timestamp: td.mTr.Timestamp,
activityType: SendAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries))
// Ensure the sub-transactions of the multi-transactions are not returned
require.False(t, testutils.StructExistsInSlice(Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.subTr.ChainID, Hash: td.subTr.Hash, Address: td.subTr.To},
id: td.subTr.MultiTransactionID,
timestamp: td.subTr.Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.subTr.ChainID, Hash: td.subTr.Hash, Address: td.subTr.To},
id: td.subTr.MultiTransactionID,
timestamp: td.subTr.Timestamp,
activityType: SendAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries))
require.False(t, testutils.StructExistsInSlice(Entry{
transactionType: PendingTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.subPendingTr.ChainID, Hash: td.subPendingTr.Hash},
id: td.subPendingTr.MultiTransactionID,
timestamp: td.subPendingTr.Timestamp,
activityType: SendAT,
payloadType: PendingTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.subPendingTr.ChainID, Hash: td.subPendingTr.Hash},
id: td.subPendingTr.MultiTransactionID,
timestamp: td.subPendingTr.Timestamp,
activityType: SendAT,
activityStatus: PendingAS,
tokenType: AssetTT,
}, entries))
}
@ -146,14 +156,40 @@ func TestGetActivityEntriesWithSameTransactionForSenderAndReceiverInDB(t *testin
prevTo := receiverTr.To
receiverTr.To = td.tr1.From
receiverTr.From = prevTo
// TODO: test also when there is a transaction in the other direction
// Ensure they are the oldest transactions (last in the list) and we have a consistent order
receiverTr.Timestamp--
transfer.InsertTestTransfer(t, db, &receiverTr)
var filter Filter
entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 10)
entries, err := GetActivityEntries(db, []eth.Address{td.tr1.From, receiverTr.From}, []common.ChainID{}, filter, 0, 10)
require.NoError(t, err)
require.Equal(t, 2, len(entries))
// Check that the transaction are labeled alternatively as send and receive
require.Equal(t, ReceiveAT, entries[1].activityType)
require.NotEqual(t, eth.Address{}, entries[1].transaction.Address)
require.Equal(t, receiverTr.To, entries[1].transaction.Address)
require.Equal(t, SendAT, entries[0].activityType)
require.NotEqual(t, eth.Address{}, entries[0].transaction.Address)
require.Equal(t, td.tr1.From, entries[0].transaction.Address)
// add accounts to DB for proper detection of sender/receiver in all cases
accounts.AddTestAccounts(t, db, []*accounts.Account{
{Address: types.Address(td.tr1.From), Chat: false, Wallet: true},
{Address: types.Address(receiverTr.From)},
})
entries, err = GetActivityEntries(db, []eth.Address{}, []common.ChainID{}, filter, 0, 10)
require.NoError(t, err)
// TODO: decide how should we handle this case filter out or include it in the result
// For now we include both. Can be changed by using UNION instead of UNION ALL in the query or by filtering out
require.Equal(t, 5, len(entries))
// Check that the transaction are labeled alternatively as send and receive
require.Equal(t, ReceiveAT, entries[4].activityType)
require.Equal(t, SendAT, entries[3].activityType)
}
func TestGetActivityEntriesFilterByTime(t *testing.T) {
@ -170,65 +206,78 @@ func TestGetActivityEntriesFilterByTime(t *testing.T) {
// Test start only
var filter Filter
filter.Period.StartTimestamp = td.singletonMTr.Timestamp
entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15)
filter.Period.EndTimestamp = NoLimitTimestampForPeriod
entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 8, len(entries))
// Check start and end content
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[5].ChainID, Hash: trs[5].Hash, Address: trs[5].To},
id: 0,
timestamp: trs[5].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[5].ChainID, Hash: trs[5].Hash, Address: trs[5].To},
id: 0,
timestamp: trs[5].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[0])
require.Equal(t, Entry{
transactionType: MultiTransactionPT,
transaction: nil,
id: td.singletonMTID,
timestamp: td.singletonMTr.Timestamp,
activityType: SendAT,
payloadType: MultiTransactionPT,
transaction: nil,
id: td.singletonMTID,
timestamp: td.singletonMTr.Timestamp,
activityType: SendAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[7])
// Test complete interval
filter.Period.EndTimestamp = trs[2].Timestamp
entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15)
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 5, len(entries))
// Check start and end content
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To},
id: 0,
timestamp: trs[2].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To},
id: 0,
timestamp: trs[2].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[0])
require.Equal(t, Entry{
transactionType: MultiTransactionPT,
transaction: nil,
id: td.singletonMTID,
timestamp: td.singletonMTr.Timestamp,
activityType: SendAT,
payloadType: MultiTransactionPT,
transaction: nil,
id: td.singletonMTID,
timestamp: td.singletonMTr.Timestamp,
activityType: SendAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[4])
// Test end only
filter.Period.StartTimestamp = 0
entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15)
filter.Period.StartTimestamp = NoLimitTimestampForPeriod
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 7, len(entries))
// Check start and end content
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To},
id: 0,
timestamp: trs[2].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To},
id: 0,
timestamp: trs[2].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[0])
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To},
id: 0,
timestamp: td.tr1.Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: td.tr1.ChainID, Hash: td.tr1.Hash, Address: td.tr1.To},
id: 0,
timestamp: td.tr1.Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[6])
}
@ -244,63 +293,73 @@ func TestGetActivityEntriesCheckOffsetAndLimit(t *testing.T) {
var filter Filter
// Get all
entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 5)
entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 5)
require.NoError(t, err)
require.Equal(t, 5, len(entries))
// Get time based interval
filter.Period.StartTimestamp = trs[2].Timestamp
filter.Period.EndTimestamp = trs[8].Timestamp
entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 3)
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 3)
require.NoError(t, err)
require.Equal(t, 3, len(entries))
// Check start and end content
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[8].ChainID, Hash: trs[8].Hash, Address: trs[8].To},
id: 0,
timestamp: trs[8].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[8].ChainID, Hash: trs[8].Hash, Address: trs[8].To},
id: 0,
timestamp: trs[8].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[0])
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To},
id: 0,
timestamp: trs[6].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To},
id: 0,
timestamp: trs[6].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[2])
// Move window 2 entries forward
entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 2, 3)
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 2, 3)
require.NoError(t, err)
require.Equal(t, 3, len(entries))
// Check start and end content
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To},
id: 0,
timestamp: trs[6].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[6].ChainID, Hash: trs[6].Hash, Address: trs[6].To},
id: 0,
timestamp: trs[6].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[0])
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To},
id: 0,
timestamp: trs[4].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To},
id: 0,
timestamp: trs[4].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[2])
// Move window 4 more entries to test filter cap
entries, err = GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 6, 3)
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 6, 3)
require.NoError(t, err)
require.Equal(t, 1, len(entries))
// Check start and end content
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To},
id: 0,
timestamp: trs[2].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[2].ChainID, Hash: trs[2].Hash, Address: trs[2].To},
id: 0,
timestamp: trs[2].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[0])
}
@ -310,7 +369,7 @@ func TestGetActivityEntriesFilterByType(t *testing.T) {
// Adds 4 extractable transactions
fillTestData(t, db)
// Add 6 extractable transactions: one MultiTransactionSwap, two MultiTransactionBridge rest Send
// Add 6 extractable transactions: one MultiTransactionSwap, two MultiTransactionBridge rest MultiTransactionSend
trs := transfer.GenerateTestTransactions(t, db, 6, 6)
trs[1].MultiTransactionType = transfer.MultiTransactionBridge
trs[3].MultiTransactionType = transfer.MultiTransactionSwap
@ -326,33 +385,43 @@ func TestGetActivityEntriesFilterByType(t *testing.T) {
// Test filtering out without address involved
var filter Filter
// TODO: add more types to cover all cases
filter.Types = allActivityTypesFilter()
entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
filter.Types = []Type{SendAT, SwapAT}
entries, err := GetActivityEntries(db, []common.Address{}, []uint64{}, filter, 0, 15)
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 8, len(entries))
swapCount := 0
sendCount := 0
receiveCount := 0
for _, entry := range entries {
if entry.activityType == SendAT {
sendCount++
}
if entry.activityType == ReceiveAT {
receiveCount++
}
if entry.activityType == SwapAT {
swapCount++
}
}
require.Equal(t, 7, sendCount)
require.Equal(t, 2, sendCount)
require.Equal(t, 5, receiveCount)
require.Equal(t, 1, swapCount)
// Test filtering out with address involved
filter.Types = []Type{BridgeAT, ReceiveAT}
// Include one "to" from transfers to be detected as receive
addresses := []common.Address{trs[0].To, trs[1].To, trs[2].From, trs[3].From, trs[5].From}
entries, err = GetActivityEntries(db, addresses, []uint64{}, filter, 0, 15)
addresses := []eth_common.Address{trs[0].To, trs[1].To, trs[2].From, trs[3].From, trs[5].From}
entries, err = GetActivityEntries(db, addresses, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 3, len(entries))
bridgeCount := 0
receiveCount := 0
receiveCount = 0
for _, entry := range entries {
if entry.activityType == BridgeAT {
bridgeCount++
@ -365,42 +434,241 @@ func TestGetActivityEntriesFilterByType(t *testing.T) {
require.Equal(t, 1, receiveCount)
}
func TestGetActivityEntriesFilterByAddress(t *testing.T) {
func TestGetActivityEntriesFilterByAddresses(t *testing.T) {
db, close := setupTestActivityDB(t)
defer close()
// Adds 4 extractable transactions
td := fillTestData(t, db)
// Add 6 extractable transactions: one MultiTransactionSwap, two MultiTransactionBridge rest Send
trs := transfer.GenerateTestTransactions(t, db, 7, 6)
for i := range trs {
transfer.InsertTestTransfer(t, db, &trs[i])
}
var filter Filter
addressesFilter := []common.Address{td.mTr.To, trs[1].From, trs[4].To}
entries, err := GetActivityEntries(db, addressesFilter, []uint64{}, filter, 0, 15)
addressesFilter := allAddressesFilter()
entries, err := GetActivityEntries(db, addressesFilter, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
addressesFilter = []eth_common.Address{td.mTr.To, trs[1].From, trs[4].To}
entries, err = GetActivityEntries(db, addressesFilter, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 3, len(entries))
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To},
id: 0,
timestamp: trs[4].Timestamp,
activityType: ReceiveAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[4].ChainID, Hash: trs[4].Hash, Address: trs[4].To},
id: 0,
timestamp: trs[4].Timestamp,
activityType: ReceiveAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[0])
require.Equal(t, Entry{
transactionType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[1].ChainID, Hash: trs[1].Hash, Address: trs[1].To},
id: 0,
timestamp: trs[1].Timestamp,
activityType: SendAT,
payloadType: SimpleTransactionPT,
transaction: &transfer.TransactionIdentity{ChainID: trs[1].ChainID, Hash: trs[1].Hash, Address: trs[1].From},
id: 0,
timestamp: trs[1].Timestamp,
activityType: SendAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[1])
require.Equal(t, Entry{
transactionType: MultiTransactionPT,
transaction: nil,
id: td.mTrID,
timestamp: td.mTr.Timestamp,
activityType: SendAT,
payloadType: MultiTransactionPT,
transaction: nil,
id: td.mTrID,
timestamp: td.mTr.Timestamp,
activityType: SendAT,
activityStatus: FinalizedAS,
tokenType: AssetTT,
}, entries[2])
}
func TestGetActivityEntriesFilterByStatus(t *testing.T) {
db, close := setupTestActivityDB(t)
defer close()
// Adds 4 extractable transactions
fillTestData(t, db)
// Add 6 extractable transactions
trs := transfer.GenerateTestTransactions(t, db, 7, 6)
for i := range trs {
transfer.InsertTestTransfer(t, db, &trs[i])
}
var filter Filter
filter.Statuses = []Status{}
entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
filter.Statuses = allActivityStatusesFilter()
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
// TODO: enabled and finish tests after extending DB with transaction status
//
// filter.Statuses = []Status{PendingAS}
// entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
// require.NoError(t, err)
// require.Equal(t, 1, len(entries))
// filter.Statuses = []Status{FailedAS, CompleteAS}
// entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
// require.NoError(t, err)
// require.Equal(t, 9, len(entries))
}
func TestGetActivityEntriesFilterByTokenType(t *testing.T) {
db, close := setupTestActivityDB(t)
defer close()
// Adds 4 extractable transactions 2 transactions ETH, one MT SNT to DAI and another MT ETH to SNT
fillTestData(t, db)
// Add 6 extractable transactions with USDC (only erc20 as type in DB)
trs := transfer.GenerateTestTransactions(t, db, 7, 6)
for i := range trs {
trs[i].FromToken = "USDC"
transfer.InsertTestTransfer(t, db, &trs[i])
}
var filter Filter
filter.Tokens = noAssetsFilter()
entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 0, len(entries))
filter.Tokens = allTokensFilter()
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
// Regression when collectibles is nil
filter.Tokens = Tokens{[]TokenCode{}, nil, []TokenType{}}
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
filter.Tokens = Tokens{Assets: []TokenCode{"ETH"}, EnabledTypes: []TokenType{AssetTT}}
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 3, len(entries))
// TODO: update tests after adding token type to transfers
filter.Tokens = Tokens{Assets: []TokenCode{"USDC", "DAI"}, EnabledTypes: []TokenType{AssetTT}}
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 1, len(entries))
// Regression when EnabledTypes ar empty
filter.Tokens = Tokens{Assets: []TokenCode{"USDC", "DAI"}, EnabledTypes: []TokenType{}}
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 1, len(entries))
}
func TestGetActivityEntriesFilterByToAddresses(t *testing.T) {
db, close := setupTestActivityDB(t)
defer close()
// Adds 4 extractable transactions
td := fillTestData(t, db)
// Add 6 extractable transactions
trs := transfer.GenerateTestTransactions(t, db, 7, 6)
for i := range trs {
transfer.InsertTestTransfer(t, db, &trs[i])
}
var filter Filter
filter.CounterpartyAddresses = allAddressesFilter()
entries, err := GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
filter.CounterpartyAddresses = []eth_common.Address{eth_common.HexToAddress("0x567890")}
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 0, len(entries))
filter.CounterpartyAddresses = []eth_common.Address{td.pendingTr.To, td.mTr.To, trs[3].To}
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 3, len(entries))
filter.CounterpartyAddresses = []eth_common.Address{td.tr1.To, td.pendingTr.From, trs[3].From, trs[5].To}
entries, err = GetActivityEntries(db, []eth_common.Address{}, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 2, len(entries))
}
func TestGetActivityEntriesFilterByNetworks(t *testing.T) {
db, close := setupTestActivityDB(t)
defer close()
// Adds 4 extractable transactions
td := fillTestData(t, db)
// Add 6 extractable transactions
trs := transfer.GenerateTestTransactions(t, db, 7, 6)
for i := range trs {
transfer.InsertTestTransfer(t, db, &trs[i])
}
var filter Filter
chainIDs := allNetworksFilter()
entries, err := GetActivityEntries(db, []eth_common.Address{}, chainIDs, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 10, len(entries))
chainIDs = []common.ChainID{5674839210}
entries, err = GetActivityEntries(db, []eth_common.Address{}, chainIDs, filter, 0, 15)
require.NoError(t, err)
// TODO: update after multi-transactions are filterable by ChainID
require.Equal(t, 2 /*0*/, len(entries))
chainIDs = []common.ChainID{td.pendingTr.ChainID, td.mTr.ChainID, trs[3].ChainID}
entries, err = GetActivityEntries(db, []eth_common.Address{}, chainIDs, filter, 0, 15)
require.NoError(t, err)
// TODO: update after multi-transactions are filterable by ChainID
require.Equal(t, 4 /*3*/, len(entries))
}
func TestGetActivityEntriesCheckToAndFrom(t *testing.T) {
db, close := setupTestActivityDB(t)
defer close()
// Adds 6 transactions from which 4 are filered out
td := fillTestData(t, db)
// Add extra transactions to test To address
trs := transfer.GenerateTestTransactions(t, db, 7, 2)
transfer.InsertTestTransfer(t, db, &trs[0])
transfer.InsertTestPendingTransaction(t, db, &trs[1])
addresses := []eth_common.Address{td.tr1.From, td.pendingTr.From,
td.singletonMTr.From, td.mTr.To, trs[0].To, trs[1].To}
var filter Filter
entries, err := GetActivityEntries(db, addresses, []common.ChainID{}, filter, 0, 15)
require.NoError(t, err)
require.Equal(t, 6, len(entries))
require.Equal(t, SendAT, entries[5].activityType) // td.tr1
require.NotEqual(t, eth.Address{}, entries[5].transaction.Address) // td.tr1
require.Equal(t, td.tr1.From, entries[5].transaction.Address) // td.tr1
require.Equal(t, SendAT, entries[4].activityType) // td.pendingTr
// Multi-transactions are always considered as SendAT
require.Equal(t, SendAT, entries[3].activityType) // td.singletonMTr
require.Equal(t, SendAT, entries[2].activityType) // td.mTr
require.Equal(t, ReceiveAT, entries[1].activityType) // trs[0]
require.NotEqual(t, eth.Address{}, entries[1].transaction.Address) // trs[0]
require.Equal(t, trs[0].To, entries[1].transaction.Address) // trs[0]
require.Equal(t, ReceiveAT, entries[0].activityType) // trs[1] (pending)
// TODO: add accounts to DB for proper detection of sender/receiver
// TODO: Test with all addresses
}

View File

@ -1,9 +1,13 @@
package activity
import "github.com/ethereum/go-ethereum/common"
import (
eth_common "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/services/wallet/common"
)
const NoLimitTimestampForPeriod = 0
type Period struct {
// 0 means no limit
StartTimestamp int64 `json:"startTimestamp"`
EndTimestamp int64 `json:"endTimestamp"`
}
@ -11,36 +15,69 @@ type Period struct {
type Type int
const (
AllAT Type = iota
SendAT
SendAT Type = iota
ReceiveAT
BuyAT
SwapAT
BridgeAT
)
func allActivityTypesFilter() []Type {
return []Type{}
}
type Status int
const (
AllAS Status = iota
FailedAS
FailedAS Status = iota
PendingAS
CompleteAS
FinalizedAS
)
func allActivityStatusesFilter() []Status {
return []Status{}
}
type TokenType int
const (
AllTT TokenType = iota
AssetTT
AssetTT TokenType = iota
CollectiblesTT
)
type Filter struct {
Period Period `json:"period"`
Types []Type `json:"types"`
Statuses []Status `json:"statuses"`
TokenTypes []TokenType `json:"tokenTypes"`
CounterpartyAddresses []common.Address `json:"counterpartyAddresses"`
type TokenCode string
// Tokens the following rules apply for its members:
// empty member: none is selected
// nil means all
// see allTokensFilter and noTokensFilter
type Tokens struct {
Assets []TokenCode `json:"assets"`
Collectibles []eth_common.Address `json:"collectibles"`
EnabledTypes []TokenType `json:"enabledTypes"`
}
func noAssetsFilter() Tokens {
return Tokens{[]TokenCode{}, []eth_common.Address{}, []TokenType{CollectiblesTT}}
}
func allTokensFilter() Tokens {
return Tokens{}
}
func allAddressesFilter() []eth_common.Address {
return []eth_common.Address{}
}
func allNetworksFilter() []common.ChainID {
return []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"`
}

View File

@ -19,6 +19,8 @@ import (
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/services/wallet/transfer"
wallet_common "github.com/status-im/status-go/services/wallet/common"
)
func NewAPI(s *Service) *API {
@ -247,7 +249,7 @@ func (api *API) GetPendingTransactionsForIdentities(ctx context.Context, identit
result = make([]*transfer.PendingTransaction, 0, len(identities))
var pt *transfer.PendingTransaction
for _, identity := range identities {
pt, err = api.s.transactionManager.GetPendingEntry(identity.ChainID, identity.Hash)
pt, err = api.s.transactionManager.GetPendingEntry(uint64(identity.ChainID), identity.Hash)
result = append(result, pt)
}
@ -525,7 +527,7 @@ func (api *API) FetchAllCurrencyFormats() (currency.FormatPerSymbol, error) {
return api.s.currency.FetchAllCurrencyFormats()
}
func (api *API) GetActivityEntries(addresses []common.Address, chainIDs []uint64, filter activity.Filter, offset int, limit int) ([]activity.Entry, error) {
func (api *API) GetActivityEntries(addresses []common.Address, chainIDs []wallet_common.ChainID, filter activity.Filter, offset int, limit int) ([]activity.Entry, error) {
log.Debug("call to GetActivityEntries")
return activity.GetActivityEntries(api.s.db, addresses, chainIDs, filter, offset, limit)
}

View File

@ -2,6 +2,10 @@ package testutils
import "reflect"
const EthSymbol = "ETH"
const SntSymbol = "SNT"
const DaiSymbol = "DAI"
func StructExistsInSlice[T any](target T, slice []T) bool {
for _, item := range slice {
if reflect.DeepEqual(target, item) {

View File

@ -279,7 +279,7 @@ func (db *Database) GetTransfersForIdentities(ctx context.Context, identities []
for _, identity := range identities {
subQuery := newSubQuery()
// TODO optimization: consider using tuples in sqlite and IN operator
subQuery = subQuery.FilterNetwork(identity.ChainID).FilterTransactionHash(identity.Hash).FilterAddress(identity.Address)
subQuery = subQuery.FilterNetwork(uint64(identity.ChainID)).FilterTransactionHash(identity.Hash).FilterAddress(identity.Address)
query.addSubQuery(subQuery, OrSeparator)
}
rows, err := db.client.QueryContext(ctx, query.String(), query.Args()...)

View File

@ -223,8 +223,8 @@ func TestGetTransfersForIdentities(t *testing.T) {
require.Equal(t, big.NewInt(trs[3].BlkNumber), entries[1].BlockNumber)
require.Equal(t, uint64(trs[1].Timestamp), entries[0].Timestamp)
require.Equal(t, uint64(trs[3].Timestamp), entries[1].Timestamp)
require.Equal(t, trs[1].ChainID, entries[0].NetworkID)
require.Equal(t, trs[3].ChainID, entries[1].NetworkID)
require.Equal(t, uint64(trs[1].ChainID), entries[0].NetworkID)
require.Equal(t, uint64(trs[3].ChainID), entries[1].NetworkID)
require.Equal(t, MultiTransactionIDType(0), entries[0].MultiTransactionID)
require.Equal(t, MultiTransactionIDType(0), entries[1].MultiTransactionID)
}

View File

@ -53,6 +53,7 @@ func (q *transfersQuery) addWhereSeparator(separator SeparatorType) {
type SeparatorType int
// Beware if changing this enum please update addWhereSeparator as well
const (
NoSeparator SeparatorType = iota + 1
OrSeparator

View File

@ -3,18 +3,23 @@ package transfer
import (
"database/sql"
"fmt"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
eth_common "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/stretchr/testify/require"
)
type TestTransaction struct {
Hash common.Hash
ChainID uint64
From common.Address // [sender]
To common.Address // [address]
Hash eth_common.Hash
ChainID common.ChainID
From eth_common.Address // [sender]
To eth_common.Address // [address]
FromToken string // used to detect type in transfers table
ToToken string // only used in multi_transactions table
Timestamp int64
Value int64
BlkNumber int64
@ -25,10 +30,10 @@ type TestTransaction struct {
func GenerateTestTransactions(t *testing.T, db *sql.DB, firstStartIndex int, count int) (result []TestTransaction) {
for i := firstStartIndex; i < (firstStartIndex + count); i++ {
tr := TestTransaction{
Hash: common.HexToHash(fmt.Sprintf("0x1%d", i)),
ChainID: uint64(i),
From: common.HexToAddress(fmt.Sprintf("0x2%d", i)),
To: common.HexToAddress(fmt.Sprintf("0x3%d", i)),
Hash: eth_common.HexToHash(fmt.Sprintf("0x1%d", i)),
ChainID: common.ChainID(i),
From: eth_common.HexToAddress(fmt.Sprintf("0x2%d", i)),
To: eth_common.HexToAddress(fmt.Sprintf("0x3%d", i)),
Timestamp: int64(i),
Value: int64(i),
BlkNumber: int64(i),
@ -42,7 +47,11 @@ func GenerateTestTransactions(t *testing.T, db *sql.DB, firstStartIndex int, cou
func InsertTestTransfer(t *testing.T, db *sql.DB, tr *TestTransaction) {
// Respect `FOREIGN KEY(network_id,address,blk_hash)` of `transfers` table
blkHash := common.HexToHash("4")
tokenType := "eth"
if tr.FromToken != "" && strings.ToUpper(tr.FromToken) != testutils.EthSymbol {
tokenType = "erc20"
}
blkHash := eth_common.HexToHash("4")
_, err := db.Exec(`
INSERT OR IGNORE INTO blocks(
network_id, address, blk_number, blk_hash
@ -50,17 +59,34 @@ func InsertTestTransfer(t *testing.T, db *sql.DB, tr *TestTransaction) {
INSERT INTO transfers (network_id, hash, address, blk_hash, tx,
sender, receipt, log, type, blk_number, timestamp, loaded,
multi_transaction_id, base_gas_fee
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "test", ?, ?, 0, ?, 0)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0)`,
tr.ChainID, tr.To, tr.BlkNumber, blkHash,
tr.ChainID, tr.Hash, tr.To, blkHash, &JSONBlob{}, tr.From, &JSONBlob{}, &JSONBlob{}, tr.BlkNumber, tr.Timestamp, tr.MultiTransactionID)
tr.ChainID, tr.Hash, tr.To, blkHash, &JSONBlob{}, tr.From, &JSONBlob{}, &JSONBlob{}, tokenType, tr.BlkNumber, tr.Timestamp, tr.MultiTransactionID)
require.NoError(t, err)
}
func InsertTestPendingTransaction(t *testing.T, db *sql.DB, tr *TestTransaction) {
_, err := db.Exec(`
INSERT INTO pending_transactions (network_id, hash, timestamp, from_address, to_address,
symbol, gas_price, gas_limit, value, data, type, additional_data, multi_transaction_id
) VALUES (?, ?, ?, ?, ?, 'ETH', 0, 0, ?, '', 'test', '', ?)`,
tr.ChainID, tr.Hash, tr.Timestamp, tr.From, tr.To, tr.Value, tr.MultiTransactionID)
require.NoError(t, err)
}
func InsertTestMultiTransaction(t *testing.T, db *sql.DB, tr *TestTransaction) MultiTransactionIDType {
fromTokenType := tr.FromToken
if tr.FromToken == "" {
fromTokenType = testutils.EthSymbol
}
toTokenType := tr.ToToken
if tr.ToToken == "" {
toTokenType = testutils.EthSymbol
}
result, err := db.Exec(`
INSERT INTO multi_transactions (from_address, from_asset, from_amount, to_address, to_asset, type, timestamp
) VALUES (?, 'ETH', 0, ?, 'SNT', ?, ?)`,
tr.From, tr.To, tr.MultiTransactionType, tr.Timestamp)
) VALUES (?, ?, 0, ?, ?, ?, ?)`,
tr.From, fromTokenType, tr.To, toTokenType, tr.MultiTransactionType, tr.Timestamp)
require.NoError(t, err)
rowID, err := result.LastInsertId()
require.NoError(t, err)

View File

@ -20,6 +20,7 @@ import (
"github.com/status-im/status-go/services/wallet/async"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/services/wallet/bridge"
wallet_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/transactions"
)
@ -96,9 +97,9 @@ type PendingTransaction struct {
}
type TransactionIdentity struct {
ChainID uint64 `json:"chainId"`
Hash common.Hash `json:"hash"`
Address common.Address `json:"address"`
ChainID wallet_common.ChainID `json:"chainId"`
Hash common.Hash `json:"hash"`
Address common.Address `json:"address"`
}
const selectFromPending = `SELECT hash, timestamp, value, from_address, to_address, data,