// Local types import * as Crypto from 'types/sdk/crypto' import { Options } from 'types/sdk/core' // External imports import assert from 'assert' import { BigNumber } from 'ethers' import { bigInt } from 'snarkjs' import { randomBytes } from 'crypto' export namespace ErrorUtils { export function ensureError(value: unknown): T { if (value instanceof Error) return value as T let stringified = '[Unable to stringify the thrown value]' try { stringified = JSON.stringify(value) } catch {} const error = getError(`This value was thrown as is, not through an Error: ${stringified}`) return error as T } export function getError(message: string): Error { let error = new Error(message) error.name = '\nError (tornado-sdk)' return error } } export namespace AsyncUtils { class PoolPromise extends Promise { orderIndex?: number } export type Callback = (...values: any[]) => Promise export type ErrorHandler = ( err: Error, numResolvedPromises: number, callbackIndex: number, orderIndex: number, ...args: any[] ) => void export class PromisePooler { concurrencyLimit: number private _totalAdded: number _results: Array _callbacks: Array _errorHandlers: Array private _promises: Array> constructor(callbacks: Array, errorHandlers: Array, concurrencyLimit: number) { if (callbacks.length == 0) throw ErrorUtils.getError('PromisePooler: callbacks are empty.') if (concurrencyLimit <= 0) throw ErrorUtils.getError("PromisePooler: concurrencyLimit can't be 0 or less.") this.concurrencyLimit = concurrencyLimit this._totalAdded = 0 this._results = [] this._promises = [] this._callbacks = callbacks this._errorHandlers = errorHandlers } get pending(): number { return this._promises.length } get totalAdded(): number { return this._totalAdded } async poolMany(...args: Array>): Promise { for (const arr of args) { await this._waitIfFull() this._pool(0, this._totalAdded++, ...arr) } } async pool(...args: Array): Promise { await this._waitIfFull() this._pool(0, this._totalAdded++, ...args) } async all(): Promise> { return Promise.all(this._results.concat(this._promises)).then((result) => { this._results = [] return result }) } async race(): Promise { if (this._results.length != 0) { return Promise.resolve(this._results.splice(0, 1)[0]) } else { if (this._promises.length != 0) { // This will actually return the right value // but note that they are stored in results automatically // due to the case of 2 promises not being raced in time // and as such we instead take from results. await Promise.race(this._promises) // This should be synchronous return this.race() } else return Promise.resolve(null) } } // TODO: Immediately set new callbacks and error handlers async reset(): Promise> { let results = await this.all() assert( this._promises.length === 0, ErrorUtils.getError('PromisePooler.reset: Resetting should have allowed all promises to resolve.') ) this._results = [] this._totalAdded = 0 this._callbacks = [] this._errorHandlers = [] return results } private _pool(callbackIndex: number, orderIndex: number, ...args: any[]): Promise { let promise: PoolPromise = this._callbacks[callbackIndex](...args).then( async (...results) => { if (callbackIndex < this._callbacks.length - 1) { // This is synchronous, callbackIndex is never 0 return this._pool(callbackIndex + 1, orderIndex, ...results) } else { let result = results.length == 1 ? results[0] : results this._promises.splice(this._getPromiseIndex(orderIndex), 1) this._results.push(result) return result } }, async (err: Error) => { let resolved = this._totalAdded - this.concurrencyLimit resolved = resolved < 0 ? 0 : resolved // Throw inside to abort this._errorHandlers[callbackIndex]( ErrorUtils.ensureError(err), resolved, callbackIndex, orderIndex, ...args ) return this._pool(callbackIndex, orderIndex, ...args) } ) promise.orderIndex = orderIndex const promiseIndex = this._getPromiseIndex(orderIndex) if (promiseIndex < 0) { this._promises.push(promise) } else { this._promises[promiseIndex] = promise } return promise } private _getPromiseIndex(orderIndex: number): number { return this._promises.findIndex((_promise) => _promise.orderIndex == orderIndex) } private async _waitIfFull(): Promise { if (this.concurrencyLimit <= this._promises.length) { await Promise.race(this._promises) } } } export class Sync { pooler?: PromisePooler concurrencyLimit: number listen: boolean constructor(options?: Options.Cache.Sync) { this.concurrencyLimit = options?.concurrencyLimit ?? 1 this.listen = options?.listen ?? false } initializePooler(callbacks: Array, errorHandlers: Array): void { if (this.pooler) this.pooler.reset() this.pooler = new PromisePooler(callbacks, errorHandlers, this.concurrencyLimit) } } export function timeout(msTimeout: number): Promise { return new Promise((resolve) => setTimeout(resolve, msTimeout)) } } export namespace NumberUtils { export function randomBigInteger(numBytes: number): Crypto.bigInt { // @ts-ignore return bigInt.leBuff2int(randomBytes(numBytes)) } export function getRandomFromRange( lowerInclusive: number, upperInclusive: number, isInteger: number = 1 ): number { isInteger = 0 < isInteger ? 1 : 0 return (isInteger ? Math.floor : (x: number) => x)( Math.random() * (upperInclusive + isInteger - lowerInclusive) + lowerInclusive ) } } export namespace HexUtils { export function bufferToHex(buffer: Buffer, byteLen: number = 32): string { return '0x' + buffer.toString('hex').padStart(byteLen * 2, '0') } export function numberToHex(number: number): string { return BigNumber.from(number).toHexString() } export function bigIntToHex(number: Crypto.bigInt, byteLen: number = 32): string { // @ts-ignore return '0x' + number.toString(16).padStart(2 * byteLen, '0') } export function prepareAddress(address: string, bytelen: number = 32): string { return (address.slice(0, 2) == '0x' ? address.slice(2) : address).toLowerCase().padStart(bytelen * 2, '0') } } export namespace ObjectUtils { export function populate(source?: T, target?: T): T { if (!source) { if (!target) { throw ErrorUtils.getError('ObjectUtils.populate: source & target object null or undefined') } else return target } if (!target) throw ErrorUtils.getError('ObjectUtils.populate: target object null or undefined') type KeyType = keyof typeof target const sourceEntries: [string, any][] = Object.entries(source) sourceEntries.forEach((sEntry: [string, any]) => { const key = sEntry[0] as KeyType // Get entry in target const tEntry = target[key] // Set if it's not present in the target if (tEntry === undefined || tEntry === null) target[key] = sEntry[1] }) return target } type Swapped = { left: T right: T } export function swap(left: T, right: T): Swapped { return { left: right, right: left } } export function exists(obj: any): boolean { return obj !== undefined && obj !== null } }