import {Injectable} from '@angular/core'; import {decodeParameter, Web3Service} from './web3.service'; import PriceOracleABI from '../abi/PriceOracleABI.json'; import ERC20ABI from '../abi/ERC20ABI.json'; import {environment} from '../../environments/environment'; import {ISymbol2Token} from './token.helper'; import {combineLatest, Observable, of} from 'rxjs'; import {catchError, map, retry, take, tap} from 'rxjs/operators'; import {fromPromise} from 'rxjs/internal-compatibility'; import {BigNumber} from 'ethers/utils'; import {MultiCallService} from './multicall.service'; import {getNumerator} from './erc20.helper'; import {TokenPriceService} from './token-price.service'; export type TokenData = { usdBalances: BigNumber[]; balances: BigNumber[]; }; @Injectable({ providedIn: 'root' }) export class TokenDataHelperService { private oracle = this.web3Service.getInstanceWithoutNetwork(PriceOracleABI); constructor( private web3Service: Web3Service, private multiCallService: MultiCallService, private tokenPriceService: TokenPriceService ) { } public getTokenBalancesAndPrices( userWalletAddress: string, tokens: ISymbol2Token ): Observable<TokenData> { const symbols = Object.keys(tokens); const addresses = symbols.map(symbol => tokens[symbol].address); const decimals = symbols.map(symbol => tokens[symbol].decimals); const token2decimals = {}; // tslint:disable-next-line:forin for (const symbol in tokens) { token2decimals[tokens[symbol].address] = tokens[symbol].decimals; } const result: TokenData = { usdBalances: [], balances: [] }; const tokenBalances$ = fromPromise(this.getBalances(userWalletAddress, addresses)); const tokenPrices$ = fromPromise(this.fetchTokenEthPricesFromOracle(addresses, decimals)); return combineLatest([ tokenBalances$, tokenPrices$, this.tokenPriceService.ethUsdPriceBN$ ]).pipe( retry(3), tap(([balances, prices, ethUsdPrice]) => { // tslint:disable-next-line:forin for (const token in balances) { const balance = new BigNumber(balances[token]); const price = new BigNumber(prices[token]).mul(ethUsdPrice).div(getNumerator(18)); const cost = balance.mul(price).div(getNumerator(token2decimals[token])); result.usdBalances.push(cost); result.balances.push(balance); } }), map(() => result), catchError((e) => { console.log(e); return of({ usdBalances: [], balances: [] }); }), take(1) ); } private async getBalances(wallet: string, tokens: string[]): Promise<{ [token: string]: string }> { const balanceOfCallData = this.web3Service.getInstanceWithoutNetwork(ERC20ABI).methods.balanceOf(wallet).encodeABI(); const callData = tokens.map((t) => ({ to: t.toLowerCase() === environment.ETH_ADDRESS.toLowerCase() ? environment.ETH_BALANCE_CONTRACT : t, data: balanceOfCallData })); const res = await this.multiCallService.callWithBatchRetry(callData, 500); const token2balance = {}; for (const [i, data] of res.entries()) { try { token2balance[tokens[i]] = decodeParameter('uint256', data).toString(); } catch (e) { console.log(`cannot decode balances for ${tokens[i]}`); token2balance[tokens[i]] = '0'; } } return token2balance; } public async fetchTokenEthPricesFromOracle(tokens: string[], decimals: number[]): Promise<{ [token: string]: string }> { return new Promise(async (resolve, reject) => { setTimeout(() => reject('prices timeout'), 50000); const tokenPrices = {}; if (tokens.length === 0) { return tokenPrices; } const dataToCall = []; for (const [i, token] of tokens.entries()) { const callData = this.oracle.methods.getRate( token, '0x0000000000000000000000000000000000000000' ).encodeABI(); dataToCall.push({ to: environment.PRICE_ORACLE_CONTRACT, data: callData }); } const response = await this.multiCallService.callWithBatchRetry(dataToCall, 20); for (const [i, res] of response.entries()) { try { if (res.length > 66) { tokenPrices[tokens[i]] = '0'; continue; } const x = new BigNumber(decodeParameter('uint256', res).toString()); tokenPrices[tokens[i]] = x.mul(getNumerator(decimals[i])).div(getNumerator(18)).toString(); } catch (e) { tokenPrices[tokens[i]] = '0'; // console.log(e.toString()); console.log('failed to fetch price for', tokens[i], 'res:', res); } } resolve(tokenPrices); }); } }