Huge commit. Multiple fixes. Refactored commands. Invitations, resources, template inspection, mnemonic stuff, cli utils, pretty printing, remove unreserve on start, fix connectino requirement for invitations, format cashAddress to lockingBytecode on send, lots and lots of other stuff.

This commit is contained in:
2026-04-06 11:56:09 +00:00
parent b475b23beb
commit 55c75501d5
24 changed files with 3284 additions and 77 deletions

View File

@@ -0,0 +1,276 @@
import { existsSync, readFileSync } from "fs";
import path from "path";
import { generateTemplateIdentifier } from "@xo-cash/engine";
import type { XOTemplate } from "@xo-cash/types";
import { bold, dim, formatObject, objectPrint } from "../cli-utils.js";
import { resolveTemplateReferences } from "../../utils/templates.js";
import type { CommandDependencies } from "./types.js";
/**
* Prints the help message for the template command
*/
export const printTemplateHelp = () => {
console.log(
`
${bold("Usage:")} xo-cli template <sub-command>
${bold("Sub-commands:")}
- import <template-file> ${dim("Import a template from a file")}
- list ${dim("List all templates")}
- list <category> <identifier> ${dim("List all options of the field type in a template")}
- inspect <category> <identifier> <field> ${dim("Inspect a field in a template")}
- set-default <template-file> <output-identifier> <role-identifier> ${dim("Set the default template")}
`);
};
/**
* Handles the template list command.
* @param deps - The command dependencies.
* @param args - Positional args after the command name, e.g. ["list", "action"] or ["list", "action", "1234567890"].
*/
export const handleTemplateListCommand = async (deps: CommandDependencies, args: string[]): Promise<void> => {
const templateCategory = args[0];
deps.verboseLogger(`Template list category: ${templateCategory}`);
// If no category was provided to list, we assume its listing out the templates
if (!templateCategory) {
const templates = await deps.app.engine.listImportedTemplates();
const formattedTemplates = templates.map((template: XOTemplate) => `${bold(generateTemplateIdentifier(template))} - ${dim(template.name)} ${dim(template.description)}`);
console.log(formattedTemplates.join('\n'));
return;
}
// Extract the template identifier from the positional args
const templateIdentifier = args[1];
deps.verboseLogger(`Template identifier: ${templateIdentifier}`);
if (!templateIdentifier) {
console.error("No template identifier provided");
return;
}
// Get the template from the engine
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
if (!rawTemplate) {
console.error(`No template found: ${templateIdentifier}`);
return;
}
// Resolve the template references
const template = await resolveTemplateReferences(rawTemplate);
deps.verboseLogger(`Template: ${formatObject(template)}`);
// List the templates in the category
switch (templateCategory) {
case "action": {
const actions = template.actions;
const formattedActions = Object.entries(actions).map(([actionIdentifier, action]) => `${bold(actionIdentifier)} ${dim(action.name)} ${dim(action.description)}`);
console.log(formattedActions.join('\n'));
break;
}
case "transaction": {
const transactions = template.transactions;
const formattedTransactions = Object.entries(transactions).map(([transactionIdentifier, transaction]) => `${bold(transactionIdentifier)} ${dim(transaction.name)} ${dim(transaction.description)}`);
console.log(formattedTransactions.join('\n'));
break;
}
case "output": {
const outputs = template.outputs;
const formattedOutputs = Object.entries(outputs).map(([outputIdentifier, output]) => `${bold(outputIdentifier)} ${dim(output.name)} ${dim(output.description)}`);
console.log(formattedOutputs.join('\n'));
break;
}
case "lockingscript": {
const lockingscripts = template.lockingScripts;
const formattedLockingscripts = Object.entries(lockingscripts).map(([lockingScriptIdentifier, lockingScript]) => `${bold(lockingScriptIdentifier)} ${dim(lockingScript.name)} ${dim(lockingScript.description)}`);
console.log(formattedLockingscripts.join('\n'));
break;
}
case "variable": {
const variables = template.variables || {};
const formattedVariables = Object.entries(variables).map(([variableIdentifier, variable]) => `${bold(variableIdentifier)} ${dim(variable.name)} ${dim(variable.description)}`);
console.log(formattedVariables.join('\n'));
break;
}
default: {
deps.verboseLogger(`Unknown template category: ${templateCategory}`);
return;
}
}
}
/**
* Prints the help message for the template inspect command
*/
export const printTemplateInspectHelp = () => {
console.log(
`
${bold("Usage:")} xo-cli template inspect <category> <identifier> <field>
${bold("Arguments:")}
<category> ${dim("The category of the template to inspect")}
<identifier> ${dim("The identifier of the template to inspect")}
<field> ${dim("The field of the template to inspect")}
${bold("Categories:")}
- action <action-identifier> ${dim("Inspect an action")}
- transaction <transaction-identifier> ${dim("Inspect a transaction")}
- output <output-identifier> ${dim("Inspect an output")}
- lockingscript <lockingscript-identifier> ${dim("Inspect a lockingscript")}
- variable <variable-identifier> ${dim("Inspect a variable")}
`);
};
/**
* Handles the template inspect command.
* @param deps - The command dependencies.
* @param args - Positional args after the command name, e.g. ["inspect", "transaction", "1234567890"].
*/
export const handleTemplateInspectCommand = async (deps: CommandDependencies, args: string[]): Promise<void> => {
const templateCategory = args[0];
const templateIdentifier = args[1];
const templateField = args[2];
deps.verboseLogger(`Template inspect args - category: ${templateCategory}, identifier: ${templateIdentifier}, field: ${templateField}`);
if (!templateCategory || !templateIdentifier || !templateField) {
console.log("No template category, identifier, or field provided");
printTemplateInspectHelp();
return;
}
// Get the template from the engine
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
if (!rawTemplate) {
console.error(`No template found: ${templateIdentifier}`);
return;
}
// Resolve the template references
const template = await resolveTemplateReferences(rawTemplate);
deps.verboseLogger(`Template: ${formatObject(template)}`);
// Inspect the template in the category
switch (templateCategory) {
case "action": {
const action = template.actions[templateField];
if (!action) {
console.error(`No action found: ${templateField}`);
return;
}
objectPrint(action);
break;
}
case "transaction": {
const transaction = template.transactions[templateField];
if (!transaction) {
console.error(`No transaction found: ${templateField}`);
return;
}
objectPrint(transaction);
break;
}
case "output": {
const output = template.outputs[templateField];
if (!output) {
console.error(`No output found: ${templateField}`);
return;
}
objectPrint(output);
break;
}
case "lockingscript": {
const lockingscript = template.lockingScripts[templateField];
if (!lockingscript) {
console.error(`No lockingscript found: ${templateField}`);
return;
}
objectPrint(lockingscript);
break;
}
case "variable": {
const variable = template.variables?.[templateField];
if (!variable) {
console.error(`No variable found: ${templateField}`);
return;
}
objectPrint(variable);
break;
}
default: {
deps.verboseLogger(`Unknown template category: ${templateCategory}`);
return;
}
}
}
/**
* Handles the template command.
* @param deps - The command dependencies.
* @param args - Positional args after the command name, e.g. ["import", "template.json"] or ["set-default", "tpl", "out", "role"].
* @param options - Parsed option flags.
*/
export const handleTemplateCommand = async (deps: CommandDependencies, args: string[], options: Record<string, string>): Promise<void> => {
const subCommand = args[0];
if (!subCommand) {
deps.verboseLogger("No sub-command provided");
printTemplateHelp();
return;
}
switch (subCommand) {
case "import": {
const templateFile = args[1];
deps.verboseLogger(`Template file: ${templateFile}`);
if (!templateFile) {
deps.verboseLogger("No template file provided");
printTemplateHelp();
return;
}
const templatePath = path.resolve(`${process.cwd()}/${templateFile}`);
deps.verboseLogger(`Template path: ${templatePath}`);
if (!existsSync(templatePath)) {
console.error(`Template file does not exist: ${templatePath}`);
printTemplateHelp();
return;
}
const template = await readFileSync(templatePath, "utf8");
deps.verboseLogger(`Importing template: ${templateFile}`);
await deps.app.engine.importTemplate(template);
deps.verboseLogger(`Template imported: ${templateFile}`);
break;
}
case "list": {
await handleTemplateListCommand(deps, args.slice(1));
break;
}
case "inspect": {
await handleTemplateInspectCommand(deps, args.slice(1));
break;
}
case "set-default": {
const templateFile = args[1];
const outputIdentifier = args[2];
const roleIdentifier = args[3];
if (!templateFile || !outputIdentifier || !roleIdentifier) {
deps.verboseLogger("No template file, output identifier, or role identifier provided");
printTemplateHelp();
return;
}
deps.verboseLogger(`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`);
await deps.app.engine.setDefaultLockingParameters(templateFile, outputIdentifier, roleIdentifier);
break;
}
default:
deps.verboseLogger(`Unknown template sub-command: ${subCommand}`);
printTemplateHelp();
return;
}
};