import { address as parseAddress } from '@solana/kit';
import { computeRemoteRoutersUpdates, } from '@hyperlane-xyz/provider-sdk/warp';
import { assert, eqAddressSol, eqOptionalAddress, isZeroishAddress, } from '@hyperlane-xyz/utils';
import { u32le } from '../codecs/binary.js';
import { InterchainGasPaymasterTypeKind } from '../codecs/shared.js';
import { SYSTEM_PROGRAM_ADDRESS } from '../constants.js';
import { getProgramUpgradeAuthority } from '../deploy/program-deployer.js';
import { getSetUpgradeAuthorityInstruction } from '../instructions/loader.js';
import { getTokenEnrollRemoteRoutersInstruction, getTokenSetDestinationGasConfigsInstruction, getTokenSetInterchainGasPaymasterInstruction, getTokenSetInterchainSecurityModuleInstruction, getTokenTransferOwnershipInstruction, } from '../instructions/token.js';
import { DEFAULT_IGP_SALT } from '../hook/igp-hook.js';
import { deriveAtaPayerPda, deriveOverheadIgpAccountPda } from '../pda.js';
import { buildInstruction, writableAccount, writableSignerAddress, } from '../instructions/utils.js';
import { routerHexToBytes } from './warp-query.js';
/** Maximum local decimals allowed by the on-chain SVM token programs. */
const MAX_LOCAL_DECIMALS = 9;
/**
 * Asserts that `localDecimals` does not exceed the SVM maximum of 9.
 * The on-chain program stores decimals as u8 and the decimal-conversion math
 * overflows for values > 9. Mirrors the Rust client's `assert_decimals_max`.
 */
export function assertLocalDecimals(localDecimals) {
    assert(Number.isInteger(localDecimals) &&
        localDecimals >= 0 &&
        localDecimals <= MAX_LOCAL_DECIMALS, `Invalid decimals: ${localDecimals}. Must be an integer in [0, ${MAX_LOCAL_DECIMALS}]. Use scale/remoteDecimals for higher-precision remote chains.`);
}
/**
 * Derives `remoteDecimals` from `localDecimals` and the optional `scale` factor.
 * scale = 10^(remoteDecimals - localDecimals), so
 * remoteDecimals = localDecimals + log10(scale).
 * Falls back to localDecimals when scale is absent or 1.
 * Supports both upscaling (scale >= 1, e.g. 1e9) and downscaling
 * (scale < 1, e.g. 1e-4) to match the on-chain program which stores
 * decimals and remote_decimals as independent u8 values.
 * Asserts that scale is an exact power of 10.
 */
export function scaleToRemoteDecimals(localDecimals, scale) {
    if (scale == null || scale === 1)
        return localDecimals;
    assert(scale > 0, `scale must be positive, got ${scale}`);
    if (scale >= 1) {
        let exp = 0;
        let remaining = scale;
        while (remaining > 1 && remaining % 10 === 0) {
            remaining /= 10;
            exp++;
        }
        assert(remaining === 1, `scale must be an exact power of 10 (e.g. 1e9), got ${scale}`);
        return localDecimals + exp;
    }
    // Downscale: scale < 1 (e.g. 0.0001 = 1e-4).
    // Invert to integer and reuse the integer-only loop to avoid FP accumulation.
    const inverse = Math.round(1 / scale);
    let exp = 0;
    let remaining = inverse;
    while (remaining > 1 && remaining % 10 === 0) {
        remaining /= 10;
        exp++;
    }
    assert(remaining === 1, `scale must be an exact power of 10 (e.g. 1e-4), got ${scale}`);
    assert(localDecimals >= exp, `scale 1e-${exp} would produce negative remoteDecimals for localDecimals=${localDecimals}`);
    return localDecimals - exp;
}
/**
 * Derives the `scale` factor from on-chain `localDecimals` and `remoteDecimals`.
 * Returns undefined when there is no scaling (remoteDecimals === localDecimals).
 */
