Skip to content
Open

Qa #587

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
import { Injectable, Logger } from '@nestjs/common'
import { lastValueFrom, map } from 'rxjs'

import { BalanceService } from '@app/network-service/balances/interfaces/balances.interface'
import { ConfigService } from '@nestjs/config'
import GraphQLService from '@app/common/services/graphql.service'
import { HttpService } from '@nestjs/axios'
import { NATIVE_FUSE_TOKEN } from '@app/smart-wallets-service/common/constants/fuseTokenInfo'
import { ethers } from 'ethers'
import { ethers, Contract } from 'ethers'
import { getCollectiblesByOwner } from '@app/network-service/common/constants/graph-queries/nfts-v3'
import { isEmpty } from 'lodash'
import { ExplorerServiceCollectibleResponse, ExplorerServiceGraphQLVariables, ExplorerServiceTransformedCollectible } from '../interfaces/balances.interface'
import MultiCallAbi from '@app/network-service/common/constants/abi/MultiCall'
import Erc20Abi from '@app/network-service/common/constants/abi/Erc20.json'
import { getERC20TokensQuery } from '@app/network-service/common/constants/graph-queries/erc20'

@Injectable()
export class ExplorerService implements BalanceService {
private readonly logger = new Logger(ExplorerService.name)
constructor (
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly graphQLService: GraphQLService
) { }

get explorerBaseUrl () {
return this.configService.get('explorer.baseUrl')
}

get explorerApiKey () {
return this.configService.get('explorer.apiKey')
}

get nftGraphUrl () {
return this.configService.get('nftGraphUrl')
}
Expand All @@ -36,6 +27,14 @@ export class ExplorerService implements BalanceService {
return this.configService.get('rpcConfig.rpc.url')
}

get erc20SubgraphUrl () {
return this.configService.get('erc20SubgraphUrl')
}

get multiCallAddress () {
return this.configService.get('multiCallAddress')
}

private async getNativeTokenBalance (address: string) {
const provider = new ethers.providers.JsonRpcProvider(this.rpcUrl)
const balance = await provider.getBalance(address)
Expand All @@ -56,20 +55,83 @@ export class ExplorerService implements BalanceService {
]
}

private async fetchAllTokensFromSubgraph (address: string) {
const PAGE_SIZE = 100
const allBalances: any[] = []
let skip = 0

while (true) {
const subgraphData = await this.graphQLService.fetchFromGraphQL(
this.erc20SubgraphUrl,
getERC20TokensQuery,
{ address: address.toLowerCase(), first: PAGE_SIZE, skip }
)

const accounts = subgraphData?.data?.accounts || []
if (accounts.length === 0 || !accounts[0]?.balances?.length) {
break
}

const balances = accounts[0].balances
allBalances.push(...balances)

if (balances.length < PAGE_SIZE) {
break
}
skip += PAGE_SIZE
}

return allBalances
}

async getERC20TokenBalances (address: string) {
const nativeTokenBalance = await this.getNativeTokenBalance(address)
const observable = this.httpService
.get(`${this.explorerBaseUrl}?module=account&action=tokenlist&address=${address}&apikey=${this.explorerApiKey}`)
.pipe(map(res => res.data))
const data = await lastValueFrom(observable)

const erc20Tokens = data.result.filter((token: any) => token.type === 'ERC-20')
// Fetch all tokens from ERC20 subgraph (paginated)
const tokenBalances = await this.fetchAllTokensFromSubgraph(address)

return {
message: data.message,
result: [...nativeTokenBalance, ...erc20Tokens],
status: data.status
if (tokenBalances.length === 0) {
if (nativeTokenBalance.length === 0) {
return { message: 'No tokens found', result: [], status: '0' }
}
return { message: 'OK', result: [...nativeTokenBalance], status: '1' }
}

const tokenAddresses = tokenBalances.map((b: any) => b.token.id)

// Use multicall to batch balanceOf calls
const provider = new ethers.providers.JsonRpcProvider(this.rpcUrl)
const multiCallContract = new Contract(this.multiCallAddress, MultiCallAbi, provider)
const erc20Interface = new ethers.utils.Interface(Erc20Abi)

const encodedCalls = tokenAddresses.map((tokenAddress: string) => ({
target: tokenAddress,
callData: erc20Interface.encodeFunctionData('balanceOf', [address])
}))

const [, results] = await multiCallContract.aggregate(encodedCalls)

// Map results and filter out zero balances
const erc20Tokens = tokenBalances
.map((tokenBalance: any, index: number) => {
const balance = erc20Interface.decodeFunctionResult('balanceOf', results[index])[0]
return {
balance: balance.toString(),
contractAddress: tokenBalance.token.id.toLowerCase(),
decimals: tokenBalance.token.decimals.toString(),
name: tokenBalance.token.name,
symbol: tokenBalance.token.symbol,
type: 'ERC-20'
}
})
.filter((token: any) => token.balance !== '0')

const allTokens = [...nativeTokenBalance, ...erc20Tokens]
if (allTokens.length === 0) {
return { message: 'No tokens found', result: [], status: '0' }
}

return { message: 'OK', result: allTokens, status: '1' }
}

async getERC721TokenBalances (address: string, limit?: number, cursor?: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default () => ({
blockGraphUrl: 'https://gateway-arbitrum.network.thegraph.com/api/47700e2a17b911be5b2186cf496a6737/subgraphs/id/4NdGNtBYVAuWriUfcb58vLmiaendp7v8EQ9tGe3i1RPo',
voltageGraphUrl: 'https://gateway-arbitrum.network.thegraph.com/api/47700e2a17b911be5b2186cf496a6737/subgraphs/id/4buFyoUT8Lay3T1DK9ctdMdcpkZMdi5EpCBWZCBTKvQd',
nftGraphUrl: 'https://gateway.thegraph.com/api/47700e2a17b911be5b2186cf496a6737/subgraphs/id/GE83YjCJaNbsKnSATYhKCFHwkcVWnT7VNfa5rcqdDuBd',
erc20SubgraphUrl: 'https://gateway.thegraph.com/api/47700e2a17b911be5b2186cf496a6737/subgraphs/id/FJc4XambRrE9iEiddypzgWjpaa3DkpTS5tkpypZqSdvx',
accountAbstractionGraphUrl: 'https://gateway-arbitrum.network.thegraph.com/api/47700e2a17b911be5b2186cf496a6737/subgraphs/id/hmmXWtoJqnvYaQKrBjXzPzwiXksVHoGrTZGrDi4FRtL',
voltageV2GraphUrl: 'https://gateway-arbitrum.network.thegraph.com/api/550967d6d70d7fce0a710f38dc7bc5df/subgraphs/id/B4BGk9itvmRXzzNRAzBWwQARHRt3ZvLz11aWNVsZPT4',
liquidStakingFuseGraphUrl: 'https://gateway-arbitrum.network.thegraph.com/api/3f81974147b5b63470524ed08206e24e/subgraphs/id/7FQVAoYfsrYPAVzaHnky1rHGYjXj2hcw3yokeLQmpntp',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { gql } from 'graphql-request'

export const getERC20TokensQuery = gql`
query getAccountBalances($address: ID!, $first: Int!, $skip: Int!) {
accounts(where: {id: $address}) {
balances(first: $first, skip: $skip) {
token {
decimals
name
id
symbol
}
amount
}
}
}
`
Loading