@wormhole-foundation/sdk-algorand-tokenbridge
Version:
SDK for Algorand, used in conjunction with @wormhole-foundation/sdk
496 lines • 24.2 kB
JavaScript
"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