Skip to content

Commit

Permalink
revert erc20 query to debank (#152)
Browse files Browse the repository at this point in the history
* revert erc20 query to debank

* add pre and post load hook

* fix doc
  • Loading branch information
domechn authored Nov 8, 2023
1 parent a446714 commit 4a9bff5
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 74 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ But currently track3 are using polybase testnet, so your data may be lost on clo

- [ ] Cannot list assets in earn wallet in OKX
- [ ] Cannot list assets in Launchpool in Binance
- [ ] If there is no BNB in BSC for ERC20 Wallet, it will mistakenly regard the balance of ETH on the ETH chain as the balance of BNB on BSC

## Sponsor

Expand All @@ -69,7 +68,6 @@ Thanks for these platform who provide powerful APIs without API Key. Fuck API Ke

- https://blockchain.info
- https://blockcypher.com
- https://ethplorer.io
- https://debank.com
- https://dogechain.info
- https://phantom.app
Expand Down
2 changes: 2 additions & 0 deletions src/middlelayers/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ async function loadPortfoliosByConfig(config: CexConfig & TokenConfig): Promise<
const anaName = a.getAnalyzeName()
console.log("loading portfolio from ", anaName)
try {
await a.preLoad()
const portfolio = await a.loadPortfolio()
console.log("loaded portfolio from ", anaName)
await a.postLoad()
return portfolio
} catch (e) {
console.error("failed to load portfolio from ", anaName, e)
Expand Down
5 changes: 5 additions & 0 deletions src/middlelayers/datafetch/coins/btc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export class BTCAnalyzer implements Analyzer {
this.btcQueriers = [new BlockCypher(), new Blockchain()]
}

async preLoad(): Promise<void> {
}
async postLoad(): Promise<void> {
}

getAnalyzeName(): string {
return "BTC Analyzer"
}
Expand Down
5 changes: 5 additions & 0 deletions src/middlelayers/datafetch/coins/cex/cex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export class CexAnalyzer implements Analyzer {
return portfolio
}

async preLoad(): Promise<void> {
}
async postLoad(): Promise<void> {
}

async loadPortfolio(): Promise<WalletCoin[]> {
const coinLists = await bluebird.map(this.exchanges, async ex => {
const portfolio = await this.fetchTotalBalance(ex)
Expand Down
5 changes: 4 additions & 1 deletion src/middlelayers/datafetch/coins/doge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export class DOGEAnalyzer implements Analyzer {
getAnalyzeName(): string {
return "DOGE Analyzer"
}

async preLoad(): Promise<void> {
}
async postLoad(): Promise<void> {
}
private async query(address: string): Promise<number> {
for (const q of this.dogeQueriers) {
try {
Expand Down
154 changes: 87 additions & 67 deletions src/middlelayers/datafetch/coins/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,104 +4,120 @@ import { asyncMap } from '../utils/async'
import { sendHttpRequest } from '../utils/http'
import { getAddressList } from '../utils/address'
import bluebird from 'bluebird'
import { invoke } from '@tauri-apps/api'

type QueryAssetResp = {
token_list: Coin[]
data: {
amount: number,
// eth, bsc or token address
id: string
name: string
}[]
}

interface ERC20Querier {
query(address: string): Promise<Coin[]>
interface DeBank429ErrorResolver {
isTried(): boolean

clean(): void
tryResolve(address?: string): Promise<void>

resolved(): Promise<void>
}

type EthplorerResp = {
address: string
ETH: {
balance: number
}
tokens: {
tokenInfo: {
address: string
name: string
symbol: string
decimals: string
price: EthplorerPriceResp | boolean
},
balance: number
}[]
class DeBank429ErrorResolverImpl implements DeBank429ErrorResolver {

private defaultAddress: string

private tried = false

constructor() {
this.defaultAddress = "0x2170ed0880ac9a755fd29b2688956bd959f933f8"

}

isTried(): boolean {
console.debug("isTried", this.tried)

return this.tried
}

async tryResolve(address?: string): Promise<void> {
this.tried = true
await invoke("open_debank_window_in_background", {
address: address || this.defaultAddress
})
}

async resolved(): Promise<void> {
this.tried = false
console.debug("resolved 429")

await invoke("close_debank_window")
}
}

type EthplorerPriceResp = {
rate: number
currency: string
volume24h: number
interface ERC20Querier {
query(address: string): Promise<Coin[]>

clean(): void
}

class EthplorerERC20Query implements ERC20Querier {
class DeBankERC20Query implements ERC20Querier {
private mainSymbol: 'ETH' | 'BNB'
private queryUrl: string
// rate limit: 2 per second, 30/min, 200/hour, 1000/24hours, 3000/week.
private readonly accessKey = "freekey"
private readonly queryUrl = 'https://api.debank.com/token/balance_list'

// add cache in one times query to avoid retry error
private cache: { [k: string]: Coin[] } = {}

constructor(mainSymbol: 'ETH' | 'BNB', queryUrl: string) {
constructor(mainSymbol: 'ETH' | 'BNB') {
this.mainSymbol = mainSymbol
this.queryUrl = queryUrl
}

async query(address: string): Promise<Coin[]> {
if (this.cache[address]) {
return this.cache[address]
}
const url = `${this.queryUrl}/${address}?apiKey=${this.accessKey}`
const data = await sendHttpRequest<EthplorerResp>("GET", url, 5000, {
"user-agent": '',
})
if (!data) {
const chain = this.mainSymbol === 'ETH' ? 'eth' : 'bsc'

const url = `${this.queryUrl}?user_addr=${address}&chain=${chain}`
const resp = await sendHttpRequest<QueryAssetResp>("GET", url, 5000)
if (!resp) {
throw new Error("failed to query erc20 assets")
}
const mainCoin: Coin = {
symbol: this.mainSymbol,
amount: data.ETH.balance,
}

// ignore tokens without price or its price is false or its volume24h less than 1k
const resp = _(data.tokens).filter(t => t.tokenInfo.price && (t.tokenInfo.price as EthplorerPriceResp).volume24h > 1000).map(t => ({
tokenAddress: t.tokenInfo.address,
symbol: t.tokenInfo.symbol,
amount: t.balance / Math.pow(10, +t.tokenInfo.decimals),
} as Coin)).push(mainCoin).value()
const res = _(resp.data).map(d => ({
symbol: d.name,
amount: d.amount,
})).value()


this.cache[address] = resp
return resp
this.cache[address] = res
return res
}

clean(): void {
this.cache = {}
}
}

class EthERC20Query extends EthplorerERC20Query {
class EthERC20Query extends DeBankERC20Query {
constructor() {
super('ETH', "https://api.ethplorer.io/getAddressInfo")
super('ETH')
}
}

class BscERC20Query extends EthplorerERC20Query {
class BscERC20Query extends DeBankERC20Query {
constructor() {
super('BNB', "https://api.binplorer.com/getAddressInfo")
super('BNB')
}
}


export class ERC20Analyzer implements Analyzer {
private readonly config: Pick<TokenConfig, 'erc20'>
// !bsc first, because of this provider has some bug, if call eth first, balance of bsc will be the same as eth
private readonly queries = [new BscERC20Query(), new EthERC20Query()]

private readonly errorResolver: DeBank429ErrorResolver = new DeBank429ErrorResolverImpl()

constructor(config: Pick<TokenConfig, 'erc20'>) {
this.config = config

Expand All @@ -111,40 +127,44 @@ export class ERC20Analyzer implements Analyzer {
}

private async query(address: string): Promise<WalletCoin[]> {
const coins: Coin[][] = []
// currency must be 1, because of bug of ethplorer.io
for (const q of this.queries) {
const coin = await q.query(address)
coins.push(coin)
}

const coins = await bluebird.map(this.queries, async q => q.query(address))
return _(coins).flatten().map(c => ({
...c,
wallet: address
})).value()
}

async preLoad(): Promise<void> {
}
async postLoad(): Promise<void> {
for (const q of this.queries) {
q.clean()
}
}
async loadPortfolio(): Promise<WalletCoin[]> {
return this.loadPortfolioWith429Retry(5)
.finally(() => {
// clean cache in the end
this.queries.forEach(q => q.clean())
return this.loadPortfolioWith429Retry(10)
.finally(async () => {
if (this.errorResolver.isTried()) {
await this.errorResolver.resolved()
}
})
}

async loadPortfolioWith429Retry(max: number): Promise<WalletCoin[]> {
try {
if (max <= 0) {
throw new Error("failed to query erc20 assets, with max retry")
throw new Error("failed to query erc20 assets")
}
const coinLists = await asyncMap(getAddressList(this.config.erc20), async addr => this.query(addr), 2, 1000)
const coinLists = await asyncMap(getAddressList(this.config.erc20), async addr => this.query(addr), 1, 1000)

return _(coinLists).flatten().value()
} catch (e) {
if (e instanceof Error && e.message.includes("429")) {
console.error("failed to query erc20 assets due to 429, retrying...")
// sleep 2s
await new Promise(resolve => setTimeout(resolve, 2000))
if (!this.errorResolver.isTried()) {
await this.errorResolver.tryResolve(getAddressList(this.config.erc20)[0])
}
// sleep 500ms
await new Promise(resolve => setTimeout(resolve, 500))

// try again
return this.loadPortfolioWith429Retry(max - 1)
Expand Down
5 changes: 4 additions & 1 deletion src/middlelayers/datafetch/coins/others.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export class OthersAnalyzer implements Analyzer {
getAnalyzeName(): string {
return "Others Analyzer"
}

async preLoad(): Promise<void> {
}
async postLoad(): Promise<void> {
}
async loadPortfolio(): Promise<WalletCoin[]> {
return _(this.config.others).map(c => ({
symbol: c.symbol,
Expand Down
5 changes: 4 additions & 1 deletion src/middlelayers/datafetch/coins/sol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export class SOLAnalyzer implements Analyzer {
getAnalyzeName(): string {
return "SOL Analyzer"
}

async preLoad(): Promise<void> {
}
async postLoad(): Promise<void> {
}
private async query(address: string): Promise<number> {
const resp = await sendHttpRequest<{ result: { value: string } }>("POST", this.queryUrl, 5000, {},
{
Expand Down
4 changes: 2 additions & 2 deletions src/middlelayers/datafetch/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export type Coin = {
// used to query price if there are multiple coins with same symbol
tokenAddress?: string
symbol: string
amount: number
}
Expand All @@ -9,7 +7,9 @@ export type WalletCoin = Coin & { wallet: string }

export interface Analyzer {
getAnalyzeName(): string
preLoad(): Promise<void>
loadPortfolio(): Promise<WalletCoin[]>
postLoad(): Promise<void>
}

export type GlobalConfig = CexConfig & TokenConfig & {
Expand Down

0 comments on commit 4a9bff5

Please sign in to comment.