diff --git a/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts new file mode 100644 index 0000000000..29781e54e3 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts @@ -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 { + 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( + path: string, + queryParameters?: QueryParameters, + items: T[] = [], + nextPageParameters?: INextPageParams | null + ): Promise { + if (nextPageParameters === null) { + return Promise.resolve(items) + } + return this.get>(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 { + const response = await this.get(`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 { + const tokenType = standard.replace('ERC', 'ERC-') + const path = `addresses/${address}/tokens` + const response = await this.get>(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 { + const path = `addresses/${address}/transactions` + const items = await this.makePaginatedGetRequest(path) + return items + } +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/api/index.ts b/packages/shared/src/lib/auxiliary/blockscout/api/index.ts new file mode 100644 index 0000000000..9c3b613282 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/api/index.ts @@ -0,0 +1 @@ +export * from './blockscout.api' 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 new file mode 100644 index 0000000000..46a5610528 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts @@ -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 + getAssetsForAddress( + address: string, + tokenStandard?: TokenStandard.Erc20 | NftStandard.Erc721 + ): Promise + getTransactionsForAddress(address: string): Promise +} diff --git a/packages/shared/src/lib/core/network/interfaces/explorer-asset-metadata.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts similarity index 85% rename from packages/shared/src/lib/core/network/interfaces/explorer-asset-metadata.interface.ts rename to packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts index 1952408e7b..6c67bc5929 100644 --- a/packages/shared/src/lib/core/network/interfaces/explorer-asset-metadata.interface.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts @@ -1,5 +1,5 @@ // snake_case returned by the API -export interface IExplorerAssetMetadata { +export interface IBlockscoutAssetMetadata { address: string circulating_market_cap: string decimals: number 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 new file mode 100644 index 0000000000..5ebb40c5fe --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts @@ -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 +} 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 new file mode 100644 index 0000000000..fb26b6845a --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts @@ -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 // 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 +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts new file mode 100644 index 0000000000..3a8f009cd6 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts @@ -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' diff --git a/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts b/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts index 20106ade3f..a0b7ef96de 100644 --- a/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts +++ b/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts @@ -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() @@ -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) ) diff --git a/packages/shared/src/lib/core/network/classes/evm-explorer-api.class.ts b/packages/shared/src/lib/core/network/classes/evm-explorer-api.class.ts deleted file mode 100644 index 9b06c043f5..0000000000 --- a/packages/shared/src/lib/core/network/classes/evm-explorer-api.class.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NftStandard } from '@core/nfts/enums' -import { TokenStandard } from '@core/token/enums' -import { BaseApi } from '@core/utils/api' - -import { DEFAULT_EXPLORER_URLS } from '../constants' -import { IExplorerApi, IExplorerAsset, IExplorerAssetMetadata } from '../interfaces' -import { NetworkId } from '../types' - -export class EvmExplorerApi extends BaseApi implements IExplorerApi { - constructor(networkId: NetworkId) { - const explorerUrl = DEFAULT_EXPLORER_URLS[networkId] - super(`${explorerUrl}/api/v2`) - } - - 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 - } - } - - async getAssetsForAddress( - address: string, - standard: TokenStandard.Erc20 | NftStandard.Erc721 = TokenStandard.Erc20 - ): Promise { - const tokenType = standard.replace('ERC', 'ERC-') - const response = await this.get<{ items: IExplorerAsset[]; next_page_params: unknown }>( - `addresses/${address}/tokens?type=${tokenType}` - ) - if (response) { - return (response?.items ?? []).map((asset) => ({ - ...asset, - token: { - ...asset.token, - type: asset.token.type.replace('-', ''), - }, - })) - } else { - return [] - } - } -} diff --git a/packages/shared/src/lib/core/network/classes/index.ts b/packages/shared/src/lib/core/network/classes/index.ts index bbca6f250e..0710e6657d 100644 --- a/packages/shared/src/lib/core/network/classes/index.ts +++ b/packages/shared/src/lib/core/network/classes/index.ts @@ -1,3 +1,2 @@ -export * from './evm-explorer-api.class' export * from './iscp-chain.class' export * from './stardust-network.class' diff --git a/packages/shared/src/lib/core/network/interfaces/explorer-api.interface.ts b/packages/shared/src/lib/core/network/interfaces/explorer-api.interface.ts deleted file mode 100644 index b219a55a0a..0000000000 --- a/packages/shared/src/lib/core/network/interfaces/explorer-api.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NftStandard } from '@core/nfts/enums' -import { TokenStandard } from '@core/token/enums' -import { IExplorerAsset } from './explorer-asset.interface' -import { IExplorerAssetMetadata } from './explorer-asset-metadata.interface' - -export interface IExplorerApi { - getAssetMetadata(assetAddress: string): Promise - getAssetsForAddress( - address: string, - tokenStandard?: TokenStandard.Erc20 | NftStandard.Erc721 - ): Promise -} diff --git a/packages/shared/src/lib/core/network/interfaces/explorer-asset.interface.ts b/packages/shared/src/lib/core/network/interfaces/explorer-asset.interface.ts deleted file mode 100644 index 9a7c8e10c9..0000000000 --- a/packages/shared/src/lib/core/network/interfaces/explorer-asset.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IExplorerAssetMetadata } from './explorer-asset-metadata.interface' - -// snake_case returned by the API -export interface IExplorerAsset { - token: IExplorerAssetMetadata - token_id: string - token_instance: unknown - value: string -} diff --git a/packages/shared/src/lib/core/network/interfaces/index.ts b/packages/shared/src/lib/core/network/interfaces/index.ts index 71e0401773..6ff8524eba 100644 --- a/packages/shared/src/lib/core/network/interfaces/index.ts +++ b/packages/shared/src/lib/core/network/interfaces/index.ts @@ -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' diff --git a/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts b/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts index 9471b0399b..19fe0c48d5 100644 --- a/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts +++ b/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts @@ -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' @@ -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 { if (!features?.collectibles?.erc721?.enabled) { @@ -28,9 +29,9 @@ export async function checkForUntrackedNfts(account: IAccountState): Promise { const { token, value } = asset diff --git a/packages/shared/src/lib/core/tide/apis/tide.api.ts b/packages/shared/src/lib/core/tide/apis/tide.api.ts index 780a88bee4..0312139097 100644 --- a/packages/shared/src/lib/core/tide/apis/tide.api.ts +++ b/packages/shared/src/lib/core/tide/apis/tide.api.ts @@ -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' @@ -82,10 +82,8 @@ export class TideApi extends BaseApi { projectId: number, queryParams?: ProjectLeaderboardQueryParams ): Promise { - const path = `${TideApiEndpoint.Project}/${projectId}/leaderboard?${ - queryParams ? buildQueryParametersFromObject(queryParams) : '' - }` - const response = await this.get(path) + const path = `${TideApiEndpoint.Project}/${projectId}/leaderboard` + const response = await this.get(path, queryParams) return response } diff --git a/packages/shared/src/lib/core/utils/api.ts b/packages/shared/src/lib/core/utils/api.ts index bfffcaf1a8..823c0afca7 100644 --- a/packages/shared/src/lib/core/utils/api.ts +++ b/packages/shared/src/lib/core/utils/api.ts @@ -1,3 +1,6 @@ +import { QueryParameters } from './types' +import { buildQueryParametersFromObject } from './url' + interface IApiRequestOptions { disableCors?: boolean } @@ -9,15 +12,29 @@ export class BaseApi { this._baseUrl = baseUrl } - protected get(path: string, options?: IApiRequestOptions): Promise { - return this.makeRequest(path, '', options) + protected get( + path: string, + queryParameters?: QueryParameters, + options?: IApiRequestOptions + ): Promise { + return this.makeRequest(path, queryParameters, undefined, options) } - protected post(path: string, body: string, options?: IApiRequestOptions): Promise { - return this.makeRequest(path, body, options) + protected post( + path: string, + queryParameters?: QueryParameters, + body?: string, + options?: IApiRequestOptions + ): Promise { + return this.makeRequest(path, queryParameters, body, options) } - private async makeRequest(path: string, body?: string, options?: IApiRequestOptions): Promise { + private async makeRequest( + path: string, + queryParameters?: QueryParameters, + body?: string, + options?: IApiRequestOptions + ): Promise { try { const requestInit: RequestInit = { method: body ? 'POST' : 'GET', @@ -28,6 +45,10 @@ export class BaseApi { ...(body && { body }), ...(options?.disableCors && { mode: 'no-cors' }), } + if (queryParameters && Object.keys(queryParameters).length) { + const queryParametersString = buildQueryParametersFromObject(queryParameters) + path = `${path}?${queryParametersString}` + } const response = await fetch(`${this._baseUrl}/${path}`, requestInit) return (await response.json()) as T } catch (err) { diff --git a/packages/shared/src/lib/core/utils/types/api.types.ts b/packages/shared/src/lib/core/utils/types/api.types.ts new file mode 100644 index 0000000000..7fb05ab28b --- /dev/null +++ b/packages/shared/src/lib/core/utils/types/api.types.ts @@ -0,0 +1 @@ +export type QueryParameters = Record diff --git a/packages/shared/src/lib/core/utils/types/index.ts b/packages/shared/src/lib/core/utils/types/index.ts index ccaa2d5c5f..deda6f81da 100644 --- a/packages/shared/src/lib/core/utils/types/index.ts +++ b/packages/shared/src/lib/core/utils/types/index.ts @@ -1,3 +1,4 @@ +export * from './api.types' export * from './currencies.type' export * from './duration.type' export * from './exchange-rates.type' diff --git a/packages/shared/src/lib/core/utils/url.ts b/packages/shared/src/lib/core/utils/url.ts index 10dd8a7455..a0a4cf8ee7 100644 --- a/packages/shared/src/lib/core/utils/url.ts +++ b/packages/shared/src/lib/core/utils/url.ts @@ -1,4 +1,5 @@ import { stripSpaces, stripTrailingSlash } from './string' +import { QueryParameters } from './types' export function cleanUrl( url: string, @@ -23,7 +24,7 @@ export function cleanUrl( return cleanedUrl } -export function buildQueryParametersFromObject(obj: Record): string { +export function buildQueryParametersFromObject(obj: QueryParameters): string { return Object.keys(obj) .map( (key) =>