mirror of
https://github.com/status-im/status-go.git
synced 2025-01-10 06:36:32 +00:00
70341f85a5
Brings consistency in case when sender and receiver are both in the filter address list. This fixes the case of sender and receiver in addresses and filters out duplicate entries. Also - refactor tests to provide support for owners - adapt TestGetActivityEntriesWithSameTransactionForSenderAndReceiverInDB to the use of owner instead of from
750 lines
24 KiB
Go
750 lines
24 KiB
Go
package activity
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
|
|
// used for embedding the sql query in the binary
|
|
_ "embed"
|
|
|
|
eth "github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
|
|
"github.com/status-im/status-go/multiaccounts/accounts"
|
|
"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/thirdparty"
|
|
"github.com/status-im/status-go/services/wallet/transfer"
|
|
"github.com/status-im/status-go/transactions"
|
|
|
|
"golang.org/x/exp/constraints"
|
|
)
|
|
|
|
type PayloadType = int
|
|
|
|
// Beware: please update multiTransactionTypeToActivityType if changing this enum
|
|
const (
|
|
MultiTransactionPT PayloadType = iota + 1
|
|
SimpleTransactionPT
|
|
PendingTransactionPT
|
|
)
|
|
|
|
const keypairAccountsTable = "keypairs_accounts"
|
|
|
|
var (
|
|
ZeroAddress = eth.Address{}
|
|
)
|
|
|
|
type TransferType = int
|
|
|
|
const (
|
|
TransferTypeEth TransferType = iota + 1
|
|
TransferTypeErc20
|
|
TransferTypeErc721
|
|
TransferTypeErc1155
|
|
)
|
|
|
|
type Entry struct {
|
|
payloadType PayloadType
|
|
transaction *transfer.TransactionIdentity
|
|
id transfer.MultiTransactionIDType
|
|
timestamp int64
|
|
activityType Type
|
|
activityStatus Status
|
|
amountOut *hexutil.Big // Used for activityType SendAT, SwapAT, BridgeAT
|
|
amountIn *hexutil.Big // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT
|
|
tokenOut *Token // Used for activityType SendAT, SwapAT, BridgeAT
|
|
tokenIn *Token // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT
|
|
symbolOut *string
|
|
symbolIn *string
|
|
sender *eth.Address
|
|
recipient *eth.Address
|
|
chainIDOut *common.ChainID
|
|
chainIDIn *common.ChainID
|
|
transferType *TransferType
|
|
contractAddress *eth.Address
|
|
}
|
|
|
|
type EntryData struct {
|
|
PayloadType PayloadType `json:"payloadType"`
|
|
Transaction *transfer.TransactionIdentity `json:"transaction,omitempty"`
|
|
ID *transfer.MultiTransactionIDType `json:"id,omitempty"`
|
|
Timestamp *int64 `json:"timestamp,omitempty"`
|
|
ActivityType *Type `json:"activityType,omitempty"`
|
|
ActivityStatus *Status `json:"activityStatus,omitempty"`
|
|
AmountOut *hexutil.Big `json:"amountOut,omitempty"`
|
|
AmountIn *hexutil.Big `json:"amountIn,omitempty"`
|
|
TokenOut *Token `json:"tokenOut,omitempty"`
|
|
TokenIn *Token `json:"tokenIn,omitempty"`
|
|
SymbolOut *string `json:"symbolOut,omitempty"`
|
|
SymbolIn *string `json:"symbolIn,omitempty"`
|
|
Sender *eth.Address `json:"sender,omitempty"`
|
|
Recipient *eth.Address `json:"recipient,omitempty"`
|
|
ChainIDOut *common.ChainID `json:"chainIdOut,omitempty"`
|
|
ChainIDIn *common.ChainID `json:"chainIdIn,omitempty"`
|
|
TransferType *TransferType `json:"transferType,omitempty"`
|
|
ContractAddress *eth.Address `json:"contractAddress,omitempty"`
|
|
|
|
NftName *string `json:"nftName,omitempty"`
|
|
NftURL *string `json:"nftUrl,omitempty"`
|
|
}
|
|
|
|
func (e *Entry) MarshalJSON() ([]byte, error) {
|
|
data := EntryData{
|
|
Timestamp: &e.timestamp,
|
|
ActivityType: &e.activityType,
|
|
ActivityStatus: &e.activityStatus,
|
|
AmountOut: e.amountOut,
|
|
AmountIn: e.amountIn,
|
|
TokenOut: e.tokenOut,
|
|
TokenIn: e.tokenIn,
|
|
SymbolOut: e.symbolOut,
|
|
SymbolIn: e.symbolIn,
|
|
Sender: e.sender,
|
|
Recipient: e.recipient,
|
|
ChainIDOut: e.chainIDOut,
|
|
ChainIDIn: e.chainIDIn,
|
|
TransferType: e.transferType,
|
|
ContractAddress: e.contractAddress,
|
|
}
|
|
|
|
if e.payloadType == MultiTransactionPT {
|
|
data.ID = common.NewAndSet(e.id)
|
|
} else {
|
|
data.Transaction = e.transaction
|
|
}
|
|
|
|
data.PayloadType = e.payloadType
|
|
|
|
return json.Marshal(data)
|
|
}
|
|
|
|
func (e *Entry) UnmarshalJSON(data []byte) error {
|
|
aux := EntryData{}
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
e.payloadType = aux.PayloadType
|
|
e.transaction = aux.Transaction
|
|
if aux.ID != nil {
|
|
e.id = *aux.ID
|
|
}
|
|
if aux.Timestamp != nil {
|
|
e.timestamp = *aux.Timestamp
|
|
}
|
|
if aux.ActivityType != nil {
|
|
e.activityType = *aux.ActivityType
|
|
}
|
|
if aux.ActivityStatus != nil {
|
|
e.activityStatus = *aux.ActivityStatus
|
|
}
|
|
e.amountOut = aux.AmountOut
|
|
e.amountIn = aux.AmountIn
|
|
e.tokenOut = aux.TokenOut
|
|
e.tokenIn = aux.TokenIn
|
|
e.symbolOut = aux.SymbolOut
|
|
e.symbolIn = aux.SymbolIn
|
|
e.sender = aux.Sender
|
|
e.recipient = aux.Recipient
|
|
e.chainIDOut = aux.ChainIDOut
|
|
e.chainIDIn = aux.ChainIDIn
|
|
e.transferType = aux.TransferType
|
|
return nil
|
|
}
|
|
|
|
func newActivityEntryWithPendingTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry {
|
|
return newActivityEntryWithTransaction(true, transaction, timestamp, activityType, activityStatus)
|
|
}
|
|
|
|
func newActivityEntryWithSimpleTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry {
|
|
return newActivityEntryWithTransaction(false, transaction, timestamp, activityType, activityStatus)
|
|
}
|
|
|
|
func newActivityEntryWithTransaction(pending bool, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry {
|
|
payloadType := SimpleTransactionPT
|
|
if pending {
|
|
payloadType = PendingTransactionPT
|
|
}
|
|
|
|
return Entry{
|
|
payloadType: payloadType,
|
|
transaction: transaction,
|
|
id: 0,
|
|
timestamp: timestamp,
|
|
activityType: activityType,
|
|
activityStatus: activityStatus,
|
|
}
|
|
}
|
|
|
|
func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry {
|
|
return Entry{
|
|
payloadType: MultiTransactionPT,
|
|
id: id,
|
|
timestamp: timestamp,
|
|
activityType: activityType,
|
|
activityStatus: activityStatus,
|
|
}
|
|
}
|
|
|
|
func (e *Entry) PayloadType() PayloadType {
|
|
return e.payloadType
|
|
}
|
|
|
|
func (e *Entry) isNFT() bool {
|
|
tt := e.transferType
|
|
return tt != nil && (*tt == TransferTypeErc721 || *tt == TransferTypeErc1155) && ((e.tokenIn != nil && e.tokenIn.TokenID != nil) || (e.tokenOut != nil && e.tokenOut.TokenID != nil))
|
|
}
|
|
|
|
// TODO - #11952: use only one of (big.Int, bigint.BigInt and hexutil.Big)
|
|
func tokenIDToWalletBigInt(tokenID *hexutil.Big) *bigint.BigInt {
|
|
if tokenID == nil {
|
|
return nil
|
|
}
|
|
|
|
bi := new(big.Int).Set((*big.Int)(tokenID))
|
|
return &bigint.BigInt{Int: bi}
|
|
}
|
|
|
|
func (e *Entry) anyIdentity() *thirdparty.CollectibleUniqueID {
|
|
if e.tokenIn != nil {
|
|
return &thirdparty.CollectibleUniqueID{
|
|
ContractID: thirdparty.ContractID{
|
|
ChainID: e.tokenIn.ChainID,
|
|
Address: e.tokenIn.Address,
|
|
},
|
|
TokenID: tokenIDToWalletBigInt(e.tokenIn.TokenID),
|
|
}
|
|
} else if e.tokenOut != nil {
|
|
return &thirdparty.CollectibleUniqueID{
|
|
ContractID: thirdparty.ContractID{
|
|
ChainID: e.tokenOut.ChainID,
|
|
Address: e.tokenOut.Address,
|
|
},
|
|
TokenID: tokenIDToWalletBigInt(e.tokenOut.TokenID),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 sliceChecksCondition[T any](slice []T, condition func(*T) bool) bool {
|
|
for i := range slice {
|
|
if condition(&slice[i]) {
|
|
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)
|
|
|
|
noEntriesInTmpTableSQLValues = "(NULL)"
|
|
noEntriesInTwoColumnsTmpTableSQLValues = "(NULL, NULL)"
|
|
)
|
|
|
|
//go:embed filter.sql
|
|
var queryFormatString string
|
|
|
|
type FilterDependencies struct {
|
|
db *sql.DB
|
|
accountsDb *accounts.Database
|
|
// use token.TokenType, token.ChainID and token.Address to find the available symbol
|
|
tokenSymbol func(token Token) string
|
|
// use the chainID and symbol to look up token.TokenType and token.Address. Return nil if not found
|
|
tokenFromSymbol func(chainID *common.ChainID, symbol string) *Token
|
|
}
|
|
|
|
// getActivityEntries queries 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.
|
|
//
|
|
// TODO: optimization: consider implementing nullable []byte instead of using strings for addresses or insert binary (X'...' syntax) directly into the query
|
|
func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses []eth.Address, chainIDs []common.ChainID, filter Filter, offset int, limit int) ([]Entry, error) {
|
|
includeAllTokenTypeAssets := len(filter.Assets) == 0 && !filter.FilterOutAssets
|
|
|
|
// Used for symbol bearing tables multi_transactions and pending_transactions
|
|
assetsTokenCodes := noEntriesInTmpTableSQLValues
|
|
// Used for identity bearing tables transfers
|
|
assetsERC20 := noEntriesInTwoColumnsTmpTableSQLValues
|
|
if !includeAllTokenTypeAssets && !filter.FilterOutAssets {
|
|
symbolsSet := make(map[string]struct{})
|
|
var symbols []string
|
|
for _, item := range filter.Assets {
|
|
symbol := deps.tokenSymbol(item)
|
|
if _, ok := symbolsSet[symbol]; !ok {
|
|
symbols = append(symbols, symbol)
|
|
symbolsSet[symbol] = struct{}{}
|
|
}
|
|
}
|
|
assetsTokenCodes = joinItems(symbols, func(s string) string {
|
|
return fmt.Sprintf("'%s'", s)
|
|
})
|
|
|
|
if sliceChecksCondition(filter.Assets, func(item *Token) bool { return item.TokenType == Erc20 }) {
|
|
assetsERC20 = joinItems(filter.Assets, func(item Token) string {
|
|
if item.TokenType == Erc20 {
|
|
// SQL HEX() (Blob->Hex) conversion returns uppercase digits with no 0x prefix
|
|
return fmt.Sprintf("%d, '%s'", item.ChainID, strings.ToUpper(item.Address.Hex()[2:]))
|
|
}
|
|
return ""
|
|
})
|
|
}
|
|
}
|
|
|
|
// construct chain IDs
|
|
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
|
|
|
|
filterStatusPending := false
|
|
filterStatusCompleted := false
|
|
filterStatusFailed := false
|
|
filterStatusFinalized := false
|
|
if !includeAllStatuses {
|
|
filterStatusPending = sliceContains(filter.Statuses, PendingAS)
|
|
filterStatusCompleted = sliceContains(filter.Statuses, CompleteAS)
|
|
filterStatusFailed = sliceContains(filter.Statuses, FailedAS)
|
|
filterStatusFinalized = sliceContains(filter.Statuses, FinalizedAS)
|
|
}
|
|
|
|
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))
|
|
})
|
|
|
|
// Since the filter query needs addresses which are in a different database, we need to update the
|
|
// keypairs_accounts table in the current database with the latest addresses from the accounts database
|
|
err := updateKeypairsAccountsTable(deps.accountsDb, deps.db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
queryString := fmt.Sprintf(queryFormatString, keypairAccountsTable, involvedAddresses, toAddresses, assetsTokenCodes, assetsERC20, networks,
|
|
joinedMTTypes)
|
|
|
|
rows, err := deps.db.QueryContext(ctx, queryString,
|
|
startFilterDisabled, filter.Period.StartTimestamp, endFilterDisabled, filter.Period.EndTimestamp,
|
|
filterActivityTypeAll, sliceContains(filter.Types, SendAT), sliceContains(filter.Types, ReceiveAT),
|
|
sliceContains(filter.Types, ContractDeploymentAT), sliceContains(filter.Types, MintAT),
|
|
transfer.MultiTransactionSend,
|
|
fromTrType, toTrType,
|
|
filterAllAddresses, filterAllToAddresses,
|
|
includeAllStatuses, filterStatusCompleted, filterStatusFailed, filterStatusFinalized, filterStatusPending,
|
|
FailedAS, CompleteAS, PendingAS,
|
|
includeAllTokenTypeAssets,
|
|
includeAllNetworks,
|
|
transactions.Pending,
|
|
limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var entries []Entry
|
|
for rows.Next() {
|
|
var transferHash, pendingHash []byte
|
|
var chainID, outChainIDDB, inChainIDDB, multiTxID, aggregatedCount sql.NullInt64
|
|
var timestamp int64
|
|
var dbMtType, dbTrType sql.NullByte
|
|
var toAddress, fromAddress eth.Address
|
|
var toAddressDB, ownerAddressDB, contractAddressDB, dbTokenID sql.RawBytes
|
|
var tokenAddress, contractAddress *eth.Address
|
|
var aggregatedStatus int
|
|
var dbTrAmount sql.NullString
|
|
var dbMtFromAmount, dbMtToAmount, contractType sql.NullString
|
|
var tokenCode, fromTokenCode, toTokenCode sql.NullString
|
|
var transferType *TransferType
|
|
err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, ×tamp, &dbMtType, &dbTrType, &fromAddress,
|
|
&toAddressDB, &ownerAddressDB, &dbTrAmount, &dbMtFromAmount, &dbMtToAmount, &aggregatedStatus, &aggregatedCount,
|
|
&tokenAddress, &dbTokenID, &tokenCode, &fromTokenCode, &toTokenCode, &outChainIDDB, &inChainIDDB, &contractType, &contractAddressDB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(toAddressDB) > 0 {
|
|
toAddress = eth.BytesToAddress(toAddressDB)
|
|
}
|
|
|
|
if contractType.Valid {
|
|
transferType = contractTypeFromDBType(contractType.String)
|
|
}
|
|
if len(contractAddressDB) > 0 {
|
|
contractAddress = new(eth.Address)
|
|
*contractAddress = eth.BytesToAddress(contractAddressDB)
|
|
}
|
|
|
|
getActivityType := func(trType sql.NullByte) (activityType Type, filteredAddress eth.Address) {
|
|
if trType.Valid {
|
|
if trType.Byte == fromTrType {
|
|
if toAddress == ZeroAddress && transferType != nil && *transferType == TransferTypeEth && contractAddress != nil && *contractAddress != ZeroAddress {
|
|
return ContractDeploymentAT, fromAddress
|
|
}
|
|
return SendAT, fromAddress
|
|
} else if trType.Byte == toTrType {
|
|
if fromAddress == ZeroAddress && transferType != nil && *transferType == TransferTypeErc721 {
|
|
return MintAT, toAddress
|
|
}
|
|
return ReceiveAT, toAddress
|
|
}
|
|
}
|
|
log.Warn(fmt.Sprintf("unexpected activity type. Missing from [%s] or to [%s] in addresses?", fromAddress, toAddress))
|
|
return ReceiveAT, toAddress
|
|
}
|
|
|
|
// Can be mapped directly because the values are injected into the query
|
|
activityStatus := Status(aggregatedStatus)
|
|
var outChainID, inChainID *common.ChainID
|
|
var entry Entry
|
|
var tokenID *hexutil.Big
|
|
if len(dbTokenID) > 0 {
|
|
tokenID = (*hexutil.Big)(new(big.Int).SetBytes(dbTokenID))
|
|
}
|
|
|
|
if transferHash != nil && chainID.Valid {
|
|
// Process `transfers` row
|
|
|
|
// Extract activity type: SendAT/ReceiveAT
|
|
activityType, _ := getActivityType(dbTrType)
|
|
|
|
ownerAddress := eth.BytesToAddress(ownerAddressDB)
|
|
inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount)
|
|
|
|
// Extract tokens and chains
|
|
var involvedToken *Token
|
|
if tokenAddress != nil && *tokenAddress != ZeroAddress {
|
|
involvedToken = &Token{TokenType: Erc20, ChainID: common.ChainID(chainID.Int64), TokenID: tokenID, Address: *tokenAddress}
|
|
} else {
|
|
involvedToken = &Token{TokenType: Native, ChainID: common.ChainID(chainID.Int64), TokenID: tokenID}
|
|
}
|
|
|
|
entry = newActivityEntryWithSimpleTransaction(
|
|
&transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64),
|
|
Hash: eth.BytesToHash(transferHash),
|
|
Address: ownerAddress,
|
|
},
|
|
timestamp, activityType, activityStatus,
|
|
)
|
|
|
|
// Extract tokens
|
|
if activityType == SendAT || activityType == ContractDeploymentAT {
|
|
entry.tokenOut = involvedToken
|
|
outChainID = new(common.ChainID)
|
|
*outChainID = common.ChainID(chainID.Int64)
|
|
} else {
|
|
entry.tokenIn = involvedToken
|
|
inChainID = new(common.ChainID)
|
|
*inChainID = common.ChainID(chainID.Int64)
|
|
}
|
|
|
|
entry.symbolOut, entry.symbolIn = lookupAndFillInTokens(deps, entry.tokenOut, entry.tokenIn)
|
|
|
|
// Complete the data
|
|
entry.amountOut = outAmount
|
|
entry.amountIn = inAmount
|
|
} else if pendingHash != nil && chainID.Valid {
|
|
// Process `pending_transactions` row
|
|
|
|
// Extract activity type: SendAT/ReceiveAT
|
|
activityType, _ := getActivityType(dbTrType)
|
|
|
|
inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount)
|
|
|
|
outChainID = new(common.ChainID)
|
|
*outChainID = common.ChainID(chainID.Int64)
|
|
|
|
entry = newActivityEntryWithPendingTransaction(
|
|
&transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64),
|
|
Hash: eth.BytesToHash(pendingHash),
|
|
},
|
|
timestamp, activityType, activityStatus,
|
|
)
|
|
|
|
// Extract tokens
|
|
if tokenCode.Valid {
|
|
cID := common.ChainID(chainID.Int64)
|
|
entry.tokenOut = deps.tokenFromSymbol(&cID, tokenCode.String)
|
|
}
|
|
entry.symbolOut, entry.symbolIn = lookupAndFillInTokens(deps, entry.tokenOut, nil)
|
|
|
|
// Complete the data
|
|
entry.amountOut = outAmount
|
|
entry.amountIn = inAmount
|
|
|
|
} else if multiTxID.Valid {
|
|
// Process `multi_transactions` row
|
|
|
|
mtInAmount, mtOutAmount := getMtInAndOutAmounts(dbMtFromAmount, dbMtToAmount)
|
|
|
|
// Extract activity type: SendAT/SwapAT/BridgeAT
|
|
activityType := multiTransactionTypeToActivityType(transfer.MultiTransactionType(dbMtType.Byte))
|
|
|
|
if outChainIDDB.Valid && outChainIDDB.Int64 != 0 {
|
|
outChainID = new(common.ChainID)
|
|
*outChainID = common.ChainID(outChainIDDB.Int64)
|
|
}
|
|
if inChainIDDB.Valid && inChainIDDB.Int64 != 0 {
|
|
inChainID = new(common.ChainID)
|
|
*inChainID = common.ChainID(inChainIDDB.Int64)
|
|
}
|
|
|
|
entry = NewActivityEntryWithMultiTransaction(transfer.MultiTransactionIDType(multiTxID.Int64),
|
|
timestamp, activityType, activityStatus)
|
|
|
|
// Extract tokens
|
|
if fromTokenCode.Valid {
|
|
entry.tokenOut = deps.tokenFromSymbol(outChainID, fromTokenCode.String)
|
|
entry.symbolOut = common.NewAndSet(fromTokenCode.String)
|
|
}
|
|
if toTokenCode.Valid {
|
|
entry.tokenIn = deps.tokenFromSymbol(inChainID, toTokenCode.String)
|
|
entry.symbolIn = common.NewAndSet(toTokenCode.String)
|
|
}
|
|
|
|
// Complete the data
|
|
entry.amountOut = mtOutAmount
|
|
entry.amountIn = mtInAmount
|
|
} else {
|
|
return nil, errors.New("invalid row data")
|
|
}
|
|
|
|
// Complete common data
|
|
entry.sender = &fromAddress
|
|
entry.recipient = &toAddress
|
|
entry.sender = &fromAddress
|
|
entry.recipient = &toAddress
|
|
entry.chainIDOut = outChainID
|
|
entry.chainIDIn = inChainID
|
|
entry.transferType = transferType
|
|
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
func getTrInAndOutAmounts(activityType Type, trAmount sql.NullString) (inAmount *hexutil.Big, outAmount *hexutil.Big) {
|
|
if trAmount.Valid {
|
|
amount, ok := new(big.Int).SetString(trAmount.String, 16)
|
|
if ok {
|
|
switch activityType {
|
|
case ContractDeploymentAT:
|
|
fallthrough
|
|
case SendAT:
|
|
inAmount = (*hexutil.Big)(big.NewInt(0))
|
|
outAmount = (*hexutil.Big)(amount)
|
|
return
|
|
case MintAT:
|
|
fallthrough
|
|
case ReceiveAT:
|
|
inAmount = (*hexutil.Big)(amount)
|
|
outAmount = (*hexutil.Big)(big.NewInt(0))
|
|
return
|
|
default:
|
|
log.Warn(fmt.Sprintf("unexpected activity type %d", activityType))
|
|
}
|
|
} else {
|
|
log.Warn(fmt.Sprintf("could not parse amount %s", trAmount.String))
|
|
}
|
|
} else {
|
|
log.Warn(fmt.Sprintf("invalid transaction amount for type %d", activityType))
|
|
}
|
|
inAmount = (*hexutil.Big)(big.NewInt(0))
|
|
outAmount = (*hexutil.Big)(big.NewInt(0))
|
|
return
|
|
}
|
|
|
|
func getMtInAndOutAmounts(dbFromAmount sql.NullString, dbToAmount sql.NullString) (inAmount *hexutil.Big, outAmount *hexutil.Big) {
|
|
if dbFromAmount.Valid && dbToAmount.Valid {
|
|
fromHexStr := dbFromAmount.String
|
|
toHexStr := dbToAmount.String
|
|
if len(fromHexStr) > 2 && len(toHexStr) > 2 {
|
|
fromAmount, frOk := new(big.Int).SetString(dbFromAmount.String[2:], 16)
|
|
toAmount, toOk := new(big.Int).SetString(dbToAmount.String[2:], 16)
|
|
if frOk && toOk {
|
|
inAmount = (*hexutil.Big)(toAmount)
|
|
outAmount = (*hexutil.Big)(fromAmount)
|
|
return
|
|
}
|
|
}
|
|
log.Warn(fmt.Sprintf("could not parse amounts %s %s", fromHexStr, toHexStr))
|
|
} else {
|
|
log.Warn("invalid transaction amounts")
|
|
}
|
|
inAmount = (*hexutil.Big)(big.NewInt(0))
|
|
outAmount = (*hexutil.Big)(big.NewInt(0))
|
|
return
|
|
}
|
|
|
|
func contractTypeFromDBType(dbType string) (transferType *TransferType) {
|
|
transferType = new(TransferType)
|
|
switch common.Type(dbType) {
|
|
case common.EthTransfer:
|
|
*transferType = TransferTypeEth
|
|
case common.Erc20Transfer:
|
|
*transferType = TransferTypeErc20
|
|
case common.Erc721Transfer:
|
|
*transferType = TransferTypeErc721
|
|
default:
|
|
return nil
|
|
}
|
|
return transferType
|
|
}
|
|
|
|
func updateKeypairsAccountsTable(accountsDb *accounts.Database, db *sql.DB) error {
|
|
_, err := db.Exec(fmt.Sprintf("CREATE TEMP TABLE IF NOT EXISTS %s (address VARCHAR PRIMARY KEY)",
|
|
keypairAccountsTable))
|
|
if err != nil {
|
|
log.Error("failed to create 'keypairs_accounts' table", "err", err)
|
|
return err
|
|
}
|
|
|
|
// TODO: remove dependency on accounts table by removing"all accounts filter" optimization; see #11980
|
|
if accountsDb == nil {
|
|
return nil
|
|
}
|
|
addresses, err := accountsDb.GetWalletAddresses()
|
|
if err != nil {
|
|
log.Error("failed to get wallet addresses", "err", err)
|
|
return err
|
|
}
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err == nil {
|
|
err = tx.Commit()
|
|
return
|
|
}
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
for _, address := range addresses {
|
|
_, err = tx.Exec(fmt.Sprintf("INSERT OR IGNORE INTO %s (address) VALUES (?)", keypairAccountsTable), address)
|
|
if err != nil {
|
|
log.Error("failed to insert wallet addresses", "err", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// lookupAndFillInTokens ignores NFTs
|
|
func lookupAndFillInTokens(deps FilterDependencies, tokenOut *Token, tokenIn *Token) (symbolOut *string, symbolIn *string) {
|
|
if tokenOut != nil && tokenOut.TokenID == nil {
|
|
symbol := deps.tokenSymbol(*tokenOut)
|
|
if len(symbol) > 0 {
|
|
symbolOut = common.NewAndSet(symbol)
|
|
}
|
|
}
|
|
if tokenIn != nil && tokenIn.TokenID == nil {
|
|
symbol := deps.tokenSymbol(*tokenIn)
|
|
if len(symbol) > 0 {
|
|
symbolIn = common.NewAndSet(symbol)
|
|
}
|
|
}
|
|
return symbolOut, symbolIn
|
|
}
|