export function remoteDecimalsToScale(localDecimals, remoteDecimals) {
    const diff = remoteDecimals - localDecimals;
    return diff === 0 ? undefined : Math.pow(10, diff);
}
/**
 * Builds the TokenInitInstructionData shared by all warp token types.
 * The caller is responsible for passing the token-type-specific decimals.
 */
export async function buildBaseInitData(config, decimals, remoteDecimals) {
    const igpProgramId = config.hook?.deployed?.address;
    let interchainGasPaymaster = null;
    if (igpProgramId) {
        const programId = parseAddress(igpProgramId);
        const { address: overheadIgpPda } = await deriveOverheadIgpAccountPda(programId, DEFAULT_IGP_SALT);
        interchainGasPaymaster = {
            programId,
            igp: {
                kind: InterchainGasPaymasterTypeKind.OverheadIgp,
                account: overheadIgpPda,
            },
        };
    }
    return {
        mailbox: parseAddress(config.mailbox),
        interchainSecurityModule: config.interchainSecurityModule?.deployed?.address
            ? parseAddress(config.interchainSecurityModule.deployed.address)
            : null,
        interchainGasPaymaster,
        decimals,
        remoteDecimals,
    };
}
// Solana txs are capped at 1232 bytes (https://solana.com/docs/core/transactions#transaction-size).
// Each router entry is 37 bytes in the EnrollRemoteRouters instruction data.
// With ~322 bytes of fixed tx overhead the 1232-byte limit allows at most 24
// routers per tx. Use 20 to stay comfortably within that bound.
const MAX_ROUTERS_PER_TX = 20;
// Each gas config entry is 13 bytes in SetDestinationGasConfigs instruction
// data. The same fixed overhead allows at most 70 configs per tx. Use 60.
export const MAX_GAS_CONFIGS_PER_TX = 60;
/**
 * Sends post-init configuration (remote routers + destination gas) in
 * separate, batched transactions — matching the Rust CLI behaviour — to
 * stay within Solana's 1232-byte transaction size limit.
 *
 * Router enrollments and gas configs are issued as independent instruction
 * streams so each can use its full per-tx budget.
 */
export async function applyPostInitConfig(signer, programId, config) {
    const receipts = [];
    const routerEntries = Object.entries(config.remoteRouters);
    for (let i = 0; i < routerEntries.length; i += MAX_ROUTERS_PER_TX) {
        const batch = routerEntries.slice(i, i + MAX_ROUTERS_PER_TX);
        receipts.push(await signer.send({
            instructions: [
                await getTokenEnrollRemoteRoutersInstruction(programId, signer.signer.address, batch.map(([domain, router]) => ({
                    domain: parseInt(domain),
                    router: routerHexToBytes(router.address),
                }))),
            ],
        }));
    }
    const gasEntries = Object.entries(config.destinationGas);
    for (let i = 0; i < gasEntries.length; i += MAX_GAS_CONFIGS_PER_TX) {
        const batch = gasEntries.slice(i, i + MAX_GAS_CONFIGS_PER_TX);
        receipts.push(await signer.send({
            instructions: [
                await getTokenSetDestinationGasConfigsInstruction(programId, signer.signer.address, batch.map(([domain, gas]) => ({
                    domain: parseInt(domain),
                    gas: BigInt(gas),
                }))),
            ],
        }));
    }
    return receipts;
}
/**
 * Returns a System Program transfer instruction to top up the ATA payer PDA
 * to at least `targetLamports`, or undefined if already sufficiently funded.
 * For synthetic and collateral tokens the ATA payer funds recipient ATA
 * creation on transfer_out.
 */
