"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AlgorandTokenBridge = exports.TransferMethodSelector = void 0;
const connect_sdk_1 = require("@wormhole-foundation/connect-sdk");
const connect_sdk_algorand_1 = require("@wormhole-foundation/connect-sdk-algorand");
const connect_sdk_algorand_core_1 = require("@wormhole-foundation/connect-sdk-algorand-core");
const algosdk_1 = require("algosdk");
require("@wormhole-foundation/connect-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 = connect_sdk_1.encoding.bytes.encode("sendTransfer");
    static attestToken = connect_sdk_1.encoding.bytes.encode("attestToken");
    static noop = connect_sdk_1.encoding.bytes.encode("nop");
    static optIn = connect_sdk_1.encoding.bytes.encode("optin");
    static completeTransfer = connect_sdk_1.encoding.bytes.encode("completeTransfer");
    static receiveAttest = connect_sdk_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, connect_sdk_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 connect_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 connect_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 connect_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 connect_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 connect_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, connect_sdk_1.toChain)(connect_sdk_1.encoding.bignum.decode(chainBytes));
        const address = new connect_sdk_1.UniversalAddress(decodedLocalState.slice(60, 60 + 32));
        return { chain, address };
    }
    // Returns the address of the native version of this asset
    async getWrappedAsset(token) {
        const storageAccount = connect_sdk_algorand_core_1.StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, token);
        const data = await connect_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, connect_sdk_1.toNative)(this.chain, connect_sdk_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, connect_sdk_1.toNative)(this.chain, "0");
    }
    async isTransferCompleted(vaa) {
        const messageStorage = connect_sdk_algorand_core_1.StorageLogicSig.forMessageId(this.tokenBridgeAppId, {
            sequence: vaa.sequence,
            chain: vaa.emitterChain,
            emitter: vaa.emitterAddress,
        });
        try {
            return await connect_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 connect_sdk_algorand_1.AlgorandAddress(payer).toString();
        const assetId = new connect_sdk_algorand_1.AlgorandAddress(token).toInt();
        const txs = [];
        const suggestedParams = await this.connection.getTransactionParams().do();
        const tbs = connect_sdk_algorand_core_1.StorageLogicSig.forEmitter(this.coreAppId, new connect_sdk_algorand_1.AlgorandAddress(this.tokenBridgeAddress).toUint8Array());
        const { accounts: [emitterAddr], txs: emitterOptInTxs, } = await connect_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 = connect_sdk_algorand_core_1.StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, BigInt(assetId));
        const txns = await connect_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, connect_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, connect_sdk_1.encoding.bignum.toBytes(assetId, 8)],
            accounts: accts,
            appIndex: (0, connect_sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId),
            foreignApps: [(0, connect_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 = connect_sdk_algorand_core_1.StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token);
        const tokenStorageAddress = tokenStorage.address();
        const txs = [];
        const foreignAssets = [];
        const data = await connect_sdk_algorand_core_1.StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, tokenStorageAddress);
        if (data.length > 8) {
            foreignAssets.push(new connect_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, connect_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, connect_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, connect_sdk_1.serialize)(vaa)],
                appIndex: (0, connect_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, connect_sdk_1.isNative)(token) ? 0 : new connect_sdk_algorand_1.AlgorandAddress(token).toInt();
        const qty = amount;
        const chainId = (0, connect_sdk_1.toChainId)(recipient.chain);
        const receiver = recipient.address.toUniversalAddress().toUint8Array();
        const suggestedParams = await this.connection.getTransactionParams().do();
        const fee = BigInt(0);
        const tbs = connect_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 connect_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 = connect_sdk_algorand_core_1.StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, BigInt(assetId));
            const { accounts: [address], txs, } = await connect_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, connect_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, connect_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,
            connect_sdk_1.encoding.bignum.toBytes(assetId, 8),
            connect_sdk_1.encoding.bignum.toBytes(qty, 8),
            receiver,
            connect_sdk_1.encoding.bignum.toBytes(chainId, 8),
            connect_sdk_1.encoding.bignum.toBytes(fee, 8),
        ];
        if (payload)
            args.push(payload);
        const acTxn = (0, algosdk_1.makeApplicationCallTxnFromObject)({
            from: senderAddr,
            appIndex: (0, connect_sdk_algorand_1.safeBigIntToNumber)(this.tokenBridgeAppId),
            onComplete: algosdk_1.OnApplicationComplete.NoOpOC,
            appArgs: args,
            foreignApps: [(0, connect_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 connect_sdk_algorand_1.AlgorandAddress(sender).toString();
        const { accounts, txs } = await connect_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 = connect_sdk_algorand_core_1.StorageLogicSig.forWrappedAsset(this.tokenBridgeAppId, vaa.payload.token);
            tokenStorageAddress = tokenStorage.address();
            const data = await connect_sdk_algorand_core_1.StorageLogicSig.decodeLocalState(this.connection, this.tokenBridgeAppId, tokenStorageAddress);
            assetId = new connect_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 connect_sdk_algorand_1.AlgorandAddress(vaa.payload.token.address).toBigInt();
            tokenStorage = connect_sdk_algorand_core_1.StorageLogicSig.forNativeAsset(this.tokenBridgeAppId, nativeTokenId);
            tokenStorageAddress = tokenStorage.address();
            assetId = (0, connect_sdk_algorand_1.safeBigIntToNumber)(nativeTokenId);
        }
        accounts.push(tokenStorageAddress);
        let appId = 0;
        let receiverAddress = "";
        if (vaa.payloadName === "TransferWithPayload") {
            appId = new connect_sdk_algorand_1.AlgorandAddress(vaa.payload.to.address).toInt();
            receiverAddress = (0, algosdk_1.getApplicationAddress)(appId);
        }
        else {
            receiverAddress = new connect_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, connect_sdk_1.serialize)(vaa)],
            appIndex: (0, connect_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, connect_sdk_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 connect_sdk_algorand_1.AlgorandUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable);
    }
}
exports.AlgorandTokenBridge = AlgorandTokenBridge;
//# sourceMappingURL=tokenBridge.js.map