import { Address, Chain, Hex, PublicClient, WalletClient } from 'viem'; import { BitcoinWallet, Hash } from '../../../shared/wallets'; import { WotsBlockPublicKey } from '../clients/vault-provider/types'; import { DepositTerms } from '../deposit-terms'; import { Network } from '../primitives'; import { UTXO } from '../utils'; /** * Configuration for the PeginManager. */ export interface PeginManagerConfig { /** * Bitcoin network to use for transactions. */ btcNetwork: Network; /** * Bitcoin wallet for signing peg-in transactions. */ btcWallet: BitcoinWallet; /** * Ethereum wallet for registering peg-in on-chain. * Uses viem's WalletClient directly for proper gas estimation. */ ethWallet: WalletClient; /** * Ethereum chain configuration. * Required for proper gas estimation in contract calls. */ ethChain: Chain; /** * Public client used for read calls (`readContract`, `estimateGas`, * `waitForTransactionReceipt`). Pass a client configured with the * caller's RPC URL so reads hit the same endpoint as the rest of the * application instead of viem's stock chain default. */ publicClient: PublicClient; /** * Vault contract addresses. */ vaultContracts: { /** * BTCVaultRegistry contract address on Ethereum. */ btcVaultRegistry: Address; }; /** * Mempool API URL for fetching UTXO data and broadcasting transactions. * Use MEMPOOL_API_URLS constant for standard mempool.space URLs, or provide * a custom URL if running your own mempool instance. */ mempoolApiUrl: string; } /** * Parameters for the pegin flow (pre-pegin + pegin transactions). */ export interface PreparePeginParams { /** * Vault core (tx-graph) version to build — the contract's * `ProtocolParams.activeVaultCoreVersion()` at build time. Stamped onto * the vault at registration; every Pre-PegIn/PegIn artifact this manager * constructs derives from this graph version. */ vaultCoreVersion: number; /** * Amounts to peg in per HTLC (in satoshis). * Must have the same length as `hashlocks`. * For single deposits, pass a single-element array. */ amounts: readonly bigint[]; /** * Vault provider's BTC public key (x-only, 64-char hex). * Can be provided with or without "0x" prefix (will be stripped automatically). */ vaultProviderBtcPubkey: string; /** * VP commission quoted for this deposit (bps). Capped to the approval * ceiling before it sizes the terms' commissionFee, so the user approves * the most the VP can take — not the quote. */ commissionBps: number; /** * Vault keeper BTC public keys (x-only, 64-char hex). * Can be provided with or without "0x" prefix (will be stripped automatically). */ vaultKeeperBtcPubkeys: readonly string[]; /** * Universal challenger BTC public keys (x-only, 64-char hex). * Can be provided with or without "0x" prefix (will be stripped automatically). */ universalChallengerBtcPubkeys: readonly string[]; /** * CSV timelock in blocks for the PegIn vault output. */ timelockPegin: number; /** * btc-vault `timelock_assert` (t2) — the Assert:0 payout-leaf CSV. Carried * into DepositTerms as its own field. Production collapses the two: the SDK * derives timelockPegin from the same on-chain timelockAssert * (`protocol-params-reader.ts` deriveTimelockPegin), mirroring vaultd * (`pegin_validation.rs`). The terms never assume that identity. */ timelockAssert: number; /** * CSV timelock in blocks for the Pre-PegIn HTLC refund path. */ timelockRefund: number; /** * TX-graph fee rate in sat/vB from the contract offchain params. * Used by WASM to size the depositor claim value (graph transactions). */ protocolFeeRate: bigint; /** * Minimum PegIn fee rate in sat/vB from the contract offchain params. * Used by WASM to size the PegIn transaction fee. */ minPeginFeeRate: bigint; /** * Mempool fee rate in sat/vB for funding the Pre-PegIn transaction. * Used for UTXO selection and change calculation. */ mempoolFeeRate: number; /** * M in M-of-N council multisig (from contract params). */ councilQuorum: number; /** * N in M-of-N council multisig (from contract params). */ councilSize: number; /** * Available UTXOs from the depositor's wallet for funding the Pre-PegIn transaction. */ availableUTXOs: readonly UTXO[]; /** * Bitcoin address for receiving change from the Pre-PegIn transaction. */ changeAddress: string; } /** * Result of preparing a pegin. */ /** Per-vault PegIn data derived from a shared Pre-PegIn transaction */ export interface PerVaultPeginData { /** Index of the HTLC output in the Pre-PegIn transaction (0, 1, 2, ...) */ htlcVout: number; /** HTLC output value in satoshis */ htlcValue: bigint; /** Depositor-signed PegIn transaction hex (for contract registration) */ peginTxHex: string; /** PegIn transaction ID */ peginTxid: string; /** Depositor's Schnorr signature over PegIn input (HTLC leaf 0) */ peginInputSignature: string; /** Vault output scriptPubKey hex */ vaultScriptPubKey: string; } /** * Broadcast-ready transaction output of {@link PeginManager.preparePegin}. * Safe to log / persist — contains no sensitive material. */ export interface PreparePeginTransaction { /** * Funded, pre-witness Pre-PegIn tx hex. Pass this for register calls' * `unsignedPrePeginTx` — despite the contract-side name, the registry * stores the funded form so indexers can rebuild refund PSBTs. */ fundedPrePeginTxHex: string; /** Funded Pre-PegIn transaction ID */ prePeginTxid: string; /** Per-vault PegIn data — one entry per amount */ perVault: PerVaultPeginData[]; /** UTXOs selected to fund the Pre-PegIn transaction */ selectedUTXOs: UTXO[]; /** Transaction fee in satoshis */ fee: bigint; /** Change amount in satoshis (if any) */ changeAmount: bigint; } /** * Sensitive material derived from the wallet root. Do not log; do not * persist beyond the activation flow. Strings are immutable in JS, so * lifetime is GC-only — secrets stay live until the result is dropped. */ export interface PreparePeginDerivedSecrets { /** Per-vault WOTS block public keys (one array per vault). */ perVaultWotsKeys: WotsBlockPublicKey[][]; /** Per-vault keccak256 of WOTS keys, ready as `depositorWotsPkHash`. */ wotsPkHashes: Hex[]; /** * Per-vault HTLC preimage hex (no 0x prefix). Re-derivable any time * via `expandHashlockSecret(root, htlcVout)`; not persisted. */ htlcSecretHexes: string[]; /** * Raw 32-byte auth-anchor preimage as 64-char lowercase hex (no `0x`). * Sent to the VP via `auth_createDepositorToken` to obtain a bearer * token; the VP validates `SHA256(authAnchorHex) === OP_RETURN_PUSH32` * in the broadcast Pre-PegIn. Reveal is intentional: once exposed * the anchor is public, but its scope is bound to a single * `peginTxid`. Domain-separated from `htlcSecretHexes` and * `perVaultWotsKeys` via the HKDF `info` label, so revealing it does * not weaken the other derived secrets. */ authAnchorHex: string; } export interface PreparePeginResult { /** Broadcast-ready Pre-PegIn + per-vault PegIn txs. Safe to log. */ transaction: PreparePeginTransaction; /** * x-only depositor pubkey snapshot used end-to-end across sizing, * vault-root derivation, and PSBT signing. Safe to persist; not * sensitive. Reusing this snapshot downstream guarantees that * derived secrets and signed PSBTs reference the same identity. */ depositorBtcPubkey: string; /** Sensitive derived material — see {@link PreparePeginDerivedSecrets}. */ derivedSecrets: PreparePeginDerivedSecrets; /** * Protocol-level deposit terms for this Pre-PegIn. Always built, regardless * of wallet capability — {@link supportsDepositApproval} wallets get it via * `approveDepositTerms` before PegIn signing; others just get it back for * reference. */ depositTerms: DepositTerms; } /** * Parameters for signing and broadcasting a transaction. */ export interface SignAndBroadcastParams { /** * Funded Pre-PegIn transaction hex from preparePegin(). */ fundedPrePeginTxHex: string; /** * Depositor's BTC public key (x-only, 64-char hex). * Can be provided with or without "0x" prefix. * Required for Taproot signing. */ depositorBtcPubkey: string; /** * Optional pre-fetched prevout data for inputs not yet in the mempool. * Key format: "txid:vout" (e.g. "abc123...def:0"). * When provided, matching inputs skip the mempool API fetch. * Useful for split transactions where outputs are unconfirmed. */ localPrevouts?: Record; /** * Approved deposit terms. REQUIRED when `config.btcWallet` supports deposit * approval (`supportsDepositApproval`) — the device signs the Pre-PegIn only * from an approved intent matching this tx. Pass `PreparePeginResult. * depositTerms` for fresh flows, or a resume rebuild. For non-approval * wallets it is ignored, but still validated against the tx's txid if given. */ depositTerms?: DepositTerms; } /** * BIP-322 BTC Proof-of-Possession binding a depositor's BTC key to their * Ethereum account. Produced by {@link PeginManager.signProofOfPossession} * and reusable across every register call in the same session — the * embedded identities are re-checked at register time. */ export interface PopSignature { /** BIP-322 signature over the PoP message (0x-prefixed hex). */ btcPopSignature: Hex; /** Ethereum address the PoP was signed for. */ depositorEthAddress: Address; /** BTC x-only public key (64-char hex, no 0x prefix). */ depositorBtcPubkey: string; } /** * Parameters for registering a peg-in on Ethereum. */ export interface RegisterPeginParams { /** * Funded, pre-witness Pre-PegIn tx hex — pass * {@link PreparePeginTransaction.fundedPrePeginTxHex} from * {@link PreparePeginResult.transaction}. The contract-side parameter * is named `unsignedPrePeginTx` but it stores the funded form. */ unsignedPrePeginTx: string; /** * Depositor-signed PegIn transaction hex (submitted to contract; vault ID derived from this). */ depositorSignedPeginTx: string; /** * Vault provider's Ethereum address. */ vaultProvider: Address; /** * SHA256 hashlock for HTLC activation (bytes32 hex with 0x prefix). */ hashlock: Hex; /** * Depositor's BTC payout address (e.g. bc1p..., bc1q...). * Converted to scriptPubKey internally via bitcoinjs-lib. * * If omitted, defaults to the connected BTC wallet's address * via `btcWallet.getAddress()`. */ depositorPayoutBtcAddress?: string; /** Keccak256 hash of the depositor's WOTS public key (bytes32) */ depositorWotsPkHash: Hex; /** Proof of possession from {@link PeginManager.signProofOfPossession}. */ popSignature: PopSignature; /** * Zero-based index of the HTLC output in the Pre-PegIn transaction that * this PegIn spends. In a batch Pre-PegIn with N HTLC outputs, each vault * registration references a different htlcVout (0..N-1). */ htlcVout: number; /** * Bounds the registration's maxAcceptableCommissionBps (#1691). REQUIRED * when the wallet approved terms — the ceiling must anchor to the approved * quote. Optional otherwise; falls back to chain-current. */ quotedCommissionBps?: number; } /** * Result of registering a peg-in on Ethereum. */ export interface RegisterPeginResult { /** * Ethereum transaction hash for the peg-in registration. */ ethTxHash: Hash; /** * Derived vault ID: keccak256(abi.encode(peginTxHash, depositor)). * Used for contract reads/writes and indexer queries. */ vaultId: Hex; /** * Raw Bitcoin pegin transaction hash (double-SHA256 of the signed pegin tx). * Used for VP RPC operations which key on the BTC transaction ID. */ peginTxHash: Hex; } /** * Single request in a batch pegin registration. * All requests in a batch share the same vault provider, depositor BTC * pubkey, and Pre-PegIn transaction. */ export interface BatchPeginRequestItem { /** Signed PegIn tx hex for this vault */ depositorSignedPeginTx: string; /** SHA256 hashlock for HTLC activation (bytes32 hex) */ hashlock: Hex; /** Zero-based HTLC output index in the Pre-PegIn tx (unique per request) */ htlcVout: number; /** Depositor's BTC payout address (required — funds are sent here on payout) */ depositorPayoutBtcAddress: string; /** Keccak256 hash of the depositor's WOTS public key (bytes32) */ depositorWotsPkHash: Hex; } /** * Parameters for registerPeginBatchOnChain. */ export interface RegisterPeginBatchParams { /** Vault provider address (shared across all vaults in batch) */ vaultProvider: Address; /** * Funded, pre-witness Pre-PegIn tx hex — shared across every request in * the batch. See {@link RegisterPeginParams.unsignedPrePeginTx}. */ unsignedPrePeginTx: string; /** Individual pegin requests (one per vault) */ requests: BatchPeginRequestItem[]; /** Proof of possession from {@link PeginManager.signProofOfPossession}. */ popSignature: PopSignature; /** See {@link RegisterPeginParams.quotedCommissionBps}. */ quotedCommissionBps?: number; } /** * Per-vault result from a batch pegin registration. */ export interface BatchPeginResultItem { /** Derived vault ID: keccak256(abi.encode(peginTxHash, depositor)) */ vaultId: Hex; /** Raw BTC pegin transaction hash */ peginTxHash: Hex; } /** * Result of registering a batch of pegins on Ethereum in a single transaction. */ export interface RegisterPeginBatchResult { /** Ethereum transaction hash */ ethTxHash: Hex; /** Per-vault results (same order as input requests) */ vaults: BatchPeginResultItem[]; } export declare class PeginManager { private readonly config; /** * Creates a new PeginManager instance. * * @param config - Manager configuration including wallets and contract addresses */ constructor(config: PeginManagerConfig); /** * Prepare a peg-in: sizing pass → vault-root derivation (one wallet * popup) → per-vault WOTS / hashlock derivation → commit pass with * PSBT signing (signPsbt for a single vault, one batch popup for a * split). Returns broadcast-ready txs, the pubkey snapshot, and the * sensitive derived material. * * @throws If the wallet rejects, insufficient funds, or an internal * invariant violation. */ preparePegin(params: PreparePeginParams): Promise; /** * Build unfunded Pre-PegIn + select UTXOs. No PSBT signing. * * Returns the full selection result (UTXOs, fee, changeAmount) so the * commit pass funds the broadcast tx with the exact same set used to * build the vault-context funding-outpoints commitment. Re-running * `selectUtxosForPegin` in the commit pass would be deterministic given * the same inputs, but threading the result through guarantees the * domain separator structurally matches the funded tx inputs. * * Sizing runs before the wallet popup, so neither the real per-vault * hashlocks nor the real `authAnchorHash` are known yet. Both slots * are filled with a 32-byte placeholder; the commit pass swaps in the * real values. Output budget is identical (32-byte push regardless of * content), so UTXO selection is invariant under substitution. */ private prepareSizing; /** * One projection for both the provisional (pre-derive, placeholder-txid) * terms and the final approved terms, so the fields the pre-check validated * cannot drift from the fields the device later displays (#2110 T4). */ private buildPeginDepositTerms; /** Build PegIn txs and batch-sign their inputs with real hashlocks. */ private preparePeginCommit; /** * Signs and broadcasts a funded peg-in transaction to the Bitcoin network. * * This method: * 1. Parses the funded transaction hex * 2. Fetches UTXO data from mempool for each input * 3. Creates a PSBT with proper witnessUtxo/tapInternalKey * 4. Signs via btcWallet.signPsbt() * 5. Finalizes and extracts the transaction * 6. Broadcasts via mempool API * * IMPORTANT — this method does NOT gate on Ethereum finality. Committing * BTC to the HTLC while the peg-in registration is still reorg-exposed can * strand the deposit: the vault record disappears from the chain while the * BTC stays locked until the HTLC refund timelock. Callers must await * `waitForPeginRegistrationDepth` for the registered vault(s) before calling * this. The gate is not applied here because the params carry no vault ID — * adding one would be a breaking signature change. * * @param params - Transaction hex and depositor public key * @returns The broadcasted Bitcoin transaction ID * @throws Error if signing or broadcasting fails */ signAndBroadcast(params: SignAndBroadcastParams): Promise; /** * Registers a peg-in on Ethereum by calling the BTCVaultRegistry contract. * * This method: * 1. Re-verifies the PopSignature against the currently connected ETH * and BTC wallets — refuses to proceed if either has changed * 2. Derives vault ID and checks if it already exists (pre-flight) * 3. Encodes the contract call using viem * 4. Estimates gas (catches contract errors early with proper revert * reasons) * 5. Sends transaction with pre-estimated gas via * ethWallet.sendTransaction() * * The PopSignature must be obtained via * {@link signProofOfPossession} before this call. * * @param params - Registration parameters including the PopSignature * and the prepared Pre-PegIn / PegIn transactions * @returns Result containing Ethereum transaction hash and vault ID * @throws Error if the PopSignature does not match the connected wallets * @throws Error if the vault already exists * @throws Error if contract simulation fails (e.g., invalid signature, * unauthorized) */ registerPeginOnChain(params: RegisterPeginParams): Promise; /** * Register multiple pegins on Ethereum in a single transaction. * * Uses the contract's submitPeginRequestBatch() to submit all vault * registrations atomically. All vaults must share the same vault provider. * The PoP signature is signed once and included in each request. * * @param params - Batch registration parameters * @returns Batch result with per-vault IDs and single ETH tx hash */ registerPeginBatchOnChain(params: RegisterPeginBatchParams): Promise; private resolveMaxAcceptableCommissionBps; /** * Check if a vault already exists for a given vault ID. * * The contract returns a default struct (with `depositor === zeroAddress`) * when no vault is registered, so existence is signalled in the response, * not via a thrown error. RPC/network failures are propagated rather than * silently treated as "vault doesn't exist", which would otherwise let * downstream calls run with stale assumptions. * * @param vaultId - The Bitcoin transaction hash (vault ID) * @returns True if vault exists, false otherwise * @throws If the underlying RPC read fails */ private checkVaultExists; /** * Resolve the BTC scriptPubKey to register as the depositor's payout sink. * * `address` is validated against the verified depositor pubkey, sourced * from `assertPopMatchesBtcWallet`'s return value rather than * `popSignature.depositorBtcPubkey` (which is x-only, parity stripped). * For wallets that expose a compressed key this preserves y-parity end to * end. For Taproot wallets that only expose an x-only key, the helper * itself fails closed for P2WPKH — the parity is unknowable, so the * payout sink must be a P2TR address derived from that same x. * * The helper does not call into the wallet so the batch path can resolve * many requests without any extra adapter reads. Threat closed: a * state-race or stale FE state that lets a non-wallet address reach the * on-chain payout-script registration. */ private resolvePayoutScriptPubKey; /** * Sign a BIP-322 BTC Proof-of-Possession binding the connected BTC * wallet to the connected ETH account for this chain and vault * registry. The returned {@link PopSignature} can be reused across * every register call in the same session. * * The witness is verified against the depositor key before it is * returned — Schnorr for one-item (P2TR), ECDSA over the BIP-322 * P2WPKH virtual transaction for two-item — see {@link verifyPopWitness}. * * @throws If the wallet returns a malformed witness or a signature that * does not verify. */ signProofOfPossession(): Promise; /** * Confirm the connected BTC wallet still matches the PoP it produced, and * return the wallet's *raw* pubkey hex (parity-preserving form, as the * wallet adapter returns it). The raw form is required by callers that * validate Native SegWit / P2WPKH addresses, since P2WPKH is derived from * a parity-bearing compressed key — an x-only form would let an attacker * substitute the opposite-parity P2WPKH address. */ private assertPopMatchesBtcWallet; /** * Gets the configured Bitcoin network. * * @returns The Bitcoin network (mainnet, testnet, signet, regtest) */ getNetwork(): Network; /** * Gets the configured BTCVaultRegistry contract address. * * @returns The Ethereum address of the BTCVaultRegistry contract */ getVaultContractAddress(): Address; } export interface EstimateSubmitPeginRequestBatchGasParams { publicClient: PublicClient; btcVaultRegistry: Address; depositorEthAddress: Address; vaultProvider: Address; batchSize: number; } /** * Estimate gas for a `submitPeginRequestBatch` call before the depositor has * signed anything. Synthesizes calldata using representative dummy bytes for * fields the depositor would normally produce (signed PegIn tx, PoP sig, * WOTS hash, payout script). The estimate is approximate — calldata-byte * gas is correct, contract-side branches that depend on the real values may * diverge — but it lands within the usual gas-estimate margin. * * Passes {@link MAX_ACCEPTABLE_COMMISSION_BPS_CAP} for the * `maxAcceptableCommissionBps` argument so the simulation does not revert on * the contract's commission-drift check regardless of the VP's current * commission. The real submit path resolves an accurate, drift-checked value * via {@link PeginManager.resolveMaxAcceptableCommissionBps}. * * Throws if the contract reverts during simulation; callers should treat the * thrown error as "unable to estimate" and decide how to surface it. */ export declare function estimateSubmitPeginRequestBatchGas(params: EstimateSubmitPeginRequestBatchGasParams): Promise; //# sourceMappingURL=PeginManager.d.ts.map