Skip to content

Commit

Permalink
feat: fetch token transfers with blockscout api (#2020)
Browse files Browse the repository at this point in the history
* refactor: create IBlockscoutTokenTransfer interface

Co-authored-by: Mark Nardi <[email protected]>

* feat: add getTokenTransfersAddress to blockscout api

* feat: fetch token transfers when fetching transactions

Co-authored-by: Mark Nardi <[email protected]>

* fix: add store name

---------

Co-authored-by: Mark Nardi <[email protected]>
  • Loading branch information
nicole-obrien and MarkNerdi authored Feb 29, 2024
1 parent 17ff5e6 commit 6201493
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 42 deletions.
28 changes: 25 additions & 3 deletions packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -54,8 +60,8 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi {
)
}

async getAssetMetadata(assetAddress: string): Promise<IBlockscoutAssetMetadata | undefined> {
const response = await this.get<IBlockscoutAssetMetadata>(`tokens/${assetAddress}`)
async getAssetMetadata(assetAddress: string): Promise<IBlockscoutTokenInfo | undefined> {
const response = await this.get<IBlockscoutTokenInfo>(`tokens/${assetAddress}`)
if (response) {
response.type = response.type.replace('-', '') as TokenStandard.Erc20 | NftStandard.Erc721
return response
Expand Down Expand Up @@ -96,4 +102,20 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi {
)
return items
}

async getTokenTransfersForAddress(
address: string,
standards?: ('ERC-20' | 'ERC-721')[],
exitFunction?: BlockscoutExitFunction<IBlockscoutTokenTransfer>
): Promise<IBlockscoutTokenTransfer[]> {
const path = `addresses/${address}/token-transfers`
const items = await this.makePaginatedGetRequest<IBlockscoutTokenTransfer>(
path,
standards && standards?.length > 0 ? { type: standards } : undefined,
[],
undefined,
exitFunction
)
return items
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum BlockscoutTransactionType {
TokenTransfer = 'token_transfer',
ContractCreation = 'contract_creation',
ContractCall = 'contract_call',
TokenCreation = 'token_creation',
CoinTransfer = 'coin_transfer',
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './blockscout-transaction-status.enum'
export * from './blockscout-transaction-type.enum'
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<IBlockscoutAssetMetadata | undefined>
getAssetMetadata(assetAddress: string): Promise<IBlockscoutTokenInfo | undefined>
getAssetsForAddress(
address: string,
tokenStandard?: TokenStandard.Erc20 | NftStandard.Erc721
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// snake_case returned by the API
export interface IBlockscoutAssetMetadata {
export interface IBlockscoutTokenInfo {
address: string
circulating_market_cap: string
decimals: number
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand All @@ -62,19 +45,19 @@ 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
hash: string
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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)
}
Expand Down Expand Up @@ -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<IBlockscoutTokenTransfer[] | undefined> {
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
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -17,7 +17,7 @@ type PersistedTransactions = {
}
}

export const persistedTransactions = persistent<PersistedTransactions>('', {})
export const persistedTransactions = persistent<PersistedTransactions>('transactions', {})

export function getPersistedTransactionsForChain(
profileId: string,
Expand Down Expand Up @@ -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]
Expand All @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 6201493

Please sign in to comment.