"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TokenTransfer = void 0;
const sdk_base_1 = require("@wormhole-foundation/sdk-base");
const sdk_definitions_1 = require("@wormhole-foundation/sdk-definitions");
const common_1 = require("../common");
const config_1 = require("../config");
const types_1 = require("../types");
const wormhole_1 = require("../wormhole");
// 8 is maximum precision supported by the token bridge VAA
const TOKEN_BRIDGE_MAX_DECIMALS = 8;
class TokenTransfer {
    wh;
    fromChain;
    toChain;
    // state machine tracker
    _state;
    // transfer details
    transfer;
    // txids, populated once transactions are submitted
    txids = [];
    // The corresponding vaa representing the TokenTransfer
    // on the source chain (if its been completed and finalized)
    attestations;
    constructor(wh, transfer, fromChain, toChain) {
        this._state = types_1.TransferState.Created;
        this.wh = wh;
        this.transfer = transfer;
        this.fromChain = fromChain ?? wh.getChain(transfer.from.chain);
        this.toChain = toChain ?? wh.getChain(transfer.to.chain);
    }
    getTransferState() {
        return this._state;
    }
    static async from(wh, from, timeout = 6000, fromChain, toChain) {
        if ((0, sdk_definitions_1.isTokenTransferDetails)(from)) {
            fromChain = fromChain ?? wh.getChain(from.from.chain);
            toChain = toChain ?? wh.getChain(from.to.chain);
            // throws if invalid
            TokenTransfer.validateTransferDetails(wh, from, fromChain, toChain);
            // Apply hackery
            from = {
                ...from,
                ...(await TokenTransfer.destinationOverrides(fromChain, toChain, from)),
            };
            return new TokenTransfer(wh, from, fromChain, toChain);
        }
        let tt;
        if ((0, sdk_definitions_1.isWormholeMessageId)(from)) {
            tt = await TokenTransfer.fromIdentifier(wh, from, timeout);
        }
        else if ((0, sdk_definitions_1.isTransactionIdentifier)(from)) {
            tt = await TokenTransfer.fromTransaction(wh, from, timeout, fromChain);
        }
        else {
            throw new Error("Invalid `from` parameter for TokenTransfer");
        }
        tt.fromChain = fromChain ?? wh.getChain(tt.transfer.from.chain);
        tt.toChain = toChain ?? wh.getChain(tt.transfer.to.chain);
        await tt.fetchAttestation(timeout);
        return tt;
    }
    // init from the seq id
    static async fromIdentifier(wh, id, timeout) {
        const vaa = await TokenTransfer.getTransferVaa(wh, id, timeout);
        const automatic = vaa.protocolName === "AutomaticTokenBridge";
        // TODO: the `from.address` here is a lie, but we don't
        // immediately have enough info to get the _correct_ one
        let from = { chain: vaa.emitterChain, address: vaa.emitterAddress };
        let { token, to } = vaa.payload;
        const scaledAmount = sdk_base_1.amount.scale(sdk_base_1.amount.fromBaseUnits(token.amount, TOKEN_BRIDGE_MAX_DECIMALS), await wh.getDecimals(token.chain, token.address));
        let nativeGasAmount = 0n;
        if (automatic) {
            nativeGasAmount = vaa.payload.payload.toNativeTokenAmount;
            from = { chain: vaa.emitterChain, address: vaa.payload.from };
            to = { chain: vaa.payload.to.chain, address: vaa.payload.payload.targetRecipient };
        }
        const details = {
            token: token,
            amount: sdk_base_1.amount.units(scaledAmount),
            from,
            to,
            automatic,
            nativeGas: nativeGasAmount,
        };
        // TODO: grab at least the init tx from the api
        const tt = new TokenTransfer(wh, details);
        tt.attestations = [{ id: id, attestation: vaa }];
        tt._state = types_1.TransferState.Attested;
        return tt;
    }
    static async fromTransaction(wh, from, timeout, fromChain) {
        fromChain = fromChain ?? wh.getChain(from.chain);
        const msg = await TokenTransfer.getTransferMessage(fromChain, from.txid, timeout);
        const tt = await TokenTransfer.fromIdentifier(wh, msg, timeout);
        tt.txids = [from];
        return tt;
    }
    // start the WormholeTransfer by submitting transactions to the source chain
    // returns a transaction hash
    async initiateTransfer(signer) {
        if (this._state !== types_1.TransferState.Created)
            throw new Error("Invalid state transition in `initiateTransfer`");
        this.txids = await TokenTransfer.transfer(this.fromChain, this.transfer, signer);
        this._state = types_1.TransferState.SourceInitiated;
        return this.txids.map(({ txid }) => txid);
    }
    // wait for the VAA to be ready
    // returns the sequence number
    async fetchAttestation(timeout) {
        if (this._state < types_1.TransferState.SourceInitiated || this._state > types_1.TransferState.Attested)
            throw new Error("Invalid state transition in `fetchAttestation`, expected at least `SourceInitiated`");
        if (!this.attestations || this.attestations.length === 0) {
            if (this.txids.length === 0)
                throw new Error("No VAAs set and txids available to look them up");
            // TODO: assuming the _last_ transaction in the list will contain the msg id
            const txid = this.txids[this.txids.length - 1];
            const msgId = await TokenTransfer.getTransferMessage(this.fromChain, txid.txid, timeout);
            this.attestations = [{ id: msgId }];
        }
        for (const idx in this.attestations) {
            // Check if we already have the VAA
            if (this.attestations[idx].attestation)
                continue;
            this.attestations[idx].attestation = await TokenTransfer.getTransferVaa(this.wh, this.attestations[idx].id, timeout);
        }
        this._state = types_1.TransferState.Attested;
        return this.attestations.map((vaa) => vaa.id);
    }
    // finish the WormholeTransfer by submitting transactions to the destination chain
    // returns a transaction hash
    async completeTransfer(signer) {
        if (this._state < types_1.TransferState.Attested)
            throw new Error("Invalid state transition, must be attested prior to calling `completeTransfer`.");
        if (!this.attestations)
            throw new Error("No VAA details available");
        const { attestation } = this.attestations[0];
        if (!attestation)
            throw new Error(`No VAA found for ${this.attestations[0].id.sequence}`);
        const redeemTxids = await TokenTransfer.redeem(this.toChain, attestation, signer);
        this.txids.push(...redeemTxids);
        this._state = types_1.TransferState.DestinationInitiated;
        return redeemTxids.map(({ txid }) => txid);
    }
    // Static method to perform the transfer so a custom RPC may be used
    // Note: this assumes the transfer has already been validated with `validateTransfer`
    static async transfer(fromChain, transfer, signer) {
        const senderAddress = (0, sdk_definitions_1.toNative)(signer.chain(), signer.address());
        const token = (0, sdk_definitions_1.isTokenId)(transfer.token) ? transfer.token.address : transfer.token;
        let xfer;
        if (transfer.automatic) {
            const tb = await fromChain.getAutomaticTokenBridge();
            xfer = tb.transfer(senderAddress, transfer.to, token, transfer.amount, transfer.nativeGas);
        }
        else {
            const tb = await fromChain.getTokenBridge();
            xfer = tb.transfer(senderAddress, transfer.to, token, transfer.amount, transfer.payload);
        }
        return (0, common_1.signSendWait)(fromChain, xfer, signer);
    }
    // Static method to allow passing a custom RPC
    static async redeem(toChain, vaa, signer) {
        const signerAddress = (0, sdk_definitions_1.toNative)(signer.chain(), signer.address());
        const xfer = vaa.protocolName === "AutomaticTokenBridge"
            ? (await toChain.getAutomaticTokenBridge()).redeem(signerAddress, vaa)
            : (await toChain.getTokenBridge()).redeem(signerAddress, vaa);
        return (0, common_1.signSendWait)(toChain, xfer, signer);
    }
    static async isTransferComplete(toChain, vaa) {
        // TODO: converter?
        if (vaa.protocolName === "AutomaticTokenBridge")
            vaa = (0, sdk_definitions_1.deserialize)("TokenBridge:TransferWithPayload", (0, sdk_definitions_1.serialize)(vaa));
        const tb = await toChain.getTokenBridge();
        return tb.isTransferCompleted(vaa);
    }
    static async getTransferMessage(chain, txid, timeout) {
        // A Single wormhole message will be returned for a standard token transfer
        const whm = await wormhole_1.Wormhole.parseMessageFromTx(chain, txid, timeout);
        if (whm.length !== 1)
            throw new Error("Expected a single Wormhole Message, got: " + whm.length);
        return whm[0];
    }
    static async getTransferVaa(wh, key, timeout) {
        const vaa = await wh.getVaa(key, sdk_definitions_1.TokenBridge.getTransferDiscriminator(), timeout);
        if (!vaa)
            throw new Error(`No VAA available after retries exhausted`);
        // TODO: converter
        // Check if its automatic and re-de-serialize
        if (vaa.payloadName === "TransferWithPayload") {
            const { chain, address } = vaa.payload.to;
            const { tokenBridgeRelayer } = wh.config.chains[chain].contracts;
            const relayerAddress = tokenBridgeRelayer ? (0, sdk_definitions_1.toUniversal)(chain, tokenBridgeRelayer) : null;
            // If the target address is the relayer address, expect its an automatic token bridge vaa
            if (!!relayerAddress && address.equals(relayerAddress)) {
                return (0, sdk_definitions_1.deserialize)("AutomaticTokenBridge:TransferWithRelay", (0, sdk_definitions_1.serialize)(vaa));
            }
        }
        return vaa;
    }
    // Lookup the token id for the destination chain given the source chain
    // and token id
    static async lookupDestinationToken(srcChain, dstChain, token) {
        // that will be minted when the transfer is redeemed
        let lookup;
        if ((0, sdk_definitions_1.isNative)(token.address)) {
            // if native, get the wrapped asset id
            lookup = await srcChain.getNativeWrappedTokenId();
        }
        else {
            try {
                const tb = await srcChain.getTokenBridge();
                // otherwise, check to see if it is a wrapped token locally
                lookup = await tb.getOriginalAsset(token.address);
            }
            catch (e) {
                // not a from-chain native wormhole-wrapped one
                lookup = token;
            }
        }
        // if the token id is actually native to the destination, return it
        if (lookup.chain === dstChain.chain) {
            return lookup;
        }
        // otherwise, figure out what the token address representing the wormhole-wrapped token we're transferring
        const dstTb = await dstChain.getTokenBridge();
        const dstAddress = await dstTb.getWrappedAsset(lookup);
        return { chain: dstChain.chain, address: dstAddress };
    }
    static validateTransferDetails(wh, transfer, fromChain, toChain) {
        if (transfer.amount === 0n)
            throw new Error("Amount cannot be 0");
        if (transfer.from.chain === transfer.to.chain)
            throw new Error("Cannot transfer to the same chain");
        fromChain = fromChain ?? wh.getChain(transfer.from.chain);
        toChain = toChain ?? wh.getChain(transfer.to.chain);
        if (transfer.automatic) {
            if (transfer.payload)
                throw new Error("Payload with automatic delivery is not supported");
            if (!fromChain.supportsAutomaticTokenBridge())
                throw new Error(`Automatic Token Bridge not supported on ${transfer.from.chain}`);
            if (!toChain.supportsAutomaticTokenBridge())
                throw new Error(`Automatic Token Bridge not supported on ${transfer.to.chain}`);
            const nativeGas = transfer.nativeGas ?? 0n;
            if (nativeGas > transfer.amount)
                throw new Error(`Native gas amount  > amount (${nativeGas} > ${transfer.amount})`);
        }
        else {
            if (transfer.nativeGas)
                throw new Error("Gas Dropoff is only supported for automatic transfers");
            if (!fromChain.supportsTokenBridge())
                throw new Error(`Token Bridge not supported on ${transfer.from.chain}`);
            if (!toChain.supportsTokenBridge())
                throw new Error(`Token Bridge not supported on ${transfer.to.chain}`);
        }
    }
    static async quoteTransfer(srcChain, dstChain, transfer) {
        const dstToken = await this.lookupDestinationToken(srcChain, dstChain, transfer.token);
        const srcToken = (0, sdk_definitions_1.isNative)(transfer.token.address)
            ? await srcChain.getNativeWrappedTokenId()
            : transfer.token;
        const srcDecimals = await srcChain.getDecimals(srcToken.address);
        const dstDecimals = await dstChain.getDecimals(dstToken.address);
        const srcAmount = sdk_base_1.amount.fromBaseUnits(transfer.amount, srcDecimals);
        const srcAmountTruncated = sdk_base_1.amount.truncate(srcAmount, TOKEN_BRIDGE_MAX_DECIMALS);
        const dstAmountReceivable = sdk_base_1.amount.scale(srcAmountTruncated, dstDecimals);
        if (!transfer.automatic) {
            return {
                sourceToken: { token: srcToken, amount: sdk_base_1.amount.units(srcAmountTruncated) },
                destinationToken: { token: dstToken, amount: sdk_base_1.amount.units(dstAmountReceivable) },
            };
        }
        // Otherwise automatic
        // The fee is removed from the amount transferred
        // quoted on the source chain
        const stb = await srcChain.getAutomaticTokenBridge();
        const fee = await stb.getRelayerFee(transfer.from.address, transfer.to, srcToken.address);
        const feeAmountDest = sdk_base_1.amount.scale(sdk_base_1.amount.truncate(sdk_base_1.amount.fromBaseUnits(fee, srcDecimals), TOKEN_BRIDGE_MAX_DECIMALS), dstDecimals);
        let dstNativeGasAmountRequested = transfer.nativeGas ?? 0n;
        // The expected destination gas can be pulled from the destination token bridge
        let destinationNativeGas = 0n;
        if (transfer.nativeGas) {
            const dtb = await dstChain.getAutomaticTokenBridge();
            // There is a limit applied to the amount of the source
            // token that may be swapped for native gas on the destination
            const maxNativeAmountIn = await dtb.maxSwapAmount(dstToken.address);
            dstNativeGasAmountRequested =
                dstNativeGasAmountRequested > maxNativeAmountIn
                    ? maxNativeAmountIn
                    : dstNativeGasAmountRequested;
            // Get the actual amount we should receive
            destinationNativeGas = await dtb.nativeTokenAmount(dstToken.address, dstNativeGasAmountRequested);
        }
        const destAmountLessFee = sdk_base_1.amount.units(dstAmountReceivable) - dstNativeGasAmountRequested - sdk_base_1.amount.units(feeAmountDest);
        return {
            sourceToken: {
                token: srcToken,
                amount: sdk_base_1.amount.units(srcAmountTruncated),
            },
            destinationToken: { token: dstToken, amount: destAmountLessFee },
            relayFee: { token: srcToken, amount: fee },
            destinationNativeGas,
        };
    }
    static async destinationOverrides(srcChain, dstChain, transfer) {
        const _transfer = { ...transfer };
        // Bit of (temporary) hackery until solana contracts support being
        // sent a VAA with the primary address
        // Note: Do _not_ override if automatic or if the destination token is native
        // gas token
        if (transfer.to.chain === "Solana" && !_transfer.automatic) {
            const destinationToken = await TokenTransfer.lookupDestinationToken(srcChain, dstChain, _transfer.token);
            // TODO: If the token is native, no need to overwrite the destination address check for native
            //if (!destinationToken.address.equals((await dstChain.getNativeWrappedTokenId()).address))
            _transfer.to = await dstChain.getTokenAccount(_transfer.to.address, destinationToken.address);
        }
        if (_transfer.to.chain === "Sei") {
            if (_transfer.to.chain === "Sei" && _transfer.payload)
                throw new Error("Arbitrary payloads unsupported for Sei");
            // For sei, we reserve the payload for a token transfer through the sei bridge.
            _transfer.payload = sdk_base_1.encoding.bytes.encode(JSON.stringify({
                basic_recipient: {
                    recipient: sdk_base_1.encoding.b64.encode(_transfer.to.address.toString()),
                },
            }));
            const translator = dstChain.config.contracts.translator;
            if (translator === undefined || translator === "")
                throw new Error("Unexpected empty translator address");
            _transfer.to = wormhole_1.Wormhole.chainAddress(_transfer.to.chain, translator);
        }
        return _transfer;
    }
    static getReceipt(xfer) {
        const { transfer } = xfer;
        const from = transfer.from.chain;
        const to = transfer.to.chain;
        let receipt = {
            from: from,
            to: to,
            state: types_1.TransferState.Created,
        };
        const originTxs = xfer.txids.filter((txid) => txid.chain === transfer.from.chain);
        if (originTxs.length > 0) {
            receipt = {
                ...receipt,
                state: types_1.TransferState.SourceInitiated,
                originTxs: originTxs,
            };
        }
        const att = xfer.attestations && xfer.attestations.length > 0 ? xfer.attestations[0] : undefined;
        const attestation = att && att.id ? { id: att.id, attestation: att.attestation } : undefined;
        if (attestation) {
            if (attestation.id) {
                receipt = {
                    ...receipt,
                    state: types_1.TransferState.SourceFinalized,
                    attestation: { id: attestation.id },
                };
                if (attestation.attestation) {
                    receipt = {
                        ...receipt,
                        state: types_1.TransferState.Attested,
                        attestation: { id: attestation.id, attestation: attestation.attestation },
                    };
                }
            }
        }
        const destinationTxs = xfer.txids.filter((txid) => txid.chain === transfer.to.chain);
        if (destinationTxs.length > 0) {
            receipt = {
                ...receipt,
                state: types_1.TransferState.DestinationInitiated,
                destinationTxs: destinationTxs,
            };
        }
        return receipt;
    }
    // AsyncGenerator fn that produces status updates through an async generator
    // eventually producing a receipt
    // can be called repeatedly so the receipt is updated as it moves through the
    // steps of the transfer
    static async *track(wh, receipt, timeout = config_1.DEFAULT_TASK_TIMEOUT, fromChain, toChain) {
        const start = Date.now();
        const leftover = (start, max) => Math.max(max - (Date.now() - start), 0);
        fromChain = fromChain ?? wh.getChain(receipt.from);
        toChain = toChain ?? wh.getChain(receipt.to);
        // Check the source chain for initiation transaction
        // and capture the message id
        if ((0, types_1.isSourceInitiated)(receipt)) {
            if (receipt.originTxs.length === 0)
                throw "Origin transactions required to fetch message id";
            const { txid } = receipt.originTxs[receipt.originTxs.length - 1];
            const msg = await TokenTransfer.getTransferMessage(fromChain, txid, leftover(start, timeout));
            receipt = {
                ...receipt,
                state: types_1.TransferState.SourceFinalized,
                attestation: { id: msg },
            };
            yield receipt;
        }
        // If the source is finalized, we need to fetch the signed attestation
        // so that we may deliver it to the destination chain
        // or at least track the transfer through its progress
        if ((0, types_1.isSourceFinalized)(receipt)) {
            if (!receipt.attestation.id)
                throw "Attestation id required to fetch attestation";
            const { id } = receipt.attestation;
            const attestation = await TokenTransfer.getTransferVaa(wh, id, leftover(start, timeout));
            receipt = {
                ...receipt,
                attestation: { id, attestation },
                state: types_1.TransferState.Attested,
            };
            yield receipt;
        }
        // First try to grab the tx status from the API
        // Note: this requires a subsequent async step on the backend
        // to have the dest txid populated, so it may be delayed by some time
        if ((0, types_1.isAttested)(receipt) || (0, types_1.isSourceFinalized)(receipt)) {
            if (!receipt.attestation.id)
                throw "Attestation id required to fetch redeem tx";
            const { id } = receipt.attestation;
            const txStatus = await wh.getTransactionStatus(id, leftover(start, timeout));
            if (txStatus && txStatus.globalTx?.destinationTx?.txHash) {
                const { chainId, txHash } = txStatus.globalTx.destinationTx;
                receipt = {
                    ...receipt,
                    destinationTxs: [{ chain: (0, sdk_base_1.toChain)(chainId), txid: txHash }],
                    state: types_1.TransferState.DestinationFinalized,
                };
            }
            yield receipt;
        }
        // Fall back to asking the destination chain if this VAA has been redeemed
        // Note: We do not get any destinationTxs with this method
        if ((0, types_1.isAttested)(receipt)) {
            if (!receipt.attestation.attestation)
                throw "Signed Attestation required to check for redeem";
            let isComplete = await TokenTransfer.isTransferComplete(toChain, receipt.attestation.attestation);
            if (isComplete) {
                receipt = {
                    ...receipt,
                    state: types_1.TransferState.DestinationFinalized,
                };
            }
            yield receipt;
        }
        yield receipt;
    }
}
exports.TokenTransfer = TokenTransfer;
//# sourceMappingURL=tokenTransfer.js.map