@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
439 lines • 18.2 kB
JavaScript
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