From 7056e868439a9ee1f3401d5b6dd9ebbb2051e0da Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Wed, 27 Dec 2023 07:46:42 -0500 Subject: [PATCH] adding Algorand work from pr --- core/base/src/constants/rpc.ts | 4 +- examples/package.json | 9 + examples/src/algoTokenBridge.ts | 178 +++++ examples/src/helpers/helpers.ts | 6 +- package-lock.json | 19 +- platforms/algorand/README.md | 5 + platforms/algorand/package.json | 9 +- .../algorand/protocols/core/package.json | 11 +- platforms/algorand/protocols/core/src/core.ts | 73 +- .../protocols/tokenBridge/package.json | 11 +- .../protocols/tokenBridge/src/_vaa.ts | 622 ++++++++++++++++++ .../protocols/tokenBridge/src/apps.ts | 132 ++++ .../protocols/tokenBridge/src/assets.ts | 197 ++++++ .../protocols/tokenBridge/src/bigVarint.ts | 57 ++ .../protocols/tokenBridge/src/constants.ts | 30 + .../protocols/tokenBridge/src/index.ts | 10 +- .../protocols/tokenBridge/src/tmplSig.ts | 59 ++ .../protocols/tokenBridge/src/tokenBridge.ts | 234 ++++++- .../protocols/tokenBridge/src/transfers.ts | 420 ++++++++++++ .../protocols/tokenBridge/src/types.ts | 66 ++ .../protocols/tokenBridge/src/utilities.ts | 55 ++ .../algorand/protocols/tokenBridge/src/vaa.ts | 397 +++++++++++ platforms/algorand/src/address.ts | 34 +- platforms/algorand/src/constants.ts | 17 + platforms/algorand/src/index.ts | 13 +- platforms/algorand/src/platform.ts | 137 ++-- platforms/algorand/src/testing/signer.ts | 59 +- platforms/algorand/src/types.ts | 11 + platforms/algorand/src/unsignedTransaction.ts | 5 +- 29 files changed, 2746 insertions(+), 134 deletions(-) create mode 100644 examples/src/algoTokenBridge.ts create mode 100644 platforms/algorand/README.md create mode 100644 platforms/algorand/protocols/tokenBridge/src/_vaa.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/apps.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/assets.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/bigVarint.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/constants.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/tmplSig.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/transfers.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/types.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/utilities.ts create mode 100644 platforms/algorand/protocols/tokenBridge/src/vaa.ts create mode 100644 platforms/algorand/src/constants.ts diff --git a/core/base/src/constants/rpc.ts b/core/base/src/constants/rpc.ts index 20b27aa42..55bc40b86 100644 --- a/core/base/src/constants/rpc.ts +++ b/core/base/src/constants/rpc.ts @@ -23,6 +23,7 @@ const rpcConfig = [[ ["Wormchain", "https://wormchain-rpc.quickapi.com"], ["Xpla", "https://dimension-rpc.xpla.dev"], ["Sei", "https://sei-rpc.polkachu.com/"], + ["Algorand", "https://mainnet-api.algonode.cloud"], ]], [ "Testnet", [ ["Ethereum", "https://rpc.ankr.com/eth_goerli"], @@ -44,7 +45,8 @@ const rpcConfig = [[ ["Evmos", "https://evmos-testnet-rpc.polkachu.com"], ["Wormchain", "https://gateway.testnet.xlabs.xyz/"], ["Xpla", "https://cube-rpc.xpla.dev"], - ["Sepolia", "https://ethereum-sepolia.publicnode.com"] + ["Sepolia", "https://ethereum-sepolia.publicnode.com"], + ["Algorand", "https://testnet-api.algonode.cloud"], ]], [ "Devnet", [ ["Ethereum", "http://eth-devnet:8545"], diff --git a/examples/package.json b/examples/package.json index 914519677..e634db7cd 100644 --- a/examples/package.json +++ b/examples/package.json @@ -33,6 +33,7 @@ }, "sideEffects": false, "scripts": { + "algo": "tsx src/algoTokenBridge.ts", "tb": "tsx src/tokenBridge.ts", "cctp": "tsx src/cctp.ts", "demo": "tsx src/index.ts", @@ -50,17 +51,25 @@ }, "dependencies": { "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-solana": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-evm": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-cosmwasm": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-evm-core": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-solana-core": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-cosmwasm-core": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand-core": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-evm-tokenbridge": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-solana-tokenbridge": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-cosmwasm-tokenbridge": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand-tokenbridge": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-evm-cctp": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-solana-cctp": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-cosmwasm-ibc": "^0.3.0-beta.5" } } \ No newline at end of file diff --git a/examples/src/algoTokenBridge.ts b/examples/src/algoTokenBridge.ts new file mode 100644 index 000000000..c025274ec --- /dev/null +++ b/examples/src/algoTokenBridge.ts @@ -0,0 +1,178 @@ +import { + Chain, + Network, + Platform, + TokenId, + TokenTransfer, + TransferState, + Wormhole, + normalizeAmount, +} from "@wormhole-foundation/connect-sdk"; +import { TransferStuff, getStuff } from "./helpers"; + +// Import the platform specific packages +import { AlgorandPlatform } from "@wormhole-foundation/connect-sdk-algorand"; +import { EvmPlatform } from "@wormhole-foundation/connect-sdk-evm"; +import { SolanaPlatform } from "@wormhole-foundation/connect-sdk-solana"; + +// Register the protocols +import "@wormhole-foundation/connect-sdk-algorand-tokenbridge"; +import "@wormhole-foundation/connect-sdk-evm-tokenbridge"; +import "@wormhole-foundation/connect-sdk-solana-tokenbridge"; + +(async function () { + // init Wormhole object, passing config for which network + // to use (e.g. Mainnet/Testnet) and what Platforms to support + 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"); + + // shortcut to allow transferring native gas token + const token: TokenId | "native" = "native"; + + // Normalized given token decimals later but can just pass bigints as base units + // Note: The Token bridge will dedust past 8 decimals + // this means any amount specified past that point will be returned + // to the caller + const amount = "0.1"; + + // With automatic set to true, perform an automatic transfer. This will invoke a relayer + // contract intermediary that knows to pick up the transfers + // With automatic set to false, perform a manual transfer from source to destination + // of the token + // On the destination side, a wrapped version of the token will be minted + // to the address specified in the transfer VAA + const automatic = false; + + // The automatic relayer has the ability to deliver some native gas funds to the destination account + // The amount specified for native gas will be swapped for the native gas token according + // to the swap rate provided by the contract, denominated in native gas tokens + const nativeGas = automatic ? "0.01" : undefined; + + // Get signer from local key but anything that implements + // Signer interface (e.g. wrapper around web wallet) should work + const source = await getStuff(sendChain); + const destination = await getStuff(rcvChain); + + // Used to normalize the amount to account for the tokens decimals + const decimals = + token === "native" + ? BigInt(sendChain.config.nativeTokenDecimals) + : await wh.getDecimals(sendChain.chain, token); + + // 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"; + // recoverTxid = + // "2daoPz9KyVkG8WGztfatMRx3EKbiRSUVGKAoCST9286eGrzXg5xowafBUUKfd3JrHzvd4AwoH57ujWaJ72k6oiCY"; + + // Finally create and perform the transfer given the parameters set above + const xfer = !recoverTxid + ? // Perform the token transfer + await tokenTransfer(wh, { + token, + amount: normalizeAmount(amount, decimals), + source, + destination, + delivery: { + automatic, + nativeGas: nativeGas ? normalizeAmount(nativeGas, decimals) : undefined, + }, + }) + : // Recover the transfer from the originating txid + await TokenTransfer.from(wh, { + chain: source.chain.chain, + txid: recoverTxid, + }); + + // Log out the results + console.log(xfer); + + if (xfer.getTransferState() <= TransferState.DestinationInitiated) { + console.log(await xfer.completeTransfer(destination.signer)); + } +})(); + +async function tokenTransfer( + wh: Wormhole, + route: { + token: TokenId | "native"; + amount: bigint; + source: TransferStuff; + destination: TransferStuff; + delivery?: { + automatic: boolean; + nativeGas?: bigint; + }; + payload?: Uint8Array; + }, + roundTrip?: boolean, +): Promise> { + // Create a TokenTransfer object to track the state of + // the transfer over time + const xfer = await wh.tokenTransfer( + route.token, + route.amount, + route.source.address, + route.destination.address, + route.delivery?.automatic ?? false, + route.payload, + route.delivery?.nativeGas, + ); + + if (xfer.transfer.automatic) { + const quote = await TokenTransfer.quoteTransfer( + route.source.chain, + route.destination.chain, + xfer.transfer, + ); + console.log(quote); + + if (quote.destinationToken.amount < 0) + throw "The amount requested is too low to cover the fee and any native gas requested."; + } + + // 1) Submit the transactions to the source chain, passing a signer to sign any txns + console.log("Starting transfer"); + const srcTxids = await xfer.initiateTransfer(route.source.signer); + console.log(`Started transfer: `, srcTxids); + + // If automatic, we're done + if (route.delivery?.automatic) return xfer; + + // 2) wait for the VAA to be signed and ready (not required for auto transfer) + console.log("Getting Attestation"); + const attestIds = await xfer.fetchAttestation(60_000); + console.log(`Got Attestation: `, attestIds); + + // 3) redeem the VAA on the dest chain + console.log("Completing Transfer"); + const destTxids = await xfer.completeTransfer(route.destination.signer); + console.log(`Completed Transfer: `, destTxids); + + return xfer; + // // No need to send back, dip + // if (!roundTrip) return xfer; + + // // We can look up the destination asset for this transfer given the context of + // // the sending chain and token and destination chain + // const token = await TokenTransfer.lookupDestinationToken( + // route.source.chain, + // route.destination.chain, + // xfer.transfer, + // ); + // console.log(token); + + // // The wrapped token may have a different number of decimals + // // to make things easy, lets just send the amount from the VAA back + // const amount = xfer.vaas![0]!.vaa!.payload.token.amount; + // return await tokenTransfer(wh, { + // ...route, + // token, + // amount, + // source: route.destination, + // destination: route.source, + // }); +} diff --git a/examples/src/helpers/helpers.ts b/examples/src/helpers/helpers.ts index 0de3ed6fe..2a5ef61d5 100644 --- a/examples/src/helpers/helpers.ts +++ b/examples/src/helpers/helpers.ts @@ -14,9 +14,10 @@ import { } from "@wormhole-foundation/connect-sdk"; // Importing from src so we dont have to rebuild to see debug stuff in signer -import { getCosmwasmSigner } from "@wormhole-foundation/connect-sdk-cosmwasm/src/testing"; import { getEvmSigner } from "@wormhole-foundation/connect-sdk-evm/src/testing"; import { getSolanaSigner } from "@wormhole-foundation/connect-sdk-solana/src/testing"; +import { getCosmwasmSigner } from "@wormhole-foundation/connect-sdk-cosmwasm/src/testing"; +import { getAlgorandSigner } from "@wormhole-foundation/connect-sdk-algorand/src/testing"; // read in from `.env` require("dotenv").config(); @@ -59,6 +60,9 @@ export async function getStuff< case "Evm": signer = await getEvmSigner(await chain.getRpc(), getEnv("ETH_PRIVATE_KEY")); break; + case "Algorand": + signer = await getAlgorandSigner(await chain.getRpc(), getEnv("ALGORAND_MNEMONIC")); + break; default: throw new Error("Unrecognized platform: " + platform); } diff --git a/package-lock.json b/package-lock.json index c6ca22ba0..67f733e52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,9 @@ "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-core": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand-tokenbridge": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-cosmwasm": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-cosmwasm-core": "^0.3.0-beta.5", "@wormhole-foundation/connect-sdk-cosmwasm-ibc": "^0.3.0-beta.5", @@ -8982,10 +8985,10 @@ }, "platforms/algorand": { "name": "@wormhole-foundation/connect-sdk-algorand", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.5", "license": "Apache-2.0", "dependencies": { - "@wormhole-foundation/connect-sdk": "^0.3.0-beta.3", + "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", "algosdk": "2.7.0" }, "engines": { @@ -8994,11 +8997,11 @@ }, "platforms/algorand/protocols/core": { "name": "@wormhole-foundation/connect-sdk-algorand-core", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.5", "license": "Apache-2.0", "dependencies": { - "@wormhole-foundation/connect-sdk": "^0.3.0-beta.3", - "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.3" + "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5" }, "engines": { "node": ">=16" @@ -9006,11 +9009,11 @@ }, "platforms/algorand/protocols/tokenBridge": { "name": "@wormhole-foundation/connect-sdk-algorand-tokenbridge", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.5", "license": "Apache-2.0", "dependencies": { - "@wormhole-foundation/connect-sdk": "^0.3.0-beta.3", - "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.3" + "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5" }, "engines": { "node": ">=16" diff --git a/platforms/algorand/README.md b/platforms/algorand/README.md new file mode 100644 index 000000000..36a03e3bf --- /dev/null +++ b/platforms/algorand/README.md @@ -0,0 +1,5 @@ +# Algorand Context + +Supported chains: + +- Algorand diff --git a/platforms/algorand/package.json b/platforms/algorand/package.json index 56c3a06ef..f54e576d0 100644 --- a/platforms/algorand/package.json +++ b/platforms/algorand/package.json @@ -1,6 +1,6 @@ { "name": "@wormhole-foundation/connect-sdk-algorand", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.5", "repository": { "type": "git", "url": "git+https://github.com/wormhole-foundation/connect-sdk.git" @@ -17,10 +17,9 @@ "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "author": "", - "description": "SDK for EVM chains, used in conjunction with @wormhole-foundation/connect-sdk", + "description": "SDK for Algorand, used in conjunction with @wormhole-foundation/connect-sdk", "files": [ - "dist/**/*", - "src/**/*" + "dist/**/*" ], "keywords": [ "wormhole", @@ -43,7 +42,7 @@ "prettier": "prettier --write ./src" }, "dependencies": { - "@wormhole-foundation/connect-sdk": "^0.3.0-beta.3", + "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", "algosdk": "2.7.0" } } \ No newline at end of file diff --git a/platforms/algorand/protocols/core/package.json b/platforms/algorand/protocols/core/package.json index 7ed87e8b1..540cfed35 100644 --- a/platforms/algorand/protocols/core/package.json +++ b/platforms/algorand/protocols/core/package.json @@ -1,6 +1,6 @@ { "name": "@wormhole-foundation/connect-sdk-algorand-core", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.5", "repository": { "type": "git", "url": "git+https://github.com/wormhole-foundation/connect-sdk.git" @@ -17,10 +17,9 @@ "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "author": "", - "description": "SDK for EVM chains, used in conjunction with @wormhole-foundation/connect-sdk", + "description": "SDK for Algorand, used in conjunction with @wormhole-foundation/connect-sdk", "files": [ - "dist/**/*", - "src/**/*" + "dist/**/*" ], "keywords": [ "wormhole", @@ -43,7 +42,7 @@ "prettier": "prettier --write ./src" }, "dependencies": { - "@wormhole-foundation/connect-sdk": "^0.3.0-beta.3", - "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.3" + "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.5" } } \ No newline at end of file diff --git a/platforms/algorand/protocols/core/src/core.ts b/platforms/algorand/protocols/core/src/core.ts index 4e9c1f153..934ea9393 100644 --- a/platforms/algorand/protocols/core/src/core.ts +++ b/platforms/algorand/protocols/core/src/core.ts @@ -5,6 +5,7 @@ import { Contracts, Network, PayloadLiteral, + UniversalAddress, UnsignedTransaction, VAA, WormholeCore, @@ -15,15 +16,19 @@ import { AlgorandChains, AlgorandPlatform, AlgorandPlatformType, + AlgorandUnsignedTransaction, AnyAlgorandAddress, } from "@wormhole-foundation/connect-sdk-algorand"; -import { Algodv2 } from "algosdk"; +import { Algodv2, bytesToBigInt, decodeAddress, getApplicationAddress, modelsv2 } from "algosdk"; export class AlgorandWormholeCore implements WormholeCore { readonly chainId: ChainId; - readonly coreBridge: string; + readonly coreAppId: bigint; + readonly coreAppAddress: string; + readonly tokenBridgeAppId: bigint; + readonly tokenBridgeAppAddress: string; private constructor( readonly network: N, @@ -32,11 +37,22 @@ export class AlgorandWormholeCore readonly contracts: Contracts, ) { this.chainId = toChainId(chain); - const coreBridgeAddress = contracts.coreBridge; - if (!coreBridgeAddress) - throw new Error(`CoreBridge contract Address for chain ${chain} not found`); - this.coreBridge = coreBridgeAddress; + + if (!contracts.coreBridge) { + throw new Error(`Core contract address for chain ${chain} not found`); + } + const core = BigInt(contracts.coreBridge); + this.coreAppId = core; + this.coreAppAddress = getApplicationAddress(core); + + if (!contracts.tokenBridge) { + throw new Error(`TokenBridge contract address for chain ${chain} not found`); + } + const tokenBridge = BigInt(contracts.tokenBridge); + this.tokenBridgeAppId = tokenBridge; + this.tokenBridgeAppAddress = getApplicationAddress(tokenBridge); } + verifyMessage( sender: AccountAddress, vaa: VAA, @@ -58,11 +74,52 @@ export class AlgorandWormholeCore async *publishMessage( sender: AnyAlgorandAddress, message: string | Uint8Array, - ): AsyncGenerator> { + ): AsyncGenerator> { throw new Error("Method not implemented."); } async parseTransaction(txid: string): Promise { - throw new Error("Not implemented"); + console.log("Txid: ", txid); + const result = await this.connection.pendingTransactionInformation(txid).do(); + console.log("Result: ", result); + + // QUESTIONBW: To make this work, I had to use the tokenBridgeAppId. Expected? + const emitterAddr = new UniversalAddress(this.getEmitterAddressAlgorand(this.tokenBridgeAppId)); + console.log("parseTransaction emitterAddr: ", emitterAddr); + + const sequence = this.parseSequenceFromLogAlgorand(result); + console.log("sequence: ", sequence); + return [ + { + chain: this.chain, + emitter: emitterAddr, + sequence, + } as WormholeMessageId, + ]; + } + + private getEmitterAddressAlgorand(appId: bigint): string { + const appAddr: string = getApplicationAddress(appId); + const decAppAddr: Uint8Array = decodeAddress(appAddr).publicKey; + const hexAppAddr: string = Buffer.from(decAppAddr).toString("hex"); + console.log("core.ts Emitter address: ", hexAppAddr); + return hexAppAddr; + } + + private parseSequenceFromLogAlgorand(result: Record): bigint { + let sequence: bigint | undefined; + const ptr = modelsv2.PendingTransactionResponse.from_obj_for_encoding(result); + if (ptr.innerTxns) { + const innerTxns = ptr.innerTxns; + innerTxns.forEach((txn) => { + if (txn?.logs && txn.logs.length > 0 && txn.logs[0]) { + sequence = bytesToBigInt(txn.logs[0].subarray(0, 8)); + } + }); + } + if (!sequence) { + throw new Error("Sequence not found"); + } + return sequence; } } diff --git a/platforms/algorand/protocols/tokenBridge/package.json b/platforms/algorand/protocols/tokenBridge/package.json index d3cb12438..ba162f4f7 100644 --- a/platforms/algorand/protocols/tokenBridge/package.json +++ b/platforms/algorand/protocols/tokenBridge/package.json @@ -1,6 +1,6 @@ { "name": "@wormhole-foundation/connect-sdk-algorand-tokenbridge", - "version": "0.3.0-beta.3", + "version": "0.3.0-beta.5", "repository": { "type": "git", "url": "git+https://github.com/wormhole-foundation/connect-sdk.git" @@ -17,10 +17,9 @@ "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "author": "", - "description": "SDK for EVM chains, used in conjunction with @wormhole-foundation/connect-sdk", + "description": "SDK for Algorand, used in conjunction with @wormhole-foundation/connect-sdk", "files": [ - "dist/**/*", - "src/**/*" + "dist/**/*" ], "keywords": [ "wormhole", @@ -43,7 +42,7 @@ "prettier": "prettier --write ./src" }, "dependencies": { - "@wormhole-foundation/connect-sdk": "^0.3.0-beta.3", - "@wormhole-foundation/connect-sdk-algorand": "^0.3.0-beta.3" + "@wormhole-foundation/connect-sdk": "^0.3.0-beta.5", + "@wormhole-foundation/connect-sdk-algorand": "^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 new file mode 100644 index 000000000..34a5f39c5 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/_vaa.ts @@ -0,0 +1,622 @@ +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 new file mode 100644 index 000000000..e680030bf --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/apps.ts @@ -0,0 +1,132 @@ +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 new file mode 100644 index 000000000..ab63bf82d --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/assets.ts @@ -0,0 +1,197 @@ +import { Chain, ChainId, toChainId } from "@wormhole-foundation/connect-sdk"; +import { + CHAIN_ID_ALGORAND, + TransactionSignerPair, +} from "@wormhole-foundation/connect-sdk-algorand"; +import { + Algodv2, + Transaction, + bigIntToBytes, + bytesToBigInt, + getApplicationAddress, + makeApplicationOptInTxnFromObject, + makePaymentTxnWithSuggestedParamsFromObject, + 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]>(); + +/** + * Returns a boolean if the asset is wrapped + * @param client Algodv2 client + * @param tokenBridgeId Application ID of the token bridge + * @param assetId Algorand asset index + * @returns Promise with True if wrapped, False otherwise + */ +export async function getIsWrappedAssetOnAlgorand( + client: Algodv2, + tokenBridgeId: bigint, + assetId: bigint, +): Promise { + if (assetId === BigInt(0)) { + return false; + } + const tbAddr: string = getApplicationAddress(tokenBridgeId); + const assetInfoResp = await client.getAssetByID(Number(assetId)).do(); + const asset = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); + const creatorAddr = asset.params.creator; + const creatorAcctInfoResp = await client.accountInformation(creatorAddr).exclude("all").do(); + const creator = modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); + const isWrapped: boolean = creator?.authAddr === tbAddr; + return isWrapped; +} + +/** + * 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 Wrapped Wormhole information structure + */ +export async function getOriginalAssetOffAlgorand( + client: Algodv2, + tokenBridgeId: bigint, + assetId: bigint, +): Promise { + let retVal: WormholeWrappedInfo = { + isWrapped: false, + chainId: CHAIN_ID_ALGORAND, + assetAddress: new Uint8Array(), + }; + retVal.isWrapped = await getIsWrappedAssetOnAlgorand(client, tokenBridgeId, assetId); + if (!retVal.isWrapped) { + retVal.assetAddress = bigIntToBytes(assetId, 32); + return retVal; + } + const assetInfoResp = await client.getAssetByID(safeBigIntToNumber(assetId)).do(); + const assetInfo = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); + const lsa = assetInfo.params.creator; + const dls = await decodeLocalState(client, tokenBridgeId, lsa); + const dlsBuffer: Buffer = Buffer.from(dls); + retVal.chainId = dlsBuffer.readInt16BE(92) as ChainId; + retVal.assetAddress = new Uint8Array(dlsBuffer.subarray(60, 60 + 32)); + 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 + * @param senderAddr Sender address + * @param appId Application ID + * @param appIndex Application index + * @param emitterId Emitter address + * @returns Address and array of TransactionSignerPairs + */ +export async function optIn( + client: Algodv2, + senderAddr: string, + appId: bigint, + appIndex: bigint, + emitterId: string, +): 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(); + let txs: TransactionSignerPair[] = []; + if (!doesExist) { + // These are the suggested params from the system + const params = await client.getTransactionParams().do(); + const seedTxn = makePaymentTxnWithSuggestedParamsFromObject({ + from: senderAddr, + to: sigAddr, + amount: SEED_AMT, + suggestedParams: params, + }); + seedTxn.fee = seedTxn.fee * 2; + txs.push({ tx: seedTxn, signer: null }); + const optinTxn = makeApplicationOptInTxnFromObject({ + from: sigAddr, + suggestedParams: params, + appIndex: safeBigIntToNumber(appId), + rekeyTo: appAddr, + }); + optinTxn.fee = 0; + txs.push({ + tx: optinTxn, + signer: { + addr: lsa.address(), + signTxn: (txn: Transaction) => Promise.resolve(signLogicSigTransaction(txn, lsa).blob), + }, + }); + + accountExistsCache.add([appId, lsa.address()]); + } + return { + addr: sigAddr, + txs, + }; +} + +/** + * Checks if the asset has been opted in by the receiver + * @param client Algodv2 client + * @param asset Algorand asset index + * @param receiver Account address + * @returns Promise with True if the asset was opted in, False otherwise + */ +export async function assetOptinCheck( + client: Algodv2, + asset: bigint, + receiver: string, +): Promise { + const acctInfoResp = await client.accountInformation(receiver).do(); + const acctInfo = modelsv2.Account.from_obj_for_encoding(acctInfoResp); + const assets = acctInfo.assets; + let ret = false; + assets && + assets.forEach((a) => { + const assetId = BigInt(a.assetId); + if (assetId === asset) { + ret = true; + return; + } + }); + return ret; +} diff --git a/platforms/algorand/protocols/tokenBridge/src/bigVarint.ts b/platforms/algorand/protocols/tokenBridge/src/bigVarint.ts new file mode 100644 index 000000000..1410d1309 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/bigVarint.ts @@ -0,0 +1,57 @@ +// 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"); + } + + i += BigInt(b & 0x7f) << BigInt(n * 7); + n++; + } while (0x80 <= b); + return i; +} diff --git a/platforms/algorand/protocols/tokenBridge/src/constants.ts b/platforms/algorand/protocols/tokenBridge/src/constants.ts new file mode 100644 index 000000000..9057c26fe --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/constants.ts @@ -0,0 +1,30 @@ +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 12922868c..a6b57e564 100644 --- a/platforms/algorand/protocols/tokenBridge/src/index.ts +++ b/platforms/algorand/protocols/tokenBridge/src/index.ts @@ -1,7 +1,6 @@ import { _platform } from "@wormhole-foundation/connect-sdk-algorand"; import { registerProtocol } from "@wormhole-foundation/connect-sdk"; import { AlgorandTokenBridge } from "./tokenBridge"; -//import { AlgorandAutomaticTokenBridge } from "./automaticTokenBridge"; declare global { namespace WormholeNamespace { @@ -13,4 +12,13 @@ declare global { registerProtocol(_platform, "TokenBridge", AlgorandTokenBridge); +export * from "./apps"; +export * from "./assets"; +export * from "./constants"; +export * from "./tmplSig"; export * from "./tokenBridge"; +export * from "./tokenBridge"; +export * from "./transfers"; +export * from "./types"; +export * from "./utilities"; +export * from "./_vaa"; diff --git a/platforms/algorand/protocols/tokenBridge/src/tmplSig.ts b/platforms/algorand/protocols/tokenBridge/src/tmplSig.ts new file mode 100644 index 000000000..d4b0068e4 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/tmplSig.ts @@ -0,0 +1,59 @@ +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 c9fd58f9f..6cebd985a 100644 --- a/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts @@ -2,43 +2,77 @@ import { AccountAddress, Chain, ChainAddress, + ChainId, ChainsConfig, Contracts, + ErrNotWrapped, NativeAddress, Network, Platform, TokenAddress, TokenBridge, TokenId, - nativeChainIds, + UniversalAddress, + UnsignedTransaction, + serialize, + toChain, + toChainId, + toNative, } from "@wormhole-foundation/connect-sdk"; -import { Algodv2 } from "algosdk"; - import { + AlgorandAddress, AlgorandChains, AlgorandPlatform, AlgorandPlatformType, + AlgorandUnsignedTransaction, + AlgorandZeroAddress, + AnyAlgorandAddress, + TransactionSignerPair, } from "@wormhole-foundation/connect-sdk-algorand"; +import { Algodv2, bigIntToBytes, bytesToBigInt, getApplicationAddress } from "algosdk"; +import { + getIsWrappedAssetOnAlgorand, + getOriginalAssetOffAlgorand, + getWrappedAssetOnAlgorand, +} from "./assets"; +import { + attestFromAlgorand, + getIsTransferCompletedAlgorand, + redeemOnAlgorand, + transferFromAlgorand, +} from "./transfers"; +import { submitVAAHeader } from "./_vaa"; export class AlgorandTokenBridge implements TokenBridge { + readonly chainId: ChainId; + readonly coreAppId: bigint; + readonly coreAppAddress: string; + readonly tokenBridgeAppId: bigint; readonly tokenBridgeAddress: string; - readonly chainId: bigint; private constructor( readonly network: N, readonly chain: C, - readonly provider: Algodv2, + readonly connection: Algodv2, readonly contracts: Contracts, ) { - this.chainId = nativeChainIds.networkChainToNativeChainId.get(network, chain) as bigint; + this.chainId = toChainId(chain); - const tokenBridgeAddress = this.contracts.tokenBridge!; - if (!tokenBridgeAddress) - throw new Error(`Wormhole Token Bridge contract for domain ${chain} not found`); + if (!contracts.coreBridge) { + throw new Error(`Core contract address for chain ${chain} not found`); + } + const core = BigInt(contracts.coreBridge); + this.coreAppId = core; + this.coreAppAddress = getApplicationAddress(core); - this.tokenBridgeAddress = tokenBridgeAddress; + if (!contracts.tokenBridge) { + throw new Error(`TokenBridge contract address for chain ${chain} not found`); + } + const tokenBridge = BigInt(contracts.tokenBridge); + this.tokenBridgeAppId = tokenBridge; + this.tokenBridgeAddress = getApplicationAddress(tokenBridge); } static async fromRpc( @@ -54,14 +88,54 @@ export class AlgorandTokenBridge return new AlgorandTokenBridge(network as N, chain, provider, conf.contracts); } + // Checks a native address to see if it's a wrapped version async isWrappedAsset(token: TokenAddress): Promise { - throw new Error("Not implemented"); + const assetId = bytesToBigInt(new AlgorandAddress(token.toString()).toUint8Array()); + + const isWrapped = await getIsWrappedAssetOnAlgorand( + this.connection, + this.tokenBridgeAppId, + assetId, + ); + return isWrapped; } + // Returns the original asset with its foreign chain async getOriginalAsset(token: TokenAddress): Promise { - throw new Error("Not implemented"); + if (!(await this.isWrappedAsset(token))) throw ErrNotWrapped(token.toString()); + + const assetId = bytesToBigInt(new AlgorandAddress(token.toString()).toUint8Array()); + + const whWrappedInfo = await getOriginalAssetOffAlgorand( + this.connection, + this.tokenBridgeAppId, + assetId, + ); + const tokenId = { + chain: toChain(whWrappedInfo.chainId), + address: new UniversalAddress(whWrappedInfo.assetAddress), + }; + return tokenId; } + // Returns the address of the native version of this asset + async getWrappedAsset(token: TokenId): Promise> { + const assetId = await getWrappedAssetOnAlgorand( + this.connection, + this.tokenBridgeAppId, + token.chain, + token.address.toString(), + ); + + if (assetId === null) { + throw new Error(`Algorand asset ${token.address} not found`); + } + + const nativeAddress = toNative(this.chain, bigIntToBytes(assetId, 8)); + return nativeAddress; + } + + // Checks if a wrapped version exists async hasWrappedAsset(token: TokenId): Promise { try { await this.getWrappedAsset(token); @@ -70,52 +144,142 @@ export class AlgorandTokenBridge return false; } - async getWrappedAsset(token: TokenId): Promise> { - throw new Error("Not implemented"); + async getWrappedNative(): Promise> { + return toNative(this.chain, new AlgorandAddress(AlgorandZeroAddress).toString()); } async isTransferCompleted( vaa: TokenBridge.VAA<"Transfer" | "TransferWithPayload">, ): Promise { - throw new Error("Not implemented"); + const completed = getIsTransferCompletedAlgorand( + this.connection, + this.tokenBridgeAppId, + serialize(vaa), + ); + return completed; } - async *createAttestation(token: TokenAddress) { - throw new Error("Not implemented"); + // Creates a Token Attestation VAA containing metadata about + // the token that may be submitted to a Token Bridge on another chain + // to allow it to create a wrapped version of the token + async *createAttestation( + token_to_attest: AnyAlgorandAddress, + payer?: AnyAlgorandAddress, + ): AsyncGenerator> { + if (!payer) throw new Error("Payer required to create attestation"); + + const senderAddr = payer.toString(); + const assetId = bytesToBigInt(new AlgorandAddress(token_to_attest.toString()).toUint8Array()); + const utxns = await attestFromAlgorand( + this.connection, + this.tokenBridgeAppId, + this.coreAppId, + senderAddr.toString(), + assetId, + ); + + for (const utxn of utxns) { + yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.createAttestation", true); + } } - async *submitAttestation(vaa: TokenBridge.VAA<"AttestMeta">) { - throw new Error("Not implemented"); + // 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, + ): AsyncGenerator> { + if (!payer) throw new Error("Payer required to create attestation"); + + const senderAddr = payer.toString(); + const { txs } = await submitVAAHeader( + this.connection, + this.tokenBridgeAppId, + serialize(vaa), + senderAddr, + this.coreAppId, + ); + + for (const utxn of txs) { + yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.submitAttestation", true); + } } async *transfer( sender: AccountAddress, - recipient: ChainAddress, + recipient: ChainAddress, token: TokenAddress, amount: bigint, payload?: Uint8Array, - ) { - throw new Error("Not implemented"); + ): AsyncGenerator> { + const senderAddr = sender.toString(); + const assetId = + token === "native" ? BigInt(0) : bytesToBigInt(new AlgorandAddress(token).toUint8Array()); + const qty = amount; + const chain = recipient.chain; + + 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( + this.connection, + this.tokenBridgeAppId, + this.coreAppId, + senderAddr, + assetId, + qty, + receiver, + chain, + fee, + payload, + ); + + for (const utxn of utxns) { + yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.transfer", true); + } } + // Redeems a transfer VAA to receive the tokens on this chain async *redeem( sender: AccountAddress, vaa: TokenBridge.VAA<"Transfer" | "TransferWithPayload">, unwrapNative: boolean = true, - ) { - throw new Error("Not implemented"); - } + ): AsyncGenerator> { + const senderAddr = new AlgorandAddress(sender.toString()).toString(); - async getWrappedNative(): Promise> { - throw new Error("Not implemented"); + const utxns = await redeemOnAlgorand( + this.connection, + this.tokenBridgeAppId, + this.coreAppId, + serialize(vaa), + senderAddr, + ); + + for (const utxn of utxns) { + yield this.createUnsignedTx(utxn, "Algorand.TokenBridge.redeem", true); + } } - // TODO: uncomment and use - // private createUnsignedTx( - // txReq: Transaction, - // description: string, - // parallelizable: boolean = false, - // ): AlgorandUnsignedTransaction { - // throw new Error("Not implemented"); - // } + private createUnsignedTx( + txReq: TransactionSignerPair, + description: string, + parallelizable: boolean = true, // Default true for Algorand atomic transaction grouping + ): AlgorandUnsignedTransaction { + return new AlgorandUnsignedTransaction( + txReq, + this.network, + this.chain, + description, + parallelizable, + ); + } } diff --git a/platforms/algorand/protocols/tokenBridge/src/transfers.ts b/platforms/algorand/protocols/tokenBridge/src/transfers.ts new file mode 100644 index 000000000..50ede98c1 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/transfers.ts @@ -0,0 +1,420 @@ +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 new file mode 100644 index 000000000..2035a4194 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/types.ts @@ -0,0 +1,66 @@ +import { ChainId } from "@wormhole-foundation/connect-sdk"; +import { LogicSigAccount } from "algosdk"; +import { TransactionSignerPair } from "@wormhole-foundation/connect-sdk-algorand"; + +export type OptInResult = { + addr: string; + txs: TransactionSignerPair[]; +}; + +export interface WormholeWrappedInfo { + isWrapped: boolean; + chainId: ChainId; + assetAddress: Uint8Array; +} + +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 new file mode 100644 index 000000000..5c1497da8 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/utilities.ts @@ -0,0 +1,55 @@ +import { bytesToBigInt } from "algosdk"; + +export function safeBigIntToNumber(b: bigint): number { + if (b < BigInt(Number.MIN_SAFE_INTEGER) || b > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error("Integer is unsafe"); + } + 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); +} + +// export function uint8ArrayToNativeStringAlgorand(a: Uint8Array): string { +// return encodeAddress(a); +// } + +// export function hexToNativeStringAlgorand(s: string): string { +// return uint8ArrayToNativeStringAlgorand(hexToUint8Array(s)); +// } + +// export function nativeStringToHexAlgorand(s: string): string { +// return uint8ArrayToHex(decodeAddress(s).publicKey); +// } + +// 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)); +} + +// export function hexToNativeAssetStringAlgorand(s: string): string { +// return uint8ArrayToNativeStringAlgorand(hexToUint8Array(s)); +// } + +// hex.encode +export const uint8ArrayToHex = (a: Uint8Array): string => { + return Buffer.from(a).toString("hex"); +}; + +// hex.decode +export const hexToUint8Array = (h: string): Uint8Array => { + if (h.startsWith("0x")) h = h.slice(2); + return new Uint8Array(Buffer.from(h, "hex")); +}; + +// TODO +export function textToHexString(name: string): string { + return Buffer.from(name, "binary").toString("hex"); +} + +// TODO: bytes.encode +export function textToUint8Array(name: string): Uint8Array { + return new Uint8Array(Buffer.from(name, "binary")); +} diff --git a/platforms/algorand/protocols/tokenBridge/src/vaa.ts b/platforms/algorand/protocols/tokenBridge/src/vaa.ts new file mode 100644 index 000000000..93fbf10e4 --- /dev/null +++ b/platforms/algorand/protocols/tokenBridge/src/vaa.ts @@ -0,0 +1,397 @@ +import { + TokenBridge, + encoding, + keccak256, + serialize, + toChainId, +} 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 { + hexToNativeAssetBigIntAlgorand, + safeBigIntToNumber, + textToHexString, + textToUint8Array, + uint8ArrayToHex, +} from "./utilities"; + +class SubmitVAAState { + vaaMap: TokenBridge.VAA; + 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 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: 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( + client, + senderAddr, + appid, + seq / BigInt(MAX_BITS), + chainId + 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(vaa.hash); + + // 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 SIG_LEN: number = 66; + const GuardianKeyLen: number = 20; + const verifySigArg: Uint8Array = textToUint8Array("verifySigs"); + const lsa = new LogicSigAccount(ALGO_VERIFY); + + for (let nt = 0; nt < numTxns; nt++) { + let sigs = vaa.signatures.slice(nt, nt + MAX_SIGS_PER_TXN); + + // 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() + let arraySize: number = sigs.length * GuardianKeyLen; + let keySet: Uint8Array = new Uint8Array(arraySize); + + for (let i = 0; i < sigs.length; 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 sig = sigs[i * SIG_LEN]; + const key = keys.slice( + sig.guardianIndex * GuardianKeyLen + 1, + (sig.guardianIndex + 1) * GuardianKeyLen + 1, + ); + keySet.set(key, i * 20); + } + + const appTxn = makeApplicationCallTxnFromObject({ + appArgs: [ + verifySigArg, + encoding.bytes.concat( + ...sigs.map((s) => + encoding.bytes.concat(new Uint8Array([s.guardianIndex]), s.signature.encode()), + ), + ), + keySet, + 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"), serialize(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(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; +} diff --git a/platforms/algorand/src/address.ts b/platforms/algorand/src/address.ts index 2f36c4436..0b1a75808 100644 --- a/platforms/algorand/src/address.ts +++ b/platforms/algorand/src/address.ts @@ -1,17 +1,37 @@ -import { Address, UniversalAddress, registerNative } from "@wormhole-foundation/connect-sdk"; +import { + Address, + Platform, + UniversalAddress, + encoding, + registerNative, +} from "@wormhole-foundation/connect-sdk"; import { AlgorandPlatform } from "./platform"; import { _platform, AnyAlgorandAddress } from "./types"; +import { decodeAddress, encodeAddress, isValidAddress } from "algosdk"; + +export const AlgorandZeroAddress = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"; export class AlgorandAddress implements Address { static readonly byteSize = 32; - + static readonly platform: Platform = _platform; // stored as checksum address private readonly address: string; constructor(address: AnyAlgorandAddress) { - // - this.address = ""; + if (AlgorandAddress.instanceof(address)) { + const a = address as unknown as AlgorandAddress; + this.address = a.address; + } else if (UniversalAddress.instanceof(address)) { + this.address = encodeAddress(address.unwrap()); + } else if (typeof address === "string") { + 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)); + } else throw new Error(`Invalid Algorand address or ASA ID: ${address}`); } unwrap(): string { @@ -24,13 +44,13 @@ export class AlgorandAddress implements Address { return this; } toUint8Array() { - return new Uint8Array(); + return decodeAddress(this.address).publicKey; } toUniversalAddress() { - return new UniversalAddress(this.address); + return new UniversalAddress(this.toUint8Array()); } static isValidAddress(address: string) { - //return ethers.isAddress(address); + return isValidAddress(address); } static instanceof(address: any): address is AlgorandAddress { return address.platform === AlgorandPlatform._platform; diff --git a/platforms/algorand/src/constants.ts b/platforms/algorand/src/constants.ts new file mode 100644 index 000000000..a4c2fd58a --- /dev/null +++ b/platforms/algorand/src/constants.ts @@ -0,0 +1,17 @@ +import { Chain, Network, RoArray, constMap } from "@wormhole-foundation/connect-sdk"; + +export const CHAIN_NAME_ALGORAND = "Algorand"; +export const CHAIN_ID_ALGORAND = 8; + +const networkChainAlgorandGenesisHashes = [ + ["Mainnet", [["Algorand", "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8="]]], + ["Testnet", [["Algorand", "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="]]], // Note: this is referred to as `devnet` on Solana + ["Devnet", [["Algorand", ""]]], // Note: this is only for local testing with Tilt QUESTIONBW: Is there a deterministic genesis hash for the localnet Tilt creates? +] as const satisfies RoArray]>; + +export const algorandGenesisHashToNetworkChainPair = constMap(networkChainAlgorandGenesisHashes, [ + 2, + [0, 1], +]); + +export const algorandNetworkChainToGenesisHash = constMap(networkChainAlgorandGenesisHashes); diff --git a/platforms/algorand/src/index.ts b/platforms/algorand/src/index.ts index f3da4f124..60502d0c8 100644 --- a/platforms/algorand/src/index.ts +++ b/platforms/algorand/src/index.ts @@ -1,7 +1,8 @@ -export * from './address'; -export * from './unsignedTransaction'; -export * from './platform'; -export * from './types'; -export * from './chain'; +export * from "./address"; +export * from "./chain"; +export * from "./constants"; +export * from "./platform"; +export * from "./types"; +export * from "./unsignedTransaction"; -export * as testing from './testing'; +export * as testing from "./testing"; diff --git a/platforms/algorand/src/platform.ts b/platforms/algorand/src/platform.ts index f78b97acf..4899a2cc6 100644 --- a/platforms/algorand/src/platform.ts +++ b/platforms/algorand/src/platform.ts @@ -4,29 +4,32 @@ import { ChainsConfig, Network, PlatformContext, - ProtocolInitializer, - ProtocolName, SignedTx, TokenId, TxHash, - WormholeCore, - WormholeMessageId, + Wormhole, chainToPlatform, - getProtocolInitializer, + decimals, nativeChainIds, networkPlatformConfigs, } from "@wormhole-foundation/connect-sdk"; - -import { Algodv2 } from "algosdk"; +import { + Algodv2, + SignedTransaction, + bytesToBigInt, + decodeSignedTransaction, + waitForConfirmation, + modelsv2, +} from "algosdk"; import { AlgorandChain } from "./chain"; import { AlgorandChains, AlgorandPlatformType, AnyAlgorandAddress, _platform } from "./types"; +import { AlgorandAddress, AlgorandZeroAddress } from "./address"; /** * @category Algorand */ - export class AlgorandPlatform extends PlatformContext { - static _platform: AlgorandPlatformType = _platform; + static _platform = _platform; constructor(network: N, _config?: ChainsConfig) { super(network, _config ?? networkPlatformConfigs(network, AlgorandPlatform._platform)); @@ -42,25 +45,13 @@ export class AlgorandPlatform extends PlatformContext( - chain: C, - rpc: Algodv2, - txid: TxHash, - ): Promise { - const wc: WormholeCore = await this.getProtocol( - "WormholeCore", - rpc, - ); - return wc.parseTransaction(txid); - } - static nativeTokenId( network: N, chain: C, ): TokenId { if (!AlgorandPlatform.isSupportedChain(chain)) throw new Error(`invalid chain for ${_platform}: ${chain}`); - throw new Error("Not implemented"); + return Wormhole.chainAddress(chain, AlgorandZeroAddress); } static isNativeTokenId( @@ -70,7 +61,8 @@ export class AlgorandPlatform extends PlatformContext extends PlatformContext { - throw new Error("Not implemented"); + if (token === "native") return BigInt(decimals.nativeDecimals(AlgorandPlatform._platform)); + const asaId = this.anyAlgorandAddressToAsaId(token); + const assetResp = await rpc.getAssetByID(asaId).do(); + const asset = modelsv2.Asset.from_obj_for_encoding(assetResp); + if (!asset.params || !asset.params.decimals) throw new Error("Could not fetch token details"); + return BigInt(asset.params.decimals); } static async getBalance( @@ -92,7 +96,15 @@ export class AlgorandPlatform extends PlatformContext { - throw new Error("Not implemented"); + if (token === "native") { + const resp = await rpc.accountInformation(walletAddr).do(); + const accountInfo = modelsv2.Account.from_obj_for_encoding(resp); + return BigInt(accountInfo.amount); + } + const asaId = this.anyAlgorandAddressToAsaId(token); + const acctAssetInfoResp = await rpc.accountAssetInformation(walletAddr, asaId).do(); + const accountAssetInfo = modelsv2.AssetHolding.from_obj_for_encoding(acctAssetInfoResp); + return BigInt(accountAssetInfo.amount); } static async getBalances( @@ -101,40 +113,87 @@ export class AlgorandPlatform extends PlatformContext { - throw new Error("Not implemented"); + let native: bigint; + if (tokens.includes("native")) { + const acctInfoResp = await rpc.accountInformation(walletAddr).do(); + const accountInfo = modelsv2.Account.from_obj_for_encoding(acctInfoResp); + native = BigInt(accountInfo.amount); + } + const balancesArr = tokens.map(async (token) => { + if (token === "native") { + return { ["native"]: native }; + } + const asaId = this.anyAlgorandAddressToAsaId(token); + const acctAssetInfoResp = await rpc.accountAssetInformation(walletAddr, asaId).do(); + const accountAssetInfo = modelsv2.AssetHolding.from_obj_for_encoding(acctAssetInfoResp); + return BigInt(accountAssetInfo.amount); + }); + + return balancesArr.reduce((obj, item) => Object.assign(obj, item), {}); } static async sendWait(chain: Chain, rpc: Algodv2, stxns: SignedTx[]): Promise { - throw new Error("Not implemented"); + const rounds = 4; + + const decodedStxns: SignedTransaction[] = stxns.map((val, idx) => { + const decodedStxn: SignedTransaction = decodeSignedTransaction(val); + return decodedStxn; + }); + + const txIds: string[] = decodedStxns.map((val, idx) => { + const id: string = val.txn.txID(); + return id; + }); + + const { txId } = await rpc.sendRawTransaction(stxns).do(); + if (!txId) { + throw new Error("Transaction(s) failed to send"); + } + const confirmResp = await waitForConfirmation(rpc, txId, rounds); + const ptr = modelsv2.PendingTransactionResponse.from_obj_for_encoding(confirmResp); + if (!ptr.confirmedRound) { + throw new Error(`Transaction(s) could not be confirmed in ${rounds} rounds`); + } + + console.log("txIds: ", txIds); + return txIds; } static async getLatestBlock(rpc: Algodv2): Promise { - throw new Error("Not implemented"); + const statusResp = await rpc.status().do(); + const status = modelsv2.NodeStatusResponse.from_obj_for_encoding(statusResp); + if (!status.lastRound) { + throw new Error("Error getting status from node"); + } + return Number(status.lastRound); } + static async getLatestFinalizedBlock(rpc: Algodv2): Promise { - throw new Error("Not implemented"); + const statusResp = await rpc.status().do(); + const status = modelsv2.NodeStatusResponse.from_obj_for_encoding(statusResp); + if (!status.lastRound) { + throw new Error("Error getting status from node"); + } + return Number(status.lastRound); } - static chainFromChainId(genesisHash: string): [Network, AlgorandChains] { + static chainFromChainId(genesisId: string): [Network, AlgorandChains] { const networkChainPair = nativeChainIds.platformNativeChainIdToNetworkChain( AlgorandPlatform._platform, // @ts-ignore - genesisHash, + genesisId, ); - if (networkChainPair === undefined) throw new Error(`Unknown native chain id ${genesisHash}`); + if (networkChainPair === undefined) throw new Error(`Unknown native chain id ${genesisId}`); const [network, chain] = networkChainPair; return [network, chain]; } static async chainFromRpc(rpc: Algodv2): Promise<[Network, AlgorandChains]> { - throw new Error("Not implemented"); - } - - static getProtocolInitializer( - protocol: PN, - ): ProtocolInitializer { - return getProtocolInitializer(this._platform, protocol); + 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 d5d88e291..3527c7678 100644 --- a/platforms/algorand/src/testing/signer.ts +++ b/platforms/algorand/src/testing/signer.ts @@ -1,32 +1,75 @@ import { - Chain, Network, SignOnlySigner, SignedTx, Signer, UnsignedTransaction, } from "@wormhole-foundation/connect-sdk"; -import { Algodv2 } from "algosdk"; +import { Account, Algodv2, assignGroupID, mnemonicToSecretKey } from "algosdk"; +import { AlgorandChains } from "../types"; import { AlgorandPlatform } from "../platform"; -export async function getAlgorandSigner(rpc: Algodv2, privateKey: string): Promise { +export async function getAlgorandSigner( + rpc: Algodv2, + mnemonic: string, // 25-word Algorand mnemonic +): Promise { const [network, chain] = await AlgorandPlatform.chainFromRpc(rpc); - return new AlgorandSigner(chain, rpc, privateKey); + return new AlgorandSigner(chain, rpc, mnemonic); } // AlgorandSigner implements SignOnlySender -export class AlgorandSigner implements SignOnlySigner { - constructor(private _chain: C, _rpc: Algodv2, privateKey: string) {} +export class AlgorandSigner + implements SignOnlySigner +{ + _account: Account; + constructor( + private _chain: C, + _rpc: Algodv2, + mnemonic: string, + ) { + this._account = mnemonicToSecretKey(mnemonic); + } chain(): C { return this._chain; } address(): string { - return ""; + return this._account.addr; } async sign(tx: UnsignedTransaction[]): Promise { - throw new Error("Not implemented"); + const signed: Uint8Array[] = []; + const ungrouped = tx.map((val, idx) => { + return val.transaction.tx; + }); + const grouped = assignGroupID(ungrouped); + + // Replace the ungrouped Transactions with grouped Transactions + const groupedAlgoUnsignedTxns = tx.map((val, idx) => { + val.transaction.tx = grouped[idx]; + return val; + }); + + for (const algoUnsignedTxn of groupedAlgoUnsignedTxns) { + const { description, transaction: tsp } = algoUnsignedTxn; + const { tx, signer } = tsp; + + if (signer) { + console.log( + `Signing: ${description} transaction ${tx._getDictForDisplay()} with signer ${ + signer.addr + } for address ${this.address()}`, + ); + signed.push(await signer.signTxn(tx)); + } else { + console.log( + `Signing: ${description} transaction ${tx._getDictForDisplay()} with signer ${this.address()} for address ${this.address()}`, + ); + signed.push(tx.signTxn(this._account.sk)); + } + } + + return signed; } } diff --git a/platforms/algorand/src/types.ts b/platforms/algorand/src/types.ts index ac2db611f..6f42a581f 100644 --- a/platforms/algorand/src/types.ts +++ b/platforms/algorand/src/types.ts @@ -1,4 +1,5 @@ import { PlatformToChains, UniversalOrNative } from "@wormhole-foundation/connect-sdk"; +import { Transaction } from "algosdk"; export const _platform: "Algorand" = "Algorand"; export type AlgorandPlatformType = typeof _platform; @@ -6,3 +7,13 @@ export type AlgorandPlatformType = typeof _platform; export type AlgorandChains = PlatformToChains; export type UniversalOrAlgorand = UniversalOrNative; export type AnyAlgorandAddress = UniversalOrAlgorand | string | Uint8Array; + +export type Signer = { + addr: string; + signTxn(txn: Transaction): Promise; +}; + +export type TransactionSignerPair = { + tx: Transaction; + signer: Signer | null; +}; diff --git a/platforms/algorand/src/unsignedTransaction.ts b/platforms/algorand/src/unsignedTransaction.ts index d393216bc..5322b04d7 100644 --- a/platforms/algorand/src/unsignedTransaction.ts +++ b/platforms/algorand/src/unsignedTransaction.ts @@ -1,12 +1,11 @@ import { Network, UnsignedTransaction } from "@wormhole-foundation/connect-sdk"; -import { Transaction } from "algosdk"; -import { AlgorandChains } from "./types"; +import { AlgorandChains, TransactionSignerPair } from "./types"; export class AlgorandUnsignedTransaction implements UnsignedTransaction { constructor( - readonly transaction: Transaction, + readonly transaction: TransactionSignerPair, readonly network: N, readonly chain: C, readonly description: string,