UNPKG

@btc-vision/transaction

Version:

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

435 lines (393 loc) 16.5 kB
import { fromHex, type Network, networks, type PsbtOutputExtended, type Script, type Signer, type Stack, toSatoshi, } from '@btc-vision/bitcoin'; import { type UniversalSigner } from '@btc-vision/ecpair'; import type { QuantumBIP32Interface } from '@btc-vision/bip32'; import type { UTXO } from '../../utxo/interfaces/IUTXO.js'; import type { AddressRotationConfig, SignerMap } from '../../signer/AddressRotation.js'; import { ChallengeSolution } from '../../epoch/ChallengeSolution.js'; import { TransactionType } from '../enums/TransactionType.js'; import { TransactionBuilder } from '../builders/TransactionBuilder.js'; import { FundingTransaction } from '../builders/FundingTransaction.js'; import { DeploymentTransaction } from '../builders/DeploymentTransaction.js'; import { InteractionTransaction } from '../builders/InteractionTransaction.js'; import { MultiSignTransaction } from '../builders/MultiSignTransaction.js'; import { CustomScriptTransaction } from '../builders/CustomScriptTransaction.js'; import { CancelTransaction } from '../builders/CancelTransaction.js'; import type { ISerializableTransactionState, SerializedOutput, SerializedUTXO, } from './interfaces/ISerializableState.js'; import { type CancelSpecificData, type CustomScriptSpecificData, type DeploymentSpecificData, type FundingSpecificData, type InteractionSpecificData, isCancelSpecificData, isCustomScriptSpecificData, isDeploymentSpecificData, isFundingSpecificData, isInteractionSpecificData, isMultiSigSpecificData, type MultiSigSpecificData, } from './interfaces/ITypeSpecificData.js'; import type { IDeploymentParameters, IFundingTransactionParameters, IInteractionParameters, ITransactionParameters, } from '../interfaces/ITransactionParameters.js'; import type { SupportedTransactionVersion } from '../interfaces/ITweakedTransactionData.js'; /** * Options for reconstructing a transaction from serialized state */ export interface ReconstructionOptions { /** Primary signer (used for normal mode or as default in rotation mode) */ signer: Signer | UniversalSigner; /** Optional: Override fee rate for fee bumping */ newFeeRate?: number; /** Optional: Override priority fee */ newPriorityFee?: bigint; /** Optional: Override gas sat fee */ newGasSatFee?: bigint; /** Signer map for address rotation mode (keyed by address) */ signerMap?: SignerMap; /** MLDSA signer (for quantum-resistant features) */ mldsaSigner?: QuantumBIP32Interface | null; } /** * Reconstructs transaction builders from serialized state. * Supports fee bumping by allowing parameter overrides during reconstruction. */ export class TransactionReconstructor { /** * Reconstruct and optionally rebuild transaction with new parameters * @param state - Serialized transaction state * @param options - Signer(s) and optional fee overrides * @returns Reconstructed transaction builder ready for signing */ public static reconstruct( state: ISerializableTransactionState, options: ReconstructionOptions, ): TransactionBuilder<TransactionType> { const network = this.nameToNetwork(state.baseParams.networkName); const utxos = this.deserializeUTXOs(state.utxos); const optionalInputs = this.deserializeUTXOs(state.optionalInputs); const optionalOutputs = this.deserializeOutputs(state.optionalOutputs); // Build address rotation config const addressRotation = this.buildAddressRotationConfig( state.addressRotationEnabled, options.signerMap, ); // Apply fee overrides const feeRate = options.newFeeRate ?? state.baseParams.feeRate; const priorityFee = options.newPriorityFee ?? BigInt(state.baseParams.priorityFee); const gasSatFee = options.newGasSatFee ?? BigInt(state.baseParams.gasSatFee); // Build base params const baseParams: ITransactionParameters = { signer: options.signer, mldsaSigner: options.mldsaSigner ?? null, network, utxos, optionalInputs, optionalOutputs, from: state.baseParams.from, feeRate, priorityFee, gasSatFee, anchor: state.baseParams.anchor, ...(state.header.chainId !== undefined ? { chainId: state.header.chainId } : {}), ...(state.baseParams.to !== undefined ? { to: state.baseParams.to } : {}), ...(state.baseParams.txVersion !== undefined ? { txVersion: state.baseParams.txVersion as SupportedTransactionVersion } : {}), ...(state.baseParams.note !== undefined ? { note: fromHex(state.baseParams.note) } : {}), ...(state.baseParams.debugFees !== undefined ? { debugFees: state.baseParams.debugFees } : {}), ...(addressRotation !== undefined ? { addressRotation } : {}), ...(state.precomputedData.estimatedFees !== undefined ? { estimatedFees: BigInt(state.precomputedData.estimatedFees) } : {}), ...(state.precomputedData.compiledTargetScript !== undefined ? { compiledTargetScript: fromHex(state.precomputedData.compiledTargetScript) } : {}), }; // Dispatch based on transaction type const typeData = state.typeSpecificData; if (isFundingSpecificData(typeData)) { return this.reconstructFunding(baseParams, typeData); } else if (isDeploymentSpecificData(typeData)) { return this.reconstructDeployment(baseParams, typeData, state); } else if (isInteractionSpecificData(typeData)) { return this.reconstructInteraction(baseParams, typeData, state); } else if (isMultiSigSpecificData(typeData)) { return this.reconstructMultiSig(baseParams, typeData); } else if (isCustomScriptSpecificData(typeData)) { return this.reconstructCustomScript(baseParams, typeData, state); } else if (isCancelSpecificData(typeData)) { return this.reconstructCancel(baseParams, typeData); } throw new Error(`Unsupported transaction type: ${state.header.transactionType}`); } /** * Reconstruct a FundingTransaction */ private static reconstructFunding( baseParams: ITransactionParameters, data: FundingSpecificData, ): FundingTransaction { const params: IFundingTransactionParameters = { ...baseParams, amount: BigInt(data.amount), splitInputsInto: data.splitInputsInto, }; return new FundingTransaction(params); } /** * Reconstruct a DeploymentTransaction */ private static reconstructDeployment( baseParams: ITransactionParameters, data: DeploymentSpecificData, state: ISerializableTransactionState, ): DeploymentTransaction { const challenge = new ChallengeSolution(data.challenge); const params: IDeploymentParameters = { ...baseParams, bytecode: fromHex(data.bytecode), challenge, ...(data.calldata !== undefined ? { calldata: fromHex(data.calldata) } : {}), ...(state.precomputedData.randomBytes !== undefined ? { randomBytes: fromHex(state.precomputedData.randomBytes) } : {}), ...(data.revealMLDSAPublicKey !== undefined ? { revealMLDSAPublicKey: data.revealMLDSAPublicKey } : {}), ...(data.linkMLDSAPublicKeyToAddress !== undefined ? { linkMLDSAPublicKeyToAddress: data.linkMLDSAPublicKeyToAddress } : {}), }; return new DeploymentTransaction(params); } /** * Reconstruct an InteractionTransaction */ private static reconstructInteraction( baseParams: ITransactionParameters, data: InteractionSpecificData, state: ISerializableTransactionState, ): InteractionTransaction { const challenge = new ChallengeSolution(data.challenge); if (!baseParams.to) { throw new Error('InteractionTransaction requires a "to" address'); } const params: IInteractionParameters = { ...baseParams, to: baseParams.to, calldata: fromHex(data.calldata), challenge, ...(data.contract !== undefined ? { contract: data.contract } : {}), ...(state.precomputedData.randomBytes !== undefined ? { randomBytes: fromHex(state.precomputedData.randomBytes) } : {}), ...(data.loadedStorage !== undefined ? { loadedStorage: data.loadedStorage } : {}), ...(data.isCancellation !== undefined ? { isCancellation: data.isCancellation } : {}), ...(data.disableAutoRefund !== undefined ? { disableAutoRefund: data.disableAutoRefund } : {}), ...(data.revealMLDSAPublicKey !== undefined ? { revealMLDSAPublicKey: data.revealMLDSAPublicKey } : {}), ...(data.linkMLDSAPublicKeyToAddress !== undefined ? { linkMLDSAPublicKeyToAddress: data.linkMLDSAPublicKeyToAddress } : {}), }; return new InteractionTransaction(params); } /** * Reconstruct a MultiSignTransaction */ private static reconstructMultiSig( baseParams: ITransactionParameters, data: MultiSigSpecificData, ): MultiSignTransaction { const pubkeys = data.pubkeys.map((pk) => fromHex(pk)); // If there's an existing PSBT, use fromBase64 to preserve partial signatures if (data.existingPsbtBase64) { return MultiSignTransaction.fromBase64({ mldsaSigner: baseParams.mldsaSigner, network: baseParams.network, utxos: baseParams.utxos, feeRate: baseParams.feeRate, pubkeys, minimumSignatures: data.minimumSignatures, receiver: data.receiver, requestedAmount: BigInt(data.requestedAmount), refundVault: data.refundVault, psbt: data.existingPsbtBase64, ...(baseParams.chainId !== undefined ? { chainId: baseParams.chainId } : {}), ...(baseParams.optionalInputs !== undefined ? { optionalInputs: baseParams.optionalInputs } : {}), ...(baseParams.optionalOutputs !== undefined ? { optionalOutputs: baseParams.optionalOutputs } : {}), }); } // No existing PSBT - create fresh transaction const params = { mldsaSigner: baseParams.mldsaSigner, network: baseParams.network, utxos: baseParams.utxos, feeRate: baseParams.feeRate, pubkeys, minimumSignatures: data.minimumSignatures, receiver: data.receiver, requestedAmount: BigInt(data.requestedAmount), refundVault: data.refundVault, ...(baseParams.chainId !== undefined ? { chainId: baseParams.chainId } : {}), ...(baseParams.optionalInputs !== undefined ? { optionalInputs: baseParams.optionalInputs } : {}), ...(baseParams.optionalOutputs !== undefined ? { optionalOutputs: baseParams.optionalOutputs } : {}), }; return new MultiSignTransaction(params); } /** * Reconstruct a CustomScriptTransaction */ private static reconstructCustomScript( baseParams: ITransactionParameters, data: CustomScriptSpecificData, state: ISerializableTransactionState, ): CustomScriptTransaction { // Convert serialized elements to (Uint8Array | Stack)[] const scriptElements: (Uint8Array | Stack)[] = data.scriptElements.map((el) => { if (el.elementType === 'buffer') { return fromHex(el.value as string); } // Opcodes stored as numbers - wrap in array for Stack type return [el.value as number] as Stack; }); const witnesses = data.witnesses.map((w) => fromHex(w)); if (!baseParams.to) { throw new Error('CustomScriptTransaction requires a "to" address'); } const params = { ...baseParams, to: baseParams.to, script: scriptElements, witnesses, ...(data.annex !== undefined ? { annex: fromHex(data.annex) } : {}), ...(state.precomputedData.randomBytes !== undefined ? { randomBytes: fromHex(state.precomputedData.randomBytes) } : {}), }; return new CustomScriptTransaction(params); } /** * Reconstruct a CancelTransaction */ private static reconstructCancel( baseParams: ITransactionParameters, data: CancelSpecificData, ): CancelTransaction { const params = { ...baseParams, compiledTargetScript: fromHex(data.compiledTargetScript), }; return new CancelTransaction(params); } /** * Build address rotation config from options */ private static buildAddressRotationConfig( enabled: boolean, signerMap?: SignerMap, ): AddressRotationConfig | undefined { if (!enabled) { return undefined; } if (!signerMap || signerMap.size === 0) { throw new Error( 'Address rotation enabled but no signerMap provided in reconstruction options', ); } return { enabled: true, signerMap, }; } /** * Deserialize UTXOs from serialized format */ private static deserializeUTXOs(serialized: SerializedUTXO[]): UTXO[] { return serialized.map((s) => { const utxo: UTXO = { transactionId: s.transactionId, outputIndex: s.outputIndex, value: BigInt(s.value), scriptPubKey: { hex: s.scriptPubKeyHex, ...(s.scriptPubKeyAddress !== undefined ? { address: s.scriptPubKeyAddress } : {}), }, }; if (s.redeemScript !== undefined) utxo.redeemScript = fromHex(s.redeemScript); if (s.witnessScript !== undefined) utxo.witnessScript = fromHex(s.witnessScript); if (s.nonWitnessUtxo !== undefined) utxo.nonWitnessUtxo = fromHex(s.nonWitnessUtxo); return utxo; }); } /** * Deserialize outputs from serialized format */ private static deserializeOutputs(serialized: SerializedOutput[]): PsbtOutputExtended[] { return serialized.map((s): PsbtOutputExtended => { const base = { value: toSatoshi(BigInt(s.value)) }; const tapKey = s.tapInternalKey !== undefined ? { tapInternalKey: fromHex(s.tapInternalKey) } : {}; // PsbtOutputExtended is a union type - either has address OR script, not both if (s.address) { return { ...base, address: s.address, ...tapKey }; } else if (s.script) { return { ...base, script: fromHex(s.script) as Script, ...tapKey }; } else { // Fallback - shouldn't happen with valid data return { ...base, address: '', ...tapKey }; } }); } /** * Convert network name to Network object */ private static nameToNetwork(name: 'mainnet' | 'testnet' | 'opnetTestnet' | 'regtest'): Network { switch (name) { case 'mainnet': return networks.bitcoin; case 'testnet': return networks.testnet; case 'opnetTestnet': return networks.opnetTestnet; case 'regtest': return networks.regtest; default: throw new Error(`Unknown network: ${name}`); } } }