UNPKG

@wormhole-foundation/sdk-algorand-tokenbridge

Version:

SDK for Algorand, used in conjunction with @wormhole-foundation/sdk

496 lines 24.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AlgorandTokenBridge = exports.TransferMethodSelector = void 0; const sdk_connect_1 = require("@wormhole-foundation/sdk-connect"); const sdk_algorand_1 = require("@wormhole-foundation/sdk-algorand"); const sdk_algorand_core_1 = require("@wormhole-foundation/sdk-algorand-core"); const algosdk_1 = require("algosdk"); require("@wormhole-foundation/sdk-algorand-core"); exports.TransferMethodSelector = algosdk_1.ABIMethod.fromSignature("portal_transfer(byte[])byte[]"); class AlgorandTokenBridge { network; chain; connection; contracts; chainId; coreBridge; coreAppId; coreAppAddress; tokenBridgeAppId; tokenBridgeAddress; static sendTransfer = sdk_connect_1.encoding.bytes.encode("sendTransfer"); static attestToken = sdk_connect_1.encoding.bytes.encode("attestToken"); static noop = sdk_connect_1.encoding.bytes.encode("nop"); static optIn = sdk_connect_1.encoding.bytes.encode("optin"); static completeTransfer = sdk_connect_1.encoding.bytes.encode("completeTransfer"); static receiveAttest = sdk_connect_1.encoding.bytes.encode("receiveAttest"); constructor(network, chain, connection, contracts) { this.network = network; this.chain = chain; this.connection = connection; this.contracts = contracts; this.chainId = (0, sdk_connect_1.toChainId)(chain); if (!contracts.coreBridge) { throw new Error(`Core contract address for chain ${chain} not found`); } const core = BigInt(contracts.coreBridge); this.coreAppId = core; this.coreAppAddress = (0, algosdk_1.getApplicationAddress)(core); this.coreBridge = new sdk_algorand_core_1.AlgorandWormholeCore(network, chain, connection, contracts); if (!contracts.tokenBridge) { throw new Error(`TokenBridge contract address for chain ${chain} not found`); } const tokenBridge = BigInt(contracts.tokenBridge); this.tokenBridgeAppId = tokenBridge; this.tokenBridgeAddress = (0, algosdk_1.getApplicationAddress)(tokenBridge); } static async fromRpc(provider, config) { const [network, chain] = await sdk_algorand_1.AlgorandPlatform.chainFromRpc(provider); const conf = config[chain]; if (conf.network !== network) throw new Error(`Network mismatch: ${conf.network} != ${network}`); return new AlgorandTokenBridge(network, chain, provider, conf.contracts); } // Checks a native address to see if it's a wrapped version async isWrappedAsset(token) { const assetId = new sdk_algorand_1.AlgorandAddress(token).toInt(); if (assetId === 0) return false; const assetInfoResp = await this.connection.getAssetByID(assetId).do(); const asset = algosdk_1.modelsv2.Asset.from_obj_for_encoding(assetInfoResp); const creatorAddr = asset.params.creator; const creatorAcctInfoResp = await this.connection .accountInformation(creatorAddr) .exclude("all") .do(); const creator = algosdk_1.modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); const isWrapped = creator?.authAddr === this.tokenBridgeAddress; return isWrapped; } // Returns the original asset with its foreign chain async getOriginalAsset(token) { const assetId = new sdk_algorand_1.AlgorandAddress(token).toInt(); const assetInfoResp = await this.connection.getAssetByID(assetId).do(); const assetInfo = algosdk_1.modelsv2.Asset.from_obj_for_encoding(assetInfoResp); const decodedLocalState = await sdk_algorand_core_1.StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, assetInfo.params.creator); if (decodedLocalState.length < 94) throw new Error("Invalid local state data"); const chainBytes = decodedLocalState.slice(92, 94); const chain = (0, sdk_connect_1.toChain)(sdk_connect_1.encoding.bignum.decode(chainBytes)); const address = new sdk_connect_1.UniversalAddress(decodedLocalState.slice(60, 60 + 32)); return { chain, address }; } async getTokenUniversalAddress(token) { return new sdk_algorand_1.AlgorandAddress(token).toUniversalAddress(); } async getTokenNativeAddress(originChain, token) { return new sdk_algorand_1.AlgorandAddress(token).toNative(); } // Returns the address of the native version of this asset async getWrappedAsset(token) { if ((0, sdk_connect_1.isNative)(token.address)) throw new Error("native asset cannot be a wrapped asset"); const storageAccount = sdk_algorand_core_1.StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, token); const data = await sdk_algorand_core_1.StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, storageAccount.address()); if (data.length < 8) throw new Error("Invalid wrapped asset data"); const nativeAddress = (0, sdk_connect_1.toNative)(this.chain, sdk_connect_1.encoding.bignum.decode(data.slice(0, 8)).toString()); return nativeAddress; } // Checks if a wrapped version exists async hasWrappedAsset(token) { try { await this.getWrappedAsset(token); return true; } catch { } return false; } async getWrappedNative() { return (0, sdk_connect_1.toNative)(this.chain, "0"); } async isTransferCompleted(vaa) { const messageStorage = sdk_algorand_core_1.StorageLogicSig.forMessageId(this.tokenBridgeAppId, { sequence: vaa.sequence, chain: vaa.emitterChain, emitter: vaa.emitterAddress, }); try { return await sdk_algorand_core_1.StorageLogicSig.checkBitsSet(this.connection, this.tokenBridgeAppId, messageStorage.address(), vaa.sequence); } catch { } return false; } // 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, payer) { if (!payer) throw new Error("Payer required to create attestation"); const senderAddr = new sdk_algorand_1.AlgorandAddress(payer).toString(); const assetId = new sdk_algorand_1.AlgorandAddress(token).toInt(); const txs = []; const suggestedParams = await this.connection.getTransactionParams().do(); const tbs = sdk_algorand_core_1.StorageLogicSig.forEmitter(this.coreAppId, new sdk_algorand_1.AlgorandAddress(this.tokenBridgeAddress).toUint8Array()); const { accounts: [emitterAddr], txs: emitterOptInTxs, } = await sdk_algorand_core_1.AlgorandWormholeCore.maybeCreateStorageTx(this.connection, senderAddr, this.coreAppId, tbs, suggestedParams); txs.push(...emitterOptInTxs); let creatorAddr = ""; let creatorAcctInfo; if (assetId !== 0) { const assetInfoResp = await this.connection.getAssetByID(assetId).do(); const assetInfo = algosdk_1.modelsv2.Asset.from_obj_for_encoding(assetInfoResp); const creatorAcctInfoResp = await this.connection .accountInformation(assetInfo.params.creator) .do(); creatorAcctInfo = algosdk_1.modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); if (creatorAcctInfo.authAddr === this.tokenBridgeAddress.toString()) { throw new Error("Cannot re-attest wormhole assets"); } } const nativeStorageAcct = sdk_algorand_core_1.StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, BigInt(assetId)); const txns = await sdk_algorand_core_1.AlgorandWormholeCore.maybeCreateStorageTx(this.connection, senderAddr, this.tokenBridgeAppId, nativeStorageAcct); creatorAddr = txns.accounts[0]; txs.push(...txns.txs); const firstTxn = (0, algosdk_1.makeApplicationCallTxnFromObject)({ from: senderAddr, appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), onComplete: algosdk_1.OnApplicationComplete.NoOpOC, appArgs: [AlgorandTokenBridge.noop], suggestedParams, }); txs.push({ tx: firstTxn }); const mfee = await this.coreBridge.getMessageFee(); if (mfee > BigInt(0)) { const feeTxn = (0, algosdk_1.makePaymentTxnWithSuggestedParamsFromObject)({ from: senderAddr, suggestedParams, to: this.tokenBridgeAddress, amount: mfee, }); txs.push({ tx: feeTxn }); } let accts = [emitterAddr, creatorAddr, this.coreAppAddress]; if (creatorAcctInfo) { accts.push(creatorAcctInfo.address); } let appTxn = (0, algosdk_1.makeApplicationCallTxnFromObject)({ appArgs: [AlgorandTokenBridge.attestToken, sdk_connect_1.encoding.bignum.toBytes(assetId, 8)], accounts: accts, appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), foreignApps: [(0, sdk_algorand_1.safeBigIntToNumber)(this.coreAppId)], foreignAssets: [assetId], from: senderAddr, onComplete: algosdk_1.OnApplicationComplete.NoOpOC, suggestedParams, }); if (mfee > BigInt(0)) { appTxn.fee *= 3; } else { appTxn.fee *= 2; } txs.push({ tx: appTxn }); for (const utxn of txs) { yield this.createUnsignedTx(utxn, "TokenBridge.createAttestation", true); } } // Submits the Token Attestation VAA to the Token Bridge // to create the wrapped token represented by the data in the VAA async *submitAttestation(vaa, sender, suggestedParams) { if (!sender) throw new Error("Sender required to submit attestation"); if (!suggestedParams) suggestedParams = await this.connection.getTransactionParams().do(); const senderAddr = sender.toString(); const tokenStorage = sdk_algorand_core_1.StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token); const tokenStorageAddress = tokenStorage.address(); const txs = []; const foreignAssets = []; const data = await sdk_algorand_core_1.StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, tokenStorageAddress); if (data.length > 8) { foreignAssets.push(new sdk_algorand_1.AlgorandAddress(data.slice(0, 8)).toInt()); } txs.push({ tx: (0, algosdk_1.makePaymentTxnWithSuggestedParamsFromObject)({ from: senderAddr, to: tokenStorageAddress, amount: 100000, suggestedParams, }), }); let buf = new Uint8Array(1); buf[0] = 0x01; txs.push({ tx: (0, algosdk_1.makeApplicationCallTxnFromObject)({ appArgs: [AlgorandTokenBridge.noop, buf], appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), from: senderAddr, onComplete: algosdk_1.OnApplicationComplete.NoOpOC, suggestedParams, }), }); buf = new Uint8Array(1); buf[0] = 0x02; txs.push({ tx: (0, algosdk_1.makeApplicationCallTxnFromObject)({ appArgs: [AlgorandTokenBridge.noop, buf], appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), from: senderAddr, onComplete: algosdk_1.OnApplicationComplete.NoOpOC, suggestedParams, }), }); txs.push({ tx: (0, algosdk_1.makeApplicationCallTxnFromObject)({ accounts: [], appArgs: [AlgorandTokenBridge.receiveAttest, (0, sdk_connect_1.serialize)(vaa)], appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), foreignAssets: foreignAssets, from: senderAddr, onComplete: algosdk_1.OnApplicationComplete.NoOpOC, suggestedParams, }), }); txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * 2; for (const utxn of txs) { yield this.createUnsignedTx(utxn, "TokenBridge.submitAttestation", true); } } async *transfer(sender, recipient, token, amount, payload) { const senderAddr = sender.toString(); const assetId = (0, sdk_connect_1.isNative)(token) ? 0 : new sdk_algorand_1.AlgorandAddress(token).toInt(); const qty = amount; const chainId = (0, sdk_connect_1.toChainId)(recipient.chain); const receiver = recipient.address.toUniversalAddress().toUint8Array(); const suggestedParams = await this.connection.getTransactionParams().do(); const fee = BigInt(0); const tbs = sdk_algorand_core_1.StorageLogicSig.fromData({ appId: this.coreAppId, appAddress: (0, algosdk_1.decodeAddress)(this.coreAppAddress).publicKey, idx: BigInt(0), address: (0, algosdk_1.decodeAddress)(this.tokenBridgeAddress).publicKey, }); const txs = []; const { accounts: [emitterAddr], txs: emitterOptInTxs, } = await sdk_algorand_core_1.AlgorandWormholeCore.maybeCreateStorageTx(this.connection, senderAddr, this.coreAppId, tbs, suggestedParams); txs.push(...emitterOptInTxs); // Check that the auth address of the creator is the token bridge let creator = ""; let creatorAcct; let wormhole = false; if (assetId !== 0) { const assetInfoResp = await this.connection.getAssetByID(assetId).do(); const asset = algosdk_1.modelsv2.Asset.from_obj_for_encoding(assetInfoResp); creator = asset.params.creator; const creatorAcctInfoResp = await this.connection.accountInformation(creator).do(); creatorAcct = algosdk_1.modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); wormhole = creatorAcct.authAddr === this.tokenBridgeAddress.toString(); } const msgFee = await this.coreBridge.getMessageFee(); if (msgFee > 0) txs.push({ tx: (0, algosdk_1.makePaymentTxnWithSuggestedParamsFromObject)({ from: senderAddr, to: this.tokenBridgeAddress, amount: msgFee, suggestedParams, }), }); if (!wormhole) { const nativeStorageAccount = sdk_algorand_core_1.StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, BigInt(assetId)); const { accounts: [address], txs, } = await sdk_algorand_core_1.AlgorandWormholeCore.maybeCreateStorageTx(this.connection, senderAddr, this.tokenBridgeAppId, nativeStorageAccount, suggestedParams); creator = address; txs.push(...txs); } if (assetId !== 0 && !(await AlgorandTokenBridge.isOptedInToAsset(this.connection, creator, assetId))) { // Looks like we need to optin const payTxn = (0, algosdk_1.makePaymentTxnWithSuggestedParamsFromObject)({ from: senderAddr, to: creator, amount: 100000, suggestedParams, }); // The tokenid app needs to do the optin since it has signature authority let txn = (0, algosdk_1.makeApplicationCallTxnFromObject)({ from: senderAddr, appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), onComplete: algosdk_1.OnApplicationComplete.NoOpOC, appArgs: [AlgorandTokenBridge.optIn, (0, algosdk_1.bigIntToBytes)(assetId, 8)], foreignAssets: [assetId], accounts: [creator], suggestedParams, }); txn.fee *= 2; txs.unshift(...[{ tx: payTxn }, { tx: txn }]); } const t = (0, algosdk_1.makeApplicationCallTxnFromObject)({ from: senderAddr, appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), onComplete: algosdk_1.OnApplicationComplete.NoOpOC, appArgs: [AlgorandTokenBridge.noop], suggestedParams, }); txs.push({ tx: t }); let accounts = []; if (assetId === 0) { const t = (0, algosdk_1.makePaymentTxnWithSuggestedParamsFromObject)({ from: senderAddr, to: creator, amount: qty, suggestedParams, }); txs.push({ tx: t }); accounts = [emitterAddr, creator, creator]; } else { const t = (0, algosdk_1.makeAssetTransferTxnWithSuggestedParamsFromObject)({ from: senderAddr, to: creator, amount: qty, assetIndex: assetId, suggestedParams, }); txs.push({ tx: t }); accounts = creatorAcct?.address ? [emitterAddr, creator, creatorAcct.address] : [emitterAddr, creator]; } const args = [ AlgorandTokenBridge.sendTransfer, sdk_connect_1.encoding.bignum.toBytes(assetId, 8), sdk_connect_1.encoding.bignum.toBytes(qty, 8), receiver, sdk_connect_1.encoding.bignum.toBytes(chainId, 8), sdk_connect_1.encoding.bignum.toBytes(fee, 8), ]; if (payload) args.push(payload); const acTxn = (0, algosdk_1.makeApplicationCallTxnFromObject)({ from: senderAddr, appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), onComplete: algosdk_1.OnApplicationComplete.NoOpOC, appArgs: args, foreignApps: [(0, sdk_algorand_1.safeBigIntToNumber)(this.coreAppId)], foreignAssets: [assetId], accounts: accounts, suggestedParams, }); acTxn.fee *= 2; txs.push({ tx: acTxn }); for (const utxn of txs) { yield this.createUnsignedTx(utxn, "TokenBridge.transfer", true); } } async *redeem(sender, vaa, unwrapNative = true, suggestedParams) { if (!suggestedParams) suggestedParams = await this.connection.getTransactionParams().do(); const senderAddr = new sdk_algorand_1.AlgorandAddress(sender).toString(); const { accounts, txs } = await sdk_algorand_core_1.AlgorandWormholeCore.submitVAAHeader(this.connection, this.coreAppId, this.tokenBridgeAppId, vaa, senderAddr); // A critical routing step occurs here let tokenStorage = undefined; let tokenStorageAddress = ""; let foreignAssets = []; let assetId = 0; if (vaa.payload.token.chain !== this.chain) { // If the token is from elsewhere we get the storage lsig for a wrapped asset tokenStorage = sdk_algorand_core_1.StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token); tokenStorageAddress = tokenStorage.address(); const data = await sdk_algorand_core_1.StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, tokenStorageAddress); assetId = new sdk_algorand_1.AlgorandAddress(data.slice(0, 8)).toInt(); } else { // Otherwise we get the storage lsig for a native asset, including ALGO (0) const nativeTokenId = new sdk_algorand_1.AlgorandAddress(vaa.payload.token.address).toBigInt(); tokenStorage = sdk_algorand_core_1.StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, nativeTokenId); tokenStorageAddress = tokenStorage.address(); assetId = (0, sdk_algorand_1.safeBigIntToNumber)(nativeTokenId); } accounts.push(tokenStorageAddress); let appId = 0; let receiverAddress = ""; if (vaa.payloadName === "TransferWithPayload") { appId = new sdk_algorand_1.AlgorandAddress(vaa.payload.to.address).toInt(); receiverAddress = (0, algosdk_1.getApplicationAddress)(appId); } else { receiverAddress = new sdk_algorand_1.AlgorandAddress(vaa.payload.to.address.toUint8Array()).toString(); } accounts.push(receiverAddress); if (assetId !== 0) { foreignAssets.push(assetId); if (!(await AlgorandTokenBridge.isOptedInToAsset(this.connection, receiverAddress, assetId))) { if (senderAddr != receiverAddress) { throw new Error("Cannot ASA optin for somebody else (asset " + assetId.toString() + ")"); } // Push asset opt in to the front txs.unshift({ tx: (0, algosdk_1.makeAssetTransferTxnWithSuggestedParamsFromObject)({ amount: 0, assetIndex: assetId, from: senderAddr, suggestedParams, to: senderAddr, }), }); } } const appCallObj = { accounts: accounts, appArgs: [AlgorandTokenBridge.completeTransfer, (0, sdk_connect_1.serialize)(vaa)], appIndex: (0, sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId), foreignAssets: foreignAssets, from: senderAddr, onComplete: algosdk_1.OnApplicationComplete.NoOpOC, suggestedParams, }; txs.push({ tx: (0, algosdk_1.makeApplicationCallTxnFromObject)(appCallObj), }); // We need to cover the inner transactions txs[txs.length - 1].tx.fee = txs[txs.length - 1].tx.fee * (vaa.payloadName === "Transfer" && vaa.payload.fee !== undefined && vaa.payload.fee === 0n ? 2 : 3); if (vaa.payloadName === "TransferWithPayload") { txs[txs.length - 1].tx.appForeignApps = [appId]; txs.push({ tx: (0, algosdk_1.makeApplicationCallTxnFromObject)({ appArgs: [ exports.TransferMethodSelector.getSelector(), exports.TransferMethodSelector.args[0].type.encode((0, sdk_connect_1.serialize)(vaa)), ], appIndex: appId, foreignAssets: foreignAssets, from: senderAddr, onComplete: algosdk_1.OnApplicationComplete.NoOpOC, suggestedParams, }), }); } for (const utxn of txs) { yield this.createUnsignedTx(utxn, "TokenBridge.redeem", true); } } /** * 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, address, asset) { try { const acctInfoResp = await client.accountAssetInformation(address, asset).do(); const acctInfo = algosdk_1.modelsv2.AccountAssetResponse.from_obj_for_encoding(acctInfoResp); return (acctInfo.assetHolding?.amount ?? 0) > 0; } catch { } return false; } createUnsignedTx(txReq, description, parallelizable = true) { return new sdk_algorand_1.AlgorandUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable); } } exports.AlgorandTokenBridge = AlgorandTokenBridge; //# sourceMappingURL=tokenBridge.js.map