github.com/status-im/status-go@v1.1.0/services/wallet/transfer/downloader.go (about) 1 package transfer 2 3 import ( 4 "context" 5 "errors" 6 "math/big" 7 "time" 8 9 "golang.org/x/exp/slices" // since 1.21, this is in the standard library 10 11 "github.com/ethereum/go-ethereum" 12 "github.com/ethereum/go-ethereum/common" 13 "github.com/ethereum/go-ethereum/core" 14 "github.com/ethereum/go-ethereum/core/types" 15 "github.com/ethereum/go-ethereum/log" 16 17 "github.com/status-im/status-go/rpc/chain" 18 w_common "github.com/status-im/status-go/services/wallet/common" 19 ) 20 21 var ( 22 zero = big.NewInt(0) 23 one = big.NewInt(1) 24 two = big.NewInt(2) 25 ) 26 27 // Partial transaction info obtained by ERC20Downloader. 28 // A PreloadedTransaction represents a Transaction which contains one 29 // ERC20/ERC721/ERC1155 transfer event. 30 // To be converted into one Transfer object post-indexing. 31 type PreloadedTransaction struct { 32 Type w_common.Type `json:"type"` 33 ID common.Hash `json:"-"` 34 Address common.Address `json:"address"` 35 // Log that was used to generate preloaded transaction. 36 Log *types.Log `json:"log"` 37 TokenID *big.Int `json:"tokenId"` 38 Value *big.Int `json:"value"` 39 } 40 41 // Transfer stores information about transfer. 42 // A Transfer represents a plain ETH transfer or some token activity inside a Transaction 43 // Since ERC1155 transfers can contain multiple tokens, a single Transfer represents a single token transfer, 44 // that means ERC1155 batch transfers will be represented by multiple Transfer objects. 45 type Transfer struct { 46 Type w_common.Type `json:"type"` 47 ID common.Hash `json:"-"` 48 Address common.Address `json:"address"` 49 BlockNumber *big.Int `json:"blockNumber"` 50 BlockHash common.Hash `json:"blockhash"` 51 Timestamp uint64 `json:"timestamp"` 52 Transaction *types.Transaction `json:"transaction"` 53 Loaded bool 54 NetworkID uint64 55 // From is derived from tx signature in order to offload this computation from UI component. 56 From common.Address `json:"from"` 57 Receipt *types.Receipt `json:"receipt"` 58 // Log that was used to generate erc20 transfer. Nil for eth transfer. 59 Log *types.Log `json:"log"` 60 // TokenID is the id of the transferred token. Nil for eth transfer. 61 TokenID *big.Int `json:"tokenId"` 62 // TokenValue is the value of the token transfer. Nil for eth transfer. 63 TokenValue *big.Int `json:"tokenValue"` 64 BaseGasFees string 65 // Internal field that is used to track multi-transaction transfers. 66 MultiTransactionID w_common.MultiTransactionIDType `json:"multi_transaction_id"` 67 } 68 69 // ETHDownloader downloads regular eth transfers and tokens transfers. 70 type ETHDownloader struct { 71 chainClient chain.ClientInterface 72 accounts []common.Address 73 signer types.Signer 74 db *Database 75 } 76 77 var errLogsDownloaderStuck = errors.New("logs downloader stuck") 78 79 func (d *ETHDownloader) GetTransfersByNumber(ctx context.Context, number *big.Int) ([]Transfer, error) { 80 blk, err := d.chainClient.BlockByNumber(ctx, number) 81 if err != nil { 82 return nil, err 83 } 84 rst, err := d.getTransfersInBlock(ctx, blk, d.accounts) 85 if err != nil { 86 return nil, err 87 } 88 return rst, err 89 } 90 91 // Only used by status-mobile 92 func getTransferByHash(ctx context.Context, client chain.ClientInterface, signer types.Signer, address common.Address, hash common.Hash) (*Transfer, error) { 93 transaction, _, err := client.TransactionByHash(ctx, hash) 94 if err != nil { 95 return nil, err 96 } 97 98 receipt, err := client.TransactionReceipt(ctx, hash) 99 if err != nil { 100 return nil, err 101 } 102 103 eventType, transactionLog := w_common.GetFirstEvent(receipt.Logs) 104 transactionType := w_common.EventTypeToSubtransactionType(eventType) 105 106 from, err := types.Sender(signer, transaction) 107 108 if err != nil { 109 return nil, err 110 } 111 112 baseGasFee, err := client.GetBaseFeeFromBlock(ctx, big.NewInt(int64(transactionLog.BlockNumber))) 113 if err != nil { 114 return nil, err 115 } 116 117 transfer := &Transfer{ 118 Type: transactionType, 119 ID: hash, 120 Address: address, 121 BlockNumber: receipt.BlockNumber, 122 BlockHash: receipt.BlockHash, 123 Timestamp: uint64(time.Now().Unix()), 124 Transaction: transaction, 125 From: from, 126 Receipt: receipt, 127 Log: transactionLog, 128 BaseGasFees: baseGasFee, 129 } 130 131 return transfer, nil 132 } 133 134 func (d *ETHDownloader) getTransfersInBlock(ctx context.Context, blk *types.Block, accounts []common.Address) ([]Transfer, error) { 135 startTs := time.Now() 136 137 rst := make([]Transfer, 0, len(blk.Transactions())) 138 139 receiptsByAddressAndTxHash := make(map[common.Address]map[common.Hash]*types.Receipt) 140 txsByAddressAndTxHash := make(map[common.Address]map[common.Hash]*types.Transaction) 141 142 addReceiptToCache := func(address common.Address, txHash common.Hash, receipt *types.Receipt) { 143 if receiptsByAddressAndTxHash[address] == nil { 144 receiptsByAddressAndTxHash[address] = make(map[common.Hash]*types.Receipt) 145 } 146 receiptsByAddressAndTxHash[address][txHash] = receipt 147 } 148 149 addTxToCache := func(address common.Address, txHash common.Hash, tx *types.Transaction) { 150 if txsByAddressAndTxHash[address] == nil { 151 txsByAddressAndTxHash[address] = make(map[common.Hash]*types.Transaction) 152 } 153 txsByAddressAndTxHash[address][txHash] = tx 154 } 155 156 getReceiptFromCache := func(address common.Address, txHash common.Hash) *types.Receipt { 157 if receiptsByAddressAndTxHash[address] == nil { 158 return nil 159 } 160 return receiptsByAddressAndTxHash[address][txHash] 161 } 162 163 getTxFromCache := func(address common.Address, txHash common.Hash) *types.Transaction { 164 if txsByAddressAndTxHash[address] == nil { 165 return nil 166 } 167 return txsByAddressAndTxHash[address][txHash] 168 } 169 170 getReceipt := func(address common.Address, txHash common.Hash) (receipt *types.Receipt, err error) { 171 receipt = getReceiptFromCache(address, txHash) 172 if receipt == nil { 173 receipt, err = d.fetchTransactionReceipt(ctx, txHash) 174 if err != nil { 175 return nil, err 176 } 177 addReceiptToCache(address, txHash, receipt) 178 } 179 return receipt, nil 180 } 181 182 getTx := func(address common.Address, txHash common.Hash) (tx *types.Transaction, err error) { 183 tx = getTxFromCache(address, txHash) 184 if tx == nil { 185 tx, err = d.fetchTransaction(ctx, txHash) 186 if err != nil { 187 return nil, err 188 } 189 addTxToCache(address, txHash, tx) 190 } 191 return tx, nil 192 } 193 194 for _, address := range accounts { 195 // During block discovery, we should have populated the DB with 1 item per transfer log containing 196 // erc20/erc721/erc1155 transfers. 197 // ID is a hash of the tx hash and the log index. log_index is unique per ERC20/721 tx, but not per ERC1155 tx. 198 transactionsToLoad, err := d.db.GetTransactionsToLoad(d.chainClient.NetworkID(), address, blk.Number()) 199 if err != nil { 200 return nil, err 201 } 202 203 areSubTxsCheckedForTxHash := make(map[common.Hash]bool) 204 205 log.Debug("getTransfersInBlock", "block", blk.Number(), "transactionsToLoad", len(transactionsToLoad)) 206 207 for _, t := range transactionsToLoad { 208 receipt, err := getReceipt(address, t.Log.TxHash) 209 if err != nil { 210 return nil, err 211 } 212 213 tx, err := getTx(address, t.Log.TxHash) 214 if err != nil { 215 return nil, err 216 } 217 218 subtransactions, err := d.subTransactionsFromPreloaded(t, tx, receipt, blk) 219 if err != nil { 220 log.Error("can't fetch subTxs for erc20/erc721/erc1155 transfer", "error", err) 221 return nil, err 222 } 223 rst = append(rst, subtransactions...) 224 areSubTxsCheckedForTxHash[t.Log.TxHash] = true 225 } 226 227 for _, tx := range blk.Transactions() { 228 // Skip dummy blob transactions, as they are not supported by us 229 if tx.Type() == types.BlobTxType { 230 continue 231 } 232 if tx.ChainId().Cmp(big.NewInt(0)) != 0 && tx.ChainId().Cmp(d.chainClient.ToBigInt()) != 0 { 233 log.Info("chain id mismatch", "tx hash", tx.Hash(), "tx chain id", tx.ChainId(), "expected chain id", d.chainClient.NetworkID()) 234 continue 235 } 236 from, err := types.Sender(d.signer, tx) 237 238 if err != nil { 239 if err == core.ErrTxTypeNotSupported { 240 log.Error("Tx Type not supported", "tx chain id", tx.ChainId(), "type", tx.Type(), "error", err) 241 continue 242 } 243 return nil, err 244 } 245 246 isPlainTransfer := from == address || (tx.To() != nil && *tx.To() == address) 247 mustCheckSubTxs := false 248 249 if !isPlainTransfer { 250 // We might miss some subTransactions of interest for some transaction types. We need to check if we 251 // find the address in the transaction data. 252 switch tx.Type() { 253 case types.DynamicFeeTxType, types.OptimismDepositTxType, types.ArbitrumDepositTxType, types.ArbitrumRetryTxType: 254 mustCheckSubTxs = !areSubTxsCheckedForTxHash[tx.Hash()] && w_common.TxDataContainsAddress(tx.Type(), tx.Data(), address) 255 } 256 } 257 258 if isPlainTransfer || mustCheckSubTxs { 259 receipt, err := getReceipt(address, tx.Hash()) 260 if err != nil { 261 return nil, err 262 } 263 264 // Since we've already got the receipt, check for subTxs of 265 // interest in case we haven't already. 266 if !areSubTxsCheckedForTxHash[tx.Hash()] { 267 subtransactions, err := d.subTransactionsFromTransactionData(address, from, tx, receipt, blk) 268 if err != nil { 269 log.Error("can't fetch subTxs for eth transfer", "error", err) 270 return nil, err 271 } 272 rst = append(rst, subtransactions...) 273 areSubTxsCheckedForTxHash[tx.Hash()] = true 274 } 275 276 // If it's a plain ETH transfer, add it to the list 277 if isPlainTransfer { 278 rst = append(rst, Transfer{ 279 Type: w_common.EthTransfer, 280 NetworkID: tx.ChainId().Uint64(), 281 ID: tx.Hash(), 282 Address: address, 283 BlockNumber: blk.Number(), 284 BlockHash: receipt.BlockHash, 285 Timestamp: blk.Time(), 286 Transaction: tx, 287 From: from, 288 Receipt: receipt, 289 Log: nil, 290 BaseGasFees: blk.BaseFee().String(), 291 MultiTransactionID: w_common.NoMultiTransactionID}) 292 } 293 } 294 } 295 } 296 log.Debug("getTransfersInBlock found", "block", blk.Number(), "len", len(rst), "time", time.Since(startTs)) 297 // TODO(dshulyak) test that balance difference was covered by transactions 298 return rst, nil 299 } 300 301 // NewERC20TransfersDownloader returns new instance. 302 func NewERC20TransfersDownloader(client chain.ClientInterface, accounts []common.Address, signer types.Signer, incomingOnly bool) *ERC20TransfersDownloader { 303 signature := w_common.GetEventSignatureHash(w_common.Erc20_721TransferEventSignature) 304 305 return &ERC20TransfersDownloader{ 306 client: client, 307 accounts: accounts, 308 signature: signature, 309 incomingOnly: incomingOnly, 310 signatureErc1155Single: w_common.GetEventSignatureHash(w_common.Erc1155TransferSingleEventSignature), 311 signatureErc1155Batch: w_common.GetEventSignatureHash(w_common.Erc1155TransferBatchEventSignature), 312 signer: signer, 313 } 314 } 315 316 // ERC20TransfersDownloader is a downloader for erc20 and erc721 tokens transfers. 317 // Since both transaction types share the same signature, both will be assigned 318 // type Erc20Transfer. Until the downloader gets refactored and a migration of the 319 // database gets implemented, differentiation between erc20 and erc721 will handled 320 // in the controller. 321 type ERC20TransfersDownloader struct { 322 client chain.ClientInterface 323 accounts []common.Address 324 incomingOnly bool 325 326 // hash of the Transfer event signature 327 signature common.Hash 328 signatureErc1155Single common.Hash 329 signatureErc1155Batch common.Hash 330 331 // signer is used to derive tx sender from tx signature 332 signer types.Signer 333 } 334 335 func topicFromAddressSlice(addresses []common.Address) []common.Hash { 336 rst := make([]common.Hash, len(addresses)) 337 for i, address := range addresses { 338 rst[i] = common.BytesToHash(address.Bytes()) 339 } 340 return rst 341 } 342 343 func (d *ERC20TransfersDownloader) inboundTopics(addresses []common.Address) [][]common.Hash { 344 return [][]common.Hash{{d.signature}, {}, topicFromAddressSlice(addresses)} 345 } 346 347 func (d *ERC20TransfersDownloader) outboundTopics(addresses []common.Address) [][]common.Hash { 348 return [][]common.Hash{{d.signature}, topicFromAddressSlice(addresses), {}} 349 } 350 351 func (d *ERC20TransfersDownloader) inboundERC20OutboundERC1155Topics(addresses []common.Address) [][]common.Hash { 352 return [][]common.Hash{{d.signature, d.signatureErc1155Single, d.signatureErc1155Batch}, {}, topicFromAddressSlice(addresses)} 353 } 354 355 func (d *ERC20TransfersDownloader) inboundTopicsERC1155(addresses []common.Address) [][]common.Hash { 356 return [][]common.Hash{{d.signatureErc1155Single, d.signatureErc1155Batch}, {}, {}, topicFromAddressSlice(addresses)} 357 } 358 359 func (d *ETHDownloader) fetchTransactionReceipt(parent context.Context, txHash common.Hash) (*types.Receipt, error) { 360 ctx, cancel := context.WithTimeout(parent, 3*time.Second) 361 receipt, err := d.chainClient.TransactionReceipt(ctx, txHash) 362 cancel() 363 if err != nil { 364 return nil, err 365 } 366 return receipt, nil 367 } 368 369 func (d *ETHDownloader) fetchTransaction(parent context.Context, txHash common.Hash) (*types.Transaction, error) { 370 ctx, cancel := context.WithTimeout(parent, 3*time.Second) 371 tx, _, err := d.chainClient.TransactionByHash(ctx, txHash) // TODO Save on requests by checking in the DB first 372 cancel() 373 if err != nil { 374 return nil, err 375 } 376 return tx, nil 377 } 378 379 func (d *ETHDownloader) subTransactionsFromPreloaded(preloadedTx *PreloadedTransaction, tx *types.Transaction, receipt *types.Receipt, blk *types.Block) ([]Transfer, error) { 380 log.Debug("subTransactionsFromPreloaded start", "txHash", tx.Hash().Hex(), "address", preloadedTx.Address, "tokenID", preloadedTx.TokenID, "value", preloadedTx.Value) 381 address := preloadedTx.Address 382 txLog := preloadedTx.Log 383 384 rst := make([]Transfer, 0, 1) 385 386 from, err := types.Sender(d.signer, tx) 387 if err != nil { 388 if err == core.ErrTxTypeNotSupported { 389 return nil, nil 390 } 391 return nil, err 392 } 393 394 eventType := w_common.GetEventType(preloadedTx.Log) 395 // Only add ERC20/ERC721/ERC1155 transfers from/to the given account 396 // from/to matching is already handled by getLogs filter 397 switch eventType { 398 case w_common.Erc20TransferEventType, 399 w_common.Erc721TransferEventType, 400 w_common.Erc1155TransferSingleEventType, w_common.Erc1155TransferBatchEventType: 401 log.Debug("subTransactionsFromPreloaded transfer", "eventType", eventType, "logIdx", txLog.Index, "txHash", tx.Hash().Hex(), "address", address.Hex(), "tokenID", preloadedTx.TokenID, "value", preloadedTx.Value, "baseFee", blk.BaseFee().String()) 402 403 transfer := Transfer{ 404 Type: w_common.EventTypeToSubtransactionType(eventType), 405 ID: preloadedTx.ID, 406 Address: address, 407 BlockNumber: new(big.Int).SetUint64(txLog.BlockNumber), 408 BlockHash: txLog.BlockHash, 409 Loaded: true, 410 NetworkID: d.signer.ChainID().Uint64(), 411 From: from, 412 Log: txLog, 413 TokenID: preloadedTx.TokenID, 414 TokenValue: preloadedTx.Value, 415 BaseGasFees: blk.BaseFee().String(), 416 Transaction: tx, 417 Receipt: receipt, 418 Timestamp: blk.Time(), 419 MultiTransactionID: w_common.NoMultiTransactionID, 420 } 421 422 rst = append(rst, transfer) 423 } 424 425 log.Debug("subTransactionsFromPreloaded end", "txHash", tx.Hash().Hex(), "address", address.Hex(), "tokenID", preloadedTx.TokenID, "value", preloadedTx.Value) 426 return rst, nil 427 } 428 429 func (d *ETHDownloader) subTransactionsFromTransactionData(address, from common.Address, tx *types.Transaction, receipt *types.Receipt, blk *types.Block) ([]Transfer, error) { 430 log.Debug("subTransactionsFromTransactionData start", "txHash", tx.Hash().Hex(), "address", address) 431 432 rst := make([]Transfer, 0, 1) 433 434 for _, txLog := range receipt.Logs { 435 eventType := w_common.GetEventType(txLog) 436 switch eventType { 437 case w_common.UniswapV2SwapEventType, w_common.UniswapV3SwapEventType, 438 w_common.HopBridgeTransferSentToL2EventType, w_common.HopBridgeTransferFromL1CompletedEventType, 439 w_common.HopBridgeWithdrawalBondedEventType, w_common.HopBridgeTransferSentEventType: 440 transfer := Transfer{ 441 Type: w_common.EventTypeToSubtransactionType(eventType), 442 ID: w_common.GetLogSubTxID(*txLog), 443 Address: address, 444 BlockNumber: new(big.Int).SetUint64(txLog.BlockNumber), 445 BlockHash: txLog.BlockHash, 446 Loaded: true, 447 NetworkID: d.signer.ChainID().Uint64(), 448 From: from, 449 Log: txLog, 450 BaseGasFees: blk.BaseFee().String(), 451 Transaction: tx, 452 Receipt: receipt, 453 Timestamp: blk.Time(), 454 MultiTransactionID: w_common.NoMultiTransactionID, 455 } 456 457 rst = append(rst, transfer) 458 } 459 } 460 461 log.Debug("subTransactionsFromTransactionData end", "txHash", tx.Hash().Hex(), "address", address.Hex()) 462 return rst, nil 463 } 464 465 func (d *ERC20TransfersDownloader) blocksFromLogs(parent context.Context, logs []types.Log) ([]*DBHeader, error) { 466 concurrent := NewConcurrentDownloader(parent, NoThreadLimit) 467 468 for i := range logs { 469 l := logs[i] 470 471 if l.Removed { 472 continue 473 } 474 475 var address common.Address 476 from, to, txIDs, tokenIDs, values, err := w_common.ParseTransferLog(l) 477 if err != nil { 478 log.Error("failed to parse transfer log", "log", l, "address", d.accounts, "error", err) 479 continue 480 } 481 482 // Double check provider returned the correct log 483 if slices.Contains(d.accounts, from) { 484 address = from 485 } else if slices.Contains(d.accounts, to) { 486 address = to 487 } else { 488 log.Error("from/to address mismatch", "log", l, "addresses", d.accounts) 489 continue 490 } 491 492 eventType := w_common.GetEventType(&l) 493 logType := w_common.EventTypeToSubtransactionType(eventType) 494 495 for i, txID := range txIDs { 496 log.Debug("block from logs", "block", l.BlockNumber, "log", l, "logType", logType, "txID", txID) 497 498 // For ERC20 there is no tokenID, so we use nil 499 var tokenID *big.Int 500 if len(tokenIDs) > i { 501 tokenID = tokenIDs[i] 502 } 503 504 header := &DBHeader{ 505 Number: big.NewInt(int64(l.BlockNumber)), 506 Hash: l.BlockHash, 507 Address: address, 508 PreloadedTransactions: []*PreloadedTransaction{{ 509 ID: txID, 510 Type: logType, 511 Log: &l, 512 TokenID: tokenID, 513 Value: values[i], 514 }}, 515 Loaded: false, 516 } 517 518 concurrent.Add(func(ctx context.Context) error { 519 concurrent.PushHeader(header) 520 return nil 521 }) 522 } 523 } 524 select { 525 case <-concurrent.WaitAsync(): 526 case <-parent.Done(): 527 return nil, errLogsDownloaderStuck 528 } 529 return concurrent.GetHeaders(), concurrent.Error() 530 } 531 532 // GetHeadersInRange returns transfers between two blocks. 533 // time to get logs for 100000 blocks = 1.144686979s. with 249 events in the result set. 534 func (d *ERC20TransfersDownloader) GetHeadersInRange(parent context.Context, from, to *big.Int) ([]*DBHeader, error) { 535 start := time.Now() 536 log.Debug("get erc20 transfers in range start", "chainID", d.client.NetworkID(), "from", from, "to", to, "accounts", d.accounts) 537 538 // TODO #16062: Figure out real root cause of invalid range 539 if from != nil && to != nil && from.Cmp(to) > 0 { 540 log.Error("invalid range", "chainID", d.client.NetworkID(), "from", from, "to", to, "accounts", d.accounts) 541 return nil, errors.New("invalid range") 542 } 543 544 headers := []*DBHeader{} 545 ctx := context.Background() 546 var err error 547 outbound := []types.Log{} 548 var inboundOrMixed []types.Log // inbound ERC20 or outbound ERC1155 share the same signature for our purposes 549 if !d.incomingOnly { 550 outbound, err = d.client.FilterLogs(ctx, ethereum.FilterQuery{ 551 FromBlock: from, 552 ToBlock: to, 553 Topics: d.outboundTopics(d.accounts), 554 }) 555 if err != nil { 556 return nil, err 557 } 558 inboundOrMixed, err = d.client.FilterLogs(ctx, ethereum.FilterQuery{ 559 FromBlock: from, 560 ToBlock: to, 561 Topics: d.inboundERC20OutboundERC1155Topics(d.accounts), 562 }) 563 if err != nil { 564 return nil, err 565 } 566 } else { 567 inboundOrMixed, err = d.client.FilterLogs(ctx, ethereum.FilterQuery{ 568 FromBlock: from, 569 ToBlock: to, 570 Topics: d.inboundTopics(d.accounts), 571 }) 572 if err != nil { 573 return nil, err 574 } 575 } 576 577 inbound1155, err := d.client.FilterLogs(ctx, ethereum.FilterQuery{ 578 FromBlock: from, 579 ToBlock: to, 580 Topics: d.inboundTopicsERC1155(d.accounts), 581 }) 582 if err != nil { 583 return nil, err 584 } 585 586 logs := concatLogs(outbound, inboundOrMixed, inbound1155) 587 588 if len(logs) == 0 { 589 log.Debug("no logs found for account") 590 return nil, nil 591 } 592 593 rst, err := d.blocksFromLogs(parent, logs) 594 if err != nil { 595 return nil, err 596 } 597 if len(rst) == 0 { 598 log.Warn("no headers found in logs for account", "chainID", d.client.NetworkID(), "addresses", d.accounts, "from", from, "to", to) 599 } else { 600 headers = append(headers, rst...) 601 log.Debug("found erc20 transfers for account", "chainID", d.client.NetworkID(), "addresses", d.accounts, 602 "from", from, "to", to, "headers", len(headers)) 603 } 604 605 log.Debug("get erc20 transfers in range end", "chainID", d.client.NetworkID(), 606 "from", from, "to", to, "headers", len(headers), "accounts", d.accounts, "took", time.Since(start)) 607 return headers, nil 608 } 609 610 func concatLogs(slices ...[]types.Log) []types.Log { 611 var totalLen int 612 for _, s := range slices { 613 totalLen += len(s) 614 } 615 tmp := make([]types.Log, totalLen) 616 var i int 617 for _, s := range slices { 618 i += copy(tmp[i:], s) 619 } 620 621 return tmp 622 }