import { addSignersToTransactionMessage, assertIsSignature, createKeyPairSignerFromBytes, createKeyPairSignerFromPrivateKeyBytes, getBase58Encoder, getBase64EncodedWireTransaction, getSignatureFromTransaction, signTransactionMessageWithSigners, } from '@solana/kit';
import { isSolanaError, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND, } from '@solana/errors';
import { assert, rootLogger, sleep, strip0x } from '@hyperlane-xyz/utils';
import { convertLegacySolanaTransaction, isLegacySolanaTransaction, } from '../legacy-compat.js';
import { createRpc } from '../rpc.js';
import { buildTransactionMessage, serializeUnsignedTransaction, } from '../tx.js';
import { SvmProvider } from './provider.js';
import { DEFAULT_COMPUTE_UNITS } from '../constants.js';
class SvmTransactionError extends Error {
    constructor(signature, cause) {
        super(`Transaction failed: ${signature}, err: ${JSON.stringify(cause, (_k, v) => (typeof v === 'bigint' ? v.toString() : v))}`);
        this.name = 'SvmTransactionError';
        this.cause = cause;
    }
}
const base58Encoder = getBase58Encoder();
function parseKeyBytes(privateKey) {
    // Try hex (32 bytes = 64 hex chars, 64 bytes = 128 hex chars)
    const stripped = strip0x(privateKey);
    if (/^[0-9a-fA-F]{64}$/.test(stripped)) {
        return new Uint8Array(Buffer.from(stripped, 'hex'));
    }
    if (/^[0-9a-fA-F]{128}$/.test(stripped)) {
        return new Uint8Array(Buffer.from(stripped, 'hex'));
    }
    // Try base58
    let keyBytes;
    try {
        keyBytes = base58Encoder.encode(privateKey);
    }
    catch (err) {
        throw new Error(`Failed to parse private key. Expected hex (64 or 128 chars) or base58. ` +
            `Base58 error: ${err instanceof Error ? err.message : String(err)}`);
    }
    if (keyBytes.length !== 32 && keyBytes.length !== 64) {
        throw new Error(`Base58-decoded key has invalid length: ${keyBytes.length}. Expected 32 (private key) or 64 (keypair).`);
    }
    return keyBytes;
}
const RPC_COMMITMENT_LEVEL = 'confirmed';
/**
 * Detects blockhash-not-found errors from sendTransaction.
 * The RPC wraps it as a preflight failure (-32002) with the
 * BlockhashNotFound error as its cause.
 */
