Skip to content

Commit

Permalink
Merge pull request #15 from RootstockCollective/DAO-741
Browse files Browse the repository at this point in the history
DAO-741 Implemented fetch token holders endpoint from blockscout
  • Loading branch information
sleyter93 authored Oct 23, 2024
2 parents 8cf09f4 + 5e4f28d commit ca6d8dd
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 9 deletions.
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@rsksmart/rsk-contract-metadata": "^1.0.15",
"@rsksmart/rsk-utils": "^1.1.0",
"axios": "^1.6.2",
"axios-cache-interceptor": "^1.6.2",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"ethers": "^6.13.1",
Expand Down
196 changes: 196 additions & 0 deletions src/api/openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ module.exports = {
url: 'http://localhost:3000/',
description: 'Local server'
},
{
url: 'https://dev.rws.app.rootstockcollective.xyz',
description: 'DAO TestNet'
},
{
url: 'https://rws.app.rootstockcollective.xyz',
description: 'DAO TestNet'
Expand Down Expand Up @@ -674,6 +678,77 @@ module.exports = {
}
}
},
'/address/{address}/holders': {
get: {
summary: 'Get Token holders that belong to an address by chainId',
tags: [
'Tokens'
],
parameters: [
{
name: 'address',
in: 'path',
required: true,
description: 'NFT address',
schema: {
type: 'string'
},
example: '0xa3076bcaCc7112B7fa7c5A87CF32275296d85D64'
},
{
name: 'chainId',
in: 'query',
description: 'Chain Id identifies the network',
required: false,
schema: {
type: 'string',
default: '31'
},
examples: {
'RSK Testnet': {
value: '31'
},
'RSK Mainnet': {
value: '30'
}
}
}
],
responses: {
200: {
description: 'successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: {
type: 'array',
items: {
$ref: '#/components/schemas/TokenHolder'
}
},
next_page_params: {
$ref: '#/components/schemas/NextPage'
}
}
}
}
}
},
400: {
description: 'Validation Error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ValidationError'
}
}
}
}
}
}
},
'/price': {
get: {
summary: 'Get token prices',
Expand Down Expand Up @@ -1751,6 +1826,127 @@ module.exports = {
}
}
},
TokenHolder: {
type: 'object',
properties: {
address: {
$ref: '#/components/schemas/AddressParam'
},
value: {
type: 'string',
example: '10000'
},
token_id: {
type: 'string',
example: '10000'
},
token: {
$ref: '#/components/schemas/TokenInfo'
}
}
},
AddressParam: {
type: 'object',
properties: {
hash: {
type: 'string',
example: '0xEb533ee5687044E622C69c58B1B12329F56eD9ad'
},
implementation_name: {
type: 'string',
example: 'implementationName'
},
name: {
type: 'string',
example: 'contractName'
},
is_contract: {
type: 'boolean'
},
private_tags: {
type: 'array',
items: {
$ref: '#/components/schemas/AddressTag'
}
},
watchlist_names: {
type: 'array',
items: {
$ref: '#/components/schemas/WatchlistName'
}
},
public_tags: {
type: 'array',
items: {
$ref: '#/components/schemas/AddressTag'
}
},
is_verified: {
type: 'boolean'
}
}
},
TokenInfo: {
type: 'object',
properties: {
circulating_market_cap: {
type: 'string',
example: '83606435600.3635'
},
icon_url: {
type: 'string',
example: 'https://raw.githubusercontent.com/trustwallet/assets/master' +
'/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png'
},
name: {
type: 'string',
example: 'Tether USD'
},
decimals: {
type: 'string',
example: '6'
},
symbol: {
type: 'string',
example: 'USDT'
},
address: {
type: 'string',
example: '0x394c399dbA25B99Ab7708EdB505d755B3aa29997'
},
type: {
type: 'string',
example: 'ERC-20'
},
holders: {
type: 'string',
example: '837494234523'
},
exchange_rate: {
type: 'string',
example: '0.99'
},
total_supply: {
type: 'string',
example: '10000000'
}
}
},
NextPage: {
type: 'object',
properties: {
address_hash: {
type: 'string'
},
items_count: {
type: 'integer'
},
value: {
type: 'integer'
}
}
},

ValidationError: {
type: 'object',
properties: {
Expand Down
43 changes: 38 additions & 5 deletions src/blockscoutApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ 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'
import { AxiosCacheInstance, setupCache } from 'axios-cache-interceptor'

export class BlockscoutAPI extends DataSource {
private chainId: number
private axiosCache: AxiosCacheInstance
private errorHandling = (e) => {
console.error(e)
return []
Expand All @@ -24,6 +29,10 @@ export class BlockscoutAPI extends DataSource {
constructor (apiURL: string, chainId: number, axios: typeof _axios, id: string) {
super(apiURL, id, axios)
this.chainId = chainId
this.axiosCache = setupCache(_axios.create(), {
ttl: 1000 * 60,
interpretHeader: false
})
}

getTokens () {
Expand Down Expand Up @@ -173,11 +182,35 @@ export class BlockscoutAPI extends DataSource {
.catch(() => [])
}

async getNftHoldersData ({ address, nextPageParams }: GetNftHoldersData) {
async getTokenHoldersByAddress ({ address, nextPageParams }: GetTokenHoldersByAddress) {
try {
const url = `${this.url}/v2/tokens/${address}/holders`
const response = await this.axiosCache.get<ServerResponseV2<TokenHoldersResponse>>(url,
{ params: nextPageParams, validateStatus: (status) => status <= 500 })
if (response?.status === 200) {
return response.data
}
return {
items: [],
next_page_params: null,
error: `Blockscout error with status ${response?.status}`
}
} catch (error) {
console.error(typeof error, error)
return {
items: [],
next_page_params: null,
error: 'Blockscout error'
}
}
}

async getNftInstancesByAddress ({ address, nextPageParams }: GetNftHoldersData) {
const url = `${this.url}/v2/tokens/${address.toLowerCase()}/instances`
try {
const response = await this.axios?.get<ServerResponseV2<NftTokenHoldersResponse>>(url, {
params: nextPageParams
params: nextPageParams,
validateStatus: (status) => status <= 500
})

if (response?.status === 200) {
Expand All @@ -194,7 +227,7 @@ export class BlockscoutAPI extends DataSource {
error: `Blockscout error with status ${response?.status}`
}
} catch (error) {
console.log(typeof error, error)
console.error(typeof error, error)
throw new Error(`Failed to get NFT holders data: ${error instanceof Error ? error.message : String(error)}`)
};
}
Expand Down
Loading

0 comments on commit ca6d8dd

Please sign in to comment.