wormhole/algorand/audit_test/src/test_helper.ts

190 lines
7.5 KiB
TypeScript

import { Algodv2, OnApplicationComplete } from "algosdk"
import { Address, ContractAmount, Asset } from "./sdk/AlgorandTypes"
import { Signer } from "./sdk/Signer"
import { Deployer, SignCallback, TealSignCallback } from "./sdk/Deployer"
import { WormholeConfig, WORMHOLE_CONFIG_TESTNET, LOCAL_CONFIG, TestExecutionEnvironmentConfig } from "./sdk/Environment"
// Avoid Jest to print where the console.log messagres are emmited
global.console = require('console');
// We want to actually see the full stacktrace for errors
Error.stackTraceLimit = Infinity;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function improve_errors(task: (() => Promise<void>) | any ) {
return async () => {
try {
await task()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error !== undefined) {
let errorExtraInfo = "No error extra info available."
if (error.response !== undefined) {
errorExtraInfo = error.response.text !== undefined
? JSON.parse(error.response.text).message
: error.response
}
// We want to preserve the error's stack trace, so we only modify the message
error.message = error.message + " Error Extra Info: " + errorExtraInfo
throw error
}
throw "UNKNOWN ERROR. Received error variable in catch is undefined"
}
}
}
export class SeededRandom {
constructor(
private seedA: number = 1234,
private seedB: number = 4321,
private seedC: number = 5678,
private seedD: number = 8765
) {}
public rand(): number {
// http://pracrand.sourceforge.net/, sfc32
this.seedA >>>= 0; this.seedB >>>= 0; this.seedC >>>= 0; this.seedD >>>= 0;
let t = (this.seedA + this.seedB) | 0;
this.seedA = this.seedB ^ this.seedB >>> 9;
this.seedB = this.seedC + (this.seedC << 3) | 0;
this.seedC = (this.seedC << 21 | this.seedC >>> 11);
this.seedD = this.seedD + 1 | 0;
t = t + this.seedD | 0;
this.seedC = this.seedC + t | 0;
return (t >>> 0) / 4294967296;
}
public randRange(lo: number, hi: number): number {
return this.rand() * (hi - lo) + lo
}
public randInt(lo: number, hi: number): number {
return Math.floor(this.randRange(lo, hi))
}
public randBool(): boolean {
return this.rand() >= 0.5
}
}
export class TestHelper {
signCallback: SignCallback
tealSignCallback: TealSignCallback
rng: SeededRandom = new SeededRandom()
public static fromConfig(config: TestExecutionEnvironmentConfig = LOCAL_CONFIG, wormholeConfig: WormholeConfig = WORMHOLE_CONFIG_TESTNET): TestHelper {
const algoSdk = new Algodv2(config.algod.token, config.algod.server, config.algod.port)
const deployer = new Deployer(algoSdk, 1_000, undefined, wormholeConfig)
const signer = new Signer()
const master = signer.addFromMnemonic(config.masterAccount)
return new TestHelper(deployer, signer, master)
}
constructor(readonly deployer: Deployer, readonly signer: Signer, readonly master: Address) {
this.signCallback = this.signer.callback
this.tealSignCallback = this.signer.tealCallback
}
public createAccount(): Address {
return this.signer.createAccount()
}
public deployerAccount(): Address {
return this.master;
}
public async waitForTransactionResponse(txId: Address): Promise<Record<string, any>> {
return this.deployer.waitForTransactionResponse(txId)
}
public async fundUser(account: Address, amount: ContractAmount, assetId = 0) {
const fundTx = assetId === 0 ?
await this.deployer.makePayTransaction(this.master, account, amount) :
await this.deployer.makeAssetTransferTransaction(this.master, account, assetId, amount)
const txId = await this.deployer.signAndSend([fundTx], this.signCallback)
await this.deployer.waitForTransactionResponse(txId)
console.log("User " + account + " received " + amount + (assetId == 0 ? " algos" : " assets"))
}
public async createAsset(unitName: string, total: ContractAmount | number, decimals = 0): Promise<Asset> {
const name = "Test Asset " + unitName
const url = unitName + ".io"
const assetTx = await this.deployer.makeAssetCreationTransaction(
this.master, total, decimals, unitName, "Test Asset " + name, name + ".io")
const creationId = await this.deployer.signAndSend([assetTx], this.signCallback)
const assetId = (await this.deployer.waitForTransactionResponse(creationId))["asset-index"]
console.log("Created Test Asset: " + name, assetId)
return {id: assetId, unitName, name, decimals, url}
}
public async optinAsset(account: Address, assetId: number) {
const optinTx = await this.deployer.makeAssetOptInTransaction(account, assetId)
const txId = await this.deployer.signAndSend([optinTx], this.signCallback)
await this.deployer.waitForTransactionResponse(txId)
console.log("Account " + account + " optin to asset: " + assetId)
}
public async optinApp(account: Address, appId: number) {
const optinTx = await this.deployer.makeCallTransaction(account, appId, OnApplicationComplete.OptInOC, [], [], [], [], "")
const txId = await this.deployer.signAndSend([optinTx], this.signCallback)
await this.deployer.waitForTransactionResponse(txId)
console.log("Account " + account + " optin to app: " + appId)
}
public deployAndFund<T extends readonly unknown[], R>(
f: (deployer: Deployer, deployerAccount: Address, signCallback: SignCallback, ...args: [...T]) => R,
...args: T
) {
return f(this.deployer, this.master, this.signCallback, ...args)
}
public async clearApps() {
try {
await this.deployer.clearApps(this.deployerAccount(), this.signCallback)
await this.deployer.deleteApps(this.deployerAccount(), this.signCallback)
} catch (e) {
console.log(`Clear/delete error being ignored: ${e}`)
}
}
public async createAccountGroup(balances: ContractAmount[][], clearApps = true): Promise<[Address[], Asset[]]> {
if (clearApps) {
await this.clearApps()
}
// Create accounts
const accounts = balances.map(() => this.createAccount())
// Calculate the total asset supply consumed by this request
const assetSupply = balances.reduce((accum, val) => {
val.forEach((x, i) => accum[i] = BigInt(accum[i] ?? 0) + BigInt(x))
return accum
}, [])
// Asset 0 is algos, so slice it off and create the remaining assets
const assets = await Promise.all(assetSupply.slice(1).map((supply, index) => {
return this.createAsset(`A${index}`, supply)
}))
// Give algos and assets to users
// NOTE: This is not a peformance critical section, so the extra await ticks should be okay
await Promise.all(balances.map(async (row, i) => {
// Asset 0 is algos
await this.fundUser(accounts[i], row[0])
// Assets 1..n are actual assets that need to be opted in to
await Promise.all(row.slice(1).flatMap(async (balance, j) => {
await this.optinAsset(accounts[i], assets[j].id)
if (balance > 0) {
await this.fundUser(accounts[i], balance, assets[j].id)
}
}))
}))
return [accounts, assets]
}
public standardAssetGrid(userCount: number, assetCount: number, algoAmount: number | ContractAmount = 2000000, assetAmount: number | ContractAmount = 1000000): ContractAmount[][] {
return [...Array(userCount)].map(() => [BigInt(algoAmount)].concat([...Array(assetCount)].fill(BigInt(assetAmount))))
}
}