@btc-stamps/tx-builder
Version:
Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection
1,088 lines (951 loc) • 29.5 kB
text/typescript
/**
* PSBT Validator
* Comprehensive PSBT validation implementation
*/
import { Buffer } from 'node:buffer';
import * as bitcoin from 'bitcoinjs-lib';
import type { Network, Psbt } from 'bitcoinjs-lib';
import type {
InputAnalysis,
InputValidationResult,
IPSBTValidator,
OutputAnalysis,
OutputValidationResult,
PSBTAnalysisReport,
PSBTValidationResult,
PSBTValidationRule,
TransactionAnalysis,
ValidationError,
ValidationWarning,
} from '../interfaces/psbt-validation.interface.ts';
/**
* PSBT Validator Implementation
*/
export class PSBTValidator implements IPSBTValidator {
private network: Network;
private _validationRules: Map<string, PSBTValidationRule>;
constructor(network: Network = bitcoin.networks.bitcoin) {
this.network = network;
this._validationRules = this.initializeValidationRules();
// Use validation rules for future extensibility
void this._validationRules;
void this._hasComplexScripts;
}
/**
* Validate PSBT comprehensively
*/
async validate(psbt: Psbt, network?: Network): Promise<PSBTValidationResult> {
const _targetNetwork = network || this.network;
// Use target network for future network-specific validation
void _targetNetwork;
const criticalErrors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
const passed: string[] = [];
try {
// Validate basic PSBT structure
await this.validatePSBTStructure(psbt, criticalErrors, warnings, passed);
// Validate inputs
const inputValidation: InputValidationResult[] = [];
for (let i = 0; i < psbt.inputCount; i++) {
const inputResult = await this.validateInput(psbt, i);
inputValidation.push(inputResult);
criticalErrors.push(
...inputResult.errors.filter((e) => e.severity === 'critical'),
);
warnings.push(...inputResult.warnings);
}
// Validate outputs
const outputValidation: OutputValidationResult[] = [];
for (let i = 0; i < psbt.txOutputs.length; i++) {
const outputResult = await this.validateOutput(psbt, i);
outputValidation.push(outputResult);
criticalErrors.push(
...outputResult.errors.filter((e) => e.severity === 'critical'),
);
warnings.push(...outputResult.warnings);
}
// Validate transaction-level properties
await this.validateTransactionLevel_func(
psbt,
criticalErrors,
warnings,
passed,
);
// Generate transaction analysis
const transactionAnalysis = this.analyzeTransaction(psbt);
const result: PSBTValidationResult = {
valid: criticalErrors.length === 0,
canFinalize: await this.canFinalize(psbt),
criticalErrors,
warnings,
passed,
inputValidation,
outputValidation,
transactionAnalysis,
};
return result;
} catch (error) {
criticalErrors.push({
rule: 'validation_error',
message: `Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
severity: 'critical',
});
return {
valid: false,
canFinalize: false,
criticalErrors,
warnings,
passed,
inputValidation: [],
outputValidation: [],
transactionAnalysis: this.getEmptyTransactionAnalysis(),
};
}
}
/**
* Validate specific input
*/
validateInput(
psbt: Psbt,
inputIndex: number,
): Promise<InputValidationResult> {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
if (inputIndex >= psbt.inputCount) {
errors.push({
rule: 'input_index_bounds',
message: `Input index ${inputIndex} out of bounds`,
index: inputIndex,
severity: 'critical',
});
return Promise.resolve({
index: inputIndex,
valid: false,
canFinalize: false,
errors,
warnings,
analysis: this.getEmptyInputAnalysis(),
});
}
const input = psbt.data.inputs[inputIndex];
const txInput = psbt.txInputs[inputIndex];
if (!input || !txInput) {
errors.push({
rule: 'input_missing',
message: `Input data missing for index ${inputIndex}`,
index: inputIndex,
severity: 'critical',
});
return Promise.resolve({
index: inputIndex,
valid: false,
canFinalize: false,
errors,
warnings,
analysis: this.getEmptyInputAnalysis(),
});
}
// Validate UTXO presence
if (!input.witnessUtxo && !input.nonWitnessUtxo) {
errors.push({
rule: 'missing_utxo',
message: `Input ${inputIndex} missing UTXO information`,
index: inputIndex,
severity: 'critical',
suggestion: 'Add witnessUtxo or nonWitnessUtxo',
});
}
// Validate signatures
this.validateInputSignatures(input, inputIndex, errors, warnings);
// Validate scripts
this.validateInputScripts(input, inputIndex, errors, warnings);
// Validate sequence numbers
this.validateSequenceNumber(
txInput.sequence ?? 0xffffffff,
inputIndex,
warnings,
);
// Generate input analysis
const analysis = this.analyzeInput(psbt, inputIndex);
return Promise.resolve({
index: inputIndex,
valid: errors.filter((e) => e.severity === 'critical').length === 0,
canFinalize: this.canFinalizeInput(psbt, inputIndex),
errors,
warnings,
analysis,
});
}
/**
* Validate specific output
*/
validateOutput(
psbt: Psbt,
outputIndex: number,
): Promise<OutputValidationResult> {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
if (outputIndex >= psbt.txOutputs.length) {
errors.push({
rule: 'output_index_bounds',
message: `Output index ${outputIndex} out of bounds`,
index: outputIndex,
severity: 'critical',
});
return Promise.resolve({
index: outputIndex,
valid: false,
errors,
warnings,
analysis: this.getEmptyOutputAnalysis(),
});
}
const output = psbt.data.outputs[outputIndex];
const txOutput = psbt.txOutputs[outputIndex];
if (!txOutput) {
errors.push({
rule: 'output_missing',
message: `Output data missing for index ${outputIndex}`,
index: outputIndex,
severity: 'critical',
});
return Promise.resolve({
index: outputIndex,
valid: false,
errors,
warnings,
analysis: this.getEmptyOutputAnalysis(),
});
}
// Validate output value
this.validateOutputValue(txOutput.value, outputIndex, errors, warnings);
// Validate output script
this.validateOutputScript(txOutput.script, outputIndex, errors, warnings);
// Validate derivation paths
if (output?.bip32Derivation) {
this.validateBIP32Derivation(
output.bip32Derivation,
outputIndex,
warnings,
);
}
// Generate output analysis
const analysis = this.analyzeOutput(psbt, outputIndex);
return Promise.resolve({
index: outputIndex,
valid: errors.filter((e) => e.severity === 'critical').length === 0,
errors,
warnings,
analysis,
});
}
/**
* Check if PSBT can be finalized
*/
canFinalize(psbt: Psbt): Promise<boolean> {
try {
for (let i = 0; i < psbt.inputCount; i++) {
if (!this.canFinalizeInput(psbt, i)) {
return Promise.resolve(false);
}
}
return Promise.resolve(true);
} catch {
return Promise.resolve(false);
}
}
/**
* Get missing components for finalization
*/
getMissingComponents(psbt: Psbt): Promise<
Array<{
inputIndex: number;
missing: string[];
}>
> {
const missing: Array<{ inputIndex: number; missing: string[] }> = [];
for (let i = 0; i < psbt.inputCount; i++) {
const inputMissing = this.getMissingInputComponents(psbt, i);
if (inputMissing.length > 0) {
missing.push({
inputIndex: i,
missing: inputMissing,
});
}
}
return Promise.resolve(missing);
}
/**
* Analyze PSBT structure and completeness
*/
async analyze(psbt: Psbt): Promise<PSBTAnalysisReport> {
const validation = await this.validate(psbt);
const finalizationReadiness = await this.getFinalizationReadiness(psbt);
const security = this.analyzeSecurityRisks(psbt, validation);
const compatibility = this.analyzeCompatibility(psbt);
const completionPercentage = this.calculateCompletionPercentage(psbt);
return {
summary: {
valid: validation.valid,
canFinalize: validation.canFinalize,
completionPercentage,
estimatedFee: validation.transactionAnalysis.fee,
estimatedFeeRate: validation.transactionAnalysis.feeRate,
},
validation,
finalization: finalizationReadiness,
security,
compatibility,
};
}
// Private helper methods
private initializeValidationRules(): Map<string, PSBTValidationRule> {
const _rules = new Map<string, PSBTValidationRule>();
// Add validation rules
_rules.set('psbt_structure', {
name: 'psbt_structure',
description: 'PSBT has valid structure',
category: 'structure',
critical: true,
});
_rules.set('input_utxo', {
name: 'input_utxo',
description: 'All inputs have UTXO information',
category: 'structure',
critical: true,
});
_rules.set('signature_validation', {
name: 'signature_validation',
description: 'All signatures are valid',
category: 'signature',
critical: true,
});
_rules.set('script_validation', {
name: 'script_validation',
description: 'All scripts are valid',
category: 'script',
critical: true,
});
_rules.set('fee_validation', {
name: 'fee_validation',
description: 'Transaction fee is reasonable',
category: 'fee',
critical: false,
});
return _rules;
}
private validatePSBTStructure(
psbt: Psbt,
errors: ValidationError[],
warnings: ValidationWarning[],
passed: string[],
): Promise<void> {
// Validate version
if (psbt.version < 1 || psbt.version > 2) {
warnings.push({
rule: 'version_compatibility',
message: `Unusual transaction version: ${psbt.version}`,
recommendation: 'Consider using version 2',
});
}
// Validate input/output counts
if (psbt.inputCount === 0) {
errors.push({
rule: 'no_inputs',
message: 'PSBT has no inputs',
severity: 'critical',
});
}
if (psbt.txOutputs.length === 0) {
errors.push({
rule: 'no_outputs',
message: 'PSBT has no outputs',
severity: 'critical',
});
}
passed.push('psbt_structure');
return Promise.resolve();
}
private validateTransactionLevel_func(
psbt: Psbt,
errors: ValidationError[],
warnings: ValidationWarning[],
passed: string[],
): Promise<void> {
void errors; // Mark as intentionally unused
// Validate locktime
if (psbt.locktime > 0 && psbt.locktime < 500000000) {
// Block height locktime
warnings.push({
rule: 'block_locktime',
message: `Transaction locked until block ${psbt.locktime}`,
});
} else if (psbt.locktime >= 500000000) {
// Timestamp locktime
const lockDate = new Date(psbt.locktime * 1000);
warnings.push({
rule: 'time_locktime',
message: `Transaction locked until ${lockDate.toISOString()}`,
});
}
// Validate fee
try {
const fee = this.calculateFee(psbt);
const size = this.estimateTransactionSize(psbt);
const feeRate = fee / size;
if (feeRate < 1) {
warnings.push({
rule: 'low_fee_rate',
message: `Low fee rate: ${feeRate.toFixed(2)} sat/vB`,
recommendation: 'Consider increasing fee for faster confirmation',
});
}
if (feeRate > 1000) {
warnings.push({
rule: 'high_fee_rate',
message: `Very high fee rate: ${feeRate.toFixed(2)} sat/vB`,
recommendation: 'Verify fee is intentional',
});
}
} catch {
warnings.push({
rule: 'fee_calculation',
message: 'Unable to calculate fee - missing UTXO information',
});
}
passed.push('transaction_level');
return Promise.resolve();
}
private validateInputSignatures(
input: any,
inputIndex: number,
errors: ValidationError[],
warnings: ValidationWarning[],
): void {
const signatures = input.partialSig || [];
if (signatures.length === 0) {
warnings.push({
rule: 'no_signatures',
message: `Input ${inputIndex} has no signatures`,
index: inputIndex,
recommendation: 'Sign input before finalizing',
});
return;
}
// Validate signature format
for (const sig of signatures) {
if (
!Buffer.isBuffer(sig.signature) ||
sig.signature.length < 8 ||
sig.signature.length > 73
) {
errors.push({
rule: 'invalid_signature',
message: `Invalid signature format for input ${inputIndex}`,
index: inputIndex,
severity: 'high',
});
}
if (
!Buffer.isBuffer(sig.pubkey) ||
(sig.pubkey.length !== 33 && sig.pubkey.length !== 65)
) {
errors.push({
rule: 'invalid_pubkey',
message: `Invalid public key format for input ${inputIndex}`,
index: inputIndex,
severity: 'high',
});
}
}
}
private validateInputScripts(
input: any,
inputIndex: number,
errors: ValidationError[],
_warnings: ValidationWarning[],
): void {
// Check for required scripts based on input type
if (input.redeemScript && input.redeemScript.length === 0) {
errors.push({
rule: 'empty_redeem_script',
message: `Empty redeem script for input ${inputIndex}`,
index: inputIndex,
severity: 'high',
});
}
if (input.witnessScript && input.witnessScript.length === 0) {
errors.push({
rule: 'empty_witness_script',
message: `Empty witness script for input ${inputIndex}`,
index: inputIndex,
severity: 'high',
});
}
}
private validateSequenceNumber(
sequence: number,
inputIndex: number,
warnings: ValidationWarning[],
): void {
if (sequence < 0xfffffffe) {
warnings.push({
rule: 'rbf_enabled',
message: `Input ${inputIndex} signals RBF (Replace-By-Fee)`,
index: inputIndex,
});
}
if (sequence < 0xf0000000) {
warnings.push({
rule: 'csv_timelock',
message: `Input ${inputIndex} has CSV (CheckSequenceVerify) timelock`,
index: inputIndex,
});
}
}
private validateOutputValue(
value: number,
outputIndex: number,
errors: ValidationError[],
warnings: ValidationWarning[],
): void {
if (value < 0) {
errors.push({
rule: 'negative_output_value',
message: `Output ${outputIndex} has negative value`,
index: outputIndex,
severity: 'critical',
});
}
if (value > 0 && value < 546) {
warnings.push({
rule: 'dust_output',
message: `Output ${outputIndex} is dust (${value} sat)`,
index: outputIndex,
recommendation: 'Consider consolidating with other outputs',
});
}
}
private validateOutputScript(
script: Buffer,
outputIndex: number,
errors: ValidationError[],
warnings: ValidationWarning[],
): void {
if (script.length === 0) {
errors.push({
rule: 'empty_output_script',
message: `Output ${outputIndex} has empty script`,
index: outputIndex,
severity: 'critical',
});
return;
}
// Basic script validation
try {
bitcoin.address.fromOutputScript(script, this.network);
// If we can extract an address, the script is likely valid
} catch {
// Check if it's an OP_RETURN script
if (script[0] === 0x6a) {
// OP_RETURN is valid
if (script.length > 83) {
warnings.push({
rule: 'large_op_return',
message: `Output ${outputIndex} has large OP_RETURN (${script.length} bytes)`,
index: outputIndex,
});
}
} else {
warnings.push({
rule: 'unrecognized_script',
message: `Output ${outputIndex} has unrecognized script type`,
index: outputIndex,
});
}
}
}
private validateBIP32Derivation(
derivations: any[],
index: number,
warnings: ValidationWarning[],
): void {
for (const derivation of derivations) {
if (!derivation.path || !derivation.path.startsWith('m/')) {
warnings.push({
rule: 'invalid_derivation_path',
message: `Invalid BIP32 derivation path at index ${index}`,
index,
});
}
}
}
private canFinalizeInput(psbt: Psbt, inputIndex: number): boolean {
try {
const input = psbt.data.inputs[inputIndex];
if (!input) return false;
// Check for basic requirements
if (!input.witnessUtxo && !input.nonWitnessUtxo) return false;
if (!input.partialSig || input.partialSig.length === 0) return false;
// Additional checks based on script type would go here
return true;
} catch {
return false;
}
}
private getMissingInputComponents(psbt: Psbt, inputIndex: number): string[] {
const missing: string[] = [];
const input = psbt.data.inputs[inputIndex];
if (!input) {
missing.push('input_data');
return missing;
}
if (!input.witnessUtxo && !input.nonWitnessUtxo) {
missing.push('utxo_information');
}
if (!input.partialSig || input.partialSig.length === 0) {
missing.push('signatures');
}
// Check for scripts based on UTXO type
if (input.witnessUtxo) {
const script = input.witnessUtxo.script;
if (this.isP2SH(script) && !input.redeemScript) {
missing.push('redeem_script');
}
if (this.isP2WSH(script) && !input.witnessScript) {
missing.push('witness_script');
}
}
return missing;
}
private analyzeInput(psbt: Psbt, inputIndex: number): InputAnalysis {
const input = psbt.data.inputs[inputIndex];
if (!input) return this.getEmptyInputAnalysis();
const analysis: InputAnalysis = {
inputType: this.detectInputType(input),
hasWitnessUtxo: !!input.witnessUtxo,
hasNonWitnessUtxo: !!input.nonWitnessUtxo,
hasRedeemScript: !!input.redeemScript,
hasWitnessScript: !!input.witnessScript,
signaturesCount: input.partialSig?.length || 0,
signaturesRequired: this.getRequiredSignatures(input),
derivationPathsCount: input.bip32Derivation?.length || 0,
sighashTypes: this.extractSighashTypes(input),
estimatedSize: this.estimateInputSize(input),
};
return analysis;
}
private analyzeOutput(psbt: Psbt, outputIndex: number): OutputAnalysis {
const output = psbt.data.outputs[outputIndex];
const txOutput = psbt.txOutputs[outputIndex];
if (!txOutput) return this.getEmptyOutputAnalysis();
const extractedAddress = this.extractAddress(txOutput.script);
const analysis: OutputAnalysis = {
outputType: this.detectOutputType(txOutput.script),
value: txOutput.value,
isChange: this.isChangeOutput(output, outputIndex),
aboveDustThreshold: txOutput.value >= 546,
derivationPathsCount: output?.bip32Derivation?.length || 0,
};
if (extractedAddress) {
analysis.address = extractedAddress;
}
return analysis;
}
private analyzeTransaction(psbt: Psbt): TransactionAnalysis {
const totalInputValue = this.calculateTotalInputValue(psbt);
const totalOutputValue = this.calculateTotalOutputValue(psbt);
const fee = Math.max(0, totalInputValue - totalOutputValue);
const estimatedSize = this.estimateTransactionSize(psbt);
const feeRate = estimatedSize > 0 ? fee / estimatedSize : 0;
return {
version: psbt.version,
locktime: psbt.locktime,
inputCount: psbt.inputCount,
outputCount: psbt.txOutputs.length,
totalInputValue,
totalOutputValue,
fee,
feeRate,
estimatedSize,
rbfEnabled: this.isRBFEnabled(psbt),
isSegwit: this.isSegwitTransaction(psbt),
complexityScore: this.calculateComplexityScore(psbt),
};
}
private getFinalizationReadiness(psbt: Psbt): Promise<{
ready: boolean;
readyInputs: number[];
blockedInputs: Array<
{ index: number; reason: string; missingComponents: string[] }
>;
}> {
const readyInputs: number[] = [];
const blockedInputs: Array<
{ index: number; reason: string; missingComponents: string[] }
> = [];
for (let i = 0; i < psbt.inputCount; i++) {
if (this.canFinalizeInput(psbt, i)) {
readyInputs.push(i);
} else {
const missing = this.getMissingInputComponents(psbt, i);
blockedInputs.push({
index: i,
reason: `Missing: ${missing.join(', ')}`,
missingComponents: missing,
});
}
}
return Promise.resolve({
ready: blockedInputs.length === 0,
readyInputs,
blockedInputs,
});
}
private analyzeSecurityRisks(
_psbt: Psbt,
validation: PSBTValidationResult,
): {
riskLevel: 'low' | 'medium' | 'high' | 'critical';
risks: string[];
recommendations: string[];
} {
const risks: string[] = [];
const recommendations: string[] = [];
// High fee risk
if (validation.transactionAnalysis.feeRate > 100) {
risks.push('Very high fee rate');
recommendations.push('Verify fee is intentional');
}
// Large transaction risk
if (validation.transactionAnalysis.estimatedSize > 100000) {
risks.push('Large transaction size may cause relay issues');
recommendations.push('Consider breaking into smaller transactions');
}
// Complexity risk
if (validation.transactionAnalysis.complexityScore > 8) {
risks.push('Complex transaction structure');
recommendations.push('Review transaction carefully before broadcasting');
}
const riskLevel = this.calculateRiskLevel(
risks.length,
validation.criticalErrors.length,
);
return {
riskLevel,
risks,
recommendations,
};
}
private analyzeCompatibility(psbt: Psbt): {
bitcoinjsLib: boolean;
bip174: boolean;
} {
return {
bitcoinjsLib: true, // Assuming compatibility since we're using bitcoinjs-lib
bip174: this.checkBIP174Compliance(psbt),
};
}
// Helper methods with basic implementations
private calculateFee(psbt: Psbt): number {
const inputValue = this.calculateTotalInputValue(psbt);
const outputValue = this.calculateTotalOutputValue(psbt);
return Math.max(0, inputValue - outputValue);
}
private calculateTotalInputValue(psbt: Psbt): number {
let total = 0;
for (let i = 0; i < psbt.inputCount; i++) {
const input = psbt.data.inputs[i];
if (input?.witnessUtxo) {
total += input.witnessUtxo.value;
}
// Note: For non-witness UTXO, we'd need to parse the transaction
}
return total;
}
private calculateTotalOutputValue(psbt: Psbt): number {
let total = 0;
for (let i = 0; i < psbt.txOutputs.length; i++) {
const output = psbt.txOutputs[i];
if (output) {
total += output.value;
}
}
return total;
}
private estimateTransactionSize(psbt: Psbt): number {
// Rough estimation
const baseSize = 10;
const inputSize = psbt.inputCount * 150;
const outputSize = psbt.txOutputs.length * 34;
return baseSize + inputSize + outputSize;
}
private calculateCompletionPercentage(psbt: Psbt): number {
let totalRequired = 0;
let totalPresent = 0;
for (let i = 0; i < psbt.inputCount; i++) {
totalRequired += 2; // UTXO + signature
const input = psbt.data.inputs[i];
if (input?.witnessUtxo || input?.nonWitnessUtxo) totalPresent++;
if (input?.partialSig && input.partialSig.length > 0) totalPresent++;
}
return totalRequired > 0 ? (totalPresent / totalRequired) * 100 : 0;
}
// Simple helper implementations
private detectInputType(input: any): InputAnalysis['inputType'] {
if (input.witnessScript) return 'P2WSH';
if (input.redeemScript) return 'P2SH';
if (input.witnessUtxo) return 'P2WPKH';
return 'P2PKH';
}
private detectOutputType(script: Buffer): OutputAnalysis['outputType'] {
if (script[0] === 0x6a) return 'OP_RETURN';
if (script.length === 22 && script[0] === 0x00 && script[1] === 0x14) {
return 'P2WPKH';
}
if (script.length === 34 && script[0] === 0x00 && script[1] === 0x20) {
return 'P2WSH';
}
if (script.length === 23 && script[0] === 0xa9 && script[22] === 0x87) {
return 'P2SH';
}
if (script.length === 25 && script[0] === 0x76 && script[1] === 0xa9) {
return 'P2PKH';
}
return 'unknown';
}
private getRequiredSignatures(_input: any): number {
// This would need script analysis for multisig
return 1;
}
private extractSighashTypes(input: any): number[] {
const types: number[] = [];
if (input.partialSig) {
for (const sig of input.partialSig) {
if (sig.signature && sig.signature.length > 0) {
types.push(sig.signature[sig.signature.length - 1]);
}
}
}
return types;
}
private estimateInputSize(input: any): number {
if (input.witnessScript) return 150;
if (input.redeemScript) return 120;
if (input.witnessUtxo) return 68;
return 148;
}
private isChangeOutput(output: any, _index: number): boolean {
return !!(output?.bip32Derivation && output.bip32Derivation.length > 0);
}
private extractAddress(script: Buffer): string | undefined {
try {
return bitcoin.address.fromOutputScript(script, this.network);
} catch {
return undefined;
}
}
private isP2SH(script: Buffer): boolean {
return script.length === 23 && script[0] === 0xa9 && script[22] === 0x87;
}
private isP2WSH(script: Buffer): boolean {
return script.length === 34 && script[0] === 0x00 && script[1] === 0x20;
}
private isRBFEnabled(psbt: Psbt): boolean {
for (let i = 0; i < psbt.inputCount; i++) {
const sequence = psbt.txInputs[i]?.sequence ?? 0xffffffff;
if (sequence < 0xfffffffe) return true;
}
return false;
}
private isSegwitTransaction(psbt: Psbt): boolean {
for (let i = 0; i < psbt.inputCount; i++) {
const input = psbt.data.inputs[i];
if (input?.witnessUtxo || input?.witnessScript) return true;
}
return false;
}
private calculateComplexityScore(psbt: Psbt): number {
let score = 0;
score += psbt.inputCount;
score += psbt.txOutputs.length;
for (let i = 0; i < psbt.inputCount; i++) {
const input = psbt.data.inputs[i];
if (input?.redeemScript) score += 2;
if (input?.witnessScript) score += 2;
if (input?.partialSig && input.partialSig.length > 1) {
score += input.partialSig.length;
}
}
return score;
}
private calculateRiskLevel(
riskCount: number,
criticalErrorCount: number,
): 'low' | 'medium' | 'high' | 'critical' {
if (criticalErrorCount > 0) return 'critical';
if (riskCount >= 3) return 'high';
if (riskCount >= 1) return 'medium';
return 'low';
}
private checkBIP174Compliance(psbt: Psbt): boolean {
// Basic BIP-174 compliance check
try {
psbt.toBase64(); // If this works, basic structure is compliant
return true;
} catch {
return false;
}
}
private _hasComplexScripts(psbt: Psbt): boolean {
for (let i = 0; i < psbt.inputCount; i++) {
const input = psbt.data.inputs[i];
if (
input?.witnessScript ||
(input?.redeemScript && input.redeemScript.length > 23)
) {
return true;
}
}
return false;
}
private getEmptyInputAnalysis(): InputAnalysis {
return {
inputType: 'unknown',
hasWitnessUtxo: false,
hasNonWitnessUtxo: false,
hasRedeemScript: false,
hasWitnessScript: false,
signaturesCount: 0,
signaturesRequired: 0,
derivationPathsCount: 0,
sighashTypes: [],
estimatedSize: 0,
};
}
private getEmptyOutputAnalysis(): OutputAnalysis {
return {
outputType: 'unknown',
value: 0,
isChange: false,
aboveDustThreshold: false,
derivationPathsCount: 0,
};
}
private getEmptyTransactionAnalysis(): TransactionAnalysis {
return {
version: 0,
locktime: 0,
inputCount: 0,
outputCount: 0,
totalInputValue: 0,
totalOutputValue: 0,
fee: 0,
feeRate: 0,
estimatedSize: 0,
rbfEnabled: false,
isSegwit: false,
complexityScore: 0,
};
}
}