Test created by the C3 team. (#1796)
Co-authored-by: qonfluent <zantrua+github@gmail.com> Co-authored-by: NoiTaTuM <noit63@gmail.com> Co-authored-by: Marcos NC <marcosnc@gmail.com>
This commit is contained in:
parent
e109024e99
commit
41d53b27c8
|
@ -135,9 +135,11 @@ class TmplSig:
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
core = TmplSig("sig")
|
core = TmplSig("sig")
|
||||||
# client = AlgodClient("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "http://localhost:4001")
|
|
||||||
# pprint.pprint(client.compile( core.get_sig_tmpl()))
|
|
||||||
|
|
||||||
with open("sig.tmpl.teal", "w") as f:
|
if len(sys.argv) == 1:
|
||||||
|
file_name = "sig.tmpl.teal"
|
||||||
|
else:
|
||||||
|
file_name = sys.argv[1]
|
||||||
|
|
||||||
|
with open(file_name, "w") as f:
|
||||||
f.write(core.get_sig_tmpl())
|
f.write(core.get_sig_tmpl())
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# How to run the tests
|
||||||
|
1. Run the sandbox with a `dev` environment and copy `reset_sandbox.sh` to the same directory as the sandbox executable
|
||||||
|
2. Run the `reset_sandbox.sh` script
|
||||||
|
3. Run `npm install` in this directory
|
||||||
|
4. Run `npm test` in this directory
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "@wormhole-foundation/algorand-audit",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "",
|
||||||
|
"main": "out/audit.js",
|
||||||
|
"types": "out/audit.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"pretest": "tsc --build",
|
||||||
|
"test": "jest Wormhole.test.ts",
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"fix": "eslint . --ext .ts --fix"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"algosdk": "^1.13.1",
|
||||||
|
"varint": "^6.0.0",
|
||||||
|
"web3-utils": "^1.7.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@certusone/wormhole-sdk": "^0.4.5",
|
||||||
|
"@types/elliptic": "^6.4.14",
|
||||||
|
"@types/jest": "^26.0.14",
|
||||||
|
"@types/node": "^16.11.9",
|
||||||
|
"@types/node-fetch": "^2.6.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
||||||
|
"@typescript-eslint/parser": "^5.12.1",
|
||||||
|
"eslint": "^8.9.0",
|
||||||
|
"ethers": "^5.6.9",
|
||||||
|
"jest": "^26.6.0",
|
||||||
|
"ts-jest": "^26.5.6",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"typescript": "^4.6.2"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"collectCoverage": true,
|
||||||
|
"testTimeout": 30000
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
./sandbox reset dev -v
|
||||||
|
account=$(./sandbox goal account list | awk '{print $2; exit}')
|
||||||
|
./sandbox goal clerk send --amount 3999000000000000 -f $account -t HL6A24OGJX4FDZT36HOQ6VWJDF6GW3IEWB4FXB4OH5FQKVI46HZBZOZFAM
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { TestHelper } from './test_helper'
|
||||||
|
import elliptic from 'elliptic'
|
||||||
|
import { concatArrays, decodeUint64, encodeUint16, sha256Hash } from './sdk/Encoding'
|
||||||
|
import { decodeAddress } from 'algosdk'
|
||||||
|
import path from 'path'
|
||||||
|
import { GovenanceMessageType, GovernancePayload, UngeneratedVAA, VAAHeader, VAAPayloadType, WormholeSigner } from './wormhole/WormholeTypes'
|
||||||
|
import { Wormhole } from './wormhole/Wormhole'
|
||||||
|
import { CHAIN_ID_ALGORAND } from '@certusone/wormhole-sdk'
|
||||||
|
import { EMITTER_GOVERNANCE, signWormholeMessage } from './wormhole/WormholeEncoders'
|
||||||
|
import { WormholeTmplSig } from './wormhole/WormholeTmplSig'
|
||||||
|
import { generateGovernanceVAA, generateVAA } from './wormhole/WormholeVAA'
|
||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
|
describe('Wormhole', () => {
|
||||||
|
const helper: TestHelper = TestHelper.fromConfig()
|
||||||
|
let wormhole: Wormhole
|
||||||
|
let signers: WormholeSigner[]
|
||||||
|
|
||||||
|
it('Can create signer set', () => {
|
||||||
|
const ec = new elliptic.ec("secp256k1")
|
||||||
|
signers = [...Array(19)].map(() => ec.genKeyPair())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can compile tmplSig', async () => {
|
||||||
|
const sequence = 0x11223344
|
||||||
|
const emitterId = decodeAddress(helper.master).publicKey
|
||||||
|
const coreId = 1234
|
||||||
|
|
||||||
|
const tmplSig = await WormholeTmplSig.compileTmplSig(helper.deployer, sequence, emitterId, coreId)
|
||||||
|
const tmplSigDirect = new WormholeTmplSig(sequence, emitterId, coreId)
|
||||||
|
const bytecode = tmplSigDirect.logicSig.lsig.logic
|
||||||
|
const returnOpcode = 0x43
|
||||||
|
const bytecodeWithReturn = new Uint8Array([...bytecode, returnOpcode])
|
||||||
|
expect(tmplSig.lsig.logic).toEqual(bytecodeWithReturn)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can sign a VAA', () => {
|
||||||
|
const vaa: UngeneratedVAA = {
|
||||||
|
command: 'governance',
|
||||||
|
version: 1,
|
||||||
|
gsIndex: 0,
|
||||||
|
header: {
|
||||||
|
timestamp: 0,
|
||||||
|
nonce: 0,
|
||||||
|
chainId: CHAIN_ID_ALGORAND,
|
||||||
|
emitter: EMITTER_GOVERNANCE,
|
||||||
|
sequence: 0,
|
||||||
|
consistencyLevel: 0,
|
||||||
|
},
|
||||||
|
signers,
|
||||||
|
payload: {
|
||||||
|
type: VAAPayloadType.Raw,
|
||||||
|
payload: new Uint8Array([1, 2, 3, 4])
|
||||||
|
},
|
||||||
|
extraTmplSigs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsignedVAA = generateVAA(vaa)
|
||||||
|
const signedVAA = signWormholeMessage(unsignedVAA)
|
||||||
|
|
||||||
|
// Validate VAA following wormhole_core
|
||||||
|
let ptr = signedVAA.data.slice(5, 6)[0]
|
||||||
|
expect(ptr).toBe(unsignedVAA.entries.length)
|
||||||
|
|
||||||
|
ptr = ptr * 66 + 14
|
||||||
|
const emitter = signedVAA.data.slice(ptr, ptr + 34)
|
||||||
|
assert(vaa.header.chainId)
|
||||||
|
assert(vaa.header.emitter)
|
||||||
|
expect(emitter).toEqual(concatArrays([encodeUint16(vaa.header.chainId), vaa.header.emitter]))
|
||||||
|
|
||||||
|
ptr += 34
|
||||||
|
// = 1254 + 48 = 1302
|
||||||
|
// NOTE: This assumes 19 guardians
|
||||||
|
expect(ptr).toBe(1302)
|
||||||
|
|
||||||
|
const sequence = decodeUint64(signedVAA.data.slice(ptr, ptr + 8))
|
||||||
|
expect(sequence).toBe(BigInt(unsignedVAA.header.sequence))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can compile vaa_verify', async () => {
|
||||||
|
const dummyWormhole = new Wormhole(helper.deployer, helper.master, 0, 0, helper.signCallback)
|
||||||
|
const vaaVerify = await dummyWormhole.compileVaaVerify(helper.deployer)
|
||||||
|
void(vaaVerify)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can be deployed', async () => {
|
||||||
|
console.log(`Owner base 64: ${Buffer.from(decodeAddress(helper.master).publicKey).toString('base64')}`)
|
||||||
|
wormhole = await helper.deployAndFund(Wormhole.deployAndFund, signers)
|
||||||
|
void(wormhole)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can set message fee', async () => {
|
||||||
|
const header: Partial<VAAHeader> = {
|
||||||
|
sequence: 1,
|
||||||
|
consistencyLevel: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: GovernancePayload = {
|
||||||
|
type: GovenanceMessageType.SetMessageFee,
|
||||||
|
targetChainId: CHAIN_ID_ALGORAND,
|
||||||
|
messageFee: 1337
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = generateGovernanceVAA(signers, 1, wormhole.coreId, header, payload)
|
||||||
|
const signedVaa = signWormholeMessage(msg)
|
||||||
|
const txId = await wormhole.sendSignedVAA(signedVaa)
|
||||||
|
await helper.waitForTransactionResponse(txId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can perform an update', async () => {
|
||||||
|
// Compile new core app
|
||||||
|
const corePath = path.join(Wormhole.BASE_PATH, 'wormhole_core.py')
|
||||||
|
const coreApp = await helper.deployer.makeSourceApp(corePath, Wormhole.CORE_STATE_MAP)
|
||||||
|
const coreCompiled = await helper.deployer.makeApp(coreApp)
|
||||||
|
|
||||||
|
// Set the update hash
|
||||||
|
const updateHash = sha256Hash(coreCompiled.approval)
|
||||||
|
|
||||||
|
const payload: GovernancePayload = {
|
||||||
|
type: GovenanceMessageType.SetUpdateHash,
|
||||||
|
targetChainId: CHAIN_ID_ALGORAND,
|
||||||
|
updateHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsignedVaa = generateGovernanceVAA(signers, 1, wormhole.coreId, { sequence: 2 }, payload)
|
||||||
|
const signedVaa = signWormholeMessage(unsignedVaa)
|
||||||
|
const txId = await wormhole.sendSignedVAA(signedVaa)
|
||||||
|
await helper.waitForTransactionResponse(txId)
|
||||||
|
|
||||||
|
// Perform the update
|
||||||
|
// TODO: Perform the update here
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can upgrade guardian set', async () => {
|
||||||
|
// const ec = new elliptic.ec("secp256k1")
|
||||||
|
// signers = [...Array(19)].map(() => ec.genKeyPair())
|
||||||
|
|
||||||
|
// const payload: GovernancePayload = {
|
||||||
|
// type: GovenanceMessageType.UpdateGuardians,
|
||||||
|
// targetChainId: CHAIN_ID_ALGORAND,
|
||||||
|
// oldGSIndex: 1,
|
||||||
|
// newGSIndex: 2,
|
||||||
|
// guardians: generateKeySet(signers)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const unsignedVaa = generateGovernanceVAA(signers, 1, wormhole.coreId, { sequence: 3 }, payload)
|
||||||
|
// const signedVaa = signWormholeMessage(unsignedVaa)
|
||||||
|
// const txId = await wormhole.sendSignedVAA(signedVaa, true)
|
||||||
|
// await helper.waitForTransactionResponse(txId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can perform register chain', async () => {
|
||||||
|
// const CHAIN_ID_FOOBAR = 65000
|
||||||
|
// const EMITTER_FOOBAR = decodeBase16('ccddeeffccddeeffccddeeffccddeeffccddeeffccddeeffccddeeffccddeeff')
|
||||||
|
// const regVaa = wormhole.generateRegisterChainVAA(0, signers, 0, 0, 0, 0, CHAIN_ID_ALGORAND, CHAIN_ID_FOOBAR, EMITTER_FOOBAR)
|
||||||
|
// const signedVaa = wormhole.signVAA(regVaa)
|
||||||
|
// const txId = await wormhole.sendSignedVAA(signedVaa)
|
||||||
|
// await helper.waitForTransactionResponse(txId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can perform token transfer', () => {})
|
||||||
|
|
||||||
|
it('Can redeem token', () => {})
|
||||||
|
|
||||||
|
it('Rejects invalid VAAs', () => {
|
||||||
|
// This should be a randomized test over the data structure
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Correctly processes many transactions', () => {
|
||||||
|
// Process at least 40_000 VAAs to ensure the sequence number system works correctly
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type Address = string
|
||||||
|
export type AssetId = number
|
||||||
|
export type AppId = number
|
||||||
|
export type UnixTimestamp = number
|
||||||
|
export type TransactionId = string
|
||||||
|
export type ContractAmount = bigint
|
||||||
|
|
||||||
|
export type Asset = {
|
||||||
|
id: AssetId,
|
||||||
|
name: string,
|
||||||
|
unitName: string,
|
||||||
|
decimals: number,
|
||||||
|
url: string
|
||||||
|
}
|
|
@ -0,0 +1,697 @@
|
||||||
|
import algosdk, { Algodv2, Transaction, TransactionType, OnApplicationComplete, SuggestedParams, LogicSigAccount } from 'algosdk'
|
||||||
|
import { AlgorandType, encodeAddress, encodeArgArray, encodeString, decodeState, IState, IStateMap, decodeString, decodeBase64, IStateType } from './Encoding'
|
||||||
|
import child_process from "child_process"
|
||||||
|
import util from "util"
|
||||||
|
import { AssetId, ContractAmount, Address, TransactionId, AppId, Asset } from './AlgorandTypes'
|
||||||
|
import { AssertionError } from 'assert'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import AnyTransaction from 'algosdk/dist/types/src/types/transactions'
|
||||||
|
import { WORMHOLE_CONFIG_TESTNET } from './Environment'
|
||||||
|
import { redeemOnAlgorand } from '@certusone/wormhole-sdk'
|
||||||
|
import { TransactionSignerPair } from "@certusone/wormhole-sdk/lib/cjs/algorand"
|
||||||
|
|
||||||
|
export enum FieldType {
|
||||||
|
UINT = 1,
|
||||||
|
STRING,
|
||||||
|
ADDRESS,
|
||||||
|
BOOL,
|
||||||
|
AMOUNT,
|
||||||
|
BYTES,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IStateInfo = {
|
||||||
|
local: IStateMap,
|
||||||
|
global: IStateMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICompiledApp {
|
||||||
|
approval: Uint8Array
|
||||||
|
clear: Uint8Array
|
||||||
|
stateInfo: IStateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISourceApp {
|
||||||
|
approval: string
|
||||||
|
clear: string
|
||||||
|
stateInfo: IStateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStatelessContract {
|
||||||
|
code: string
|
||||||
|
address: string
|
||||||
|
parameters: IParameter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IParameter {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
type: FieldType
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SignCallback = (txs: Transaction[]) => Promise<Uint8Array[]>
|
||||||
|
export type TealSignCallback = (data: Uint8Array, from: Address, to: Address) => Promise<Uint8Array>
|
||||||
|
|
||||||
|
export class Deployer {
|
||||||
|
// Teal cache
|
||||||
|
private static tealCache: Map<string, string[]> = new Map()
|
||||||
|
|
||||||
|
constructor(readonly algodClient: Algodv2, readonly minFee = 1000, readonly minBalance = 100000, readonly wormholeConfig = WORMHOLE_CONFIG_TESTNET) {}
|
||||||
|
|
||||||
|
getMinFee(): number {
|
||||||
|
return this.minFee
|
||||||
|
}
|
||||||
|
|
||||||
|
getMinBalance(): number {
|
||||||
|
return this.minBalance
|
||||||
|
}
|
||||||
|
|
||||||
|
private async compileProgram(program: string, templateValues?: Map<string, AlgorandType>): Promise<Uint8Array> {
|
||||||
|
const parsedCode = Deployer.parseCode(program, templateValues)
|
||||||
|
const compileResponse = await this.algodClient.compile(parsedCode).do()
|
||||||
|
return new Uint8Array(Buffer.from(compileResponse.result, 'base64'))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getParams(): Promise<SuggestedParams> {
|
||||||
|
const params: SuggestedParams = await this.algodClient.getTransactionParams().do()
|
||||||
|
params.fee = this.minFee
|
||||||
|
params.flatFee = true
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
static isStatic(stateless: IStatelessContract) {
|
||||||
|
return stateless.parameters.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async signAndSend(transactions: Transaction[], signCallback: SignCallback, stateless: Map<Address, LogicSigAccount> = new Map(), dryrunTest = false): Promise<TransactionId> {
|
||||||
|
// Validate the total fees
|
||||||
|
const totalFee = transactions.reduce((acc, val) => acc + val.fee, 0)
|
||||||
|
const minFee = transactions.length * this.minFee
|
||||||
|
if (totalFee < minFee) {
|
||||||
|
throw new AssertionError({ message: `The minimum fee for a group of size ${transactions.length} is ${minFee}, but only given ${totalFee}` })
|
||||||
|
}
|
||||||
|
if (dryrunTest) {
|
||||||
|
const result = await this.dryrunRequest({ transactions, signCallback, stateless })
|
||||||
|
this.debugDryrunResult(result)
|
||||||
|
}
|
||||||
|
// Sign transactions
|
||||||
|
const txIndexes: number[] = []
|
||||||
|
const logicSigned: Uint8Array[] = []
|
||||||
|
const txsToSign: Transaction[] = []
|
||||||
|
// Sign the stateless transactions first
|
||||||
|
for (let i = 0; i < transactions.length; i++) {
|
||||||
|
const sender = encodeAddress(transactions[i].from.publicKey)
|
||||||
|
const lsig = stateless.get(sender)
|
||||||
|
if (lsig) {
|
||||||
|
logicSigned.push(algosdk.signLogicSigTransactionObject(transactions[i], lsig).blob)
|
||||||
|
txIndexes[i] = 0
|
||||||
|
} else {
|
||||||
|
txsToSign.push(transactions[i])
|
||||||
|
txIndexes[i] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sign the normal transactions all at once
|
||||||
|
const txSigned = await signCallback(txsToSign)
|
||||||
|
// Reconstruct the group in the same order as before
|
||||||
|
let logicIndex = 0
|
||||||
|
let txsIndex = 0
|
||||||
|
const signed: Uint8Array[] = []
|
||||||
|
for (let i = 0; i < txIndexes.length; i++) {
|
||||||
|
signed.push(txIndexes[i] === 0 ? logicSigned[logicIndex++] : txSigned[txsIndex++])
|
||||||
|
}
|
||||||
|
// Send transactions
|
||||||
|
return (await this.algodClient.sendRawTransaction(signed).do()).txId
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionFailed(result: Record<string, any>): boolean {
|
||||||
|
return (result["confirmed-round"] == null || result["confirmed-round"] <= 0)
|
||||||
|
&& result["pool-error"] != null
|
||||||
|
&& result["pool-error"].length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForTransactionResponse(txId: string): Promise<Record<string, any>> {
|
||||||
|
// Validate transaction was sucessful
|
||||||
|
const result = await algosdk.waitForConfirmation(this.algodClient, txId, 10000)
|
||||||
|
if (this.transactionFailed(result)) {
|
||||||
|
throw new Error(JSON.stringify(result))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async dryrunRequest({
|
||||||
|
transactions,
|
||||||
|
signCallback,
|
||||||
|
stateless = new Map(),
|
||||||
|
latestTimestamp,
|
||||||
|
}: {
|
||||||
|
transactions: Transaction[],
|
||||||
|
signCallback: SignCallback,
|
||||||
|
stateless?: Map<Address, LogicSigAccount>
|
||||||
|
latestTimestamp?: number | bigint
|
||||||
|
}): Promise<any> {
|
||||||
|
// Validate the total fees
|
||||||
|
const totalFee = transactions.reduce((acc, val) => acc + val.fee, 0)
|
||||||
|
const minFee = transactions.length * this.minFee
|
||||||
|
if (totalFee < minFee) {
|
||||||
|
throw new AssertionError({ message: `The minimum fee for a group of size ${transactions.length} is ${minFee}, but only given ${totalFee}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign transactions
|
||||||
|
const signed = await Promise.all(transactions.map(async tx => {
|
||||||
|
const sender = encodeAddress(tx.from.publicKey)
|
||||||
|
const lsig = stateless.get(sender)
|
||||||
|
if (lsig)
|
||||||
|
return algosdk.signLogicSigTransactionObject(tx, lsig).blob
|
||||||
|
const signedTx = await signCallback([tx])
|
||||||
|
return signedTx[0]
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create dryrun request
|
||||||
|
const dr = await algosdk.createDryrun({
|
||||||
|
client: this.algodClient,
|
||||||
|
txns: signed.map((stxn) => algosdk.decodeSignedTransaction(stxn)),
|
||||||
|
latestTimestamp,
|
||||||
|
})
|
||||||
|
const dryrunResponse = await this.algodClient.dryrun(dr).do()
|
||||||
|
return dryrunResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
debugDryrunResult(result: any) {
|
||||||
|
console.log(`Transaction count: ${result.txns.length}`)
|
||||||
|
result.txns.forEach((txn: any, i: number) => {
|
||||||
|
// Log call messages(i.e. passed/failed and error messages)
|
||||||
|
console.log(txn['app-call-messages'] ?? '')
|
||||||
|
|
||||||
|
// Log local deltas
|
||||||
|
if (txn['local-deltas']) {
|
||||||
|
const deltas = txn['local-deltas'].filter((x: any) => x.address !== '')
|
||||||
|
const result = deltas.map((x: any) => {
|
||||||
|
const updates = x.delta.map((y: any) => ` Var: ${escape(decodeBase64(y.key).toString())}, Action: ${y.value.action}, Value: ${Buffer.from(decodeBase64(y.value.bytes)).toString('hex')}\n`).join()
|
||||||
|
return `Account: ${x.address}\nUpdates: ${updates}`
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(result.join())
|
||||||
|
} else {
|
||||||
|
console.log(`Local state did not change`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log logic sigs traces
|
||||||
|
if (txn['logic-sig-trace']) {
|
||||||
|
const passed = txn['logic-sig-messages'][0] === 'PASS'
|
||||||
|
const disassembly = txn['logic-sig-disassembly']
|
||||||
|
const trace: { line: number, pc: number, stack: any }[] = txn['logic-sig-trace']
|
||||||
|
|
||||||
|
const msgHeader = `Group[${i}] logic sig: ${passed ? 'PASSED' : `FAILED: ${txn['logic-sig-messages'][1]}`}`
|
||||||
|
if (!passed) {
|
||||||
|
const msgBody = trace.map(({ line, pc, stack }) => {
|
||||||
|
const stackMsg = stack.map((entry: any) => {
|
||||||
|
switch (entry.type) {
|
||||||
|
case 1: return `bytes ${Buffer.from(entry.bytes, 'base64').toString('hex')}`
|
||||||
|
case 2: return `uint ${entry.uint}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return `${pc}: ${disassembly[line]} | ${stackMsg}`
|
||||||
|
}).join('\n')
|
||||||
|
|
||||||
|
const msg = msgHeader + '\n' + msgBody
|
||||||
|
console.log(msg)
|
||||||
|
} else {
|
||||||
|
console.log(msgHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log apps traces
|
||||||
|
if (txn['app-call-messages'] !== undefined) {
|
||||||
|
const passed = txn['app-call-messages'][1] === 'PASS'
|
||||||
|
const trace = txn['app-call-trace']
|
||||||
|
const cost = txn['cost']
|
||||||
|
const disassembly = txn['disassembly']
|
||||||
|
|
||||||
|
const msgHeader = `Group[${i}]: ${passed ? 'PASSED' : `FAILED: ${txn['app-call-messages'][2]}`}, cost: ${cost}`
|
||||||
|
|
||||||
|
if (true) {
|
||||||
|
const msgBody = trace.map((entry: any) => {
|
||||||
|
const opcode = disassembly[entry.line]
|
||||||
|
const scratchMsg = entry.scratch?.map((x: any, i: number) => {
|
||||||
|
switch (x.type) {
|
||||||
|
case 0: return ''
|
||||||
|
case 1: return `${i}: bytes ${Buffer.from(x.bytes, 'base64').toString('hex')}`
|
||||||
|
case 2: return `${i}: uint ${x.uint}`
|
||||||
|
default: return `${i}: UNKNOWN`
|
||||||
|
}
|
||||||
|
}).filter((x: string) => x !== '').join('\n')
|
||||||
|
const stackMsg = entry.stack.map((x: any) => {
|
||||||
|
switch (x.type) {
|
||||||
|
case 1: return `bytes ${Buffer.from(x.bytes, 'base64').toString('hex')}`
|
||||||
|
case 2: return `uint ${x.uint}`
|
||||||
|
default: return `UNKNOWN`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return `${entry.line}(${entry.pc}): ${opcode} | [${stackMsg.join(', ')}]` + `\n${scratchMsg ?? ''}`
|
||||||
|
}).join("\n\n")
|
||||||
|
|
||||||
|
console.log(msgHeader + "\n" + msgBody)
|
||||||
|
} else {
|
||||||
|
console.log(msgHeader)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Group[${i}] keys: ${Object.keys(txn)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeApp(app: ISourceApp, templateValues?: Map<string, AlgorandType>): Promise<ICompiledApp> {
|
||||||
|
return {
|
||||||
|
approval: await this.compileProgram(app.approval, templateValues),
|
||||||
|
clear: await this.compileProgram(app.clear, templateValues),
|
||||||
|
stateInfo: app.stateInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeSourceApp(pySourcePath: string, stateInfo: IStateInfo): Promise<ISourceApp> {
|
||||||
|
// Compile python program
|
||||||
|
const results = await this.compilePyTeal(pySourcePath, 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
approval: results[0],
|
||||||
|
clear: results[1],
|
||||||
|
stateInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteApplication(sender: Address, id: number, signCallback: SignCallback): Promise<string> {
|
||||||
|
const params = await this.getParams()
|
||||||
|
const txApp = algosdk.makeApplicationDeleteTxn(sender, params, id);
|
||||||
|
const txns = [txApp]
|
||||||
|
return this.signAndSend(txns, signCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAsset(sender: Address, id: number, signCallback: SignCallback): Promise<string> {
|
||||||
|
const params = await this.getParams()
|
||||||
|
const tx = algosdk.makeAssetDestroyTxnWithSuggestedParams(sender, undefined, id, params);
|
||||||
|
const txns = [tx]
|
||||||
|
return this.signAndSend(txns, signCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearApplication(sender: Address, id: number, signCallback: SignCallback): Promise<string> {
|
||||||
|
const params = await this.getParams()
|
||||||
|
const txApp = algosdk.makeApplicationClearStateTxn(sender, params, id)
|
||||||
|
const txns = [txApp]
|
||||||
|
return this.signAndSend(txns, signCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeApplication(sender: Address, id: number, signCallback: SignCallback): Promise<any> {
|
||||||
|
const params = await this.getParams()
|
||||||
|
const txApp = algosdk.makeApplicationCloseOutTxn(sender, params, id)
|
||||||
|
const txns = [txApp]
|
||||||
|
return this.signAndSend(txns, signCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deployApplication(sender: Address, app: ICompiledApp, signCallback: SignCallback, extraCompBudgetTxns?: Transaction[],
|
||||||
|
args?: AlgorandType[], appAccounts?: Address[], appApps?: number[], fee?: number, debug?: boolean): Promise<string> {
|
||||||
|
const compBudgetTxns = extraCompBudgetTxns ? extraCompBudgetTxns : []
|
||||||
|
const params = await this.getParams()
|
||||||
|
params.fee = fee ? fee : params.fee
|
||||||
|
const appArgs = args ? encodeArgArray(args) : undefined
|
||||||
|
const onComplete = OnApplicationComplete.NoOpOC
|
||||||
|
const foreignApps = appApps ? appApps : undefined
|
||||||
|
const foreignAssets = undefined
|
||||||
|
const note = undefined
|
||||||
|
const lease = undefined
|
||||||
|
const rekeyTo = undefined
|
||||||
|
|
||||||
|
// Calculate extra pages
|
||||||
|
const bytes_per_page = 2048
|
||||||
|
const extraPages = Math.ceil((app.approval.length + app.clear.length) / bytes_per_page)
|
||||||
|
|
||||||
|
const localInts = Object.entries(app.stateInfo.local).filter(([_, type]) => type === 'uint').length
|
||||||
|
const localBytes = Object.entries(app.stateInfo.local).filter(([_, type]) => type === 'bytes').length
|
||||||
|
const globalInts = Object.entries(app.stateInfo.global).filter(([_, type]) => type === 'uint').length
|
||||||
|
const globalBytes = Object.entries(app.stateInfo.global).filter(([_, type]) => type === 'bytes').length
|
||||||
|
|
||||||
|
const txApp = algosdk.makeApplicationCreateTxn(
|
||||||
|
sender, params, onComplete, app.approval, app.clear, localInts,
|
||||||
|
localBytes, globalInts, globalBytes, appArgs, appAccounts,
|
||||||
|
foreignApps, foreignAssets, note, lease, rekeyTo, extraPages
|
||||||
|
)
|
||||||
|
const txns = [txApp, ...compBudgetTxns]
|
||||||
|
return this.callGroupTransaction(txns, new Map(), signCallback, debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deploySourceApplication(from: Address, sourceApp: ISourceApp, signCallback: any): Promise<string> {
|
||||||
|
const compiledApp: ICompiledApp = await this.makeApp(sourceApp)
|
||||||
|
const deployId = await this.deployApplication(from, compiledApp, signCallback, [])
|
||||||
|
return deployId
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateApplication(
|
||||||
|
sender: Address,
|
||||||
|
id: number,
|
||||||
|
app: ICompiledApp,
|
||||||
|
signCallback: SignCallback,
|
||||||
|
args?: AlgorandType[],
|
||||||
|
appAccounts?: Address[]
|
||||||
|
): Promise<string> {
|
||||||
|
const params = await this.getParams()
|
||||||
|
const appArgs = args ? encodeArgArray(args) : undefined
|
||||||
|
const txApp = algosdk.makeApplicationUpdateTxn(
|
||||||
|
sender, params, id, app.approval, app.clear, appArgs, appAccounts
|
||||||
|
)
|
||||||
|
const txns = [txApp]
|
||||||
|
return this.signAndSend(txns, signCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeCallTransaction(
|
||||||
|
from: Address,
|
||||||
|
id: number,
|
||||||
|
appOnComplete: OnApplicationComplete = OnApplicationComplete.NoOpOC,
|
||||||
|
args: AlgorandType[] = [],
|
||||||
|
accounts: string[] = [],
|
||||||
|
foreignApps: number[] = [],
|
||||||
|
foreignAssets: number[] = [],
|
||||||
|
txNote = "",
|
||||||
|
fee: number = this.minFee,
|
||||||
|
reKeyTo?: Address
|
||||||
|
): Promise<Transaction> {
|
||||||
|
const suggestedParams = await this.getParams()
|
||||||
|
suggestedParams.fee = fee
|
||||||
|
const appArgs = args.length > 0 ? encodeArgArray(args) : undefined
|
||||||
|
const appAccounts = accounts.length > 0 ? accounts : undefined
|
||||||
|
const appForeignApps = foreignApps.length > 0 ? foreignApps : undefined
|
||||||
|
const appForeignAssets = foreignAssets.length > 0 ? foreignAssets : undefined
|
||||||
|
const note = encodeString(txNote)
|
||||||
|
const txObj: AnyTransaction = {
|
||||||
|
type: TransactionType.appl, from, suggestedParams, appIndex: id,
|
||||||
|
appOnComplete, appArgs, appAccounts, appForeignApps, appForeignAssets, note, reKeyTo
|
||||||
|
}
|
||||||
|
return new Transaction(txObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
async makePayTransaction(
|
||||||
|
from: Address,
|
||||||
|
to: Address,
|
||||||
|
amount: ContractAmount,
|
||||||
|
fee: number = this.minFee,
|
||||||
|
txNote: string | Uint8Array = ""
|
||||||
|
): Promise<Transaction> {
|
||||||
|
const suggestedParams = await this.getParams()
|
||||||
|
suggestedParams.fee = fee as number
|
||||||
|
const note = encodeString(txNote)
|
||||||
|
const txObj: any = {
|
||||||
|
type: TransactionType.pay, from, to, amount, suggestedParams, note
|
||||||
|
}
|
||||||
|
return new Transaction(txObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeAssetTransferTransaction(
|
||||||
|
from: Address,
|
||||||
|
to: Address,
|
||||||
|
assetIndex: number,
|
||||||
|
amount: number | bigint,
|
||||||
|
fee = this.minFee, txNote = ""
|
||||||
|
): Promise<Transaction> {
|
||||||
|
const suggestedParams = await this.getParams()
|
||||||
|
suggestedParams.fee = fee
|
||||||
|
const note = encodeString(txNote)
|
||||||
|
const txObj: any = {
|
||||||
|
type: TransactionType.axfer, from, to, assetIndex, amount, suggestedParams, note
|
||||||
|
}
|
||||||
|
return new Transaction(txObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeAssetCreationTransaction(
|
||||||
|
from: Address,
|
||||||
|
assetTotal: number | bigint,
|
||||||
|
assetDecimals: number,
|
||||||
|
assetUnitName: string,
|
||||||
|
assetName: string,
|
||||||
|
assetURL: string,
|
||||||
|
fee = this.minFee,
|
||||||
|
txNote = ""
|
||||||
|
): Promise<Transaction> {
|
||||||
|
const suggestedParams = await this.getParams()
|
||||||
|
suggestedParams.fee = fee
|
||||||
|
const note = encodeString(txNote)
|
||||||
|
const assetDefaultFrozen = false
|
||||||
|
const assetManager = from
|
||||||
|
const assetReserve = from
|
||||||
|
const assetFreeze = from
|
||||||
|
const assetClawback = from
|
||||||
|
const txObj: any = {
|
||||||
|
type: TransactionType.acfg, from, assetTotal, assetDecimals,
|
||||||
|
assetDefaultFrozen, assetManager, assetReserve, assetFreeze,
|
||||||
|
assetClawback, assetUnitName, assetName, assetURL,
|
||||||
|
suggestedParams, note
|
||||||
|
}
|
||||||
|
return new Transaction(txObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeAssetOptInTransaction(from: Address, assetId: number, fee: number = this.minFee, txNote = ""): Promise<Transaction> {
|
||||||
|
const suggestedParams = await this.getParams()
|
||||||
|
suggestedParams.fee = fee
|
||||||
|
const note = new Uint8Array(Buffer.from(txNote))
|
||||||
|
return algosdk.makeAssetTransferTxnWithSuggestedParams(from, from, undefined, undefined, 0, note, assetId, suggestedParams, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async callApplication(sender: Address, id: number, appOnComplete: OnApplicationComplete,
|
||||||
|
args: AlgorandType[], accounts: string[],
|
||||||
|
foreignApps: number[], foreignAssets: number[],
|
||||||
|
txNote: string, signCallback: SignCallback, fee?: number): Promise<string> {
|
||||||
|
const txApp = await this.makeCallTransaction(sender, id, appOnComplete, args,
|
||||||
|
accounts, foreignApps, foreignAssets, txNote, fee)
|
||||||
|
const txns = [txApp]
|
||||||
|
return this.signAndSend(txns, signCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
assignGroupID(txns: Transaction[]): void {
|
||||||
|
algosdk.assignGroupID(txns)
|
||||||
|
}
|
||||||
|
|
||||||
|
async callGroupTransaction(
|
||||||
|
txns: Transaction[],
|
||||||
|
mappedStateless: Map<Address, LogicSigAccount>,
|
||||||
|
signCallback: SignCallback,
|
||||||
|
dryrunTest?: boolean
|
||||||
|
): Promise<string> {
|
||||||
|
if (txns.length == 0) {
|
||||||
|
throw new Error('Invalid transaction count')
|
||||||
|
}
|
||||||
|
this.assignGroupID(txns)
|
||||||
|
return this.signAndSend(txns, signCallback, mappedStateless, dryrunTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
async dryrunGroupTransaction(
|
||||||
|
txns: Transaction[],
|
||||||
|
mappedStateless: Map<Address, LogicSigAccount>,
|
||||||
|
signCallback: SignCallback,
|
||||||
|
latestTimestamp?: number | bigint,
|
||||||
|
): Promise<any> {
|
||||||
|
this.assignGroupID(txns)
|
||||||
|
const result = await this.dryrunRequest({ transactions: txns, signCallback, stateless: mappedStateless, latestTimestamp })
|
||||||
|
txns.forEach((tx: Transaction) => tx.group = undefined)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseCode(code: string, templateValues?: Map<string, AlgorandType>): string {
|
||||||
|
const substitutions = templateValues ?? new Map();
|
||||||
|
const result = [...substitutions.entries()].reduce((acc, [key, val]) => {
|
||||||
|
let printedVal = ""
|
||||||
|
if (typeof val === "string" || typeof val === "number") {
|
||||||
|
printedVal = val.toString()
|
||||||
|
} else if (val instanceof Uint8Array) {
|
||||||
|
printedVal = '0x' + Buffer.from(val).toString('hex')
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown template type while parsing code for: ${val}`)
|
||||||
|
}
|
||||||
|
return acc.split(key).join(printedVal)
|
||||||
|
}, code);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async compileStateless(pyPath: string, templateValues?: Map<string, AlgorandType>, overrideArgs?: string[]): Promise<LogicSigAccount> {
|
||||||
|
const code = await this.compilePyTeal(pyPath, 1, overrideArgs)
|
||||||
|
return new LogicSigAccount(await this.compileProgram(code[0], templateValues))
|
||||||
|
}
|
||||||
|
|
||||||
|
async readAsset(asset: AssetId): Promise<any> {
|
||||||
|
return this.algodClient.getAssetByID(asset).do()
|
||||||
|
}
|
||||||
|
|
||||||
|
private ALGO_ASSET: Asset = {
|
||||||
|
id: 0,
|
||||||
|
name: "ALGO",
|
||||||
|
unitName: "ALGO",
|
||||||
|
decimals: 6,
|
||||||
|
url: ""
|
||||||
|
}
|
||||||
|
// As the basic asset information is immutable we store it in a local variable
|
||||||
|
private assetsInfo: Map<AssetId, Asset> = new Map()
|
||||||
|
async getAssetInfo(assetId: AssetId): Promise<Asset|undefined> {
|
||||||
|
if (assetId===0) {
|
||||||
|
return this.ALGO_ASSET
|
||||||
|
}
|
||||||
|
let assetInfo = this.assetsInfo.get(assetId)
|
||||||
|
if (!assetInfo) {
|
||||||
|
const assetFromAlgorand = await this.readAsset(assetId)
|
||||||
|
if (assetFromAlgorand) {
|
||||||
|
assetInfo = {
|
||||||
|
id: assetId,
|
||||||
|
name: assetFromAlgorand.params['name'],
|
||||||
|
unitName: assetFromAlgorand.params['unit-name'],
|
||||||
|
decimals: assetFromAlgorand.params['decimals'],
|
||||||
|
url: assetFromAlgorand.params['url']
|
||||||
|
}
|
||||||
|
this.assetsInfo.set(assetId, assetInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assetInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
async readAccount(from: Address): Promise<any> {
|
||||||
|
return this.algodClient.accountInformation(from).do()
|
||||||
|
}
|
||||||
|
|
||||||
|
async readCreatedApps(from: Address): Promise<Record<string, string>> {
|
||||||
|
const response = await this.readAccount(from)
|
||||||
|
return response['created-apps'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async readCreatedAssets(from: Address): Promise<Record<string, string>> {
|
||||||
|
const response = await this.readAccount(from)
|
||||||
|
return response['created-assets'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async readOptedInApps(from: Address): Promise<Record<string, any>[]> {
|
||||||
|
const response = await this.readAccount(from)
|
||||||
|
return response['apps-local-state'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async readOptedInAssets(from: Address): Promise<Record<string, string>[]> {
|
||||||
|
const response = await this.readAccount(from)
|
||||||
|
return response['assets'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async readAmount(from: Address): Promise<ContractAmount> {
|
||||||
|
const response = await this.readAccount(from)
|
||||||
|
return BigInt(response['amount'])
|
||||||
|
}
|
||||||
|
|
||||||
|
async readAssetBalances(from: Address): Promise<Map<AssetId, ContractAmount>> {
|
||||||
|
const assets = await this.readOptedInAssets(from)
|
||||||
|
return new Map(assets.map((asset: Record<string, any>) => [asset['asset-id'], BigInt(asset['amount'])]))
|
||||||
|
}
|
||||||
|
|
||||||
|
async readAssetAmount(from: Address, id: AssetId): Promise<ContractAmount> {
|
||||||
|
return (await this.readAssetBalances(from)).get(id) ?? BigInt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllAppGlobalState(id: AppId): Promise<{ key: string, value: { bytes: string, type: number, uint: number } }[] | undefined> {
|
||||||
|
const response = await this.algodClient.getApplicationByID(id).do()
|
||||||
|
return response.params['global-state']
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Extend to handle local state as well
|
||||||
|
public async getAppStateInfo(id: AppId): Promise<IStateInfo> {
|
||||||
|
const state = await this.getAllAppGlobalState(id)
|
||||||
|
if (!state) {
|
||||||
|
throw new Error('App state is missing')
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalPairs = state.map(entry => [decodeString(decodeBase64(entry.key)), entry.value.type === 0 ? 'uint' : 'bytes'] as [string, IStateType])
|
||||||
|
|
||||||
|
return {
|
||||||
|
global: Object.fromEntries(globalPairs),
|
||||||
|
local: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readAppGlobalState(id: AppId, stateInfo: IStateInfo, errorOnMissing = true): Promise<IState> {
|
||||||
|
const app = await this.algodClient.getApplicationByID(id).do()
|
||||||
|
const state = app.params['global-state']
|
||||||
|
return decodeState(state, stateInfo.global, errorOnMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readAppLocalState(id: AppId, from: Address, stateInfo: IStateInfo, errorOnMissing = true): Promise<IState> {
|
||||||
|
const info = await this.readAccount(from)
|
||||||
|
const state = info['apps-local-state'].find((v: any) => v['id'] === id)
|
||||||
|
if (!state)
|
||||||
|
throw new Error("No local state found for address " + from)
|
||||||
|
return decodeState(state['key-value'], stateInfo.local, errorOnMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteApps(address: Address, signCallback: SignCallback) {
|
||||||
|
const apps: any = await this.readCreatedApps(address)
|
||||||
|
await Promise.all(apps.map(async (app: any) => {
|
||||||
|
const txID = await this.deleteApplication(address, app.id, signCallback);
|
||||||
|
await this.waitForTransactionResponse(txID);
|
||||||
|
console.log(`Application Deleted -> TxID: ${txID}`);
|
||||||
|
}))
|
||||||
|
console.log("Deletion finished.")
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearApps(address: Address, signCallback: SignCallback) {
|
||||||
|
const apps: any = await this.readOptedInApps(address)
|
||||||
|
await Promise.all(apps.map(async (app: any) => {
|
||||||
|
const txID = await this.clearApplication(address, app.id, signCallback);
|
||||||
|
await this.waitForTransactionResponse(txID);
|
||||||
|
console.log(`Cleared from Application -> TxID: ${txID}`);
|
||||||
|
}))
|
||||||
|
console.log("Clear finished.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Include the remaining compile steps in this function, so it returns the entire compiled program ready to use all in one step
|
||||||
|
async compilePyTeal(pytealSourceFile: string, outputCount: number, overrideArgs?: string[]): Promise<string[]> {
|
||||||
|
// Check the in-memory cache to perform fewer file stats
|
||||||
|
const cached = Deployer.tealCache.get(pytealSourceFile)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate compile directory for teal files
|
||||||
|
const tealPath = '../../../.teal'
|
||||||
|
if (!fs.existsSync(tealPath)) {
|
||||||
|
fs.mkdirSync(tealPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique name
|
||||||
|
const fileBody = fs.readFileSync(pytealSourceFile)
|
||||||
|
const nonce = crypto.createHash('sha256').update(fileBody).digest('hex')
|
||||||
|
const outputPaths = [...Array(outputCount)].map((_, index) => path.join(tealPath, `${path.basename(pytealSourceFile, '.py')}-${index}-${nonce}.teal`))
|
||||||
|
|
||||||
|
// Check disk cache to skip compile if we can
|
||||||
|
const alreadyExists = outputPaths.reduce((accum, p) => accum && fs.existsSync(p), true)
|
||||||
|
if (!alreadyExists) {
|
||||||
|
// Run current program
|
||||||
|
const pythonCommand = 'python3.10'
|
||||||
|
const preArgs = overrideArgs ?? []
|
||||||
|
const args = [...preArgs, ...outputPaths]
|
||||||
|
const cmd = `${pythonCommand} "${pytealSourceFile}" ${args.join(' ')}`
|
||||||
|
console.log(`Running command ${cmd}`)
|
||||||
|
const logs = await util.promisify(child_process.exec)(cmd)
|
||||||
|
if (logs.stderr && logs.stderr.length > 0) {
|
||||||
|
throw Error(`Could not compile file: ${pytealSourceFile} with ${pythonCommand}.\nError: ${logs.stderr}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather results
|
||||||
|
const results = outputPaths.map(p => fs.readFileSync(p, 'utf-8'))
|
||||||
|
|
||||||
|
// Update in-memory cache
|
||||||
|
Deployer.tealCache.set(pytealSourceFile, results)
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRedeemWormholeTransactions(vaa: Uint8Array, sender: string): Promise<TransactionSignerPair[]> {
|
||||||
|
const redeemTransactions: TransactionSignerPair[] = await redeemOnAlgorand(
|
||||||
|
this.algodClient,
|
||||||
|
this.wormholeConfig.tokenBridgeAppId,
|
||||||
|
this.wormholeConfig.coreBridgeAppId,
|
||||||
|
new Uint8Array(vaa),
|
||||||
|
sender
|
||||||
|
)
|
||||||
|
return redeemTransactions
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,579 @@
|
||||||
|
import { decodeAddress, encodeAddress, isValidAddress } from 'algosdk'
|
||||||
|
import sha512 from "js-sha512"
|
||||||
|
import { Buffer } from 'buffer'
|
||||||
|
import { Address } from './AlgorandTypes'
|
||||||
|
import assert from 'assert'
|
||||||
|
import { ethers } from 'ethers'
|
||||||
|
|
||||||
|
export const TEAL_SIGNATURE_LENGTH = 64
|
||||||
|
export const SHA256_HASH_LENGTH = 32
|
||||||
|
|
||||||
|
export type AlgorandType = bigint | string | boolean | number | Uint8Array
|
||||||
|
|
||||||
|
export type IPackedInfoFixed = {
|
||||||
|
type: "uint" | "number" | "address" | "double" | "boolean" | "emptyString"
|
||||||
|
}
|
||||||
|
export type IPackedInfoVariable = {
|
||||||
|
type: "string" | "bytes" | "base64"
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
export type IPackedInfoObject = {
|
||||||
|
type: "object" | "hash"
|
||||||
|
info: IPackedInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IPackedInfoArray = {
|
||||||
|
type: "array"
|
||||||
|
info: IPackedInfoAny
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IPackedInfoFixedBytes = {
|
||||||
|
type: "fixed"
|
||||||
|
valueHex: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IPackedInfoAny = IPackedInfoFixed | IPackedInfoVariable | IPackedInfoObject | IPackedInfoArray | IPackedInfoFixedBytes
|
||||||
|
export type IPackedInfo = Record<string, IPackedInfoAny>
|
||||||
|
|
||||||
|
export type IStateType = 'uint' | 'bytes'
|
||||||
|
export type IStateMap = Record<string, IStateType>
|
||||||
|
|
||||||
|
export type IStateVar = Uint8Array | bigint
|
||||||
|
export type IState = Record<string, IStateVar>
|
||||||
|
|
||||||
|
// NOTE: !!!! ONLY MODIFY THIS BY APPENDING TO THE END. THE INDEXES EFFECT THE MERKLE LOG HASH VALUES !!!!
|
||||||
|
export const packedTypeMap = [
|
||||||
|
"uint",
|
||||||
|
"number",
|
||||||
|
"address",
|
||||||
|
"double",
|
||||||
|
"boolean",
|
||||||
|
"string",
|
||||||
|
"bytes",
|
||||||
|
"base64",
|
||||||
|
"object",
|
||||||
|
"hash",
|
||||||
|
"array",
|
||||||
|
"emptyString",
|
||||||
|
"fixed",
|
||||||
|
]
|
||||||
|
|
||||||
|
assert(packedTypeMap.length < 128, 'Too many types in packedTypeMap')
|
||||||
|
|
||||||
|
export function concatArrays(arrays: Uint8Array[]): Uint8Array {
|
||||||
|
const totalLength = arrays.reduce((accum, x) => accum + x.length, 0)
|
||||||
|
const result = new Uint8Array(totalLength)
|
||||||
|
|
||||||
|
for (let i = 0, offset = 0; i < arrays.length; i++) {
|
||||||
|
result.set(arrays[i], offset)
|
||||||
|
offset += arrays[i].length
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the format itself as part of the data for forward compatibility
|
||||||
|
export function packFormat(format: IPackedInfo): Uint8Array {
|
||||||
|
const chunks: Uint8Array[] = []
|
||||||
|
|
||||||
|
// NOTE: Byte-size fields are capped at 128 to allow for future expansion with varints
|
||||||
|
// Encode number of fields
|
||||||
|
const fieldCount = Object.entries(format).length
|
||||||
|
assert(fieldCount < 128, `Too many fields in object: ${fieldCount}`)
|
||||||
|
chunks.push(new Uint8Array([fieldCount]))
|
||||||
|
|
||||||
|
for (const [name, type] of Object.entries(format)) {
|
||||||
|
// Encode name and type index
|
||||||
|
assert(name.length < 128, `Name of property ${name} too long`)
|
||||||
|
chunks.push(new Uint8Array([name.length]))
|
||||||
|
chunks.push(encodeString(name))
|
||||||
|
|
||||||
|
const typeIndex = packedTypeMap.indexOf(type.type)
|
||||||
|
assert(typeIndex >= 0, 'Type index not found in packedTypeMap')
|
||||||
|
|
||||||
|
chunks.push(new Uint8Array([typeIndex]))
|
||||||
|
|
||||||
|
// For complex types, encode additional data
|
||||||
|
switch (type.type) {
|
||||||
|
case "string":
|
||||||
|
case "bytes":
|
||||||
|
case "base64":
|
||||||
|
assert(type.size < 128, `Sized data was too large: ${type.size}`)
|
||||||
|
chunks.push(new Uint8Array([type.size]))
|
||||||
|
break
|
||||||
|
|
||||||
|
case "hash":
|
||||||
|
case "object":
|
||||||
|
case "array": {
|
||||||
|
const format = packFormat(type.type === 'array' ? { value: type.info } : type.info)
|
||||||
|
chunks.push(encodeUint64(format.length))
|
||||||
|
chunks.push(format)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return concatArrays(chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpackFormat(data: Uint8Array): IPackedInfo {
|
||||||
|
let index = 0
|
||||||
|
// Decode field count
|
||||||
|
const fieldCount = data[index]
|
||||||
|
index++
|
||||||
|
|
||||||
|
const format: IPackedInfo = {}
|
||||||
|
for (let i = 0; i < fieldCount; i++) {
|
||||||
|
// Decode name
|
||||||
|
const nameLen = data[index]
|
||||||
|
index++
|
||||||
|
|
||||||
|
const name = decodeString(data.slice(index, index + nameLen))
|
||||||
|
index += nameLen
|
||||||
|
|
||||||
|
// Decode type
|
||||||
|
const type = packedTypeMap[data[index]]
|
||||||
|
index++
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "uint":
|
||||||
|
case "number":
|
||||||
|
case "address":
|
||||||
|
case "double":
|
||||||
|
case "boolean":
|
||||||
|
case "emptyString":
|
||||||
|
format[name] = { type }
|
||||||
|
break
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
case "bytes":
|
||||||
|
case "base64": {
|
||||||
|
const size = data[index]
|
||||||
|
index++
|
||||||
|
|
||||||
|
format[name] = { type, size }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "object":
|
||||||
|
case "hash":
|
||||||
|
case "array": {
|
||||||
|
const length = Number(decodeUint64(data.slice(index, index + 8)))
|
||||||
|
index += 8
|
||||||
|
|
||||||
|
const info = unpackFormat(data.slice(index, index + length))
|
||||||
|
index += length
|
||||||
|
|
||||||
|
if (type === "array") {
|
||||||
|
format[name] = { type, info: info.value }
|
||||||
|
} else {
|
||||||
|
format[name] = { type, info }
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
export function packData(value: Record<string, any>, format: IPackedInfo, includeType = false): Uint8Array {
|
||||||
|
const chunks: Uint8Array[] = []
|
||||||
|
|
||||||
|
if (includeType) {
|
||||||
|
const packedFormat = packFormat(format)
|
||||||
|
chunks.push(encodeUint64(packedFormat.length))
|
||||||
|
chunks.push(packedFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the data fields
|
||||||
|
for (const [name, type] of Object.entries(format)) {
|
||||||
|
const v = value[name]
|
||||||
|
if (v === undefined && type.type !== 'fixed') {
|
||||||
|
throw new Error(`Key "${name}" missing from value:\n${value.keys}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type.type) {
|
||||||
|
case 'object':
|
||||||
|
if (v instanceof Object) {
|
||||||
|
chunks.push(packData(v, type.info, false))
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected object, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'hash':
|
||||||
|
if (v instanceof Object) {
|
||||||
|
// NOTE: Hashes always refer to the typed version of the data to enable forward compatibility
|
||||||
|
chunks.push(sha256Hash(packData(v, type.info, true)))
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected object for hashing, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'array':
|
||||||
|
if (v instanceof Array) {
|
||||||
|
assert(v.length < 128, `Array too large to be encoded: ${v}`)
|
||||||
|
chunks.push(new Uint8Array([v.length]))
|
||||||
|
v.forEach((value) => {
|
||||||
|
chunks.push(packData({ value }, { value: type.info }, false))
|
||||||
|
})
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected array, got ${v}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'address':
|
||||||
|
if (v instanceof Uint8Array) {
|
||||||
|
if (v.length === 20) {
|
||||||
|
const newValue = new Uint8Array(32)
|
||||||
|
newValue.set(new TextEncoder().encode("EthereumAddr"))
|
||||||
|
newValue.set(v, 12)
|
||||||
|
chunks.push(newValue)
|
||||||
|
} else if (v.length === 32) {
|
||||||
|
chunks.push(v)
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid address byte array length ${v.length}, expected 20 or 32`)
|
||||||
|
}
|
||||||
|
} else if (typeof v === 'string') {
|
||||||
|
if (ethers.utils.isAddress(v)) {
|
||||||
|
const newValue = new Uint8Array(32)
|
||||||
|
newValue.set(new TextEncoder().encode("EthereumAddr"))
|
||||||
|
newValue.set(Buffer.from(v.slice(2), 'hex'), 12)
|
||||||
|
chunks.push(newValue)
|
||||||
|
} else if (isValidAddress(v)) {
|
||||||
|
chunks.push(decodeAddress(v).publicKey)
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid address string ${v}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected address, got ${v}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'bytes':
|
||||||
|
if (v instanceof Uint8Array) {
|
||||||
|
if (v.length === type.size) {
|
||||||
|
chunks.push(v)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Bytes length is wrong, expected ${type.size}, got ${v.length}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected bytes[${type.size}], got ${v}`)
|
||||||
|
}
|
||||||
|
case 'base64':
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
try {
|
||||||
|
const bytes = decodeBase64(v)
|
||||||
|
if (bytes.length === type.size) {
|
||||||
|
chunks.push(bytes)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Base64 length is wrong, expected ${type.size}, got ${bytes.length}`)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${name}: Base64 encoding is wrong, got ${v}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected Base64 string, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'double':
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
const bytes = new ArrayBuffer(8)
|
||||||
|
Buffer.from(bytes).writeDoubleLE(v, 0)
|
||||||
|
chunks.push(new Uint8Array(bytes))
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected double, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'boolean':
|
||||||
|
if (typeof v === 'boolean') {
|
||||||
|
chunks.push(new Uint8Array([v ? 1 : 0]))
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected boolean, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'number':
|
||||||
|
case 'uint':
|
||||||
|
if (typeof v === 'bigint' || typeof v === 'number') {
|
||||||
|
chunks.push(encodeUint64(v))
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected uint or number, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'string':
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
const str = encodeString(v)
|
||||||
|
if (str.length === type.size) {
|
||||||
|
chunks.push(str)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected string length ${type.size}, got string length ${str.length}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected string length ${type.size}, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'emptyString':
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
throw new Error(`${name}: Expected string, got ${v}`)
|
||||||
|
}
|
||||||
|
case 'fixed':
|
||||||
|
chunks.push(decodeBase16(type.valueHex))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return concatArrays(chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpackData(data: Uint8Array, formatOpt?: IPackedInfo): Record<string, any> {
|
||||||
|
let format: IPackedInfo
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
// Decode format
|
||||||
|
if (formatOpt) {
|
||||||
|
format = formatOpt
|
||||||
|
} else {
|
||||||
|
const length = Number(decodeUint64(data.slice(index, index + 8)))
|
||||||
|
index += 8
|
||||||
|
|
||||||
|
format = unpackFormat(data.slice(index, index + length))
|
||||||
|
index += length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode data
|
||||||
|
// NOTE: This needs to be an inner function to maintain the index across calls
|
||||||
|
const unpackInner = (data: Uint8Array, format: IPackedInfo) => {
|
||||||
|
const object: Record<string, any> = {}
|
||||||
|
for (const [name, type] of Object.entries(format)) {
|
||||||
|
if (index >= data.length) {
|
||||||
|
throw new Error(`Unpack data length was not enough for the format provided. Data: ${data}, format: ${JSON.stringify(format)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: any
|
||||||
|
switch (type.type) {
|
||||||
|
case 'object':
|
||||||
|
value = unpackInner(data, type.info)
|
||||||
|
break
|
||||||
|
case 'hash':
|
||||||
|
value = new Uint8Array(data.slice(index, index + SHA256_HASH_LENGTH))
|
||||||
|
index += SHA256_HASH_LENGTH
|
||||||
|
break
|
||||||
|
case 'array': {
|
||||||
|
const count = data[index++]
|
||||||
|
value = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
value.push(unpackInner(data, { value: type.info }).value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'address':
|
||||||
|
value = encodeAddress(data.slice(index, index + 32))
|
||||||
|
index += 32
|
||||||
|
break
|
||||||
|
case 'bytes':
|
||||||
|
value = new Uint8Array(data.slice(index, index + type.size))
|
||||||
|
index += type.size
|
||||||
|
break
|
||||||
|
case 'base64':
|
||||||
|
value = encodeBase64(data.slice(index, index + type.size))
|
||||||
|
index += type.size
|
||||||
|
break
|
||||||
|
case 'double':
|
||||||
|
value = Buffer.from(data.slice(index, index + 8)).readDoubleLE(0)
|
||||||
|
index += 8
|
||||||
|
break
|
||||||
|
case 'boolean':
|
||||||
|
value = data.slice(index, index + 1)[0] === 1
|
||||||
|
index += 1
|
||||||
|
break
|
||||||
|
case 'number':
|
||||||
|
value = Number(decodeUint64(data.slice(index, index + 8)))
|
||||||
|
index += 8
|
||||||
|
break
|
||||||
|
case 'uint':
|
||||||
|
value = decodeUint64(data.slice(index, index + 8))
|
||||||
|
index += 8
|
||||||
|
break
|
||||||
|
case 'string':
|
||||||
|
value = decodeString(data.slice(index, index + type.size))
|
||||||
|
index += type.size
|
||||||
|
break
|
||||||
|
case 'emptyString':
|
||||||
|
value = ""
|
||||||
|
break
|
||||||
|
case 'fixed':
|
||||||
|
value = decodeBase16(type.valueHex)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown decode type: ${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
object[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = unpackInner(data, format)
|
||||||
|
|
||||||
|
if (index !== data.length) {
|
||||||
|
throw new Error(`Data consumed(${index} bytes) did not match expected (${data.length} bytes) for format\nFormat: ${JSON.stringify(format)}\nValue: ${Buffer.from(data).toString('hex')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeArgArray(params: AlgorandType[]): Uint8Array[] {
|
||||||
|
return params.map(param => {
|
||||||
|
if (param instanceof Uint8Array)
|
||||||
|
return new Uint8Array(param)
|
||||||
|
if (typeof param === "string")
|
||||||
|
return encodeString(param)
|
||||||
|
if (typeof param === "boolean")
|
||||||
|
param = BigInt(param ? 1 : 0)
|
||||||
|
if (typeof param === "number")
|
||||||
|
param = BigInt(param)
|
||||||
|
return encodeUint64(param)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeString(value: string | Uint8Array): Uint8Array {
|
||||||
|
return new Uint8Array(Buffer.from(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeString(value: Uint8Array): string {
|
||||||
|
return Buffer.from(value).toString('utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeState(state: Record<string, Record<string, string>>[], stateMap: IStateMap, errorOnMissing = true): IState {
|
||||||
|
const result: IState = {}
|
||||||
|
for (const [name, type] of Object.entries(stateMap)) {
|
||||||
|
const stateName = encodeBase64(encodeString(name))
|
||||||
|
const key = state.find((v: any) => v['key'] === stateName)
|
||||||
|
if (errorOnMissing && key === undefined) {
|
||||||
|
throw new Error(`Expected key ${name} was not found in state`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = key ? key['value'][type] : undefined
|
||||||
|
if (errorOnMissing && value === undefined) {
|
||||||
|
throw new Error(`Expected value for key ${name} was not found in state`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const typedValue = type === 'bytes' ? decodeBase64(value ?? '') : BigInt(value ?? '')
|
||||||
|
result[name] = typedValue
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeUint64(value: number | bigint): Uint8Array {
|
||||||
|
const bytes: Buffer = Buffer.alloc(8)
|
||||||
|
for (let index = 0; index < 8; index++)
|
||||||
|
bytes[7 - index] = Number((BigInt(value) >> BigInt(index * 8)) & BigInt(0xFF))
|
||||||
|
return new Uint8Array(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeUint64(value: Uint8Array): bigint {
|
||||||
|
assert(value.length >= 8, `Expected at least 8 bytes to decode a uint64, but got ${value.length} bytes\nValue: ${Buffer.from(value).toString('hex')}`)
|
||||||
|
|
||||||
|
let num = BigInt(0)
|
||||||
|
for (let index = 0; index < 8; index++) {
|
||||||
|
num = (num << BigInt(8)) | BigInt(value[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeUint32(value: number): Uint8Array {
|
||||||
|
if (value >= 2 ** 32 || value < 0) {
|
||||||
|
throw new Error(`Out of bound value in Uint16: ${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes: Buffer = Buffer.alloc(4)
|
||||||
|
for (let index = 0; index < 4; index++)
|
||||||
|
bytes[3 - index] = Number((BigInt(value) >> BigInt(index * 8)) & BigInt(0xFF))
|
||||||
|
return new Uint8Array(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeUint32(value: Uint8Array): number {
|
||||||
|
let num = BigInt(0)
|
||||||
|
for (let index = 0; index < 4; index++)
|
||||||
|
num = (num << BigInt(8)) | BigInt(value[index])
|
||||||
|
return Number(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeUint16(value: number): Uint8Array {
|
||||||
|
if (value >= 2 ** 16 || value < 0) {
|
||||||
|
throw new Error(`Out of bound value in Uint16: ${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array([value >> 8, value & 0xFF])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeUint16(value: Uint8Array): number {
|
||||||
|
if (value.length !== 2) {
|
||||||
|
throw new Error(`Invalid value length, expected 2, got ${value.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value[0] * 256 + value[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeUint8(value: number): Uint8Array {
|
||||||
|
if (value >= 2 ** 8 || value < 0) {
|
||||||
|
throw new Error(`Out of bound value in Uint8: ${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array([value])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeBase16(value: string): Uint8Array {
|
||||||
|
return Buffer.from(value, 'hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeBase64(value: Uint8Array): string {
|
||||||
|
return Buffer.from(value).toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeBase64(value: string): Uint8Array {
|
||||||
|
return Buffer.from(value, 'base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha256Hash(arr: sha512.Message): Uint8Array {
|
||||||
|
return new Uint8Array(sha512.sha512_256.arrayBuffer(arr))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeApplicationAddress(id: number): Address {
|
||||||
|
const APP_ID_PREFIX = Buffer.from('appID');
|
||||||
|
const toBeSigned = concatArrays([APP_ID_PREFIX, encodeUint64(BigInt(id))]);
|
||||||
|
return encodeAddress(sha256Hash(toBeSigned));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareArrays(a: Uint8Array[], b: Uint8Array[]) {
|
||||||
|
return (a===undefined || b===undefined) ? a===b : (a.length === b.length && a.reduce((equal, item, index) => equal && item===b[index], true))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDelta(response: any, key: string): any | undefined {
|
||||||
|
const delta = response['global-state-delta'].find((v: any) => v.key === key)
|
||||||
|
if (delta === undefined)
|
||||||
|
return undefined
|
||||||
|
return delta['value']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeltaUint(response: any, key: string): bigint | undefined {
|
||||||
|
const delta = getDelta(response, key)
|
||||||
|
if (delta === undefined)
|
||||||
|
return undefined
|
||||||
|
return BigInt(delta['uint'])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeltaBytes(response: any, key: string): Uint8Array | undefined {
|
||||||
|
const delta = getDelta(response, key)
|
||||||
|
if (delta === undefined)
|
||||||
|
return undefined
|
||||||
|
return decodeBase64(delta['bytes'])
|
||||||
|
}
|
||||||
|
|
||||||
|
export { encodeAddress } from 'algosdk'
|
|
@ -0,0 +1,62 @@
|
||||||
|
export const DEVELOPMENT_KMD_TOKEN = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
export const DEVELOPMENT_KMD_HOST = "localhost"
|
||||||
|
export const DEVELOPMENT_KMD_PORT = 4002
|
||||||
|
|
||||||
|
export type AlgorandServerConnectionConfig = {
|
||||||
|
token: string,
|
||||||
|
server: string,
|
||||||
|
port: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExecutionEnvironmentConfig = {
|
||||||
|
algod: AlgorandServerConnectionConfig,
|
||||||
|
kmd?: AlgorandServerConnectionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountSigningData = {
|
||||||
|
mnemonic: string
|
||||||
|
} | {
|
||||||
|
secretKey: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Mnemonic = string
|
||||||
|
export type TestExecutionEnvironmentConfig = ExecutionEnvironmentConfig & {
|
||||||
|
masterAccount: Mnemonic
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BETANET_CONFIG: TestExecutionEnvironmentConfig = {
|
||||||
|
algod: {token: "", server: "https://node.betanet.algoexplorerapi.io", port: ""},
|
||||||
|
masterAccount: "rate firm prefer portion innocent public large original fit shoulder solve scorpion battle end jealous off pause inner toddler year grab chaos result about capital"
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Path: Direct path to sandbox executable, in this example
|
||||||
|
* since I use Windows OS i had to add sh command in front of full path
|
||||||
|
* In my case I downloaded sandbox in disk D:\
|
||||||
|
*/
|
||||||
|
export const LOCAL_CONFIG: TestExecutionEnvironmentConfig = {
|
||||||
|
algod: {
|
||||||
|
token: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
server: "http://localhost",
|
||||||
|
port: 4001
|
||||||
|
},
|
||||||
|
// Public Key: HL6A24OGJX4FDZT36HOQ6VWJDF6GW3IEWB4FXB4OH5FQKVI46HZBZOZFAM
|
||||||
|
masterAccount: "general foster traffic label come once baby attract travel nose clap mystery want problem beyond side wing bridge drastic one sun diet trigger absent fossil"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WormholeConfig = {
|
||||||
|
coreBridgeAppId: bigint,
|
||||||
|
tokenBridgeAppId: bigint
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORMHOLE_CONFIG_MAINNET: WormholeConfig = {
|
||||||
|
coreBridgeAppId: BigInt("0"),
|
||||||
|
tokenBridgeAppId: BigInt("0")
|
||||||
|
}
|
||||||
|
export const WORMHOLE_CONFIG_TESTNET: WormholeConfig = {
|
||||||
|
coreBridgeAppId: BigInt("86525623"),
|
||||||
|
tokenBridgeAppId: BigInt("86525641")
|
||||||
|
}
|
||||||
|
export const WORMHOLE_CONFIG_DEVNET: WormholeConfig = {
|
||||||
|
coreBridgeAppId: BigInt("4"),
|
||||||
|
tokenBridgeAppId: BigInt("6")
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import algosdk, { Transaction } from 'algosdk'
|
||||||
|
import { Address } from './AlgorandTypes'
|
||||||
|
import { SignCallback, TealSignCallback } from './Deployer'
|
||||||
|
|
||||||
|
export class Signer {
|
||||||
|
private signatures: Map<Address, Uint8Array> = new Map()
|
||||||
|
readonly callback: SignCallback
|
||||||
|
readonly tealCallback: TealSignCallback
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.callback = this.sign.bind(this)
|
||||||
|
this.tealCallback = this.tealSign.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrivateKey(addr: Address): Uint8Array {
|
||||||
|
const pk = this.signatures.get(addr)
|
||||||
|
if (pk === undefined)
|
||||||
|
throw new Error("Couldn't find account " + addr + " for signing")
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
addFromMnemonic(mnemonic: string): Address {
|
||||||
|
const account = algosdk.mnemonicToSecretKey(mnemonic)
|
||||||
|
this.signatures.set(account.addr, account.sk)
|
||||||
|
return account.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
addFromSecretKey(secretKey: Uint8Array): Address {
|
||||||
|
const mnemonic = algosdk.secretKeyToMnemonic(secretKey)
|
||||||
|
return this.addFromMnemonic(mnemonic)
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccount(): Address {
|
||||||
|
const { sk: secretKey, addr: address } = algosdk.generateAccount();
|
||||||
|
this.signatures.set(address, secretKey)
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
async sign(txs: Transaction[]): Promise<Uint8Array[]> {
|
||||||
|
return Promise.all(txs.map(async tx => {
|
||||||
|
const sender = algosdk.encodeAddress(tx.from.publicKey)
|
||||||
|
return tx.signTxn(this.getPrivateKey(sender))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSign(txs: Transaction[]): Uint8Array[] {
|
||||||
|
return txs.map(tx => {
|
||||||
|
const sender = algosdk.encodeAddress(tx.from.publicKey)
|
||||||
|
return tx.rawSignTxn(this.getPrivateKey(sender))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async tealSign(data: Uint8Array, from: Address, to: Address): Promise<Uint8Array> {
|
||||||
|
return algosdk.tealSign(this.getPrivateKey(from), data, to)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'varint' {
|
||||||
|
export function encode(n: number): Uint8Array
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
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))))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,262 @@
|
||||||
|
import { decodeAddress, getApplicationAddress, LogicSigAccount, OnApplicationComplete, Transaction } from 'algosdk'
|
||||||
|
import { Address, AppId } from '../sdk/AlgorandTypes'
|
||||||
|
import { concatArrays, encodeUint16 } from "../sdk/Encoding"
|
||||||
|
import { Deployer, SignCallback, IStateInfo } from "../sdk/Deployer"
|
||||||
|
import path from 'path'
|
||||||
|
import assert from 'assert'
|
||||||
|
import { SignedVAA, WormholeSigner } from './WormholeTypes'
|
||||||
|
import { signWormholeMessage } from './WormholeEncoders'
|
||||||
|
import { generateInitVAA } from './WormholeVAA'
|
||||||
|
import { EMITTER_GUARDIAN, WormholeTmplSig } from './WormholeTmplSig'
|
||||||
|
|
||||||
|
export class Wormhole {
|
||||||
|
public static BASE_PATH = path.resolve(__dirname, '../../../')
|
||||||
|
|
||||||
|
public static CORE_STATE_MAP: IStateInfo = {
|
||||||
|
local: {
|
||||||
|
'meta': 'bytes',
|
||||||
|
'\x00': 'bytes',
|
||||||
|
'\x01': 'bytes',
|
||||||
|
'\x02': 'bytes',
|
||||||
|
'\x03': 'bytes',
|
||||||
|
'\x04': 'bytes',
|
||||||
|
'\x05': 'bytes',
|
||||||
|
'\x06': 'bytes',
|
||||||
|
'\x07': 'bytes',
|
||||||
|
'\x08': 'bytes',
|
||||||
|
'\x09': 'bytes',
|
||||||
|
'\x0A': 'bytes',
|
||||||
|
'\x0B': 'bytes',
|
||||||
|
'\x0C': 'bytes',
|
||||||
|
'\x0D': 'bytes',
|
||||||
|
'\x0E': 'bytes',
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
'MessageFee': 'uint',
|
||||||
|
'currentGuardianSetIndex': 'uint',
|
||||||
|
'booted': 'bytes',
|
||||||
|
'vphash': 'bytes',
|
||||||
|
'validUpdateApproveHash': 'bytes',
|
||||||
|
'validUpdateClearHash': 'bytes',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BRIDGE_STATE_MAP: IStateInfo = {
|
||||||
|
local: {},
|
||||||
|
global: {
|
||||||
|
'coreid': 'uint',
|
||||||
|
'coreAddr': 'bytes',
|
||||||
|
'validUpdateApproveHash': 'bytes',
|
||||||
|
'validUpdateClearHash': 'bytes',
|
||||||
|
|
||||||
|
'chain\x00': 'bytes',
|
||||||
|
'chain\x01': 'bytes',
|
||||||
|
'chain\x02': 'bytes',
|
||||||
|
'chain\x03': 'bytes',
|
||||||
|
'chain\x04': 'bytes',
|
||||||
|
'chain\x05': 'bytes',
|
||||||
|
'chain\x06': 'bytes',
|
||||||
|
'chain\x07': 'bytes',
|
||||||
|
'chain\x08': 'bytes',
|
||||||
|
'chain\x09': 'bytes',
|
||||||
|
|
||||||
|
'chain\x0A': 'bytes',
|
||||||
|
'chain\x0B': 'bytes',
|
||||||
|
'chain\x0C': 'bytes',
|
||||||
|
'chain\x0D': 'bytes',
|
||||||
|
'chain\x0E': 'bytes',
|
||||||
|
'chain\x0F': 'bytes',
|
||||||
|
'chain\x10': 'bytes',
|
||||||
|
'chain\x11': 'bytes',
|
||||||
|
'chain\x12': 'bytes',
|
||||||
|
'chain\x13': 'bytes',
|
||||||
|
|
||||||
|
'chain\x14': 'bytes',
|
||||||
|
'chain\x15': 'bytes',
|
||||||
|
'chain\x16': 'bytes',
|
||||||
|
'chain\x17': 'bytes',
|
||||||
|
'chain\x18': 'bytes',
|
||||||
|
'chain\x19': 'bytes',
|
||||||
|
'chain\x1A': 'bytes',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly deployer: Deployer,
|
||||||
|
private readonly owner: Address,
|
||||||
|
public readonly coreId: AppId,
|
||||||
|
public readonly bridgeId: AppId,
|
||||||
|
private readonly signCallback: SignCallback,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static async deployAndFund(
|
||||||
|
deployer: Deployer,
|
||||||
|
owner: Address,
|
||||||
|
signCallback: SignCallback,
|
||||||
|
signers: WormholeSigner[],
|
||||||
|
): Promise<Wormhole> {
|
||||||
|
// Generate paths
|
||||||
|
const corePath = path.join(Wormhole.BASE_PATH, 'wormhole_core.py')
|
||||||
|
const bridgePath = path.join(Wormhole.BASE_PATH, 'token_bridge.py')
|
||||||
|
|
||||||
|
// Deploy core contract
|
||||||
|
const coreApp = await deployer.makeSourceApp(corePath, Wormhole.CORE_STATE_MAP)
|
||||||
|
const coreCompiled = await deployer.makeApp(coreApp)
|
||||||
|
const coreDeployId = await deployer.deployApplication(owner, coreCompiled, signCallback)
|
||||||
|
const coreId = (await deployer.waitForTransactionResponse(coreDeployId))["application-index"]
|
||||||
|
console.log(`Wormhole core ID: ${coreId}`)
|
||||||
|
console.log(`Core address: ${getApplicationAddress(coreId)}, base64: ${Buffer.from(decodeAddress(getApplicationAddress(coreId)).publicKey).toString('base64')}`)
|
||||||
|
|
||||||
|
// Deploy token bridge
|
||||||
|
const bridgeApp = await deployer.makeSourceApp(bridgePath, Wormhole.BRIDGE_STATE_MAP)
|
||||||
|
const bridgeCompiled = await deployer.makeApp(bridgeApp)
|
||||||
|
// FIXME: The application address should be generated contract side for security
|
||||||
|
const bridgeArgs = [coreId, decodeAddress(getApplicationAddress(coreId)).publicKey]
|
||||||
|
const bridgeDeployId = await deployer.deployApplication(owner, bridgeCompiled, signCallback, undefined, bridgeArgs)
|
||||||
|
const bridgeId = (await deployer.waitForTransactionResponse(bridgeDeployId))["application-index"]
|
||||||
|
console.log(`Wormhole bridge ID: ${bridgeId}`)
|
||||||
|
console.log(`Bridge address: ${getApplicationAddress(bridgeId)}, base64: ${Buffer.from(decodeAddress(getApplicationAddress(bridgeId)).publicKey).toString('base64')}`)
|
||||||
|
|
||||||
|
// Create object
|
||||||
|
const result = new Wormhole(deployer, owner, coreId, bridgeId, signCallback)
|
||||||
|
|
||||||
|
// Initialize applications
|
||||||
|
const initUnsigned = generateInitVAA(signers, coreId)
|
||||||
|
const initialVaa = await signWormholeMessage(initUnsigned)
|
||||||
|
const initTxId = await result.sendSignedVAA(initialVaa)
|
||||||
|
await deployer.waitForTransactionResponse(initTxId)
|
||||||
|
console.log(`Wormhole initialization complete`)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private static splitKeysAndSignatures(vaa: SignedVAA): {sigData: Uint8Array[], keyData: Uint8Array[]} {
|
||||||
|
const subSetSize = 7
|
||||||
|
const sigData = []
|
||||||
|
const keyData = []
|
||||||
|
for (let start = 0; start < vaa.signatures.length; start += subSetSize) {
|
||||||
|
const end = start + subSetSize
|
||||||
|
sigData.push(concatArrays(vaa.signatures.slice(start, end)))
|
||||||
|
keyData.push(concatArrays(vaa.keys.slice(start, end)))
|
||||||
|
}
|
||||||
|
return {sigData, keyData}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compileVaaVerify(deployer: Deployer): Promise<LogicSigAccount> {
|
||||||
|
const vaaVerifyPath = path.join(Wormhole.BASE_PATH, 'vaa_verify.py')
|
||||||
|
|
||||||
|
const vaaVerifyApp = await deployer.compileStateless(vaaVerifyPath)
|
||||||
|
|
||||||
|
return vaaVerifyApp
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendSignedVAA(vaa: SignedVAA, dryrunDebug = false): Promise<string> {
|
||||||
|
// Generate deduplication template sig
|
||||||
|
const deduplicationEmitter = concatArrays([encodeUint16(vaa.chainId), vaa.emitter])
|
||||||
|
const bits_per_sig = 8 * 15 * 127
|
||||||
|
const sequence = Math.floor(vaa.sequence / bits_per_sig)
|
||||||
|
const dedupTmplSig = new WormholeTmplSig(sequence, deduplicationEmitter, this.coreId)
|
||||||
|
console.log(`TmplSig address: ${dedupTmplSig.address}, base64: ${Buffer.from(decodeAddress(dedupTmplSig.address).publicKey).toString('base64')}`)
|
||||||
|
|
||||||
|
// Generate guardian template sig
|
||||||
|
const guardianTmplSig = new WormholeTmplSig(vaa.gsIndex, EMITTER_GUARDIAN, this.coreId)
|
||||||
|
|
||||||
|
// Opt-in tmplsig
|
||||||
|
const templateSigs = [dedupTmplSig, guardianTmplSig].concat(vaa.extraTmplSigs)
|
||||||
|
const logicSigMap: Map<Address, LogicSigAccount> = new Map()
|
||||||
|
for (const sig of templateSigs) {
|
||||||
|
await sig.optin(this.deployer, this.owner, this.coreId, this.signCallback, dryrunDebug)
|
||||||
|
logicSigMap.set(sig.address, sig.logicSig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate VAA verifier
|
||||||
|
const vaaVerify = await this.compileVaaVerify(this.deployer)
|
||||||
|
const vaaVerifyId = decodeAddress(vaaVerify.lsig.address()).publicKey
|
||||||
|
console.log(`VAA Verify address: ${vaaVerify.address()}, base64: ${Buffer.from(decodeAddress(vaaVerify.address()).publicKey).toString('base64')}`)
|
||||||
|
|
||||||
|
// Generate init transaction group
|
||||||
|
const accounts = [dedupTmplSig.address, guardianTmplSig.address].concat(vaa.extraTmplSigs.map((tmplSig) => tmplSig.address))
|
||||||
|
let txns: Transaction[]
|
||||||
|
switch (vaa.command) {
|
||||||
|
case 'init': {
|
||||||
|
txns = await Promise.all([
|
||||||
|
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['nop', Math.floor(Math.random() * 2 ** 32)]),
|
||||||
|
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['nop', Math.floor(Math.random() * 2 ** 32)]),
|
||||||
|
this.deployer.makeCallTransaction(
|
||||||
|
this.owner,
|
||||||
|
this.coreId,
|
||||||
|
OnApplicationComplete.NoOpOC,
|
||||||
|
['init', vaa.data, vaaVerifyId],
|
||||||
|
accounts,
|
||||||
|
),
|
||||||
|
this.deployer.makePayTransaction(this.owner, vaaVerify.address(), BigInt(100_000)),
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'governance': {
|
||||||
|
const {sigData, keyData} = Wormhole.splitKeysAndSignatures(vaa)
|
||||||
|
assert(sigData.length === 3)
|
||||||
|
assert(keyData.length === 3)
|
||||||
|
|
||||||
|
// Assertions mirroring underlying contract code
|
||||||
|
assert(accounts.length >= 2)
|
||||||
|
assert(sigData.length > 0)
|
||||||
|
|
||||||
|
sigData.map((sigs, i) => {
|
||||||
|
let offset = 6
|
||||||
|
const sigLength = sigs.length
|
||||||
|
const vaaData = vaa.data.slice(offset, offset + sigLength)
|
||||||
|
if (Buffer.compare(vaaData, sigs) !== 0) {
|
||||||
|
console.log(`On entry ${i}, expected VAA sig data: ${Buffer.from(vaaData).toString('hex')}\nGot: ${Buffer.from(sigs).toString('hex')}`)
|
||||||
|
} else {
|
||||||
|
console.log(`Data matched expected for entry ${i}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let guardianCounter = 0
|
||||||
|
const endPointer = offset + sigLength
|
||||||
|
const keyBuffers: Uint8Array[] = []
|
||||||
|
while (offset < endPointer) {
|
||||||
|
const guardian = sigs[offset]
|
||||||
|
if (guardian !== guardianCounter) {
|
||||||
|
// TODO: This is broken and does not generate the same output as the contract!
|
||||||
|
console.log(`Expected ${guardianCounter}, got ${guardian}`)
|
||||||
|
}
|
||||||
|
const guardianKey = keyData[i].slice(guardian * 20, guardian * 20 + 20)
|
||||||
|
keyBuffers.push(guardianKey)
|
||||||
|
|
||||||
|
offset += 66
|
||||||
|
guardianCounter++
|
||||||
|
}
|
||||||
|
|
||||||
|
const allKeyData = concatArrays(keyBuffers)
|
||||||
|
if (Buffer.compare(allKeyData, keyData[i]) !== 0) {
|
||||||
|
console.log(`On entry ${i}, expected key data ${Buffer.from(keyData[i]).toString('hex')}, got ${Buffer.from(allKeyData).toString('hex')}`)
|
||||||
|
} else {
|
||||||
|
console.log(`Key matched expected for entry ${i}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate transactions
|
||||||
|
txns = await Promise.all([
|
||||||
|
this.deployer.makeCallTransaction(vaaVerify.address(), this.coreId, OnApplicationComplete.NoOpOC, ['verifySigs', sigData[0], keyData[0], vaa.hash], accounts, [], [], '', 0),
|
||||||
|
this.deployer.makeCallTransaction(vaaVerify.address(), this.coreId, OnApplicationComplete.NoOpOC, ['verifySigs', sigData[1], keyData[1], vaa.hash], accounts, [], [], '', 0),
|
||||||
|
this.deployer.makeCallTransaction(vaaVerify.address(), this.coreId, OnApplicationComplete.NoOpOC, ['verifySigs', sigData[2], keyData[2], vaa.hash], accounts, [], [], '', 0),
|
||||||
|
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['verifyVAA', vaa.data], accounts, [], [], '', 0),
|
||||||
|
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['governance', vaa.data], accounts, [], [], '', 5000),
|
||||||
|
])
|
||||||
|
|
||||||
|
logicSigMap.set(vaaVerify.address(), vaaVerify)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown command ${vaa.command}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call init group
|
||||||
|
const initTxId = await this.deployer.callGroupTransaction(txns, logicSigMap, this.signCallback, dryrunDebug)
|
||||||
|
|
||||||
|
return initTxId
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { decodeAddress, encodeUint64 } from 'algosdk'
|
||||||
|
import elliptic from 'elliptic'
|
||||||
|
import assert from 'assert'
|
||||||
|
import web3Utils from 'web3-utils'
|
||||||
|
import { zeroPad } from 'ethers/lib/utils'
|
||||||
|
import { concatArrays, encodeUint32, encodeUint16, encodeUint8, decodeBase16 } from '../sdk/Encoding'
|
||||||
|
|
||||||
|
import { GovenanceMessageType, GovernancePayload, RegisterChainPayload, SignedVAA, UnsignedVAA, VAAPayload, VAAPayloadType } from './WormholeTypes'
|
||||||
|
|
||||||
|
export const EMITTER_GOVERNANCE = decodeBase16('0000000000000000000000000000000000000000000000000000000000000004')
|
||||||
|
|
||||||
|
function encodeGovernancePayload(payload: GovernancePayload): Uint8Array {
|
||||||
|
let body: Uint8Array
|
||||||
|
switch (payload.type) {
|
||||||
|
case GovenanceMessageType.SetUpdateHash: {
|
||||||
|
body = payload.updateHash
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case GovenanceMessageType.UpdateGuardians: {
|
||||||
|
body = concatArrays([
|
||||||
|
encodeUint32(payload.newGSIndex),
|
||||||
|
encodeUint8(payload.guardians.length),
|
||||||
|
...payload.guardians
|
||||||
|
])
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case GovenanceMessageType.SetMessageFee: {
|
||||||
|
body = concatArrays([
|
||||||
|
zeroPad([], 24),
|
||||||
|
encodeUint64(payload.messageFee),
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case GovenanceMessageType.SendAlgo: {
|
||||||
|
body = concatArrays([
|
||||||
|
zeroPad([], 24),
|
||||||
|
encodeUint64(payload.fee),
|
||||||
|
decodeAddress(payload.dest).publicKey,
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown governance message type`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = concatArrays([
|
||||||
|
decodeBase16('00000000000000000000000000000000000000000000000000000000436f7265'),
|
||||||
|
encodeUint8(payload.type),
|
||||||
|
encodeUint16(payload.targetChainId),
|
||||||
|
body,
|
||||||
|
])
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeRegisterChainPayload(payload: RegisterChainPayload): Uint8Array {
|
||||||
|
return concatArrays([
|
||||||
|
decodeBase16('0000000000000000000000000000000000000000000000000000000000000000'),
|
||||||
|
decodeBase16('000000000000000000000000000000000000000000546f6b656e427269646765'), // "TokenBridge"
|
||||||
|
encodeUint8(1), // FIXME: Is this a version number? We should make it variable so we can test the contract
|
||||||
|
encodeUint16(payload.targetChainId),
|
||||||
|
encodeUint16(payload.emitterChainId),
|
||||||
|
payload.emitterAddress,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodePayload(payload: VAAPayload): Uint8Array {
|
||||||
|
switch (payload.type) {
|
||||||
|
case VAAPayloadType.Raw:
|
||||||
|
return payload.payload
|
||||||
|
|
||||||
|
case VAAPayloadType.Governance:
|
||||||
|
return encodeGovernancePayload(payload.payload)
|
||||||
|
|
||||||
|
case VAAPayloadType.RegisterChain:
|
||||||
|
return encodeRegisterChainPayload(payload.payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Ideally, this should not be exported so we can contain the complexity here
|
||||||
|
export function generateKeySet(signers: elliptic.ec.KeyPair[]): Uint8Array[] {
|
||||||
|
const slices = signers.map((signer) => {
|
||||||
|
const pub = signer.getPublic()
|
||||||
|
const x = pub.getX().toBuffer()
|
||||||
|
const y = pub.getY().toBuffer()
|
||||||
|
const combined = concatArrays([x, y])
|
||||||
|
const hashStr = web3Utils.keccak256('0x' + Buffer.from(combined).toString('hex'))
|
||||||
|
const hash = new Uint8Array(Buffer.from(hashStr.slice(2), 'hex'))
|
||||||
|
const result = hash.slice(12, 32)
|
||||||
|
assert(result.length === 20, `Expected 20 bytes for key, got ${result.length}`)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
return slices
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signWormholeMessage(vaa: UnsignedVAA): SignedVAA {
|
||||||
|
assert(vaa.entries.length < 2 ** 8)
|
||||||
|
|
||||||
|
// Generate body of payload
|
||||||
|
const body = concatArrays([
|
||||||
|
encodeUint32(vaa.header.timestamp),
|
||||||
|
encodeUint32(vaa.header.nonce),
|
||||||
|
encodeUint16(vaa.header.chainId),
|
||||||
|
vaa.header.emitter,
|
||||||
|
encodeUint64(vaa.header.sequence),
|
||||||
|
encodeUint8(vaa.header.consistencyLevel),
|
||||||
|
encodePayload(vaa.payload),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Generate hash of body
|
||||||
|
const hexHash = web3Utils.keccak256(web3Utils.keccak256('0x' + Buffer.from(body).toString('hex')))
|
||||||
|
const hash = new Uint8Array(Buffer.from(hexHash.slice(2), 'hex'))
|
||||||
|
|
||||||
|
// Generate signature set
|
||||||
|
const signatures = vaa.entries.map((entry, i) => {
|
||||||
|
// Create signature over hash
|
||||||
|
const signature: elliptic.ec.Signature = entry.signer.sign(hash, { canonical: true })
|
||||||
|
|
||||||
|
// Create resulting data structure
|
||||||
|
const result = concatArrays([
|
||||||
|
encodeUint8(i),
|
||||||
|
zeroPad(signature.r.toBuffer(), 32),
|
||||||
|
zeroPad(signature.s.toBuffer(), 32),
|
||||||
|
encodeUint8(signature.recoveryParam ?? 0),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Validate result
|
||||||
|
assert(result.length === 66)
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate key set data
|
||||||
|
const keys = generateKeySet(vaa.entries.map(({ signer }) => signer))
|
||||||
|
|
||||||
|
// Generate data needed to send VAA
|
||||||
|
return {
|
||||||
|
command: vaa.command,
|
||||||
|
gsIndex: vaa.gsIndex,
|
||||||
|
signatures,
|
||||||
|
keys,
|
||||||
|
hash,
|
||||||
|
sequence: vaa.header.sequence,
|
||||||
|
chainId: vaa.header.chainId,
|
||||||
|
emitter: vaa.header.emitter,
|
||||||
|
extraTmplSigs: vaa.extraTmplSigs,
|
||||||
|
data: concatArrays([
|
||||||
|
// Header
|
||||||
|
encodeUint8(vaa.version),
|
||||||
|
encodeUint32(vaa.gsIndex),
|
||||||
|
encodeUint8(vaa.entries.length),
|
||||||
|
|
||||||
|
// Signatures
|
||||||
|
...signatures,
|
||||||
|
body
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { decodeAddress, getApplicationAddress, LogicSigAccount, OnApplicationComplete } from 'algosdk'
|
||||||
|
import { Address, AppId } from '../sdk/AlgorandTypes'
|
||||||
|
import { concatArrays, decodeBase16, AlgorandType } from "../sdk/Encoding"
|
||||||
|
import { Deployer, SignCallback } from "../sdk/Deployer"
|
||||||
|
import path from 'path'
|
||||||
|
import varint from 'varint'
|
||||||
|
import { Wormhole } from './Wormhole'
|
||||||
|
|
||||||
|
export const EMITTER_GUARDIAN = new TextEncoder().encode("guardian")
|
||||||
|
|
||||||
|
export class WormholeTmplSig {
|
||||||
|
private _logicSig: LogicSigAccount
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
sequence: number,
|
||||||
|
emitterId: Uint8Array,
|
||||||
|
coreId: AppId,
|
||||||
|
) {
|
||||||
|
// Generate the template sig bytecode directly
|
||||||
|
const program = concatArrays([
|
||||||
|
decodeBase16('0620010181'),
|
||||||
|
varint.encode(sequence),
|
||||||
|
decodeBase16('4880'),
|
||||||
|
varint.encode(emitterId.length),
|
||||||
|
emitterId,
|
||||||
|
decodeBase16('483110810612443119221244311881'),
|
||||||
|
varint.encode(coreId),
|
||||||
|
decodeBase16('124431208020'),
|
||||||
|
decodeAddress(getApplicationAddress(coreId)).publicKey,
|
||||||
|
decodeBase16('124431018100124431093203124431153203124422'),
|
||||||
|
])
|
||||||
|
|
||||||
|
this._logicSig = new LogicSigAccount(program)
|
||||||
|
}
|
||||||
|
|
||||||
|
public get logicSig(): LogicSigAccount {
|
||||||
|
return this._logicSig
|
||||||
|
}
|
||||||
|
|
||||||
|
public get address(): Address {
|
||||||
|
return this.logicSig.address()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async optin(deployer: Deployer, owner: Address, coreId: AppId, signCallback: SignCallback, dryrunDebug = false): Promise<void> {
|
||||||
|
// Test if contract is opted in
|
||||||
|
const optedIn = await deployer.readOptedInApps(this.address)
|
||||||
|
if (optedIn.find((entry) => entry.id === coreId) === undefined) {
|
||||||
|
console.log(`Performing optin for TmplSig ${this.address}`)
|
||||||
|
const tmplSigPayment = BigInt(1_002_000)
|
||||||
|
const optinTxns = await Promise.all([
|
||||||
|
deployer.makePayTransaction(owner, this.address, tmplSigPayment, 2 * deployer.minFee),
|
||||||
|
deployer.makeCallTransaction(this.address, coreId, OnApplicationComplete.OptInOC, [], [], [], [], '', 0, getApplicationAddress(coreId))
|
||||||
|
])
|
||||||
|
|
||||||
|
const initTxId = await deployer.callGroupTransaction(optinTxns, new Map([[this.address, this.logicSig]]), signCallback, dryrunDebug)
|
||||||
|
await deployer.waitForTransactionResponse(initTxId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async compileTmplSig(deployer: Deployer, sequence: number | bigint, emitterId: Uint8Array, coreId: AppId): Promise<LogicSigAccount> {
|
||||||
|
const tmplSigPath = path.join(Wormhole.BASE_PATH, 'TmplSig.py')
|
||||||
|
|
||||||
|
const tmplSigArgs = new Map<string, AlgorandType>([
|
||||||
|
['TMPL_ADDR_IDX', sequence],
|
||||||
|
['TMPL_EMITTER_ID', emitterId],
|
||||||
|
['TMPL_APP_ID', coreId],
|
||||||
|
// FIXME: The application address should be generated contract side for security
|
||||||
|
['TMPL_APP_ADDRESS', decodeAddress(getApplicationAddress(coreId)).publicKey],
|
||||||
|
])
|
||||||
|
|
||||||
|
const tmplSigApp = await deployer.compileStateless(tmplSigPath, tmplSigArgs)
|
||||||
|
|
||||||
|
return tmplSigApp
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { Address } from '../sdk/AlgorandTypes'
|
||||||
|
import elliptic from 'elliptic'
|
||||||
|
import { WormholeTmplSig } from './WormholeTmplSig'
|
||||||
|
|
||||||
|
export type WormholeSigner = elliptic.ec.KeyPair
|
||||||
|
export type VAACommand = 'init' | 'governance'
|
||||||
|
|
||||||
|
// Wormhole pipeline
|
||||||
|
// 1. Raw data -> generate*VAA (WormholeVAA.ts)
|
||||||
|
// Whatever data is needed is passed in to the corresponding generator/helper
|
||||||
|
// These functions mostly hold the constants and common generation tasks for a given message type
|
||||||
|
//
|
||||||
|
// generateVAA itself is used as the final step before signing, to set up all members to sign the same message(standard use case for testing)
|
||||||
|
//
|
||||||
|
// 2. generate<x>VAA -> signWormholeMessage (WormholeEncoders.ts)
|
||||||
|
// After generateVAA runs inside the generate*VAA function, the UnsignedVAA is encoded and signed by signWormholeMessage
|
||||||
|
|
||||||
|
// Core types
|
||||||
|
// The VAA header is a common data structure shared across the various stages of the pipeline below
|
||||||
|
export type VAAHeader = {
|
||||||
|
timestamp: number
|
||||||
|
nonce: number
|
||||||
|
chainId: number
|
||||||
|
emitter: Uint8Array
|
||||||
|
sequence: number
|
||||||
|
consistencyLevel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UngeneratedVAA = {
|
||||||
|
command: VAACommand
|
||||||
|
version: number
|
||||||
|
gsIndex: number
|
||||||
|
header: Partial<VAAHeader>
|
||||||
|
signers: elliptic.ec.KeyPair[]
|
||||||
|
payload: VAAPayload
|
||||||
|
extraTmplSigs: WormholeTmplSig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VAAEntry = {
|
||||||
|
signer: WormholeSigner
|
||||||
|
header: VAAHeader
|
||||||
|
payload: VAAPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsigned VAAs
|
||||||
|
export type UnsignedVAA = {
|
||||||
|
command: VAACommand
|
||||||
|
version: number
|
||||||
|
gsIndex: number
|
||||||
|
header: VAAHeader
|
||||||
|
entries: VAAEntry[]
|
||||||
|
|
||||||
|
payload: VAAPayload
|
||||||
|
extraTmplSigs: WormholeTmplSig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// This encodes all information required to generate the transaction stack. A SignedVAA is one step before being a raw transaction
|
||||||
|
export type SignedVAA = {
|
||||||
|
command: string
|
||||||
|
gsIndex: number
|
||||||
|
signatures: Uint8Array[]
|
||||||
|
keys: Uint8Array[]
|
||||||
|
hash: Uint8Array
|
||||||
|
data: Uint8Array
|
||||||
|
sequence: number
|
||||||
|
chainId: number
|
||||||
|
emitter: Uint8Array
|
||||||
|
extraTmplSigs: WormholeTmplSig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload types
|
||||||
|
export enum VAAPayloadType {
|
||||||
|
Raw,
|
||||||
|
Governance,
|
||||||
|
RegisterChain,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VAAPayload = {
|
||||||
|
type: VAAPayloadType.Raw
|
||||||
|
payload: Uint8Array
|
||||||
|
} | {
|
||||||
|
type: VAAPayloadType.Governance
|
||||||
|
payload: GovernancePayload
|
||||||
|
} | {
|
||||||
|
type: VAAPayloadType.RegisterChain
|
||||||
|
payload: RegisterChainPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Governance payload type
|
||||||
|
export enum GovenanceMessageType {
|
||||||
|
SetUpdateHash = 1,
|
||||||
|
UpdateGuardians = 2,
|
||||||
|
SetMessageFee = 3,
|
||||||
|
SendAlgo = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GovernancePayload = {
|
||||||
|
// NOTE: I'm not sure if this belongs here, or if it should go in the unsigned VAA, it seems to be common on all messages?
|
||||||
|
targetChainId: number
|
||||||
|
} & ({
|
||||||
|
type: GovenanceMessageType.SetUpdateHash
|
||||||
|
updateHash: Uint8Array
|
||||||
|
} | {
|
||||||
|
type: GovenanceMessageType.UpdateGuardians
|
||||||
|
oldGSIndex: number
|
||||||
|
newGSIndex: number
|
||||||
|
guardians: Uint8Array[]
|
||||||
|
} | {
|
||||||
|
type: GovenanceMessageType.SetMessageFee
|
||||||
|
messageFee: number
|
||||||
|
} | {
|
||||||
|
type: GovenanceMessageType.SendAlgo
|
||||||
|
unknown: Uint8Array // 24 bytes
|
||||||
|
fee: number
|
||||||
|
dest: Address
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register chain payload type
|
||||||
|
//
|
||||||
|
// Since anyone can use Wormhole to publish messages that match the payload format of the token bridge, an authorization payload needs to be implemented.
|
||||||
|
// This is done using an (emitter_chain, emitter_address) tuple. Every endpoint of the token bridge needs to know the addresses of the respective other
|
||||||
|
// endpoints on other chains. This registration of token bridge endpoints is implemented via RegisterChain where a (chain_id, emitter_address) tuple can be registered.
|
||||||
|
|
||||||
|
export type RegisterChainPayload = {
|
||||||
|
targetChainId: number
|
||||||
|
emitterChainId: number
|
||||||
|
emitterAddress: Uint8Array
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { CHAIN_ID_ALGORAND, CHAIN_ID_SOLANA } from '@certusone/wormhole-sdk'
|
||||||
|
import assert from 'assert'
|
||||||
|
import { AppId } from '../sdk/AlgorandTypes'
|
||||||
|
import { EMITTER_GOVERNANCE, generateKeySet } from './WormholeEncoders'
|
||||||
|
import { EMITTER_GUARDIAN, WormholeTmplSig } from './WormholeTmplSig'
|
||||||
|
import { UnsignedVAA, GovernancePayload, UngeneratedVAA, GovenanceMessageType, WormholeSigner, VAAHeader, VAAPayloadType } from './WormholeTypes'
|
||||||
|
|
||||||
|
// Takes an ungenerated VAA and generates a fully prepared VAA
|
||||||
|
// The ungenerated VAA represents a signle header, this function spreads it across the signer set
|
||||||
|
export function generateVAA(vaa: UngeneratedVAA): UnsignedVAA {
|
||||||
|
assert(vaa.gsIndex < vaa.signers.length && vaa.gsIndex >= 0 && Number.isSafeInteger(vaa.gsIndex))
|
||||||
|
|
||||||
|
const header: VAAHeader = {
|
||||||
|
timestamp: vaa.header.timestamp ?? 0,
|
||||||
|
nonce: vaa.header.nonce ?? Math.floor(Math.random() * (2 ** 32)),
|
||||||
|
chainId: vaa.header.chainId ?? CHAIN_ID_SOLANA,
|
||||||
|
emitter: vaa.header.emitter ?? EMITTER_GOVERNANCE,
|
||||||
|
sequence: vaa.header.sequence ?? 0,
|
||||||
|
consistencyLevel: vaa.header.consistencyLevel ?? 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: vaa.command,
|
||||||
|
version: vaa.version,
|
||||||
|
gsIndex: vaa.gsIndex,
|
||||||
|
header,
|
||||||
|
entries: vaa.signers.map((signer) => ({
|
||||||
|
signer,
|
||||||
|
header,
|
||||||
|
payload: vaa.payload,
|
||||||
|
})),
|
||||||
|
payload: vaa.payload,
|
||||||
|
extraTmplSigs: vaa.extraTmplSigs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the initialization transaction
|
||||||
|
export function generateInitVAA(signers: WormholeSigner[], coreId: AppId): UnsignedVAA {
|
||||||
|
const header: VAAHeader = {
|
||||||
|
timestamp: 0,
|
||||||
|
nonce: 0,
|
||||||
|
chainId: CHAIN_ID_SOLANA,
|
||||||
|
emitter: EMITTER_GOVERNANCE,
|
||||||
|
sequence: 0,
|
||||||
|
consistencyLevel: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: GovernancePayload = {
|
||||||
|
type: GovenanceMessageType.UpdateGuardians,
|
||||||
|
targetChainId: CHAIN_ID_ALGORAND,
|
||||||
|
oldGSIndex: 0,
|
||||||
|
newGSIndex: 1,
|
||||||
|
guardians: generateKeySet(signers),
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = generateGovernanceVAA(signers, 0, coreId, header, payload)
|
||||||
|
|
||||||
|
result.command = 'init'
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateGovernanceVAA(signers: WormholeSigner[], gsIndex: number, coreId: AppId, header: Partial<VAAHeader>, payload: GovernancePayload): UnsignedVAA {
|
||||||
|
const template: UngeneratedVAA = {
|
||||||
|
command: 'governance',
|
||||||
|
version: 1,
|
||||||
|
gsIndex,
|
||||||
|
header,
|
||||||
|
signers,
|
||||||
|
payload: {
|
||||||
|
type: VAAPayloadType.Governance,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
extraTmplSigs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.type) {
|
||||||
|
case GovenanceMessageType.UpdateGuardians: {
|
||||||
|
const newGuardianTmplSig = new WormholeTmplSig(payload.newGSIndex, EMITTER_GUARDIAN, coreId)
|
||||||
|
template.extraTmplSigs.push(newGuardianTmplSig)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateVAA(template)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"include": ["./src/**/*"],
|
||||||
|
"exclude": ["node_modules", "out"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./out",
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"incremental": true,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUnusedLocals": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -1043,3 +1043,14 @@ def get_token_bridge(genTeal, approve_name, clear_name, client: AlgodClient, see
|
||||||
CLEAR_STATE_PROGRAM = fullyCompileContract(genTeal, client, clear_token_bridge(), clear_name, devMode)
|
CLEAR_STATE_PROGRAM = fullyCompileContract(genTeal, client, clear_token_bridge(), clear_name, devMode)
|
||||||
|
|
||||||
return APPROVAL_PROGRAM, CLEAR_STATE_PROGRAM
|
return APPROVAL_PROGRAM, CLEAR_STATE_PROGRAM
|
||||||
|
|
||||||
|
def cli(output_approval, output_clear):
|
||||||
|
seed_amt = 1002000
|
||||||
|
tmpl_sig = TmplSig("sig")
|
||||||
|
|
||||||
|
client = AlgodClient("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "https://testnet-api.algonode.cloud")
|
||||||
|
|
||||||
|
approval, clear = get_token_bridge(True, output_approval, output_clear, client, seed_amt, tmpl_sig, False)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli(sys.argv[1], sys.argv[2])
|
||||||
|
|
|
@ -117,10 +117,16 @@ def vaa_verify_program():
|
||||||
Approve()]
|
Approve()]
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_vaa_verify():
|
def get_vaa_verify(file_name = "teal/vaa_verify.teal"):
|
||||||
teal = compileTeal(vaa_verify_program(), mode=Mode.Signature, version=6)
|
teal = compileTeal(vaa_verify_program(), mode=Mode.Signature, version=6)
|
||||||
|
|
||||||
with open("teal/vaa_verify.teal", "w") as f:
|
with open(file_name, "w") as f:
|
||||||
f.write(teal)
|
f.write(teal)
|
||||||
|
|
||||||
return teal
|
return teal
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
get_vaa_verify(sys.argv[1])
|
||||||
|
else:
|
||||||
|
get_vaa_verify()
|
||||||
|
|
|
@ -598,3 +598,13 @@ def getCoreContracts( genTeal, approve_name, clear_name,
|
||||||
|
|
||||||
return APPROVAL_PROGRAM, CLEAR_STATE_PROGRAM
|
return APPROVAL_PROGRAM, CLEAR_STATE_PROGRAM
|
||||||
|
|
||||||
|
def cli(output_approval, output_clear):
|
||||||
|
seed_amt = 1002000
|
||||||
|
tmpl_sig = TmplSig("sig")
|
||||||
|
|
||||||
|
client = AlgodClient("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "https://testnet-api.algonode.cloud")
|
||||||
|
|
||||||
|
approval, clear = getCoreContracts(True, output_approval, output_clear, client, seed_amt, tmpl_sig, True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli(sys.argv[1], sys.argv[2])
|
||||||
|
|
Loading…
Reference in New Issue