export async function buildFundAtaPayerInstruction(rpc, payer, programId, targetLamports) {
    const { address: ataPayerPda } = await deriveAtaPayerPda(programId);
    const balance = await rpc.getBalance(ataPayerPda).send();
    const current = BigInt(balance.value);
    if (current >= targetLamports)
        return undefined;
    const topUp = targetLamports - current;
    const data = new Uint8Array(12);
    data.set(u32le(2), 0); // SystemProgram::Transfer discriminator (u32 LE)
    new DataView(data.buffer).setBigUint64(4, topUp, true); // lamports, LE
    return buildInstruction(SYSTEM_PROGRAM_ADDRESS, [writableSignerAddress(payer), writableAccount(ataPayerPda)], data);
}
/**
 * Diffs current vs expected config and returns the minimal set of annotated
 * update transactions needed to reconcile them, batched to stay within
 * Solana's 1232-byte transaction size limit.
 *
 * Transaction grouping:
 *   1. ISM + IGP hook changes (combined, both are small)
 *   2. Router unenroll/enroll batches (MAX_ROUTERS_PER_TX each)
 *   3. Gas config unenroll/enroll batches (MAX_GAS_CONFIGS_PER_TX each)
 *   4. Ownership transfer (always last, own tx)
 *
 * @param label - Prefix used for transaction annotations,
 *                e.g. "native token <programId>"
 */
