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, ×tamp, &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 }