import di from 'a-di'; import alot from 'alot'; import memd from 'memd'; import { Web3Client } from '@dequanto/clients/Web3Client'; import { $bigint } from '@dequanto/utils/$bigint'; import { ITokenBase } from '@dequanto/models/IToken'; import { ContractReader } from '@dequanto/contracts/ContractReader'; import { IOracle, IOracleResult, ISwapOptions } from '../IOracle'; import { TResultAsync } from '@dequanto/models/TResult'; import { ChainlinkFeedProvider, IChainlinkFeedInfo } from './ChainlinkFeedProvider'; import { $require } from '@dequanto/utils/$require'; import { TPlatform } from '@dequanto/models/TPlatform'; import { Web3ClientFactory } from '@dequanto/clients/Web3ClientFactory'; import { $cache } from '@dequanto/utils/$cache'; const CACHE_PATH = $cache.file(`chainlink-feeds.json`); export class ChainlinkOracle implements IOracle { protected feeds = di.resolve(ChainlinkFeedProvider) protected abi = { latestAnswer: `latestAnswer(): uint256`, latestTimestamp: `latestTimestamp(): uint256`, latestRoundData: `latestRoundData(): (uint256 roundId, uint256 answer, uint256 startedAt, uint256 updatedAt)`, decimals: `decimals(): uint64`, description: `description(): string`, } public constructor (private clients?: Web3Client[]) { } public async getPrice(token: ITokenBase, opts?: ISwapOptions): TResultAsync { opts ??= {}; $require.notNull(token.symbol, `Chainlink gets the feed by token's symbol, but it is empty`); let route = await this.feeds.getRouteForSymbol(token.symbol); if (route == null || route.length === 0) { return { error: new Error(`Chainlink feeds not found for ${token.symbol} to get the USD price`) } } let hops = await alot(route).mapAsync(async hop => { let [ price, config, ] = await Promise.all([ this.price(hop), this.config(hop) ]); return { price, config } }).toArrayAsync(); let { amountEth: price } = hops.reduce((prev, hop) => { let { price, config } = hop; let amountOut = $bigint.multWithFloat(price.answer, prev.amountEth); let amountEth = $bigint.toEther(amountOut, config.decimals); return { amountEth }; }, { amountEth: 1 }) let date = alot(hops).min(x => x.price.updatedAt); return { result: { quote: { symbol: 'USD' }, price, date, } }; } @memd.deco.memoize({ perInstance: true, trackRef: true, key: (ctx, feed: IChainlinkFeedInfo) => { let self = ctx.this as ChainlinkOracle; let key = `cl_feed_${feed.address}`; return key; }, persistence: new memd.FsTransport({ path: CACHE_PATH }) }) private async config (feed: IChainlinkFeedInfo) { let reader = this.getReader(feed.platform) let feedAddress = feed.address; let [ decimals, description ] = await Promise.all([ reader.readAsync (feedAddress, this.abi.decimals), reader.readAsync (feedAddress, this.abi.description) ]); return { decimals, description }; } @memd.deco.memoize({ maxAge: 60 /* minute */}) private async price (feed: IChainlinkFeedInfo) { let reader = this.getReader(feed.platform) let feedAddress = feed.address; try { let { updatedAt, answer } = await reader.readAsync<{ updatedAt: bigint, answer: bigint }>(feedAddress, this.abi.latestRoundData); return { answer: answer, updatedAt: new Date( Number(updatedAt) * 1000 ), }; } catch (error) { // skip error, and try to check another old ABI } let [ answer, updatedAt ] = await Promise.all([ reader.readAsync (feedAddress, this.abi.latestAnswer), reader.readAsync (feedAddress, this.abi.latestTimestamp) ]); return { answer: answer, updatedAt: new Date( Number(updatedAt) * 1000 ), }; } @memd.deco.memoize() private getReader(platform: TPlatform) { let c = this.getClient(platform); return new ContractReader(c); } private getClient(platform: TPlatform) { let c = this.clients?.find(x => x.platform === platform); if (c) { return c; } c = Web3ClientFactory.get(platform); return c; } }