153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
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;
|
|
};
|