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; }): 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 => { 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 | undefined => { const outputDefinition = template.outputs?.[outputIdentifier]; if (!outputDefinition || typeof outputDefinition.lockscript !== 'string') return undefined; const lockingScriptDefinition = (template.lockingScripts as Record | undefined)?.[outputDefinition.lockscript] as | { lockingScript?: string } | undefined; const scriptIdentifier = lockingScriptDefinition?.lockingScript; if (!scriptIdentifier) return undefined; const scriptExpression = (template.scripts as Record | 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(); 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; };