export async function computeWarpTokenUpdateInstructions(current, expected, programId, ownerAddress, rpc, label) {
    const txs = [];
    // 1. ISM + IGP hook — combined into a single tx (both are small instructions)
    const configInstructions = [];
    const currentIsm = current.interchainSecurityModule?.deployed?.address;
    const expectedIsm = expected.interchainSecurityModule?.deployed?.address;
    if (!eqOptionalAddress(currentIsm, expectedIsm, eqAddressSol)) {
        configInstructions.push(await getTokenSetInterchainSecurityModuleInstruction(programId, ownerAddress, expectedIsm ? parseAddress(expectedIsm) : null));
    }
    const currentHook = current.hook?.deployed?.address;
    const expectedHook = expected.hook?.deployed?.address;
    if (!eqOptionalAddress(currentHook, expectedHook, eqAddressSol)) {
        let igpValue = null;
        if (expectedHook) {
            const expectedHookAddress = parseAddress(expectedHook);
            const { address: overheadIgpPda } = await deriveOverheadIgpAccountPda(expectedHookAddress, DEFAULT_IGP_SALT);
            igpValue = [
                expectedHookAddress,
                {
                    kind: InterchainGasPaymasterTypeKind.OverheadIgp,
                    account: overheadIgpPda,
                },
            ];
        }
        configInstructions.push(await getTokenSetInterchainGasPaymasterInstruction(programId, ownerAddress, igpValue));
    }
    if (configInstructions.length > 0) {
        txs.push({
            feePayer: ownerAddress,
            instructions: configInstructions,
            annotation: `Update ${label}: ISM/hook config`,
        });
    }
    // 2. Router diff — the same domain set (toUnenroll/toEnroll) drives both
    //    router and gas config changes, batched independently because gas config
    //    instructions are smaller and allow more entries per tx.
    const diff = computeRemoteRoutersUpdates({
        remoteRouters: current.remoteRouters,
        destinationGas: current.destinationGas,
    }, {
        remoteRouters: expected.remoteRouters,
        destinationGas: expected.destinationGas,
    }, eqAddressSol);
    const unenrollRouterBatches = Math.ceil(diff.toUnenroll.length / MAX_ROUTERS_PER_TX);
    for (let i = 0; i < diff.toUnenroll.length; i += MAX_ROUTERS_PER_TX) {
        const batch = diff.toUnenroll.slice(i, i + MAX_ROUTERS_PER_TX);
        const batchNum = i / MAX_ROUTERS_PER_TX + 1;
        txs.push({
            feePayer: ownerAddress,
            instructions: [
                await getTokenEnrollRemoteRoutersInstruction(programId, ownerAddress, batch.map((domain) => ({ domain, router: null }))),
            ],
            annotation: `Update ${label}: unenroll routers${unenrollRouterBatches > 1 ? ` (${batchNum}/${unenrollRouterBatches})` : ''}`,
        });
    }
    const unenrollGasBatches = Math.ceil(diff.toUnenroll.length / MAX_GAS_CONFIGS_PER_TX);
    for (let i = 0; i < diff.toUnenroll.length; i += MAX_GAS_CONFIGS_PER_TX) {
        const batch = diff.toUnenroll.slice(i, i + MAX_GAS_CONFIGS_PER_TX);
        const batchNum = i / MAX_GAS_CONFIGS_PER_TX + 1;
        txs.push({
            feePayer: ownerAddress,
            instructions: [
                await getTokenSetDestinationGasConfigsInstruction(programId, ownerAddress, batch.map((domain) => ({ domain, gas: null }))),
            ],
            annotation: `Update ${label}: unenroll gas configs${unenrollGasBatches > 1 ? ` (${batchNum}/${unenrollGasBatches})` : ''}`,
        });
    }
    const enrollRouterBatches = Math.ceil(diff.toEnroll.length / MAX_ROUTERS_PER_TX);
    for (let i = 0; i < diff.toEnroll.length; i += MAX_ROUTERS_PER_TX) {
        const batch = diff.toEnroll.slice(i, i + MAX_ROUTERS_PER_TX);
        const batchNum = i / MAX_ROUTERS_PER_TX + 1;
        txs.push({
            feePayer: ownerAddress,
            instructions: [
                await getTokenEnrollRemoteRoutersInstruction(programId, ownerAddress, batch.map((e) => ({
                    domain: e.domainId,
                    router: routerHexToBytes(e.routerAddress),
                }))),
            ],
            annotation: `Update ${label}: enroll routers${enrollRouterBatches > 1 ? ` (${batchNum}/${enrollRouterBatches})` : ''}`,
        });
    }
    const enrollGasBatches = Math.ceil(diff.toEnroll.length / MAX_GAS_CONFIGS_PER_TX);
    for (let i = 0; i < diff.toEnroll.length; i += MAX_GAS_CONFIGS_PER_TX) {
        const batch = diff.toEnroll.slice(i, i + MAX_GAS_CONFIGS_PER_TX);
        const batchNum = i / MAX_GAS_CONFIGS_PER_TX + 1;
        txs.push({
            feePayer: ownerAddress,
            instructions: [
                await getTokenSetDestinationGasConfigsInstruction(programId, ownerAddress, batch.map((e) => ({
                    domain: e.domainId,
                    gas: BigInt(e.gas),
                }))),
            ],
            annotation: `Update ${label}: enroll gas configs${enrollGasBatches > 1 ? ` (${batchNum}/${enrollGasBatches})` : ''}`,
        });
    }
    const expectedOwnerAddress = expected.owner && !isZeroishAddress(expected.owner) ? expected.owner : null;
    // 3. Ownership change — own tx
    if (!eqOptionalAddress(current.owner, expected.owner, eqAddressSol)) {
        txs.push({
            feePayer: ownerAddress,
            instructions: [
                await getTokenTransferOwnershipInstruction(programId, ownerAddress, expectedOwnerAddress ? parseAddress(expectedOwnerAddress) : null),
            ],
            annotation: `Update ${label}: transfer ownership`,
        });
    }
    // 4. Upgrade authority — always last tx.
    // Skip when the program is immutable (no current authority).
    const currentUpgradeAuthority = await getProgramUpgradeAuthority(rpc, programId);
    if (currentUpgradeAuthority &&
        !eqOptionalAddress(currentUpgradeAuthority, expectedOwnerAddress ?? undefined, eqAddressSol)) {
        txs.push({
            feePayer: currentUpgradeAuthority,
            instructions: [
                await getSetUpgradeAuthorityInstruction(programId, currentUpgradeAuthority, expectedOwnerAddress ? parseAddress(expectedOwnerAddress) : null),
            ],
            annotation: `Update ${label}: ${expectedOwnerAddress ? 'transfer' : 'renounce'} upgrade authority`,
        });
    }
    return txs;
}
//# sourceMappingURL=warp-tx.js.map