From 62014934c6a247de8fcb6b82c282d98579355c5d Mon Sep 17 00:00:00 2001 From: Nicole O'Brien Date: Thu, 29 Feb 2024 16:00:17 +0000 Subject: [PATCH] feat: fetch token transfers with blockscout api (#2020) * refactor: create IBlockscoutTokenTransfer interface Co-authored-by: Mark Nardi * feat: add getTokenTransfersAddress to blockscout api * feat: fetch token transfers when fetching transactions Co-authored-by: Mark Nardi * fix: add store name --------- Co-authored-by: Mark Nardi --- .../blockscout/api/blockscout.api.ts | 28 +++++++++-- .../enums/blockscout-transaction-type.enum.ts | 7 +++ .../lib/auxiliary/blockscout/enums/index.ts | 1 + .../blockscout-address-param.interface.ts | 12 +++++ .../interfaces/blockscout-api.interface.ts | 4 +- .../interfaces/blockscout-asset.interface.ts | 4 +- ....ts => blockscout-token-info.interface.ts} | 2 +- .../blockscout-token-transfer.interface.ts | 19 +++++++ .../blockscout-transaction.interface.ts | 39 ++++----------- .../auxiliary/blockscout/interfaces/index.ts | 4 +- .../fetchAndPersistTransactionsForAccounts.ts | 50 ++++++++++++++++++- .../transactions/stores/transactions.store.ts | 47 ++++++++++++++++- .../types/persisted-transaction.type.ts | 10 +++- 13 files changed, 185 insertions(+), 42 deletions(-) create mode 100644 packages/shared/src/lib/auxiliary/blockscout/enums/blockscout-transaction-type.enum.ts create mode 100644 packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-address-param.interface.ts rename packages/shared/src/lib/auxiliary/blockscout/interfaces/{blockscout-asset-metadata.interface.ts => blockscout-token-info.interface.ts} (85%) create mode 100644 packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-token-transfer.interface.ts diff --git a/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts index b408a9f773..928eb10a7f 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts @@ -4,7 +4,13 @@ import { QueryParameters } from '@core/utils' import { BaseApi } from '@core/utils/api' import { DEFAULT_EXPLORER_URLS } from '@core/network/constants' import { SupportedNetworkId } from '@core/network/enums' -import { IBlockscoutApi, IBlockscoutAsset, IBlockscoutAssetMetadata, IBlockscoutTransaction } from '../interfaces' +import { + IBlockscoutApi, + IBlockscoutAsset, + IBlockscoutTokenInfo, + IBlockscoutTokenTransfer, + IBlockscoutTransaction, +} from '../interfaces' import { NetworkId } from '@core/network/types' interface INextPageParams { @@ -54,8 +60,8 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi { ) } - async getAssetMetadata(assetAddress: string): Promise { - const response = await this.get(`tokens/${assetAddress}`) + async getAssetMetadata(assetAddress: string): Promise { + const response = await this.get(`tokens/${assetAddress}`) if (response) { response.type = response.type.replace('-', '') as TokenStandard.Erc20 | NftStandard.Erc721 return response @@ -96,4 +102,20 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi { ) return items } + + async getTokenTransfersForAddress( + address: string, + standards?: ('ERC-20' | 'ERC-721')[], + exitFunction?: BlockscoutExitFunction + ): Promise { + const path = `addresses/${address}/token-transfers` + const items = await this.makePaginatedGetRequest( + path, + standards && standards?.length > 0 ? { type: standards } : undefined, + [], + undefined, + exitFunction + ) + return items + } } diff --git a/packages/shared/src/lib/auxiliary/blockscout/enums/blockscout-transaction-type.enum.ts b/packages/shared/src/lib/auxiliary/blockscout/enums/blockscout-transaction-type.enum.ts new file mode 100644 index 0000000000..617f3532f7 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/enums/blockscout-transaction-type.enum.ts @@ -0,0 +1,7 @@ +export enum BlockscoutTransactionType { + TokenTransfer = 'token_transfer', + ContractCreation = 'contract_creation', + ContractCall = 'contract_call', + TokenCreation = 'token_creation', + CoinTransfer = 'coin_transfer', +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/enums/index.ts b/packages/shared/src/lib/auxiliary/blockscout/enums/index.ts index e79f70a7b6..23dec3c6b5 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/enums/index.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/enums/index.ts @@ -1 +1,2 @@ export * from './blockscout-transaction-status.enum' +export * from './blockscout-transaction-type.enum' diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-address-param.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-address-param.interface.ts new file mode 100644 index 0000000000..0a88c655d4 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-address-param.interface.ts @@ -0,0 +1,12 @@ +import { IAddressTag, IWatchlistName } from './blockscout-transaction.interface' + +export interface IBlockscoutAddressParam { + hash: string + implementation_name: string + name: string + is_contract: boolean + private_tags: IAddressTag[] + watchlist_names: IWatchlistName[] + public_tags: IAddressTag[] + is_verified: boolean +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts index 46a5610528..44b66ba092 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts @@ -1,11 +1,11 @@ import { NftStandard } from '@core/nfts/enums' import { TokenStandard } from '@core/token/enums' import { IBlockscoutAsset } from './blockscout-asset.interface' -import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface' +import { IBlockscoutTokenInfo } from './blockscout-token-info.interface' import { IBlockscoutTransaction } from './blockscout-transaction.interface' export interface IBlockscoutApi { - getAssetMetadata(assetAddress: string): Promise + getAssetMetadata(assetAddress: string): Promise getAssetsForAddress( address: string, tokenStandard?: TokenStandard.Erc20 | NftStandard.Erc721 diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts index 5ebb40c5fe..d7d89b4e32 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts @@ -1,8 +1,8 @@ -import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface' +import { IBlockscoutTokenInfo } from './blockscout-token-info.interface' // snake_case returned by the API export interface IBlockscoutAsset { - token: IBlockscoutAssetMetadata + token: IBlockscoutTokenInfo token_id: string token_instance: unknown value: string diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-token-info.interface.ts similarity index 85% rename from packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts rename to packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-token-info.interface.ts index 6c67bc5929..a497f4a17c 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-token-info.interface.ts @@ -1,5 +1,5 @@ // snake_case returned by the API -export interface IBlockscoutAssetMetadata { +export interface IBlockscoutTokenInfo { address: string circulating_market_cap: string decimals: number diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-token-transfer.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-token-transfer.interface.ts new file mode 100644 index 0000000000..b1a6a9891c --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-token-transfer.interface.ts @@ -0,0 +1,19 @@ +import { IBlockscoutAddressParam } from './blockscout-address-param.interface' +import { BlockscoutTransactionType } from '../enums/blockscout-transaction-type.enum' +import { IBlockscoutTokenInfo } from './blockscout-token-info.interface' + +export interface IBlockscoutTokenTransfer { + block_hash: string + from: IBlockscoutAddressParam + log_index: string + method: string // could be an enum? + timestamp: string + to: IBlockscoutAddressParam + token: IBlockscoutTokenInfo + total: { + decimals: number + value: number + } + tx_hash: string + type: BlockscoutTransactionType.TokenTransfer +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts index 8f43b97b74..d109e59522 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts @@ -1,33 +1,24 @@ import { BlockscoutTransactionStatus } from '../enums' -import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface' +import { BlockscoutTransactionType } from '../enums/blockscout-transaction-type.enum' +import { IBlockscoutAddressParam } from './blockscout-address-param.interface' +import { IBlockscoutTokenInfo } from './blockscout-token-info.interface' interface IFee { type: 'maximum' | 'actual' value: string } -interface IAddressTag { +export interface IAddressTag { address_hash: string display_name: string label: string } -interface IWatchlistName { +export interface IWatchlistName { display_name: string label: string } -interface IAddressParam { - hash: string - implementation_name: string - name: string - is_contract: boolean - private_tags: IAddressTag[] - watchlist_names: IWatchlistName[] - public_tags: IAddressTag[] - is_verified: boolean -} - interface IDecodedInput { method_call: string method_id: string @@ -36,20 +27,12 @@ interface IDecodedInput { interface ITokenTransfer { block_hash: string - from: IAddressParam + from: IBlockscoutAddressParam log_index: string method: string timestamp: string - to: IAddressParam - token: IBlockscoutAssetMetadata -} - -enum BlockscoutTransactionType { - TokenTransfer = 'token_transfer', - ContractCreation = 'contract_creation', - ContractCall = 'contract_call', - TokenCreation = 'token_creation', - CoinTransfer = 'coin_transfer', + to: IBlockscoutAddressParam + token: IBlockscoutTokenInfo } export interface IBlockscoutTransaction { @@ -62,7 +45,7 @@ export interface IBlockscoutTransaction { confirmations: number type: number exchange_rate: string - to: IAddressParam + to: IBlockscoutAddressParam tx_burnt_fee: string max_fee_per_gas: string result: string @@ -70,11 +53,11 @@ export interface IBlockscoutTransaction { gas_price: string priority_fee: string base_fee_per_gas: string - from: IAddressParam + from: IBlockscoutAddressParam token_transfers: ITokenTransfer[] tx_types: BlockscoutTransactionType[] gas_used: string - created_contract: IAddressParam + created_contract: IBlockscoutAddressParam position: number nonce: number has_error_in_internal_txs: boolean diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts index 3a8f009cd6..5a291065bc 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts @@ -1,4 +1,6 @@ +export * from './blockscout-address-param.interface' export * from './blockscout-api.interface' export * from './blockscout-asset.interface' -export * from './blockscout-asset-metadata.interface' +export * from './blockscout-token-info.interface' +export * from './blockscout-token-transfer.interface' export * from './blockscout-transaction.interface' diff --git a/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts b/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts index a3adfb9976..4d99b4b692 100644 --- a/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts +++ b/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts @@ -1,6 +1,11 @@ -import { IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' +import { IBlockscoutTokenTransfer, IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' import { IAccountState, getAddressFromAccountForNetwork } from '@core/account' -import { addBlockscoutTransactionToPersistedTransactions, isBlockscoutTransactionPersisted } from '../stores' +import { + addBlockscoutTokenTransferToPersistedTransactions, + addBlockscoutTransactionToPersistedTransactions, + isBlockscoutTokenTransferPersisted, + isBlockscoutTransactionPersisted, +} from '../stores' import { BlockscoutApi } from '@auxiliary/blockscout/api' import { EvmNetworkId, getNetwork } from '@core/network' @@ -25,6 +30,19 @@ export async function fetchAndPersistTransactionsForAccounts( networkId, blockscoutTransactions ) + + const blockscoutTokenTransfers = await fetchBlockscoutTokenTransfersForAccount( + profileId, + account, + networkId + ) + blockscoutTokenTransfers && + addBlockscoutTokenTransferToPersistedTransactions( + profileId, + account.index, + networkId, + blockscoutTokenTransfers + ) } catch (err) { console.error(err) } @@ -57,3 +75,31 @@ async function fetchBlockscoutTransactionsForAccount( ) return transactions } + +function getTokenTransferExitFunction( + items: IBlockscoutTokenTransfer[], + profileId: string, + accountIndex: number, + networkId: EvmNetworkId +): boolean { + const lastItem = items[items.length - 1] + return lastItem ? isBlockscoutTokenTransferPersisted(profileId, accountIndex, networkId, lastItem.tx_hash) : false +} + +async function fetchBlockscoutTokenTransfersForAccount( + profileId: string, + account: IAccountState, + networkId: EvmNetworkId +): Promise { + const address = getAddressFromAccountForNetwork(account, networkId) + if (!address) { + return undefined + } + const blockscoutApi = new BlockscoutApi(networkId) + const tokenTransfers = await blockscoutApi.getTokenTransfersForAddress( + address, + undefined, + (items: IBlockscoutTokenTransfer[]) => getTokenTransferExitFunction(items, profileId, account.index, networkId) + ) + return tokenTransfers +} diff --git a/packages/shared/src/lib/core/transactions/stores/transactions.store.ts b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts index 36833c1b59..893c0e2e14 100644 --- a/packages/shared/src/lib/core/transactions/stores/transactions.store.ts +++ b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts @@ -1,4 +1,4 @@ -import { IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' +import { IBlockscoutTokenTransfer, IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' import { PersistedEvmTransaction } from '@core/activity' import { EvmNetworkId } from '@core/network' import { IChain } from '@core/network/interfaces' @@ -17,7 +17,7 @@ type PersistedTransactions = { } } -export const persistedTransactions = persistent('', {}) +export const persistedTransactions = persistent('transactions', {}) export function getPersistedTransactionsForChain( profileId: string, @@ -96,6 +96,40 @@ export function addBlockscoutTransactionToPersistedTransactions( }) } +export function addBlockscoutTokenTransferToPersistedTransactions( + profileId: string, + accountIndex: number, + networkId: EvmNetworkId, + newTokenTransfers: IBlockscoutTokenTransfer[] +): void { + persistedTransactions.update((state) => { + if (!state[profileId]) { + state[profileId] = {} + } + if (!state[profileId][accountIndex]) { + state[profileId][accountIndex] = { + [networkId]: {}, + } + } + if (!state[profileId][accountIndex][networkId]) { + state[profileId][accountIndex][networkId] = {} + } + + const _transactions = state[get(activeProfileId)][accountIndex][networkId] ?? {} + for (const tokenTransfer of newTokenTransfers) { + const existingTransaction = _transactions?.[tokenTransfer.tx_hash.toLowerCase()] + const updatedTransaction: PersistedTransaction = { + ...existingTransaction, + tokenTransfer, + } + _transactions[tokenTransfer.tx_hash.toLowerCase()] = updatedTransaction + } + state[get(activeProfileId)][accountIndex][networkId] = _transactions + + return state + }) +} + export function removePersistedTransactionsForProfile(profileId: string): void { persistedTransactions.update((state) => { delete state[profileId] @@ -111,3 +145,12 @@ export function isBlockscoutTransactionPersisted( ): boolean { return !!get(persistedTransactions)?.[profileId]?.[accountIndex]?.[networkId]?.[transactionHash]?.blockscout } + +export function isBlockscoutTokenTransferPersisted( + profileId: string, + accountIndex: number, + networkId: EvmNetworkId, + transactionHash: string +): boolean { + return !!get(persistedTransactions)?.[profileId]?.[accountIndex]?.[networkId]?.[transactionHash]?.tokenTransfer +} diff --git a/packages/shared/src/lib/core/transactions/types/persisted-transaction.type.ts b/packages/shared/src/lib/core/transactions/types/persisted-transaction.type.ts index bea7cb573b..186ed31d5b 100644 --- a/packages/shared/src/lib/core/transactions/types/persisted-transaction.type.ts +++ b/packages/shared/src/lib/core/transactions/types/persisted-transaction.type.ts @@ -1,16 +1,24 @@ -import { IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' +import { IBlockscoutTokenTransfer, IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' import { PersistedEvmTransaction } from '@core/activity' export type PersistedTransaction = | { blockscout: IBlockscoutTransaction + tokenTransfer: IBlockscoutTokenTransfer local: PersistedEvmTransaction } | { blockscout?: IBlockscoutTransaction + tokenTransfer: IBlockscoutTokenTransfer local: PersistedEvmTransaction } | { blockscout: IBlockscoutTransaction + tokenTransfer: IBlockscoutTokenTransfer + local?: PersistedEvmTransaction + } + | { + blockscout?: IBlockscoutTransaction + tokenTransfer: IBlockscoutTokenTransfer local?: PersistedEvmTransaction }