@btc-stamps/tx-builder
Version:
Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection
273 lines (235 loc) • 8.12 kB
text/typescript
import type { UTXO } from '../interfaces/provider.interface.ts';
import type { SelectionOptions } from '../interfaces/selector.interface.ts';
import type { EnhancedSelectionResult } from '../interfaces/selector-result.interface.ts';
import { SelectionFailureReason } from '../interfaces/selector-result.interface.ts';
import type {
IProtectionDetector,
ProtectedAssetData,
} from '../interfaces/protection.interface.ts';
import { BaseSelector } from './base-selector.ts';
/**
* Mock implementation for testing and development
* Allows manual specification of protected UTXOs
*/
export class MockProtectionDetector implements IProtectionDetector {
private protectedUtxos: Set<string>;
private assetData: Map<string, ProtectedAssetData>;
constructor(
protectedUtxos: string[] = [],
assetData: Map<string, ProtectedAssetData> = new Map(),
) {
this.protectedUtxos = new Set(protectedUtxos);
this.assetData = assetData;
}
async isProtectedUtxo(utxo: UTXO): Promise<boolean> {
const utxoId = `${utxo.txid}:${utxo.vout}`;
return await Promise.resolve(this.protectedUtxos.has(utxoId));
}
async getAssetData(utxo: UTXO): Promise<ProtectedAssetData | null> {
const utxoId = `${utxo.txid}:${utxo.vout}`;
return await Promise.resolve(this.assetData.get(utxoId) || null);
}
addProtectedUtxo(utxoId: string, assetData?: ProtectedAssetData): void {
this.protectedUtxos.add(utxoId);
if (assetData) {
this.assetData.set(utxoId, assetData);
}
}
removeProtectedUtxo(utxoId: string): void {
this.protectedUtxos.delete(utxoId);
this.assetData.delete(utxoId);
}
clearProtectedUtxos(): void {
this.protectedUtxos.clear();
this.assetData.clear();
}
getProtectedUtxoIds(): string[] {
return Array.from(this.protectedUtxos);
}
}
/**
* Protection-aware UTXO selector
*
* Wraps another selector and filters out UTXOs that contain
* valuable ordinals, stamps, or other protected assets.
*
* Can use different protection strategies:
* - Strict: Never use protected UTXOs (safest)
* - Careful: Use dummy UTXOs from protected assets if needed
* - Emergency: Use any UTXO as last resort (not recommended)
*/
export class ProtectionAwareSelector extends BaseSelector {
private detector: IProtectionDetector;
private fallbackSelector: BaseSelector;
private allowProtectedIfNecessary: boolean;
private dummyUtxoAmount: number;
constructor(
detector: IProtectionDetector,
fallbackSelector: BaseSelector,
allowProtectedIfNecessary = false,
dummyUtxoAmount = 546,
) {
super();
this.detector = detector;
this.fallbackSelector = fallbackSelector;
this.allowProtectedIfNecessary = allowProtectedIfNecessary;
this.dummyUtxoAmount = dummyUtxoAmount;
}
getName(): string {
return 'protection-aware';
}
select(utxos: UTXO[], options: SelectionOptions): EnhancedSelectionResult {
// Note: For synchronous operation, we assume all UTXOs are spendable
// Async protection checking should be done before calling this selector
const spendableUtxos = utxos;
const protectedUtxos: UTXO[] = [];
// For synchronous operation, protection detection should be done externally
// This selector now focuses on UTXO selection with pre-filtered inputs
// Try selection with only spendable UTXOs
let result = this.fallbackSelector.select(spendableUtxos, options);
// If selection failed and we allow protected UTXOs as last resort
if (
!result.success && this.allowProtectedIfNecessary && protectedUtxos.length > 0
) {
console.warn(
'ProtectionAwareSelector: No solution with spendable UTXOs, ' +
'considering protected UTXOs as last resort',
);
// Try with all UTXOs but prefer dummy UTXOs for ordinals
const dummyUtxos = protectedUtxos.filter(
(utxo) =>
utxo.value >= this.dummyUtxoAmount &&
utxo.value <= this.dummyUtxoAmount * 2,
);
// Combine dummy UTXOs with spendable ones
const combinedUtxos = [...spendableUtxos, ...dummyUtxos];
result = this.fallbackSelector.select(combinedUtxos, options);
// If still no solution, use all UTXOs as absolute last resort
if (!result.success) {
console.warn(
'ProtectionAwareSelector: WARNING - Using protected UTXOs to complete transaction',
);
result = this.fallbackSelector.select(utxos, options);
if (result.success) {
// Mark result as containing protected UTXOs
(result as any).containsProtectedUtxos = true;
(result as any).warning = 'This selection contains protected ordinals/stamps UTXOs';
}
}
}
// If still no solution, return structured failure
if (!result.success) {
return {
success: false,
reason: SelectionFailureReason.PROTECTED_UTXOS,
message: 'Could not find solution without using protected UTXOs',
details: {
utxoCount: utxos.length,
availableBalance: spendableUtxos.reduce((sum, utxo) => sum + utxo.value, 0),
},
};
}
return result;
}
/**
* Check if a specific UTXO is protected
*/
isProtected(utxo: UTXO): Promise<boolean> {
return this.detector.isProtectedUtxo(utxo);
}
/**
* Get asset data for a UTXO
*/
getAssetData(utxo: UTXO): Promise<ProtectedAssetData | null> {
return this.detector.getAssetData(utxo);
}
/**
* Filter UTXOs into protected and spendable categories
*/
private async categorizeUtxos(
utxos: UTXO[],
): Promise<{ spendableUtxos: UTXO[]; protectedUtxos: UTXO[] }> {
const spendableUtxos: UTXO[] = [];
const protectedUtxos: UTXO[] = [];
// Check each UTXO for protection status
for (const utxo of utxos) {
const isProtected = await this.detector.isProtectedUtxo(utxo);
if (isProtected) {
protectedUtxos.push(utxo);
} else {
spendableUtxos.push(utxo);
}
}
return { spendableUtxos, protectedUtxos };
}
/**
* Get protection summary for a set of UTXOs
*/
async getProtectionSummary(utxos: UTXO[]): Promise<{
totalUtxos: number;
protectedCount: number;
spendableCount: number;
totalValue: number;
protectedValue: number;
spendableValue: number;
protectedAssets: ProtectedAssetData[];
}> {
const { spendableUtxos, protectedUtxos } = await this.categorizeUtxos(utxos);
// Collect asset data for protected UTXOs
const protectedAssets: ProtectedAssetData[] = [];
for (const utxo of protectedUtxos) {
const assetData = await this.detector.getAssetData(utxo);
if (assetData) {
protectedAssets.push(assetData);
}
}
const totalValue = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
const protectedValue = protectedUtxos.reduce((sum, utxo) => sum + utxo.value, 0);
const spendableValue = spendableUtxos.reduce((sum, utxo) => sum + utxo.value, 0);
return {
totalUtxos: utxos.length,
protectedCount: protectedUtxos.length,
spendableCount: spendableUtxos.length,
totalValue,
protectedValue,
spendableValue,
protectedAssets,
};
}
/**
* Set whether to allow protected UTXOs if necessary
*/
setAllowProtectedIfNecessary(allow: boolean): void {
this.allowProtectedIfNecessary = allow;
}
/**
* Set the dummy UTXO amount for ordinal protection
*/
setDummyUtxoAmount(amount: number): void {
this.dummyUtxoAmount = amount;
}
/**
* Get the current fallback selector
*/
getFallbackSelector(): BaseSelector {
return this.fallbackSelector;
}
/**
* Set a new fallback selector
*/
setFallbackSelector(selector: BaseSelector): void {
this.fallbackSelector = selector;
}
/**
* Get the current protection detector
*/
getProtectionDetector(): IProtectionDetector {
return this.detector;
}
/**
* Set a new protection detector
*/
setProtectionDetector(detector: IProtectionDetector): void {
this.detector = detector;
}
}