import { getProvider, Chain } from "../general"; import fetch from "node-fetch"; import rawTokenList from "./tokenList"; import type { Address } from "../types"; import { utils } from "ethers"; import type { Log } from "@ethersproject/abstract-provider"; import { symbol, decimals } from "../erc20"; interface TimestampBlock { number: number; timestamp: number; } const kavaBlockProvider = { getBlock: async (height: number | "latest") => fetch(`https://api.data.kava.io/blocks/${height}`) .then((res) => res.json()) .then((block) => ({ number: Number(block.block.header.height), timestamp: Math.round(Date.parse(block.block.header.time) / 1000), })), }; const terraBlockProvider = { getBlock: async (height: number | "latest") => fetch(`https://lcd.terra.dev/blocks/${height}`) .then((res) => res.json()) .then((block) => ({ number: Number(block.block.header.height), timestamp: Math.round(Date.parse(block.block.header.time) / 1000), })), }; async function getBlock(provider: typeof terraBlockProvider, height: number | "latest", chain:string|undefined){ const block = await provider.getBlock(height) if(block === null){ throw new Error(`Can't get block of chain ${chain ?? 'ethereum'}`) } return block } function getExtraProvider(chain:string|undefined){ if(chain === "terra"){ return terraBlockProvider } else if(chain === "kava"){ return kavaBlockProvider } return getProvider(chain as any); } export async function getLatestBlock(chain:string){ const provider = getExtraProvider(chain) return getBlock(provider, "latest", chain); } const intialBlocks = { terra: 4724001, crab: 4969901 } as { [chain: string]:number|undefined } export async function lookupBlock( timestamp: number, extraParams: { chain?: Chain | "terra" | "kava"; } = {} ) { try { const provider = getExtraProvider(extraParams.chain) const lastBlock = await getBlock(provider, "latest", extraParams.chain); if((lastBlock.timestamp - timestamp)<-30*60){ throw new Error(`Last block of chain "${extraParams.chain}" is further than 30 minutes into the past. Provider is "${(provider as any)?.connection?.url}"`) } if (Math.abs(lastBlock.timestamp - timestamp) < 60) { // Short-circuit in case we are trying to get the current block return { block: lastBlock.number, timestamp: lastBlock.timestamp, }; } let high = lastBlock.number; let low = intialBlocks[extraParams?.chain ?? "ethereum"] ?? 0; let block: TimestampBlock; do { const mid = Math.floor((high + low) / 2); block = await getBlock(provider, mid, extraParams.chain); if (block.timestamp < timestamp) { low = mid + 1; } else { high = mid - 1; } } while (high - low > 4); // We lose some precision (~4 blocks) but reduce #calls needed if(Math.abs(block.timestamp - timestamp) > 3600){ throw new Error("Block selected is more than 1 hour away from the requested timestamp") } return { block: block.number, timestamp: block.timestamp, }; } catch (e) { console.log(e) throw new Error(`Couldn't find block height for chain ${extraParams.chain ?? 'ethereum'}, RPC node rugged`) } } // TODO: Pull the data from somewhere like coingecko export async function tokenList() { return rawTokenList; } export async function kyberTokens() { const pairs = await fetch( `https://api.kyber.network/api/tokens/pairs` ).then((res) => res.json()); const tokens = Object.keys(pairs).reduce( (acc, pairName) => { const pair = pairs[pairName]; acc[pair.contractAddress] = { symbol: pair.symbol, decimals: pair.decimals, ethPrice: pair.currentPrice, }; return acc; }, {} as { [address: string]: { symbol: string; decimals: number; ethPrice: number; }; } ); return { output: tokens, }; } export async function toSymbols(tokenBalances: { [address: string]: string }) { const tokens = await tokenList(); const output = Object.entries(tokenBalances).map(async ([token, balance]) => { let tokenData = tokens.find( (possibleToken) => possibleToken.contract.toLowerCase() === token.toLowerCase() ); if (token.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") { tokenData = { symbol: "ETH", contract: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", decimals: "18", }; } if (tokenData === undefined) { try { const tokenSymbol = symbol(token); const tokenDecimals = decimals(token); tokenData = { decimals: (await tokenDecimals).output, contract: token.toLowerCase(), symbol: (await tokenSymbol).output, }; } catch (e) { throw new Error(`Failed to get token data for token at ${token}`); } } const decimalBalance = ( Number(balance) / 10 ** Number(tokenData?.decimals ?? (await decimals(token)).output) ).toFixed(6); return { symbol: tokenData.symbol, address: tokenData.contract, balance: decimalBalance, }; }); return { output: await Promise.all(output), }; } // SMALL INCOMPATIBILITY: On the old API we don't return ids but we should export async function getLogs(params: { target: Address; topic: string; keys: string[]; // This is just used to select only part of the logs fromBlock: number; toBlock: number; // DefiPulse's implementation is buggy and doesn't take this into account topics?: string[]; // This is an outdated part of DefiPulse's API which is still used in some old adapters chain?: Chain; }) { if(params.toBlock === undefined || params.fromBlock === undefined){ throw new Error("toBlock and fromBlock need to be defined in all calls to getLogs") } const filter = { address: params.target, topics: params.topics ?? [utils.id(params.topic)], fromBlock: params.fromBlock, toBlock: params.toBlock, // We don't replicate Defipulse's bug because the results end up being the same anyway and hopefully they'll eventually fix it }; let logs: Log[] = []; let blockSpread = params.toBlock - params.fromBlock; let currentBlock = params.fromBlock; while (currentBlock < params.toBlock) { const nextBlock = Math.min(params.toBlock, currentBlock + blockSpread); try { const partLogs = await getProvider(params.chain).getLogs({ ...filter, fromBlock: currentBlock, toBlock: nextBlock, }); logs = logs.concat(partLogs); currentBlock = nextBlock; } catch (e) { if (blockSpread >= 2e3) { // We got too many results // We could chop it up into 2K block spreads as that is guaranteed to always return but then we'll have to make a lot of queries (easily >1000), so instead we'll keep dividing the block spread by two until we make it blockSpread = Math.floor(blockSpread / 2); } else { throw e; } } } if (params.keys.length > 0) { if (params.keys[0] !== "topics") { throw new Error("Unsupported"); } return { output: logs.map((log) => log.topics), }; } return { output: logs, }; }