UNPKG

@wormhole-foundation/sdk-algorand-tokenbridge

Version:

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

492 lines 22.6 kB
import { UniversalAddress, encoding, isNative, serialize, toChain, toChainId, toNative, } from "@wormhole-foundation/sdk-connect"; import { AlgorandAddress, AlgorandPlatform, AlgorandUnsignedTransaction, safeBigIntToNumber, } from "@wormhole-foundation/sdk-algorand"; import { AlgorandWormholeCore, StorageLogicSig } from "@wormhole-foundation/sdk-algorand-core"; import { ABIMethod, OnApplicationComplete, bigIntToBytes, decodeAddress, getApplicationAddress, makeApplicationCallTxnFromObject, makeAssetTransferTxnWithSuggestedParamsFromObject, makePaymentTxnWithSuggestedParamsFromObject, modelsv2, } from "algosdk"; import "@wormhole-foundation/sdk-algorand-core"; export const TransferMethodSelector = ABIMethod.fromSignature("portal_transfer(byte[])byte[]"); export class AlgorandTokenBridge { network; chain; connection; contracts; chainId; coreBridge; coreAppId; coreAppAddress; tokenBridgeAppId; tokenBridgeAddress; static sendTransfer = encoding.bytes.encode("sendTransfer"); static attestToken = encoding.bytes.encode("attestToken"); static noop = encoding.bytes.encode("nop"); static optIn = encoding.bytes.encode("optin"); static completeTransfer = encoding.bytes.encode("completeTransfer"); static receiveAttest = encoding.bytes.encode("receiveAttest"); constructor(network, chain, connection, contracts) { this.network = network; this.chain = chain; this.connection = connection; this.contracts = contracts; this.chainId = 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 = getApplicationAddress(core); this.coreBridge = new 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 = getApplicationAddress(tokenBridge); } static async fromRpc(provider, config) { const [network, chain] = await 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 AlgorandAddress(token).toInt(); if (assetId === 0) return false; const assetInfoResp = await this.connection.getAssetByID(assetId).do(); const asset = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); const creatorAddr = asset.params.creator; const creatorAcctInfoResp = await this.connection .accountInformation(creatorAddr) .exclude("all") .do(); const creator = 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 AlgorandAddress(token).toInt(); const assetInfoResp = await this.connection.getAssetByID(assetId).do(); const assetInfo = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); const decodedLocalState = await 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 = toChain(encoding.bignum.decode(chainBytes)); const address = new UniversalAddress(decodedLocalState.slice(60, 60 + 32)); return { chain, address }; } async getTokenUniversalAddress(token) { return new AlgorandAddress(token).toUniversalAddress(); } async getTokenNativeAddress(originChain, token) { return new AlgorandAddress(token).toNative(); } // Returns the address of the native version of this asset async getWrappedAsset(token) { if (isNative(token.address)) throw new Error("native asset cannot be a wrapped asset"); const storageAccount = StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, token); const data = await StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, storageAccount.address()); if (data.length < 8) throw new Error("Invalid wrapped asset data"); const nativeAddress = toNative(this.chain, 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 toNative(this.chain, "0"); } async isTransferCompleted(vaa) { const messageStorage = StorageLogicSig.forMessageId(this.tokenBridgeAppId, { sequence: vaa.sequence, chain: vaa.emitterChain, emitter: vaa.emitterAddress, }); try { return await 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 AlgorandAddress(payer).toString(); const assetId = new AlgorandAddress(token).toInt(); const txs = []; const suggestedParams = await this.connection.getTransactionParams().do(); const tbs = StorageLogicSig.forEmitter(this.coreAppId, new AlgorandAddress(this.tokenBridgeAddress).toUint8Array()); const { accounts: [emitterAddr], txs: emitterOptInTxs, } = await 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 = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); const creatorAcctInfoResp = await this.connection .accountInformation(assetInfo.params.creator) .do(); creatorAcctInfo = modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); if (creatorAcctInfo.authAddr === this.tokenBridgeAddress.toString()) { throw new Error("Cannot re-attest wormhole assets"); } } const nativeStorageAcct = StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, BigInt(assetId)); const txns = await AlgorandWormholeCore.maybeCreateStorageTx(this.connection, senderAddr, this.tokenBridgeAppId, nativeStorageAcct); creatorAddr = txns.accounts[0]; txs.push(...txns.txs); const firstTxn = makeApplicationCallTxnFromObject({ from: senderAddr, appIndex: safeBigIntToNumber(this.tokenBridgeAppId), onComplete: OnApplicationComplete.NoOpOC, appArgs: [AlgorandTokenBridge.noop], suggestedParams, }); txs.push({ tx: firstTxn }); const mfee = await this.coreBridge.getMessageFee(); if (mfee > BigInt(0)) { const feeTxn = 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 = makeApplicationCallTxnFromObject({ appArgs: [AlgorandTokenBridge.attestToken, encoding.bignum.toBytes(assetId, 8)], accounts: accts, appIndex: safeBigIntToNumber(this.tokenBridgeAppId), foreignApps: [safeBigIntToNumber(this.coreAppId)], foreignAssets: [assetId], from: senderAddr, onComplete: 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 = StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token); const tokenStorageAddress = tokenStorage.address(); const txs = []; const foreignAssets = []; const data = await StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, tokenStorageAddress); if (data.length > 8) { foreignAssets.push(new AlgorandAddress(data.slice(0, 8)).toInt()); } txs.push({ tx: makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, to: tokenStorageAddress, amount: 100000, suggestedParams, }), }); let buf = new Uint8Array(1); buf[0] = 0x01; txs.push({ tx: makeApplicationCallTxnFromObject({ appArgs: [AlgorandTokenBridge.noop, buf], appIndex: safeBigIntToNumber(this.tokenBridgeAppId), from: senderAddr, onComplete: OnApplicationComplete.NoOpOC, suggestedParams, }), }); buf = new Uint8Array(1); buf[0] = 0x02; txs.push({ tx: makeApplicationCallTxnFromObject({ appArgs: [AlgorandTokenBridge.noop, buf], appIndex: safeBigIntToNumber(this.tokenBridgeAppId), from: senderAddr, onComplete: OnApplicationComplete.NoOpOC, suggestedParams, }), }); txs.push({ tx: makeApplicationCallTxnFromObject({ accounts: [], appArgs: [AlgorandTokenBridge.receiveAttest, serialize(vaa)], appIndex: safeBigIntToNumber(this.tokenBridgeAppId), foreignAssets: foreignAssets, from: senderAddr, onComplete: 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 = isNative(token) ? 0 : new AlgorandAddress(token).toInt(); const qty = amount; const chainId = toChainId(recipient.chain); const receiver = recipient.address.toUniversalAddress().toUint8Array(); const suggestedParams = await this.connection.getTransactionParams().do(); const fee = BigInt(0); const tbs = StorageLogicSig.fromData({ appId: this.coreAppId, appAddress: decodeAddress(this.coreAppAddress).publicKey, idx: BigInt(0), address: decodeAddress(this.tokenBridgeAddress).publicKey, }); const txs = []; const { accounts: [emitterAddr], txs: emitterOptInTxs, } = await 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 = modelsv2.Asset.from_obj_for_encoding(assetInfoResp); creator = asset.params.creator; const creatorAcctInfoResp = await this.connection.accountInformation(creator).do(); creatorAcct = modelsv2.Account.from_obj_for_encoding(creatorAcctInfoResp); wormhole = creatorAcct.authAddr === this.tokenBridgeAddress.toString(); } const msgFee = await this.coreBridge.getMessageFee(); if (msgFee > 0) txs.push({ tx: makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, to: this.tokenBridgeAddress, amount: msgFee, suggestedParams, }), }); if (!wormhole) { const nativeStorageAccount = StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, BigInt(assetId)); const { accounts: [address], txs, } = await 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 = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, to: creator, amount: 100000, suggestedParams, }); // The tokenid app needs to do the optin since it has signature authority let txn = makeApplicationCallTxnFromObject({ from: senderAddr, appIndex: safeBigIntToNumber(this.tokenBridgeAppId), onComplete: OnApplicationComplete.NoOpOC, appArgs: [AlgorandTokenBridge.optIn, bigIntToBytes(assetId, 8)], foreignAssets: [assetId], accounts: [creator], suggestedParams, }); txn.fee *= 2; txs.unshift(...[{ tx: payTxn }, { tx: txn }]); } const t = makeApplicationCallTxnFromObject({ from: senderAddr, appIndex: safeBigIntToNumber(this.tokenBridgeAppId), onComplete: OnApplicationComplete.NoOpOC, appArgs: [AlgorandTokenBridge.noop], suggestedParams, }); txs.push({ tx: t }); let accounts = []; if (assetId === 0) { const t = makePaymentTxnWithSuggestedParamsFromObject({ from: senderAddr, to: creator, amount: qty, suggestedParams, }); txs.push({ tx: t }); accounts = [emitterAddr, creator, creator]; } else { const t = 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, encoding.bignum.toBytes(assetId, 8), encoding.bignum.toBytes(qty, 8), receiver, encoding.bignum.toBytes(chainId, 8), encoding.bignum.toBytes(fee, 8), ]; if (payload) args.push(payload); const acTxn = makeApplicationCallTxnFromObject({ from: senderAddr, appIndex: safeBigIntToNumber(this.tokenBridgeAppId), onComplete: OnApplicationComplete.NoOpOC, appArgs: args, foreignApps: [safeBigIntToNumber(this.coreAppId)], foreignAssets: [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 AlgorandAddress(sender).toString(); const { accounts, txs } = await 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 = StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token); tokenStorageAddress = tokenStorage.address(); const data = await StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, tokenStorageAddress); assetId = new AlgorandAddress(data.slice(0, 8)).toInt(); } else { // Otherwise we get the storage lsig for a native asset, including ALGO (0) const nativeTokenId = new AlgorandAddress(vaa.payload.token.address).toBigInt(); tokenStorage = StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, nativeTokenId); tokenStorageAddress = tokenStorage.address(); assetId = safeBigIntToNumber(nativeTokenId); } accounts.push(tokenStorageAddress); let appId = 0; let receiverAddress = ""; if (vaa.payloadName === "TransferWithPayload") { appId = new AlgorandAddress(vaa.payload.to.address).toInt(); receiverAddress = getApplicationAddress(appId); } else { receiverAddress = new 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: makeAssetTransferTxnWithSuggestedParamsFromObject({ amount: 0, assetIndex: assetId, from: senderAddr, suggestedParams, to: senderAddr, }), }); } } const appCallObj = { accounts: accounts, appArgs: [AlgorandTokenBridge.completeTransfer, serialize(vaa)], appIndex: safeBigIntToNumber(this.tokenBridgeAppId), foreignAssets: foreignAssets, from: senderAddr, onComplete: OnApplicationComplete.NoOpOC, suggestedParams, }; txs.push({ tx: 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: makeApplicationCallTxnFromObject({ appArgs: [ TransferMethodSelector.getSelector(), TransferMethodSelector.args[0].type.encode(serialize(vaa)), ], appIndex: appId, foreignAssets: foreignAssets, from: senderAddr, onComplete: 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 = modelsv2.AccountAssetResponse.from_obj_for_encoding(acctInfoResp); return (acctInfo.assetHolding?.amount ?? 0) > 0; } catch { } return false; } createUnsignedTx(txReq, description, parallelizable = true) { return new AlgorandUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable); } } //# sourceMappingURL=tokenBridge.js.map