Skip to content

Commit

Permalink
feat: explorer api pagination (#2002)
Browse files Browse the repository at this point in the history
* refactor: query parameters on base api
add: make paginated request on explorer api

Co-authored-by: Tuditi <[email protected]>

* fix: paginated request logic

* refactor: move evm explorer to blockscout module

* fix: imports

* feat: add blockscout transaction interface

* fix: correct queryParameters

---------

Co-authored-by: Tuditi <[email protected]>
Co-authored-by: Tuditi <[email protected]>
Co-authored-by: Tuditi <[email protected]>
  • Loading branch information
4 people authored Feb 27, 2024
1 parent 5ec84d6 commit 70a7506
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 88 deletions.
84 changes: 84 additions & 0 deletions packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { NftStandard } from '@core/nfts/enums'
import { TokenStandard } from '@core/token/enums'
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 { NetworkId } from '@core/network/types'

interface INextPageParams {
block_number: number
index: number
items_count: number
}

interface IPaginationResponse<T> {
items: T[]
next_page_params: INextPageParams | null
}

export class BlockscoutApi extends BaseApi implements IBlockscoutApi {
constructor(networkId: NetworkId) {
const explorerUrl = DEFAULT_EXPLORER_URLS[networkId as SupportedNetworkId]
super(`${explorerUrl}/api/v2`)
}

private async makePaginatedGetRequest<T>(
path: string,
queryParameters?: QueryParameters,
items: T[] = [],
nextPageParameters?: INextPageParams | null
): Promise<T[]> {
if (nextPageParameters === null) {
return Promise.resolve(items)
}
return this.get<IPaginationResponse<T>>(path, { ...queryParameters, ...nextPageParameters }).then(
(response) => {
if (!response) {
return Promise.resolve(items)
}
return this.makePaginatedGetRequest(
path,
queryParameters,
items.concat(response.items),
response.next_page_params
)
}
)
}

async getAssetMetadata(assetAddress: string): Promise<IBlockscoutAssetMetadata | undefined> {
const response = await this.get<IBlockscoutAssetMetadata>(`tokens/${assetAddress}`)
if (response) {
response.type = response.type.replace('-', '') as TokenStandard.Erc20 | NftStandard.Erc721
return response
}
}

async getAssetsForAddress(
address: string,
standard: TokenStandard.Erc20 | NftStandard.Erc721 = TokenStandard.Erc20
): Promise<IBlockscoutAsset[]> {
const tokenType = standard.replace('ERC', 'ERC-')
const path = `addresses/${address}/tokens`
const response = await this.get<IPaginationResponse<IBlockscoutAsset>>(path, { type: tokenType })
if (response) {
return (response?.items ?? []).map((asset) => ({
...asset,
token: {
...asset.token,
type: asset.token.type.replace('-', ''),
},
}))
} else {
return []
}
}

async getTransactionsForAddress(address: string): Promise<IBlockscoutTransaction[]> {
const path = `addresses/${address}/transactions`
const items = await this.makePaginatedGetRequest<IBlockscoutTransaction>(path)
return items
}
}
1 change: 1 addition & 0 deletions packages/shared/src/lib/auxiliary/blockscout/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './blockscout.api'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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 { IBlockscoutTransaction } from './blockscout-transaction.interface'

export interface IBlockscoutApi {
getAssetMetadata(assetAddress: string): Promise<IBlockscoutAssetMetadata | undefined>
getAssetsForAddress(
address: string,
tokenStandard?: TokenStandard.Erc20 | NftStandard.Erc721
): Promise<IBlockscoutAsset[]>
getTransactionsForAddress(address: string): Promise<IBlockscoutTransaction[]>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// snake_case returned by the API
export interface IExplorerAssetMetadata {
export interface IBlockscoutAssetMetadata {
address: string
circulating_market_cap: string
decimals: number
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface'

// snake_case returned by the API
export interface IBlockscoutAsset {
token: IBlockscoutAssetMetadata
token_id: string
token_instance: unknown
value: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface'

interface IFee {
type: string
value: string
}

interface IAddressTag {
address_hash: string
display_name: string
label: string
}

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
parameters: Record<string, string> // IDecodedInputParameters
}

interface ITokenTransfer {
block_hash: string
from: IAddressParam
log_index: string
method: string
timestamp: string
to: IAddressParam
token: IBlockscoutAssetMetadata
}

export interface IBlockscoutTransaction {
timestamp: string
fee: IFee
gas_limit: number
block: number
status: string // e.g ok | error
method: string // e.g transferFrom
confirmations: number
type: number
exchange_rate: string
to: IAddressParam
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
token_transfers: ITokenTransfer[]
tx_types: string[]
gas_used: string
created_contract: IAddressParam
position: number
nonce: number
has_error_in_internal_txs: boolean
actions: unknown // TransactionAction
decoded_input: IDecodedInput
token_transfers_overflow: boolean
raw_input: string
value: string
max_priority_fee_per_gas: string
revert_reason: string
confirmation_duration: string
tx_tag: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './blockscout-api.interface'
export * from './blockscout-asset.interface'
export * from './blockscout-asset-metadata.interface'
export * from './blockscout-transaction.interface'
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { IAccountState } from '@core/account/interfaces'
import { EvmExplorerApi, EvmNetworkId } from '@core/network'
import { EvmNetworkId } from '@core/network'
import { getNetwork } from '@core/network/stores'
import { TokenStandard, TokenTrackingStatus } from '@core/token'
import { addNewTrackedTokenToActiveProfile, hasTokenBeenUntracked } from '@core/wallet/actions'
import { BASE_TOKEN_CONTRACT_ADDRESS } from '../constants'
import { BlockscoutApi } from '@auxiliary/blockscout/api'

export function checkForUntrackedTokens(account: IAccountState, addPreviouslyUntracked?: boolean): void {
const chains = getNetwork()?.getChains()
Expand All @@ -14,9 +15,9 @@ export function checkForUntrackedTokens(account: IAccountState, addPreviouslyUnt
return
}
const networkId = chain.getConfiguration().id
const explorerApi = new EvmExplorerApi(networkId)
const blockscoutApi = new BlockscoutApi(networkId)

const tokens = await explorerApi.getAssetsForAddress(evmAddress)
const tokens = await blockscoutApi.getAssetsForAddress(evmAddress)
const untrackedTokensToTrack = tokens.filter(
({ token }) => addPreviouslyUntracked || !hasTokenBeenUntracked(token.address.toLowerCase(), networkId)
)
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion packages/shared/src/lib/core/network/classes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './evm-explorer-api.class'
export * from './iscp-chain.class'
export * from './stardust-network.class'

This file was deleted.

This file was deleted.

3 changes: 0 additions & 3 deletions packages/shared/src/lib/core/network/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ export * from './chain.interface'
export * from './client-options.interface'
export * from './connected-chain.interface'
export * from './evm-addresses.interface'
export * from './explorer-api.interface'
export * from './explorer-asset-metadata.interface'
export * from './explorer-asset.interface'
export * from './gas-fee-policy.interface'
export * from './gas-limits.interface'
export * from './network-status.interface'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { IAccountState } from '@core/account/interfaces'
import { ContractType } from '@core/layer-2/enums'
import { EvmExplorerApi } from '@core/network/classes'
import { getNetwork } from '@core/network/stores'
import { IChain, IExplorerAsset } from '@core/network/interfaces'
import { IChain } from '@core/network/interfaces'
import features from '@features/features'

import { NftStandard } from '../enums'
Expand All @@ -13,6 +12,8 @@ import { addNftsToDownloadQueue } from './addNftsToDownloadQueue'
import { Nft } from '../interfaces'
import { addNewTrackedNftToActiveProfile } from './addNewTrackedNftToActiveProfile'
import { TokenTrackingStatus } from '@core/token'
import { IBlockscoutAsset } from '@auxiliary/blockscout/interfaces'
import { BlockscoutApi } from '@auxiliary/blockscout/api'

export async function checkForUntrackedNfts(account: IAccountState): Promise<void> {
if (!features?.collectibles?.erc721?.enabled) {
Expand All @@ -28,9 +29,9 @@ export async function checkForUntrackedNfts(account: IAccountState): Promise<voi
return
}
const networkId = chain.getConfiguration().id
const explorerApi = new EvmExplorerApi(networkId)
const blockscoutApi = new BlockscoutApi(networkId)

const explorerNfts = await explorerApi.getAssetsForAddress(evmAddress, NftStandard.Erc721)
const explorerNfts = await blockscoutApi.getAssetsForAddress(evmAddress, NftStandard.Erc721)
for (const explorerNft of explorerNfts) {
await persistNftsFromExplorerAsset(account, evmAddress, explorerNft, chain)
}
Expand All @@ -40,7 +41,7 @@ export async function checkForUntrackedNfts(account: IAccountState): Promise<voi
async function persistNftsFromExplorerAsset(
account: IAccountState,
evmAddress: string,
asset: IExplorerAsset,
asset: IBlockscoutAsset,
chain: IChain
): Promise<void> {
const { token, value } = asset
Expand Down
8 changes: 3 additions & 5 deletions packages/shared/src/lib/core/tide/apis/tide.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { INftAttribute } from '@core/nfts'
import { BaseApi, buildQueryParametersFromObject } from '@core/utils'
import { BaseApi } from '@core/utils'
import { TIDE_API_BASE_URL } from '../constants'
import { TideApiEndpoint } from '../enums'
import { ITideLeaderboardItem, ITideUserPosition } from '../interfaces'
Expand Down Expand Up @@ -82,10 +82,8 @@ export class TideApi extends BaseApi {
projectId: number,
queryParams?: ProjectLeaderboardQueryParams
): Promise<IProjectLeaderboardResponse | undefined> {
const path = `${TideApiEndpoint.Project}/${projectId}/leaderboard?${
queryParams ? buildQueryParametersFromObject(queryParams) : ''
}`
const response = await this.get<IProjectLeaderboardResponse>(path)
const path = `${TideApiEndpoint.Project}/${projectId}/leaderboard`
const response = await this.get<IProjectLeaderboardResponse>(path, queryParams)
return response
}

Expand Down
Loading

0 comments on commit 70a7506

Please sign in to comment.