github.com/status-im/status-go@v1.1.0/services/wallet/activity/activity.go (about)

     1  package activity
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"math/big"
    11  	"strconv"
    12  	"strings"
    13  
    14  	// used for embedding the sql query in the binary
    15  	_ "embed"
    16  
    17  	eth "github.com/ethereum/go-ethereum/common"
    18  	"github.com/ethereum/go-ethereum/common/hexutil"
    19  	"github.com/ethereum/go-ethereum/log"
    20  
    21  	"github.com/status-im/status-go/services/wallet/bigint"
    22  	"github.com/status-im/status-go/services/wallet/common"
    23  	"github.com/status-im/status-go/services/wallet/thirdparty"
    24  	"github.com/status-im/status-go/services/wallet/transfer"
    25  	"github.com/status-im/status-go/transactions"
    26  
    27  	"golang.org/x/exp/constraints"
    28  )
    29  
    30  type PayloadType = int
    31  
    32  // Beware: please update multiTransactionTypeToActivityType if changing this enum
    33  const (
    34  	MultiTransactionPT PayloadType = iota + 1
    35  	SimpleTransactionPT
    36  	PendingTransactionPT
    37  )
    38  
    39  var (
    40  	ZeroAddress = eth.Address{}
    41  )
    42  
    43  type TransferType = int
    44  
    45  const (
    46  	TransferTypeEth TransferType = iota + 1
    47  	TransferTypeErc20
    48  	TransferTypeErc721
    49  	TransferTypeErc1155
    50  )
    51  
    52  type Entry struct {
    53  	payloadType     PayloadType
    54  	transaction     *transfer.TransactionIdentity
    55  	id              common.MultiTransactionIDType
    56  	timestamp       int64
    57  	activityType    Type
    58  	activityStatus  Status
    59  	amountOut       *hexutil.Big // Used for activityType SendAT, SwapAT, BridgeAT
    60  	amountIn        *hexutil.Big // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT, ApproveAT
    61  	tokenOut        *Token       // Used for activityType SendAT, SwapAT, BridgeAT
    62  	tokenIn         *Token       // Used for activityType ReceiveAT, BuyAT, SwapAT, BridgeAT, ApproveAT
    63  	symbolOut       *string
    64  	symbolIn        *string
    65  	sender          *eth.Address
    66  	recipient       *eth.Address
    67  	chainIDOut      *common.ChainID
    68  	chainIDIn       *common.ChainID
    69  	transferType    *TransferType
    70  	contractAddress *eth.Address
    71  	communityID     *string
    72  
    73  	isNew bool // isNew is used to indicate if the entry is newer than session start (changed state also)
    74  }
    75  
    76  // Only used for JSON marshalling
    77  type EntryData struct {
    78  	PayloadType     PayloadType                    `json:"payloadType"`
    79  	Transaction     *transfer.TransactionIdentity  `json:"transaction,omitempty"`
    80  	ID              *common.MultiTransactionIDType `json:"id,omitempty"`
    81  	Timestamp       *int64                         `json:"timestamp,omitempty"`
    82  	ActivityType    *Type                          `json:"activityType,omitempty"`
    83  	ActivityStatus  *Status                        `json:"activityStatus,omitempty"`
    84  	AmountOut       *hexutil.Big                   `json:"amountOut,omitempty"`
    85  	AmountIn        *hexutil.Big                   `json:"amountIn,omitempty"`
    86  	TokenOut        *Token                         `json:"tokenOut,omitempty"`
    87  	TokenIn         *Token                         `json:"tokenIn,omitempty"`
    88  	SymbolOut       *string                        `json:"symbolOut,omitempty"`
    89  	SymbolIn        *string                        `json:"symbolIn,omitempty"`
    90  	Sender          *eth.Address                   `json:"sender,omitempty"`
    91  	Recipient       *eth.Address                   `json:"recipient,omitempty"`
    92  	ChainIDOut      *common.ChainID                `json:"chainIdOut,omitempty"`
    93  	ChainIDIn       *common.ChainID                `json:"chainIdIn,omitempty"`
    94  	TransferType    *TransferType                  `json:"transferType,omitempty"`
    95  	ContractAddress *eth.Address                   `json:"contractAddress,omitempty"`
    96  	CommunityID     *string                        `json:"communityId,omitempty"`
    97  
    98  	IsNew *bool `json:"isNew,omitempty"`
    99  
   100  	NftName *string `json:"nftName,omitempty"`
   101  	NftURL  *string `json:"nftUrl,omitempty"`
   102  }
   103  
   104  func (e *Entry) MarshalJSON() ([]byte, error) {
   105  	data := EntryData{
   106  		Timestamp:       &e.timestamp,
   107  		ActivityType:    &e.activityType,
   108  		ActivityStatus:  &e.activityStatus,
   109  		AmountOut:       e.amountOut,
   110  		AmountIn:        e.amountIn,
   111  		TokenOut:        e.tokenOut,
   112  		TokenIn:         e.tokenIn,
   113  		SymbolOut:       e.symbolOut,
   114  		SymbolIn:        e.symbolIn,
   115  		Sender:          e.sender,
   116  		Recipient:       e.recipient,
   117  		ChainIDOut:      e.chainIDOut,
   118  		ChainIDIn:       e.chainIDIn,
   119  		TransferType:    e.transferType,
   120  		ContractAddress: e.contractAddress,
   121  		CommunityID:     e.communityID,
   122  	}
   123  
   124  	if e.payloadType == MultiTransactionPT {
   125  		data.ID = common.NewAndSet(e.id)
   126  	} else {
   127  		data.Transaction = e.transaction
   128  	}
   129  
   130  	data.PayloadType = e.payloadType
   131  	if e.isNew {
   132  		data.IsNew = &e.isNew
   133  	}
   134  
   135  	return json.Marshal(data)
   136  }
   137  
   138  func (e *Entry) UnmarshalJSON(data []byte) error {
   139  	aux := EntryData{}
   140  	if err := json.Unmarshal(data, &aux); err != nil {
   141  		return err
   142  	}
   143  	e.payloadType = aux.PayloadType
   144  	e.transaction = aux.Transaction
   145  	if aux.ID != nil {
   146  		e.id = *aux.ID
   147  	}
   148  	if aux.Timestamp != nil {
   149  		e.timestamp = *aux.Timestamp
   150  	}
   151  	if aux.ActivityType != nil {
   152  		e.activityType = *aux.ActivityType
   153  	}
   154  	if aux.ActivityStatus != nil {
   155  		e.activityStatus = *aux.ActivityStatus
   156  	}
   157  	e.amountOut = aux.AmountOut
   158  	e.amountIn = aux.AmountIn
   159  	e.tokenOut = aux.TokenOut
   160  	e.tokenIn = aux.TokenIn
   161  	e.symbolOut = aux.SymbolOut
   162  	e.symbolIn = aux.SymbolIn
   163  	e.sender = aux.Sender
   164  	e.recipient = aux.Recipient
   165  	e.chainIDOut = aux.ChainIDOut
   166  	e.chainIDIn = aux.ChainIDIn
   167  	e.transferType = aux.TransferType
   168  	e.communityID = aux.CommunityID
   169  
   170  	e.isNew = aux.IsNew != nil && *aux.IsNew
   171  
   172  	return nil
   173  }
   174  
   175  func newActivityEntryWithPendingTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry {
   176  	return newActivityEntryWithTransaction(true, transaction, timestamp, activityType, activityStatus)
   177  }
   178  
   179  func newActivityEntryWithSimpleTransaction(transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry {
   180  	return newActivityEntryWithTransaction(false, transaction, timestamp, activityType, activityStatus)
   181  }
   182  
   183  func newActivityEntryWithTransaction(pending bool, transaction *transfer.TransactionIdentity, timestamp int64, activityType Type, activityStatus Status) Entry {
   184  	payloadType := SimpleTransactionPT
   185  	if pending {
   186  		payloadType = PendingTransactionPT
   187  	}
   188  
   189  	return Entry{
   190  		payloadType:    payloadType,
   191  		transaction:    transaction,
   192  		id:             0,
   193  		timestamp:      timestamp,
   194  		activityType:   activityType,
   195  		activityStatus: activityStatus,
   196  	}
   197  }
   198  
   199  func NewActivityEntryWithMultiTransaction(id common.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry {
   200  	return Entry{
   201  		payloadType:    MultiTransactionPT,
   202  		id:             id,
   203  		timestamp:      timestamp,
   204  		activityType:   activityType,
   205  		activityStatus: activityStatus,
   206  	}
   207  }
   208  
   209  func (e *Entry) PayloadType() PayloadType {
   210  	return e.payloadType
   211  }
   212  
   213  func (e *Entry) isNFT() bool {
   214  	tt := e.transferType
   215  	return tt != nil && (*tt == TransferTypeErc721 || *tt == TransferTypeErc1155) && ((e.tokenIn != nil && e.tokenIn.TokenID != nil) || (e.tokenOut != nil && e.tokenOut.TokenID != nil))
   216  }
   217  
   218  func tokenIDToWalletBigInt(tokenID *hexutil.Big) *bigint.BigInt {
   219  	if tokenID == nil {
   220  		return nil
   221  	}
   222  
   223  	bi := new(big.Int).Set((*big.Int)(tokenID))
   224  	return &bigint.BigInt{Int: bi}
   225  }
   226  
   227  func (e *Entry) anyIdentity() *thirdparty.CollectibleUniqueID {
   228  	if e.tokenIn != nil {
   229  		return &thirdparty.CollectibleUniqueID{
   230  			ContractID: thirdparty.ContractID{
   231  				ChainID: e.tokenIn.ChainID,
   232  				Address: e.tokenIn.Address,
   233  			},
   234  			TokenID: tokenIDToWalletBigInt(e.tokenIn.TokenID),
   235  		}
   236  	} else if e.tokenOut != nil {
   237  		return &thirdparty.CollectibleUniqueID{
   238  			ContractID: thirdparty.ContractID{
   239  				ChainID: e.tokenOut.ChainID,
   240  				Address: e.tokenOut.Address,
   241  			},
   242  			TokenID: tokenIDToWalletBigInt(e.tokenOut.TokenID),
   243  		}
   244  	}
   245  	return nil
   246  }
   247  
   248  func (e *Entry) getIdentity() EntryIdentity {
   249  	return EntryIdentity{
   250  		payloadType: e.payloadType,
   251  		id:          e.id,
   252  		transaction: e.transaction,
   253  	}
   254  }
   255  
   256  func multiTransactionTypeToActivityType(mtType transfer.MultiTransactionType) Type {
   257  	if mtType == transfer.MultiTransactionSend {
   258  		return SendAT
   259  	} else if mtType == transfer.MultiTransactionSwap {
   260  		return SwapAT
   261  	} else if mtType == transfer.MultiTransactionBridge {
   262  		return BridgeAT
   263  	} else if mtType == transfer.MultiTransactionApprove {
   264  		return ApproveAT
   265  	}
   266  	panic("unknown multi transaction type")
   267  }
   268  
   269  func sliceContains[T constraints.Ordered](slice []T, item T) bool {
   270  	for _, a := range slice {
   271  		if a == item {
   272  			return true
   273  		}
   274  	}
   275  	return false
   276  }
   277  
   278  func sliceChecksCondition[T any](slice []T, condition func(*T) bool) bool {
   279  	for i := range slice {
   280  		if condition(&slice[i]) {
   281  			return true
   282  		}
   283  	}
   284  	return false
   285  }
   286  
   287  func joinItems[T interface{}](items []T, itemConversion func(T) string) string {
   288  	if len(items) == 0 {
   289  		return ""
   290  	}
   291  	var sb strings.Builder
   292  	if itemConversion == nil {
   293  		itemConversion = func(item T) string {
   294  			return fmt.Sprintf("%v", item)
   295  		}
   296  	}
   297  	for i, item := range items {
   298  		if i == 0 {
   299  			sb.WriteString("(")
   300  		} else {
   301  			sb.WriteString("),(")
   302  		}
   303  		sb.WriteString(itemConversion(item))
   304  	}
   305  	sb.WriteString(")")
   306  
   307  	return sb.String()
   308  }
   309  
   310  func joinAddresses(addresses []eth.Address) string {
   311  	return joinItems(addresses, func(a eth.Address) string {
   312  		return fmt.Sprintf("X'%s'", hex.EncodeToString(a[:]))
   313  	})
   314  }
   315  
   316  func activityTypesToMultiTransactionTypes(trTypes []Type) []transfer.MultiTransactionType {
   317  	mtTypes := make([]transfer.MultiTransactionType, 0, len(trTypes))
   318  	for _, t := range trTypes {
   319  		var mtType transfer.MultiTransactionType
   320  		if t == SendAT {
   321  			mtType = transfer.MultiTransactionSend
   322  		} else if t == SwapAT {
   323  			mtType = transfer.MultiTransactionSwap
   324  		} else if t == BridgeAT {
   325  			mtType = transfer.MultiTransactionBridge
   326  		} else if t == ApproveAT {
   327  			mtType = transfer.MultiTransactionApprove
   328  		} else {
   329  			continue
   330  		}
   331  		mtTypes = append(mtTypes, mtType)
   332  	}
   333  	return mtTypes
   334  }
   335  
   336  const (
   337  	fromTrType = byte(1)
   338  	toTrType   = byte(2)
   339  
   340  	noEntriesInTmpTableSQLValues             = "(NULL)"
   341  	noEntriesInTwoColumnsTmpTableSQLValues   = "(NULL, NULL)"
   342  	noEntriesInThreeColumnsTmpTableSQLValues = "(NULL, NULL, NULL)"
   343  )
   344  
   345  //go:embed filter.sql
   346  var queryFormatString string
   347  var mintATQuery = "SELECT hash FROM input_data WHERE method IN ('mint', 'mintToken')"
   348  
   349  type FilterDependencies struct {
   350  	db *sql.DB
   351  	// use token.TokenType, token.ChainID and token.Address to find the available symbol
   352  	tokenSymbol func(token Token) string
   353  	// use the chainID and symbol to look up token.TokenType and token.Address. Return nil if not found
   354  	tokenFromSymbol func(chainID *common.ChainID, symbol string) *Token
   355  	// use to get current timestamp
   356  	currentTimestamp func() int64
   357  }
   358  
   359  // getActivityEntries queries the transfers, pending_transactions, and multi_transactions tables based on filter parameters and arguments
   360  // it returns metadata for all entries ordered by timestamp column
   361  //
   362  // addresses are mandatory and used to detect activity types SendAT and ReceiveAT for transfers entries
   363  //
   364  // allAddresses optimization indicates if the passed addresses include all the owners in the wallet DB
   365  //
   366  // Adding a no-limit option was never considered or required.
   367  func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses []eth.Address, allAddresses bool, chainIDs []common.ChainID, filter Filter, offset int, limit int) ([]Entry, error) {
   368  	if len(addresses) == 0 {
   369  		return nil, errors.New("no addresses provided")
   370  	}
   371  
   372  	includeAllTokenTypeAssets := len(filter.Assets) == 0 && !filter.FilterOutAssets
   373  
   374  	// Used for symbol bearing tables multi_transactions and pending_transactions
   375  	assetsTokenCodes := noEntriesInTmpTableSQLValues
   376  	// Used for identity bearing tables transfers
   377  	assetsERC20 := noEntriesInTwoColumnsTmpTableSQLValues
   378  	if !includeAllTokenTypeAssets && !filter.FilterOutAssets {
   379  		symbolsSet := make(map[string]struct{})
   380  		var symbols []string
   381  		for _, item := range filter.Assets {
   382  			symbol := deps.tokenSymbol(item)
   383  			if _, ok := symbolsSet[symbol]; !ok {
   384  				symbols = append(symbols, symbol)
   385  				symbolsSet[symbol] = struct{}{}
   386  			}
   387  		}
   388  		assetsTokenCodes = joinItems(symbols, func(s string) string {
   389  			return fmt.Sprintf("'%s'", s)
   390  		})
   391  
   392  		if sliceChecksCondition(filter.Assets, func(item *Token) bool { return item.TokenType == Erc20 }) {
   393  			assetsERC20 = joinItems(filter.Assets, func(item Token) string {
   394  				if item.TokenType == Erc20 {
   395  					return fmt.Sprintf("%d, X'%s'", item.ChainID, item.Address.Hex()[2:])
   396  				}
   397  				return ""
   398  			})
   399  		}
   400  	}
   401  
   402  	includeAllCollectibles := len(filter.Collectibles) == 0 && !filter.FilterOutCollectibles
   403  	assetsERC721 := noEntriesInThreeColumnsTmpTableSQLValues
   404  	if !includeAllCollectibles && !filter.FilterOutCollectibles {
   405  		assetsERC721 = joinItems(filter.Collectibles, func(item Token) string {
   406  			tokenID := item.TokenID.String()[2:]
   407  			address := item.Address.Hex()[2:]
   408  			// SQLite mandates that byte length is an even number which hexutil.EncodeBig doesn't guarantee
   409  			if len(tokenID)%2 == 1 {
   410  				tokenID = "0" + tokenID
   411  			}
   412  			return fmt.Sprintf("%d, X'%s', X'%s'", item.ChainID, tokenID, address)
   413  		})
   414  	}
   415  
   416  	// construct chain IDs
   417  	includeAllNetworks := len(chainIDs) == 0
   418  	networks := noEntriesInTmpTableSQLValues
   419  	if !includeAllNetworks {
   420  		networks = joinItems(chainIDs, nil)
   421  	}
   422  
   423  	layer2Chains := []uint64{common.OptimismMainnet, common.OptimismGoerli, common.ArbitrumMainnet, common.ArbitrumGoerli}
   424  	layer2Networks := joinItems(layer2Chains, func(chainID uint64) string {
   425  		return fmt.Sprintf("%d", chainID)
   426  	})
   427  
   428  	startFilterDisabled := !(filter.Period.StartTimestamp > 0)
   429  	endFilterDisabled := !(filter.Period.EndTimestamp > 0)
   430  	filterActivityTypeAll := len(filter.Types) == 0
   431  	filterAllToAddresses := len(filter.CounterpartyAddresses) == 0
   432  	includeAllStatuses := len(filter.Statuses) == 0
   433  
   434  	filterStatusPending := false
   435  	filterStatusCompleted := false
   436  	filterStatusFailed := false
   437  	filterStatusFinalized := false
   438  	if !includeAllStatuses {
   439  		filterStatusPending = sliceContains(filter.Statuses, PendingAS)
   440  		filterStatusCompleted = sliceContains(filter.Statuses, CompleteAS)
   441  		filterStatusFailed = sliceContains(filter.Statuses, FailedAS)
   442  		filterStatusFinalized = sliceContains(filter.Statuses, FinalizedAS)
   443  	}
   444  
   445  	involvedAddresses := joinAddresses(addresses)
   446  	toAddresses := noEntriesInTmpTableSQLValues
   447  	if !filterAllToAddresses {
   448  		toAddresses = joinAddresses(filter.CounterpartyAddresses)
   449  	}
   450  
   451  	mtTypes := activityTypesToMultiTransactionTypes(filter.Types)
   452  	joinedMTTypes := joinItems(mtTypes, func(t transfer.MultiTransactionType) string {
   453  		return strconv.Itoa(int(t))
   454  	})
   455  
   456  	inputDataMethods := make([]string, 0)
   457  
   458  	if includeAllStatuses || sliceContains(filter.Types, MintAT) || sliceContains(filter.Types, ReceiveAT) {
   459  		inputDataRows, err := deps.db.QueryContext(ctx, mintATQuery)
   460  
   461  		if err != nil {
   462  			return nil, err
   463  		}
   464  
   465  		for inputDataRows.Next() {
   466  			var inputData sql.NullString
   467  			err := inputDataRows.Scan(&inputData)
   468  			if err == nil && inputData.Valid {
   469  				inputDataMethods = append(inputDataMethods, inputData.String)
   470  			}
   471  		}
   472  	}
   473  
   474  	queryString := fmt.Sprintf(queryFormatString, involvedAddresses, toAddresses, assetsTokenCodes, assetsERC20, assetsERC721, networks,
   475  		layer2Networks, mintATQuery, joinedMTTypes)
   476  
   477  	// The duplicated temporary table UNION with CTE acts as an optimization
   478  	// As soon as we use filter_addresses CTE or filter_addresses_table temp table
   479  	// or switch them alternatively for JOIN or IN clauses the performance drops significantly
   480  	_, err := deps.db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS filter_addresses_table; CREATE TEMP TABLE filter_addresses_table (address VARCHAR PRIMARY KEY); INSERT INTO filter_addresses_table (address) VALUES %s;\n", involvedAddresses))
   481  	if err != nil {
   482  		return nil, err
   483  	}
   484  
   485  	rows, err := deps.db.QueryContext(ctx, queryString,
   486  		startFilterDisabled, filter.Period.StartTimestamp, endFilterDisabled, filter.Period.EndTimestamp,
   487  		filterActivityTypeAll, sliceContains(filter.Types, SendAT), sliceContains(filter.Types, ReceiveAT),
   488  		sliceContains(filter.Types, ContractDeploymentAT), sliceContains(filter.Types, MintAT),
   489  		transfer.MultiTransactionSend,
   490  		fromTrType, toTrType,
   491  		allAddresses, filterAllToAddresses,
   492  		includeAllStatuses, filterStatusCompleted, filterStatusFailed, filterStatusFinalized, filterStatusPending,
   493  		FailedAS, CompleteAS, FinalizedAS, PendingAS,
   494  		includeAllTokenTypeAssets,
   495  		includeAllCollectibles,
   496  		includeAllNetworks,
   497  		transactions.Pending,
   498  		deps.currentTimestamp(),
   499  		648000, // 7.5 days in seconds for layer 2 finalization. 0.5 day is buffer to not create false positive.
   500  		960,    // A block on layer 1 is every 12s, finalization require 64 blocks. A buffer of 16 blocks is added to not create false positives.
   501  		limit, offset)
   502  	if err != nil {
   503  		return nil, err
   504  	}
   505  	defer rows.Close()
   506  
   507  	var entries []Entry
   508  	for rows.Next() {
   509  		var transferHash, pendingHash []byte
   510  		var chainID, outChainIDDB, inChainIDDB, multiTxID, aggregatedCount sql.NullInt64
   511  		var timestamp int64
   512  		var dbMtType, dbTrType sql.NullByte
   513  		var toAddress, fromAddress eth.Address
   514  		var toAddressDB, ownerAddressDB, contractAddressDB, dbTokenID sql.RawBytes
   515  		var tokenAddress, contractAddress *eth.Address
   516  		var aggregatedStatus int
   517  		var dbTrAmount sql.NullString
   518  		dbPTrAmount := new(big.Int)
   519  		var dbMtFromAmount, dbMtToAmount, contractType sql.NullString
   520  		var tokenCode, fromTokenCode, toTokenCode sql.NullString
   521  		var methodHash, communityID sql.NullString
   522  		var transferType *TransferType
   523  		var communityMintEventDB sql.NullBool
   524  		var communityMintEvent bool
   525  		err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, &timestamp, &dbMtType, &dbTrType, &fromAddress,
   526  			&toAddressDB, &ownerAddressDB, &dbTrAmount, (*bigint.SQLBigIntBytes)(dbPTrAmount), &dbMtFromAmount, &dbMtToAmount, &aggregatedStatus, &aggregatedCount,
   527  			&tokenAddress, &dbTokenID, &tokenCode, &fromTokenCode, &toTokenCode, &outChainIDDB, &inChainIDDB, &contractType,
   528  			&contractAddressDB, &methodHash, &communityMintEventDB, &communityID)
   529  		if err != nil {
   530  			return nil, err
   531  		}
   532  
   533  		if len(toAddressDB) > 0 {
   534  			toAddress = eth.BytesToAddress(toAddressDB)
   535  		}
   536  
   537  		if contractType.Valid {
   538  			transferType = contractTypeFromDBType(contractType.String)
   539  		}
   540  
   541  		if communityMintEventDB.Valid {
   542  			communityMintEvent = communityMintEventDB.Bool
   543  		}
   544  
   545  		if len(contractAddressDB) > 0 {
   546  			contractAddress = new(eth.Address)
   547  			*contractAddress = eth.BytesToAddress(contractAddressDB)
   548  		}
   549  
   550  		getActivityType := func(trType sql.NullByte) (activityType Type, filteredAddress eth.Address) {
   551  			if trType.Valid {
   552  				if trType.Byte == fromTrType {
   553  					if toAddress == ZeroAddress && transferType != nil && *transferType == TransferTypeEth && contractAddress != nil && *contractAddress != ZeroAddress {
   554  						return ContractDeploymentAT, fromAddress
   555  					}
   556  					return SendAT, fromAddress
   557  				} else if trType.Byte == toTrType {
   558  					at := ReceiveAT
   559  					if fromAddress == ZeroAddress && transferType != nil {
   560  						if *transferType == TransferTypeErc721 || *transferType == TransferTypeErc1155 || (*transferType == TransferTypeErc20 && methodHash.Valid && (communityMintEvent || sliceContains(inputDataMethods, methodHash.String))) {
   561  							at = MintAT
   562  						}
   563  					}
   564  					return at, toAddress
   565  				}
   566  			}
   567  			log.Warn(fmt.Sprintf("unexpected activity type. Missing from [%s] or to [%s] in addresses?", fromAddress, toAddress))
   568  			return ReceiveAT, toAddress
   569  		}
   570  
   571  		// Can be mapped directly because the values are injected into the query
   572  		activityStatus := Status(aggregatedStatus)
   573  		var outChainID, inChainID *common.ChainID
   574  		var entry Entry
   575  		var tokenID *hexutil.Big
   576  		if len(dbTokenID) > 0 {
   577  			tokenID = (*hexutil.Big)(new(big.Int).SetBytes(dbTokenID))
   578  		}
   579  
   580  		if transferHash != nil && chainID.Valid {
   581  			// Process `transfers` row
   582  
   583  			// Extract activity type: SendAT/ReceiveAT
   584  			activityType, _ := getActivityType(dbTrType)
   585  
   586  			ownerAddress := eth.BytesToAddress(ownerAddressDB)
   587  			inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount, dbPTrAmount)
   588  
   589  			// Extract tokens and chains
   590  			var tokenContractAddress eth.Address
   591  			if tokenAddress != nil && *tokenAddress != ZeroAddress {
   592  				tokenContractAddress = *tokenAddress
   593  			}
   594  			involvedToken := &Token{
   595  				TokenType: transferTypeToTokenType(transferType),
   596  				ChainID:   common.ChainID(chainID.Int64),
   597  				Address:   tokenContractAddress,
   598  				TokenID:   tokenID,
   599  			}
   600  
   601  			entry = newActivityEntryWithSimpleTransaction(
   602  				&transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64),
   603  					Hash:    eth.BytesToHash(transferHash),
   604  					Address: ownerAddress,
   605  				},
   606  				timestamp, activityType, activityStatus,
   607  			)
   608  
   609  			// Extract tokens
   610  			if activityType == SendAT || activityType == ContractDeploymentAT {
   611  				entry.tokenOut = involvedToken
   612  				outChainID = new(common.ChainID)
   613  				*outChainID = common.ChainID(chainID.Int64)
   614  			} else {
   615  				entry.tokenIn = involvedToken
   616  				inChainID = new(common.ChainID)
   617  				*inChainID = common.ChainID(chainID.Int64)
   618  			}
   619  
   620  			entry.symbolOut, entry.symbolIn = lookupAndFillInTokens(deps, entry.tokenOut, entry.tokenIn)
   621  
   622  			// Complete the data
   623  			entry.amountOut = outAmount
   624  			entry.amountIn = inAmount
   625  		} else if pendingHash != nil && chainID.Valid {
   626  			// Process `pending_transactions` row
   627  
   628  			// Extract activity type: SendAT/ReceiveAT
   629  			activityType, _ := getActivityType(dbTrType)
   630  
   631  			inAmount, outAmount := getTrInAndOutAmounts(activityType, dbTrAmount, dbPTrAmount)
   632  
   633  			outChainID = new(common.ChainID)
   634  			*outChainID = common.ChainID(chainID.Int64)
   635  
   636  			entry = newActivityEntryWithPendingTransaction(
   637  				&transfer.TransactionIdentity{ChainID: common.ChainID(chainID.Int64),
   638  					Hash: eth.BytesToHash(pendingHash),
   639  				},
   640  				timestamp, activityType, activityStatus,
   641  			)
   642  
   643  			// Extract tokens
   644  			if tokenCode.Valid {
   645  				cID := common.ChainID(chainID.Int64)
   646  				entry.tokenOut = deps.tokenFromSymbol(&cID, tokenCode.String)
   647  			}
   648  			entry.symbolOut, entry.symbolIn = lookupAndFillInTokens(deps, entry.tokenOut, nil)
   649  
   650  			// Complete the data
   651  			entry.amountOut = outAmount
   652  			entry.amountIn = inAmount
   653  
   654  		} else if multiTxID.Valid {
   655  			// Process `multi_transactions` row
   656  
   657  			mtInAmount, mtOutAmount := getMtInAndOutAmounts(dbMtFromAmount, dbMtToAmount)
   658  
   659  			// Extract activity type: SendAT/SwapAT/BridgeAT/ApproveAT
   660  			activityType := multiTransactionTypeToActivityType(transfer.MultiTransactionType(dbMtType.Byte))
   661  
   662  			if outChainIDDB.Valid && outChainIDDB.Int64 != 0 {
   663  				outChainID = new(common.ChainID)
   664  				*outChainID = common.ChainID(outChainIDDB.Int64)
   665  			}
   666  			if inChainIDDB.Valid && inChainIDDB.Int64 != 0 {
   667  				inChainID = new(common.ChainID)
   668  				*inChainID = common.ChainID(inChainIDDB.Int64)
   669  			}
   670  
   671  			entry = NewActivityEntryWithMultiTransaction(common.MultiTransactionIDType(multiTxID.Int64),
   672  				timestamp, activityType, activityStatus)
   673  
   674  			// Extract tokens
   675  			if fromTokenCode.Valid {
   676  				entry.tokenOut = deps.tokenFromSymbol(outChainID, fromTokenCode.String)
   677  				entry.symbolOut = common.NewAndSet(fromTokenCode.String)
   678  			}
   679  			if toTokenCode.Valid {
   680  				entry.tokenIn = deps.tokenFromSymbol(inChainID, toTokenCode.String)
   681  				entry.symbolIn = common.NewAndSet(toTokenCode.String)
   682  			}
   683  
   684  			// Complete the data
   685  			entry.amountOut = mtOutAmount
   686  			entry.amountIn = mtInAmount
   687  		} else {
   688  			return nil, errors.New("invalid row data")
   689  		}
   690  
   691  		if communityID.Valid {
   692  			entry.communityID = common.NewAndSet(communityID.String)
   693  		}
   694  
   695  		// Complete common data
   696  		entry.recipient = &toAddress
   697  		entry.sender = &fromAddress
   698  		entry.recipient = &toAddress
   699  		entry.chainIDOut = outChainID
   700  		entry.chainIDIn = inChainID
   701  		entry.transferType = transferType
   702  
   703  		entries = append(entries, entry)
   704  	}
   705  
   706  	if err = rows.Err(); err != nil {
   707  		return nil, err
   708  	}
   709  
   710  	return entries, nil
   711  }
   712  
   713  func getTrInAndOutAmounts(activityType Type, trAmount sql.NullString, pTrAmount *big.Int) (inAmount *hexutil.Big, outAmount *hexutil.Big) {
   714  	var amount *big.Int
   715  	ok := false
   716  	if trAmount.Valid {
   717  		amount, ok = new(big.Int).SetString(trAmount.String, 16)
   718  	} else if pTrAmount != nil {
   719  		// Process pending transaction value
   720  		amount = pTrAmount
   721  		ok = true
   722  	} else {
   723  		log.Warn(fmt.Sprintf("invalid transaction amount for type %d", activityType))
   724  	}
   725  
   726  	if ok {
   727  		switch activityType {
   728  		case ApproveAT:
   729  			fallthrough
   730  		case ContractDeploymentAT:
   731  			fallthrough
   732  		case SendAT:
   733  			inAmount = (*hexutil.Big)(big.NewInt(0))
   734  			outAmount = (*hexutil.Big)(amount)
   735  			return
   736  		case MintAT:
   737  			fallthrough
   738  		case ReceiveAT:
   739  			inAmount = (*hexutil.Big)(amount)
   740  			outAmount = (*hexutil.Big)(big.NewInt(0))
   741  			return
   742  		default:
   743  			log.Warn(fmt.Sprintf("unexpected activity type %d", activityType))
   744  		}
   745  	} else {
   746  		log.Warn(fmt.Sprintf("could not parse amount %s", trAmount.String))
   747  	}
   748  
   749  	inAmount = (*hexutil.Big)(big.NewInt(0))
   750  	outAmount = (*hexutil.Big)(big.NewInt(0))
   751  	return
   752  }
   753  
   754  func getMtInAndOutAmounts(dbFromAmount sql.NullString, dbToAmount sql.NullString) (inAmount *hexutil.Big, outAmount *hexutil.Big) {
   755  	if dbFromAmount.Valid && dbToAmount.Valid {
   756  		fromHexStr := dbFromAmount.String
   757  		toHexStr := dbToAmount.String
   758  		if len(fromHexStr) > 2 && len(toHexStr) > 2 {
   759  			fromAmount, frOk := new(big.Int).SetString(dbFromAmount.String[2:], 16)
   760  			toAmount, toOk := new(big.Int).SetString(dbToAmount.String[2:], 16)
   761  			if frOk && toOk {
   762  				inAmount = (*hexutil.Big)(toAmount)
   763  				outAmount = (*hexutil.Big)(fromAmount)
   764  				return
   765  			}
   766  		}
   767  		log.Warn(fmt.Sprintf("could not parse amounts %s %s", fromHexStr, toHexStr))
   768  	} else {
   769  		log.Warn("invalid transaction amounts")
   770  	}
   771  	inAmount = (*hexutil.Big)(big.NewInt(0))
   772  	outAmount = (*hexutil.Big)(big.NewInt(0))
   773  	return
   774  }
   775  
   776  func contractTypeFromDBType(dbType string) (transferType *TransferType) {
   777  	transferType = new(TransferType)
   778  	switch common.Type(dbType) {
   779  	case common.EthTransfer:
   780  		*transferType = TransferTypeEth
   781  	case common.Erc20Transfer:
   782  		*transferType = TransferTypeErc20
   783  	case common.Erc721Transfer:
   784  		*transferType = TransferTypeErc721
   785  	case common.Erc1155Transfer:
   786  		*transferType = TransferTypeErc1155
   787  	default:
   788  		return nil
   789  	}
   790  	return transferType
   791  }
   792  
   793  func transferTypeToTokenType(transferType *TransferType) TokenType {
   794  	if transferType == nil {
   795  		return Native
   796  	}
   797  	switch *transferType {
   798  	case TransferTypeEth:
   799  		return Native
   800  	case TransferTypeErc20:
   801  		return Erc20
   802  	case TransferTypeErc721:
   803  		return Erc721
   804  	case TransferTypeErc1155:
   805  		return Erc1155
   806  	default:
   807  		log.Error(fmt.Sprintf("unexpected transfer type %d", transferType))
   808  	}
   809  	return Native
   810  }
   811  
   812  // lookupAndFillInTokens ignores NFTs
   813  func lookupAndFillInTokens(deps FilterDependencies, tokenOut *Token, tokenIn *Token) (symbolOut *string, symbolIn *string) {
   814  	if tokenOut != nil && tokenOut.TokenID == nil {
   815  		symbol := deps.tokenSymbol(*tokenOut)
   816  		if len(symbol) > 0 {
   817  			symbolOut = common.NewAndSet(symbol)
   818  		}
   819  	}
   820  	if tokenIn != nil && tokenIn.TokenID == nil {
   821  		symbol := deps.tokenSymbol(*tokenIn)
   822  		if len(symbol) > 0 {
   823  			symbolIn = common.NewAndSet(symbol)
   824  		}
   825  	}
   826  	return symbolOut, symbolIn
   827  }