@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
text/typescript
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));
}