Tests. Autocomplete. Few Fixes. Mocks for Electrum Service. Template-to-Json parser. Fix global paths. Use IO Dependency injection for logging from cli. Additional commands in CLI.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
export type { CommandDependencies } from "./types.js";
|
||||
|
||||
export { handleMnemonicCommand, printMnemonicHelp } from "./mnemonic.js";
|
||||
export { handleTemplateCommand, printTemplateHelp } from "./template.js";
|
||||
export { handleInvitationCommand, printInvitationHelp } from "./invitation.js";
|
||||
export { handleReceiveCommand, printReceiveHelp } from "./receive.js";
|
||||
export { handleResourceCommand, printResourceHelp } from "./resource.js";
|
||||
|
||||
export * from "./types.js";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { generateTemplateIdentifier } from "@xo-cash/engine";
|
||||
import { binToHex, hexToBin } from "@bitauth/libauth";
|
||||
|
||||
import { bold, dim, formatObject } from "../cli-utils.js";
|
||||
import type { CommandDependencies } from "./types.js";
|
||||
import type { CommandDependencies, CommandIO } from "./types.js";
|
||||
import { CommandError } from "./types.js";
|
||||
import type { Invitation } from "../../services/invitation.js";
|
||||
import {
|
||||
resolveProvidedLockingBytecodeHex,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
autoSelectGreedyUtxos,
|
||||
} from "../../utils/invitation-flow.js";
|
||||
import { encodeExtendedJson } from "../../utils/ext-json.js";
|
||||
import { resolveTemplate } from "../utils.js";
|
||||
|
||||
const DEFAULT_FEE = 500n;
|
||||
const DUST_THRESHOLD = 546n;
|
||||
@@ -89,10 +91,10 @@ async function buildAppendParams(
|
||||
}));
|
||||
|
||||
if (inputs.length === 0) {
|
||||
console.error("No suitable UTXOs found for auto-input selection.");
|
||||
deps.io.err("No suitable UTXOs found for auto-input selection.");
|
||||
return null;
|
||||
}
|
||||
deps.verboseLogger(`Auto-selected ${inputs.length} input(s)`);
|
||||
deps.io.verbose(`Auto-selected ${inputs.length} input(s)`);
|
||||
} else if (options["addInput"]) {
|
||||
inputs = options["addInput"].split(",").map((entry) => {
|
||||
const separatorIndex = entry.lastIndexOf(":");
|
||||
@@ -110,7 +112,7 @@ async function buildAppendParams(
|
||||
};
|
||||
});
|
||||
}
|
||||
deps.verboseLogger(`Inputs: ${formatObject(inputs.map(i => ({ txHash: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })))}`);
|
||||
deps.io.verbose(`Inputs: ${formatObject(inputs.map(i => ({ txHash: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })))}`);
|
||||
|
||||
// --- Outputs ---
|
||||
// When --add-output is provided, use those identifiers explicitly.
|
||||
@@ -133,7 +135,7 @@ async function buildAppendParams(
|
||||
}
|
||||
outputIdentifiers = [...discovered];
|
||||
if (outputIdentifiers.length > 0) {
|
||||
deps.verboseLogger(`Auto-discovered output(s) from template: ${outputIdentifiers.join(", ")}`);
|
||||
deps.io.verbose(`Auto-discovered output(s) from template: ${outputIdentifiers.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,14 +163,14 @@ async function buildAppendParams(
|
||||
const lockingBytecodeHex = providedHex
|
||||
?? await invitation.generateLockingBytecode(outputId, roleIdentifier);
|
||||
|
||||
deps.verboseLogger(`Locking bytecode for output "${outputId}": ${lockingBytecodeHex}`);
|
||||
deps.io.verbose(`Locking bytecode for output "${outputId}": ${lockingBytecodeHex}`);
|
||||
return {
|
||||
outputIdentifier: outputId,
|
||||
lockingBytecode: new Uint8Array(Buffer.from(lockingBytecodeHex, "hex")),
|
||||
};
|
||||
}),
|
||||
);
|
||||
deps.verboseLogger(`Outputs: ${formatObject(outputs.map(o => o.outputIdentifier))}`);
|
||||
deps.io.verbose(`Outputs: ${formatObject(outputs.map(o => o.outputIdentifier))}`);
|
||||
|
||||
// --- Auto change output ---
|
||||
// When inputs are provided, look up each UTXO's value, compute the
|
||||
@@ -182,29 +184,29 @@ async function buildAppendParams(
|
||||
const txHashHex = binToHex(input.outpointTransactionHash);
|
||||
const utxo = utxoMap.get(`${txHashHex}:${input.outpointIndex}`);
|
||||
if (!utxo) {
|
||||
console.error(`UTXO not found: ${txHashHex}:${input.outpointIndex}. Make sure it exists in your wallet.`);
|
||||
deps.io.err(`UTXO not found: ${txHashHex}:${input.outpointIndex}. Make sure it exists in your wallet.`);
|
||||
return null;
|
||||
}
|
||||
totalInputSats += BigInt(utxo.valueSatoshis);
|
||||
}
|
||||
deps.verboseLogger(`Total input value: ${totalInputSats} satoshis`);
|
||||
deps.io.verbose(`Total input value: ${totalInputSats} satoshis`);
|
||||
|
||||
const requiredSats = await invitation.getSatsOut();
|
||||
deps.verboseLogger(`Required output value: ${requiredSats} satoshis`);
|
||||
deps.io.verbose(`Required output value: ${requiredSats} satoshis`);
|
||||
|
||||
const changeAmount = totalInputSats - requiredSats - DEFAULT_FEE;
|
||||
deps.verboseLogger(`Change amount: ${changeAmount} satoshis (fee: ${DEFAULT_FEE})`);
|
||||
deps.io.verbose(`Change amount: ${changeAmount} satoshis (fee: ${DEFAULT_FEE})`);
|
||||
|
||||
if (changeAmount < 0n) {
|
||||
console.error(`Insufficient funds. Inputs total ${totalInputSats} sats, but need ${requiredSats + DEFAULT_FEE} sats (${requiredSats} required + ${DEFAULT_FEE} fee).`);
|
||||
deps.io.err(`Insufficient funds. Inputs total ${totalInputSats} sats, but need ${requiredSats + DEFAULT_FEE} sats (${requiredSats} required + ${DEFAULT_FEE} fee).`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (changeAmount >= DUST_THRESHOLD) {
|
||||
outputs.push({ valueSatoshis: changeAmount });
|
||||
console.log(`Auto-adding change output: ${changeAmount} satoshis`);
|
||||
deps.io.out(`Auto-adding change output: ${changeAmount} satoshis`);
|
||||
} else if (changeAmount > 0n) {
|
||||
console.log(`Change ${changeAmount} sats is below dust threshold (${DUST_THRESHOLD} sats), donating to miners as fee.`);
|
||||
deps.io.out(`Change ${changeAmount} sats is below dust threshold (${DUST_THRESHOLD} sats), donating to miners as fee.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,8 +216,8 @@ async function buildAppendParams(
|
||||
/**
|
||||
* Prints the help message for the invitation command
|
||||
*/
|
||||
export const printInvitationHelp = () => {
|
||||
console.log(
|
||||
export const printInvitationHelp = (io: CommandIO): void => {
|
||||
io.out(
|
||||
`
|
||||
${bold("Usage:")} xo-cli invitation <sub-command>
|
||||
|
||||
@@ -242,82 +244,87 @@ ${bold("Create / Append options:")}
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Result data returned by invitation commands on success.
|
||||
*/
|
||||
export type InvitationCommandResult = {
|
||||
invitationIdentifier?: string;
|
||||
txHash?: string;
|
||||
count?: number;
|
||||
templateName?: string;
|
||||
actionIdentifier?: string;
|
||||
status?: string;
|
||||
entities?: { entityIdentifier: string; roles: (string | undefined)[] }[];
|
||||
inputs?: unknown[];
|
||||
outputs?: unknown[];
|
||||
variables?: unknown[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the invitation command.
|
||||
* Throws CommandError on failure, returns result data on success.
|
||||
* @param deps - The command dependencies.
|
||||
* @param args - Positional args after the command name, e.g. ["create", "template.json", "action-id"].
|
||||
* @param options - Parsed option flags, e.g. { varRequestedSatohis: "1000", role: "receiver" }.
|
||||
*/
|
||||
export const handleInvitationCommand = async (deps: CommandDependencies, args: string[], options: Record<string, string>): Promise<void> => {
|
||||
export const handleInvitationCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
options: Record<string, string>,
|
||||
): Promise<InvitationCommandResult> => {
|
||||
const subCommand = args[0];
|
||||
deps.verboseLogger(`Invitation sub-command: ${subCommand}`);
|
||||
deps.io.verbose(`Invitation sub-command: ${subCommand}`);
|
||||
|
||||
if (!subCommand) {
|
||||
deps.verboseLogger("No sub-command provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.subcommand.missing", "No sub-command provided");
|
||||
}
|
||||
|
||||
switch (subCommand) {
|
||||
case "create": {
|
||||
const templateFile = args[1];
|
||||
const templateQuery = args[1];
|
||||
const actionIdentifier = args[2];
|
||||
deps.verboseLogger(`Template file: ${templateFile}, action identifier: ${actionIdentifier}`);
|
||||
deps.io.verbose(`Template query: ${templateQuery}, action identifier: ${actionIdentifier}`);
|
||||
|
||||
if (!templateFile || !actionIdentifier) {
|
||||
deps.verboseLogger("No template file or action identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
if (!templateQuery || !actionIdentifier) {
|
||||
deps.io.verbose("No template file or action identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.create.arguments_missing", "No template file or action identifier provided");
|
||||
}
|
||||
|
||||
// Resolve and validate the template file path
|
||||
const templatePath = path.resolve(`${process.cwd()}/${templateFile}`);
|
||||
deps.verboseLogger(`Template path: ${templatePath}`);
|
||||
|
||||
if (!existsSync(templatePath)) {
|
||||
console.error(`Template file does not exist: ${templatePath}`);
|
||||
printInvitationHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const template = await readFileSync(templatePath, "utf8");
|
||||
const templateIdentifier = generateTemplateIdentifier(JSON.parse(template));
|
||||
|
||||
// Create the base invitation via the engine
|
||||
const template = await resolveTemplate(deps, templateQuery);
|
||||
const templateIdentifier = generateTemplateIdentifier(template);
|
||||
const rawInvitation = await deps.app.engine.createInvitation({
|
||||
templateIdentifier: templateIdentifier,
|
||||
actionIdentifier: actionIdentifier,
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
});
|
||||
deps.verboseLogger(`XOInvitation created: ${formatObject(rawInvitation)}`);
|
||||
deps.io.verbose(`XOInvitation created: ${formatObject(rawInvitation)}`);
|
||||
|
||||
const invitationInstance = await deps.app.createInvitation(rawInvitation);
|
||||
deps.verboseLogger(`Invitation created: ${formatObject(invitationInstance.data)}`);
|
||||
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
|
||||
|
||||
// Commit variables first so getSatsOut can resolve them for change calc
|
||||
// and resolveProvidedLockingBytecodeHex can read them from commits.
|
||||
const variables = parseVariablesFromOptions(options);
|
||||
deps.verboseLogger(`Variables: ${formatObject(variables)}`);
|
||||
deps.io.verbose(`Variables: ${formatObject(variables)}`);
|
||||
if (variables.length > 0) {
|
||||
await invitationInstance.addVariables(variables);
|
||||
}
|
||||
|
||||
// Parse inputs/outputs and calculate change (variables are now committed)
|
||||
const params = await buildAppendParams(deps, invitationInstance, options);
|
||||
if (!params) return;
|
||||
if (!params) {
|
||||
throw new CommandError("invitation.create.append_params_failed", "Failed to build append parameters");
|
||||
}
|
||||
|
||||
const { inputs, outputs } = params;
|
||||
|
||||
if (inputs.length > 0 || outputs.length > 0) {
|
||||
await invitationInstance.append({ inputs, outputs });
|
||||
}
|
||||
|
||||
// Save the invitation to a file
|
||||
const invitationFilePath = `${process.cwd()}/inv-${invitationInstance.data.invitationIdentifier}.json`;
|
||||
deps.verboseLogger(`Invitation file path: ${invitationFilePath}`);
|
||||
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitationInstance.data.invitationIdentifier}.json`;
|
||||
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
||||
writeFileSync(invitationFilePath, encodeExtendedJson(invitationInstance.data, 2));
|
||||
console.log(`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`);
|
||||
deps.io.out(`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`);
|
||||
|
||||
// Check remaining requirements
|
||||
const missingRequirements = await invitationInstance.getMissingRequirements();
|
||||
const hasMissing =
|
||||
(missingRequirements.variables?.length ?? 0) > 0 ||
|
||||
@@ -326,76 +333,74 @@ export const handleInvitationCommand = async (deps: CommandDependencies, args: s
|
||||
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
|
||||
|
||||
if (hasMissing) {
|
||||
console.log(`\n${bold("Remaining requirements:")}`);
|
||||
console.log(formatObject(missingRequirements));
|
||||
deps.io.out(`\n${bold("Remaining requirements:")}`);
|
||||
deps.io.out(formatObject(missingRequirements));
|
||||
} else {
|
||||
// --broadcast implies --sign
|
||||
const shouldSign = options["sign"] === "true" || options["broadcast"] === "true";
|
||||
const shouldBroadcast = options["broadcast"] === "true";
|
||||
|
||||
if (shouldSign) {
|
||||
await invitationInstance.sign();
|
||||
console.log(`Invitation signed: ${invitationInstance.data.invitationIdentifier}`);
|
||||
deps.io.out(`Invitation signed: ${invitationInstance.data.invitationIdentifier}`);
|
||||
}
|
||||
|
||||
if (shouldBroadcast) {
|
||||
const txHash = await invitationInstance.broadcast();
|
||||
console.log(`Transaction broadcast: ${bold(txHash)}`);
|
||||
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
|
||||
} else if (!shouldSign) {
|
||||
console.log(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationInstance.data.invitationIdentifier}`);
|
||||
deps.io.out(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationInstance.data.invitationIdentifier}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
return { invitationIdentifier: invitationInstance.data.invitationIdentifier };
|
||||
}
|
||||
|
||||
case "append": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.append.identifier_missing", "No invitation identifier provided");
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = deps.app.invitations.find(inv => inv.data.invitationIdentifier === invitationIdentifier);
|
||||
const invitation = deps.app.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError("invitation.append.not_found", `Invitation not found: ${invitationIdentifier}`);
|
||||
}
|
||||
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Commit variables first so getSatsOut can resolve them for change calc
|
||||
const variables = parseVariablesFromOptions(options);
|
||||
deps.verboseLogger(`Variables to append: ${formatObject(variables)}`);
|
||||
deps.io.verbose(`Variables to append: ${formatObject(variables)}`);
|
||||
if (variables.length > 0) {
|
||||
await invitation.addVariables(variables);
|
||||
}
|
||||
|
||||
// Parse inputs/outputs and calculate change (variables are now committed)
|
||||
const params = await buildAppendParams(deps, invitation, options);
|
||||
if (!params) return;
|
||||
if (!params) {
|
||||
throw new CommandError("invitation.append.params_failed", "Failed to build append parameters");
|
||||
}
|
||||
|
||||
const { inputs, outputs } = params;
|
||||
|
||||
if (variables.length === 0 && inputs.length === 0 && outputs.length === 0) {
|
||||
console.error("Nothing to append. Provide variables (-var-<name> <value>), inputs (--add-input <txhash>:<vout>), or outputs (--add-output <identifier>).");
|
||||
return;
|
||||
const error = "Nothing to append. Provide variables (-var-<name> <value>), inputs (--add-input <txhash>:<vout>), or outputs (--add-output <identifier>).";
|
||||
deps.io.err(error);
|
||||
throw new CommandError("invitation.append.empty", error);
|
||||
}
|
||||
|
||||
if (inputs.length > 0 || outputs.length > 0) {
|
||||
await invitation.append({ inputs, outputs });
|
||||
}
|
||||
deps.verboseLogger(`Invitation appended: ${formatObject(invitation.data)}`);
|
||||
console.log(`Invitation appended: ${invitationIdentifier}`);
|
||||
deps.io.verbose(`Invitation appended: ${formatObject(invitation.data)}`);
|
||||
deps.io.out(`Invitation appended: ${invitationIdentifier}`);
|
||||
|
||||
// Save the updated invitation to a file
|
||||
const invitationFilePath = `${process.cwd()}/inv-${invitation.data.invitationIdentifier}.json`;
|
||||
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitation.data.invitationIdentifier}.json`;
|
||||
writeFileSync(invitationFilePath, encodeExtendedJson(invitation.data, 2));
|
||||
console.log(`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`);
|
||||
deps.io.out(`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`);
|
||||
|
||||
// Check remaining requirements
|
||||
const missingRequirements = await invitation.getMissingRequirements();
|
||||
const hasMissing =
|
||||
(missingRequirements.variables?.length ?? 0) > 0 ||
|
||||
@@ -404,159 +409,212 @@ export const handleInvitationCommand = async (deps: CommandDependencies, args: s
|
||||
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
|
||||
|
||||
if (hasMissing) {
|
||||
console.log(`\n${bold("Remaining requirements:")}`);
|
||||
console.log(formatObject(missingRequirements));
|
||||
deps.io.out(`\n${bold("Remaining requirements:")}`);
|
||||
deps.io.out(formatObject(missingRequirements));
|
||||
} else {
|
||||
const shouldSign = options["sign"] === "true" || options["broadcast"] === "true";
|
||||
const shouldBroadcast = options["broadcast"] === "true";
|
||||
|
||||
if (shouldSign) {
|
||||
await invitation.sign();
|
||||
console.log(`Invitation signed: ${invitationIdentifier}`);
|
||||
deps.io.out(`Invitation signed: ${invitationIdentifier}`);
|
||||
}
|
||||
|
||||
if (shouldBroadcast) {
|
||||
const txHash = await invitation.broadcast();
|
||||
console.log(`Transaction broadcast: ${bold(txHash)}`);
|
||||
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
|
||||
} else if (!shouldSign) {
|
||||
console.log(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationIdentifier}`);
|
||||
deps.io.out(`\n${bold("All requirements satisfied.")} You can now sign with: xo-cli invitation sign ${invitationIdentifier}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
return { invitationIdentifier };
|
||||
}
|
||||
|
||||
case "sign": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// Check if the invitation identifier is provided
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.sign.identifier_missing", "No invitation identifier provided");
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
|
||||
const invitation = deps.app.invitations.find(
|
||||
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError("invitation.sign.not_found", `Invitation not found: ${invitationIdentifier}`);
|
||||
}
|
||||
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Sign the invitation
|
||||
await invitation.sign();
|
||||
deps.verboseLogger(`Invitation signed: ${formatObject(invitation.data)}`);
|
||||
console.log(`Invitation signed: ${invitationIdentifier}`);
|
||||
break;
|
||||
deps.io.verbose(`Invitation signed: ${formatObject(invitation.data)}`);
|
||||
deps.io.out(`Invitation signed: ${invitationIdentifier}`);
|
||||
return { invitationIdentifier };
|
||||
}
|
||||
|
||||
case "broadcast": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// Check if the invitation identifier is provided
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.broadcast.identifier_missing", "No invitation identifier provided");
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
|
||||
const invitation = deps.app.invitations.find(
|
||||
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError("invitation.broadcast.not_found", `Invitation not found: ${invitationIdentifier}`);
|
||||
}
|
||||
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Broadcast the invitation
|
||||
const txHash = await invitation.broadcast();
|
||||
deps.verboseLogger(`Invitation broadcasted: ${formatObject(invitation.data)}`);
|
||||
console.log(`Transaction broadcast: ${bold(txHash)}`);
|
||||
break;
|
||||
deps.io.verbose(`Invitation broadcasted: ${formatObject(invitation.data)}`);
|
||||
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
|
||||
return { invitationIdentifier, txHash };
|
||||
}
|
||||
|
||||
case "requirements": {
|
||||
const invitationIdentifier = args[1];
|
||||
deps.verboseLogger(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// Check if the invitation identifier is provided
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
if (!invitationIdentifier) {
|
||||
deps.verboseLogger("No invitation identifier provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.requirements.identifier_missing", "No invitation identifier provided");
|
||||
}
|
||||
|
||||
// Find the invitation by identifier
|
||||
const invitation = await deps.app.invitations.find(invitation => invitation.data.invitationIdentifier === invitationIdentifier);
|
||||
const invitation = deps.app.invitations.find(
|
||||
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
if (!invitation) {
|
||||
console.error(`Invitation not found: ${invitationIdentifier}`);
|
||||
return;
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError("invitation.requirements.not_found", `Invitation not found: ${invitationIdentifier}`);
|
||||
}
|
||||
deps.verboseLogger(`Invitation: ${formatObject(invitation.data)}`);
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// List the requirements for the invitation
|
||||
const requirements = await deps.app.engine.listRequirements(invitation.data);
|
||||
deps.verboseLogger(`Requirements: ${formatObject(requirements)}`);
|
||||
console.log(formatObject(requirements));
|
||||
break;
|
||||
deps.io.verbose(`Requirements: ${formatObject(requirements)}`);
|
||||
deps.io.out(formatObject(requirements));
|
||||
return { invitationIdentifier };
|
||||
}
|
||||
|
||||
case "inspect": {
|
||||
const invitationFilePath = args[1];
|
||||
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
||||
|
||||
if (!invitationFilePath) {
|
||||
deps.io.verbose("No invitation file provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.inspect.file_missing", "No invitation file provided");
|
||||
}
|
||||
|
||||
const invitationFile = await readFileSync(invitationFilePath, "utf8");
|
||||
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
||||
|
||||
const invitation = JSON.parse(invitationFile);
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
|
||||
|
||||
const invitationInstance = await deps.app.createInvitation(invitation);
|
||||
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
|
||||
|
||||
const template = await deps.app.engine.getTemplate(invitationInstance.data.templateIdentifier);
|
||||
|
||||
const action = template?.actions[invitationInstance.data.actionIdentifier];
|
||||
deps.io.verbose(`Action: ${formatObject(action)}`);
|
||||
if (!action) {
|
||||
deps.io.err(`Action not found: ${invitationInstance.data.actionIdentifier}`);
|
||||
throw new CommandError("invitation.inspect.action_not_found", `Action not found: ${invitationInstance.data.actionIdentifier}`);
|
||||
}
|
||||
|
||||
const status = invitationInstance.status;
|
||||
deps.io.verbose(`Status: ${status}`);
|
||||
|
||||
const entities = Array.from(new Set(invitationInstance.data.commits.map((commit) => commit.entityIdentifier)));
|
||||
deps.io.verbose(`Entities: ${formatObject(entities)}`);
|
||||
|
||||
const entitiesWithRoles = entities.map((entity) => {
|
||||
return {
|
||||
entityIdentifier: entity,
|
||||
roles: invitationInstance.data.commits.filter((commit) => commit.entityIdentifier === entity).map((commit) => {
|
||||
return [
|
||||
...(commit.data.inputs?.map((input) => input.roleIdentifier) ?? []),
|
||||
...(commit.data.outputs?.map((output) => output.roleIdentifier) ?? []),
|
||||
...(commit.data.variables?.map((variable) => variable.roleIdentifier) ?? []),
|
||||
];
|
||||
}).flat().filter((role) => role !== undefined)
|
||||
};
|
||||
});
|
||||
|
||||
const inputs = invitationInstance.data.commits.flatMap((commit) => commit.data.inputs ?? []);
|
||||
deps.io.verbose(`Inputs: ${formatObject(inputs)}`);
|
||||
|
||||
const outputs = invitationInstance.data.commits.flatMap((commit) => commit.data.outputs ?? []);
|
||||
deps.io.verbose(`Outputs: ${formatObject(outputs)}`);
|
||||
|
||||
const variables = invitationInstance.data.commits.flatMap((commit) => commit.data.variables ?? []);
|
||||
deps.io.verbose(`Variables: ${formatObject(variables)}`);
|
||||
|
||||
return {
|
||||
templateName: template?.name ?? "Unknown",
|
||||
actionIdentifier: invitationInstance.data.actionIdentifier,
|
||||
status: status,
|
||||
entities: entitiesWithRoles,
|
||||
inputs: inputs,
|
||||
outputs: outputs,
|
||||
variables: variables,
|
||||
};
|
||||
}
|
||||
|
||||
case "import": {
|
||||
// Check if the invitation file path is provided
|
||||
const invitationFilePath = args[1];
|
||||
deps.verboseLogger(`Invitation file path: ${invitationFilePath}`);
|
||||
|
||||
// Check if the invitation file path is provided
|
||||
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
||||
|
||||
if (!invitationFilePath) {
|
||||
deps.verboseLogger("No invitation file provided");
|
||||
printInvitationHelp();
|
||||
return;
|
||||
deps.io.verbose("No invitation file provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.import.file_missing", "No invitation file provided");
|
||||
}
|
||||
|
||||
// Read the invitation file
|
||||
const invitationFile = await readFileSync(invitationFilePath, "utf8");
|
||||
deps.verboseLogger(`Invitation file: ${invitationFile}`);
|
||||
|
||||
// Parse the invitation file
|
||||
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
||||
const invitation = JSON.parse(invitationFile);
|
||||
deps.verboseLogger(`Invitation: ${formatObject(invitation)}`);
|
||||
|
||||
// Create the invitation (internal to XO Engine)
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
|
||||
const xoInvitation = await deps.app.engine.createInvitation(invitation);
|
||||
|
||||
// Create the invitation instance
|
||||
const invitationInstance = await deps.app.createInvitation(xoInvitation);
|
||||
deps.verboseLogger(`Invitation created: ${formatObject(invitationInstance.data)}`);
|
||||
break;
|
||||
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
|
||||
return { invitationIdentifier: invitationInstance.data.invitationIdentifier };
|
||||
}
|
||||
|
||||
case "list": {
|
||||
// List all invitations
|
||||
const invitations = await Promise.all(deps.app.invitations.map(async invitation => {
|
||||
// Get the template for the invitation so we can display the name of it
|
||||
const template = await deps.app.engine.getTemplate(invitation.data.templateIdentifier);
|
||||
return {
|
||||
invitationIdentifier: invitation.data.invitationIdentifier,
|
||||
templateIdentifier: invitation.data.templateIdentifier,
|
||||
actionIdentifier: invitation.data.actionIdentifier,
|
||||
templateName: template?.name ?? "Unknown",
|
||||
status: invitation.status,
|
||||
roleIdentifier: 'TODO: Get role identifier',
|
||||
};
|
||||
}));
|
||||
deps.verboseLogger(`Invitations: ${formatObject(invitations)}`);
|
||||
|
||||
// Format the invitations for display and print it
|
||||
const formattedInvitations = invitations.map(invitation => `${bold(invitation.templateName)} ${dim(invitation.status)} ${dim(invitation.invitationIdentifier)} ${dim(invitation.actionIdentifier)} (${dim(invitation.roleIdentifier)})`);
|
||||
console.log(formattedInvitations.join('\n'));
|
||||
break;
|
||||
const invitations = await Promise.all(
|
||||
deps.app.invitations.map(async (invitation) => {
|
||||
const template = await deps.app.engine.getTemplate(invitation.data.templateIdentifier);
|
||||
return {
|
||||
invitationIdentifier: invitation.data.invitationIdentifier,
|
||||
templateIdentifier: invitation.data.templateIdentifier,
|
||||
actionIdentifier: invitation.data.actionIdentifier,
|
||||
templateName: template?.name ?? "Unknown",
|
||||
status: invitation.status,
|
||||
roleIdentifier: "TODO: Get role identifier",
|
||||
};
|
||||
}),
|
||||
);
|
||||
deps.io.verbose(`Invitations: ${formatObject(invitations)}`);
|
||||
const formattedInvitations = invitations.map(
|
||||
(invitation) =>
|
||||
`${bold(invitation.templateName)} ${dim(invitation.status)} ${dim(invitation.invitationIdentifier)} ${dim(invitation.actionIdentifier)} (${dim(invitation.roleIdentifier)})`,
|
||||
);
|
||||
deps.io.out(formattedInvitations.join("\n"));
|
||||
return { count: invitations.length };
|
||||
}
|
||||
|
||||
default:
|
||||
deps.verboseLogger(`Unknown invitation sub-command: ${subCommand}`);
|
||||
printInvitationHelp();
|
||||
return;
|
||||
deps.io.verbose(`Unknown invitation sub-command: ${subCommand}`);
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError("invitation.subcommand.unknown", `Unknown invitation sub-command: ${subCommand}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { bold, dim } from "../cli-utils.js";
|
||||
import { listMnemonicFiles, createMnemonicFile, createMnemonicSeed } from "../mnemonic.js";
|
||||
import type { CommandDependencies } from "./types.js";
|
||||
import { listMnemonicFiles, createMnemonicFile, createMnemonicSeed, loadMnemonic } from "../mnemonic.js";
|
||||
import type { BaseCommandDependencies, CommandIO } from "./types.js";
|
||||
import { CommandError } from "./types.js";
|
||||
|
||||
/**
|
||||
* Prints the help message for the mnemonic command
|
||||
*/
|
||||
export const printMnemonicHelp = () => {
|
||||
console.log(
|
||||
export const printMnemonicHelp = (io: CommandIO): void => {
|
||||
io.out(
|
||||
`
|
||||
${bold("Usage:")} xo-cli mnemonic <sub-command>
|
||||
|
||||
@@ -22,52 +23,79 @@ ${bold("Options:")}
|
||||
|
||||
/**
|
||||
* Handles the mnemonic command.
|
||||
* Throws CommandError on failure, returns result data on success.
|
||||
* @param deps - The command dependencies.
|
||||
* @param args - Positional args after the command name, e.g. ["create"] or ["import", "page", "pencil", ...].
|
||||
* @param options - Parsed option flags, e.g. { output: "mnemonic.txt" }.
|
||||
*/
|
||||
export const handleMnemonicCommand = async (deps: Omit<CommandDependencies, "app">, args: string[], options: Record<string, string>): Promise<void> => {
|
||||
export const handleMnemonicCommand = async (
|
||||
deps: BaseCommandDependencies,
|
||||
args: string[],
|
||||
options: Record<string, string>,
|
||||
): Promise<{ savedAs?: string; count?: number; mnemonic?: string }> => {
|
||||
const subCommand = args[0];
|
||||
const { mnemonicsDir } = deps.paths;
|
||||
|
||||
if (!subCommand) {
|
||||
deps.verboseLogger("No sub-command provided");
|
||||
printMnemonicHelp();
|
||||
return;
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printMnemonicHelp(deps.io);
|
||||
throw new CommandError("mnemonic.subcommand.missing", "No sub-command provided");
|
||||
}
|
||||
|
||||
switch (subCommand) {
|
||||
case "create": {
|
||||
const mnemonicSeed = createMnemonicSeed();
|
||||
await createMnemonicFile(mnemonicSeed, options["output"]);
|
||||
const savedAs = createMnemonicFile(mnemonicsDir, mnemonicSeed, options["output"]);
|
||||
|
||||
console.log(`Mnemonic file created: ${options["output"]} (${mnemonicSeed})`);
|
||||
break;
|
||||
deps.io.out(`Mnemonic file created: ${savedAs} (${mnemonicSeed})`);
|
||||
return { savedAs };
|
||||
}
|
||||
|
||||
case "import": {
|
||||
// The mnemonic seed words are all the positional args after the sub-command
|
||||
const mnemonicSeed = args.slice(1).join(" ");
|
||||
|
||||
if (!mnemonicSeed) {
|
||||
deps.verboseLogger("No mnemonic seed provided");
|
||||
printMnemonicHelp();
|
||||
return;
|
||||
deps.io.verbose("No mnemonic seed provided");
|
||||
printMnemonicHelp(deps.io);
|
||||
throw new CommandError("mnemonic.import.seed_missing", "No mnemonic seed provided");
|
||||
}
|
||||
|
||||
deps.verboseLogger(`Mnemonic seed: ${mnemonicSeed}`);
|
||||
await createMnemonicFile(mnemonicSeed, options["output"]);
|
||||
break;
|
||||
deps.io.verbose(`Mnemonic seed: ${mnemonicSeed}`);
|
||||
const savedAs = createMnemonicFile(mnemonicsDir, mnemonicSeed, options["output"]);
|
||||
deps.io.out(`Mnemonic file created: ${savedAs}`);
|
||||
return { savedAs };
|
||||
}
|
||||
|
||||
case "list": {
|
||||
const mnemonicFiles = listMnemonicFiles();
|
||||
console.log(mnemonicFiles.join('\n'));
|
||||
break;
|
||||
const mnemonicFiles = listMnemonicFiles(mnemonicsDir);
|
||||
deps.io.out(mnemonicFiles.join('\n'));
|
||||
return { count: mnemonicFiles.length };
|
||||
}
|
||||
|
||||
case "expose": {
|
||||
const mnemonicFile = args[1];
|
||||
|
||||
if (!mnemonicFile) {
|
||||
deps.io.verbose("No mnemonic file provided");
|
||||
printMnemonicHelp(deps.io);
|
||||
throw new CommandError("mnemonic.expose.file_missing", "No mnemonic file provided");
|
||||
}
|
||||
|
||||
try {
|
||||
const mnemonic = loadMnemonic(mnemonicsDir, mnemonicFile);
|
||||
deps.io.out(mnemonic);
|
||||
return { mnemonic };
|
||||
} catch (error) {
|
||||
throw new CommandError(
|
||||
"mnemonic.expose.file_not_found",
|
||||
`Mnemonic file not found: ${mnemonicFile}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`Unknown sub-command: ${subCommand}`);
|
||||
printMnemonicHelp();
|
||||
return;
|
||||
deps.io.err(`Unknown sub-command: ${subCommand}`);
|
||||
printMnemonicHelp(deps.io);
|
||||
throw new CommandError("mnemonic.subcommand.unknown", `Unknown sub-command: ${subCommand}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { generateTemplateIdentifier } from "@xo-cash/engine";
|
||||
import { hexToBin, lockingBytecodeToCashAddress } from "@bitauth/libauth";
|
||||
|
||||
import { bold, dim } from "../cli-utils.js";
|
||||
import type { CommandDependencies } from "./types.js";
|
||||
import type { CommandDependencies, CommandIO } from "./types.js";
|
||||
import { CommandError } from "./types.js";
|
||||
|
||||
import { resolveTemplate } from "../utils.js";
|
||||
|
||||
/**
|
||||
* Prints the help message for the receive command
|
||||
*/
|
||||
export const printReceiveHelp = () => {
|
||||
console.log(
|
||||
export const printReceiveHelp = (io: CommandIO): void => {
|
||||
io.out(
|
||||
`
|
||||
${bold("Usage:")} xo-cli receive <template-file> <output-identifier> [role-identifier]
|
||||
|
||||
@@ -29,36 +30,35 @@ ${bold("Options:")}
|
||||
|
||||
/**
|
||||
* Command which creates a single-use address/lockingScript for a given template and role.
|
||||
* Throws CommandError on failure, returns address data on success.
|
||||
*
|
||||
* @param deps - The command dependencies.
|
||||
* @param args - Positional args after the command name, e.g. ["template.json", "receiveOutput", "receiver"].
|
||||
* @param options - Parsed option flags.
|
||||
* @returns The address data.
|
||||
* @throws CommandError if the command fails.
|
||||
*/
|
||||
export const handleReceiveCommand = async (deps: CommandDependencies, args: string[], options: Record<string, string>): Promise<void> => {
|
||||
const templateFile = args[0];
|
||||
export const handleReceiveCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
_options: Record<string, string>,
|
||||
): Promise<{ address: string }> => {
|
||||
const templateQuery = args[0];
|
||||
const outputIdentifier = args[1];
|
||||
const roleIdentifier = args[2];
|
||||
|
||||
deps.verboseLogger(`Receive args - template: ${templateFile}, output: ${outputIdentifier}, role: ${roleIdentifier}`);
|
||||
deps.io.verbose(`Receive args - template: ${templateQuery}, output: ${outputIdentifier}, role: ${roleIdentifier}`);
|
||||
|
||||
if (!templateFile || !outputIdentifier) {
|
||||
deps.verboseLogger("Missing required arguments");
|
||||
printReceiveHelp();
|
||||
return;
|
||||
if (!templateQuery || !outputIdentifier) {
|
||||
deps.io.verbose("Missing required arguments");
|
||||
printReceiveHelp(deps.io);
|
||||
throw new CommandError("receive.arguments.missing", "Missing required arguments");
|
||||
}
|
||||
|
||||
// Resolve and read the template file
|
||||
const templatePath = path.resolve(`${process.cwd()}/${templateFile}`);
|
||||
deps.verboseLogger(`Template path: ${templatePath}`);
|
||||
|
||||
if (!existsSync(templatePath)) {
|
||||
console.error(`Template file does not exist: ${templatePath}`);
|
||||
printReceiveHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const template = readFileSync(templatePath, "utf8");
|
||||
const templateIdentifier = generateTemplateIdentifier(JSON.parse(template));
|
||||
deps.verboseLogger(`Template identifier: ${templateIdentifier}`);
|
||||
const template = await resolveTemplate(deps, templateQuery);
|
||||
const templateIdentifier = generateTemplateIdentifier(template);
|
||||
deps.io.verbose(`Template identifier: ${templateIdentifier}`);
|
||||
|
||||
// Generate the locking bytecode (returned as a hex string)
|
||||
const lockingBytecodeHex = await deps.app.engine.generateLockingBytecode(
|
||||
@@ -66,15 +66,16 @@ export const handleReceiveCommand = async (deps: CommandDependencies, args: stri
|
||||
outputIdentifier,
|
||||
roleIdentifier,
|
||||
);
|
||||
deps.verboseLogger(`Locking bytecode hex: ${lockingBytecodeHex}`);
|
||||
deps.io.verbose(`Locking bytecode hex: ${lockingBytecodeHex}`);
|
||||
|
||||
// Convert the locking bytecode to a BCH cash address
|
||||
const result = lockingBytecodeToCashAddress({ bytecode: hexToBin(lockingBytecodeHex), prefix: 'bitcoincash' });
|
||||
|
||||
if (typeof result === 'string') {
|
||||
console.error(`Failed to encode address: ${result}`);
|
||||
return;
|
||||
deps.io.err(`Failed to encode address: ${result}`);
|
||||
throw new CommandError("receive.address.encode_failed", `Failed to encode address: ${result}`);
|
||||
}
|
||||
|
||||
console.log(result.address);
|
||||
deps.io.out(result.address);
|
||||
return { address: result.address };
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { hexToBin } from "@bitauth/libauth";
|
||||
|
||||
import { bold, dim } from "../cli-utils.js";
|
||||
import type { CommandDependencies } from "./types.js";
|
||||
import type { CommandDependencies, CommandIO } from "./types.js";
|
||||
import type { UnspentOutputData } from "@xo-cash/state";
|
||||
import { CommandError } from "./types.js";
|
||||
|
||||
/**
|
||||
* Prints the help message for the resource command.
|
||||
*/
|
||||
export const printResourceHelp = () => {
|
||||
console.log(
|
||||
export const printResourceHelp = (io: CommandIO): void => {
|
||||
io.out(
|
||||
`
|
||||
${bold("Usage:")} xo-cli resource <sub-command>
|
||||
|
||||
@@ -23,14 +25,14 @@ ${bold("Sub-commands:")}
|
||||
/**
|
||||
* Formats a single UTXO for display, optionally including reservation info.
|
||||
*/
|
||||
function formatResource(resource: { outpointTransactionHash: string; outpointIndex: number; valueSatoshis: number; outputIdentifier: string; minedAtHeight: number; reserved?: boolean; invitationIdentifier?: string }, showReserved = false): string {
|
||||
function formatResource(resource: UnspentOutputData, showReserved = false): string {
|
||||
const outpoint = bold(`${resource.outpointTransactionHash}:${resource.outpointIndex}`);
|
||||
const value = dim(`${resource.valueSatoshis} sats`);
|
||||
const output = dim(resource.outputIdentifier);
|
||||
const height = dim(`(height ${resource.minedAtHeight})`);
|
||||
|
||||
if (showReserved && resource.reserved) {
|
||||
const inv = dim(`reserved for ${resource.invitationIdentifier}`);
|
||||
if (showReserved && resource.reservedBy) {
|
||||
const inv = dim(`reserved for ${resource.reservedBy}`);
|
||||
return `${outpoint} ${value} ${output} ${height} ${inv}`;
|
||||
}
|
||||
|
||||
@@ -39,107 +41,110 @@ function formatResource(resource: { outpointTransactionHash: string; outpointInd
|
||||
|
||||
/**
|
||||
* Handles the resource command.
|
||||
* Throws CommandError on failure, returns result data on success.
|
||||
* @param deps - The command dependencies.
|
||||
* @param args - Positional args after the command name, e.g. ["list"].
|
||||
* @param options - Parsed option flags.
|
||||
*/
|
||||
export const handleResourceCommand = async (deps: CommandDependencies, args: string[], options: Record<string, string>): Promise<void> => {
|
||||
export const handleResourceCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
_options: Record<string, string>,
|
||||
): Promise<{ count?: number }> => {
|
||||
const subCommand = args[0];
|
||||
deps.verboseLogger(`Resource sub-command: ${subCommand}`);
|
||||
deps.io.verbose(`Resource sub-command: ${subCommand}`);
|
||||
|
||||
if (!subCommand) {
|
||||
deps.verboseLogger("No sub-command provided");
|
||||
printResourceHelp();
|
||||
return;
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printResourceHelp(deps.io);
|
||||
throw new CommandError("resource.subcommand.missing", "No sub-command provided");
|
||||
}
|
||||
|
||||
switch (subCommand) {
|
||||
case "list": {
|
||||
const qualifier = args[1]; // "reserved", "all", or undefined (defaults to unreserved)
|
||||
const qualifier = args[1];
|
||||
const allResources = await deps.app.engine.listUnspentOutputsData();
|
||||
|
||||
let filtered;
|
||||
if (qualifier === "reserved") {
|
||||
filtered = allResources.filter((r) => r.reserved);
|
||||
filtered = allResources.filter((r) => r.reservedBy);
|
||||
} else if (qualifier === "all") {
|
||||
filtered = allResources;
|
||||
} else {
|
||||
// Default: show only unreserved (selectable) resources
|
||||
filtered = allResources.filter((r) => !r.reserved);
|
||||
filtered = allResources.filter((r) => !r.reservedBy);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
console.log(dim("No resources found."));
|
||||
return;
|
||||
deps.io.out(dim("No resources found."));
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
const showReserved = qualifier === "all" || qualifier === "reserved";
|
||||
const formattedResources = filtered.map((r) => formatResource(r, showReserved));
|
||||
console.log(formattedResources.join("\n"));
|
||||
console.log(`Total satoshis: ${filtered.reduce((acc, r) => acc + r.valueSatoshis, 0)}`);
|
||||
console.log(`Total resources: ${filtered.length}`);
|
||||
break;
|
||||
deps.io.out(formattedResources.join("\n"));
|
||||
deps.io.out(`Total satoshis: ${filtered.reduce((acc, r) => acc + r.valueSatoshis, 0)}`);
|
||||
deps.io.out(`Total resources: ${filtered.length}`);
|
||||
return { count: filtered.length };
|
||||
}
|
||||
|
||||
case "unreserve": {
|
||||
const outpointArg = args[1];
|
||||
if (!outpointArg) {
|
||||
console.error("Please provide a UTXO in <txhash>:<vout> format.");
|
||||
printResourceHelp();
|
||||
return;
|
||||
deps.io.err("Please provide a UTXO in <txhash>:<vout> format.");
|
||||
printResourceHelp(deps.io);
|
||||
throw new CommandError("resource.unreserve.outpoint_missing", "Please provide a UTXO in <txhash>:<vout> format.");
|
||||
}
|
||||
|
||||
const separatorIndex = outpointArg.lastIndexOf(":");
|
||||
if (separatorIndex === -1) {
|
||||
console.error(`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
|
||||
return;
|
||||
deps.io.err(`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
|
||||
throw new CommandError("resource.unreserve.outpoint_invalid", `Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
|
||||
}
|
||||
|
||||
const txHash = outpointArg.substring(0, separatorIndex);
|
||||
const vout = parseInt(outpointArg.substring(separatorIndex + 1), 10);
|
||||
if (!txHash || isNaN(vout)) {
|
||||
console.error(`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
|
||||
return;
|
||||
deps.io.err(`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
|
||||
throw new CommandError("resource.unreserve.outpoint_invalid", `Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
|
||||
}
|
||||
|
||||
// Look up the UTXO to get its invitation identifier (required by the engine).
|
||||
const allResources = await deps.app.engine.listUnspentOutputsData();
|
||||
const target = allResources.find(
|
||||
(r) => r.outpointTransactionHash === txHash && r.outpointIndex === vout,
|
||||
);
|
||||
|
||||
if (!target) {
|
||||
console.error(`UTXO not found: ${txHash}:${vout}`);
|
||||
return;
|
||||
deps.io.err(`UTXO not found: ${txHash}:${vout}`);
|
||||
throw new CommandError("resource.unreserve.utxo_missing", `UTXO not found: ${txHash}:${vout}`);
|
||||
}
|
||||
|
||||
if (!target.reserved) {
|
||||
console.log(dim("UTXO is not reserved. Nothing to do."));
|
||||
return;
|
||||
if (!target.reservedBy) {
|
||||
deps.io.out(dim("UTXO is not reserved. Nothing to do."));
|
||||
return {};
|
||||
}
|
||||
|
||||
await deps.app.engine.unreserveResources(
|
||||
[{ outpointTransactionHash: hexToBin(txHash), outpointIndex: vout }],
|
||||
target.invitationIdentifier,
|
||||
target.reservedBy ,
|
||||
);
|
||||
console.log(`Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.invitationIdentifier})`);
|
||||
break;
|
||||
deps.io.out(`Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.reservedBy})`);
|
||||
return {};
|
||||
}
|
||||
|
||||
case "unreserve-all": {
|
||||
const count = await deps.app.unreserveAllResources();
|
||||
if (count === 0) {
|
||||
console.log(dim("No reserved resources to unreserve."));
|
||||
deps.io.out(dim("No reserved resources to unreserve."));
|
||||
} else {
|
||||
console.log(`Unreserved ${bold(String(count))} resource(s).`);
|
||||
deps.io.out(`Unreserved ${bold(String(count))} resource(s).`);
|
||||
}
|
||||
break;
|
||||
return { count };
|
||||
}
|
||||
|
||||
default: {
|
||||
deps.verboseLogger(`Unknown resource sub-command: ${subCommand}`);
|
||||
printResourceHelp();
|
||||
return;
|
||||
deps.io.verbose(`Unknown resource sub-command: ${subCommand}`);
|
||||
printResourceHelp(deps.io);
|
||||
throw new CommandError("resource.subcommand.unknown", `Unknown resource sub-command: ${subCommand}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,15 +3,17 @@ 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 { bold, dim, formatObject } from "../cli-utils.js";
|
||||
import { resolveTemplateReferences } from "../../utils/templates.js";
|
||||
import type { CommandDependencies } from "./types.js";
|
||||
import type { CommandDependencies, CommandIO } from "./types.js";
|
||||
import { CommandError } from "./types.js";
|
||||
import { resolveTemplate } from "../utils.js";
|
||||
|
||||
/**
|
||||
* Prints the help message for the template command
|
||||
*/
|
||||
export const printTemplateHelp = () => {
|
||||
console.log(
|
||||
export const printTemplateHelp = (io: CommandIO): void => {
|
||||
io.out(
|
||||
`
|
||||
${bold("Usage:")} xo-cli template <sub-command>
|
||||
|
||||
@@ -26,76 +28,75 @@ ${bold("Sub-commands:")}
|
||||
|
||||
/**
|
||||
* Handles the template list command.
|
||||
* Throws CommandError on failure, returns result data on success.
|
||||
* @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> => {
|
||||
export const handleTemplateListCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
): Promise<{ count?: number }> => {
|
||||
const templateCategory = args[0];
|
||||
deps.verboseLogger(`Template list category: ${templateCategory}`);
|
||||
deps.io.verbose(`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;
|
||||
deps.io.out(formattedTemplates.join('\n'));
|
||||
return { count: templates.length };
|
||||
}
|
||||
|
||||
// Extract the template identifier from the positional args
|
||||
const templateIdentifier = args[1];
|
||||
deps.verboseLogger(`Template identifier: ${templateIdentifier}`);
|
||||
deps.io.verbose(`Template identifier: ${templateIdentifier}`);
|
||||
|
||||
if (!templateIdentifier) {
|
||||
console.error("No template identifier provided");
|
||||
return;
|
||||
deps.io.err("No template identifier provided");
|
||||
throw new CommandError("template.list.identifier_missing", "No template identifier provided");
|
||||
}
|
||||
|
||||
// Get the template from the engine
|
||||
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
|
||||
if (!rawTemplate) {
|
||||
console.error(`No template found: ${templateIdentifier}`);
|
||||
return;
|
||||
deps.io.err(`No template found: ${templateIdentifier}`);
|
||||
throw new CommandError("template.list.not_found", `No template found: ${templateIdentifier}`);
|
||||
}
|
||||
|
||||
// Resolve the template references
|
||||
const template = await resolveTemplateReferences(rawTemplate);
|
||||
deps.verboseLogger(`Template: ${formatObject(template)}`);
|
||||
deps.io.verbose(`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;
|
||||
deps.io.out(formattedActions.join('\n'));
|
||||
return {};
|
||||
}
|
||||
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;
|
||||
deps.io.out(formattedTransactions.join('\n'));
|
||||
return {};
|
||||
}
|
||||
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;
|
||||
deps.io.out(formattedOutputs.join('\n'));
|
||||
return {};
|
||||
}
|
||||
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;
|
||||
deps.io.out(formattedLockingscripts.join('\n'));
|
||||
return {};
|
||||
}
|
||||
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;
|
||||
deps.io.out(formattedVariables.join('\n'));
|
||||
return {};
|
||||
}
|
||||
default: {
|
||||
deps.verboseLogger(`Unknown template category: ${templateCategory}`);
|
||||
return;
|
||||
deps.io.verbose(`Unknown template category: ${templateCategory}`);
|
||||
throw new CommandError("template.list.category_unknown", `Unknown template category: ${templateCategory}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,8 +104,8 @@ export const handleTemplateListCommand = async (deps: CommandDependencies, args:
|
||||
/**
|
||||
* Prints the help message for the template inspect command
|
||||
*/
|
||||
export const printTemplateInspectHelp = () => {
|
||||
console.log(
|
||||
export const printTemplateInspectHelp = (io: CommandIO): void => {
|
||||
io.out(
|
||||
`
|
||||
${bold("Usage:")} xo-cli template inspect <category> <identifier> <field>
|
||||
|
||||
@@ -124,153 +125,151 @@ ${bold("Categories:")}
|
||||
|
||||
/**
|
||||
* Handles the template inspect command.
|
||||
* Throws CommandError on failure, returns empty object on success.
|
||||
* @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> => {
|
||||
export const handleTemplateInspectCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
): Promise<Record<string, never>> => {
|
||||
const templateCategory = args[0];
|
||||
const templateIdentifier = args[1];
|
||||
const templateQuery = args[1];
|
||||
const templateField = args[2];
|
||||
|
||||
deps.verboseLogger(`Template inspect args - category: ${templateCategory}, identifier: ${templateIdentifier}, field: ${templateField}`);
|
||||
deps.io.verbose(`Template inspect args - category: ${templateCategory}, identifier: ${templateQuery}, field: ${templateField}`);
|
||||
|
||||
if (!templateCategory || !templateIdentifier || !templateField) {
|
||||
console.log("No template category, identifier, or field provided");
|
||||
printTemplateInspectHelp();
|
||||
return;
|
||||
if (!templateCategory || !templateQuery || !templateField) {
|
||||
deps.io.err("No template category, identifier, or field provided");
|
||||
printTemplateInspectHelp(deps.io);
|
||||
throw new CommandError("template.inspect.arguments_missing", "No template category, identifier, or field provided");
|
||||
}
|
||||
|
||||
// Get the template from the engine
|
||||
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
|
||||
if (!rawTemplate) {
|
||||
console.error(`No template found: ${templateIdentifier}`);
|
||||
return;
|
||||
}
|
||||
const template = await resolveTemplate(deps, templateQuery);
|
||||
deps.io.verbose(`Template: ${formatObject(template)}`);
|
||||
|
||||
// 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;
|
||||
deps.io.err(`No action found: ${templateField}`);
|
||||
throw new CommandError("template.inspect.action_missing", `No action found: ${templateField}`);
|
||||
}
|
||||
objectPrint(action);
|
||||
break;
|
||||
deps.io.out(formatObject(action));
|
||||
return {};
|
||||
}
|
||||
case "transaction": {
|
||||
const transaction = template.transactions[templateField];
|
||||
const transaction = template.transactions?.[templateField];
|
||||
if (!transaction) {
|
||||
console.error(`No transaction found: ${templateField}`);
|
||||
return;
|
||||
deps.io.err(`No transaction found: ${templateField}`);
|
||||
throw new CommandError("template.inspect.transaction_missing", `No transaction found: ${templateField}`);
|
||||
}
|
||||
objectPrint(transaction);
|
||||
break;
|
||||
deps.io.out(formatObject(transaction));
|
||||
return {};
|
||||
}
|
||||
case "output": {
|
||||
const output = template.outputs[templateField];
|
||||
if (!output) {
|
||||
console.error(`No output found: ${templateField}`);
|
||||
return;
|
||||
deps.io.err(`No output found: ${templateField}`);
|
||||
throw new CommandError("template.inspect.output_missing", `No output found: ${templateField}`);
|
||||
}
|
||||
objectPrint(output);
|
||||
break;
|
||||
deps.io.out(formatObject(output));
|
||||
return {};
|
||||
}
|
||||
case "lockingscript": {
|
||||
const lockingscript = template.lockingScripts[templateField];
|
||||
if (!lockingscript) {
|
||||
console.error(`No lockingscript found: ${templateField}`);
|
||||
return;
|
||||
deps.io.err(`No lockingscript found: ${templateField}`);
|
||||
throw new CommandError("template.inspect.lockingscript_missing", `No lockingscript found: ${templateField}`);
|
||||
}
|
||||
objectPrint(lockingscript);
|
||||
break;
|
||||
deps.io.out(formatObject(lockingscript));
|
||||
return {};
|
||||
}
|
||||
case "variable": {
|
||||
const variable = template.variables?.[templateField];
|
||||
if (!variable) {
|
||||
console.error(`No variable found: ${templateField}`);
|
||||
return;
|
||||
deps.io.err(`No variable found: ${templateField}`);
|
||||
throw new CommandError("template.inspect.variable_missing", `No variable found: ${templateField}`);
|
||||
}
|
||||
objectPrint(variable);
|
||||
break;
|
||||
deps.io.out(formatObject(variable));
|
||||
return {};
|
||||
}
|
||||
default: {
|
||||
deps.verboseLogger(`Unknown template category: ${templateCategory}`);
|
||||
return;
|
||||
deps.io.verbose(`Unknown template category: ${templateCategory}`);
|
||||
throw new CommandError("template.inspect.category_unknown", `Unknown template category: ${templateCategory}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the template command.
|
||||
* Throws CommandError on failure, returns result data on success.
|
||||
* @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> => {
|
||||
export const handleTemplateCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
_options: Record<string, string>,
|
||||
): Promise<{ templateFile?: string; count?: number }> => {
|
||||
const subCommand = args[0];
|
||||
|
||||
if (!subCommand) {
|
||||
deps.verboseLogger("No sub-command provided");
|
||||
printTemplateHelp();
|
||||
return;
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printTemplateHelp(deps.io);
|
||||
throw new CommandError("template.subcommand.missing", "No sub-command provided");
|
||||
}
|
||||
|
||||
switch (subCommand) {
|
||||
case "import": {
|
||||
const templateFile = args[1];
|
||||
deps.verboseLogger(`Template file: ${templateFile}`);
|
||||
deps.io.verbose(`Template file: ${templateFile}`);
|
||||
|
||||
if (!templateFile) {
|
||||
deps.verboseLogger("No template file provided");
|
||||
printTemplateHelp();
|
||||
return;
|
||||
deps.io.verbose("No template file provided");
|
||||
printTemplateHelp(deps.io);
|
||||
throw new CommandError("template.import.file_missing", "No template file provided");
|
||||
}
|
||||
|
||||
const templatePath = path.resolve(`${process.cwd()}/${templateFile}`);
|
||||
deps.verboseLogger(`Template path: ${templatePath}`);
|
||||
deps.io.verbose(`Template path: ${templatePath}`);
|
||||
|
||||
if (!existsSync(templatePath)) {
|
||||
console.error(`Template file does not exist: ${templatePath}`);
|
||||
printTemplateHelp();
|
||||
return;
|
||||
deps.io.err(`Template file does not exist: ${templatePath}`);
|
||||
printTemplateHelp(deps.io);
|
||||
throw new CommandError("template.import.file_not_found", `Template file does not exist: ${templatePath}`);
|
||||
}
|
||||
|
||||
const template = await readFileSync(templatePath, "utf8");
|
||||
|
||||
deps.verboseLogger(`Importing template: ${templateFile}`);
|
||||
deps.io.verbose(`Importing template: ${templateFile}`);
|
||||
await deps.app.engine.importTemplate(template);
|
||||
deps.verboseLogger(`Template imported: ${templateFile}`);
|
||||
break;
|
||||
deps.io.verbose(`Template imported: ${templateFile}`);
|
||||
return { templateFile };
|
||||
}
|
||||
case "list": {
|
||||
await handleTemplateListCommand(deps, args.slice(1));
|
||||
break;
|
||||
return handleTemplateListCommand(deps, args.slice(1));
|
||||
}
|
||||
case "inspect": {
|
||||
await handleTemplateInspectCommand(deps, args.slice(1));
|
||||
break;
|
||||
return handleTemplateInspectCommand(deps, args.slice(1));
|
||||
}
|
||||
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.io.verbose("No template file, output identifier, or role identifier provided");
|
||||
printTemplateHelp(deps.io);
|
||||
throw new CommandError("template.default.arguments_missing", "No template file, output identifier, or role identifier provided");
|
||||
}
|
||||
deps.verboseLogger(`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`);
|
||||
deps.io.verbose(`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`);
|
||||
await deps.app.engine.setDefaultLockingParameters(templateFile, outputIdentifier, roleIdentifier);
|
||||
break;
|
||||
return {};
|
||||
}
|
||||
default:
|
||||
deps.verboseLogger(`Unknown template sub-command: ${subCommand}`);
|
||||
printTemplateHelp();
|
||||
return;
|
||||
deps.io.verbose(`Unknown template sub-command: ${subCommand}`);
|
||||
printTemplateHelp(deps.io);
|
||||
throw new CommandError("template.subcommand.unknown", `Unknown template sub-command: ${subCommand}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,59 @@
|
||||
import type { AppService } from "../../services/app.js";
|
||||
|
||||
export type CommandDependencies = {
|
||||
verboseLogger: (message: string) => void;
|
||||
/**
|
||||
* IO contract for CLI command handlers.
|
||||
* Handlers write user-visible output through this abstraction so unit tests can
|
||||
* assert behavior without spying on global console methods.
|
||||
*/
|
||||
export type CommandIO = {
|
||||
out: (message: string) => void;
|
||||
err: (message: string) => void;
|
||||
verbose: (message: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Paths configuration for CLI commands.
|
||||
* Allows injection of custom paths for testing.
|
||||
*/
|
||||
export type CommandPaths = {
|
||||
/** Directory for mnemonic wallet files */
|
||||
mnemonicsDir: string;
|
||||
/** Directory for engine DB and invitation storage files */
|
||||
dataDir: string;
|
||||
/** File storing the last-used mnemonic reference */
|
||||
walletConfigPath: string;
|
||||
/** Working directory for file output (invitation files, etc.) */
|
||||
workingDir: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base dependencies available to every command handler.
|
||||
*/
|
||||
export type BaseCommandDependencies = {
|
||||
io: CommandIO;
|
||||
paths: CommandPaths;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dependencies for app-backed commands.
|
||||
*/
|
||||
export type CommandDependencies = BaseCommandDependencies & {
|
||||
app: AppService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error class for command failures.
|
||||
* Thrown by command handlers when an operation fails.
|
||||
* The `event` property can be used for telemetry/testing to identify failure types.
|
||||
*/
|
||||
export class CommandError extends Error {
|
||||
public readonly event: string;
|
||||
public readonly code: number;
|
||||
|
||||
constructor(event: string, message: string, code = 1) {
|
||||
super(message);
|
||||
this.name = "CommandError";
|
||||
this.event = event;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user