Fix receive and send
This commit is contained in:
170
src/utils/bch-mnemonic-url.ts
Normal file
170
src/utils/bch-mnemonic-url.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Handles BCH Mnemonic parsing to/from URL form.
|
||||
* Pulled directly from the old stack package.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
export type BCHMnemonicURLRaw = {
|
||||
entropy: Uint8Array;
|
||||
passphrase?: string;
|
||||
language?: (typeof BCHMnemonicURL.SUPPORTED_LANGUAGES)[number];
|
||||
comment?: string;
|
||||
path?: string;
|
||||
startHeight?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles BCHMnemonic URLs
|
||||
*/
|
||||
export class BCHMnemonicURL {
|
||||
static PROTOCOL = 'bch-mnemonic';
|
||||
|
||||
/**
|
||||
* Check if a URL is a valid wallet backup URL
|
||||
*
|
||||
* @param url The URL to check
|
||||
* @returns True if the URL is a valid wallet backup URL, false otherwise
|
||||
*/
|
||||
public static canHandle(urlStr: string): boolean {
|
||||
try {
|
||||
BCHMnemonicURL.fromURL(urlStr);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BCHMnemonic from a URL-encoded string
|
||||
* @param urlStr - The URL-encoded mnemonic string
|
||||
* @returns A new BCHMnemonic instance
|
||||
* @throws Error if the URL format is invalid or entropy is invalid
|
||||
*/
|
||||
static fromURL(urlStr: string): BCHMnemonicURL {
|
||||
const url = new URL(urlStr);
|
||||
|
||||
if (url.protocol !== `${BCHMnemonicURL.PROTOCOL}:`) {
|
||||
throw new Error(`Invalid URL protocol: ${url.protocol}`);
|
||||
}
|
||||
|
||||
// Decode the entropy.
|
||||
const entropy = new Uint8Array(Buffer.from(url.pathname, 'base64'));
|
||||
|
||||
// Pick out our encoding keys from the URL
|
||||
const params = BCHMnemonicURL.schema.parse(
|
||||
Object.fromEntries(url.searchParams.entries()),
|
||||
);
|
||||
|
||||
// Create and return the backup with validated parameters
|
||||
return BCHMnemonicURL.fromRaw({
|
||||
entropy,
|
||||
language: params[BCHMnemonicURL.ENCODING_KEYS.language],
|
||||
comment: params[BCHMnemonicURL.ENCODING_KEYS.comment],
|
||||
path: params[BCHMnemonicURL.ENCODING_KEYS.path],
|
||||
startHeight: params[BCHMnemonicURL.ENCODING_KEYS.startHeight],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WalletBackup from a raw object
|
||||
*
|
||||
* @param raw - The raw object to create the WalletBackup from
|
||||
* @returns The created WalletBackup
|
||||
*/
|
||||
static fromRaw(raw: BCHMnemonicURLRaw): BCHMnemonicURL {
|
||||
// Add entropy validation
|
||||
if (!raw.entropy || raw.entropy.length === 0) {
|
||||
throw new Error('Invalid entropy: must be non-empty');
|
||||
}
|
||||
|
||||
// Validate entropy length (typically 16, 20, 24, 28, or 32 bytes for BIP39)
|
||||
const validLengths = [16, 20, 24, 28, 32];
|
||||
if (!validLengths.includes(raw.entropy.length)) {
|
||||
throw new Error(`Invalid entropy length: ${raw.entropy.length} bytes`);
|
||||
}
|
||||
|
||||
return new BCHMnemonicURL(raw);
|
||||
}
|
||||
|
||||
constructor(protected raw: BCHMnemonicURLRaw) {}
|
||||
|
||||
toObject() {
|
||||
return this.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the backup into a URL encoding
|
||||
*
|
||||
* @param prefix - The prefix to use for the URL encoding
|
||||
* @returns The URL encoding of the backup
|
||||
*/
|
||||
toURL(): string {
|
||||
// Conver the mnemonic words into the entropy used to derive the mnemonic words
|
||||
const entropyBase64 = Buffer.from(this.raw.entropy).toString('base64');
|
||||
|
||||
// Create a new URL object with the prefix and the base64 encoded mnemonic
|
||||
const url = new URL(`${BCHMnemonicURL.PROTOCOL}:${entropyBase64}`);
|
||||
|
||||
// Add the raw values to the url encoded string. Only add the values that are defined.
|
||||
if (this.raw.language !== undefined) {
|
||||
url.searchParams.set(
|
||||
BCHMnemonicURL.ENCODING_KEYS.language,
|
||||
this.raw.language,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.raw.comment !== undefined) {
|
||||
url.searchParams.set(
|
||||
BCHMnemonicURL.ENCODING_KEYS.comment,
|
||||
this.raw.comment,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.raw.path !== undefined) {
|
||||
url.searchParams.set(BCHMnemonicURL.ENCODING_KEYS.path, this.raw.path);
|
||||
}
|
||||
|
||||
if (this.raw.startHeight !== undefined) {
|
||||
url.searchParams.set(
|
||||
BCHMnemonicURL.ENCODING_KEYS.startHeight,
|
||||
this.raw.startHeight.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
static ENCODING_KEYS = {
|
||||
language: 'l',
|
||||
passphrase: 'p',
|
||||
comment: 'c',
|
||||
path: 'd',
|
||||
startHeight: 'h',
|
||||
} as const;
|
||||
|
||||
static SUPPORTED_LANGUAGES = [
|
||||
'en',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'ja',
|
||||
'es',
|
||||
'pt',
|
||||
'ko',
|
||||
'fr',
|
||||
'it',
|
||||
'cs',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Zod schema for validating URL parameters
|
||||
*/
|
||||
static schema = z.object({
|
||||
[BCHMnemonicURL.ENCODING_KEYS.language]: z
|
||||
.enum(BCHMnemonicURL.SUPPORTED_LANGUAGES)
|
||||
.optional(),
|
||||
[BCHMnemonicURL.ENCODING_KEYS.passphrase]: z.string().optional(),
|
||||
[BCHMnemonicURL.ENCODING_KEYS.comment]: z.string().optional(),
|
||||
[BCHMnemonicURL.ENCODING_KEYS.path]: z.string().optional(),
|
||||
[BCHMnemonicURL.ENCODING_KEYS.startHeight]: z.coerce.number().optional(),
|
||||
});
|
||||
}
|
||||
@@ -1,259 +1,92 @@
|
||||
/**
|
||||
* History utility functions.
|
||||
*
|
||||
* Pure functions for parsing and formatting wallet history data.
|
||||
* These functions have no React dependencies and can be used
|
||||
* in both TUI and CLI contexts.
|
||||
*/
|
||||
import type { HistoryItem, HistoryInvitationItem, HistoryUtxoItem } from '../services/history.js';
|
||||
|
||||
import type { HistoryItem, HistoryItemType } from '../services/history.js';
|
||||
|
||||
/**
|
||||
* Color names for history item types.
|
||||
* These are semantic color names that can be mapped to actual colors
|
||||
* by the consuming application (TUI or CLI).
|
||||
*/
|
||||
export type HistoryColorName = 'info' | 'warning' | 'success' | 'error' | 'muted' | 'text';
|
||||
|
||||
/**
|
||||
* Formatted history list item data.
|
||||
*/
|
||||
export interface FormattedHistoryItem {
|
||||
/** The display label for the history item */
|
||||
export type HistoryRowType = 'invitation' | 'invitation_input' | 'invitation_output' | 'utxo';
|
||||
|
||||
export interface HistoryDisplayRow {
|
||||
id: string;
|
||||
type: HistoryRowType;
|
||||
label: string;
|
||||
/** Optional secondary description */
|
||||
description?: string;
|
||||
/** The formatted date string */
|
||||
dateStr?: string;
|
||||
/** The semantic color name for this item type */
|
||||
color: HistoryColorName;
|
||||
/** The history item type */
|
||||
type: HistoryItemType;
|
||||
/** Whether the item data is valid */
|
||||
isValid: boolean;
|
||||
timestamp?: number;
|
||||
isNested: boolean;
|
||||
utxo?: HistoryUtxoItem;
|
||||
invitation?: HistoryInvitationItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the semantic color name for a history item type.
|
||||
*
|
||||
* @param type - The history item type
|
||||
* @param isSelected - Whether the item is currently selected
|
||||
* @returns A semantic color name
|
||||
*/
|
||||
export function getHistoryItemColorName(type: HistoryItemType, isSelected: boolean = false): HistoryColorName {
|
||||
if (isSelected) return 'info'; // Use focus color when selected
|
||||
|
||||
switch (type) {
|
||||
case 'invitation_created':
|
||||
return 'text';
|
||||
case 'utxo_reserved':
|
||||
return 'warning';
|
||||
case 'utxo_received':
|
||||
return 'success';
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a satoshi value for display.
|
||||
*
|
||||
* @param satoshis - The value in satoshis
|
||||
* @returns Formatted string with BCH amount
|
||||
*/
|
||||
export function formatSatoshisValue(satoshis: bigint | number): string {
|
||||
const value = typeof satoshis === 'bigint' ? satoshis : BigInt(satoshis);
|
||||
const bch = Number(value) / 100_000_000;
|
||||
return `${bch.toFixed(8)} BCH (${value.toLocaleString()} sats)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp for display.
|
||||
*
|
||||
* @param timestamp - Unix timestamp in milliseconds
|
||||
* @returns Formatted date string or undefined
|
||||
*/
|
||||
export function formatHistoryDate(timestamp?: number): string | undefined {
|
||||
if (!timestamp) return undefined;
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a history item for display in a list.
|
||||
*
|
||||
* @param item - The history item to format
|
||||
* @param isSelected - Whether the item is currently selected
|
||||
* @returns Formatted item data for display
|
||||
*/
|
||||
export function formatHistoryListItem(
|
||||
item: HistoryItem | null | undefined,
|
||||
isSelected: boolean = false
|
||||
): FormattedHistoryItem {
|
||||
if (!item) {
|
||||
return {
|
||||
label: '',
|
||||
description: undefined,
|
||||
dateStr: undefined,
|
||||
color: 'muted',
|
||||
type: 'utxo_received',
|
||||
isValid: false,
|
||||
};
|
||||
}
|
||||
|
||||
const dateStr = formatHistoryDate(item.timestamp);
|
||||
const color = getHistoryItemColorName(item.type, isSelected);
|
||||
|
||||
switch (item.type) {
|
||||
case 'invitation_created':
|
||||
return {
|
||||
label: `[Invitation] ${item.description}`,
|
||||
description: undefined,
|
||||
dateStr,
|
||||
color,
|
||||
type: item.type,
|
||||
isValid: true,
|
||||
};
|
||||
|
||||
case 'utxo_reserved': {
|
||||
const satsStr = item.valueSatoshis !== undefined
|
||||
? formatSatoshisValue(item.valueSatoshis)
|
||||
: 'Unknown amount';
|
||||
return {
|
||||
label: `[Reserved] ${satsStr}`,
|
||||
description: item.description,
|
||||
dateStr,
|
||||
color,
|
||||
type: item.type,
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
case 'utxo_received': {
|
||||
const satsStr = item.valueSatoshis !== undefined
|
||||
? formatSatoshisValue(item.valueSatoshis)
|
||||
: 'Unknown amount';
|
||||
const reservedTag = item.reserved ? ' [Reserved]' : '';
|
||||
return {
|
||||
label: satsStr,
|
||||
description: `${item.description}${reservedTag}`,
|
||||
dateStr,
|
||||
color,
|
||||
type: item.type,
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
label: `${item.type}: ${item.description}`,
|
||||
description: undefined,
|
||||
dateStr,
|
||||
color: 'text',
|
||||
type: item.type,
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type label for display.
|
||||
*
|
||||
* @param type - The history item type
|
||||
* @returns Human-readable type label
|
||||
*/
|
||||
export function getHistoryTypeLabel(type: HistoryItemType): string {
|
||||
switch (type) {
|
||||
case 'invitation_created':
|
||||
return 'Invitation';
|
||||
case 'utxo_reserved':
|
||||
return 'Reserved';
|
||||
case 'utxo_received':
|
||||
return 'Received';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate scrolling window indices for a list.
|
||||
*
|
||||
* @param selectedIndex - Currently selected index
|
||||
* @param totalItems - Total number of items
|
||||
* @param maxVisible - Maximum visible items
|
||||
* @returns Start and end indices for the visible window
|
||||
*/
|
||||
export function calculateScrollWindow(
|
||||
selectedIndex: number,
|
||||
totalItems: number,
|
||||
maxVisible: number
|
||||
): { startIndex: number; endIndex: number } {
|
||||
const halfWindow = Math.floor(maxVisible / 2);
|
||||
let startIndex = Math.max(0, selectedIndex - halfWindow);
|
||||
const endIndex = Math.min(totalItems, startIndex + maxVisible);
|
||||
|
||||
// Adjust start if we're near the end
|
||||
if (endIndex - startIndex < maxVisible) {
|
||||
startIndex = Math.max(0, endIndex - maxVisible);
|
||||
}
|
||||
|
||||
return { startIndex, endIndex };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a history item is a UTXO-related event.
|
||||
*
|
||||
* @param item - The history item to check
|
||||
* @returns True if the item is UTXO-related
|
||||
*/
|
||||
export function isUtxoEvent(item: HistoryItem): boolean {
|
||||
return item.type === 'utxo_received' || item.type === 'utxo_reserved';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter history items by type.
|
||||
*
|
||||
* @param items - Array of history items
|
||||
* @param types - Types to include
|
||||
* @returns Filtered array
|
||||
*/
|
||||
export function filterHistoryByType(
|
||||
items: HistoryItem[],
|
||||
types: HistoryItemType[]
|
||||
): HistoryItem[] {
|
||||
return items.filter(item => types.includes(item.type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary statistics for history items.
|
||||
*
|
||||
* @param items - Array of history items
|
||||
* @returns Summary statistics
|
||||
*/
|
||||
export function getHistorySummary(items: HistoryItem[]): {
|
||||
totalReceived: bigint;
|
||||
totalReserved: bigint;
|
||||
invitationCount: number;
|
||||
utxoCount: number;
|
||||
} {
|
||||
let totalReceived = 0n;
|
||||
let totalReserved = 0n;
|
||||
let invitationCount = 0;
|
||||
let utxoCount = 0;
|
||||
export function buildHistoryDisplayRows(items: HistoryItem[]): HistoryDisplayRow[] {
|
||||
const rows: HistoryDisplayRow[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
switch (item.type) {
|
||||
case 'invitation_created':
|
||||
invitationCount++;
|
||||
break;
|
||||
case 'utxo_reserved':
|
||||
totalReserved += item.valueSatoshis ?? 0n;
|
||||
break;
|
||||
case 'utxo_received':
|
||||
totalReceived += item.valueSatoshis ?? 0n;
|
||||
utxoCount++;
|
||||
break;
|
||||
if (item.kind === 'invitation') {
|
||||
rows.push({
|
||||
id: item.id,
|
||||
type: 'invitation',
|
||||
label: item.description,
|
||||
timestamp: item.createdAtTimestamp,
|
||||
isNested: false,
|
||||
invitation: item,
|
||||
});
|
||||
|
||||
for (const input of item.inputs) {
|
||||
const satsPrefix = input.valueSatoshis !== undefined ? `${input.valueSatoshis.toLocaleString()} sats ` : '';
|
||||
rows.push({
|
||||
id: `${item.id}-input-${input.id}`,
|
||||
type: 'invitation_input',
|
||||
label: `${satsPrefix}${input.outpoint.txid}:${input.outpoint.index}`,
|
||||
description: input.description,
|
||||
isNested: true,
|
||||
utxo: input,
|
||||
invitation: item,
|
||||
});
|
||||
}
|
||||
|
||||
for (const output of item.outputs) {
|
||||
rows.push({
|
||||
id: `${item.id}-output-${output.id}`,
|
||||
type: 'invitation_output',
|
||||
label: output.valueSatoshis !== undefined ? `${output.valueSatoshis.toLocaleString()} sats` : 'Output',
|
||||
description: output.description,
|
||||
isNested: true,
|
||||
utxo: output,
|
||||
invitation: item,
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
id: item.id,
|
||||
type: 'utxo',
|
||||
label: item.valueSatoshis !== undefined ? `${item.valueSatoshis.toLocaleString()} sats` : 'UTXO',
|
||||
description: item.description,
|
||||
isNested: false,
|
||||
utxo: item,
|
||||
});
|
||||
}
|
||||
|
||||
return { totalReceived, totalReserved, invitationCount, utxoCount };
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function getHistoryItemColorName(row: HistoryDisplayRow, isSelected: boolean = false): HistoryColorName {
|
||||
if (isSelected) return 'info';
|
||||
switch (row.type) {
|
||||
case 'invitation':
|
||||
return 'text';
|
||||
case 'invitation_input':
|
||||
return 'error';
|
||||
case 'invitation_output':
|
||||
return 'success';
|
||||
case 'utxo':
|
||||
return row.utxo?.reserved ? 'warning' : 'success';
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
|
||||
152
src/utils/invitation-flow.ts
Normal file
152
src/utils/invitation-flow.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { XOTemplate, XOTemplateTransactionOutput } from '@xo-cash/types';
|
||||
import type { Invitation } from '../services/invitation.js';
|
||||
|
||||
export interface SelectableUtxoLike {
|
||||
outpointTransactionHash: string;
|
||||
outpointIndex: number;
|
||||
valueSatoshis: bigint;
|
||||
lockingBytecode?: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export const hasMissingRequirements = (missingRequirements: {
|
||||
variables?: string[];
|
||||
inputs?: string[];
|
||||
outputs?: string[];
|
||||
roles?: Record<string, unknown>;
|
||||
}): boolean => {
|
||||
return (
|
||||
(missingRequirements.variables?.length ?? 0) > 0
|
||||
|| (missingRequirements.inputs?.length ?? 0) > 0
|
||||
|| (missingRequirements.outputs?.length ?? 0) > 0
|
||||
|| (missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
export const isInvitationRequirementsComplete = async (invitation: Invitation): Promise<boolean> => {
|
||||
const missingRequirements = await invitation.getMissingRequirements();
|
||||
return !hasMissingRequirements(missingRequirements);
|
||||
};
|
||||
|
||||
export const resolveActionRoles = (
|
||||
template: XOTemplate | undefined,
|
||||
actionIdentifier: string | undefined,
|
||||
rolesFromNavigation?: string[],
|
||||
): string[] => {
|
||||
if (rolesFromNavigation && rolesFromNavigation.length > 0) {
|
||||
return [ ...new Set(rolesFromNavigation) ];
|
||||
}
|
||||
|
||||
if (!template || !actionIdentifier) return [];
|
||||
const starts = template.start ?? [];
|
||||
const roleIds = starts
|
||||
.filter((entry) => entry.action === actionIdentifier)
|
||||
.map((entry) => entry.role);
|
||||
|
||||
return [ ...new Set(roleIds) ];
|
||||
};
|
||||
|
||||
export const roleRequiresInputs = (
|
||||
template: XOTemplate | undefined,
|
||||
actionIdentifier: string | undefined,
|
||||
roleIdentifier: string | undefined,
|
||||
): boolean => {
|
||||
if (!template || !actionIdentifier || !roleIdentifier) return false;
|
||||
const action = template.actions?.[actionIdentifier];
|
||||
if (!action) return false;
|
||||
|
||||
const actionRole = action.roles?.[roleIdentifier];
|
||||
const roleSlotsMin = actionRole?.requirements?.slots?.min ?? 0;
|
||||
if (roleSlotsMin > 0) return true;
|
||||
|
||||
// Some templates specify slot/input requirements at action.requirements.roles
|
||||
// instead of role.requirements. Respect those as well.
|
||||
const roleRequirement = action.requirements?.roles?.find((requirement) => requirement.role === roleIdentifier);
|
||||
const actionLevelSlotsMin = roleRequirement?.slots?.min ?? 0;
|
||||
if (actionLevelSlotsMin > 0) return true;
|
||||
|
||||
const transactionIdentifier = action.transaction;
|
||||
const transaction = transactionIdentifier ? template.transactions?.[transactionIdentifier] : undefined;
|
||||
const roleInputs = transaction?.roles?.[roleIdentifier]?.inputs;
|
||||
|
||||
return (roleInputs?.length ?? 0) > 0;
|
||||
};
|
||||
|
||||
export const getTransactionOutputIdentifier = (output: XOTemplateTransactionOutput): string | undefined => {
|
||||
if (typeof output === 'string') return output;
|
||||
if (output && typeof output === 'object' && 'output' in output && typeof output.output === 'string') {
|
||||
return output.output;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const normalizeLockingBytecodeHex = (value: string): string => value.trim().replace(/^0x/i, '');
|
||||
|
||||
export const resolveProvidedLockingBytecodeHex = (
|
||||
template: XOTemplate,
|
||||
outputIdentifier: string,
|
||||
variableValues: Record<string, string>,
|
||||
): string | undefined => {
|
||||
const outputDefinition = template.outputs?.[outputIdentifier];
|
||||
if (!outputDefinition || typeof outputDefinition.lockscript !== 'string') return undefined;
|
||||
|
||||
const lockingScriptDefinition = (template.lockingScripts as Record<string, unknown> | undefined)?.[outputDefinition.lockscript] as
|
||||
| { lockingScript?: string }
|
||||
| undefined;
|
||||
const scriptIdentifier = lockingScriptDefinition?.lockingScript;
|
||||
if (!scriptIdentifier) return undefined;
|
||||
|
||||
const scriptExpression = (template.scripts as Record<string, unknown> | undefined)?.[scriptIdentifier];
|
||||
if (typeof scriptExpression !== 'string') return undefined;
|
||||
|
||||
const directVariableMatch = scriptExpression.match(/^<\s*([A-Za-z0-9_]+)\s*>$/);
|
||||
if (!directVariableMatch) return undefined;
|
||||
|
||||
const variableIdentifier = directVariableMatch[1];
|
||||
if (!variableIdentifier) return undefined;
|
||||
|
||||
const providedValue = variableValues[variableIdentifier];
|
||||
if (!providedValue) return undefined;
|
||||
|
||||
return normalizeLockingBytecodeHex(providedValue);
|
||||
};
|
||||
|
||||
export const mapUnspentOutputsToSelectable = (unspentOutputs: any[]): SelectableUtxoLike[] => {
|
||||
return unspentOutputs.map((utxo: any) => ({
|
||||
outpointTransactionHash: utxo.outpointTransactionHash,
|
||||
outpointIndex: utxo.outpointIndex,
|
||||
valueSatoshis: BigInt(utxo.valueSatoshis),
|
||||
lockingBytecode: utxo.lockingBytecode
|
||||
? typeof utxo.lockingBytecode === 'string'
|
||||
? utxo.lockingBytecode
|
||||
: Buffer.from(utxo.lockingBytecode).toString('hex')
|
||||
: undefined,
|
||||
selected: false,
|
||||
}));
|
||||
};
|
||||
|
||||
export const autoSelectGreedyUtxos = (
|
||||
utxos: SelectableUtxoLike[],
|
||||
requiredWithFee: bigint,
|
||||
): SelectableUtxoLike[] => {
|
||||
let accumulated = 0n;
|
||||
const seenLockingBytecodes = new Set<string>();
|
||||
|
||||
for (const utxo of utxos) {
|
||||
if (utxo.lockingBytecode && seenLockingBytecodes.has(utxo.lockingBytecode)) {
|
||||
continue;
|
||||
}
|
||||
if (utxo.lockingBytecode) {
|
||||
seenLockingBytecodes.add(utxo.lockingBytecode);
|
||||
}
|
||||
|
||||
utxo.selected = true;
|
||||
accumulated += utxo.valueSatoshis;
|
||||
|
||||
if (accumulated >= requiredWithFee) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return utxos;
|
||||
};
|
||||
@@ -84,6 +84,7 @@ export function getStateColorName(state: string): StateColorName {
|
||||
return 'warning';
|
||||
case 'ready':
|
||||
case 'signed':
|
||||
case 'complete':
|
||||
case 'broadcast':
|
||||
case 'completed':
|
||||
return 'success';
|
||||
|
||||
Reference in New Issue
Block a user