From 471b78ddd1ea4ff76877b4902e0444fb7603f2ec Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Fri, 29 Dec 2023 17:48:09 -0500 Subject: [PATCH] cleanup again --- examples/package.json | 3 +- examples/src/algoTokenBridge.ts | 174 ------------------ examples/src/tokenBridge.ts | 10 +- platforms/algorand/protocols/core/src/core.ts | 22 ++- .../algorand/protocols/core/src/storage.ts | 48 ++++- .../protocols/tokenBridge/src/tokenBridge.ts | 26 ++- platforms/algorand/src/address.ts | 3 +- platforms/algorand/src/index.ts | 1 - platforms/algorand/src/types.ts | 7 + platforms/algorand/src/utilities.ts | 83 --------- 10 files changed, 101 insertions(+), 276 deletions(-) delete mode 100644 examples/src/algoTokenBridge.ts delete mode 100644 platforms/algorand/src/utilities.ts diff --git a/examples/package.json b/examples/package.json index fa65ba262..372851bbe 100644 --- a/examples/package.json +++ b/examples/package.json @@ -33,8 +33,7 @@ }, "sideEffects": false, "scripts": { - "algo": "cd ../platforms/algorand/protocols/tokenBridge && npm run build && cd - && tsx src/algoTokenBridge.ts", - "wrapped": "cd ../platforms/algorand/protocols/tokenBridge && npm run build && cd - && tsx src/createWrapped.ts", + "wrapped": "tsx src/createWrapped.ts", "tb": "tsx src/tokenBridge.ts", "cctp": "tsx src/cctp.ts", "demo": "tsx src/index.ts", diff --git a/examples/src/algoTokenBridge.ts b/examples/src/algoTokenBridge.ts deleted file mode 100644 index 81862cab7..000000000 --- a/examples/src/algoTokenBridge.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - Chain, - Network, - Platform, - TokenId, - TokenTransfer, - TransferState, - Wormhole, - isTokenId, - 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"; - -/* -# Scenario | Status | TxID -1. Algorand native ALGO to other chain | OK | NK7DK5CLRU2HWNHFNBNFCLM5RLNBVICBUYEW6FBEQVMCVFBBG7JA -2. Return wrapped ALGO from other chain to Algorand native ALGO | FAIL | 4JGo9dwVv8XVTyf9CDXzX5QD4aBc4ZB6sHF7wFqw5LNLstqkRFmumwvu8HATddBVDcybKAAvrACfw1UEw3TD122b -3. Algorand ASA to other chain | OK_Ava | BNRWXLRWR7FVYMBBWHNWWCF65YQBDJAHVA5AMWADEC6K3WH76VYQ -4. Return wrapped token from other chain to original Algorand ASA | FAIL | 0x0dc8e8a052de3c62cda7d8a8211ac49c3c2c43d8841ee462e309c3d1abccbda4 -5. Other chain native asset orand wrapped token | OK | 4wapEufhVAtv8oRqdrRzEovBHKkJDDD3m2pf1Jq3XtpvFwqA2YUijFqFufrGNyxY54vohmy3tsXCb2frcuBNa61T -6. Return Algorand wrapped token to other chain native asset | OK | TD5OWVV6BED5VFAGXBUWVITW6EX3KYOPXEJQLGMFIXM3JJ75HULQ -7. Other chain token to Algorand wrapped token | | -8. Return Algorand wrapped token to other chain token | | -*/ - -(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, EvmPlatform, SolanaPlatform]); - - // Grab chain Contexts -- these hold a reference to a cached rpc client - const sendChain = wh.getChain("Algorand"); - const rcvChain = wh.getChain("Solana"); - - // Shortcut to allow transferring native gas token - //const token: TokenId | "native" = "native"; - - const token = Wormhole.chainAddress("Algorand", "10458941"); // USDC on Algorand - // const token = Wormhole.chainAddress("Avalanche", "0x12EB0d635FD4C5692d779755Ba82b33F6439fc73"); // wUSDC on Avalanche - // const token = Wormhole.chainAddress("Algorand", "86897238"); // wSOL on Algorand - // const token = Wormhole.chainAddress("Solana", "9rU2jFrzA5zDDmt9yR7vEABvXCUNJ1YgGigdTb9oCaTv"); // wALGO on Solana - - // 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.00001"; - - // 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 - // Used to normalize the amount to account for the tokens decimals - const decimals = isTokenId(token) - ? await wh.getDecimals(token.chain, token.address) - : BigInt(sendChain.config.nativeTokenDecimals); - - // 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 = undefined; - // recoverTxid = - // "4JGo9dwVv8XVTyf9CDXzX5QD4aBc4ZB6sHF7wFqw5LNLstqkRFmumwvu8HATddBVDcybKAAvrACfw1UEw3TD122b"; // Recover scenario 2 - // recoverTxid = - // "0x0dc8e8a052de3c62cda7d8a8211ac49c3c2c43d8841ee462e309c3d1abccbda4"; // Recover scenario 4 - - // 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, - }); - - console.log("xfer: ", xfer); - // Log out the results - 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; - }, -): 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; -} diff --git a/examples/src/tokenBridge.ts b/examples/src/tokenBridge.ts index ab8331645..49289067c 100644 --- a/examples/src/tokenBridge.ts +++ b/examples/src/tokenBridge.ts @@ -13,23 +13,27 @@ import { TransferStuff, getStuff, waitLog } from "./helpers"; // Import the platform specific packages import { EvmPlatform } from "@wormhole-foundation/connect-sdk-evm"; import { SolanaPlatform } from "@wormhole-foundation/connect-sdk-solana"; +import { AlgorandPlatform } from "@wormhole-foundation/connect-sdk-algorand"; // Register the protocols import "@wormhole-foundation/connect-sdk-evm-tokenbridge"; import "@wormhole-foundation/connect-sdk-solana-tokenbridge"; +import "@wormhole-foundation/connect-sdk-algorand-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", [EvmPlatform, SolanaPlatform]); + const wh = new Wormhole("Testnet", [EvmPlatform, SolanaPlatform, AlgorandPlatform]); // Grab chain Contexts -- these hold a reference to a cached rpc client - const sendChain = wh.getChain("Solana"); - const rcvChain = wh.getChain("Avalanche"); + const sendChain = wh.getChain("Algorand"); + const rcvChain = wh.getChain("Solana"); // shortcut to allow transferring native gas token const token: TokenId | "native" = "native"; + // const token = Wormhole.chainAddress("Algorand", "10458941"); // USDC on Algorand + // A TokenId is just a `{chain, address}` pair and an alias for ChainAddress // The `address` field must be a parsed address. // You can get a TokenId (or ChainAddress) prepared for you diff --git a/platforms/algorand/protocols/core/src/core.ts b/platforms/algorand/protocols/core/src/core.ts index 9e1a250ab..d77028cdd 100644 --- a/platforms/algorand/protocols/core/src/core.ts +++ b/platforms/algorand/protocols/core/src/core.ts @@ -21,9 +21,6 @@ import { TransactionSet, TransactionSignerPair, safeBigIntToNumber, - ALGO_VERIFY, - ALGO_VERIFY_HASH, - MAX_SIGS_PER_TXN, } from "@wormhole-foundation/connect-sdk-algorand"; import { Algodv2, @@ -50,6 +47,17 @@ export class AlgorandWormholeCore readonly tokenBridgeAppId: bigint; readonly tokenBridgeAppAddress: string; + static MAX_SIGS_PER_TXN: number = 6; + static ALGO_VERIFY_HASH = "EZATROXX2HISIRZDRGXW4LRQ46Z6IUJYYIHU3PJGP7P5IQDPKVX42N767A"; + static 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, + ]); + // global state key for message fee static feeKey = encoding.b64.encode("MessageFee"); // method selector for verifying a VAA @@ -317,14 +325,14 @@ export class AlgorandWormholeCore // 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; - const numTxns: number = Math.floor(numSigs / MAX_SIGS_PER_TXN) + 1; + const numTxns: number = Math.floor(numSigs / AlgorandWormholeCore.MAX_SIGS_PER_TXN) + 1; const SIG_LEN: number = 66; const GuardianKeyLen: number = 20; - const lsa = new LogicSigAccount(ALGO_VERIFY); + const lsa = new LogicSigAccount(AlgorandWormholeCore.ALGO_VERIFY); for (let nt = 0; nt < numTxns; nt++) { - let sigs = vaa.signatures.slice(nt, nt + MAX_SIGS_PER_TXN); + let sigs = vaa.signatures.slice(nt, nt + AlgorandWormholeCore.MAX_SIGS_PER_TXN); // The keyset is the set of Guardians that correspond // to the current set of signatures in this loop. @@ -356,7 +364,7 @@ export class AlgorandWormholeCore ], accounts: accts, appIndex: safeBigIntToNumber(coreId), - from: ALGO_VERIFY_HASH, + from: AlgorandWormholeCore.ALGO_VERIFY_HASH, onComplete: OnApplicationComplete.NoOpOC, suggestedParams, }); diff --git a/platforms/algorand/protocols/core/src/storage.ts b/platforms/algorand/protocols/core/src/storage.ts index 06a70b9a9..29dd2f0cd 100644 --- a/platforms/algorand/protocols/core/src/storage.ts +++ b/platforms/algorand/protocols/core/src/storage.ts @@ -6,7 +6,7 @@ import { toChainId, } from "@wormhole-foundation/connect-sdk"; import { Algodv2, LogicSigAccount, decodeAddress, getApplicationAddress, modelsv2 } from "algosdk"; -import { safeBigIntToNumber, varint } from "@wormhole-foundation/connect-sdk-algorand"; +import { safeBigIntToNumber } from "@wormhole-foundation/connect-sdk-algorand"; export const SEED_AMT: number = 1002000; export const MAX_KEYS: number = 15; @@ -23,6 +23,52 @@ export interface PopulateData { idx: bigint; } +// Useful for encoding numbers as varints to patch TEAL binary +export const varint = { + // Forever grateful to https://github.com/joeltg/big-varint/blob/main/src/unsigned.ts + _limit: 0x7f, + encodingLength: (value: number) => { + let i = 0; + for (; value >= 0x80; i++) value >>= 7; + return i + 1; + }, + encode: (i: bigint | number, buffer?: ArrayBuffer, byteOffset?: number) => { + if (typeof i === "bigint") i = safeBigIntToNumber(i); + + if (i < 0) throw new RangeError("value must be unsigned"); + + const byteLength = varint.encodingLength(i); + buffer = buffer || new ArrayBuffer(byteLength); + byteOffset = byteOffset || 0; + + if (buffer.byteLength < byteOffset + byteLength) + throw new RangeError("the buffer is too small to encode the number at the offset"); + + const array = new Uint8Array(buffer, byteOffset); + + let offset = 0; + while (varint._limit < i) { + array[offset++] = (i & varint._limit) | 0x80; + i >>= 7; + } + array[offset] = Number(i); + return array; + }, + decode: (data: Uint8Array, offset = 0) => { + let i = 0; + let n = 0; + let b: number | undefined; + do { + b = data[offset + n]; + if (b === undefined) throw new RangeError("offset out of range"); + + i += (b & varint._limit) << (n * 7); + n++; + } while (0x80 <= b); + return i; + }, +}; + export const StorageLogicSig = { // Get the storage lsig for a Wormhole message ID forMessageId: (appId: bigint, whm: WormholeMessageId) => { diff --git a/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts b/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts index 458a9719f..e9b175f0d 100644 --- a/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts +++ b/platforms/algorand/protocols/tokenBridge/src/tokenBridge.ts @@ -27,7 +27,6 @@ import { AlgorandUnsignedTransaction, AnyAlgorandAddress, TransactionSignerPair, - isOptedIn, safeBigIntToNumber, } from "@wormhole-foundation/connect-sdk-algorand"; import { @@ -464,7 +463,10 @@ export class AlgorandTokenBridge txs.push(...txs); } - if (assetId !== 0 && !(await isOptedIn(this.connection, creator, assetId))) { + if ( + assetId !== 0 && + !(await AlgorandTokenBridge.isOptedInToAsset(this.connection, creator, assetId)) + ) { // Looks like we need to optin const payTxn = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, @@ -596,7 +598,9 @@ export class AlgorandTokenBridge if (assetId !== 0) { foreignAssets.push(assetId); - if (!(await isOptedIn(this.connection, receiverAddress, assetId))) { + if ( + !(await AlgorandTokenBridge.isOptedInToAsset(this.connection, receiverAddress, assetId)) + ) { if (senderAddr != receiverAddress) { throw new Error("Cannot ASA optin for somebody else (asset " + assetId.toString() + ")"); } @@ -661,6 +665,22 @@ export class AlgorandTokenBridge } } + /** + * 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 + */ + static async isOptedInToAsset(client: Algodv2, address: string, asset: number): Promise { + try { + const acctInfoResp = await client.accountAssetInformation(address, asset).do(); + const acctInfo = modelsv2.AccountAssetResponse.from_obj_for_encoding(acctInfoResp); + return acctInfo.assetHolding.amount > 0; + } catch {} + return false; + } + private createUnsignedTx( txReq: TransactionSignerPair, description: string, diff --git a/platforms/algorand/src/address.ts b/platforms/algorand/src/address.ts index d1f6a7138..95085f4f5 100644 --- a/platforms/algorand/src/address.ts +++ b/platforms/algorand/src/address.ts @@ -7,9 +7,8 @@ import { } from "@wormhole-foundation/connect-sdk"; import { AlgorandPlatform } from "./platform"; -import { _platform, AnyAlgorandAddress } from "./types"; +import { _platform, AnyAlgorandAddress, safeBigIntToNumber } from "./types"; import { decodeAddress, encodeAddress, isValidAddress } from "algosdk"; -import { safeBigIntToNumber } from "./utilities"; export const AlgorandZeroAddress = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"; diff --git a/platforms/algorand/src/index.ts b/platforms/algorand/src/index.ts index c1dc236e1..b8c76a0f6 100644 --- a/platforms/algorand/src/index.ts +++ b/platforms/algorand/src/index.ts @@ -3,6 +3,5 @@ export * from "./chain"; export * from "./platform"; export * from "./types"; export * from "./unsignedTransaction"; -export * from "./utilities"; export * as testing from "./testing"; diff --git a/platforms/algorand/src/types.ts b/platforms/algorand/src/types.ts index 9128fda51..e78928d8f 100644 --- a/platforms/algorand/src/types.ts +++ b/platforms/algorand/src/types.ts @@ -22,3 +22,10 @@ export type TransactionSet = { accounts: string[]; txs: TransactionSignerPair[]; }; + +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); +} diff --git a/platforms/algorand/src/utilities.ts b/platforms/algorand/src/utilities.ts deleted file mode 100644 index 7e18d4056..000000000 --- a/platforms/algorand/src/utilities.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Algodv2, modelsv2 } from "algosdk"; - -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 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); -} - -/** - * 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 isOptedIn(client: Algodv2, address: string, asset: number): Promise { - try { - const acctInfoResp = await client.accountAssetInformation(address, asset).do(); - const acctInfo = modelsv2.AccountAssetResponse.from_obj_for_encoding(acctInfoResp); - return acctInfo.assetHolding.amount > 0; - } catch {} - return false; -} - -// Useful for encoding numbers as varints to patch TEAL binary -export const varint = { - // Forever grateful to https://github.com/joeltg/big-varint/blob/main/src/unsigned.ts - _limit: 0x7f, - encodingLength: (value: number) => { - let i = 0; - for (; value >= 0x80; i++) value >>= 7; - return i + 1; - }, - encode: (i: bigint | number, buffer?: ArrayBuffer, byteOffset?: number) => { - if (typeof i === "bigint") i = safeBigIntToNumber(i); - - if (i < 0) throw new RangeError("value must be unsigned"); - - const byteLength = varint.encodingLength(i); - buffer = buffer || new ArrayBuffer(byteLength); - byteOffset = byteOffset || 0; - - if (buffer.byteLength < byteOffset + byteLength) - throw new RangeError("the buffer is too small to encode the number at the offset"); - - const array = new Uint8Array(buffer, byteOffset); - - let offset = 0; - while (varint._limit < i) { - array[offset++] = (i & varint._limit) | 0x80; - i >>= 7; - } - array[offset] = Number(i); - return array; - }, - decode: (data: Uint8Array, offset = 0) => { - let i = 0; - let n = 0; - let b: number | undefined; - do { - b = data[offset + n]; - if (b === undefined) throw new RangeError("offset out of range"); - - i += (b & varint._limit) << (n * 7); - n++; - } while (0x80 <= b); - return i; - }, -};