UNPKG

@flarenetwork/ftso_price_provider_kick_off_package

Version:

Kick of package for FTSO price providers. Includes user facing interfaces and mock contracts to test price provider pipeline.

367 lines (320 loc) 13.5 kB
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; // This sometimes break tests // @ts-ignore import { time } from '@openzeppelin/test-helpers'; import BN from "bn.js"; import { BigNumber, ContractReceipt, ContractTransaction, Signer } from "ethers"; import { ethers } from "hardhat"; import { MIN_RANDOM } from "./constants"; const Wallet = require('ethereumjs-wallet').default; /** * Helper function for instantiating and deploying a contract by using factory. * @param name Name of the contract * @param signer signer * @param args Constructor params * @returns deployed contract instance (promise) */ export async function newContract<T>(name: string, signer: Signer, ...args: any[]) { const factory = await ethers.getContractFactory(name, signer); let contractInstance = (await factory.deploy(...args)); await contractInstance.deployed(); return contractInstance as unknown as T; } /** * Auxilliary date formating. * @param date * @returns */ export function formatTime(date: Date): string { return `${ ('0000' + date.getFullYear()).slice(-4) }-${ ('0' + (date.getMonth() + 1)).slice(-2) }-${ ('0' + date.getDate()).slice(-2) } ${ ('0' + date.getHours()).slice(-2) }:${ ('0' + date.getMinutes()).slice(-2) }:${ ('0' + date.getSeconds()).slice(-2) }` } /** * Sets parameters for shifting time to future. Note: seems like * no block is mined after this call, but the next mined block has * the the timestamp equal time + 1 * @param tm */ export async function increaseTimeTo(tm: number, callType: 'ethers' | 'web3' = "ethers") { if (process.env.VM_FLARE_TEST == "real") { // delay while (true) { let now = Math.round(Date.now() / 1000); if (now > tm) break; // console.log(`Waiting: ${time - now}`); await new Promise((resolve: any) => setTimeout(() => resolve(), 1000)); } return await advanceBlock(); } else if (process.env.VM_FLARE_TEST == "shift") { // timeshift let dt = new Date(0); dt.setUTCSeconds(tm); let strTime = formatTime(dt); const got = require('got'); let res = await got(`http://localhost:8080/${ strTime }`) // console.log("RES", strTime, res.body) return await advanceBlock(); } else { // Hardhat if (callType == "ethers") { await ethers.provider.send("evm_mine", [tm]); } else { await time.increaseTo(tm); } // THIS RETURN CAUSES PROBLEMS FOR SOME STRANGE REASON!!! // ethers.provider.getBlock stops to work!!! // return await ethers.provider.getBlock(await ethers.provider.getBlockNumber()); } } /** * Hardhat wrapper for use with web3/truffle * @param tm * @param advanceBlock * @returns */ export async function increaseTimeTo3(tm: number, advanceBlock: () => Promise<FlareBlock>) { return increaseTimeTo(tm, "web3") } /** * Finalization wrapper for ethers. Needed on Flare network since account nonce has to increase * to have the transaction confirmed. * @param address * @param func * @returns */ export async function waitFinalize(signer: SignerWithAddress, func: () => Promise<ContractTransaction>): Promise<ContractReceipt> { let nonce = await ethers.provider.getTransactionCount(signer.address); let res = await (await func()).wait(); if (res.from !== signer.address) { throw new Error("Transaction from and signer mismatch, did you forget connect()?"); } while ((await ethers.provider.getTransactionCount(signer.address)) == nonce) { await sleep(100); } return res; } /** * Finalization wrapper for web3/truffle. Needed on Flare network since account nonce has to increase * to have the transaction confirmed. * @param address * @param func * @returns */ export async function waitFinalize3<T>(address: string, func: () => Promise<T>) { let nonce = await web3.eth.getTransactionCount(address); let res = await func(); while ((await web3.eth.getTransactionCount(address)) == nonce) { await sleep(1000); } return res; } // Copied from Ethers library as types seem to not be exported properly interface _Block { hash: string; parentHash: string; number: number; timestamp: number; nonce: string; difficulty: number; gasLimit: BigNumber; gasUsed: BigNumber; miner: string; extraData: string; } export interface FlareBlock extends _Block { transactions: Array<string>; } /** * Artificial advance block making simple transaction and mining the block * @returns Returns data about the mined block */ export async function advanceBlock(): Promise<FlareBlock> { let signers = await ethers.getSigners(); await waitFinalize(signers[0], () => signers[0].sendTransaction({ to: signers[1].address, // value: ethers.utils.parseUnits("1", "wei"), value: 0, data: ethers.utils.hexlify([1]) })); let blockInfo = await ethers.provider.getBlock(await ethers.provider.getBlockNumber()); return blockInfo; // // console.log(`MINE BEAT: ${ blockInfo.timestamp - blockInfoStart.timestamp}`) } /** * Helper wrapper to convert number to BN * @param x number expressed in any reasonable type * @returns same number as BN */ export function toBN(x: BN | BigNumber | number | string): BN { if (x instanceof BN) return x; if (x instanceof BigNumber) return new BN(x.toHexString().slice(2), 16) return web3.utils.toBN(x); } /** * Helper wrapper to convert number to Ethers' BigNumber * @param x number expressed in any reasonable type * @returns same number as BigNumber */ export function toBigNumber(x: BN | BigNumber | number | string): BigNumber { if (x instanceof BigNumber) return x; if (x instanceof BN) return BigNumber.from(`0x${x.toString(16)}`); return BigNumber.from(x); } export function numberedKeyedObjectToList<T>(obj: any) { let lst: any[] = [] for (let i = 0; ; i++) { if (i in obj) { lst.push(obj[i]) } else { break; } } return lst as T[]; } export function doBNListsMatch(lst1: BN[], lst2: BN[]) { if (lst1.length != lst2.length) return false; for (let i = 0; i < lst1.length; i++) { if (!lst1[i].eq(lst2[i])) return false; } return true; } export function lastOf(lst: any[]) { return lst[lst.length-1]; } export function zip<T1, T2>(a: T1[], b: T2[]): [T1, T2][] { return a.map((x, i) => [x, b[i]]); } export function zip_many<T>(l: T[], ...lst: T[][]): T[][] { return l.map( (x, i) => [x, ...lst.map( l => l[i])] ); } export function zipi<T1, T2>(a: T1[], b: T2[]): [number, T1, T2][] { return a.map((x, i) => [i, x, b[i]]); } export function zip_manyi<T>(l: T[], ...lst: T[][]): [number, T[]][] { return l.map( (x, i) => [i, [x, ...lst.map( l => l[i])]] ); } export function compareNumberArrays(a: BN[], b: number[]) { expect(a.length, `Expected array length ${a.length} to equal ${b.length}`).to.equals(b.length); for (let i = 0; i < a.length; i++) { expect(a[i].toNumber(), `Expected ${a[i].toNumber()} to equal ${b[i]} at index ${i}`).to.equals(b[i]); } } export function compareArrays<T>(a: T[], b: T[]) { expect(a.length, `Expected array length ${a.length} to equal ${b.length}`).to.equals(b.length); for (let i = 0; i < a.length; i++) { expect(a[i], `Expected ${a[i]} to equal ${b[i]} at index ${i}`).to.equals(b[i]); } } export function compareSets<T>(a: T[] | Iterable<T>, b: T[] | Iterable<T>) { const aset = new Set(a); const bset = new Set(b); for (const elt of aset) { assert.isTrue(bset.has(elt), `Element ${elt} missing in second set`); } for (const elt of bset) { assert.isTrue(aset.has(elt), `Element ${elt} missing in first set`); } } export function assertNumberEqual(a: BN, b: number, message?: string) { return assert.equal(a.toNumber(), b, message); } export function submitPriceHash(price: number | BN | BigNumber, random: number | BN | BigNumber, address: string): string { return ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode([ "uint256", "uint256", "address" ], [ price.toString(), random.toString(), address])) } export function submitHash(ftsoIndices: (number | BN | BigNumber)[], prices: (number | BN | BigNumber)[], random: number | BN | BigNumber, address: string): string { return ethers.utils.keccak256(web3.eth.abi.encodeParameters([ "uint256[]", "uint256[]", "uint256", "address" ], [ ftsoIndices, prices, random, address ])); } function computeOneVoteRandom(price: number | BN | BigNumber, random: number | BN | BigNumber): BN { return web3.utils.toBN(ethers.utils.solidityKeccak256([ "uint256", "uint256[]" ], [ random.toString(), [price.toString()] ])); } // price_random is an array of pairs [price, random] that are being submitted export function computeVoteRandom(price_random: number[][] | BN[][] | BigNumber[][]): string { let sum = toBN(0); for (let i = 0; i < price_random.length; i++) { sum = sum.add(computeOneVoteRandom(price_random[i][0], price_random[i][1])); } return sum.mod(toBN(2).pow(toBN(256))).toString(); } function computeOneVoteRandom2(prices: (number | BN | BigNumber)[], random: number | BN | BigNumber): BN { return web3.utils.toBN(ethers.utils.keccak256(web3.eth.abi.encodeParameters([ "uint256", "uint256[]" ], [ random, prices ]))); } // price_random is an array of pairs [[prices], random] that are being submitted export function computeVoteRandom2(prices_random: any): string { let sum = toBN(0); for (let i = 0; i < prices_random.length; i++) { sum = sum.add(computeOneVoteRandom2(prices_random[i][0], prices_random[i][1])); } return sum.mod(toBN(2).pow(toBN(256))).toString(); } export function isAddressEligible(random: number | BN | BigNumber, address: string): boolean { return web3.utils.toBN(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode([ "uint256", "address" ], [ random.toString(), address]))).mod(toBN(2)).eq(toBN(1)); } export async function sleep(ms: number) { await new Promise<void>(resolve => setTimeout(() => resolve(), ms)); } export function resultTuple<T0, T1, T2, T3, T4>(obj: { 0: T0, 1: T1, 2: T2, 3: T3, 4: T4 }): [T0, T1, T2, T3, T4]; export function resultTuple<T0, T1, T2, T3>(obj: { 0: T0, 1: T1, 2: T2, 3: T3 }): [T0, T1, T2, T3]; export function resultTuple<T0, T1, T2>(obj: { 0: T0, 1: T1, 2: T2 }): [T0, T1, T2]; export function resultTuple<T0, T1>(obj: { 0: T0, 1: T1 }): [T0, T1]; export function resultTuple<T0>(obj: { 0: T0 }): [T0]; export function resultTuple(obj: any): any[] { const keys = Object.keys(obj).filter(k => /^\d+$/.test(k)).map(k => Number(k)); keys.sort((a, b) => a - b); return keys.map(k => obj[k]); } export function encodeContractNames(names: string[]): string[] { return names.map( name => ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["string"], [name])) ); } export function isNotNull<T>(x: T): x is NonNullable<T> { return x != null; } // return String(Math.round(x * 10^exponent)), but sets places below float precision to zero instead of some random digits export function toStringFixedPrecision(x: number, exponent: number): string { const significantDecimals = x !== 0 ? Math.max(0, 14 - Math.floor(Math.log10(x))) : 0; const precision = Math.min(exponent, significantDecimals); const xstr = x.toFixed(precision); const dot = xstr.indexOf('.'); const mantissa = xstr.slice(0, dot) + xstr.slice(dot + 1); if (precision === exponent) return mantissa; const zeros = Array.from({length: exponent - precision}, () => '0').join(''); // trailing zeros return mantissa + zeros; } // return BN(x * 10^exponent) export function toBNFixedPrecision(x: number, exponent: number): BN { return toBN(toStringFixedPrecision(x, exponent)); } // return BigNumber(x * 10^exponent) export function toBigNumberFixedPrecision(x: number, exponent: number): BigNumber { return BigNumber.from(toStringFixedPrecision(x, exponent)); } /** * Run an async task on every element of an array. Start tasks for all elements immediately (in parallel) and complete when all are completed. * @param array array of arguments * @param func the task to run for every element of the array */ export async function foreachAsyncParallel<T>(array: T[], func: (x: T, index: number) => Promise<void>) { await Promise.all(array.map(func)); } /** * Run an async task on every element of an array. Start tasks for every element when the previous completes (serial). Complete when all are completed. * @param array array of arguments * @param func the task to run for every element of the array */ export async function foreachAsyncSerial<T>(array: T[], func: (x: T, index: number) => Promise<void>) { for (let i = 0; i < array.length; i++) { await func(array[i], i); } } export async function getAddressWithZeroBalance() { let wallet = Wallet.generate(); while(toBN(await web3.eth.getBalance(wallet.getChecksumAddressString())).gtn(0)) { wallet = Wallet.generate(); } return wallet.getChecksumAddressString(); } export function getRandom(): BN { return MIN_RANDOM.addn(Math.floor(Math.random() * 1000)); }