From 1862b5f7e3122ffd53b1c01ca11462c5083ce2eb Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Wed, 27 Dec 2023 11:48:29 -0500 Subject: [PATCH] squiggle --- core/base/src/utils/encoding.ts | 18 +- examples/src/algoTokenBridge.ts | 15 +- package-lock.json | 3 +- .../algorand/__tests__/unit/address.test.ts | 16 + platforms/algorand/jest.config.ts | 17 + platforms/algorand/package.json | 3 +- .../protocols/tokenBridge/package.json | 3 +- .../protocols/tokenBridge/src/_vaa.ts | 622 ------------------ .../protocols/tokenBridge/src/apps.ts | 132 ---- .../protocols/tokenBridge/src/assets.ts | 80 +-- .../protocols/tokenBridge/src/bigVarint.ts | 111 ++-- .../protocols/tokenBridge/src/constants.ts | 30 - .../protocols/tokenBridge/src/index.ts | 7 +- .../protocols/tokenBridge/src/storage.ts | 99 +++ .../protocols/tokenBridge/src/tmplSig.ts | 59 -- .../protocols/tokenBridge/src/tokenBridge.ts | 515 +++++++++++++-- .../protocols/tokenBridge/src/transfers.ts | 420 ------------ .../protocols/tokenBridge/src/types.ts | 51 +- .../protocols/tokenBridge/src/utilities.ts | 190 +++++- .../algorand/protocols/tokenBridge/src/vaa.ts | 317 +-------- platforms/algorand/src/address.ts | 23 +- platforms/algorand/src/platform.ts | 2 - platforms/algorand/src/testing/signer.ts | 16 +- platforms/algorand/src/types.ts | 4 +- 24 files changed, 896 insertions(+), 1857 deletions(-) create mode 100644 platforms/algorand/__tests__/unit/address.test.ts create mode 100644 platforms/algorand/jest.config.ts delete mode 100644 platforms/algorand/protocols/tokenBridge/src/_vaa.ts delete mode 100644 platforms/algorand/protocols/tokenBridge/src/apps.ts delete mode 100644 platforms/algorand/protocols/tokenBridge/src/constants.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/storage.ts delete mode 100644 platforms/algorand/protocols/tokenBridge/src/tmplSig.ts delete mode 100644 platforms/algorand/protocols/tokenBridge/src/transfers.ts diff --git a/core/base/src/utils/encoding.ts b/core/base/src/utils/encoding.ts index 30712bc49..d96f1e63e 100644 --- a/core/base/src/utils/encoding.ts +++ b/core/base/src/utils/encoding.ts @@ -34,14 +34,22 @@ export const b58 = { export const bignum = { decode: (input: string | Uint8Array) => typeof input === "string" ? BigInt(input) : BigInt(hex.encode(input, true)), - encode: (input: bigint, prefix: boolean = false) => (prefix ? "0x" : "") + input.toString(16), + encode: (input: bigint, prefix: boolean = false) => bignum.toString(input, prefix), + toString: (input: bigint, prefix: boolean = false) => { + let str = input.toString(16); + str = str.length % 2 === 1 ? (str = "0" + str) : str; + if (prefix) return "0x" + str; + return str; + }, + toBytes: (input: bigint, length?: number) => { + const b = hex.decode(bignum.toString(input)); + if (!length) return b; + return bytes.zpad(b, length); + }, }; export const bytes = { - encode: (value: string | bigint): Uint8Array => - typeof value === "bigint" - ? bytes.encode(bignum.encode(value)) - : new TextEncoder().encode(value), + encode: (value: string): Uint8Array => new TextEncoder().encode(value), decode: (value: Uint8Array): string => new TextDecoder().decode(value), equals: (lhs: Uint8Array, rhs: Uint8Array): boolean => lhs.length === rhs.length && lhs.every((v, i) => v === rhs[i]), diff --git a/examples/src/algoTokenBridge.ts b/examples/src/algoTokenBridge.ts index c025274ec..f3f7eb0a4 100644 --- a/examples/src/algoTokenBridge.ts +++ b/examples/src/algoTokenBridge.ts @@ -26,8 +26,8 @@ import "@wormhole-foundation/connect-sdk-solana-tokenbridge"; const wh = new Wormhole("Testnet", [AlgorandPlatform, SolanaPlatform, EvmPlatform]); // Grab chain Contexts -- these hold a reference to a cached rpc client - const sendChain = wh.getChain("Avalanche"); - const rcvChain = wh.getChain("Algorand"); + const sendChain = wh.getChain("Algorand"); + const rcvChain = wh.getChain("Avalanche"); // shortcut to allow transferring native gas token const token: TokenId | "native" = "native"; @@ -64,7 +64,7 @@ import "@wormhole-foundation/connect-sdk-solana-tokenbridge"; // Set this to the transfer txid of the initiating transaction to recover a token transfer // and attempt to fetch details about its progress. - let recoverTxid = "0x191ffc4682aa0713d1010cff9fa5921c7941871cc9bd0ef431b7154d719825fa"; + let recoverTxid = undefined; // recoverTxid = // "2daoPz9KyVkG8WGztfatMRx3EKbiRSUVGKAoCST9286eGrzXg5xowafBUUKfd3JrHzvd4AwoH57ujWaJ72k6oiCY"; @@ -87,12 +87,11 @@ import "@wormhole-foundation/connect-sdk-solana-tokenbridge"; txid: recoverTxid, }); - // Log out the results console.log(xfer); - - if (xfer.getTransferState() <= TransferState.DestinationInitiated) { - console.log(await xfer.completeTransfer(destination.signer)); - } + // Log out the results + // if (xfer.getTransferState() <= TransferState.DestinationInitiated) { + // console.log(await xfer.completeTransfer(destination.signer)); + // } })(); async function tokenTransfer( diff --git a/package-lock.json b/package-lock.json index 67f733e52..bb4de5f57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9013,7 +9013,8 @@ "license": "Apache-2.0", "dependencies": { "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", - "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5" + "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand-core": "^0.3.0-beta.5" }, "engines": { "node": ">=16" diff --git a/platforms/algorand/__tests__/unit/address.test.ts b/platforms/algorand/__tests__/unit/address.test.ts new file mode 100644 index 000000000..32663418e --- /dev/null +++ b/platforms/algorand/__tests__/unit/address.test.ts @@ -0,0 +1,16 @@ +import { AlgorandAddress } from "../../src"; + +describe("Algorand Address Tests", () => { + describe("Parse Address", () => { + test("An address parses", () => { + let address = new AlgorandAddress( + "6XHBAFTDDSGTD4AOR67SLCCY7HAQGYBUWCVP2DYPIE7HI7G7IPNOEYM6XE", + ); + expect(address).toBeTruthy(); + }); + + test("An invalid address is rejected", () => { + expect(() => new AlgorandAddress("bogusybogusybogus")).toThrow(); + }); + }); +}); diff --git a/platforms/algorand/jest.config.ts b/platforms/algorand/jest.config.ts new file mode 100644 index 000000000..85f2617de --- /dev/null +++ b/platforms/algorand/jest.config.ts @@ -0,0 +1,17 @@ +import type { JestConfigWithTsJest } from "ts-jest"; + +const jestConfig: JestConfigWithTsJest = { + preset: "ts-jest", + verbose: true, + modulePathIgnorePatterns: ["mocks"], + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + isolatedModules: true, + }, + ], + }, +}; + +export default jestConfig; diff --git a/platforms/algorand/package.json b/platforms/algorand/package.json index f54e576d0..8b9ef1b6b 100644 --- a/platforms/algorand/package.json +++ b/platforms/algorand/package.json @@ -39,7 +39,8 @@ "rebuild": "npm run clean && npm run build:cjs && npm run build:esm", "clean": "rm -rf ./dist && rm -f ./*.tsbuildinfo", "lint": "npm run prettier && eslint --fix", - "prettier": "prettier --write ./src" + "prettier": "prettier --write ./src", + "test": "jest --config ./jest.config.ts" }, "dependencies": { "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", diff --git a/platforms/algorand/protocols/tokenBridge/package.json b/platforms/algorand/protocols/tokenBridge/package.json index ba162f4f7..3ed96dace 100644 --- a/platforms/algorand/protocols/tokenBridge/package.json +++ b/platforms/algorand/protocols/tokenBridge/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", - "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5" + "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand-core": "^0.3.0-beta.5" } } \ No newline at end of file diff --git a/platforms/algorand/protocols/tokenBridge/src/_vaa.ts b/platforms/algorand/protocols/tokenBridge/src/_vaa.ts deleted file mode 100644 index 34a5f39c5..000000000 --- a/platforms/algorand/protocols/tokenBridge/src/_vaa.ts +++ /dev/null @@ -1,622 +0,0 @@ -import { - CHAIN_ID_ALGORAND, - TransactionSignerPair, -} from "@wormhole-foundation/connect-sdk-algorand"; -import { - ABIMethod, - ABIType, - Algodv2, - LogicSigAccount, - OnApplicationComplete, - SuggestedParams, - Transaction, - bigIntToBytes, - encodeAddress, - getApplicationAddress, - makeApplicationCallTxnFromObject, - makeAssetTransferTxnWithSuggestedParamsFromObject, - makePaymentTxnWithSuggestedParamsFromObject, - signLogicSigTransaction, -} from "algosdk"; -import { ParsedVAA } from "./types"; -import { - ALGO_VERIFY, - ALGO_VERIFY_HASH, - MAX_BITS, - MAX_SIGS_PER_TXN, - ZERO_PAD_BYTES, -} from "./constants"; -import { - extract3, - hexToNativeAssetBigIntAlgorand, - hexToUint8Array, - safeBigIntToNumber, - textToHexString, - textToUint8Array, - uint8ArrayToHex, -} from "./utilities"; // TODO: Replace these to remove dependency on Buffer -import { assetOptinCheck, optIn } from "./assets"; -import { decodeLocalState } from "./apps"; - -class SubmitVAAState { - vaaMap: ParsedVAA; - accounts: string[]; - txs: TransactionSignerPair[]; - guardianAddr: string; - - constructor( - vaaMap: ParsedVAA, - accounts: string[], - txs: TransactionSignerPair[], - guardianAddr: string, - ) { - this.vaaMap = vaaMap; - this.accounts = accounts; - this.txs = txs; - this.guardianAddr = guardianAddr; - } -} - -/** - * Submits just the header of the VAA - * @param client AlgodV2 client - * @param bridgeId Application ID of the core b * ridge - * @param vaa The VAA (Just the header is used) - * @param senderAddr Sending account address - * @param appid Application ID - * @returns Promise with current VAA state - */ -export async function submitVAAHeader( - client: Algodv2, - bridgeId: bigint, - vaa: Uint8Array, - senderAddr: string, - appid: bigint, -): Promise { - // A lot of our logic here depends on parseVAA and knowing what the payload is - const parsedVAA = _parseVAAAlgorand(vaa); // TODO: Replace with deserialize() - const seq: bigint = parsedVAA.sequence / BigInt(MAX_BITS); - const chainRaw: string = parsedVAA.chainRaw; - const em: string = parsedVAA.emitter; - const index: number = parsedVAA.index; - - let txs: TransactionSignerPair[] = []; - // "seqAddr" - const { addr: seqAddr, txs: seqOptInTxs } = await optIn( - client, - senderAddr, - appid, - seq, - chainRaw + em, - ); - txs.push(...seqOptInTxs); - const guardianPgmName = textToHexString("guardian"); - // And then the signatures to help us verify the vaa_s - // "guardianAddr" - const { addr: guardianAddr, txs: guardianOptInTxs } = await optIn( - client, - senderAddr, - bridgeId, - BigInt(index), - guardianPgmName, - ); - txs.push(...guardianOptInTxs); - let accts: string[] = [seqAddr, guardianAddr]; - - // When we attest for a new token, we need some place to store the info... later we will need to - // mirror the other way as well - const keys: Uint8Array = await decodeLocalState(client, bridgeId, guardianAddr); - - const params: 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 - // This is a 2 pass digest - const digest = ""; // keccak256(keccak256(parsedVAA.digest)).slice(2); - - // How many signatures can we process in a single txn... we can do 6! - // There are likely upwards of 19 signatures. So, we ned to split things up - const numSigs: number = parsedVAA.siglen; - let numTxns: number = Math.floor(numSigs / MAX_SIGS_PER_TXN) + 1; - - const SIG_LEN: number = 66; - const BSIZE: number = SIG_LEN * MAX_SIGS_PER_TXN; - const signatures: Uint8Array = parsedVAA.signatures; - const verifySigArg: Uint8Array = textToUint8Array("verifySigs"); - const lsa = new LogicSigAccount(ALGO_VERIFY); - for (let nt = 0; nt < numTxns; nt++) { - let sigs: Uint8Array = signatures.slice(nt * BSIZE); - if (sigs.length > BSIZE) { - sigs = sigs.slice(0, BSIZE); - } - - // The keyset is the set of guardians that correspond - // to the current set of signatures in this loop. - // Each signature in 20 bytes and comes from decodeLocalState() - const GuardianKeyLen: number = 20; - const numSigsThisTxn = sigs.length / SIG_LEN; - let arraySize: number = numSigsThisTxn * GuardianKeyLen; - let keySet: Uint8Array = new Uint8Array(arraySize); - for (let i = 0; i < numSigsThisTxn; i++) { - // The first byte of the sig is the relative index of that signature in the signatures array - // Use that index to get the appropriate guardian key - const idx = sigs[i * SIG_LEN]!; // Added non-null assertion - const key = keys.slice(idx * GuardianKeyLen + 1, (idx + 1) * GuardianKeyLen + 1); - keySet.set(key, i * 20); - } - - const appTxn = makeApplicationCallTxnFromObject({ - appArgs: [verifySigArg, sigs, keySet, hexToUint8Array(digest)], - accounts: accts, - appIndex: safeBigIntToNumber(bridgeId), - from: ALGO_VERIFY_HASH, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }); - appTxn.fee = 0; - txs.push({ - tx: appTxn, - signer: { - addr: lsa.address(), - signTxn: (txn: Transaction) => Promise.resolve(signLogicSigTransaction(txn, lsa).blob), - }, - }); - } - const appTxn = makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("verifyVAA"), vaa], - accounts: accts, - appIndex: safeBigIntToNumber(bridgeId), - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }); - appTxn.fee = appTxn.fee * (2 + numTxns); // Was 1 - txs.push({ tx: appTxn, signer: null }); - - return new SubmitVAAState(parsedVAA, accts, txs, guardianAddr); -} - -/** - * Submits the VAA to the application - * @param client AlgodV2 client - * @param tokenBridgeId Application ID of the token bridge - * @param bridgeId Application ID of the core bridge - * @param vaa The VAA to be submitted - * @param senderAddr Sending account address - * @returns Promise with an array of TransactionSignerPair - */ -export async function _submitVAAAlgorand( - client: Algodv2, - tokenBridgeId: bigint, - bridgeId: bigint, - vaa: Uint8Array, - senderAddr: string, -): Promise { - let sstate = await submitVAAHeader(client, bridgeId, vaa, senderAddr, tokenBridgeId); - - let parsedVAA = sstate.vaaMap; - let accts = sstate.accounts; - let txs = sstate.txs; - - // If this happens to be setting up a new guardian set, we probably need it as well... - if ( - parsedVAA.Meta === "CoreGovernance" && - parsedVAA.action === 2 && - parsedVAA.NewGuardianSetIndex !== undefined - ) { - const ngsi = parsedVAA.NewGuardianSetIndex; - const guardianPgmName = textToHexString("guardian"); - // "newGuardianAddr" - const { addr: newGuardianAddr, txs: newGuardianOptInTxs } = await optIn( - client, - senderAddr, - bridgeId, - BigInt(ngsi), - guardianPgmName, - ); - accts.push(newGuardianAddr); - txs.unshift(...newGuardianOptInTxs); - } - - // When we attest for a new token, we need some place to store the info... later we will need to - // mirror the other way as well - const meta = parsedVAA.Meta; - let chainAddr: string = ""; - if ( - (meta === "TokenBridge Attest" || - meta === "TokenBridge Transfer" || - meta === "TokenBridge Transfer With Payload") && - parsedVAA.Contract !== undefined - ) { - if (parsedVAA.FromChain !== CHAIN_ID_ALGORAND && parsedVAA.FromChain) { - // "TokenBridge chainAddr" - const result = await optIn( - client, - senderAddr, - tokenBridgeId, - BigInt(parsedVAA.FromChain), - parsedVAA.Contract, - ); - chainAddr = result.addr; - txs.unshift(...result.txs); - } else { - const assetId = hexToNativeAssetBigIntAlgorand(parsedVAA.Contract); - // "TokenBridge native chainAddr" - const result = await optIn( - client, - senderAddr, - tokenBridgeId, - assetId, - textToHexString("native"), - ); - chainAddr = result.addr; - txs.unshift(...result.txs); - } - accts.push(chainAddr); - } - - const params: SuggestedParams = await client.getTransactionParams().do(); - - if (meta === "CoreGovernance") { - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("governance"), vaa], - accounts: accts, - appIndex: safeBigIntToNumber(bridgeId), - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("nop"), bigIntToBytes(5, 8)], - appIndex: safeBigIntToNumber(bridgeId), - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - } - if (meta === "TokenBridge RegisterChain" || meta === "TokenBridge UpgradeContract") { - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("governance"), vaa], - accounts: accts, - appIndex: safeBigIntToNumber(tokenBridgeId), - foreignApps: [safeBigIntToNumber(bridgeId)], - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - } - - if (meta === "TokenBridge Attest") { - let asset: Uint8Array = await decodeLocalState(client, tokenBridgeId, chainAddr); - let foreignAssets: number[] = []; - if (asset.length > 8) { - const tmp = Buffer.from(asset.slice(0, 8)); - foreignAssets.push(safeBigIntToNumber(tmp.readBigUInt64BE(0))); - } - txs.push({ - tx: makePaymentTxnWithSuggestedParamsFromObject({ - from: senderAddr, - to: chainAddr, - amount: 100000, - suggestedParams: params, - }), - signer: null, - }); - let buf: Uint8Array = new Uint8Array(1); - buf[0] = 0x01; - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("nop"), buf], - appIndex: safeBigIntToNumber(tokenBridgeId), - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - - buf = new Uint8Array(1); - buf[0] = 0x02; - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("nop"), buf], - appIndex: safeBigIntToNumber(tokenBridgeId), - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - - txs.push({ - tx: makeApplicationCallTxnFromObject({ - accounts: accts, - appArgs: [textToUint8Array("receiveAttest"), vaa], - appIndex: safeBigIntToNumber(tokenBridgeId), - foreignAssets: foreignAssets, - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - txs[txs.length - 1]!.tx.fee = txs[txs.length - 1]!.tx.fee * 2; // Added non-null assertion. QUESTIONBW: There are like 3 different ways of adjusting fees in various functions--this should be standardized - } - - if ( - (meta === "TokenBridge Transfer" || meta === "TokenBridge Transfer With Payload") && - parsedVAA.Contract !== undefined - ) { - let foreignAssets: number[] = []; - let a: number = 0; - if (parsedVAA.FromChain !== CHAIN_ID_ALGORAND) { - let asset = await decodeLocalState(client, tokenBridgeId, chainAddr); - - if (asset.length > 8) { - const tmp = Buffer.from(asset.slice(0, 8)); - a = safeBigIntToNumber(tmp.readBigUInt64BE(0)); - } - } else { - a = parseInt(parsedVAA.Contract, 16); - } - - // The receiver needs to be optin in to receive the coins... Yeah, the relayer pays for this - - let aid = 0; - let addr = ""; - - if (parsedVAA.ToAddress !== undefined) { - if (parsedVAA.ToChain === 8 && parsedVAA.Type === 3) { - aid = Number(hexToNativeAssetBigIntAlgorand(uint8ArrayToHex(parsedVAA.ToAddress))); - addr = getApplicationAddress(aid); - } else { - addr = encodeAddress(parsedVAA.ToAddress); - } - } - - if (a !== 0) { - foreignAssets.push(a); - if (!(await assetOptinCheck(client, BigInt(a), addr))) { - if (senderAddr != addr) { - throw new Error("cannot ASA optin for somebody else (asset " + a.toString() + ")"); - } - - txs.unshift({ - tx: makeAssetTransferTxnWithSuggestedParamsFromObject({ - amount: 0, - assetIndex: a, - from: senderAddr, - suggestedParams: params, - to: senderAddr, - }), - signer: null, - }); - } - } - accts.push(addr); - txs.push({ - tx: makeApplicationCallTxnFromObject({ - accounts: accts, - appArgs: [textToUint8Array("completeTransfer"), vaa], - appIndex: safeBigIntToNumber(tokenBridgeId), - foreignAssets: foreignAssets, - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - - // We need to cover the inner transactions - if ( - parsedVAA.Fee !== undefined && - Buffer.compare(parsedVAA.Fee, Buffer.from(ZERO_PAD_BYTES, "hex")) === 0 - ) - txs[txs.length - 1]!.tx.fee = txs[txs.length - 1]!.tx.fee * 2; // Added non-null assertions - else txs[txs.length - 1]!.tx.fee = txs[txs.length - 1]!.tx.fee * 3; - - if (meta === "TokenBridge Transfer With Payload") { - txs[txs.length - 1]!.tx.appForeignApps = [aid]; // Added non-null assertion - - let m = ABIMethod.fromSignature("portal_transfer(byte[])byte[]"); - - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [m.getSelector(), (m.args[0]!.type as ABIType).encode(vaa)], // Added non-null assertion - appIndex: aid, - foreignAssets: foreignAssets, - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - } - } - - return txs; -} - -/** - * Parses the VAA into a Map - * @param vaa The VAA to be parsed - * @returns The ParsedVAA containing the parsed elements of the VAA - */ -export function _parseVAAAlgorand(vaa: Uint8Array): ParsedVAA { - let ret = {} as ParsedVAA; - let buf = Buffer.from(vaa); - ret.version = buf.readIntBE(0, 1); - ret.index = buf.readIntBE(1, 4); - ret.siglen = buf.readIntBE(5, 1); - const siglen = ret.siglen; - if (siglen) { - ret.signatures = extract3(vaa, 6, siglen * 66); - } - const sigs: Uint8Array[] = []; - for (let i = 0; i < siglen; i++) { - const start = 6 + i * 66; - const len = 66; - const sigBuf = extract3(vaa, start, len); - sigs.push(sigBuf); - } - ret.sigs = sigs; - let off = siglen * 66 + 6; - ret.digest = vaa.slice(off); // This is what is actually signed... - ret.timestamp = buf.readIntBE(off, 4); - off += 4; - ret.nonce = buf.readIntBE(off, 4); - off += 4; - ret.chainRaw = Buffer.from(extract3(vaa, off, 2)).toString("hex"); - ret.chain = buf.readIntBE(off, 2); - off += 2; - ret.emitter = Buffer.from(extract3(vaa, off, 32)).toString("hex"); - off += 32; - ret.sequence = buf.readBigUInt64BE(off); - off += 8; - ret.consistency = buf.readIntBE(off, 1); - off += 1; - - ret.Meta = "Unknown"; - - if ( - !Buffer.compare( - extract3(buf, off, 32), - Buffer.from("000000000000000000000000000000000000000000546f6b656e427269646765", "hex"), - ) - ) { - ret.Meta = "TokenBridge"; - ret.module = extract3(vaa, off, 32); - off += 32; - ret.action = buf.readIntBE(off, 1); - off += 1; - if (ret.action === 1) { - ret.Meta = "TokenBridge RegisterChain"; - ret.targetChain = buf.readIntBE(off, 2); - off += 2; - ret.EmitterChainID = buf.readIntBE(off, 2); - off += 2; - ret.targetEmitter = extract3(vaa, off, 32); - off += 32; - } else if (ret.action === 2) { - ret.Meta = "TokenBridge UpgradeContract"; - ret.targetChain = buf.readIntBE(off, 2); - off += 2; - ret.newContract = extract3(vaa, off, 32); - off += 32; - } - } else if ( - !Buffer.compare( - extract3(buf, off, 32), - Buffer.from("00000000000000000000000000000000000000000000000000000000436f7265", "hex"), - ) - ) { - ret.Meta = "CoreGovernance"; - ret.module = extract3(vaa, off, 32); - off += 32; - ret.action = buf.readIntBE(off, 1); - off += 1; - ret.targetChain = buf.readIntBE(off, 2); - off += 2; - ret.NewGuardianSetIndex = buf.readIntBE(off, 4); - } - - // ret.len=vaa.slice(off).length) - // ret.act=buf.readIntBE(off, 1)) - - ret.Body = vaa.slice(off); - - if (vaa.slice(off).length === 100 && buf.readIntBE(off, 1) === 2) { - ret.Meta = "TokenBridge Attest"; - ret.Type = buf.readIntBE(off, 1); - off += 1; - ret.Contract = uint8ArrayToHex(extract3(vaa, off, 32)); - off += 32; - ret.FromChain = buf.readIntBE(off, 2); - off += 2; - ret.Decimals = buf.readIntBE(off, 1); - off += 1; - ret.Symbol = extract3(vaa, off, 32); - off += 32; - ret.Name = extract3(vaa, off, 32); - } - - if (vaa.slice(off).length === 133 && buf.readIntBE(off, 1) === 1) { - ret.Meta = "TokenBridge Transfer"; - ret.Type = buf.readIntBE(off, 1); - off += 1; - ret.Amount = extract3(vaa, off, 32); - off += 32; - ret.Contract = uint8ArrayToHex(extract3(vaa, off, 32)); - off += 32; - ret.FromChain = buf.readIntBE(off, 2); - off += 2; - ret.ToAddress = extract3(vaa, off, 32); - off += 32; - ret.ToChain = buf.readIntBE(off, 2); - off += 2; - ret.Fee = extract3(vaa, off, 32); - } - - if (off >= buf.length) { - return ret; - } - if (buf.readIntBE(off, 1) === 3) { - ret.Meta = "TokenBridge Transfer With Payload"; - ret.Type = buf.readIntBE(off, 1); - off += 1; - ret.Amount = extract3(vaa, off, 32); - off += 32; - ret.Contract = uint8ArrayToHex(extract3(vaa, off, 32)); - off += 32; - ret.FromChain = buf.readIntBE(off, 2); - off += 2; - ret.ToAddress = extract3(vaa, off, 32); - off += 32; - ret.ToChain = buf.readIntBE(off, 2); - off += 2; - ret.FromAddress = extract3(vaa, off, 32); - off += 32; - ret.Payload = vaa.slice(off); - } - - console.log("Parsed VAA: ", ret); - return ret; -} - -// QUESTIONBW: Can this be removed entirely? -/** - * Parses the VAA into a Map - * @param vaa The VAA to be parsed - * @returns The ParsedVAA containing the parsed elements of the VAA - */ -// export function _parseNFTAlgorand(vaa: Uint8Array): ParsedVAA { -// let ret = _parseVAAAlgorand(vaa); - -// let arr = Buffer.from(ret.Body as Uint8Array); - -// ret.action = arr.readUInt8(0); -// ret.Contract = arr.slice(1, 1 + 32).toString('hex'); -// ret.FromChain = arr.readUInt16BE(33); -// ret.Symbol = Buffer.from(arr.slice(35, 35 + 32)); -// ret.Name = Buffer.from(arr.slice(67, 67 + 32)); -// ret.TokenId = arr.slice(99, 99 + 32); -// let uri_len = arr.readUInt8(131); -// ret.uri = Buffer.from(arr.slice(132, 132 + uri_len)) -// .toString('utf8') -// .replace(METADATA_REPLACE, ''); -// let target_offset = 132 + uri_len; -// ret.ToAddress = arr.slice(target_offset, target_offset + 32); -// ret.ToChain = arr.readUInt16BE(target_offset + 32); - -// return ret; -// } diff --git a/platforms/algorand/protocols/tokenBridge/src/apps.ts b/platforms/algorand/protocols/tokenBridge/src/apps.ts deleted file mode 100644 index e680030bf..000000000 --- a/platforms/algorand/protocols/tokenBridge/src/apps.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Algodv2, LogicSigAccount, decodeAddress, getApplicationAddress, modelsv2 } from "algosdk"; -import { LogicSigAccountInfo } from "./types"; -import { PopulateData, TmplSig } from "./tmplSig"; -import { hex } from "@wormhole-foundation/sdk-base/src/utils/encoding"; - -const accountExistsCache = new Set<[bigint, string]>(); - -export function getEmitterAddressAlgorand(appId: bigint): string { - console.log("appId: ", appId); - const appAddr: string = getApplicationAddress(appId); - const decAppAddr: Uint8Array = decodeAddress(appAddr).publicKey; - const hexAppAddr: string = hex.encode(decAppAddr); - console.log("Emitter address: ", hexAppAddr); - return hexAppAddr; -} - -/** - * Returns the local data for an application ID - * @param client Algodv2 client - * @param appId Application ID of interest - * @param address Address of the account - * @returns Promise with Uint8Array of data squirreled away - */ -export async function decodeLocalState( - client: Algodv2, - appId: bigint, - address: string, -): Promise { - let appState; - const ai = await client.accountInformation(address).do(); - const acctInfo = modelsv2.Account.from_obj_for_encoding(ai); - for (const app of acctInfo.appsLocalState!) { - if (BigInt(app.id) === appId) { - appState = app.keyValue; - break; - } - } - - let ret = Buffer.alloc(0); - let empty = Buffer.alloc(0); - if (appState) { - const e = Buffer.alloc(127); - const m = Buffer.from("meta"); - - let sk: string[] = []; - let vals: Map = new Map(); - for (const kv of appState) { - const k = Buffer.from(kv.key, "base64"); - const key: number = k.readInt8(); - if (!Buffer.compare(k, m)) { - continue; - } - const v: Buffer = Buffer.from(kv.value.bytes, "base64"); - if (Buffer.compare(v, e)) { - vals.set(key.toString(), v); - sk.push(key.toString()); - } - } - - sk.sort((a, b) => a.localeCompare(b, "en", { numeric: true })); - - sk.forEach((v) => { - ret = Buffer.concat([ret, vals.get(v) || empty]); - }); - } - return new Uint8Array(ret); -} - -/** - * Checks to see if the account exists for the application - * @param client An Algodv2 client - * @param appId Application ID - * @param acctAddr Account address to check - * @returns True, if account exists for application, False otherwise - */ -export async function accountExists( - client: Algodv2, - appId: bigint, - acctAddr: string, -): Promise { - if (accountExistsCache.has([appId, acctAddr])) return true; - - let ret = false; - try { - const acctInfoResp = await client.accountInformation(acctAddr).do(); - const acctInfo = modelsv2.Account.from_obj_for_encoding(acctInfoResp); - const als = acctInfo.appsLocalState; - if (!als) { - return ret; - } - als.forEach((app) => { - if (BigInt(app.id) === appId) { - accountExistsCache.add([appId, acctAddr]); - ret = true; - return; - } - }); - } catch (e) {} - return ret; -} - -/** - * Calculates the logic sig account for the application - * @param client An Algodv2 client - * @param appId Application ID - * @param appIndex Application index - * @param emitterId Emitter address - * @returns Promise with LogicSigAccountInfo - */ -export async function calcLogicSigAccount( - client: Algodv2, - appId: bigint, - appIndex: bigint, - emitterId: string, -): Promise { - let data: PopulateData = { - addrIdx: appIndex, - appAddress: getEmitterAddressAlgorand(appId), - appId: appId, - emitterId: emitterId, - }; - - const ts: TmplSig = new TmplSig(client); - const lsa: LogicSigAccount = await ts.populate(data); - const sigAddr: string = lsa.address(); - - const doesExist: boolean = await accountExists(client, appId, sigAddr); - return { - lsa, - doesExist, - }; -} diff --git a/platforms/algorand/protocols/tokenBridge/src/assets.ts b/platforms/algorand/protocols/tokenBridge/src/assets.ts index ab63bf82d..90636be21 100644 --- a/platforms/algorand/protocols/tokenBridge/src/assets.ts +++ b/platforms/algorand/protocols/tokenBridge/src/assets.ts @@ -1,4 +1,4 @@ -import { Chain, ChainId, toChainId } from "@wormhole-foundation/connect-sdk"; +import { ChainId } from "@wormhole-foundation/connect-sdk"; import { CHAIN_ID_ALGORAND, TransactionSignerPair, @@ -7,20 +7,15 @@ import { Algodv2, Transaction, bigIntToBytes, - bytesToBigInt, getApplicationAddress, makeApplicationOptInTxnFromObject, makePaymentTxnWithSuggestedParamsFromObject, + modelsv2, signLogicSigTransaction, - modelsv2 } from "algosdk"; -import { OptInResult, WormholeWrappedInfo } from "./types"; -import { safeBigIntToNumber } from "./utilities"; -import { calcLogicSigAccount, decodeLocalState } from "./apps"; -import { hex } from "@wormhole-foundation/sdk-base/src/utils/encoding"; -import { SEED_AMT } from "./constants"; - -const accountExistsCache = new Set<[bigint, string]>(); +import { StorageLsig } from "./storage"; +import { TransactionSet, WormholeWrappedInfo } from "./types"; +import { SEED_AMT, decodeLocalState, safeBigIntToNumber } from "./utilities"; /** * Returns a boolean if the asset is wrapped @@ -79,40 +74,6 @@ export async function getOriginalAssetOffAlgorand( return retVal; } -/** - * Returns an origin chain and asset address on {originChain} for a provided Wormhole wrapped address - * @param client Algodv2 client - * @param tokenBridgeId Application ID of the token bridge - * @param assetId Algorand asset index - * @returns Promise with the Algorand asset index or null - */ -export async function getWrappedAssetOnAlgorand( - client: Algodv2, - tokenBridgeId: bigint, - chain: ChainId | Chain, - contract: string, -): Promise { - const chainId = toChainId(chain); - if (chainId === CHAIN_ID_ALGORAND) { - return bytesToBigInt(hex.decode(contract)); - } else { - let { lsa, doesExist } = await calcLogicSigAccount( - client, - tokenBridgeId, - BigInt(chainId), - contract, - ); - if (!doesExist) { - return null; - } - let asset: Uint8Array = await decodeLocalState(client, tokenBridgeId, lsa.address()); - if (asset.length > 8) { - const tmp = Buffer.from(asset.slice(0, 8)); - return tmp.readBigUInt64BE(0); - } else return null; - } -} - /** * Calculates the logic sig account for the application * @param client An Algodv2 client @@ -122,32 +83,38 @@ export async function getWrappedAssetOnAlgorand( * @param emitterId Emitter address * @returns Address and array of TransactionSignerPairs */ -export async function optIn( +export async function maybeOptInTx( client: Algodv2, senderAddr: string, appId: bigint, - appIndex: bigint, - emitterId: string, -): Promise { + storage: StorageLsig, +): Promise { const appAddr: string = getApplicationAddress(appId); - // Check to see if we need to create this - const { doesExist, lsa } = await calcLogicSigAccount(client, appId, appIndex, emitterId); - const sigAddr: string = lsa.address(); + const lsa = storage.lsig(); + const storageAddress = lsa.address(); + + let exists = false; + try { + // TODO: check + await client.accountInformation(storageAddress).do(); + exists = true; + } catch {} + let txs: TransactionSignerPair[] = []; - if (!doesExist) { + if (!exists) { // These are the suggested params from the system const params = await client.getTransactionParams().do(); const seedTxn = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, - to: sigAddr, + to: storageAddress, amount: SEED_AMT, suggestedParams: params, }); seedTxn.fee = seedTxn.fee * 2; txs.push({ tx: seedTxn, signer: null }); const optinTxn = makeApplicationOptInTxnFromObject({ - from: sigAddr, + from: storageAddress, suggestedParams: params, appIndex: safeBigIntToNumber(appId), rekeyTo: appAddr, @@ -160,11 +127,10 @@ export async function optIn( signTxn: (txn: Transaction) => Promise.resolve(signLogicSigTransaction(txn, lsa).blob), }, }); - - accountExistsCache.add([appId, lsa.address()]); } + return { - addr: sigAddr, + address: storageAddress, txs, }; } diff --git a/platforms/algorand/protocols/tokenBridge/src/bigVarint.ts b/platforms/algorand/protocols/tokenBridge/src/bigVarint.ts index 1410d1309..beac1869c 100644 --- a/platforms/algorand/protocols/tokenBridge/src/bigVarint.ts +++ b/platforms/algorand/protocols/tokenBridge/src/bigVarint.ts @@ -1,57 +1,60 @@ // Forever grateful to https://github.com/joeltg/big-varint/blob/main/src/unsigned.ts -const LIMIT = BigInt(0x7f); - -export function encodingLength(value: bigint): number { - let i = 0; - - for (; value >= BigInt(0x80); i++) { - value >>= BigInt(7); - } - - return i + 1; -} - -export function encode(i: bigint, buffer?: ArrayBuffer, byteOffset?: number): Uint8Array { - if (i < BigInt(0)) { - throw new RangeError("value must be unsigned"); - } - - const byteLength = encodingLength(i); - buffer = buffer || new ArrayBuffer(byteLength); - byteOffset = byteOffset || 0; - if (buffer.byteLength < byteOffset + byteLength) { - throw new RangeError("the buffer is too small to encode the number at the offset"); - } - - const array = new Uint8Array(buffer, byteOffset); - - let offset = 0; - while (LIMIT < i) { - array[offset++] = Number(i & LIMIT) | 0x80; - i >>= BigInt(7); - } - - array[offset] = Number(i); - - return array; -} - -export function encodeHex(i: bigint, buffer?: ArrayBuffer, byteOffset?: number): string { - return Buffer.from(encode(i, buffer, byteOffset)).toString("hex"); -} - -export function decode(data: Uint8Array, offset = 0): bigint { - let i = BigInt(0); - let n = 0; - let b: number | undefined; - do { - b = data[offset + n]; - if (b === undefined) { - throw new RangeError("offset out of range"); + +import { encoding } from "@wormhole-foundation/connect-sdk"; + +// Useful for encoding numbers as varints to patch TEAL binary +export const varint = { + encodingLength: (value: bigint) => { + let i = 0; + for (; value >= BigInt(0x80); i++) { + value >>= BigInt(7); + } + return i + 1; + }, + + encode: (i: bigint | number, buffer?: ArrayBuffer, byteOffset?: number) => { + if (typeof i === "number") i = BigInt(i); + + const LIMIT = BigInt(0x7f); + if (i < BigInt(0)) { + throw new RangeError("value must be unsigned"); + } + + const byteLength = varint.encodingLength(i); + buffer = buffer || new ArrayBuffer(byteLength); + byteOffset = byteOffset || 0; + if (buffer.byteLength < byteOffset + byteLength) { + throw new RangeError("the buffer is too small to encode the number at the offset"); + } + + const array = new Uint8Array(buffer, byteOffset); + + let offset = 0; + while (LIMIT < i) { + array[offset++] = Number(i & LIMIT) | 0x80; + i >>= BigInt(7); } - i += BigInt(b & 0x7f) << BigInt(n * 7); - n++; - } while (0x80 <= b); - return i; -} + array[offset] = Number(i); + + return array; + }, + decode: (data: Uint8Array, offset = 0) => { + let i = BigInt(0); + let n = 0; + let b: number | undefined; + do { + b = data[offset + n]; + if (b === undefined) { + throw new RangeError("offset out of range"); + } + + i += BigInt(b & 0x7f) << BigInt(n * 7); + n++; + } while (0x80 <= b); + return i; + }, + encodeHex: (i: bigint | number, buffer?: ArrayBuffer, byteOffset?: number) => { + return encoding.hex.encode(varint.encode(i, buffer, byteOffset)); + }, +}; diff --git a/platforms/algorand/protocols/tokenBridge/src/constants.ts b/platforms/algorand/protocols/tokenBridge/src/constants.ts deleted file mode 100644 index 9057c26fe..000000000 --- a/platforms/algorand/protocols/tokenBridge/src/constants.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const SEED_AMT: number = 1002000; - -export const ZERO_PAD_BYTES = "0000000000000000000000000000000000000000000000000000000000000000"; - -export const MAX_KEYS: number = 15; - -export const MAX_BYTES_PER_KEY: number = 127; - -export const BITS_PER_BYTE: number = 8; - -export const BITS_PER_KEY: number = MAX_BYTES_PER_KEY * BITS_PER_BYTE; - -export const MAX_BYTES: number = MAX_BYTES_PER_KEY * MAX_KEYS; - -export const MAX_BITS: number = BITS_PER_BYTE * MAX_BYTES; - -export const MAX_SIGS_PER_TXN: number = 6; - -export const ALGO_VERIFY_HASH = "EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A"; - -export const ALGO_VERIFY = new Uint8Array([ - 6, 32, 4, 1, 0, 32, 20, 38, 1, 0, 49, 32, 50, 3, 18, 68, 49, 1, 35, 18, 68, 49, 16, 129, 6, 18, - 68, 54, 26, 1, 54, 26, 3, 54, 26, 2, 136, 0, 3, 68, 34, 67, 53, 2, 53, 1, 53, 0, 40, 53, 240, 40, - 53, 241, 52, 0, 21, 53, 5, 35, 53, 3, 35, 53, 4, 52, 3, 52, 5, 12, 65, 0, 68, 52, 1, 52, 0, 52, 3, - 129, 65, 8, 34, 88, 23, 52, 0, 52, 3, 34, 8, 36, 88, 52, 0, 52, 3, 129, 33, 8, 36, 88, 7, 0, 53, - 241, 53, 240, 52, 2, 52, 4, 37, 88, 52, 240, 52, 241, 80, 2, 87, 12, 20, 18, 68, 52, 3, 129, 66, - 8, 53, 3, 52, 4, 37, 8, 53, 4, 66, 255, 180, 34, 137, -]); - -export const METADATA_REPLACE = new RegExp("\u0000", "g"); diff --git a/platforms/algorand/protocols/tokenBridge/src/index.ts b/platforms/algorand/protocols/tokenBridge/src/index.ts index a6b57e564..f6ed9f03c 100644 --- a/platforms/algorand/protocols/tokenBridge/src/index.ts +++ b/platforms/algorand/protocols/tokenBridge/src/index.ts @@ -12,13 +12,10 @@ declare global { registerProtocol(_platform, "TokenBridge", AlgorandTokenBridge); -export * from "./apps"; export * from "./assets"; -export * from "./constants"; -export * from "./tmplSig"; +export * from "./storage"; export * from "./tokenBridge"; export * from "./tokenBridge"; -export * from "./transfers"; export * from "./types"; export * from "./utilities"; -export * from "./_vaa"; +export * from "./vaa"; diff --git a/platforms/algorand/protocols/tokenBridge/src/storage.ts b/platforms/algorand/protocols/tokenBridge/src/storage.ts new file mode 100644 index 000000000..edf1e4589 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/storage.ts @@ -0,0 +1,99 @@ +import { + Chain, + TokenId, + WormholeMessageId, + encoding, + toChainId, +} from "@wormhole-foundation/connect-sdk"; +import { LogicSigAccount, decodeAddress, getApplicationAddress } from "algosdk"; +import { varint } from "./bigVarint"; +import { MAX_BITS } from "./utilities"; + +export interface PopulateData { + // App Id we're storing data for + appId: bigint; + appAddress: Uint8Array; + + // address for the emitter or contract or + address: Uint8Array; + // specific for the emi + idx: bigint; +} + +export class StorageLsig { + // Used only to cache the compiled bytecode + constructor(private bytecode: Uint8Array) {} + + lsig() { + return new LogicSigAccount(this.bytecode); + } + + // Get the storage lsig for a wormhole message id + static forMessageId(appId: bigint, whm: WormholeMessageId): StorageLsig { + const appAddress = decodeAddress(getApplicationAddress(appId)).publicKey; + + const emitterAddr = whm.emitter.toUniversalAddress().toUint8Array(); + const chainIdBytes = encoding.bignum.toBytes(BigInt(toChainId(whm.chain)), 2); + const address = encoding.bytes.concat(chainIdBytes, emitterAddr); + + return StorageLsig.fromData({ + appId, + appAddress, + idx: whm.sequence / BigInt(MAX_BITS), + address, + }); + } + + // Get the storage lsig for a wrapped asset + static forWrappedAsset(appId: bigint, token: TokenId): StorageLsig { + const appAddress = decodeAddress(getApplicationAddress(appId)).publicKey; + return StorageLsig.fromData({ + appId, + appAddress, + idx: BigInt(toChainId(token.chain)), + address: token.address.toUniversalAddress().toUint8Array(), + }); + } + + // Get the storage lsig for a wrapped asset + static forNativeAsset(appId: bigint, tokenId: bigint): StorageLsig { + const appAddress = decodeAddress(getApplicationAddress(appId)).publicKey; + return StorageLsig.fromData({ + appId, + appAddress, + idx: tokenId, + address: encoding.bytes.encode("native"), + }); + } + + // Get the storage lsig for the guardian set + static forGuardianSet(appId: bigint, idx: bigint | number): StorageLsig { + const appAddress = decodeAddress(getApplicationAddress(appId)).publicKey; + return StorageLsig.fromData({ + appId, + appAddress, + idx: BigInt(idx), + address: encoding.bytes.encode("guardian"), + }); + } + + static fromData(data: PopulateData): StorageLsig { + // This patches the binary of the TEAL program used to store data + // to produce a logic sig that can be used to sign transactions + // to store data in the its account local state for a given app + const byteStrings = [ + "0620010181", + varint.encodeHex(data.idx), + "4880", + varint.encodeHex(data.address.length), + encoding.hex.encode(data.address), + "483110810612443119221244311881", + varint.encodeHex(data.appId), + "1244312080", + varint.encodeHex(data.appAddress.length), + encoding.hex.encode(data.appAddress), + "124431018100124431093203124431153203124422", + ]; + return new StorageLsig(encoding.hex.decode(byteStrings.join(""))); + } +} diff --git a/platforms/algorand/protocols/tokenBridge/src/tmplSig.ts b/platforms/algorand/protocols/tokenBridge/src/tmplSig.ts deleted file mode 100644 index d4b0068e4..000000000 --- a/platforms/algorand/protocols/tokenBridge/src/tmplSig.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Algodv2, LogicSigAccount } from "algosdk"; -import { encodeHex } from "./bigVarint"; -import { hexToUint8Array } from "./utilities"; -import { encoding, sha512_256 } from "@wormhole-foundation/connect-sdk"; - -// This is the data structure to be populated in the call to populate() below -// Yes, it needs to be filled out before calling populate() -interface IPopulateData { - appId: bigint; - appAddress: string; - addrIdx: bigint; - emitterId: string; -} -export type PopulateData = Required; - -export class TmplSig { - algoClient: Algodv2; - sourceHash: string; - bytecode: Uint8Array; - - constructor(algoClient: Algodv2) { - this.algoClient = algoClient; - this.sourceHash = ""; - this.bytecode = new Uint8Array(); - } - - async compile(source: string) { - const hash = encoding.hex.encode(sha512_256(source)); - if (hash !== this.sourceHash) { - const response = await this.algoClient.compile(source).do(); - this.bytecode = new Uint8Array(Buffer.from(response.result, "base64")); - this.sourceHash = hash; - } - } - - /** - * Populate data in the TEAL source and return the LogicSig object based on the resulting compiled bytecode. - * @param data The data to populate fields with. - * @notes emitterId must be prefixed with '0x'. appAddress must be decoded with algoSDK and prefixed with '0x'. - * @returns A LogicSig object. - */ - async populate(data: PopulateData): Promise { - const byteString: string = [ - "0620010181", - encodeHex(data.addrIdx), - "4880", - encodeHex(BigInt(data.emitterId.length / 2)), - data.emitterId, - "483110810612443119221244311881", - encodeHex(data.appId), - "1244312080", - encodeHex(BigInt(data.appAddress.length / 2)), - data.appAddress, - "124431018100124431093203124431153203124422", - ].join(""); - this.bytecode = hexToUint8Array(byteString); - return new LogicSigAccount(this.bytecode); - } -} diff --git a/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts b/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts index 6cebd985a..682f43b8f 100644 --- a/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts @@ -14,6 +14,7 @@ import { TokenId, UniversalAddress, UnsignedTransaction, + encoding, serialize, toChain, toChainId, @@ -29,19 +30,33 @@ import { AnyAlgorandAddress, TransactionSignerPair, } from "@wormhole-foundation/connect-sdk-algorand"; -import { Algodv2, bigIntToBytes, bytesToBigInt, getApplicationAddress } from "algosdk"; import { + ABIMethod, + ABIType, + Algodv2, + OnApplicationComplete, + SuggestedParams, + bigIntToBytes, + bytesToBigInt, + decodeAddress, + encodeAddress, + getApplicationAddress, + makeApplicationCallTxnFromObject, + makeAssetTransferTxnWithSuggestedParamsFromObject, + makePaymentTxnWithSuggestedParamsFromObject, + modelsv2, +} from "algosdk"; +import { + assetOptinCheck, getIsWrappedAssetOnAlgorand, getOriginalAssetOffAlgorand, - getWrappedAssetOnAlgorand, + maybeOptInTx, } from "./assets"; -import { - attestFromAlgorand, - getIsTransferCompletedAlgorand, - redeemOnAlgorand, - transferFromAlgorand, -} from "./transfers"; -import { submitVAAHeader } from "./_vaa"; +import { StorageLsig } from "./storage"; +import { decodeLocalState, safeBigIntToNumber, checkBitsSet, getMessageFee } from "./utilities"; +import { submitVAAHeader } from "./vaa"; + +import "@wormhole-foundation/connect-sdk-algorand-core"; export class AlgorandTokenBridge implements TokenBridge @@ -120,23 +135,22 @@ export class AlgorandTokenBridge // Returns the address of the native version of this asset async getWrappedAsset(token: TokenId): Promise> { - const assetId = await getWrappedAssetOnAlgorand( + const storageAccount = StorageLsig.forWrappedAsset(this.tokenBridgeAppId, token); + const lsa = storageAccount.lsig(); + + let asset: Uint8Array = await decodeLocalState( this.connection, this.tokenBridgeAppId, - token.chain, - token.address.toString(), + lsa.address(), ); - if (assetId === null) { - throw new Error(`Algorand asset ${token.address} not found`); - } - - const nativeAddress = toNative(this.chain, bigIntToBytes(assetId, 8)); + if (asset.length < 8) throw new Error("Invalid wrapped asset data"); + const nativeAddress = toNative(this.chain, asset.slice(0, 8)); return nativeAddress; } // Checks if a wrapped version exists - async hasWrappedAsset(token: TokenId): Promise { + async hasWrappedAsset(token: TokenId): Promise { try { await this.getWrappedAsset(token); return true; @@ -148,15 +162,24 @@ export class AlgorandTokenBridge return toNative(this.chain, new AlgorandAddress(AlgorandZeroAddress).toString()); } - async isTransferCompleted( - vaa: TokenBridge.VAA<"Transfer" | "TransferWithPayload">, - ): Promise { - const completed = getIsTransferCompletedAlgorand( - this.connection, - this.tokenBridgeAppId, - serialize(vaa), - ); - return completed; + async isTransferCompleted(vaa: TokenBridge.TransferVAA): Promise { + const whm = { + sequence: vaa.sequence, + chain: vaa.emitterChain, + emitter: vaa.emitterAddress, + }; + const sl = StorageLsig.forMessageId(this.tokenBridgeAppId, whm); + try { + const isBitSet = await checkBitsSet( + this.connection, + this.tokenBridgeAppId, + sl.lsig().address(), + whm.sequence, + ); + return isBitSet; + } catch { + return false; + } } // Creates a Token Attestation VAA containing metadata about @@ -170,38 +193,177 @@ export class AlgorandTokenBridge const senderAddr = payer.toString(); const assetId = bytesToBigInt(new AlgorandAddress(token_to_attest.toString()).toUint8Array()); - const utxns = await attestFromAlgorand( + const txs: TransactionSignerPair[] = []; + + const tbs = StorageLsig.fromData({ + appId: this.coreAppId, + appAddress: decodeAddress(this.coreAppAddress).publicKey, + idx: BigInt(0), + address: decodeAddress(this.tokenBridgeAddress).publicKey, + }); + + const { address: emitterAddr, txs: emitterOptInTxs } = await maybeOptInTx( this.connection, - this.tokenBridgeAppId, + senderAddr, this.coreAppId, - senderAddr.toString(), - assetId, + tbs, + ); + txs.push(...emitterOptInTxs); + + let creatorAddr = ""; + let creatorAcctInfo; + const attestSelector: Uint8Array = encoding.bytes.encode("attestToken"); + + if (assetId !== BigInt(0)) { + const assetInfoResp = await this.connection.getAssetByID(safeBigIntToNumber(assetId)).do(); + const assetInfo = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); + const creatorAcctInfoResp = await this.connection + .accountInformation(assetInfo.params.creator) + .do(); + creatorAcctInfo = modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); + if (creatorAcctInfo.authAddr === this.tokenBridgeAddress.toString()) { + throw new Error("Cannot re-attest wormhole assets"); + } + } + + const nativeStorageAcct = StorageLsig.forNativeAsset(this.tokenBridgeAppId, assetId); + const txns = await maybeOptInTx( + this.connection, + senderAddr, + this.tokenBridgeAppId, + nativeStorageAcct, ); + creatorAddr = txns.address; + txs.push(...txns.txs); + + const suggParams: SuggestedParams = await this.connection.getTransactionParams().do(); + + const firstTxn = makeApplicationCallTxnFromObject({ + from: senderAddr, + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + onComplete: OnApplicationComplete.NoOpOC, + appArgs: [encoding.bytes.encode("nop")], + suggestedParams: suggParams, + }); + txs.push({ tx: firstTxn, signer: null }); + + const mfee = await getMessageFee(this.connection, this.coreAppId); + if (mfee > BigInt(0)) { + const feeTxn = makePaymentTxnWithSuggestedParamsFromObject({ + from: senderAddr, + suggestedParams: suggParams, + to: this.tokenBridgeAddress, + amount: mfee, + }); + txs.push({ tx: feeTxn, signer: null }); + } - for (const utxn of utxns) { - yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.createAttestation", true); + let accts: string[] = [emitterAddr, creatorAddr, this.coreAppAddress]; + + if (creatorAcctInfo) { + accts.push(creatorAcctInfo.address); + } + + let appTxn = makeApplicationCallTxnFromObject({ + appArgs: [attestSelector, bigIntToBytes(assetId, 8)], + accounts: accts, + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + foreignApps: [safeBigIntToNumber(this.coreAppId)], + foreignAssets: [safeBigIntToNumber(assetId)], + from: senderAddr, + onComplete: OnApplicationComplete.NoOpOC, + suggestedParams: suggParams, + }); + if (mfee > BigInt(0)) { + appTxn.fee *= 3; + } else { + appTxn.fee *= 2; + } + txs.push({ tx: appTxn, signer: null }); + + for (const utxn of txs) { + yield this.createUnsignedTx(utxn, "TokenBridge.createAttestation", true); } } // Submits the Token Attestation VAA to the Token Bridge // to create the wrapped token represented by the data in the VAA async *submitAttestation( - vaa: TokenBridge.VAA<"AttestMeta">, - payer?: AnyAlgorandAddress, + vaa: TokenBridge.AttestVAA, + sender?: AnyAlgorandAddress, + params?: SuggestedParams, ): AsyncGenerator> { - if (!payer) throw new Error("Payer required to create attestation"); + if (!sender) throw new Error("Payer required to create attestation"); + if (!params) params = await this.connection.getTransactionParams().do(); - const senderAddr = payer.toString(); - const { txs } = await submitVAAHeader( + const senderAddr = sender.toString(); + + const tokenStorage = StorageLsig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token); + const tokenStorageAddress = tokenStorage.lsig().address(); + + const txs: TransactionSignerPair[] = []; + + let asset: Uint8Array = await decodeLocalState( this.connection, this.tokenBridgeAppId, - serialize(vaa), - senderAddr, - this.coreAppId, + tokenStorageAddress, ); + let foreignAssets: number[] = []; + if (asset.length > 8) { + const tmp = Buffer.from(asset.slice(0, 8)); + foreignAssets.push(safeBigIntToNumber(tmp.readBigUInt64BE(0))); + } + + const noopSelector = encoding.bytes.encode("nop"); + txs.push({ + tx: makePaymentTxnWithSuggestedParamsFromObject({ + from: senderAddr, + to: tokenStorageAddress, + amount: 100000, + suggestedParams: params, + }), + }); + let buf: Uint8Array = new Uint8Array(1); + buf[0] = 0x01; + txs.push({ + tx: makeApplicationCallTxnFromObject({ + appArgs: [noopSelector, buf], + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + from: senderAddr, + onComplete: OnApplicationComplete.NoOpOC, + suggestedParams: params, + }), + }); + + buf = new Uint8Array(1); + buf[0] = 0x02; + txs.push({ + tx: makeApplicationCallTxnFromObject({ + appArgs: [noopSelector, buf], + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + from: senderAddr, + onComplete: OnApplicationComplete.NoOpOC, + suggestedParams: params, + }), + }); + const receiveAttestSelector = encoding.bytes.encode("receiveAttest"); + txs.push({ + tx: makeApplicationCallTxnFromObject({ + accounts: [], // TODO: + appArgs: [receiveAttestSelector, serialize(vaa)], + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + foreignAssets: foreignAssets, + from: senderAddr, + onComplete: OnApplicationComplete.NoOpOC, + suggestedParams: params, + }), + }); + + txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * 2; // QUESTIONBW: There are like 3 different ways of adjusting fees in various functions--this should be standardized + for (const utxn of txs) { - yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.submitAttestation", true); + yield this.createUnsignedTx(utxn, "TokenBridge.submitAttestation", true); } } @@ -221,51 +383,260 @@ export class AlgorandTokenBridge const receiver = recipient.address.toUniversalAddress(); const fee = BigInt(0); - console.log( - "About to transferFromAlgorand: ", - senderAddr, - assetId, - qty, - receiver.toString(), - chain, - fee, - ); - const utxns = await transferFromAlgorand( + + const recipientChainId = toChainId(chain); + + const tbs = StorageLsig.fromData({ + appId: this.coreAppId, + appAddress: decodeAddress(this.coreAppAddress).publicKey, + idx: BigInt(0), + address: decodeAddress(this.tokenBridgeAddress).publicKey, + }); + + const txs: TransactionSignerPair[] = []; + const { address: emitterAddr, txs: emitterOptInTxs } = await maybeOptInTx( this.connection, - this.tokenBridgeAppId, - this.coreAppId, senderAddr, - assetId, - qty, - receiver, - chain, - fee, - payload, + this.coreAppId, + tbs, ); + txs.push(...emitterOptInTxs); + + // Check that the auth address of the creator + // is the token bridge + let creator = ""; + let creatorAcct: modelsv2.Account | undefined; + let wormhole: boolean = false; + if (assetId !== BigInt(0)) { + const assetInfoResp: Record = await this.connection + .getAssetByID(safeBigIntToNumber(assetId)) + .do(); + const asset = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); + creator = asset.params.creator; + const creatorAcctInfoResp = await this.connection.accountInformation(creator).do(); + creatorAcct = modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); + wormhole = creatorAcct.authAddr === this.tokenBridgeAddress.toString(); + } + + const params: SuggestedParams = await this.connection.getTransactionParams().do(); + const msgFee: bigint = await getMessageFee(this.connection, this.coreAppId); + if (msgFee > 0) + txs.push({ + tx: makePaymentTxnWithSuggestedParamsFromObject({ + from: senderAddr, + suggestedParams: params, + to: this.tokenBridgeAddress, + amount: msgFee, + }), + signer: null, + }); + + if (!wormhole) { + const storage = StorageLsig.forNativeAsset(this.tokenBridgeAppId, assetId); + const { address, txs } = await maybeOptInTx( + this.connection, + senderAddr, + this.tokenBridgeAppId, + storage, + ); + creator = address; + txs.push(...txs); + } + + if (assetId !== BigInt(0) && !(await assetOptinCheck(this.connection, assetId, creator))) { + // Looks like we need to optin + const payTxn = makePaymentTxnWithSuggestedParamsFromObject({ + from: senderAddr, + to: creator, + amount: 100000, + suggestedParams: params, + }); + txs.push({ tx: payTxn, signer: null }); + // The tokenid app needs to do the optin since it has signature authority + const bOptin: Uint8Array = encoding.bytes.encode("optin"); + let txn = makeApplicationCallTxnFromObject({ + from: senderAddr, + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + onComplete: OnApplicationComplete.NoOpOC, + appArgs: [bOptin, bigIntToBytes(assetId, 8)], + foreignAssets: [safeBigIntToNumber(assetId)], + accounts: [creator], + suggestedParams: params, + }); + txn.fee *= 2; + txs.push({ tx: txn, signer: null }); + } - for (const utxn of utxns) { - yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.transfer", true); + const t = makeApplicationCallTxnFromObject({ + from: senderAddr, + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + onComplete: OnApplicationComplete.NoOpOC, + appArgs: [encoding.bytes.encode("nop")], + suggestedParams: params, + }); + txs.push({ tx: t, signer: null }); + + let accounts: string[] = []; + if (assetId === BigInt(0)) { + const t = makePaymentTxnWithSuggestedParamsFromObject({ + from: senderAddr, + to: creator, + amount: qty, + suggestedParams: params, + }); + txs.push({ tx: t, signer: null }); + accounts = [emitterAddr, creator, creator]; + } else { + const t = makeAssetTransferTxnWithSuggestedParamsFromObject({ + from: senderAddr, + to: creator, + suggestedParams: params, + amount: qty, + assetIndex: safeBigIntToNumber(assetId), + }); + txs.push({ tx: t, signer: null }); + + accounts = creatorAcct?.address + ? [emitterAddr, creator, creatorAcct.address] + : [emitterAddr, creator]; + } + + const args = [ + encoding.bytes.encode("sendTransfer"), + bigIntToBytes(assetId, 8), + bigIntToBytes(qty, 8), + receiver.toUint8Array(), + bigIntToBytes(recipientChainId, 8), + bigIntToBytes(fee, 8), + ]; + + if (payload) args.push(payload); + + const acTxn = makeApplicationCallTxnFromObject({ + from: senderAddr, + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + onComplete: OnApplicationComplete.NoOpOC, + appArgs: args, + foreignApps: [safeBigIntToNumber(this.coreAppId)], + foreignAssets: [safeBigIntToNumber(assetId)], + accounts: accounts, + suggestedParams: params, + }); + acTxn.fee *= 2; + txs.push({ tx: acTxn, signer: null }); + + for (const utxn of txs) { + yield this.createUnsignedTx(utxn, "TokenBridge.transfer", true); } } - // Redeems a transfer VAA to receive the tokens on this chain async *redeem( - sender: AccountAddress, - vaa: TokenBridge.VAA<"Transfer" | "TransferWithPayload">, + sender: AnyAlgorandAddress, + vaa: TokenBridge.TransferVAA, unwrapNative: boolean = true, - ): AsyncGenerator> { - const senderAddr = new AlgorandAddress(sender.toString()).toString(); + params?: SuggestedParams, + ) { + if (!params) params = await this.connection.getTransactionParams().do(); - const utxns = await redeemOnAlgorand( + const senderAddr = new AlgorandAddress(sender).toString(); + let { accounts, txs } = await submitVAAHeader( this.connection, - this.tokenBridgeAppId, this.coreAppId, - serialize(vaa), + this.tokenBridgeAppId, + vaa, senderAddr, ); - for (const utxn of utxns) { - yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.redeem", true); + const tokenStorage = StorageLsig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token); + const tokenStorageAddress = tokenStorage.lsig().address(); + + let foreignAssets: number[] = []; + let assetId: number = 0; + if (vaa.payload.token.chain !== "Algorand") { + let asset = await decodeLocalState( + this.connection, + this.tokenBridgeAppId, + tokenStorageAddress, + ); + if (asset.length > 8) { + const tmp = Buffer.from(asset.slice(0, 8)); + assetId = safeBigIntToNumber(tmp.readBigUInt64BE(0)); + } + } else { + assetId = parseInt(vaa.payload.token.address.toString().slice(2), 16); + } + accounts.push(tokenStorageAddress); + + let aid = 0; + let addr = ""; + if (vaa.payloadName === "TransferWithPayload") { + aid = Number(bytesToBigInt(vaa.payload.to.address.toUint8Array())); + addr = getApplicationAddress(aid); + } else { + addr = encodeAddress(vaa.payload.to.address.toUint8Array()); + } + + if (assetId !== 0) { + foreignAssets.push(assetId); + if (!(await assetOptinCheck(this.connection, BigInt(assetId), addr))) { + if (senderAddr != addr) { + throw new Error("cannot ASA optin for somebody else (asset " + assetId.toString() + ")"); + } + + txs.unshift({ + tx: makeAssetTransferTxnWithSuggestedParamsFromObject({ + amount: 0, + assetIndex: assetId, + from: senderAddr, + suggestedParams: params, + to: senderAddr, + }), + signer: null, + }); + } + } + + accounts.push(addr); + txs.push({ + tx: makeApplicationCallTxnFromObject({ + accounts: accounts, + appArgs: [encoding.bytes.encode("completeTransfer"), serialize(vaa)], + appIndex: safeBigIntToNumber(this.tokenBridgeAppId), + foreignAssets: foreignAssets, + from: senderAddr, + onComplete: OnApplicationComplete.NoOpOC, + suggestedParams: params, + }), + signer: null, + }); + + // We need to cover the inner transactions + if (vaa.payloadName === "Transfer" && vaa.payload.fee !== undefined && vaa.payload.fee === 0n) { + txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * 2; + } else { + txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * 3; + } + + if (vaa.payloadName === "TransferWithPayload") { + txs[txs.length - 1].tx.appForeignApps = [aid]; + + let m = ABIMethod.fromSignature("portal_transfer(byte[])byte[]"); + + txs.push({ + tx: makeApplicationCallTxnFromObject({ + appArgs: [m.getSelector(), (m.args[0].type as ABIType).encode(serialize(vaa))], + appIndex: aid, + foreignAssets: foreignAssets, + from: senderAddr, + onComplete: OnApplicationComplete.NoOpOC, + suggestedParams: params, + }), + signer: null, + }); + } + + for (const utxn of txs) { + yield this.createUnsignedTx(utxn, "TokenBridge.redeem", true); } } diff --git a/platforms/algorand/protocols/tokenBridge/src/transfers.ts b/platforms/algorand/protocols/tokenBridge/src/transfers.ts deleted file mode 100644 index 50ede98c1..000000000 --- a/platforms/algorand/protocols/tokenBridge/src/transfers.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { - Algodv2, - OnApplicationComplete, - SuggestedParams, - bigIntToBytes, - decodeAddress, - getApplicationAddress, - makeApplicationCallTxnFromObject, - makeAssetTransferTxnWithSuggestedParamsFromObject, - makePaymentTxnWithSuggestedParamsFromObject, - modelsv2, -} from "algosdk"; -import { calcLogicSigAccount, getEmitterAddressAlgorand } from "./apps"; -import { BITS_PER_KEY, MAX_BITS } from "./constants"; -import { - safeBigIntToNumber, - textToHexString, - textToUint8Array, - uint8ArrayToHex, -} from "./utilities"; -import { _parseVAAAlgorand, _submitVAAAlgorand } from "./_vaa"; -import { assetOptinCheck, optIn } from "./assets"; -import { Chain, UniversalAddress, toChainId } from "@wormhole-foundation/connect-sdk"; -import { TransactionSignerPair } from "@wormhole-foundation/connect-sdk-algorand"; - -/** - * This function is used to check if a VAA has been redeemed by looking at a specific bit - * @param client AlgodV2 client - * @param appId Application Id - * @param addr Wallet address. Someone has to pay for this - * @param seq The sequence number of the redemption - * @returns True, if the bit was set and VAA was redeemed, False otherwise - */ -async function checkBitsSet( - client: Algodv2, - appId: bigint, - addr: string, - seq: bigint, -): Promise { - let retval: boolean = false; - let appState: modelsv2.TealKeyValue[] | undefined; - const acctInfoResp = await client.accountInformation(addr).do(); - const acctInfo = modelsv2.Account.from_obj_for_encoding(acctInfoResp); - const als = acctInfo.appsLocalState; - als && - als.forEach((app) => { - if (BigInt(app.id) === appId) { - appState = app.keyValue; - } - }); - if (appState?.length === 0) { - return retval; - } - - const BIG_MAX_BITS: bigint = BigInt(MAX_BITS); - const BIG_EIGHT: bigint = BigInt(8); - // Start on a MAX_BITS boundary - const start: bigint = (seq / BIG_MAX_BITS) * BIG_MAX_BITS; - // beg should be in the range [0..MAX_BITS] - const beg: number = safeBigIntToNumber(seq - start); - // s should be in the range [0..15] - const s: number = Math.floor(beg / BITS_PER_KEY); - const b: number = Math.floor((beg - s * BITS_PER_KEY) / 8); - - const key = Buffer.from(bigIntToBytes(s, 1)).toString("base64"); - appState?.forEach((kv) => { - if (kv.key === key) { - const v = Buffer.from(kv.value.bytes, "base64"); - const bt = 1 << safeBigIntToNumber(seq % BIG_EIGHT); - retval = (v[b]! & bt) != 0; // Added non-null assertion - return; - } - }); - return retval; -} - -/** - * Returns true if this transfer was completed on Algorand - * @param client AlgodV2 client - * @param appId Most likely the Token bridge ID - * @param signedVAA VAA to check - * @returns True if VAA has been redeemed, False otherwise - */ -export async function getIsTransferCompletedAlgorand( - client: Algodv2, - appId: bigint, - signedVAA: Uint8Array, -): Promise { - const parsedVAA = _parseVAAAlgorand(signedVAA); // TODO: rip this out and look for deserialize('TokenBridge:Attestation', bytes) - const seq: bigint = parsedVAA.sequence; - const chainRaw: string = parsedVAA.chainRaw; // this needs to be a hex string - const em: string = parsedVAA.emitter; // this needs to be a hex string - - const { doesExist, lsa } = await calcLogicSigAccount( - client, - appId, - seq / BigInt(MAX_BITS), - chainRaw + em, - ); - if (!doesExist) { - return false; - } - const seqAddr = lsa.address(); - const retVal: boolean = await checkBitsSet(client, appId, seqAddr, seq); - return retVal; -} - -/** - * Return the message fee for the core bridge - * @param client An Algodv2 client - * @param bridgeId The application ID of the core bridge - * @returns Promise with the message fee for the core bridge - */ -export async function getMessageFee(client: Algodv2, bridgeId: bigint): Promise { - const applInfoResp: Record = await client - .getApplicationByID(safeBigIntToNumber(bridgeId)) - .do(); - const appInfo = modelsv2.Application.from_obj_for_encoding(applInfoResp); - const globalState = appInfo.params.globalState; - const key: string = Buffer.from("MessageFee", "binary").toString("base64"); - let ret = BigInt(0); - globalState && - globalState.forEach((kv) => { - if (kv.key === key) { - ret = BigInt(kv.value.uint); - return; - } - }); - console.log("Message Fee: ", ret); - return ret; -} - -/** - * Attest an already created asset - * If you create a new asset on algorand and want to transfer it elsewhere, - * you create an attestation for it on algorand, pass that vaa to the target chain, - * submit it, and then you can transfer from Algorand to that target chain - * @param client An Algodv2 client - * @param tokenBridgeId The ID of the token bridge - * @param senderAcct The account paying fees - * @param assetId The asset index - * @returns Transaction ID - */ -export async function attestFromAlgorand( - client: Algodv2, - tokenBridgeId: bigint, - bridgeId: bigint, - senderAddr: string, - assetId: bigint, -): Promise { - const tbAddr: string = getApplicationAddress(tokenBridgeId); - const decTbAddr: Uint8Array = decodeAddress(tbAddr).publicKey; - const aa: string = uint8ArrayToHex(decTbAddr); - const txs: TransactionSignerPair[] = []; - // "attestFromAlgorand::emitterAddr" - const { addr: emitterAddr, txs: emitterOptInTxs } = await optIn( - client, - senderAddr, - bridgeId, - BigInt(0), - aa, - ); - txs.push(...emitterOptInTxs); - - let creatorAddr = ""; - let creatorAcctInfo; - const bPgmName: Uint8Array = textToUint8Array("attestToken"); - - if (assetId !== BigInt(0)) { - const assetInfoResp = await client.getAssetByID(safeBigIntToNumber(assetId)).do(); - const assetInfo = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); - const creatorAcctInfoResp = await client.accountInformation(assetInfo.params.creator).do(); - creatorAcctInfo = modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); - if (creatorAcctInfo.authAddr === tbAddr) { - throw new Error("Cannot re-attest wormhole assets"); - } - } - const result = await optIn(client, senderAddr, tokenBridgeId, assetId, textToHexString("native")); - creatorAddr = result.addr; - txs.push(...result.txs); - - const suggParams: SuggestedParams = await client.getTransactionParams().do(); - - const firstTxn = makeApplicationCallTxnFromObject({ - from: senderAddr, - appIndex: safeBigIntToNumber(tokenBridgeId), - onComplete: OnApplicationComplete.NoOpOC, - appArgs: [textToUint8Array("nop")], - suggestedParams: suggParams, - }); - txs.push({ tx: firstTxn, signer: null }); - - const mfee = await getMessageFee(client, bridgeId); - if (mfee > BigInt(0)) { - const feeTxn = makePaymentTxnWithSuggestedParamsFromObject({ - from: senderAddr, - suggestedParams: suggParams, - to: getApplicationAddress(tokenBridgeId), - amount: mfee, - }); - txs.push({ tx: feeTxn, signer: null }); - } - - let accts: string[] = [emitterAddr, creatorAddr, getApplicationAddress(bridgeId)]; - - if (creatorAcctInfo) { - accts.push(creatorAcctInfo.address); - } - - let appTxn = makeApplicationCallTxnFromObject({ - appArgs: [bPgmName, bigIntToBytes(assetId, 8)], - accounts: accts, - appIndex: safeBigIntToNumber(tokenBridgeId), - foreignApps: [safeBigIntToNumber(bridgeId)], - foreignAssets: [safeBigIntToNumber(assetId)], - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: suggParams, - }); - if (mfee > BigInt(0)) { - appTxn.fee *= 3; - } else { - appTxn.fee *= 2; - } - txs.push({ tx: appTxn, signer: null }); - - return txs; -} - -/** - * Submits the VAA to Algorand - * @param client AlgodV2 client - * @param tokenBridgeId Token bridge ID - * @param bridgeId Core bridge ID - * @param vaa The VAA to be redeemed - * @param acct Sending account - * @returns Promise with array of TransactionSignerPair - */ -export async function redeemOnAlgorand( - client: Algodv2, - tokenBridgeId: bigint, - bridgeId: bigint, - vaa: Uint8Array, - senderAddr: string, -): Promise { - return await _submitVAAAlgorand(client, tokenBridgeId, bridgeId, vaa, senderAddr); -} - -/** - * Transfers an asset from Algorand to a receiver on another chain - * @param client AlgodV2 client - * @param tokenBridgeId Application ID of the token bridge - * @param bridgeId Application ID of the core bridge - * @param senderAddr Sending account - * @param assetId Asset index - * @param qty Quantity to transfer - * @param receiver Receiving account - * @param chain Reeiving chain - * @param fee Transfer fee - * @param payload payload for payload3 transfers - * @returns Promise with array of TransactionSignerPair - */ -export async function transferFromAlgorand( - client: Algodv2, - tokenBridgeId: bigint, - bridgeId: bigint, - senderAddr: string, - assetId: bigint, - qty: bigint, - receiver: UniversalAddress, - chain: Chain, - fee: bigint, - payload: Uint8Array | null = null, -): Promise { - const recipientChainId = toChainId(chain); - const tokenAddr: string = getApplicationAddress(tokenBridgeId); - const applAddr: string = getEmitterAddressAlgorand(tokenBridgeId); - const txs: TransactionSignerPair[] = []; - // "transferAsset" - const { addr: emitterAddr, txs: emitterOptInTxs } = await optIn( - client, - senderAddr, - bridgeId, - BigInt(0), - applAddr, - ); - txs.push(...emitterOptInTxs); - let creator = ""; - let creatorAcct: modelsv2.Account | undefined; - let wormhole: boolean = false; - if (assetId !== BigInt(0)) { - const assetInfoResp: Record = await client - .getAssetByID(safeBigIntToNumber(assetId)) - .do(); - const asset = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); - creator = asset.params.creator; - const creatorAcctInfoResp = await client.accountInformation(creator).do(); - creatorAcct = modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); - const authAddr = creatorAcct.authAddr; - if (authAddr === tokenAddr) { - wormhole = true; - } - } - - const params: SuggestedParams = await client.getTransactionParams().do(); - const msgFee: bigint = await getMessageFee(client, bridgeId); - if (msgFee > 0) { - const payTxn = makePaymentTxnWithSuggestedParamsFromObject({ - from: senderAddr, - suggestedParams: params, - to: getApplicationAddress(tokenBridgeId), - amount: msgFee, - }); - txs.push({ tx: payTxn, signer: null }); - } - if (!wormhole) { - const bNat = Buffer.from("native", "binary").toString("hex"); - // "creator" - const result = await optIn(client, senderAddr, tokenBridgeId, assetId, bNat); - creator = result.addr; - txs.push(...result.txs); - } - - if (assetId !== BigInt(0) && !(await assetOptinCheck(client, assetId, creator))) { - // Looks like we need to optin - const payTxn = makePaymentTxnWithSuggestedParamsFromObject({ - from: senderAddr, - to: creator, - amount: 100000, - suggestedParams: params, - }); - txs.push({ tx: payTxn, signer: null }); - // The tokenid app needs to do the optin since it has signature authority - const bOptin: Uint8Array = textToUint8Array("optin"); - let txn = makeApplicationCallTxnFromObject({ - from: senderAddr, - appIndex: safeBigIntToNumber(tokenBridgeId), - onComplete: OnApplicationComplete.NoOpOC, - appArgs: [bOptin, bigIntToBytes(assetId, 8)], - foreignAssets: [safeBigIntToNumber(assetId)], - accounts: [creator], - suggestedParams: params, - }); - txn.fee *= 2; - txs.push({ tx: txn, signer: null }); - } - const t = makeApplicationCallTxnFromObject({ - from: senderAddr, - appIndex: safeBigIntToNumber(tokenBridgeId), - onComplete: OnApplicationComplete.NoOpOC, - appArgs: [textToUint8Array("nop")], - suggestedParams: params, - }); - txs.push({ tx: t, signer: null }); - - let accounts: string[] = []; - if (assetId === BigInt(0)) { - const t = makePaymentTxnWithSuggestedParamsFromObject({ - from: senderAddr, - to: creator, - amount: qty, - suggestedParams: params, - }); - txs.push({ tx: t, signer: null }); - accounts = [emitterAddr, creator, creator]; - } else { - const t = makeAssetTransferTxnWithSuggestedParamsFromObject({ - from: senderAddr, - to: creator, - suggestedParams: params, - amount: qty, - assetIndex: safeBigIntToNumber(assetId), - }); - txs.push({ tx: t, signer: null }); - - accounts = creatorAcct?.address - ? [emitterAddr, creator, creatorAcct.address] - : [emitterAddr, creator]; - } - console.log("transferFromAlgorand receiver: ", receiver); - const receiverBytes = new Uint8Array(receiver.toUint8Array()); - console.log("receiverBytes: ", receiverBytes); - - let args = [ - textToUint8Array("sendTransfer"), - bigIntToBytes(assetId, 8), - bigIntToBytes(qty, 8), - receiverBytes, - bigIntToBytes(recipientChainId, 8), - bigIntToBytes(fee, 8), - ]; - console.log("Args: ", args); - if (payload !== null) { - args.push(payload); - } - let acTxn = makeApplicationCallTxnFromObject({ - from: senderAddr, - appIndex: safeBigIntToNumber(tokenBridgeId), - onComplete: OnApplicationComplete.NoOpOC, - appArgs: args, - foreignApps: [safeBigIntToNumber(bridgeId)], - foreignAssets: [safeBigIntToNumber(assetId)], - accounts: accounts, - suggestedParams: params, - }); - acTxn.fee *= 2; - txs.push({ tx: acTxn, signer: null }); - return txs; -} - -// TODO: Need to figure out what to do with this -export async function createWrappedOnAlgorand( - client: Algodv2, - tokenBridgeId: bigint, - bridgeId: bigint, - senderAddr: string, - attestVAA: Uint8Array, -): Promise { - return await _submitVAAAlgorand(client, tokenBridgeId, bridgeId, attestVAA, senderAddr); -} diff --git a/platforms/algorand/protocols/tokenBridge/src/types.ts b/platforms/algorand/protocols/tokenBridge/src/types.ts index 2035a4194..aa6f7089f 100644 --- a/platforms/algorand/protocols/tokenBridge/src/types.ts +++ b/platforms/algorand/protocols/tokenBridge/src/types.ts @@ -2,8 +2,8 @@ import { ChainId } from "@wormhole-foundation/connect-sdk"; import { LogicSigAccount } from "algosdk"; import { TransactionSignerPair } from "@wormhole-foundation/connect-sdk-algorand"; -export type OptInResult = { - addr: string; +export type TransactionSet = { + address: string; txs: TransactionSignerPair[]; }; @@ -17,50 +17,3 @@ export type LogicSigAccountInfo = { lsa: LogicSigAccount; doesExist: boolean; }; - -export type ParsedVAA = { - version: number; - index: number; - siglen: number; - signatures: Uint8Array; - sigs: Uint8Array[]; - digest: Uint8Array; - timestamp: number; - nonce: number; - chainRaw: string; - chain: number; - emitter: string; - sequence: bigint; - consistency: number; - Meta: - | "Unknown" - | "TokenBridge" - | "TokenBridge RegisterChain" - | "TokenBridge UpgradeContract" - | "CoreGovernance" - | "TokenBridge Attest" - | "TokenBridge Transfer" - | "TokenBridge Transfer With Payload"; - module?: Uint8Array; - action?: number; - targetChain?: number; - EmitterChainID?: number; - targetEmitter?: Uint8Array; - newContract?: Uint8Array; - NewGuardianSetIndex?: number; - Type?: number; - Contract?: string; - FromChain?: number; - Decimals?: number; - Symbol?: Uint8Array; - Name?: Uint8Array; - TokenId?: Uint8Array; - Amount?: Uint8Array; - ToAddress?: Uint8Array; - ToChain?: number; - Fee?: Uint8Array; - FromAddress?: Uint8Array; - Payload?: Uint8Array; - Body?: Uint8Array; - uri?: string; -}; diff --git a/platforms/algorand/protocols/tokenBridge/src/utilities.ts b/platforms/algorand/protocols/tokenBridge/src/utilities.ts index 5c1497da8..5b176da82 100644 --- a/platforms/algorand/protocols/tokenBridge/src/utilities.ts +++ b/platforms/algorand/protocols/tokenBridge/src/utilities.ts @@ -1,4 +1,35 @@ -import { bytesToBigInt } from "algosdk"; +import { Algodv2, bigIntToBytes, modelsv2, decodeAddress, getApplicationAddress } from "algosdk"; + +export const SEED_AMT: number = 1002000; + +export const ZERO_PAD_BYTES = "0000000000000000000000000000000000000000000000000000000000000000"; + +export const MAX_KEYS: number = 15; + +export const MAX_BYTES_PER_KEY: number = 127; + +export const BITS_PER_BYTE: number = 8; + +export const BITS_PER_KEY: number = MAX_BYTES_PER_KEY * BITS_PER_BYTE; + +export const MAX_BYTES: number = MAX_BYTES_PER_KEY * MAX_KEYS; + +export const MAX_BITS: number = BITS_PER_BYTE * MAX_BYTES; + +export const MAX_SIGS_PER_TXN: number = 6; + +export const ALGO_VERIFY_HASH = "EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A"; + +export const ALGO_VERIFY = new Uint8Array([ + 6, 32, 4, 1, 0, 32, 20, 38, 1, 0, 49, 32, 50, 3, 18, 68, 49, 1, 35, 18, 68, 49, 16, 129, 6, 18, + 68, 54, 26, 1, 54, 26, 3, 54, 26, 2, 136, 0, 3, 68, 34, 67, 53, 2, 53, 1, 53, 0, 40, 53, 240, 40, + 53, 241, 52, 0, 21, 53, 5, 35, 53, 3, 35, 53, 4, 52, 3, 52, 5, 12, 65, 0, 68, 52, 1, 52, 0, 52, 3, + 129, 65, 8, 34, 88, 23, 52, 0, 52, 3, 34, 8, 36, 88, 52, 0, 52, 3, 129, 33, 8, 36, 88, 7, 0, 53, + 241, 53, 240, 52, 2, 52, 4, 37, 88, 52, 240, 52, 241, 80, 2, 87, 12, 20, 18, 68, 52, 3, 129, 66, + 8, 53, 3, 52, 4, 37, 8, 53, 4, 66, 255, 180, 34, 137, +]); + +export const METADATA_REPLACE = new RegExp("\u0000", "g"); export function safeBigIntToNumber(b: bigint): number { if (b < BigInt(Number.MIN_SAFE_INTEGER) || b > BigInt(Number.MAX_SAFE_INTEGER)) { @@ -7,49 +38,136 @@ export function safeBigIntToNumber(b: bigint): number { return Number(b); } -// TODO: Remove - VAA encoding/decoding can be done by the serialize/deserialize functions -export function extract3(buffer: Uint8Array, start: number, size: number) { - return buffer.slice(start, start + size); -} +/** + * This function is used to check if a VAA has been redeemed by looking at a specific bit + * @param client AlgodV2 client + * @param appId Application Id + * @param addr Wallet address. Someone has to pay for this + * @param seq The sequence number of the redemption + * @returns True, if the bit was set and VAA was redeemed, False otherwise + */ +export async function checkBitsSet( + client: Algodv2, + appId: bigint, + addr: string, + seq: bigint, +): Promise { + let retval: boolean = false; + let appState: modelsv2.TealKeyValue[] | undefined; + const acctInfoResp = await client.accountInformation(addr).do(); + const acctInfo = modelsv2.Account.from_obj_for_encoding(acctInfoResp); + const als = acctInfo.appsLocalState; + als && + als.forEach((app) => { + if (BigInt(app.id) === appId) { + appState = app.keyValue; + } + }); + if (appState?.length === 0) { + return retval; + } -// export function uint8ArrayToNativeStringAlgorand(a: Uint8Array): string { -// return encodeAddress(a); -// } + const BIG_MAX_BITS: bigint = BigInt(MAX_BITS); + const BIG_EIGHT: bigint = BigInt(8); + // Start on a MAX_BITS boundary + const start: bigint = (seq / BIG_MAX_BITS) * BIG_MAX_BITS; + // beg should be in the range [0..MAX_BITS] + const beg: number = safeBigIntToNumber(seq - start); + // s should be in the range [0..15] + const s: number = Math.floor(beg / BITS_PER_KEY); + const b: number = Math.floor((beg - s * BITS_PER_KEY) / 8); -// export function hexToNativeStringAlgorand(s: string): string { -// return uint8ArrayToNativeStringAlgorand(hexToUint8Array(s)); -// } + const key = Buffer.from(bigIntToBytes(s, 1)).toString("base64"); + appState?.forEach((kv) => { + if (kv.key === key) { + const v = Buffer.from(kv.value.bytes, "base64"); + const bt = 1 << safeBigIntToNumber(seq % BIG_EIGHT); + retval = (v[b]! & bt) != 0; // Added non-null assertion + return; + } + }); + return retval; +} -// export function nativeStringToHexAlgorand(s: string): string { -// return uint8ArrayToHex(decodeAddress(s).publicKey); -// } +/** + * Return the message fee for the core bridge + * @param client An Algodv2 client + * @param bridgeId The application ID of the core bridge + * @returns Promise with the message fee for the core bridge + */ +export async function getMessageFee(client: Algodv2, bridgeId: bigint): Promise { + const applInfoResp: Record = await client + .getApplicationByID(safeBigIntToNumber(bridgeId)) + .do(); + const appInfo = modelsv2.Application.from_obj_for_encoding(applInfoResp); + const globalState = appInfo.params.globalState; + const key: string = Buffer.from("MessageFee", "binary").toString("base64"); + let ret = BigInt(0); + globalState && + globalState.forEach((kv) => { + if (kv.key === key) { + ret = BigInt(kv.value.uint); + return; + } + }); + return ret; +} -// TODO: If the string is a hex string, this can be replaced by bigname.decode() -export function hexToNativeAssetBigIntAlgorand(s: string): bigint { - return bytesToBigInt(hexToUint8Array(s)); +// Convert an id to the 32 byte Uint8array representing +// the bytes of the derived app address +export function idToAddressBytes(id: bigint): Uint8Array { + const appAddr: string = getApplicationAddress(id); + return decodeAddress(appAddr).publicKey; } -// export function hexToNativeAssetStringAlgorand(s: string): string { -// return uint8ArrayToNativeStringAlgorand(hexToUint8Array(s)); -// } +/** + * Returns the local data for an application ID + * @param client Algodv2 client + * @param appId Application ID of interest + * @param address Address of the account + * @returns Promise with Uint8Array of data squirreled away + */ +export async function decodeLocalState( + client: Algodv2, + appId: bigint, + address: string, +): Promise { + let appState; + const ai = await client.accountInformation(address).do(); + const acctInfo = modelsv2.Account.from_obj_for_encoding(ai); + for (const app of acctInfo.appsLocalState!) { + if (BigInt(app.id) === appId) { + appState = app.keyValue; + break; + } + } -// hex.encode -export const uint8ArrayToHex = (a: Uint8Array): string => { - return Buffer.from(a).toString("hex"); -}; + let ret = Buffer.alloc(0); + let empty = Buffer.alloc(0); + if (appState) { + const e = Buffer.alloc(127); + const m = Buffer.from("meta"); -// hex.decode -export const hexToUint8Array = (h: string): Uint8Array => { - if (h.startsWith("0x")) h = h.slice(2); - return new Uint8Array(Buffer.from(h, "hex")); -}; + let sk: string[] = []; + let vals: Map = new Map(); + for (const kv of appState) { + const k = Buffer.from(kv.key, "base64"); + const key: number = k.readInt8(); + if (!Buffer.compare(k, m)) { + continue; + } + const v: Buffer = Buffer.from(kv.value.bytes, "base64"); + if (Buffer.compare(v, e)) { + vals.set(key.toString(), v); + sk.push(key.toString()); + } + } -// TODO -export function textToHexString(name: string): string { - return Buffer.from(name, "binary").toString("hex"); -} + sk.sort((a, b) => a.localeCompare(b, "en", { numeric: true })); -// TODO: bytes.encode -export function textToUint8Array(name: string): Uint8Array { - return new Uint8Array(Buffer.from(name, "binary")); + sk.forEach((v) => { + ret = Buffer.concat([ret, vals.get(v) || empty]); + }); + } + return new Uint8Array(ret); } diff --git a/platforms/algorand/protocols/tokenBridge/src/vaa.ts b/platforms/algorand/protocols/tokenBridge/src/vaa.ts index 93fbf10e4..da73350bf 100644 --- a/platforms/algorand/protocols/tokenBridge/src/vaa.ts +++ b/platforms/algorand/protocols/tokenBridge/src/vaa.ts @@ -1,60 +1,33 @@ -import { - TokenBridge, - encoding, - keccak256, - serialize, - toChainId, -} from "@wormhole-foundation/connect-sdk"; +import { TokenBridge, encoding, keccak256, serialize } from "@wormhole-foundation/connect-sdk"; import { TransactionSignerPair } from "@wormhole-foundation/connect-sdk-algorand"; import { - ABIMethod, - ABIType, Algodv2, LogicSigAccount, OnApplicationComplete, SuggestedParams, Transaction, - encodeAddress, - getApplicationAddress, makeApplicationCallTxnFromObject, - makeAssetTransferTxnWithSuggestedParamsFromObject, - makePaymentTxnWithSuggestedParamsFromObject, signLogicSigTransaction, } from "algosdk"; -import { decodeLocalState } from "./apps"; -import { assetOptinCheck, optIn } from "./assets"; -import { ALGO_VERIFY, ALGO_VERIFY_HASH, MAX_BITS, MAX_SIGS_PER_TXN } from "./constants"; +import { maybeOptInTx } from "./assets"; +import { StorageLsig } from "./storage"; import { - hexToNativeAssetBigIntAlgorand, + ALGO_VERIFY, + ALGO_VERIFY_HASH, + MAX_SIGS_PER_TXN, safeBigIntToNumber, - textToHexString, - textToUint8Array, - uint8ArrayToHex, + decodeLocalState, } from "./utilities"; -class SubmitVAAState { - vaaMap: TokenBridge.VAA; +type SubmitVAAState = { accounts: string[]; txs: TransactionSignerPair[]; - guardianAddr: string; - - constructor( - vaaMap: TokenBridge.VAA, - accounts: string[], - txs: TransactionSignerPair[], - guardianAddr: string, - ) { - this.vaaMap = vaaMap; - this.accounts = accounts; - this.txs = txs; - this.guardianAddr = guardianAddr; - } -} +}; /** * Submits just the header of the VAA * @param client AlgodV2 client - * @param bridgeId Application ID of the core b * ridge + * @param bridgeId Application ID of the core bridge * @param vaa The VAA (Just the header is used) * @param senderAddr Sending account address * @param appid Application ID @@ -62,45 +35,41 @@ class SubmitVAAState { */ export async function submitVAAHeader( client: Algodv2, - bridgeId: bigint, + coreId: bigint, + appid: bigint, vaa: TokenBridge.VAA, senderAddr: string, - appid: bigint, ): Promise { - const index: number = vaa.guardianSet; - const seq: bigint = vaa.sequence / BigInt(MAX_BITS); - const em: string = vaa.emitterAddress.toString().slice(2); - const chainId: string = BigInt(toChainId(vaa.emitterChain)).toString(16).padStart(4, "0"); - console.log(em, chainId); - let txs: TransactionSignerPair[] = []; - // "seqAddr" - console.log("SEQY", seq / BigInt(MAX_BITS)); - const { addr: seqAddr, txs: seqOptInTxs } = await optIn( + + // Get storage acct for message id + const msgStorage = StorageLsig.forMessageId(appid, { + chain: vaa.emitterChain, + sequence: vaa.sequence, + emitter: vaa.emitterAddress, + }); + const { address: seqAddr, txs: seqOptInTxs } = await maybeOptInTx( client, senderAddr, appid, - seq / BigInt(MAX_BITS), - chainId + em, + msgStorage, ); txs.push(...seqOptInTxs); - const guardianPgmName = textToHexString("guardian"); - // And then the signatures to help us verify the vaa_s - // "guardianAddr" - const { addr: guardianAddr, txs: guardianOptInTxs } = await optIn( + // Get storage account for guardian set + const gsStorage = StorageLsig.forGuardianSet(coreId, vaa.guardianSet); + const { address: guardianAddr, txs: guardianOptInTxs } = await maybeOptInTx( client, senderAddr, - bridgeId, - BigInt(index), - guardianPgmName, + coreId, + gsStorage, ); txs.push(...guardianOptInTxs); + let accts: string[] = [seqAddr, guardianAddr]; - // When we attest for a new token, we need some place to store the info... later we will need to - // mirror the other way as well - const keys: Uint8Array = await decodeLocalState(client, bridgeId, guardianAddr); + // Get the guardian keys + const keys: Uint8Array = await decodeLocalState(client, coreId, guardianAddr); const params: SuggestedParams = await client.getTransactionParams().do(); @@ -113,11 +82,11 @@ export async function submitVAAHeader( // How many signatures can we process in a single txn... we can do 6! // There are likely upwards of 19 signatures. So, we ned to split things up const numSigs: number = vaa.signatures.length; - let numTxns: number = Math.floor(numSigs / MAX_SIGS_PER_TXN) + 1; + const numTxns: number = Math.floor(numSigs / MAX_SIGS_PER_TXN) + 1; const SIG_LEN: number = 66; const GuardianKeyLen: number = 20; - const verifySigArg: Uint8Array = textToUint8Array("verifySigs"); + const verifySigArg: Uint8Array = encoding.bytes.encode("verifySigs"); const lsa = new LogicSigAccount(ALGO_VERIFY); for (let nt = 0; nt < numTxns; nt++) { @@ -152,7 +121,7 @@ export async function submitVAAHeader( digest, ], accounts: accts, - appIndex: safeBigIntToNumber(bridgeId), + appIndex: safeBigIntToNumber(coreId), from: ALGO_VERIFY_HASH, onComplete: OnApplicationComplete.NoOpOC, suggestedParams: params, @@ -167,9 +136,9 @@ export async function submitVAAHeader( }); } const appTxn = makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("verifyVAA"), serialize(vaa)], + appArgs: [encoding.bytes.encode("verifyVAA"), serialize(vaa)], accounts: accts, - appIndex: safeBigIntToNumber(bridgeId), + appIndex: safeBigIntToNumber(coreId), from: senderAddr, onComplete: OnApplicationComplete.NoOpOC, suggestedParams: params, @@ -177,221 +146,5 @@ export async function submitVAAHeader( appTxn.fee = appTxn.fee * (2 + numTxns); // Was 1 txs.push({ tx: appTxn, signer: null }); - return new SubmitVAAState(vaa, accts, txs, guardianAddr); -} - -/** - * Submits the VAA to the application - * @param client AlgodV2 client - * @param tokenBridgeId Application ID of the token bridge - * @param bridgeId Application ID of the core bridge - * @param vaa The VAA to be submitted - * @param senderAddr Sending account address - * @returns Promise with an array of TransactionSignerPair - */ -export async function _submitVAAAlgorand( - client: Algodv2, - tokenBridgeId: bigint, - bridgeId: bigint, - vaa: TokenBridge.VAA, - senderAddr: string, -): Promise { - let sstate = await submitVAAHeader(client, bridgeId, vaa, senderAddr, tokenBridgeId); - - let parsedVAA = sstate.vaaMap; - let accts = sstate.accounts; - let txs = sstate.txs; - - // When we attest for a new token, we need some place to store the info... later we will need to - // mirror the other way as well - const meta = parsedVAA.payloadName; - let chainAddr: string = ""; - if (meta === "AttestMeta" || meta === "Transfer" || meta === "TransferWithPayload") { - if (parsedVAA.payload.token.chain !== "Algorand") { - // "TokenBridge chainAddr" - - const chainId = BigInt(toChainId(parsedVAA.payload.token.chain)); - const result = await optIn( - client, - senderAddr, - tokenBridgeId, - chainId, - parsedVAA.payload.token.address.toString().slice(2), - ); - chainAddr = result.addr; - txs.unshift(...result.txs); - } else { - const assetId = hexToNativeAssetBigIntAlgorand( - parsedVAA.payload.token.address.toString().slice(2), - ); - console.log("OTHER ASSET ID?", assetId); - // "TokenBridge native chainAddr" - const result = await optIn( - client, - senderAddr, - tokenBridgeId, - assetId, - textToHexString("native"), - ); - chainAddr = result.addr; - txs.unshift(...result.txs); - } - accts.push(chainAddr); - } - - const params: SuggestedParams = await client.getTransactionParams().do(); - - if (meta === "AttestMeta") { - let asset: Uint8Array = await decodeLocalState(client, tokenBridgeId, chainAddr); - let foreignAssets: number[] = []; - if (asset.length > 8) { - const tmp = Buffer.from(asset.slice(0, 8)); - foreignAssets.push(safeBigIntToNumber(tmp.readBigUInt64BE(0))); - } - txs.push({ - tx: makePaymentTxnWithSuggestedParamsFromObject({ - from: senderAddr, - to: chainAddr, - amount: 100000, - suggestedParams: params, - }), - signer: null, - }); - let buf: Uint8Array = new Uint8Array(1); - buf[0] = 0x01; - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("nop"), buf], - appIndex: safeBigIntToNumber(tokenBridgeId), - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - - buf = new Uint8Array(1); - buf[0] = 0x02; - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [textToUint8Array("nop"), buf], - appIndex: safeBigIntToNumber(tokenBridgeId), - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - - txs.push({ - tx: makeApplicationCallTxnFromObject({ - accounts: accts, - appArgs: [textToUint8Array("receiveAttest"), serialize(vaa)], - appIndex: safeBigIntToNumber(tokenBridgeId), - foreignAssets: foreignAssets, - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * 2; // QUESTIONBW: There are like 3 different ways of adjusting fees in various functions--this should be standardized - } - - if (meta === "Transfer" || meta === "TransferWithPayload") { - let foreignAssets: number[] = []; - let assetId: number = 0; - if (parsedVAA.payload.token.chain !== "Algorand") { - let asset = await decodeLocalState(client, tokenBridgeId, chainAddr); - if (asset.length > 8) { - const tmp = Buffer.from(asset.slice(0, 8)); - assetId = safeBigIntToNumber(tmp.readBigUInt64BE(0)); - } - } else { - assetId = parseInt(parsedVAA.payload.token.address.toString().slice(2), 16); - } - - console.log("ASSET ID", assetId); - // The receiver needs to be optin in to receive the coins... Yeah, the relayer pays for this - - let aid = 0; - let addr = ""; - - if (parsedVAA.payload !== undefined) { - if (parsedVAA.payload.to.chain === "Algorand" && meta === "TransferWithPayload") { - aid = Number( - hexToNativeAssetBigIntAlgorand( - uint8ArrayToHex(parsedVAA.payload.to.address.toUint8Array()), - ), - ); - addr = getApplicationAddress(aid); - } else { - addr = encodeAddress(parsedVAA.payload.to.address.toUint8Array()); - } - } - - if (assetId !== 0) { - foreignAssets.push(assetId); - if (!(await assetOptinCheck(client, BigInt(assetId), addr))) { - if (senderAddr != addr) { - throw new Error("cannot ASA optin for somebody else (asset " + assetId.toString() + ")"); - } - - txs.unshift({ - tx: makeAssetTransferTxnWithSuggestedParamsFromObject({ - amount: 0, - assetIndex: assetId, - from: senderAddr, - suggestedParams: params, - to: senderAddr, - }), - signer: null, - }); - } - } - accts.push(addr); - txs.push({ - tx: makeApplicationCallTxnFromObject({ - accounts: accts, - appArgs: [textToUint8Array("completeTransfer"), serialize(vaa)], - appIndex: safeBigIntToNumber(tokenBridgeId), - foreignAssets: foreignAssets, - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - - // We need to cover the inner transactions - if ( - parsedVAA.payloadName === "Transfer" && - parsedVAA.payload.fee !== undefined && - parsedVAA.payload.fee === 0n - ) { - txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * 2; - } else { - txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * 3; - } - - if (meta === "TransferWithPayload") { - txs[txs.length - 1].tx.appForeignApps = [aid]; - - let m = ABIMethod.fromSignature("portal_transfer(byte[])byte[]"); - - txs.push({ - tx: makeApplicationCallTxnFromObject({ - appArgs: [m.getSelector(), (m.args[0].type as ABIType).encode(serialize(vaa))], - appIndex: aid, - foreignAssets: foreignAssets, - from: senderAddr, - onComplete: OnApplicationComplete.NoOpOC, - suggestedParams: params, - }), - signer: null, - }); - } - } - - return txs; + return { accounts: accts, txs }; } diff --git a/platforms/algorand/src/address.ts b/platforms/algorand/src/address.ts index 0b1a75808..8f104e132 100644 --- a/platforms/algorand/src/address.ts +++ b/platforms/algorand/src/address.ts @@ -15,22 +15,24 @@ export const AlgorandZeroAddress = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA export class AlgorandAddress implements Address { static readonly byteSize = 32; static readonly platform: Platform = _platform; + + readonly type: string = "Native"; + // stored as checksum address private readonly address: string; constructor(address: AnyAlgorandAddress) { if (AlgorandAddress.instanceof(address)) { - const a = address as unknown as AlgorandAddress; - this.address = a.address; + this.address = address.address; } else if (UniversalAddress.instanceof(address)) { - this.address = encodeAddress(address.unwrap()); - } else if (typeof address === "string") { + this.address = encodeAddress(address.toUint8Array()); + } else if (typeof address === "string" && isValidAddress(address)) { this.address = address; } else if (address instanceof Uint8Array && address.byteLength === AlgorandAddress.byteSize) { this.address = encodeAddress(address); } else if (address instanceof Uint8Array && address.byteLength === 8) { // ASA IDs are 8 bytes; this is padded to 32 bytes like addresses - this.address = encodeAddress(encoding.bytes.zpad(address, 32)); + this.address = encodeAddress(encoding.bytes.zpad(address, AlgorandAddress.byteSize)); } else throw new Error(`Invalid Algorand address or ASA ID: ${address}`); } @@ -49,23 +51,22 @@ export class AlgorandAddress implements Address { toUniversalAddress() { return new UniversalAddress(this.toUint8Array()); } - static isValidAddress(address: string) { - return isValidAddress(address); - } + static instanceof(address: any): address is AlgorandAddress { - return address.platform === AlgorandPlatform._platform; + return address.constructor.platform === AlgorandPlatform._platform; } + equals(other: AlgorandAddress | UniversalAddress): boolean { if (AlgorandAddress.instanceof(other)) { return other.address === this.address; } else { - return other.equals(this.toUniversalAddress()); + return this.toUniversalAddress().equals(other); } } } declare global { - namespace Wormhole { + namespace WormholeNamespace { interface PlatformToNativeAddressMapping { // @ts-ignore Algorand: AlgorandAddress; diff --git a/platforms/algorand/src/platform.ts b/platforms/algorand/src/platform.ts index 4899a2cc6..2e5e3fee4 100644 --- a/platforms/algorand/src/platform.ts +++ b/platforms/algorand/src/platform.ts @@ -180,7 +180,6 @@ export class AlgorandPlatform extends PlatformContext extends PlatformContext { const versionResp = await rpc.versionsCheck().do(); const version = modelsv2.Version.from_obj_for_encoding(versionResp); - // const genesisHash = Buffer.from(version.genesisHashB64).toString("base64"); return this.chainFromChainId(version.genesisId); } } diff --git a/platforms/algorand/src/testing/signer.ts b/platforms/algorand/src/testing/signer.ts index 3527c7678..de3d4cf71 100644 --- a/platforms/algorand/src/testing/signer.ts +++ b/platforms/algorand/src/testing/signer.ts @@ -5,7 +5,7 @@ import { Signer, UnsignedTransaction, } from "@wormhole-foundation/connect-sdk"; -import { Account, Algodv2, assignGroupID, mnemonicToSecretKey } from "algosdk"; +import { Account, Algodv2, Transaction, assignGroupID, mnemonicToSecretKey } from "algosdk"; import { AlgorandChains } from "../types"; import { AlgorandPlatform } from "../platform"; @@ -53,19 +53,19 @@ export class AlgorandSigner