github.com/status-im/status-go@v1.1.0/services/wallet/transfer/commands.go (about) 1 package transfer 2 3 import ( 4 "context" 5 "database/sql" 6 "math/big" 7 "time" 8 9 "golang.org/x/exp/maps" 10 11 "github.com/ethereum/go-ethereum/common" 12 "github.com/ethereum/go-ethereum/core/types" 13 "github.com/ethereum/go-ethereum/event" 14 "github.com/ethereum/go-ethereum/log" 15 16 "github.com/status-im/status-go/rpc/chain" 17 "github.com/status-im/status-go/services/wallet/async" 18 "github.com/status-im/status-go/services/wallet/balance" 19 w_common "github.com/status-im/status-go/services/wallet/common" 20 "github.com/status-im/status-go/services/wallet/token" 21 "github.com/status-im/status-go/services/wallet/walletevent" 22 "github.com/status-im/status-go/transactions" 23 ) 24 25 const ( 26 // EventNewTransfers emitted when new block was added to the same canonical chan. 27 EventNewTransfers walletevent.EventType = "new-transfers" 28 // EventFetchingRecentHistory emitted when fetching of lastest tx history is started 29 EventFetchingRecentHistory walletevent.EventType = "recent-history-fetching" 30 // EventRecentHistoryReady emitted when fetching of lastest tx history is started 31 EventRecentHistoryReady walletevent.EventType = "recent-history-ready" 32 // EventFetchingHistoryError emitted when fetching of tx history failed 33 EventFetchingHistoryError walletevent.EventType = "fetching-history-error" 34 // EventNonArchivalNodeDetected emitted when a connection to a non archival node is detected 35 EventNonArchivalNodeDetected walletevent.EventType = "non-archival-node-detected" 36 37 // Internal events emitted when different kinds of transfers are detected 38 EventInternalETHTransferDetected walletevent.EventType = walletevent.InternalEventTypePrefix + "eth-transfer-detected" 39 EventInternalERC20TransferDetected walletevent.EventType = walletevent.InternalEventTypePrefix + "erc20-transfer-detected" 40 EventInternalERC721TransferDetected walletevent.EventType = walletevent.InternalEventTypePrefix + "erc721-transfer-detected" 41 EventInternalERC1155TransferDetected walletevent.EventType = walletevent.InternalEventTypePrefix + "erc1155-transfer-detected" 42 43 numberOfBlocksCheckedPerIteration = 40 44 noBlockLimit = 0 45 ) 46 47 var ( 48 // This will work only for binance testnet as mainnet doesn't support 49 // archival request. 50 binanceChainErc20BatchSize = big.NewInt(5000) 51 goerliErc20BatchSize = big.NewInt(100000) 52 goerliErc20ArbitrumBatchSize = big.NewInt(10000) 53 goerliErc20OptimismBatchSize = big.NewInt(10000) 54 sepoliaErc20BatchSize = big.NewInt(100000) 55 sepoliaErc20ArbitrumBatchSize = big.NewInt(10000) 56 sepoliaErc20OptimismBatchSize = big.NewInt(10000) 57 erc20BatchSize = big.NewInt(100000) 58 59 transfersRetryInterval = 5 * time.Second 60 ) 61 62 type ethHistoricalCommand struct { 63 address common.Address 64 chainClient chain.ClientInterface 65 balanceCacher balance.Cacher 66 feed *event.Feed 67 foundHeaders []*DBHeader 68 error error 69 noLimit bool 70 71 from *Block 72 to, resultingFrom, startBlock *big.Int 73 threadLimit uint32 74 } 75 76 type Transaction []*Transfer 77 78 func (c *ethHistoricalCommand) Command() async.Command { 79 return async.FiniteCommand{ 80 Interval: 5 * time.Second, 81 Runable: c.Run, 82 }.Run 83 } 84 85 func (c *ethHistoricalCommand) Run(ctx context.Context) (err error) { 86 log.Debug("eth historical downloader start", "chainID", c.chainClient.NetworkID(), "address", c.address, 87 "from", c.from.Number, "to", c.to, "noLimit", c.noLimit) 88 89 start := time.Now() 90 if c.from.Number != nil && c.from.Balance != nil { 91 c.balanceCacher.Cache().AddBalance(c.address, c.chainClient.NetworkID(), c.from.Number, c.from.Balance) 92 } 93 if c.from.Number != nil && c.from.Nonce != nil { 94 c.balanceCacher.Cache().AddNonce(c.address, c.chainClient.NetworkID(), c.from.Number, c.from.Nonce) 95 } 96 from, headers, startBlock, err := findBlocksWithEthTransfers(ctx, c.chainClient, 97 c.balanceCacher, c.address, c.from.Number, c.to, c.noLimit, c.threadLimit) 98 99 if err != nil { 100 c.error = err 101 log.Error("failed to find blocks with transfers", "error", err, "chainID", c.chainClient.NetworkID(), 102 "address", c.address, "from", c.from.Number, "to", c.to) 103 return nil 104 } 105 106 c.foundHeaders = headers 107 c.resultingFrom = from 108 c.startBlock = startBlock 109 110 log.Debug("eth historical downloader finished successfully", "chain", c.chainClient.NetworkID(), 111 "address", c.address, "from", from, "to", c.to, "total blocks", len(headers), "time", time.Since(start)) 112 113 return nil 114 } 115 116 type erc20HistoricalCommand struct { 117 erc20 BatchDownloader 118 chainClient chain.ClientInterface 119 feed *event.Feed 120 121 iterator *IterativeDownloader 122 to *big.Int 123 from *big.Int 124 foundHeaders []*DBHeader 125 } 126 127 func (c *erc20HistoricalCommand) Command() async.Command { 128 return async.FiniteCommand{ 129 Interval: 5 * time.Second, 130 Runable: c.Run, 131 }.Run 132 } 133 134 func getErc20BatchSize(chainID uint64) *big.Int { 135 switch chainID { 136 case w_common.EthereumSepolia: 137 return sepoliaErc20BatchSize 138 case w_common.OptimismSepolia: 139 return sepoliaErc20OptimismBatchSize 140 case w_common.ArbitrumSepolia: 141 return sepoliaErc20ArbitrumBatchSize 142 case w_common.EthereumGoerli: 143 return goerliErc20BatchSize 144 case w_common.OptimismGoerli: 145 return goerliErc20OptimismBatchSize 146 case w_common.ArbitrumGoerli: 147 return goerliErc20ArbitrumBatchSize 148 case w_common.BinanceChainID: 149 return binanceChainErc20BatchSize 150 case w_common.BinanceTestChainID: 151 return binanceChainErc20BatchSize 152 default: 153 return erc20BatchSize 154 } 155 } 156 157 func (c *erc20HistoricalCommand) Run(ctx context.Context) (err error) { 158 log.Debug("wallet historical downloader for erc20 transfers start", "chainID", c.chainClient.NetworkID(), 159 "from", c.from, "to", c.to) 160 161 start := time.Now() 162 if c.iterator == nil { 163 c.iterator, err = SetupIterativeDownloader( 164 c.chainClient, 165 c.erc20, getErc20BatchSize(c.chainClient.NetworkID()), c.to, c.from) 166 if err != nil { 167 log.Error("failed to setup historical downloader for erc20") 168 return err 169 } 170 } 171 for !c.iterator.Finished() { 172 headers, _, _, err := c.iterator.Next(ctx) 173 if err != nil { 174 log.Error("failed to get next batch", "error", err, "chainID", c.chainClient.NetworkID()) // TODO: stop inifinite command in case of an error that we can't fix like missing trie node 175 return err 176 } 177 c.foundHeaders = append(c.foundHeaders, headers...) 178 } 179 log.Debug("wallet historical downloader for erc20 transfers finished", "chainID", c.chainClient.NetworkID(), 180 "from", c.from, "to", c.to, "time", time.Since(start), "headers", len(c.foundHeaders)) 181 return nil 182 } 183 184 type transfersCommand struct { 185 db *Database 186 blockDAO *BlockDAO 187 eth *ETHDownloader 188 blockNums []*big.Int 189 address common.Address 190 chainClient chain.ClientInterface 191 blocksLimit int 192 transactionManager *TransactionManager 193 pendingTxManager *transactions.PendingTxTracker 194 tokenManager *token.Manager 195 feed *event.Feed 196 197 // result 198 fetchedTransfers []Transfer 199 } 200 201 func (c *transfersCommand) Runner(interval ...time.Duration) async.Runner { 202 intvl := transfersRetryInterval 203 if len(interval) > 0 { 204 intvl = interval[0] 205 } 206 return async.FiniteCommandWithErrorCounter{ 207 FiniteCommand: async.FiniteCommand{ 208 Interval: intvl, 209 Runable: c.Run, 210 }, 211 ErrorCounter: async.NewErrorCounter(5, "transfersCommand"), 212 } 213 } 214 215 func (c *transfersCommand) Command(interval ...time.Duration) async.Command { 216 return c.Runner(interval...).Run 217 } 218 219 func (c *transfersCommand) Run(ctx context.Context) (err error) { 220 // Take blocks from cache if available and disrespect the limit 221 // If no blocks are available in cache, take blocks from DB respecting the limit 222 // If no limit is set, take all blocks from DB 223 log.Debug("start transfersCommand", "chain", c.chainClient.NetworkID(), "address", c.address, "blockNums", c.blockNums) 224 startTs := time.Now() 225 226 for { 227 blocks := c.blockNums 228 if blocks == nil { 229 blocks, _ = c.blockDAO.GetBlocksToLoadByAddress(c.chainClient.NetworkID(), c.address, numberOfBlocksCheckedPerIteration) 230 } 231 232 for _, blockNum := range blocks { 233 log.Debug("transfersCommand block start", "chain", c.chainClient.NetworkID(), "address", c.address, "block", blockNum) 234 235 allTransfers, err := c.eth.GetTransfersByNumber(ctx, blockNum) 236 if err != nil { 237 log.Error("getTransfersByBlocks error", "error", err) 238 return err 239 } 240 241 c.processUnknownErc20CommunityTransactions(ctx, allTransfers) 242 243 if len(allTransfers) > 0 { 244 // First, try to match to any pre-existing pending/multi-transaction 245 err := c.saveAndConfirmPending(allTransfers, blockNum) 246 if err != nil { 247 log.Error("saveAndConfirmPending error", "error", err) 248 return err 249 } 250 251 // Check if multi transaction needs to be created 252 err = c.processMultiTransactions(ctx, allTransfers) 253 if err != nil { 254 log.Error("processMultiTransactions error", "error", err) 255 return err 256 } 257 } else { 258 // If no transfers found, that is suspecting, because downloader returned this block as containing transfers 259 log.Error("no transfers found in block", "chain", c.chainClient.NetworkID(), "address", c.address, "block", blockNum) 260 261 err = markBlocksAsLoaded(c.chainClient.NetworkID(), c.db.client, c.address, []*big.Int{blockNum}) 262 if err != nil { 263 log.Error("Mark blocks loaded error", "error", err) 264 return err 265 } 266 } 267 268 c.fetchedTransfers = append(c.fetchedTransfers, allTransfers...) 269 270 c.notifyOfNewTransfers(blockNum, allTransfers) 271 c.notifyOfLatestTransfers(allTransfers, w_common.EthTransfer) 272 c.notifyOfLatestTransfers(allTransfers, w_common.Erc20Transfer) 273 c.notifyOfLatestTransfers(allTransfers, w_common.Erc721Transfer) 274 c.notifyOfLatestTransfers(allTransfers, w_common.Erc1155Transfer) 275 276 log.Debug("transfersCommand block end", "chain", c.chainClient.NetworkID(), "address", c.address, 277 "block", blockNum, "tranfers.len", len(allTransfers), "fetchedTransfers.len", len(c.fetchedTransfers)) 278 } 279 280 if c.blockNums != nil || len(blocks) == 0 || 281 (c.blocksLimit > noBlockLimit && len(blocks) >= c.blocksLimit) { 282 log.Debug("loadTransfers breaking loop on block limits reached or 0 blocks", "chain", c.chainClient.NetworkID(), 283 "address", c.address, "limit", c.blocksLimit, "blocks", len(blocks)) 284 break 285 } 286 } 287 288 log.Debug("end transfersCommand", "chain", c.chainClient.NetworkID(), "address", c.address, 289 "blocks.len", len(c.blockNums), "transfers.len", len(c.fetchedTransfers), "in", time.Since(startTs)) 290 291 return nil 292 } 293 294 // saveAndConfirmPending ensures only the transaction that has owner (Address) as a sender is matched to the 295 // corresponding multi-transaction (by multi-transaction ID). This way we ensure that if receiver is in the list 296 // of accounts filter will discard the proper one 297 func (c *transfersCommand) saveAndConfirmPending(allTransfers []Transfer, blockNum *big.Int) error { 298 tx, resErr := c.db.client.Begin() 299 if resErr != nil { 300 return resErr 301 } 302 notifyFunctions := c.confirmPendingTransactions(tx, allTransfers) 303 defer func() { 304 if resErr == nil { 305 commitErr := tx.Commit() 306 if commitErr != nil { 307 log.Error("failed to commit", "error", commitErr) 308 } 309 for _, notify := range notifyFunctions { 310 notify() 311 } 312 } else { 313 rollbackErr := tx.Rollback() 314 if rollbackErr != nil { 315 log.Error("failed to rollback", "error", rollbackErr) 316 } 317 } 318 }() 319 320 resErr = saveTransfersMarkBlocksLoaded(tx, c.chainClient.NetworkID(), c.address, allTransfers, []*big.Int{blockNum}) 321 if resErr != nil { 322 log.Error("SaveTransfers error", "error", resErr) 323 } 324 325 return resErr 326 } 327 328 func externalTransactionOrError(err error, mTID int64) bool { 329 if err == sql.ErrNoRows { 330 // External transaction downloaded, ignore it 331 return true 332 } else if err != nil { 333 log.Warn("GetOwnedMultiTransactionID", "error", err) 334 return true 335 } else if mTID <= 0 { 336 // Existing external transaction, ignore it 337 return true 338 } 339 return false 340 } 341 342 func (c *transfersCommand) confirmPendingTransactions(tx *sql.Tx, allTransfers []Transfer) (notifyFunctions []func()) { 343 notifyFunctions = make([]func(), 0) 344 345 // Confirm all pending transactions that are included in this block 346 for i, tr := range allTransfers { 347 chainID := w_common.ChainID(tr.NetworkID) 348 txHash := tr.Receipt.TxHash 349 txType, mTID, err := transactions.GetOwnedPendingStatus(tx, chainID, txHash, tr.Address) 350 if err == sql.ErrNoRows { 351 if tr.MultiTransactionID > 0 { 352 continue 353 } else { 354 // Outside transaction, already confirmed by another duplicate or not yet downloaded 355 existingMTID, err := GetOwnedMultiTransactionID(tx, chainID, txHash, tr.Address) 356 if externalTransactionOrError(err, existingMTID) { 357 continue 358 } 359 mTID = w_common.NewAndSet(existingMTID) 360 } 361 } else if err != nil { 362 log.Warn("GetOwnedPendingStatus", "error", err) 363 continue 364 } 365 366 if mTID != nil { 367 allTransfers[i].MultiTransactionID = w_common.MultiTransactionIDType(*mTID) 368 } 369 if txType != nil && *txType == transactions.WalletTransfer { 370 notify, err := c.pendingTxManager.DeleteBySQLTx(tx, chainID, txHash) 371 if err != nil && err != transactions.ErrStillPending { 372 log.Error("DeleteBySqlTx error", "error", err) 373 } 374 notifyFunctions = append(notifyFunctions, notify) 375 } 376 } 377 return notifyFunctions 378 } 379 380 // Mark all subTxs of a given Tx with the same multiTxID 381 func setMultiTxID(tx Transaction, multiTxID w_common.MultiTransactionIDType) { 382 for _, subTx := range tx { 383 subTx.MultiTransactionID = multiTxID 384 } 385 } 386 387 func (c *transfersCommand) markMultiTxTokensAsPreviouslyOwned(ctx context.Context, multiTransaction *MultiTransaction, ownerAddress common.Address) { 388 if multiTransaction == nil { 389 return 390 } 391 if len(multiTransaction.ToAsset) > 0 && multiTransaction.ToNetworkID > 0 { 392 token := c.tokenManager.GetToken(multiTransaction.ToNetworkID, multiTransaction.ToAsset) 393 _, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress) 394 } 395 if len(multiTransaction.FromAsset) > 0 && multiTransaction.FromNetworkID > 0 { 396 token := c.tokenManager.GetToken(multiTransaction.FromNetworkID, multiTransaction.FromAsset) 397 _, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress) 398 } 399 } 400 401 func (c *transfersCommand) checkAndProcessSwapMultiTx(ctx context.Context, tx Transaction) (bool, error) { 402 for _, subTx := range tx { 403 switch subTx.Type { 404 // If the Tx contains any uniswapV2Swap/uniswapV3Swap subTx, generate a Swap multiTx 405 case w_common.UniswapV2Swap, w_common.UniswapV3Swap: 406 multiTransaction, err := buildUniswapSwapMultitransaction(ctx, c.chainClient, c.tokenManager, subTx) 407 if err != nil { 408 return false, err 409 } 410 411 if multiTransaction != nil { 412 id, err := c.transactionManager.InsertMultiTransaction(multiTransaction) 413 if err != nil { 414 return false, err 415 } 416 setMultiTxID(tx, id) 417 c.markMultiTxTokensAsPreviouslyOwned(ctx, multiTransaction, subTx.Address) 418 return true, nil 419 } 420 } 421 } 422 423 return false, nil 424 } 425 426 func (c *transfersCommand) checkAndProcessBridgeMultiTx(ctx context.Context, tx Transaction) (bool, error) { 427 for _, subTx := range tx { 428 switch subTx.Type { 429 // If the Tx contains any hopBridge subTx, create/update Bridge multiTx 430 case w_common.HopBridgeFrom, w_common.HopBridgeTo: 431 multiTransaction, err := buildHopBridgeMultitransaction(ctx, c.chainClient, c.transactionManager, c.tokenManager, subTx) 432 if err != nil { 433 return false, err 434 } 435 436 if multiTransaction != nil { 437 setMultiTxID(tx, multiTransaction.ID) 438 c.markMultiTxTokensAsPreviouslyOwned(ctx, multiTransaction, subTx.Address) 439 return true, nil 440 } 441 } 442 } 443 444 return false, nil 445 } 446 447 func (c *transfersCommand) processUnknownErc20CommunityTransactions(ctx context.Context, allTransfers []Transfer) { 448 for _, tx := range allTransfers { 449 // To can be nil in case of erc20 contract creation 450 if tx.Type == w_common.Erc20Transfer && tx.Transaction.To() != nil { 451 // Find token in db or if this is a community token, find its metadata 452 token := c.tokenManager.FindOrCreateTokenByAddress(ctx, tx.NetworkID, *tx.Transaction.To()) 453 if token != nil { 454 isFirst := false 455 if token.Verified || token.CommunityData != nil { 456 isFirst, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address) 457 } 458 if token.CommunityData != nil { 459 go c.tokenManager.SignalCommunityTokenReceived(tx.Address, tx.ID, tx.TokenValue, token, isFirst) 460 } 461 } 462 } 463 } 464 } 465 466 func (c *transfersCommand) processMultiTransactions(ctx context.Context, allTransfers []Transfer) error { 467 txByTxHash := subTransactionListToTransactionsByTxHash(allTransfers) 468 469 // Detect / Generate multitransactions 470 // Iterate over all detected transactions 471 for _, tx := range txByTxHash { 472 // Check if already matched to a multi transaction 473 if tx[0].MultiTransactionID > 0 { 474 continue 475 } 476 477 // Then check for a Swap transaction 478 txProcessed, err := c.checkAndProcessSwapMultiTx(ctx, tx) 479 if err != nil { 480 return err 481 } 482 if txProcessed { 483 continue 484 } 485 486 // Then check for a Bridge transaction 487 _, err = c.checkAndProcessBridgeMultiTx(ctx, tx) 488 if err != nil { 489 return err 490 } 491 } 492 493 return nil 494 } 495 496 func (c *transfersCommand) notifyOfNewTransfers(blockNum *big.Int, transfers []Transfer) { 497 if c.feed != nil { 498 if len(transfers) > 0 { 499 c.feed.Send(walletevent.Event{ 500 Type: EventNewTransfers, 501 Accounts: []common.Address{c.address}, 502 ChainID: c.chainClient.NetworkID(), 503 BlockNumber: blockNum, 504 }) 505 } 506 } 507 } 508 509 func transferTypeToEventType(transferType w_common.Type) walletevent.EventType { 510 switch transferType { 511 case w_common.EthTransfer: 512 return EventInternalETHTransferDetected 513 case w_common.Erc20Transfer: 514 return EventInternalERC20TransferDetected 515 case w_common.Erc721Transfer: 516 return EventInternalERC721TransferDetected 517 case w_common.Erc1155Transfer: 518 return EventInternalERC1155TransferDetected 519 default: 520 return "" 521 } 522 } 523 524 func (c *transfersCommand) notifyOfLatestTransfers(transfers []Transfer, transferType w_common.Type) { 525 if c.feed != nil { 526 eventTransfers := make([]Transfer, 0, len(transfers)) 527 latestTransferTimestamp := uint64(0) 528 for _, transfer := range transfers { 529 if transfer.Type == transferType { 530 eventTransfers = append(eventTransfers, transfer) 531 if transfer.Timestamp > latestTransferTimestamp { 532 latestTransferTimestamp = transfer.Timestamp 533 } 534 } 535 } 536 if len(eventTransfers) > 0 { 537 c.feed.Send(walletevent.Event{ 538 Type: transferTypeToEventType(transferType), 539 Accounts: []common.Address{c.address}, 540 ChainID: c.chainClient.NetworkID(), 541 At: int64(latestTransferTimestamp), 542 EventParams: eventTransfers, 543 }) 544 } 545 } 546 } 547 548 type loadTransfersCommand struct { 549 accounts []common.Address 550 db *Database 551 blockDAO *BlockDAO 552 chainClient chain.ClientInterface 553 blocksByAddress map[common.Address][]*big.Int 554 transactionManager *TransactionManager 555 pendingTxManager *transactions.PendingTxTracker 556 blocksLimit int 557 tokenManager *token.Manager 558 feed *event.Feed 559 } 560 561 func (c *loadTransfersCommand) Command() async.Command { 562 return async.FiniteCommand{ 563 Interval: 5 * time.Second, 564 Runable: c.Run, 565 }.Run 566 } 567 568 // This command always returns nil, even if there is an error in one of the commands. 569 // `transferCommand`s retry until maxError, but this command doesn't retry. 570 // In case some transfer is not loaded after max retries, it will be retried only after restart of the app. 571 // Currently there is no implementation to keep retrying until success. I think this should be implemented 572 // in `transferCommand` with exponential backoff instead of `loadTransfersCommand` (issue #4608). 573 func (c *loadTransfersCommand) Run(parent context.Context) (err error) { 574 return loadTransfers(parent, c.blockDAO, c.db, c.chainClient, c.blocksLimit, c.blocksByAddress, 575 c.transactionManager, c.pendingTxManager, c.tokenManager, c.feed) 576 } 577 578 func loadTransfers(ctx context.Context, blockDAO *BlockDAO, db *Database, 579 chainClient chain.ClientInterface, blocksLimitPerAccount int, blocksByAddress map[common.Address][]*big.Int, 580 transactionManager *TransactionManager, pendingTxManager *transactions.PendingTxTracker, 581 tokenManager *token.Manager, feed *event.Feed) error { 582 583 log.Debug("loadTransfers start", "chain", chainClient.NetworkID(), "limit", blocksLimitPerAccount) 584 585 start := time.Now() 586 group := async.NewGroup(ctx) 587 588 accounts := maps.Keys(blocksByAddress) 589 for _, address := range accounts { 590 transfers := &transfersCommand{ 591 db: db, 592 blockDAO: blockDAO, 593 chainClient: chainClient, 594 address: address, 595 eth: ÐDownloader{ 596 chainClient: chainClient, 597 accounts: []common.Address{address}, 598 signer: types.LatestSignerForChainID(chainClient.ToBigInt()), 599 db: db, 600 }, 601 blockNums: blocksByAddress[address], 602 transactionManager: transactionManager, 603 pendingTxManager: pendingTxManager, 604 tokenManager: tokenManager, 605 feed: feed, 606 } 607 group.Add(transfers.Command()) 608 } 609 610 select { 611 case <-ctx.Done(): 612 log.Debug("loadTransfers cancelled", "chain", chainClient.NetworkID(), "error", ctx.Err()) 613 case <-group.WaitAsync(): 614 log.Debug("loadTransfers finished for account", "in", time.Since(start), "chain", chainClient.NetworkID()) 615 } 616 return nil 617 } 618 619 // Ensure 1 DBHeader per Block Hash 620 func uniqueHeaderPerBlockHash(allHeaders []*DBHeader) []*DBHeader { 621 uniqHeadersByHash := map[common.Hash]*DBHeader{} 622 for _, header := range allHeaders { 623 uniqHeader, ok := uniqHeadersByHash[header.Hash] 624 if ok { 625 if len(header.PreloadedTransactions) > 0 { 626 uniqHeader.PreloadedTransactions = append(uniqHeader.PreloadedTransactions, header.PreloadedTransactions...) 627 } 628 uniqHeadersByHash[header.Hash] = uniqHeader 629 } else { 630 uniqHeadersByHash[header.Hash] = header 631 } 632 } 633 634 uniqHeaders := []*DBHeader{} 635 for _, header := range uniqHeadersByHash { 636 uniqHeaders = append(uniqHeaders, header) 637 } 638 639 return uniqHeaders 640 } 641 642 // Organize subTransactions by Transaction Hash 643 func subTransactionListToTransactionsByTxHash(subTransactions []Transfer) map[common.Hash]Transaction { 644 rst := map[common.Hash]Transaction{} 645 646 for index := range subTransactions { 647 subTx := &subTransactions[index] 648 txHash := subTx.Transaction.Hash() 649 650 if _, ok := rst[txHash]; !ok { 651 rst[txHash] = make([]*Transfer, 0) 652 } 653 rst[txHash] = append(rst[txHash], subTx) 654 } 655 656 return rst 657 } 658 659 func IsTransferDetectionEvent(ev walletevent.EventType) bool { 660 if ev == EventInternalETHTransferDetected || 661 ev == EventInternalERC20TransferDetected || 662 ev == EventInternalERC721TransferDetected || 663 ev == EventInternalERC1155TransferDetected { 664 return true 665 } 666 667 return false 668 }