UNPKG

@btc-vision/transaction

Version:

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

317 lines 13.6 kB
import { fromHex, networks, toSatoshi, } from '@btc-vision/bitcoin'; import {} from '@btc-vision/ecpair'; 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 { isCancelSpecificData, isCustomScriptSpecificData, isDeploymentSpecificData, isFundingSpecificData, isInteractionSpecificData, isMultiSigSpecificData, } from './interfaces/ITypeSpecificData.js'; /** * 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 */ static reconstruct(state, options) { 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 = { 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 } : {}), ...(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 */ static reconstructFunding(baseParams, data) { const params = { ...baseParams, amount: BigInt(data.amount), splitInputsInto: data.splitInputsInto, }; return new FundingTransaction(params); } /** * Reconstruct a DeploymentTransaction */ static reconstructDeployment(baseParams, data, state) { const challenge = new ChallengeSolution(data.challenge); const params = { ...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 */ static reconstructInteraction(baseParams, data, state) { const challenge = new ChallengeSolution(data.challenge); if (!baseParams.to) { throw new Error('InteractionTransaction requires a "to" address'); } const params = { ...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 */ static reconstructMultiSig(baseParams, data) { 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 */ static reconstructCustomScript(baseParams, data, state) { // Convert serialized elements to (Uint8Array | Stack)[] const scriptElements = data.scriptElements.map((el) => { if (el.elementType === 'buffer') { return fromHex(el.value); } // Opcodes stored as numbers - wrap in array for Stack type return [el.value]; }); 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 */ static reconstructCancel(baseParams, data) { const params = { ...baseParams, compiledTargetScript: fromHex(data.compiledTargetScript), }; return new CancelTransaction(params); } /** * Build address rotation config from options */ static buildAddressRotationConfig(enabled, signerMap) { 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 */ static deserializeUTXOs(serialized) { return serialized.map((s) => { const 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 */ static deserializeOutputs(serialized) { return serialized.map((s) => { 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), ...tapKey }; } else { // Fallback - shouldn't happen with valid data return { ...base, address: '', ...tapKey }; } }); } /** * Convert network name to Network object */ static nameToNetwork(name) { 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}`); } } } //# sourceMappingURL=TransactionReconstructor.js.map