472 lines
15 KiB
Go
472 lines
15 KiB
Go
package activity
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
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
|
|
PendingTransactionPT
|
|
)
|
|
|
|
type Entry struct {
|
|
payloadType PayloadType
|
|
transaction *transfer.TransactionIdentity
|
|
id transfer.MultiTransactionIDType
|
|
timestamp int64
|
|
activityType Type
|
|
activityStatus Status
|
|
tokenType TokenType
|
|
}
|
|
|
|
type jsonSerializationTemplate struct {
|
|
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{
|
|
PayloadType: e.payloadType,
|
|
Transaction: e.transaction,
|
|
ID: e.id,
|
|
Timestamp: e.timestamp,
|
|
ActivityType: e.activityType,
|
|
ActivityStatus: e.activityStatus,
|
|
TokenType: e.tokenType,
|
|
})
|
|
}
|
|
|
|
func (e *Entry) UnmarshalJSON(data []byte) error {
|
|
aux := jsonSerializationTemplate{}
|
|
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.payloadType = aux.PayloadType
|
|
e.transaction = aux.Transaction
|
|
e.id = aux.ID
|
|
e.timestamp = aux.Timestamp
|
|
e.activityType = aux.ActivityType
|
|
return nil
|
|
}
|
|
|
|
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{
|
|
payloadType: payloadType,
|
|
transaction: transaction,
|
|
id: 0,
|
|
timestamp: timestamp,
|
|
activityType: activityType,
|
|
activityStatus: activityStatus,
|
|
tokenType: AssetTT,
|
|
}
|
|
}
|
|
|
|
func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry {
|
|
return Entry{
|
|
payloadType: MultiTransactionPT,
|
|
id: id,
|
|
timestamp: timestamp,
|
|
activityType: activityType,
|
|
activityStatus: activityStatus,
|
|
tokenType: AssetTT,
|
|
}
|
|
}
|
|
|
|
func (e *Entry) PayloadType() PayloadType {
|
|
return e.payloadType
|
|
}
|
|
|
|
func multiTransactionTypeToActivityType(mtType transfer.MultiTransactionType) Type {
|
|
if mtType == transfer.MultiTransactionSend {
|
|
return SendAT
|
|
} else if mtType == transfer.MultiTransactionSwap {
|
|
return SwapAT
|
|
} else if mtType == transfer.MultiTransactionBridge {
|
|
return BridgeAT
|
|
}
|
|
panic("unknown multi transaction type")
|
|
}
|
|
|
|
func sliceContains[T constraints.Ordered](slice []T, item T) bool {
|
|
for _, a := range slice {
|
|
if a == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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 []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 {
|
|
mtTypes := make([]transfer.MultiTransactionType, 0, len(trTypes))
|
|
for _, t := range trTypes {
|
|
var mtType transfer.MultiTransactionType
|
|
if t == SendAT {
|
|
mtType = transfer.MultiTransactionSend
|
|
} else if t == SwapAT {
|
|
mtType = transfer.MultiTransactionSwap
|
|
} else if t == BridgeAT {
|
|
mtType = transfer.MultiTransactionBridge
|
|
} else {
|
|
continue
|
|
}
|
|
mtTypes = append(mtTypes, mtType)
|
|
}
|
|
return mtTypes
|
|
}
|
|
|
|
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,
|
|
? AS startTimestamp,
|
|
? AS endFilterDisabled,
|
|
? AS endTimestamp,
|
|
|
|
? AS filterActivityTypeAll,
|
|
? AS filterActivityTypeSend,
|
|
? AS filterActivityTypeReceive,
|
|
|
|
? AS filterAllAddresses,
|
|
? AS filterAllToAddresses,
|
|
? AS filterAllActivityStatus,
|
|
? AS includeAllTokenTypeAssets,
|
|
? AS statusIsPending,
|
|
|
|
? AS includeAllNetworks
|
|
),
|
|
filter_addresses(address) AS (
|
|
SELECT HEX(address) FROM keypairs_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
|
|
transfers.hash AS transfer_hash,
|
|
NULL AS pending_hash,
|
|
transfers.network_id AS network_id,
|
|
0 AS multi_tx_id,
|
|
transfers.timestamp AS timestamp,
|
|
NULL AS mt_type,
|
|
|
|
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 (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
|
|
|
|
SELECT
|
|
NULL AS transfer_hash,
|
|
pending_transactions.hash AS pending_hash,
|
|
pending_transactions.network_id AS network_id,
|
|
0 AS multi_tx_id,
|
|
pending_transactions.timestamp AS timestamp,
|
|
NULL AS mt_type,
|
|
|
|
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 (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 (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
|
|
|
|
SELECT
|
|
NULL AS transfer_hash,
|
|
NULL AS pending_hash,
|
|
NULL AS network_id,
|
|
multi_transactions.ROWID AS multi_tx_id,
|
|
multi_transactions.timestamp AS timestamp,
|
|
multi_transactions.type AS mt_type,
|
|
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)
|
|
)
|
|
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 (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 ?`
|
|
|
|
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)
|
|
}
|
|
|
|
startFilterDisabled := !(filter.Period.StartTimestamp > 0)
|
|
endFilterDisabled := !(filter.Period.EndTimestamp > 0)
|
|
filterActivityTypeAll := len(filter.Types) == 0
|
|
filterAllAddresses := len(addresses) == 0
|
|
filterAllToAddresses := len(filter.CounterpartyAddresses) == 0
|
|
includeAllStatuses := len(filter.Statuses) == 0
|
|
|
|
statusIsPending := false
|
|
if !includeAllStatuses {
|
|
statusIsPending = sliceContains(filter.Statuses, PendingAS)
|
|
}
|
|
|
|
involvedAddresses := noEntriesInTmpTableSQLValues
|
|
if !filterAllAddresses {
|
|
involvedAddresses = joinAddresses(addresses)
|
|
}
|
|
toAddresses := noEntriesInTmpTableSQLValues
|
|
if !filterAllToAddresses {
|
|
toAddresses = joinAddresses(filter.CounterpartyAddresses)
|
|
}
|
|
|
|
mtTypes := activityTypesToMultiTransactionTypes(filter.Types)
|
|
joinedMTTypes := joinItems(mtTypes, func(t transfer.MultiTransactionType) string {
|
|
return strconv.Itoa(int(t))
|
|
})
|
|
|
|
queryString := fmt.Sprintf(queryFormatString, involvedAddresses, toAddresses, assets, networks,
|
|
joinedMTTypes)
|
|
|
|
rows, err := db.Query(queryString,
|
|
startFilterDisabled, filter.Period.StartTimestamp, endFilterDisabled, filter.Period.EndTimestamp,
|
|
filterActivityTypeAll, sliceContains(filter.Types, SendAT), sliceContains(filter.Types, ReceiveAT),
|
|
filterAllAddresses, filterAllToAddresses, includeAllStatuses, includeAllTokenTypeAssets, statusIsPending,
|
|
includeAllNetworks,
|
|
limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var entries []Entry
|
|
for rows.Next() {
|
|
var transferHash, pendingHash []byte
|
|
var chainID, multiTxID sql.NullInt64
|
|
var timestamp int64
|
|
var dbMtType, dbTrType sql.NullByte
|
|
var toAddress, fromAddress eth.Address
|
|
err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, ×tamp, &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 {
|
|
// 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 {
|
|
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(dbMtType.Byte))
|
|
// TODO: aggregate status from all sub-transactions
|
|
activityStatus := FinalizedAS
|
|
entry = NewActivityEntryWithMultiTransaction(transfer.MultiTransactionIDType(multiTxID.Int64),
|
|
timestamp, activityType, activityStatus)
|
|
} else {
|
|
return nil, errors.New("invalid row data")
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return entries, nil
|
|
}
|