"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SolanaTokenBridge = void 0;
const connect_sdk_1 = require("@wormhole-foundation/connect-sdk");
const connect_sdk_solana_1 = require("@wormhole-foundation/connect-sdk-solana");
const connect_sdk_solana_core_1 = require("@wormhole-foundation/connect-sdk-solana-core");
const spl_token_1 = require("@solana/spl-token");
const web3_js_1 = require("@solana/web3.js");
const utils_1 = require("./utils");
require("@wormhole-foundation/connect-sdk-solana-core");
class SolanaTokenBridge {
    network;
    chain;
    connection;
    contracts;
    chainId;
    coreBridge;
    tokenBridge;
    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);
        const tokenBridgeAddress = contracts.tokenBridge;
        if (!tokenBridgeAddress)
            throw new Error(`TokenBridge contract Address for chain ${chain} not found`);
        this.tokenBridge = (0, utils_1.createReadOnlyTokenBridgeProgramInterface)(tokenBridgeAddress, connection);
        this.coreBridge = new connect_sdk_solana_core_1.SolanaWormholeCore(network, chain, connection, contracts);
    }
    static async fromRpc(connection, config) {
        const [network, chain] = await connect_sdk_solana_1.SolanaPlatform.chainFromRpc(connection);
        const conf = config[chain];
        if (conf.network !== network)
            throw new Error(`Network mismatch for chain ${chain}: ${conf.network} != ${network}`);
        return new SolanaTokenBridge(network, chain, connection, conf.contracts);
    }
    async isWrappedAsset(token) {
        return (0, utils_1.getWrappedMeta)(this.connection, this.tokenBridge.programId, new connect_sdk_solana_1.SolanaAddress(token).toUint8Array())
            .catch((_) => null)
            .then((meta) => meta != null);
    }
    async getOriginalAsset(token) {
        if (!(await this.isWrappedAsset(token)))
            throw (0, connect_sdk_1.ErrNotWrapped)(token.toString());
        const tokenAddr = new connect_sdk_solana_1.SolanaAddress(token).toUint8Array();
        const mint = new web3_js_1.PublicKey(tokenAddr);
        try {
            const meta = await (0, utils_1.getWrappedMeta)(this.connection, this.tokenBridge.programId, tokenAddr);
            if (meta === null)
                return {
                    chain: this.chain,
                    address: new connect_sdk_solana_1.SolanaAddress(mint.toBytes()).toUniversalAddress(),
                };
            return {
                chain: (0, connect_sdk_1.toChain)(meta.chain),
                address: new connect_sdk_1.UniversalAddress(meta.tokenAddress),
            };
        }
        catch (_) {
            throw (0, connect_sdk_1.ErrNotWrapped)(token.toString());
        }
    }
    async hasWrappedAsset(token) {
        try {
            await this.getWrappedAsset(token);
            return true;
        }
        catch (_) { }
        return false;
    }
    async getWrappedAsset(token) {
        if ((0, connect_sdk_1.isNative)(token.address))
            throw new Error('Native cannot be a wrapped asset');
        const mint = (0, utils_1.deriveWrappedMintKey)(this.tokenBridge.programId, (0, connect_sdk_1.toChainId)(token.chain), token.address.toUniversalAddress().toUint8Array());
        // If we don't throw an error getting wrapped meta, we're good to return
        // the derived mint address back to the caller.
        try {
            await (0, utils_1.getWrappedMeta)(this.connection, this.tokenBridge.programId, mint);
            return (0, connect_sdk_1.toNative)(this.chain, mint.toBase58());
        }
        catch (_) { }
        throw (0, connect_sdk_1.ErrNotWrapped)(`${mint}: ${token.address.toUniversalAddress().toString()}`);
    }
    async isTransferCompleted(vaa) {
        return connect_sdk_solana_core_1.utils
            .getClaim(this.connection, this.tokenBridge.programId, vaa.emitterAddress.toUint8Array(), (0, connect_sdk_1.toChainId)(vaa.emitterChain), vaa.sequence, this.connection.commitment)
            .catch(() => false);
    }
    async getWrappedNative() {
        return (0, connect_sdk_1.toNative)(this.chain, spl_token_1.NATIVE_MINT.toBase58());
    }
    async *createAttestation(token, payer) {
        if (!payer)
            throw new Error('Payer required to create attestation');
        const senderAddress = new connect_sdk_solana_1.SolanaAddress(payer).unwrap();
        // TODO: createNonce().readUInt32LE(0);
        const nonce = 0;
        const msgFee = await this.coreBridge.getMessageFee();
        const transferIx = connect_sdk_solana_core_1.utils.createBridgeFeeTransferInstruction(this.coreBridge.coreBridge.programId, senderAddress, msgFee);
        const messageKey = web3_js_1.Keypair.generate();
        const attestIx = (0, utils_1.createAttestTokenInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, new connect_sdk_solana_1.SolanaAddress(token).toUint8Array(), messageKey.publicKey, nonce);
        const transaction = new web3_js_1.Transaction().add(transferIx, attestIx);
        transaction.feePayer = senderAddress;
        yield this.createUnsignedTx({ transaction, signers: [messageKey] }, 'Solana.AttestToken');
    }
    async *submitAttestation(vaa, payer) {
        if (!payer)
            throw new Error('Payer required to create attestation');
        const senderAddress = new connect_sdk_solana_1.SolanaAddress(payer).unwrap();
        // Yield transactions to verify sigs and post the VAA
        yield* this.coreBridge.postVaa(senderAddress, vaa);
        // Now yield the transaction to actually create the token
        const transaction = new web3_js_1.Transaction().add((0, utils_1.createCreateWrappedInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, vaa));
        transaction.feePayer = senderAddress;
        yield this.createUnsignedTx({ transaction }, 'Solana.CreateWrapped');
    }
    async transferSol(sender, recipient, amount, payload) {
        //  https://github.com/wormhole-foundation/wormhole-connect/blob/development/sdk/src/contexts/solana/context.ts#L245
        const senderAddress = new connect_sdk_solana_1.SolanaAddress(sender).unwrap();
        // TODO: the payer can actually be different from the sender. We need to allow the user to pass in an optional payer
        const payerPublicKey = senderAddress;
        const recipientAddress = recipient.address
            .toUniversalAddress()
            .toUint8Array();
        const recipientChainId = (0, connect_sdk_1.toChainId)(recipient.chain);
        const nonce = 0;
        const relayerFee = 0n;
        const message = web3_js_1.Keypair.generate();
        const ancillaryKeypair = web3_js_1.Keypair.generate();
        const rentBalance = await (0, spl_token_1.getMinimumBalanceForRentExemptAccount)(this.connection);
        //This will create a temporary account where the wSOL will be created.
        const createAncillaryAccountIx = web3_js_1.SystemProgram.createAccount({
            fromPubkey: payerPublicKey,
            newAccountPubkey: ancillaryKeypair.publicKey,
            lamports: rentBalance,
            space: spl_token_1.ACCOUNT_SIZE,
            programId: spl_token_1.TOKEN_PROGRAM_ID,
        });
        //Send in the amount of SOL which we want converted to wSOL
        const initialBalanceTransferIx = web3_js_1.SystemProgram.transfer({
            fromPubkey: payerPublicKey,
            lamports: amount,
            toPubkey: ancillaryKeypair.publicKey,
        });
        //Initialize the account as a WSOL account, with the original payerAddress as owner
        const initAccountIx = (0, spl_token_1.createInitializeAccountInstruction)(ancillaryKeypair.publicKey, spl_token_1.NATIVE_MINT, payerPublicKey);
        //Normal approve & transfer instructions, except that the wSOL is sent from the ancillary account.
        const approvalIx = (0, utils_1.createApproveAuthoritySignerInstruction)(this.tokenBridge.programId, ancillaryKeypair.publicKey, payerPublicKey, amount);
        const tokenBridgeTransferIx = payload
            ? (0, utils_1.createTransferNativeWithPayloadInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, message.publicKey, ancillaryKeypair.publicKey, spl_token_1.NATIVE_MINT, nonce, amount, recipientAddress, recipientChainId, payload)
            : (0, utils_1.createTransferNativeInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, message.publicKey, ancillaryKeypair.publicKey, spl_token_1.NATIVE_MINT, nonce, amount, relayerFee, recipientAddress, recipientChainId);
        //Close the ancillary account for cleanup. Payer address receives any remaining funds
        const closeAccountIx = (0, spl_token_1.createCloseAccountInstruction)(ancillaryKeypair.publicKey, //account to close
        payerPublicKey, //Remaining funds destination
        payerPublicKey);
        const transaction = new web3_js_1.Transaction();
        transaction.feePayer = payerPublicKey;
        transaction.add(createAncillaryAccountIx, initialBalanceTransferIx, initAccountIx, approvalIx, tokenBridgeTransferIx, closeAccountIx);
        return this.createUnsignedTx({ transaction, signers: [message, ancillaryKeypair] }, 'TokenBridge.TransferNative');
    }
    async *transfer(sender, recipient, token, amount, payload) {
        // TODO: payer vs sender?? can caller add diff payer later?
        if ((0, connect_sdk_1.isNative)(token)) {
            yield await this.transferSol(sender, recipient, amount, payload);
            return;
        }
        const tokenAddress = new connect_sdk_solana_1.SolanaAddress(token).unwrap();
        const senderAddress = new connect_sdk_solana_1.SolanaAddress(sender).unwrap();
        const senderTokenAddress = await (0, spl_token_1.getAssociatedTokenAddress)(tokenAddress, senderAddress);
        const recipientAddress = recipient.address
            .toUniversalAddress()
            .toUint8Array();
        const recipientChainId = (0, connect_sdk_1.toChainId)(recipient.chain);
        const nonce = 0;
        const relayerFee = 0n;
        const isSolanaNative = !(await this.isWrappedAsset(token));
        const message = web3_js_1.Keypair.generate();
        let tokenBridgeTransferIx;
        if (isSolanaNative) {
            tokenBridgeTransferIx = payload
                ? (0, utils_1.createTransferNativeWithPayloadInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, message.publicKey, senderTokenAddress, tokenAddress, nonce, amount, recipientAddress, recipientChainId, payload)
                : (0, utils_1.createTransferNativeInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, message.publicKey, senderTokenAddress, tokenAddress, nonce, amount, relayerFee, recipientAddress, recipientChainId);
        }
        else {
            const originAsset = await this.getOriginalAsset(token);
            if ((0, connect_sdk_1.isNative)(originAsset.address))
                throw new Error('Native cannot be an original asset');
            tokenBridgeTransferIx = payload
                ? (0, utils_1.createTransferWrappedWithPayloadInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, message.publicKey, senderTokenAddress, senderAddress, (0, connect_sdk_1.toChainId)(originAsset.chain), originAsset.address.toUint8Array(), nonce, amount, recipientAddress, recipientChainId, payload)
                : (0, utils_1.createTransferWrappedInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, message.publicKey, senderTokenAddress, senderAddress, (0, connect_sdk_1.toChainId)(originAsset.chain), originAsset.address.toUint8Array(), nonce, amount, relayerFee, recipientAddress, recipientChainId);
        }
        const approvalIx = (0, utils_1.createApproveAuthoritySignerInstruction)(this.tokenBridge.programId, senderTokenAddress, senderAddress, amount);
        const transaction = new web3_js_1.Transaction().add(approvalIx, tokenBridgeTransferIx);
        transaction.feePayer = senderAddress;
        yield this.createUnsignedTx({ transaction, signers: [message] }, 'TokenBridge.TransferTokens');
    }
    async *redeemAndUnwrap(sender, vaa) {
        // sender, fee payer
        const payerPublicKey = new connect_sdk_solana_1.SolanaAddress(sender).unwrap();
        // (maybe) ATA for this account
        const targetPublicKey = new connect_sdk_solana_1.SolanaAddress(vaa.payload.to.address).unwrap();
        const targetAmount = await (0, spl_token_1.getMint)(this.connection, spl_token_1.NATIVE_MINT).then((info) => vaa.payload.token.amount * BigInt(Math.pow(10, info.decimals - 8)));
        const rentBalance = await (0, spl_token_1.getMinimumBalanceForRentExemptAccount)(this.connection);
        const ancillaryKeypair = web3_js_1.Keypair.generate();
        const completeTransferIx = (0, utils_1.createCompleteTransferNativeInstruction)(this.connection, this.tokenBridge.programId, this.coreBridge.address, payerPublicKey, vaa);
        //This will create a temporary account where the wSOL will be moved
        const createAncillaryAccountIx = web3_js_1.SystemProgram.createAccount({
            fromPubkey: payerPublicKey,
            newAccountPubkey: ancillaryKeypair.publicKey,
            lamports: rentBalance,
            space: spl_token_1.ACCOUNT_SIZE,
            programId: spl_token_1.TOKEN_PROGRAM_ID,
        });
        //Initialize the account as a WSOL account, with the original payerAddress as owner
        const initAccountIx = (0, spl_token_1.createInitializeAccountInstruction)(ancillaryKeypair.publicKey, spl_token_1.NATIVE_MINT, payerPublicKey);
        //Send in the amount of wSOL which we want converted to SOL
        const balanceTransferIx = (0, spl_token_1.createTransferInstruction)(targetPublicKey, ancillaryKeypair.publicKey, payerPublicKey, targetAmount.valueOf());
        //Close the ancillary account for cleanup. Payer address receives any remaining funds
        const closeAccountIx = (0, spl_token_1.createCloseAccountInstruction)(ancillaryKeypair.publicKey, //account to close
        payerPublicKey, //Remaining funds destination
        payerPublicKey);
        const transaction = new web3_js_1.Transaction();
        transaction.feePayer = payerPublicKey;
        transaction.add(completeTransferIx, createAncillaryAccountIx, initAccountIx, balanceTransferIx, closeAccountIx);
        yield this.createUnsignedTx({ transaction, signers: [ancillaryKeypair] }, 'TokenBridge.RedeemAndUnwrap');
    }
    async *createAta(sender, token) {
        const senderAddress = new connect_sdk_solana_1.SolanaAddress(sender).unwrap();
        const tokenAddress = new connect_sdk_solana_1.SolanaAddress(token).unwrap();
        const ata = await (0, spl_token_1.getAssociatedTokenAddress)(tokenAddress, senderAddress);
        // If the ata doesn't exist yet, create it
        const acctInfo = await this.connection.getAccountInfo(ata);
        if (acctInfo === null) {
            const transaction = new web3_js_1.Transaction().add((0, spl_token_1.createAssociatedTokenAccountInstruction)(senderAddress, ata, senderAddress, tokenAddress));
            transaction.feePayer = senderAddress;
            yield this.createUnsignedTx({ transaction }, 'Redeem.CreateATA');
        }
    }
    async *redeem(sender, vaa, unwrapNative = true) {
        // Find the token address local to this chain
        const nativeAddress = vaa.payload.token.chain === this.chain
            ? vaa.payload.token.address
            : (await this.getWrappedAsset(vaa.payload.token)).toUniversalAddress();
        // Create an ATA if necessary
        yield* this.createAta(sender, nativeAddress);
        // Post the VAA if necessary
        yield* this.coreBridge.postVaa(sender, vaa);
        // redeem vaa and unwrap to native sol from wrapped sol
        if (unwrapNative) {
            // Check if this is native wrapped sol
            const wrappedNative = new connect_sdk_solana_1.SolanaAddress(await this.getWrappedNative());
            if (connect_sdk_1.encoding.bytes.equals(nativeAddress.toUint8Array(), wrappedNative.toUint8Array())) {
                yield* this.redeemAndUnwrap(sender, vaa);
                return;
            }
        }
        const senderAddress = new connect_sdk_solana_1.SolanaAddress(sender).unwrap();
        const createCompleteTransferInstruction = vaa.payload.token.chain == this.chain
            ? utils_1.createCompleteTransferNativeInstruction
            : utils_1.createCompleteTransferWrappedInstruction;
        const transaction = new web3_js_1.Transaction().add(createCompleteTransferInstruction(this.connection, this.tokenBridge.programId, this.coreBridge.address, senderAddress, vaa));
        transaction.feePayer = senderAddress;
        yield this.createUnsignedTx({ transaction }, 'Solana.RedeemTransfer');
    }
    createUnsignedTx(txReq, description, parallelizable = false) {
        return new connect_sdk_solana_1.SolanaUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable);
    }
}
exports.SolanaTokenBridge = SolanaTokenBridge;
//# sourceMappingURL=tokenBridge.js.map