import { address as parseAddress } from '@solana/kit';
import { keccak_256 } from '@noble/hashes/sha3';
import { HookType } from '@hyperlane-xyz/provider-sdk/altvm';
import { ArtifactState, } from '@hyperlane-xyz/provider-sdk/artifact';
import { assert, ZERO_ADDRESS_HEX_32 } from '@hyperlane-xyz/utils';
import { resolveProgram } from '../deploy/resolve-program.js';
import { getInitIgpInstruction, getInitIgpProgramInstruction, getInitOverheadIgpInstruction, getSetDestinationGasOverheadsInstruction, getSetGasOracleConfigsInstruction, } from '../instructions/igp.js';
import { deriveIgpAccountPda, deriveOverheadIgpAccountPda } from '../pda.js';
import { fetchIgpAccount, fetchIgpProgramData, fetchOverheadIgpAccount, remoteGasDataToConfig, } from './hook-query.js';
export function deriveIgpSalt(context) {
    return keccak_256(new TextEncoder().encode(context));
}
/** Zero salt — matches mainnet IGP deployments (H256::zero() in Rust). */
export const DEFAULT_IGP_SALT = new Uint8Array(32);
export class SvmIgpHookReader {
    rpc;
    salt;
    constructor(rpc, salt) {
        this.rpc = rpc;
        this.salt = salt;
    }
    async read(address) {
        const programId = parseAddress(address);
        const igp = await fetchIgpAccount(this.rpc, programId, this.salt);
        if (!igp) {
            throw new Error(`IGP account not found for program: ${programId}`);
        }
        const overheadIgp = await fetchOverheadIgpAccount(this.rpc, programId, this.salt);
        const oracleConfig = {};
        for (const [domain, oracle] of igp.gasOracles.entries()) {
            oracleConfig[domain] = remoteGasDataToConfig(oracle);
        }
        const overhead = {};
        if (overheadIgp) {
            for (const [domain, gas] of overheadIgp.gasOverheads.entries()) {
                overhead[domain] = Number(gas);
            }
        }
        const owner = igp.owner;
        const beneficiary = igp.beneficiary;
        const { address: igpPda } = await deriveIgpAccountPda(programId, this.salt);
        const { address: overheadIgpPda } = await deriveOverheadIgpAccountPda(programId, this.salt);
        return {
            artifactState: ArtifactState.DEPLOYED,
            config: {
                type: HookType.INTERCHAIN_GAS_PAYMASTER,
                // FIXME: address provider sdk type in a separate pr
                owner: owner ?? ZERO_ADDRESS_HEX_32,
                beneficiary,
                // FIXME: address provider sdk type in a separate pr
                oracleKey: owner ?? ZERO_ADDRESS_HEX_32,
                overhead,
                oracleConfig,
            },
            deployed: {
                address: programId,
                programId,
                igpPda,
                overheadIgpPda: overheadIgp ? overheadIgpPda : undefined,
            },
        };
    }
}
export class SvmIgpHookWriter extends SvmIgpHookReader {
    config;
    svmSigner;
    constructor(config, rpc, salt, svmSigner) {
        super(rpc, salt);
        this.config = config;
        this.svmSigner = svmSigner;
    }
    async create(artifact) {
        const config = artifact.config;
        const { programAddress: programId, receipts } = await resolveProgram(this.config.program, this.svmSigner, this.rpc);
        // Phase 1: Initialize program data PDA if not yet created
        const programData = await fetchIgpProgramData(this.rpc, programId);
        if (!programData) {
            const initProgramIx = await getInitIgpProgramInstruction(programId, this.svmSigner.signer);
            const initProgramReceipt = await this.svmSigner.send({
                instructions: [initProgramIx],
                skipPreflight: true,
            });
            receipts.push(initProgramReceipt);
        }
        // Phase 2: Initialize specific IGP account
        let igp = await fetchIgpAccount(this.rpc, programId, this.salt);
        if (!igp) {
            const initIgpIx = await getInitIgpInstruction(programId, this.svmSigner.signer, {
                salt: this.salt,
                owner: config.owner ? parseAddress(config.owner) : null,
                beneficiary: parseAddress(config.beneficiary),
            });
            const initReceipt = await this.svmSigner.send({
                instructions: [initIgpIx],
                skipPreflight: true,
            });
            receipts.push(initReceipt);
            igp = await fetchIgpAccount(this.rpc, programId, this.salt);
            assert(igp, 'IGP account not found after init');
        }
        const { address: igpPda } = await deriveIgpAccountPda(programId, this.salt);
        const overheadIgp = await fetchOverheadIgpAccount(this.rpc, programId, this.salt);
        let overheadIgpPda;
        if (!overheadIgp && Object.keys(config.overhead).length > 0) {
            const initOverheadIx = await getInitOverheadIgpInstruction(programId, this.svmSigner.signer, {
                salt: this.salt,
                owner: config.owner ? parseAddress(config.owner) : null,
                inner: igpPda,
            });
            const initOverheadReceipt = await this.svmSigner.send({
                instructions: [initOverheadIx],
                skipPreflight: true,
            });
            receipts.push(initOverheadReceipt);
        }
        const oracleConfigs = Object.entries(config.oracleConfig).map(([domainStr, oracleData]) => {
            const domain = Number(domainStr);
            assert(Number.isInteger(domain) && domain >= 0, `Invalid domain: '${domainStr}'`);
            return {
                domain,
                gasOracle: {
                    kind: 0,
                    value: {
                        gasPrice: BigInt(oracleData.gasPrice),
                        tokenExchangeRate: BigInt(oracleData.tokenExchangeRate),
                        tokenDecimals: oracleData.tokenDecimals ?? 9,
                    },
                },
            };
        });
        if (oracleConfigs.length > 0) {
            const setOracleIx = await getSetGasOracleConfigsInstruction(programId, this.svmSigner.signer.address, igpPda, oracleConfigs);
            const oracleReceipt = await this.svmSigner.send({
                instructions: [setOracleIx],
            });
            receipts.push(oracleReceipt);
        }
        const overheadConfigs = Object.entries(config.overhead).map(([domainStr, gas]) => {
            const domain = Number(domainStr);
            assert(Number.isInteger(domain) && domain >= 0, `Invalid domain: '${domainStr}'`);
            return {
                destinationDomain: domain,
                gasOverhead: BigInt(gas),
            };
        });
        if (overheadConfigs.length > 0) {
            const derivedOverheadPda = await deriveOverheadIgpAccountPda(programId, this.salt);
            overheadIgpPda = derivedOverheadPda.address;
            const setOverheadIx = await getSetDestinationGasOverheadsInstruction(programId, this.svmSigner.signer.address, overheadIgpPda, overheadConfigs);
            const overheadReceipt = await this.svmSigner.send({
                instructions: [setOverheadIx],
            });
            receipts.push(overheadReceipt);
        }
        return [
            {
                artifactState: ArtifactState.DEPLOYED,
                config: config,
                deployed: {
                    address: programId,
                    programId,
                    igpPda,
                    overheadIgpPda,
                },
            },
            receipts,
        ];
    }
    async update(artifact) {
        const txs = [];
        const config = artifact.config;
        const programId = artifact.deployed.programId;
        const currentIgp = await fetchIgpAccount(this.rpc, programId, this.salt);
        if (!currentIgp) {
            throw new Error('IGP account not initialized');
        }
        assert(currentIgp.owner, `IGP ${programId} has no owner`);
        const ownerAddress = currentIgp.owner;
        const { address: igpPda } = await deriveIgpAccountPda(programId, this.salt);
        const oracleConfigsToUpdate = [];
        for (const [domainStr, oracleData] of Object.entries(config.oracleConfig)) {
            const domain = Number(domainStr);
            assert(Number.isInteger(domain) && domain >= 0, `Invalid domain: '${domainStr}'`);
            const existingOracle = currentIgp.gasOracles.get(domain);
            const newGasPrice = BigInt(oracleData.gasPrice);
            const newTokenExchangeRate = BigInt(oracleData.tokenExchangeRate);
            const newTokenDecimals = oracleData.tokenDecimals ?? 9;
            let needsUpdate = false;
            if (!existingOracle) {
                needsUpdate = true;
            }
            else {
                const existing = existingOracle.value;
                if (existing.gasPrice !== newGasPrice ||
                    existing.tokenExchangeRate !== newTokenExchangeRate ||
                    existing.tokenDecimals !== newTokenDecimals) {
                    needsUpdate = true;
                }
            }
            if (needsUpdate) {
                oracleConfigsToUpdate.push({
                    domain,
                    gasOracle: {
                        kind: 0,
                        value: {
                            gasPrice: newGasPrice,
                            tokenExchangeRate: newTokenExchangeRate,
                            tokenDecimals: newTokenDecimals,
                        },
                    },
                });
            }
        }
        if (oracleConfigsToUpdate.length > 0) {
            const setOracleIx = await getSetGasOracleConfigsInstruction(programId, ownerAddress, igpPda, oracleConfigsToUpdate);
            txs.push({
                feePayer: ownerAddress,
                instructions: [setOracleIx],
                annotation: `Update gas oracles for ${oracleConfigsToUpdate.length} domains`,
            });
        }
        const currentOverheadIgp = await fetchOverheadIgpAccount(this.rpc, programId, this.salt);
        if (!currentOverheadIgp) {
            throw new Error('Overhead IGP account not initialized');
        }
        const overheadConfigsToUpdate = [];
        for (const [domainStr, gas] of Object.entries(config.overhead)) {
            const domain = Number(domainStr);
            assert(Number.isInteger(domain) && domain >= 0, `Invalid domain: '${domainStr}'`);
            const existingOverhead = currentOverheadIgp?.gasOverheads.get(domain);
            const newOverhead = BigInt(gas);
            if (!existingOverhead || existingOverhead !== newOverhead) {
                overheadConfigsToUpdate.push({
                    destinationDomain: domain,
                    gasOverhead: newOverhead,
                });
            }
        }
        if (overheadConfigsToUpdate.length > 0) {
            const { address: overheadIgpPda } = await deriveOverheadIgpAccountPda(programId, this.salt);
            const setOverheadIx = await getSetDestinationGasOverheadsInstruction(programId, ownerAddress, overheadIgpPda, overheadConfigsToUpdate);
            txs.push({
                feePayer: ownerAddress,
                instructions: [setOverheadIx],
                annotation: `Update gas overheads for ${overheadConfigsToUpdate.length} domains`,
            });
        }
        return txs;
    }
}
//# sourceMappingURL=igp-hook.js.map