diff --git a/src/blockscoutApi/index.ts b/src/blockscoutApi/index.ts index b84598c..f4b1474 100644 --- a/src/blockscoutApi/index.ts +++ b/src/blockscoutApi/index.ts @@ -6,13 +6,13 @@ import { TokenInfoResponse, TokenServerResponse, TokenTransferApi, TransactionServerResponse, TransactionsServerResponse, BlockscoutTransactionResponseTxResult, - NftTokenHoldersResponse + NftTokenHoldersResponse, TokenHoldersResponse } from './types' import { fromApiToInternalTransaction, fromApiToNft, fromApiToNftOwner, fromApiToRtbcBalance, fromApiToTEvents, fromApiToTokenWithBalance, fromApiToTokens, fromApiToTransaction, transformResponseToNftHolder } from './utils' -import { GetEventLogsByAddressAndTopic0, GetNftHoldersData } from '../service/address/AddressService' +import { GetEventLogsByAddressAndTopic0, GetNftHoldersData, GetTokenHoldersByAddress } from '../service/address/AddressService' export class BlockscoutAPI extends DataSource { private chainId: number @@ -173,6 +173,29 @@ export class BlockscoutAPI extends DataSource { .catch(() => []) } + async getTokenHoldersByAddress ({ address, nextPageParams }: GetTokenHoldersByAddress) { + try { + const url = `${this.url}/v2/tokens/${address}/holders` + const response = await this.axios?.get>(url, { params: nextPageParams }) + if (response?.status === 200) { + return response.data + } + return { + items: [], + next_page_params: null, + error: `Blockscout error with status ${response?.status}` + } + } catch (error) { + console.log(typeof error, error) + // @TODO handle error + return { + items: [], + next_page_params: null, + error: 'Blockscout error' + } + } + } + async getNftHoldersData ({ address, nextPageParams }: GetNftHoldersData) { const url = `${this.url}/v2/tokens/${address.toLowerCase()}/instances` try { diff --git a/src/blockscoutApi/types.ts b/src/blockscoutApi/types.ts index f8d35a6..bc6e1ae 100644 --- a/src/blockscoutApi/types.ts +++ b/src/blockscoutApi/types.ts @@ -324,3 +324,37 @@ export interface NftTokenHoldersTransformedResponse { name: string; } } +export interface TokenHolderAddress { + ens_domain_name: string; + hash: string; + implementations: any[]; + is_contract: boolean; + is_verified: boolean; + metadata: null; + name: null; + private_tags: any[]; + proxy_type: null; + public_tags: any[]; + watchlist_names: any[]; +} + +export interface TokenHolderToken { + address: string; + circulating_market_cap: null; + decimals: string; + exchange_rate: null; + holders: string; + icon_url: null; + name: string; + symbol: string; + total_supply: string; + type: string; + volume_24h: null; +} + +export interface TokenHoldersResponse { + address: TokenHolderAddress; + token: Token; + token_id: null; + value: string; +} diff --git a/src/controller/httpsAPI.ts b/src/controller/httpsAPI.ts index 60f3050..1eafb26 100644 --- a/src/controller/httpsAPI.ts +++ b/src/controller/httpsAPI.ts @@ -193,6 +193,25 @@ export class HttpsAPI { } }) + this.app.get('/address/:address/holders', + async ({ params: { address }, query: { chainId = '31', ...rest } } : Request, res: Response, + nextFunction: NextFunction) => { + try { + chainIdSchema.validateSync({ chainId }) + addressSchema.validateSync({ address }) + const result = await this.addressService + .getTokenHoldersByAddress({ + chainId: chainId as string, + address: address as string, + ...rest + }) + .catch(nextFunction) + return this.responseJsonOk(res)(result) + } catch (e) { + this.handleValidationError(e, res) + } + }) + this.app.get( '/price', async (req: Request<{}, {}, {}, PricesQueryParams>, res: Response) => { diff --git a/src/repository/DataSource.ts b/src/repository/DataSource.ts index be3049c..3d0f9f0 100644 --- a/src/repository/DataSource.ts +++ b/src/repository/DataSource.ts @@ -1,7 +1,8 @@ import _axios from 'axios' import { ethers } from 'ethers' import BitcoinCore from '../service/bitcoin/BitcoinCore' -import { GetEventLogsByAddressAndTopic0, GetNftHoldersData } from '../service/address/AddressService' +import { GetEventLogsByAddressAndTopic0, GetNftHoldersData, + GetTokenHoldersByAddress } from '../service/address/AddressService' export abstract class DataSource { readonly url: string @@ -32,6 +33,7 @@ export abstract class DataSource { Omit); abstract getNftHoldersData({ address }: Omit); + abstract getTokenHoldersByAddress({ address }: Omit) } export type RSKDatasource = { diff --git a/src/rskExplorerApi/index.ts b/src/rskExplorerApi/index.ts index 0728159..716342c 100644 --- a/src/rskExplorerApi/index.ts +++ b/src/rskExplorerApi/index.ts @@ -144,7 +144,11 @@ export class RSKExplorerAPI extends DataSource { throw new Error('Feature not supported') } - getNftHoldersData () { + getNftHoldersData () { + throw new Error('Feature not supported') + } + + getTokenHoldersByAddress () { throw new Error('Feature not supported') } } diff --git a/src/service/address/AddressService.ts b/src/service/address/AddressService.ts index ab167c9..a6eb8fe 100644 --- a/src/service/address/AddressService.ts +++ b/src/service/address/AddressService.ts @@ -43,6 +43,7 @@ export interface GetEventLogsByAddressAndTopic0 { } export interface GetNftHoldersData { address: string, nextPageParams?: NextPageParams, chainId: string } +export interface GetTokenHoldersByAddress { address: string, nextPageParams?: NextPageParams, chainId: string } type GetBalancesTransactionsPricesByAddress = { chainId: string @@ -160,4 +161,9 @@ export class AddressService { const dataSource = this.dataSourceMapping[chainId] return dataSource.getNftHoldersData(rest) } + + async getTokenHoldersByAddress ({ chainId, ...rest }: GetTokenHoldersByAddress) { + const dataSource = this.dataSourceMapping[chainId] + return dataSource.getTokenHoldersByAddress(rest) + } }