function isBlockhashNotFoundError(error) {
    if (isSolanaError(error, SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND)) {
        return true;
    }
    if (isSolanaError(error, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) {
        return isSolanaError(error.cause, SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND);
    }
    return false;
}
export class SvmSigner extends SvmProvider {
    signer;
    logger = rootLogger.child({ module: 'SvmSigner' });
    constructor(rpc, rpcUrls, signer) {
        super(rpc, rpcUrls);
        this.signer = signer;
    }
    static async connectWithSigner(rpcUrls, privateKey, _extraParams) {
        assert(rpcUrls.length > 0, 'At least one RPC URL is required');
        const rpc = createRpc(rpcUrls[0]);
        const keyBytes = parseKeyBytes(privateKey);
        let keypair;
        if (keyBytes.length === 32) {
            keypair = await createKeyPairSignerFromPrivateKeyBytes(keyBytes);
        }
        else if (keyBytes.length === 64) {
            keypair = await createKeyPairSignerFromBytes(keyBytes);
        }
        else {
            throw new Error(`Invalid key length: ${keyBytes.length}. Expected 32 (private key) or 64 (keypair).`);
        }
        return new SvmSigner(rpc, rpcUrls, keypair);
    }
    getSignerAddress() {
        return this.signer.address;
    }
    supportsTransactionBatching() {
        return false;
    }
    async transactionToPrintableJson(transaction) {
        const { transactionBase58, messageBase58 } = serializeUnsignedTransaction(transaction.instructions, transaction.feePayer ?? this.signer.address);
        return {
            annotation: transaction.annotation,
            instructions: transaction.instructions.map((ix) => ({
                programAddress: ix.programAddress,
                accounts: ix.accounts,
                data: ix.data ? Buffer.from(ix.data).toString('hex') : undefined,
            })),
            computeUnits: transaction.computeUnits,
            transaction_base58: transactionBase58,
            message_base58: messageBase58,
        };
    }
    /**
     * Builds, signs, and sends a transaction with a confirmed blockhash.
     * Retries on blockhash-not-found errors with backoff to handle
     * load-balanced RPC node desync.
     */
    async signAndSend(tx, maxAttempts = 5) {
        for (let attempt = 0; attempt < maxAttempts; attempt++) {
            const { value: latestBlockhash } = await this.rpc
                .getLatestBlockhash({ commitment: RPC_COMMITMENT_LEVEL })
                .send();
            let txMessage = buildTransactionMessage({
                instructions: tx.instructions,
                feePayer: this.signer,
                recentBlockhash: latestBlockhash.blockhash,
                lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
                computeUnits: tx.computeUnits ?? DEFAULT_COMPUTE_UNITS,
            });
            if (tx.additionalSigners?.length) {
                txMessage = addSignersToTransactionMessage(tx.additionalSigners, txMessage);
            }
            const signedTx = await signTransactionMessageWithSigners(txMessage);
            const signature = getSignatureFromTransaction(signedTx);
            try {
                const rawTx = getBase64EncodedWireTransaction(signedTx);
                await this.rpc
                    .sendTransaction(rawTx, {
                    encoding: 'base64',
                    skipPreflight: tx.skipPreflight ?? false,
                    // Set to 0 to avoid conflicts between the rpc/provider
                    // retrying and the signer as it tracks and retries
                    // pending txs manually
                    maxRetries: 0n,
                    preflightCommitment: RPC_COMMITMENT_LEVEL,
                })
                    .send();
                return {
                    signature,
                    rawTx,
                    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
                };
            }
            catch (error) {
                if (isBlockhashNotFoundError(error) && attempt < maxAttempts - 1) {
                    const delay = 500 * (attempt + 1);
                    this.logger.debug(`Blockhash not found on attempt ${attempt + 1}, retrying in ${delay}ms`);
                    await sleep(delay);
                    continue;
                }
                throw error;
            }
        }
        // Unreachable: the loop always either returns or throws
        throw new Error('signAndSend: unreachable');
    }
    /**
     * Evaluates a signature status result. Throws on error, returns
     * `{ confirmed: true, slot }` if confirmed/finalized, `{ confirmed: false }`
     * if still progressing, or `null` if the signature was not found.
     */
    checkSignatureResult(signature, result) {
        if (!result)
            return null;
        if (result.err) {
            throw new SvmTransactionError(signature, result.err);
        }
        if (result.confirmationStatus === RPC_COMMITMENT_LEVEL ||
            result.confirmationStatus === 'finalized') {
            return { confirmed: true, slot: result.slot };
        }
        return { confirmed: false };
    }
    /**
     * Sends a transaction and polls for confirmation. On blockhash expiry,
     * checks transaction history before resubmitting to prevent double-execution.
     */
    async send(tx) {
        const maxBlockhashAttempts = 3;
        const pollIntervalMs = 2000;
        let lastSignature;
        for (let blockhashAttempt = 0; blockhashAttempt < maxBlockhashAttempts; blockhashAttempt++) {
            const { signature, rawTx, lastValidBlockHeight } = await this.signAndSend(tx);
            lastSignature = signature;
            // Poll while blockhash is valid
            const result = await this.pollForConfirmation(signature, rawTx, lastValidBlockHeight, pollIntervalMs);
            if (result) {
                return result;
            }
            // Blockhash expired — check history before resubmitting
            let historyCheck = null;
            try {
                const historyStatus = await this.rpc
                    .getSignatureStatuses([signature], {
                    searchTransactionHistory: true,
                })
                    .send();
                historyCheck = this.checkSignatureResult(signature, historyStatus.value[0]);
            }
            catch (error) {
                if (error instanceof SvmTransactionError)
                    throw error;
                throw new Error(`Cannot safely resubmit: history lookup failed for ${signature}, aborting to prevent double-execution.`, { cause: error });
            }
            if (historyCheck?.confirmed) {
                return { signature, slot: historyCheck.slot };
            }
            if (historyCheck) {
                // Tx still progressing (e.g. 'processed') — keep polling same
                // signature with a fresh block height ceiling
                const { value: freshBlockhash } = await this.rpc
                    .getLatestBlockhash({ commitment: RPC_COMMITMENT_LEVEL })
                    .send();
                const retry = await this.pollForConfirmation(signature, rawTx, freshBlockhash.lastValidBlockHeight, pollIntervalMs);
                if (retry)
                    return retry;
                throw new Error(`Transaction ${signature} was observed at 'processed' but never confirmed`);
            }
            if (blockhashAttempt < maxBlockhashAttempts - 1) {
                this.logger.debug(`Blockhash expired and tx not found, resubmitting (attempt ${blockhashAttempt + 2}/${maxBlockhashAttempts})`);
            }
        }
        throw new Error(`Transaction not confirmed after ${maxBlockhashAttempts} blockhash attempts (last signature: ${lastSignature})`);
    }
    /**
     * Polls getSignatureStatuses while the blockhash is still valid.
     * Rebroadcasts the same signed tx periodically (fire-and-forget).
     * Returns the receipt if confirmed, or null if the blockhash expired.
     */
    async pollForConfirmation(signature, rawTx, lastValidBlockHeight, pollIntervalMs) {
        const maxBlockHeightFailures = 3;
        const wallClockDeadline = Date.now() + 2 * 60 * 1000;
        let blockHeightFailures = 0;
        let delay = Math.max(Math.floor(pollIntervalMs / 4), 250);
        while (Date.now() < wallClockDeadline) {
            await sleep(delay);
            delay = Math.min(Math.floor(delay * 1.5), pollIntervalMs);
            // Check confirmation status
            try {
                const status = await this.rpc.getSignatureStatuses([signature]).send();
                const check = this.checkSignatureResult(signature, status.value[0]);
                if (check?.confirmed) {
                    return { signature, slot: check.slot };
                }
                // Transaction seen (e.g. 'processed') — keep polling, don't rebroadcast
                if (check)
                    continue;
            }
            catch (error) {
                if (error instanceof SvmTransactionError)
                    throw error;
                this.logger.debug('Status poll failed', { error });
            }
            // Check if blockhash expired
            try {
                const currentBlockHeight = await this.rpc
                    .getBlockHeight({ commitment: RPC_COMMITMENT_LEVEL })
                    .send();
                // Reset on successful fetch
                blockHeightFailures = 0;
                if (currentBlockHeight > lastValidBlockHeight) {
                    return null;
                }
            }
            catch (error) {
                blockHeightFailures++;
                this.logger.debug('Block height check failed', { error });
                if (blockHeightFailures >= maxBlockHeightFailures) {
                    this.logger.warn(`Block height check failed ${maxBlockHeightFailures} times, treating as expired`);
                    return null;
                }
            }
            // Rebroadcast same signed tx (fire-and-forget, always skip preflight)
            try {
                await this.rpc
                    .sendTransaction(rawTx, {
                    encoding: 'base64',
                    skipPreflight: true,
                    maxRetries: 0n,
                })
                    .send();
            }
            catch (error) {
                this.logger.debug('Rebroadcast failed', { error, signature });
            }
        }
        // Wall-clock timeout exceeded — tx fate unknown, unsafe to resubmit
        throw new Error(`Poll timeout exceeded for ${signature}, aborting to prevent potential double-execution`);
    }
    async sendAndConfirmTransaction(transaction) {
        const tx = isLegacySolanaTransaction(transaction)
            ? await convertLegacySolanaTransaction(transaction, transaction.extraSigners)
            : transaction;
        const receipt = await this.send(tx);
        return this.fetchTransactionMeta(receipt);
    }
    /**
     * Fetches full transaction to populate meta (including logs) on the receipt.
     * Retries with backoff since RPC indexing can lag behind confirmation.
     * // TODO: make meta-fetch opt-in once the legacy compat layer is removed
     */
    async fetchTransactionMeta(receipt, maxRetries = 5) {
        assertIsSignature(receipt.signature);
        for (let attempt = 0; attempt < maxRetries; attempt++) {
            try {
                const fullTx = await this.rpc
                    .getTransaction(receipt.signature, {
                    commitment: RPC_COMMITMENT_LEVEL,
                    maxSupportedTransactionVersion: 0,
                    encoding: 'jsonParsed',
                })
                    .send();
                if (fullTx?.meta?.logMessages) {
                    return {
                        ...receipt,
                        meta: { logMessages: fullTx.meta.logMessages },
                    };
                }
            }
            catch (error) {
                this.logger.debug(`Attempt ${attempt + 1} failed to fetch tx meta`, {
                    error,
                });
            }
            if (attempt < maxRetries - 1) {
                await sleep(1000 * (attempt + 1));
            }
        }
        this.logger.warn('Transaction meta unavailable after retries', {
            signature: receipt.signature,
        });
        return receipt;
    }
    async sendAndConfirmBatchTransactions(_transactions) {
        throw new Error('Sealevel does not support transaction batching');
    }
}
//# sourceMappingURL=signer.js.map