UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

439 lines 18.2 kB
import { fromHex, Psbt, toHex } from '@btc-vision/bitcoin'; import {} from '@btc-vision/ecpair'; import { TransactionType } from '../enums/TransactionType.js'; import { TransactionBuilder } from '../builders/TransactionBuilder.js'; import { MultiSignTransaction } from '../builders/MultiSignTransaction.js'; import { TransactionSerializer } from './TransactionSerializer.js'; import { TransactionReconstructor, } from './TransactionReconstructor.js'; import { TransactionStateCapture } from './TransactionStateCapture.js'; import { isMultiSigSpecificData } from './interfaces/ITypeSpecificData.js'; /** * Main entry point for offline transaction signing workflow. * * This class provides a complete API for: * 1. Phase 1 (Online): Building transactions and exporting state for offline signing * 2. Phase 2 (Offline): Importing state, providing signers, and signing transactions * * Also supports fee bumping by allowing reconstruction with new fee parameters. * * @example * ```typescript * // Phase 1 (Online environment) * const params: IFundingTransactionParameters = { ... }; * const state = OfflineTransactionManager.exportFunding(params); * // Send state to offline environment * * // Phase 2 (Offline environment) * const signedTxHex = await OfflineTransactionManager.importSignAndExport(state, { * signer: offlineSigner, * }); * // Send signedTxHex back to online environment for broadcast * ``` */ export class OfflineTransactionManager { /** * Export a FundingTransaction for offline signing * @param params - Funding transaction parameters * @param precomputed - Optional precomputed data * @returns Base64-encoded serialized state */ static exportFunding(params, precomputed) { const state = TransactionStateCapture.fromFunding(params, precomputed); return TransactionSerializer.toBase64(state); } /** * Export a DeploymentTransaction for offline signing * @param params - Deployment transaction parameters * @param precomputed - Required precomputed data (randomBytes, compiledTargetScript) * @returns Base64-encoded serialized state */ static exportDeployment(params, precomputed) { const state = TransactionStateCapture.fromDeployment(params, precomputed); return TransactionSerializer.toBase64(state); } /** * Export an InteractionTransaction for offline signing * @param params - Interaction transaction parameters * @param precomputed - Required precomputed data (randomBytes, compiledTargetScript) * @returns Base64-encoded serialized state */ static exportInteraction(params, precomputed) { const state = TransactionStateCapture.fromInteraction(params, precomputed); return TransactionSerializer.toBase64(state); } /** * Export a MultiSignTransaction for offline signing * @param params - MultiSig transaction parameters * @param precomputed - Optional precomputed data * @returns Base64-encoded serialized state */ static exportMultiSig(params, precomputed) { const state = TransactionStateCapture.fromMultiSig(params, precomputed); return TransactionSerializer.toBase64(state); } /** * Export a CustomScriptTransaction for offline signing * @param params - Custom script transaction parameters * @param precomputed - Optional precomputed data * @returns Base64-encoded serialized state */ static exportCustomScript(params, precomputed) { const state = TransactionStateCapture.fromCustomScript(params, precomputed); return TransactionSerializer.toBase64(state); } /** * Export a CancelTransaction for offline signing * @param params - Cancel transaction parameters * @param precomputed - Optional precomputed data * @returns Base64-encoded serialized state */ static exportCancel(params, precomputed) { const state = TransactionStateCapture.fromCancel(params, precomputed); return TransactionSerializer.toBase64(state); } /** * Export transaction state from a builder instance. * The builder must have been built but not yet signed. * @param builder - Transaction builder instance * @param params - Original construction parameters * @param precomputed - Precomputed data from the builder * @returns Base64-encoded serialized state */ static exportFromBuilder(builder, params, precomputed) { const type = builder.type; let state; switch (type) { case TransactionType.FUNDING: state = TransactionStateCapture.fromFunding(params, precomputed); break; case TransactionType.DEPLOYMENT: state = TransactionStateCapture.fromDeployment(params, precomputed); break; case TransactionType.INTERACTION: state = TransactionStateCapture.fromInteraction(params, precomputed); break; default: throw new Error(`Unsupported transaction type for export: ${type}`); } return TransactionSerializer.toBase64(state); } /** * Import and reconstruct transaction for signing * @param serializedState - Base64-encoded state from Phase 1 * @param options - Signer(s) and optional fee overrides * @returns Reconstructed transaction builder ready for signing */ static importForSigning(serializedState, options) { const state = TransactionSerializer.fromBase64(serializedState); return TransactionReconstructor.reconstruct(state, options); } /** * Complete signing and export signed transaction * @param builder - Reconstructed builder from importForSigning * @returns Signed transaction hex ready for broadcast */ static async signAndExport(builder) { const tx = await builder.signTransaction(); return tx.toHex(); } /** * Convenience: Full Phase 2 in one call - import, sign, and export * @param serializedState - Base64-encoded state * @param options - Signer(s) and optional fee overrides * @returns Signed transaction hex ready for broadcast */ static async importSignAndExport(serializedState, options) { const builder = this.importForSigning(serializedState, options); return this.signAndExport(builder); } /** * Rebuild transaction with new fee rate (fee bumping) * @param serializedState - Original state * @param newFeeRate - New fee rate in sat/vB * @returns New serialized state with updated fees (not signed yet) */ static rebuildWithNewFees(serializedState, newFeeRate) { // Parse the existing state const state = TransactionSerializer.fromBase64(serializedState); // Create a new state with updated fee rate const newState = { ...state, baseParams: { ...state.baseParams, feeRate: newFeeRate, }, }; return TransactionSerializer.toBase64(newState); } /** * Rebuild and immediately sign with new fee rate * @param serializedState - Original state * @param newFeeRate - New fee rate in sat/vB * @param options - Signer options * @returns Signed transaction hex with new fees */ static async rebuildSignAndExport(serializedState, newFeeRate, options) { const builder = this.importForSigning(serializedState, { ...options, newFeeRate, }); return this.signAndExport(builder); } /** * Inspect serialized state without signing * @param serializedState - Base64-encoded state * @returns Parsed state object for inspection */ static inspect(serializedState) { return TransactionSerializer.fromBase64(serializedState); } /** * Validate serialized state integrity * @param serializedState - Base64-encoded state * @returns True if checksum and format are valid */ static validate(serializedState) { try { TransactionSerializer.fromBase64(serializedState); return true; } catch { return false; } } /** * Get transaction type from serialized state * @param serializedState - Base64-encoded state * @returns Transaction type enum value */ static getType(serializedState) { const state = TransactionSerializer.fromBase64(serializedState); return state.header.transactionType; } /** * Parse base64-encoded state into state object * @param base64State - Base64-encoded state * @returns Parsed state object */ static fromBase64(base64State) { return TransactionSerializer.fromBase64(base64State); } /** * Serialize state object to base64 * @param state - State object to serialize * @returns Base64-encoded state */ static toBase64(state) { return TransactionSerializer.toBase64(state); } /** * Convert serialized state to hex format * @param serializedState - Base64-encoded state * @returns Hex-encoded state */ static toHex(serializedState) { const state = TransactionSerializer.fromBase64(serializedState); return TransactionSerializer.toHex(state); } /** * Convert hex format back to base64 * @param hexState - Hex-encoded state * @returns Base64-encoded state */ static fromHex(hexState) { const state = TransactionSerializer.fromHex(hexState); return TransactionSerializer.toBase64(state); } /** * Add a partial signature to a multisig transaction state. * This method signs the transaction with the provided signer and returns * updated state with the new signature included. * * @param serializedState - Base64-encoded multisig state * @param signer - The signer to add a signature with * @returns Updated state with new signature, and signing result */ static async multiSigAddSignature(serializedState, signer) { const state = TransactionSerializer.fromBase64(serializedState); if (!isMultiSigSpecificData(state.typeSpecificData)) { throw new Error('State is not a multisig transaction'); } const typeData = state.typeSpecificData; const pubkeys = typeData.pubkeys.map((pk) => fromHex(pk)); // Parse existing PSBT or create new one let psbt; const network = TransactionReconstructor['nameToNetwork'](state.baseParams.networkName); if (typeData.existingPsbtBase64) { psbt = Psbt.fromBase64(typeData.existingPsbtBase64, { network }); } else { // Need to build the transaction first const builder = this.importForSigning(serializedState, { signer, }); psbt = await builder.signPSBT(); } // Calculate minimums array for each input const minimums = []; for (let i = typeData.originalInputCount; i < psbt.data.inputs.length; i++) { minimums.push(typeData.minimumSignatures); } // Sign the PSBT const result = MultiSignTransaction.signPartial(psbt, signer, typeData.originalInputCount, minimums); // Finalize inputs (partial finalization to preserve signatures) const orderedPubKeys = []; for (let i = typeData.originalInputCount; i < psbt.data.inputs.length; i++) { orderedPubKeys.push(pubkeys); } MultiSignTransaction.attemptFinalizeInputs(psbt, typeData.originalInputCount, orderedPubKeys, result.final); const newPsbtBase64 = psbt.toBase64(); // Update the state with new PSBT const newState = { ...state, typeSpecificData: { ...typeData, existingPsbtBase64: newPsbtBase64, }, }; return { state: TransactionSerializer.toBase64(newState), signed: result.signed, final: result.final, psbtBase64: newPsbtBase64, }; } /** * Check if a public key has already signed a multisig transaction * * @param serializedState - Base64-encoded multisig state * @param signerPubKey - Public key to check (Uint8Array or hex string) * @returns True if the public key has already signed */ static multiSigHasSigned(serializedState, signerPubKey) { const state = TransactionSerializer.fromBase64(serializedState); if (!isMultiSigSpecificData(state.typeSpecificData)) { throw new Error('State is not a multisig transaction'); } const typeData = state.typeSpecificData; if (!typeData.existingPsbtBase64) { return false; } const network = TransactionReconstructor['nameToNetwork'](state.baseParams.networkName); const psbt = Psbt.fromBase64(typeData.existingPsbtBase64, { network }); const pubKeyBuffer = signerPubKey instanceof Uint8Array ? signerPubKey : fromHex(signerPubKey); return MultiSignTransaction.verifyIfSigned(psbt, pubKeyBuffer); } /** * Get the current signature count for a multisig transaction * * @param serializedState - Base64-encoded multisig state * @returns Object with signature count info */ static multiSigGetSignatureStatus(serializedState) { const state = TransactionSerializer.fromBase64(serializedState); if (!isMultiSigSpecificData(state.typeSpecificData)) { throw new Error('State is not a multisig transaction'); } const typeData = state.typeSpecificData; const required = typeData.minimumSignatures; if (!typeData.existingPsbtBase64) { return { required, collected: 0, isComplete: false, signers: [], }; } const network = TransactionReconstructor['nameToNetwork'](state.baseParams.networkName); const psbt = Psbt.fromBase64(typeData.existingPsbtBase64, { network }); // Collect signers from all inputs const signerSet = new Set(); for (let i = typeData.originalInputCount; i < psbt.data.inputs.length; i++) { const input = psbt.data.inputs[i]; if (input.tapScriptSig) { for (const sig of input.tapScriptSig) { signerSet.add(toHex(sig.pubkey)); } } if (input.finalScriptWitness) { const decoded = TransactionBuilder.readScriptWitnessToWitnessStack(input.finalScriptWitness); for (let j = 0; j < decoded.length - 2; j += 3) { const pubKey = decoded[j + 2]; signerSet.add(toHex(pubKey)); } } } const signers = Array.from(signerSet); return { required, collected: signers.length, isComplete: signers.length >= required, signers, }; } /** * Finalize a multisig transaction and extract the signed transaction hex. * Only call this when all required signatures have been collected. * * @param serializedState - Base64-encoded multisig state with all signatures * @returns Signed transaction hex ready for broadcast */ static multiSigFinalize(serializedState) { const state = TransactionSerializer.fromBase64(serializedState); if (!isMultiSigSpecificData(state.typeSpecificData)) { throw new Error('State is not a multisig transaction'); } const typeData = state.typeSpecificData; if (!typeData.existingPsbtBase64) { throw new Error('No PSBT found in state - transaction has not been signed'); } const network = TransactionReconstructor['nameToNetwork'](state.baseParams.networkName); const psbt = Psbt.fromBase64(typeData.existingPsbtBase64, { network }); const pubkeys = typeData.pubkeys.map((pk) => fromHex(pk)); const orderedPubKeys = []; for (let i = typeData.originalInputCount; i < psbt.data.inputs.length; i++) { orderedPubKeys.push(pubkeys); } // Final finalization const success = MultiSignTransaction.attemptFinalizeInputs(psbt, typeData.originalInputCount, orderedPubKeys, true); if (!success) { throw new Error('Failed to finalize multisig transaction - not enough signatures'); } return psbt.extractTransaction(true, true).toHex(); } /** * Get the PSBT from a multisig state (for external signing tools) * * @param serializedState - Base64-encoded multisig state * @returns PSBT in base64 format, or null if not yet built */ static multiSigGetPsbt(serializedState) { const state = TransactionSerializer.fromBase64(serializedState); if (!isMultiSigSpecificData(state.typeSpecificData)) { throw new Error('State is not a multisig transaction'); } return state.typeSpecificData.existingPsbtBase64 || null; } /** * Update the PSBT in a multisig state (after external signing) * * @param serializedState - Base64-encoded multisig state * @param psbtBase64 - New PSBT with additional signatures * @returns Updated state */ static multiSigUpdatePsbt(serializedState, psbtBase64) { const state = TransactionSerializer.fromBase64(serializedState); if (!isMultiSigSpecificData(state.typeSpecificData)) { throw new Error('State is not a multisig transaction'); } const newState = { ...state, typeSpecificData: { ...state.typeSpecificData, existingPsbtBase64: psbtBase64, }, }; return TransactionSerializer.toBase64(newState); } } //# sourceMappingURL=OfflineTransactionManager.js.map