From a0f6fb5a041d99431fc8b822b106b08b037bd3f2 Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Fri, 29 Dec 2023 09:44:39 -0500 Subject: [PATCH] make core bridge work --- examples/package.json | 5 +- examples/src/messaging.ts | 21 ++-- package-lock.json | 3 +- platforms/algorand/protocols/core/src/core.ts | 115 ++++++++++++------ .../algorand/protocols/core/src/storage.ts | 15 +-- platforms/algorand/protocols/core/src/vaa.ts | 15 +-- platforms/algorand/src/platform.ts | 4 +- platforms/algorand/src/storage.ts | 10 ++ 8 files changed, 121 insertions(+), 67 deletions(-) diff --git a/examples/package.json b/examples/package.json index 126e7174e..6cdbf5216 100644 --- a/examples/package.json +++ b/examples/package.json @@ -39,7 +39,7 @@ "demo": "tsx src/index.ts", "cosmos": "tsx src/cosmos.ts", "retb": "cd .. && npm run build && cd - && npm run tb", - "msg": "tsx src/messaging.ts", + "msg": "cd ../platforms/algorand/protocols/core && npm run build && cd - && tsx src/messaging.ts", "clean": "rm -rf ./dist && rm -f ./*.tsbuildinfo", "lint": "npm run prettier && eslint --fix", "prettier": "prettier --write ./src", @@ -63,6 +63,7 @@ "@wormhole-foundation/connect-sdk-cosmwasm-tokenbridge": "^0.3.0-beta.6", "@wormhole-foundation/connect-sdk-evm-cctp": "^0.3.0-beta.6", "@wormhole-foundation/connect-sdk-solana-cctp": "^0.3.0-beta.6", - "@wormhole-foundation/connect-sdk-cosmwasm-ibc": "^0.3.0-beta.6" + "@wormhole-foundation/connect-sdk-cosmwasm-ibc": "^0.3.0-beta.6", + "algosdk":"^2.7.0" } } diff --git a/examples/src/messaging.ts b/examples/src/messaging.ts index c4fbe2bed..07ed2cabc 100644 --- a/examples/src/messaging.ts +++ b/examples/src/messaging.ts @@ -1,14 +1,14 @@ import { Wormhole, encoding, signSendWait } from "@wormhole-foundation/connect-sdk"; -import { SolanaPlatform } from "@wormhole-foundation/connect-sdk-solana"; +import { AlgorandPlatform } from "@wormhole-foundation/connect-sdk-algorand"; import { getStuff } from "./helpers"; // register the protocol -import "@wormhole-foundation/connect-sdk-solana-core"; +import "@wormhole-foundation/connect-sdk-algorand-core"; (async function () { - const wh = new Wormhole("Testnet", [SolanaPlatform]); + const wh = new Wormhole("Testnet", [AlgorandPlatform]); - const chain = wh.getChain("Solana"); + const chain = wh.getChain("Algorand"); const { signer, address } = await getStuff(chain); // Get a reference to the core messaging bridge @@ -16,19 +16,24 @@ import "@wormhole-foundation/connect-sdk-solana-core"; // Generate transactions, sign and send them const publishTxs = coreBridge.publishMessage(address.address, encoding.bytes.encode("lol"), 0, 0); - const [txid] = await signSendWait(chain, publishTxs, signer); + const txids = await signSendWait(chain, publishTxs, signer); - // Grab the wormhole message from the transaction logs or storage + // Take the last txid in case multiple were sent + // the last one should be the one containing the relevant + // event or log info + const txid = txids[txids.length - 1]; + + // // Grab the wormhole message from the transaction logs or storage const [whm] = await chain.parseTransaction(txid!.txid); - // Wait for the vaa with a timeout, + // // Wait for the vaa with a timeout, const vaa = await wh.getVaa(whm!, "Uint8Array", 60_000); console.log(vaa); // can also search by txid but it takes longer to show up // console.log(await wh.getVaaByTxHash(txid!.txid, "Uint8Array")); const verifyTxs = coreBridge.verifyMessage(address.address, vaa!); - await signSendWait(chain, verifyTxs, signer); + console.log(await signSendWait(chain, verifyTxs, signer)); // reposting the vaa should result in 0 transactions being issued // assuming you're reading your writes diff --git a/package-lock.json b/package-lock.json index 941c21ebd..e2d273827 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,7 +92,8 @@ "@wormhole-foundation/connect-sdk-solana": "^0.3.0-beta.6", "@wormhole-foundation/connect-sdk-solana-cctp": "^0.3.0-beta.6", "@wormhole-foundation/connect-sdk-solana-core": "^0.3.0-beta.6", - "@wormhole-foundation/connect-sdk-solana-tokenbridge": "^0.3.0-beta.6" + "@wormhole-foundation/connect-sdk-solana-tokenbridge": "^0.3.0-beta.6", + "algosdk": "^2.7.0" }, "devDependencies": { "dotenv": "^16.3.1", diff --git a/platforms/algorand/protocols/core/src/core.ts b/platforms/algorand/protocols/core/src/core.ts index ac92888e7..9efbf9187 100644 --- a/platforms/algorand/protocols/core/src/core.ts +++ b/platforms/algorand/protocols/core/src/core.ts @@ -3,7 +3,6 @@ import { ChainsConfig, Contracts, Network, - UniversalAddress, VAA, WormholeCore, WormholeMessageId, @@ -17,9 +16,18 @@ import { AlgorandPlatformType, AlgorandUnsignedTransaction, AnyAlgorandAddress, + StorageLogicSig, TransactionSignerPair, + safeBigIntToNumber, } from "@wormhole-foundation/connect-sdk-algorand"; -import { Algodv2, bytesToBigInt, decodeAddress, getApplicationAddress, modelsv2 } from "algosdk"; +import { + Algodv2, + OnApplicationComplete, + getApplicationAddress, + makeApplicationCallTxnFromObject, + modelsv2, +} from "algosdk"; +import { maybeCreateStorageTx } from "./storage"; import { submitVAAHeader } from "./vaa"; export class AlgorandWormholeCore @@ -31,6 +39,13 @@ export class AlgorandWormholeCore readonly tokenBridgeAppId: bigint; readonly tokenBridgeAppAddress: string; + // method selector for verifying a VAA + static verifyVaa = encoding.bytes.encode("verifyVAA"); + // method selector for verifying signatures of a VAA + static verifySigs = encoding.bytes.encode("verifySigs"); + // method selector string for publishing a message + static publishMessage = encoding.bytes.encode("publishMessage"); + constructor( readonly network: N, readonly chain: C, @@ -55,9 +70,14 @@ export class AlgorandWormholeCore } async *verifyMessage(sender: AnyAlgorandAddress, vaa: VAA) { - const appId = 0n; const address = new AlgorandAddress(sender).toString(); - const txset = await submitVAAHeader(this.connection, this.coreAppId, appId, vaa, address); + const txset = await submitVAAHeader( + this.connection, + this.coreAppId, + this.coreAppId, + vaa, + address, + ); for (const tx of txset.txs) { yield this.createUnsignedTx(tx, "Core.verifyMessage"); } @@ -74,49 +94,64 @@ export class AlgorandWormholeCore return new AlgorandWormholeCore(network as N, chain, connection, conf.contracts); } - async *publishMessage( - sender: AnyAlgorandAddress, - message: string | Uint8Array, - ): AsyncGenerator> { - throw new Error("Method not implemented."); + async *publishMessage(sender: AnyAlgorandAddress, message: Uint8Array) { + // Call core bridge to publish message + const _sender = new AlgorandAddress(sender); + const address = _sender.toString(); + const suggestedParams = await this.connection.getTransactionParams().do(); + + const storage = StorageLogicSig.forEmitter(this.coreAppId, _sender.toUint8Array()); + + const { + accounts: [storageAddress], + txs, + } = await maybeCreateStorageTx( + this.connection, + address, + this.coreAppId, + storage, + suggestedParams, + ); + + for (const tx of txs) { + yield this.createUnsignedTx(tx, "Core.publishMessage", true); + } + + const act = makeApplicationCallTxnFromObject({ + from: address, + appIndex: safeBigIntToNumber(this.coreAppId), + appArgs: [AlgorandWormholeCore.publishMessage, message, encoding.bignum.toBytes(0n, 8)], + accounts: [storageAddress], + onComplete: OnApplicationComplete.NoOpOC, + suggestedParams, + }); + + yield this.createUnsignedTx({ tx: act }, "Core.publishMessage", true); } async parseTransaction(txId: string): Promise { const result = await this.connection.pendingTransactionInformation(txId).do(); - const emitterAddr = new UniversalAddress(this.getEmitterAddressAlgorand(this.tokenBridgeAppId)); - const sequence = this.parseSequenceFromLogAlgorand(result); - return [ - { - chain: this.chain, - emitter: emitterAddr, - sequence, - } as WormholeMessageId, - ]; - } + const ptr = modelsv2.PendingTransactionResponse.from_obj_for_encoding(result); - private getEmitterAddressAlgorand(appId: bigint): string { - const appAddr: string = getApplicationAddress(appId); - const decodedAppAddr: Uint8Array = decodeAddress(appAddr).publicKey; - const hexAppAddr: string = encoding.hex.encode(decodedAppAddr); - return hexAppAddr; - } + // Expect target is core app + if (BigInt(ptr.txn.txn.apid) !== this.coreAppId) throw new Error("Invalid app id"); - private parseSequenceFromLogAlgorand(result: Record): bigint { - let sequence: bigint | undefined; - const ptr = modelsv2.PendingTransactionResponse.from_obj_for_encoding(result); - if (ptr.innerTxns) { - const innerTxns = ptr.innerTxns; - innerTxns.forEach((txn) => { - if (txn?.logs && txn.logs.length > 0 && txn.logs[0]) { - sequence = bytesToBigInt(txn.logs[0].subarray(0, 8)); - } - }); - } - if (!sequence) { - throw new Error("parseSequenceFromLogAlgorand - Sequence not found"); - } - return sequence; + // Expect publish messeage as first arg + const args = ptr.txn.txn.apaa; + if ( + args.length !== 3 || + !encoding.bytes.equals(new Uint8Array(args[0]), AlgorandWormholeCore.publishMessage) + ) + throw new Error("Invalid transaction arguments"); + + if (!ptr.logs || ptr.logs.length === 0) throw new Error("No logs found to parse sequence"); + + const sequence = encoding.bignum.decode(ptr.logs[0]); + const emitter = new AlgorandAddress(ptr.txn.txn.snd).toUniversalAddress(); + + return [{ chain: this.chain, emitter, sequence }]; } + private createUnsignedTx( txReq: TransactionSignerPair, description: string, diff --git a/platforms/algorand/protocols/core/src/storage.ts b/platforms/algorand/protocols/core/src/storage.ts index bb24cbcbf..063932aa8 100644 --- a/platforms/algorand/protocols/core/src/storage.ts +++ b/platforms/algorand/protocols/core/src/storage.ts @@ -28,11 +28,11 @@ export async function storageAccountExists( appId: bigint, ): Promise { try { - const acctAppInfo = client + const acctAppInfo = await client .accountApplicationInformation(address, safeBigIntToNumber(appId)) .do(); return Object.keys(acctAppInfo).length > 0; - } catch (e) {} + } catch {} return false; } @@ -56,13 +56,12 @@ export async function maybeCreateStorageTx( const txs: TransactionSignerPair[] = []; - try { - const exists = await storageAccountExists(client, storageAddress, appId); - if (exists) return { accounts: [storageAddress], txs }; - } catch {} + if (await storageAccountExists(client, storageAddress, appId)) + return { accounts: [storageAddress], txs }; suggestedParams = suggestedParams ?? (await client.getTransactionParams().do()); + // Pay the storage account some ALGO to min balance requirements const seedTxn = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, to: storageAddress, @@ -72,11 +71,13 @@ export async function maybeCreateStorageTx( seedTxn.fee = seedTxn.fee * 2; txs.push({ tx: seedTxn, signer: null }); + // Opt in to the app and rekey to the app address that is using + // this as storage const optinTxn = makeApplicationOptInTxnFromObject({ from: storageAddress, - suggestedParams, appIndex: safeBigIntToNumber(appId), rekeyTo: appAddr, + suggestedParams, }); optinTxn.fee = 0; txs.push({ diff --git a/platforms/algorand/protocols/core/src/vaa.ts b/platforms/algorand/protocols/core/src/vaa.ts index dd8bd50d4..4e17863ad 100644 --- a/platforms/algorand/protocols/core/src/vaa.ts +++ b/platforms/algorand/protocols/core/src/vaa.ts @@ -19,6 +19,7 @@ import { signLogicSigTransaction, } from "algosdk"; import { maybeCreateStorageTx } from "./storage"; +import { AlgorandWormholeCore } from "./core"; /** * Submits just the header of the VAA @@ -35,7 +36,10 @@ export async function submitVAAHeader( appid: bigint, vaa: VAA, senderAddr: string, + suggestedParams?: SuggestedParams, ): Promise { + suggestedParams = suggestedParams ?? (await client.getTransactionParams().do()); + let txs: TransactionSignerPair[] = []; // Get storage acct for message ID @@ -47,7 +51,7 @@ export async function submitVAAHeader( const { accounts: [seqAddr], txs: seqOptInTxs, - } = await maybeCreateStorageTx(client, senderAddr, appid, msgStorage); + } = await maybeCreateStorageTx(client, senderAddr, appid, msgStorage, suggestedParams); txs.push(...seqOptInTxs); // Get storage account for Guardian set @@ -55,7 +59,7 @@ export async function submitVAAHeader( const { accounts: [guardianAddr], txs: guardianOptInTxs, - } = await maybeCreateStorageTx(client, senderAddr, coreId, gsStorage); + } = await maybeCreateStorageTx(client, senderAddr, coreId, gsStorage, suggestedParams); txs.push(...guardianOptInTxs); let accts: string[] = [seqAddr, guardianAddr]; @@ -63,8 +67,6 @@ export async function submitVAAHeader( // Get the Guardian keys const keys: Uint8Array = await decodeLocalState(client, coreId, guardianAddr); - const suggestedParams: SuggestedParams = await client.getTransactionParams().do(); - // We don't pass the entire payload in but instead just pass it pre-digested. This gets around size // limitations with lsigs AND reduces the cost of the entire operation on a congested network by reducing the // bytes passed into the transaction @@ -78,7 +80,6 @@ export async function submitVAAHeader( const SIG_LEN: number = 66; const GuardianKeyLen: number = 20; - const verifySigArg: Uint8Array = encoding.bytes.encode("verifySigs"); const lsa = new LogicSigAccount(ALGO_VERIFY); for (let nt = 0; nt < numTxns; nt++) { @@ -103,7 +104,7 @@ export async function submitVAAHeader( const appTxn = makeApplicationCallTxnFromObject({ appArgs: [ - verifySigArg, + AlgorandWormholeCore.verifySigs, encoding.bytes.concat( ...sigs.map((s) => encoding.bytes.concat(new Uint8Array([s.guardianIndex]), s.signature.encode()), @@ -128,7 +129,7 @@ export async function submitVAAHeader( }); } const appTxn = makeApplicationCallTxnFromObject({ - appArgs: [encoding.bytes.encode("verifyVAA"), serialize(vaa)], + appArgs: [AlgorandWormholeCore.verifyVaa, serialize(vaa)], accounts: accts, appIndex: safeBigIntToNumber(coreId), from: senderAddr, diff --git a/platforms/algorand/src/platform.ts b/platforms/algorand/src/platform.ts index 1c3924566..f71547887 100644 --- a/platforms/algorand/src/platform.ts +++ b/platforms/algorand/src/platform.ts @@ -159,8 +159,8 @@ export class AlgorandPlatform extends PlatformContext { + const appAddress = decodeAddress(getApplicationAddress(appId)).publicKey; + return StorageLogicSig.fromData({ + appId, + appAddress, + idx: 0n, + address: emitter, + }); + }, + fromData: (data: PopulateData) => { // This patches the binary of the TEAL program used to store data // to produce a logic sig that can be used to sign transactions