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:
jumpsiegel 2022-10-26 09:55:53 -05:00 committed by GitHub
parent e109024e99
commit 41d53b27c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 19925 additions and 6 deletions

View File

@ -135,9 +135,11 @@ class TmplSig:
if __name__ == '__main__':
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())

View File

@ -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

17336
algorand/audit_test/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}

View File

@ -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

View File

@ -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
})
})

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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'

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -0,0 +1,3 @@
declare module 'varint' {
export function encode(n: number): Uint8Array
}

View File

@ -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))))
}
}

View File

@ -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
}
}

View File

@ -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
])
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
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])

View File

@ -117,10 +117,16 @@ def vaa_verify_program():
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)
with open("teal/vaa_verify.teal", "w") as f:
with open(file_name, "w") as f:
f.write(teal)
return teal
if __name__ == '__main__':
if len(sys.argv) == 2:
get_vaa_verify(sys.argv[1])
else:
get_vaa_verify()

View File

@ -598,3 +598,13 @@ def getCoreContracts( genTeal, approve_name, clear_name,
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])