import { isNative, toChainId, toNative, } from '@wormhole-foundation/connect-sdk';
import { SolanaAddress, SolanaPlatform, SolanaUnsignedTransaction, } from '@wormhole-foundation/connect-sdk-solana';
import { BN } from '@project-serum/anchor';
import { PublicKey, Transaction } from '@solana/web3.js';
import { createTokenBridgeRelayerProgramInterface, createTransferNativeTokensWithRelayInstruction, createTransferWrappedTokensWithRelayInstruction, deriveForeignContractAddress, deriveRedeemerConfigAddress, deriveRegisteredTokenAddress, } from './utils/automaticTokenBridge';
import { NATIVE_MINT, TokenAccountNotFoundError, createAssociatedTokenAccountInstruction, getAccount, getAssociatedTokenAddressSync, } from '@solana/spl-token';
import '@wormhole-foundation/connect-sdk-solana-core';
import { registeredTokens } from './consts';
const SOL_DECIMALS = 9;
const TEN = new BN(10);
const SWAP_RATE_PRECISION = new BN(100000000);
export class SolanaAutomaticTokenBridge {
    network;
    chain;
    connection;
    contracts;
    chainId;
    coreBridgeProgramId;
    tokenBridgeProgramId;
    tokenBridgeRelayer;
    constructor(network, chain, connection, contracts) {
        this.network = network;
        this.chain = chain;
        this.connection = connection;
        this.contracts = contracts;
        this.chainId = toChainId(chain);
        const tokenBridgeRelayerAddress = contracts.tokenBridgeRelayer;
        if (!tokenBridgeRelayerAddress)
            throw new Error(`TokenBridge contract Address for chain ${chain} not found`);
        this.tokenBridgeRelayer = createTokenBridgeRelayerProgramInterface(tokenBridgeRelayerAddress, connection);
        this.tokenBridgeProgramId = new PublicKey(contracts.tokenBridge);
        this.coreBridgeProgramId = new PublicKey(contracts.coreBridge);
    }
    static async fromRpc(connection, config) {
        const [network, chain] = await SolanaPlatform.chainFromRpc(connection);
        const conf = config[chain];
        if (conf.network !== network)
            throw new Error(`Network mismatch for chain ${chain}: ${conf.network} != ${network}`);
        return new SolanaAutomaticTokenBridge(network, chain, connection, conf.contracts);
    }
    async *transfer(sender, recipient, token, amount, nativeGas) {
        const nonce = 0;
        const senderAddress = new SolanaAddress(sender).unwrap();
        const recipientAddress = recipient.address
            .toUniversalAddress()
            .toUint8Array();
        const tokenMint = this.mintAddress(token);
        const transaction = new Transaction();
        if (isNative(token)) {
            const ata = getAssociatedTokenAddressSync(tokenMint, senderAddress);
            try {
                await getAccount(this.connection, ata);
            }
            catch (e) {
                if (e instanceof TokenAccountNotFoundError) {
                    // the relayer expects the wSOL associated token account to exist
                    const createAccountInst = createAssociatedTokenAccountInstruction(senderAddress, ata, senderAddress, tokenMint);
                    transaction.add(createAccountInst);
                }
                else {
                    throw e;
                }
            }
        }
        const nativeGasAmount = nativeGas ? nativeGas : 0n;
        const tokenIsNative = isNative(token);
        const transferIx = tokenIsNative
            ? await createTransferNativeTokensWithRelayInstruction(this.connection, this.tokenBridgeRelayer.programId, senderAddress, this.tokenBridgeProgramId, this.coreBridgeProgramId, tokenMint, amount, nativeGasAmount, recipientAddress, recipient.chain, nonce, tokenIsNative)
            : await createTransferWrappedTokensWithRelayInstruction(this.connection, this.tokenBridgeRelayer.programId, senderAddress, this.tokenBridgeProgramId, this.coreBridgeProgramId, tokenMint, amount, nativeGasAmount, recipientAddress, recipient.chain, nonce);
        transaction.add(transferIx);
        transaction.feePayer = senderAddress;
        yield this.createUnsignedTx({ transaction }, 'AutomaticTokenBridge.Transfer');
    }
    async *redeem(sender, vaa) {
        const transaction = new Transaction();
        yield this.createUnsignedTx({ transaction }, 'AutomaticTokenBridge.Redeem');
        throw new Error('Method not implemented.');
    }
    async getRelayerFee(sender, recipient, token) {
        const tokenAddress = this.mintAddress(token);
        const [{ fee }, { swapRate }, { relayerFeePrecision }] = await Promise.all([
            this.getForeignContract(recipient.chain),
            this.getRegisteredToken(tokenAddress),
            this.getRedeemerConfig(),
        ]);
        const decimals = Number(await SolanaPlatform.getDecimals(this.chain, this.connection, token));
        const relayerFee = TEN.pow(new BN(decimals))
            .mul(fee)
            .mul(SWAP_RATE_PRECISION)
            .div(new BN(relayerFeePrecision).mul(swapRate));
        return BigInt(relayerFee.toString());
    }
    async maxSwapAmount(token) {
        const mint = this.mintAddress(token);
        const [{ swapRate, maxNativeSwapAmount }, { swapRate: solSwapRate }] = await Promise.all([
            this.getRegisteredToken(mint),
            this.getRegisteredToken(NATIVE_MINT),
        ]);
        const decimals = Number(await SolanaPlatform.getDecimals(this.chain, this.connection, token));
        const nativeSwapRate = this.calculateNativeSwapRate(solSwapRate, swapRate);
        const maxSwapAmountIn = decimals > SOL_DECIMALS
            ? maxNativeSwapAmount
                .mul(nativeSwapRate)
                .mul(TEN.pow(new BN(decimals - SOL_DECIMALS)))
                .div(SWAP_RATE_PRECISION)
            : maxNativeSwapAmount
                .mul(nativeSwapRate)
                .div(TEN.pow(new BN(SOL_DECIMALS - decimals)).mul(SWAP_RATE_PRECISION));
        return BigInt(maxSwapAmountIn.toString());
    }
    async nativeTokenAmount(token, amount) {
        if (amount === 0n)
            return 0n;
        const mint = this.mintAddress(token);
        const decimals = Number(await SolanaPlatform.getDecimals(this.chain, this.connection, token));
        const [{ swapRate }, { swapRate: solSwapRate }] = await Promise.all([
            this.getRegisteredToken(mint),
            this.getRegisteredToken(NATIVE_MINT),
        ]);
        const nativeSwapRate = this.calculateNativeSwapRate(solSwapRate, swapRate);
        const swapAmountOut = decimals > SOL_DECIMALS
            ? SWAP_RATE_PRECISION.mul(new BN(amount.toString())).div(nativeSwapRate.mul(TEN.pow(new BN(decimals - SOL_DECIMALS))))
            : SWAP_RATE_PRECISION.mul(new BN(amount.toString()))
                .mul(TEN.pow(new BN(SOL_DECIMALS - decimals)))
                .div(nativeSwapRate);
        return BigInt(swapAmountOut.toString());
    }
    async isRegisteredToken(token) {
        const mint = this.mintAddress(token);
        try {
            await this.getRegisteredToken(mint);
            return true;
        }
        catch (e) {
            if (e.message?.includes('Account does not exist')) {
                // the token is not registered
                return false;
            }
            throw e;
        }
    }
    mintAddress(token) {
        return isNative(token)
            ? new PublicKey(NATIVE_MINT)
            : new SolanaAddress(token).unwrap();
    }
    async getRegisteredTokens() {
        return registeredTokens[this.network].map((addr) => toNative(this.chain, addr));
    }
    calculateNativeSwapRate(solSwapRate, swapRate) {
        return SWAP_RATE_PRECISION.mul(solSwapRate).div(swapRate);
    }
    async getForeignContract(chain) {
        return await this.tokenBridgeRelayer.account.foreignContract.fetch(deriveForeignContractAddress(this.tokenBridgeRelayer.programId, chain));
    }
    async getRegisteredToken(mint) {
        return await this.tokenBridgeRelayer.account.registeredToken.fetch(deriveRegisteredTokenAddress(this.tokenBridgeRelayer.programId, mint));
    }
    async getRedeemerConfig() {
        return await this.tokenBridgeRelayer.account.redeemerConfig.fetch(deriveRedeemerConfigAddress(this.tokenBridgeRelayer.programId));
    }
    createUnsignedTx(txReq, description, parallelizable = false) {
        return new SolanaUnsignedTransaction(txReq, this.network, this.chain, description, parallelizable);
    }
}
//# sourceMappingURL=automaticTokenBridge.js.map