Formatting

This commit is contained in:
2026-04-20 12:26:35 +00:00
parent 32c42cdc2d
commit dbfb2c68d2
32 changed files with 3557 additions and 1828 deletions

View File

@@ -73,14 +73,16 @@ async function buildAppendParams(
// --- Inputs ---
// Accepts comma-separated <txhash>:<vout> pairs via --add-input,
// OR automatic selection via --auto-inputs.
let inputs: { outpointTransactionHash: Uint8Array; outpointIndex: number }[] = [];
let inputs: { outpointTransactionHash: Uint8Array; outpointIndex: number }[] =
[];
if (options["autoInputs"] === "true") {
// Auto-select UTXOs using the greedy algorithm from invitation-flow.
const suitableResources = await invitation.findSuitableResources();
const selectable = mapUnspentOutputsToSelectable(suitableResources);
const requiredWithFee = (await invitation.getSatsOut().catch(() => 0n)) + DEFAULT_FEE;
const requiredWithFee =
(await invitation.getSatsOut().catch(() => 0n)) + DEFAULT_FEE;
autoSelectGreedyUtxos(selectable, requiredWithFee);
inputs = selectable
@@ -99,12 +101,16 @@ async function buildAppendParams(
inputs = options["addInput"].split(",").map((entry) => {
const separatorIndex = entry.lastIndexOf(":");
if (separatorIndex === -1) {
throw new Error(`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`);
throw new Error(
`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`,
);
}
const txHash = entry.substring(0, separatorIndex);
const vout = parseInt(entry.substring(separatorIndex + 1), 10);
if (!txHash || isNaN(vout)) {
throw new Error(`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`);
throw new Error(
`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`,
);
}
return {
outpointTransactionHash: hexToBin(txHash),
@@ -112,7 +118,9 @@ async function buildAppendParams(
};
});
}
deps.io.verbose(`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.
@@ -135,7 +143,9 @@ async function buildAppendParams(
}
outputIdentifiers = [...discovered];
if (outputIdentifiers.length > 0) {
deps.io.verbose(`Auto-discovered output(s) from template: ${outputIdentifiers.join(", ")}`);
deps.io.verbose(
`Auto-discovered output(s) from template: ${outputIdentifiers.join(", ")}`,
);
}
}
@@ -151,40 +161,58 @@ async function buildAppendParams(
}
}
const template = await deps.app.engine.getTemplate(invitation.data.templateIdentifier);
const template = await deps.app.engine.getTemplate(
invitation.data.templateIdentifier,
);
const outputs: any[] = await Promise.all(
outputIdentifiers.map(async (outputId) => {
// Try variable-based resolution first (e.g. sendSatoshis → recipientLockingscript)
const providedHex = template
? resolveProvidedLockingBytecodeHex(template, outputId, variableValuesByIdentifier)
? resolveProvidedLockingBytecodeHex(
template,
outputId,
variableValuesByIdentifier,
)
: undefined;
const lockingBytecodeHex = providedHex
?? await invitation.generateLockingBytecode(outputId, roleIdentifier);
const lockingBytecodeHex =
providedHex ??
(await invitation.generateLockingBytecode(outputId, roleIdentifier));
deps.io.verbose(`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.io.verbose(`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
// required sats, and return the excess minus fees back to the user.
if (inputs.length > 0) {
const allUtxos = await deps.app.engine.listUnspentOutputsData();
const utxoMap = new Map(allUtxos.map(u => [`${u.outpointTransactionHash}:${u.outpointIndex}`, u]));
const utxoMap = new Map(
allUtxos.map((u) => [
`${u.outpointTransactionHash}:${u.outpointIndex}`,
u,
]),
);
let totalInputSats = 0n;
for (const input of inputs) {
const txHashHex = binToHex(input.outpointTransactionHash);
const utxo = utxoMap.get(`${txHashHex}:${input.outpointIndex}`);
if (!utxo) {
deps.io.err(`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);
@@ -195,10 +223,14 @@ async function buildAppendParams(
deps.io.verbose(`Required output value: ${requiredSats} satoshis`);
const changeAmount = totalInputSats - requiredSats - DEFAULT_FEE;
deps.io.verbose(`Change amount: ${changeAmount} satoshis (fee: ${DEFAULT_FEE})`);
deps.io.verbose(
`Change amount: ${changeAmount} satoshis (fee: ${DEFAULT_FEE})`,
);
if (changeAmount < 0n) {
deps.io.err(`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;
}
@@ -206,7 +238,9 @@ async function buildAppendParams(
outputs.push({ valueSatoshis: changeAmount });
deps.io.out(`Auto-adding change output: ${changeAmount} satoshis`);
} else if (changeAmount > 0n) {
deps.io.out(`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.`,
);
}
}
@@ -218,7 +252,7 @@ async function buildAppendParams(
*/
export const printInvitationHelp = (io: CommandIO): void => {
io.out(
`
`
${bold("Usage:")} xo-cli invitation <sub-command>
${bold("Sub-commands:")}
@@ -241,7 +275,8 @@ ${bold("Create / Append options:")}
${dim("When inputs are provided, a change output is automatically added if the")}
${dim("input total exceeds the required amount + fee.")}
`);
`,
);
};
/**
@@ -278,19 +313,27 @@ export const handleInvitationCommand = async (
if (!subCommand) {
deps.io.verbose("No sub-command provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.subcommand.missing", "No sub-command provided");
throw new CommandError(
"invitation.subcommand.missing",
"No sub-command provided",
);
}
switch (subCommand) {
case "create": {
const templateQuery = args[1];
const actionIdentifier = args[2];
deps.io.verbose(`Template query: ${templateQuery}, action identifier: ${actionIdentifier}`);
deps.io.verbose(
`Template query: ${templateQuery}, action identifier: ${actionIdentifier}`,
);
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");
throw new CommandError(
"invitation.create.arguments_missing",
"No template file or action identifier provided",
);
}
const template = await resolveTemplate(deps, templateQuery);
@@ -302,7 +345,9 @@ export const handleInvitationCommand = async (
deps.io.verbose(`XOInvitation created: ${formatObject(rawInvitation)}`);
const invitationInstance = await deps.app.createInvitation(rawInvitation);
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
deps.io.verbose(
`Invitation created: ${formatObject(invitationInstance.data)}`,
);
const variables = parseVariablesFromOptions(options);
deps.io.verbose(`Variables: ${formatObject(variables)}`);
@@ -312,7 +357,10 @@ export const handleInvitationCommand = async (
const params = await buildAppendParams(deps, invitationInstance, options);
if (!params) {
throw new CommandError("invitation.create.append_params_failed", "Failed to build append parameters");
throw new CommandError(
"invitation.create.append_params_failed",
"Failed to build append parameters",
);
}
const { inputs, outputs } = params;
@@ -322,36 +370,50 @@ export const handleInvitationCommand = async (
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitationInstance.data.invitationIdentifier}.json`;
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
writeFileSync(invitationFilePath, encodeExtendedJson(invitationInstance.data, 2));
deps.io.out(`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`);
writeFileSync(
invitationFilePath,
encodeExtendedJson(invitationInstance.data, 2),
);
deps.io.out(
`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`,
);
const missingRequirements = await invitationInstance.getMissingRequirements();
const missingRequirements =
await invitationInstance.getMissingRequirements();
const hasMissing =
(missingRequirements.variables?.length ?? 0) > 0 ||
(missingRequirements.inputs?.length ?? 0) > 0 ||
(missingRequirements.outputs?.length ?? 0) > 0 ||
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
(missingRequirements.roles !== undefined &&
Object.keys(missingRequirements.roles).length > 0);
if (hasMissing) {
deps.io.out(`\n${bold("Remaining requirements:")}`);
deps.io.out(formatObject(missingRequirements));
} else {
const shouldSign = options["sign"] === "true" || options["broadcast"] === "true";
const shouldSign =
options["sign"] === "true" || options["broadcast"] === "true";
const shouldBroadcast = options["broadcast"] === "true";
if (shouldSign) {
await invitationInstance.sign();
deps.io.out(`Invitation signed: ${invitationInstance.data.invitationIdentifier}`);
deps.io.out(
`Invitation signed: ${invitationInstance.data.invitationIdentifier}`,
);
}
if (shouldBroadcast) {
const txHash = await invitationInstance.broadcast();
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
} else if (!shouldSign) {
deps.io.out(`\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}`,
);
}
}
return { invitationIdentifier: invitationInstance.data.invitationIdentifier };
return {
invitationIdentifier: invitationInstance.data.invitationIdentifier,
};
}
case "append": {
@@ -361,7 +423,10 @@ export const handleInvitationCommand = async (
if (!invitationIdentifier) {
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.append.identifier_missing", "No invitation identifier provided");
throw new CommandError(
"invitation.append.identifier_missing",
"No invitation identifier provided",
);
}
const invitation = deps.app.invitations.find(
@@ -369,7 +434,10 @@ export const handleInvitationCommand = async (
);
if (!invitation) {
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.append.not_found", `Invitation not found: ${invitationIdentifier}`);
throw new CommandError(
"invitation.append.not_found",
`Invitation not found: ${invitationIdentifier}`,
);
}
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
@@ -381,12 +449,20 @@ export const handleInvitationCommand = async (
const params = await buildAppendParams(deps, invitation, options);
if (!params) {
throw new CommandError("invitation.append.params_failed", "Failed to build append parameters");
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) {
const error = "Nothing to append. Provide variables (-var-<name> <value>), inputs (--add-input <txhash>:<vout>), or outputs (--add-output <identifier>).";
if (
variables.length === 0 &&
inputs.length === 0 &&
outputs.length === 0
) {
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);
}
@@ -399,20 +475,24 @@ export const handleInvitationCommand = async (
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitation.data.invitationIdentifier}.json`;
writeFileSync(invitationFilePath, encodeExtendedJson(invitation.data, 2));
deps.io.out(`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`);
deps.io.out(
`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`,
);
const missingRequirements = await invitation.getMissingRequirements();
const hasMissing =
(missingRequirements.variables?.length ?? 0) > 0 ||
(missingRequirements.inputs?.length ?? 0) > 0 ||
(missingRequirements.outputs?.length ?? 0) > 0 ||
(missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0);
(missingRequirements.roles !== undefined &&
Object.keys(missingRequirements.roles).length > 0);
if (hasMissing) {
deps.io.out(`\n${bold("Remaining requirements:")}`);
deps.io.out(formatObject(missingRequirements));
} else {
const shouldSign = options["sign"] === "true" || options["broadcast"] === "true";
const shouldSign =
options["sign"] === "true" || options["broadcast"] === "true";
const shouldBroadcast = options["broadcast"] === "true";
if (shouldSign) {
@@ -424,7 +504,9 @@ export const handleInvitationCommand = async (
const txHash = await invitation.broadcast();
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
} else if (!shouldSign) {
deps.io.out(`\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}`,
);
}
}
return { invitationIdentifier };
@@ -436,15 +518,22 @@ export const handleInvitationCommand = async (
if (!invitationIdentifier) {
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.sign.identifier_missing", "No invitation identifier provided");
throw new CommandError(
"invitation.sign.identifier_missing",
"No invitation identifier provided",
);
}
const invitation = deps.app.invitations.find(
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
(candidate) =>
candidate.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.sign.not_found", `Invitation not found: ${invitationIdentifier}`);
throw new CommandError(
"invitation.sign.not_found",
`Invitation not found: ${invitationIdentifier}`,
);
}
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
@@ -460,20 +549,29 @@ export const handleInvitationCommand = async (
if (!invitationIdentifier) {
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.broadcast.identifier_missing", "No invitation identifier provided");
throw new CommandError(
"invitation.broadcast.identifier_missing",
"No invitation identifier provided",
);
}
const invitation = deps.app.invitations.find(
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
(candidate) =>
candidate.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.broadcast.not_found", `Invitation not found: ${invitationIdentifier}`);
throw new CommandError(
"invitation.broadcast.not_found",
`Invitation not found: ${invitationIdentifier}`,
);
}
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
const txHash = await invitation.broadcast();
deps.io.verbose(`Invitation broadcasted: ${formatObject(invitation.data)}`);
deps.io.verbose(
`Invitation broadcasted: ${formatObject(invitation.data)}`,
);
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
return { invitationIdentifier, txHash };
}
@@ -484,19 +582,28 @@ export const handleInvitationCommand = async (
if (!invitationIdentifier) {
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.requirements.identifier_missing", "No invitation identifier provided");
throw new CommandError(
"invitation.requirements.identifier_missing",
"No invitation identifier provided",
);
}
const invitation = deps.app.invitations.find(
(candidate) => candidate.data.invitationIdentifier === invitationIdentifier,
(candidate) =>
candidate.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError("invitation.requirements.not_found", `Invitation not found: ${invitationIdentifier}`);
throw new CommandError(
"invitation.requirements.not_found",
`Invitation not found: ${invitationIdentifier}`,
);
}
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
const requirements = await deps.app.engine.listRequirements(invitation.data);
const requirements = await deps.app.engine.listRequirements(
invitation.data,
);
deps.io.verbose(`Requirements: ${formatObject(requirements)}`);
deps.io.out(formatObject(requirements));
return { invitationIdentifier };
@@ -509,56 +616,90 @@ export const handleInvitationCommand = async (
if (!invitationFilePath) {
deps.io.verbose("No invitation file provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.inspect.file_missing", "No invitation file provided");
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];
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}`);
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)));
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)
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 ?? []);
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 ?? []);
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 ?? []);
const variables = invitationInstance.data.commits.flatMap(
(commit) => commit.data.variables ?? [],
);
deps.io.verbose(`Variables: ${formatObject(variables)}`);
return {
return {
templateName: template?.name ?? "Unknown",
actionIdentifier: invitationInstance.data.actionIdentifier,
status: status,
@@ -576,7 +717,10 @@ export const handleInvitationCommand = async (
if (!invitationFilePath) {
deps.io.verbose("No invitation file provided");
printInvitationHelp(deps.io);
throw new CommandError("invitation.import.file_missing", "No invitation file provided");
throw new CommandError(
"invitation.import.file_missing",
"No invitation file provided",
);
}
const invitationFile = await readFileSync(invitationFilePath, "utf8");
@@ -585,14 +729,20 @@ export const handleInvitationCommand = async (
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
const xoInvitation = await deps.app.engine.createInvitation(invitation);
const invitationInstance = await deps.app.createInvitation(xoInvitation);
deps.io.verbose(`Invitation created: ${formatObject(invitationInstance.data)}`);
return { invitationIdentifier: invitationInstance.data.invitationIdentifier };
deps.io.verbose(
`Invitation created: ${formatObject(invitationInstance.data)}`,
);
return {
invitationIdentifier: invitationInstance.data.invitationIdentifier,
};
}
case "list": {
const invitations = await Promise.all(
deps.app.invitations.map(async (invitation) => {
const template = await deps.app.engine.getTemplate(invitation.data.templateIdentifier);
const template = await deps.app.engine.getTemplate(
invitation.data.templateIdentifier,
);
return {
invitationIdentifier: invitation.data.invitationIdentifier,
templateIdentifier: invitation.data.templateIdentifier,
@@ -615,6 +765,9 @@ export const handleInvitationCommand = async (
default:
deps.io.verbose(`Unknown invitation sub-command: ${subCommand}`);
printInvitationHelp(deps.io);
throw new CommandError("invitation.subcommand.unknown", `Unknown invitation sub-command: ${subCommand}`);
throw new CommandError(
"invitation.subcommand.unknown",
`Unknown invitation sub-command: ${subCommand}`,
);
}
};

View File

@@ -1,5 +1,10 @@
import { bold, dim } from "../cli-utils.js";
import { listMnemonicFiles, createMnemonicFile, createMnemonicSeed, loadMnemonic } from "../mnemonic.js";
import {
listMnemonicFiles,
createMnemonicFile,
createMnemonicSeed,
loadMnemonic,
} from "../mnemonic.js";
import type { BaseCommandDependencies, CommandIO } from "./types.js";
import { CommandError } from "./types.js";
@@ -8,7 +13,7 @@ import { CommandError } from "./types.js";
*/
export const printMnemonicHelp = (io: CommandIO): void => {
io.out(
`
`
${bold("Usage:")} xo-cli mnemonic <sub-command>
${bold("Sub-commands:")}
@@ -18,7 +23,8 @@ ${bold("Sub-commands:")}
${bold("Options:")}
-o --output <output-filename> ${dim("Output filename for the mnemonic file")}
-h --help ${dim("Show this help message")}
`);
`,
);
};
/**
@@ -39,13 +45,20 @@ export const handleMnemonicCommand = async (
if (!subCommand) {
deps.io.verbose("No sub-command provided");
printMnemonicHelp(deps.io);
throw new CommandError("mnemonic.subcommand.missing", "No sub-command provided");
throw new CommandError(
"mnemonic.subcommand.missing",
"No sub-command provided",
);
}
switch (subCommand) {
case "create": {
const mnemonicSeed = createMnemonicSeed();
const savedAs = createMnemonicFile(mnemonicsDir, mnemonicSeed, options["output"]);
const savedAs = createMnemonicFile(
mnemonicsDir,
mnemonicSeed,
options["output"],
);
deps.io.out(`Mnemonic file created: ${savedAs} (${mnemonicSeed})`);
return { savedAs };
@@ -57,18 +70,25 @@ export const handleMnemonicCommand = async (
if (!mnemonicSeed) {
deps.io.verbose("No mnemonic seed provided");
printMnemonicHelp(deps.io);
throw new CommandError("mnemonic.import.seed_missing", "No mnemonic seed provided");
throw new CommandError(
"mnemonic.import.seed_missing",
"No mnemonic seed provided",
);
}
deps.io.verbose(`Mnemonic seed: ${mnemonicSeed}`);
const savedAs = createMnemonicFile(mnemonicsDir, mnemonicSeed, options["output"]);
const savedAs = createMnemonicFile(
mnemonicsDir,
mnemonicSeed,
options["output"],
);
deps.io.out(`Mnemonic file created: ${savedAs}`);
return { savedAs };
}
case "list": {
const mnemonicFiles = listMnemonicFiles(mnemonicsDir);
deps.io.out(mnemonicFiles.join('\n'));
deps.io.out(mnemonicFiles.join("\n"));
return { count: mnemonicFiles.length };
}
@@ -78,7 +98,10 @@ export const handleMnemonicCommand = async (
if (!mnemonicFile) {
deps.io.verbose("No mnemonic file provided");
printMnemonicHelp(deps.io);
throw new CommandError("mnemonic.expose.file_missing", "No mnemonic file provided");
throw new CommandError(
"mnemonic.expose.file_missing",
"No mnemonic file provided",
);
}
try {
@@ -96,6 +119,9 @@ export const handleMnemonicCommand = async (
default:
deps.io.err(`Unknown sub-command: ${subCommand}`);
printMnemonicHelp(deps.io);
throw new CommandError("mnemonic.subcommand.unknown", `Unknown sub-command: ${subCommand}`);
throw new CommandError(
"mnemonic.subcommand.unknown",
`Unknown sub-command: ${subCommand}`,
);
}
};

View File

@@ -12,7 +12,7 @@ import { resolveTemplate } from "../utils.js";
*/
export const printReceiveHelp = (io: CommandIO): void => {
io.out(
`
`
${bold("Usage:")} xo-cli receive <template-file> <output-identifier> [role-identifier]
${bold("Description:")}
@@ -25,7 +25,8 @@ ${bold("Arguments:")}
${bold("Options:")}
-h --help ${dim("Show this help message")}
`);
`,
);
};
/**
@@ -47,12 +48,17 @@ export const handleReceiveCommand = async (
const outputIdentifier = args[1];
const roleIdentifier = args[2];
deps.io.verbose(`Receive args - template: ${templateQuery}, output: ${outputIdentifier}, role: ${roleIdentifier}`);
deps.io.verbose(
`Receive args - template: ${templateQuery}, output: ${outputIdentifier}, role: ${roleIdentifier}`,
);
if (!templateQuery || !outputIdentifier) {
deps.io.verbose("Missing required arguments");
printReceiveHelp(deps.io);
throw new CommandError("receive.arguments.missing", "Missing required arguments");
throw new CommandError(
"receive.arguments.missing",
"Missing required arguments",
);
}
// Resolve and read the template file
@@ -69,11 +75,17 @@ export const handleReceiveCommand = async (
deps.io.verbose(`Locking bytecode hex: ${lockingBytecodeHex}`);
// Convert the locking bytecode to a BCH cash address
const result = lockingBytecodeToCashAddress({ bytecode: hexToBin(lockingBytecodeHex), prefix: 'bitcoincash' });
const result = lockingBytecodeToCashAddress({
bytecode: hexToBin(lockingBytecodeHex),
prefix: "bitcoincash",
});
if (typeof result === 'string') {
if (typeof result === "string") {
deps.io.err(`Failed to encode address: ${result}`);
throw new CommandError("receive.address.encode_failed", `Failed to encode address: ${result}`);
throw new CommandError(
"receive.address.encode_failed",
`Failed to encode address: ${result}`,
);
}
deps.io.out(result.address);

View File

@@ -10,7 +10,7 @@ import { CommandError } from "./types.js";
*/
export const printResourceHelp = (io: CommandIO): void => {
io.out(
`
`
${bold("Usage:")} xo-cli resource <sub-command>
${bold("Sub-commands:")}
@@ -19,14 +19,20 @@ ${bold("Sub-commands:")}
- list all ${dim("List all resources (reserved + unreserved)")}
- unreserve <txhash:vout> ${dim("Unreserve a specific UTXO")}
- unreserve-all ${dim("Unreserve all reserved UTXOs")}
`);
`,
);
};
/**
* Formats a single UTXO for display, optionally including reservation info.
*/
function formatResource(resource: UnspentOutputData, showReserved = false): string {
const outpoint = bold(`${resource.outpointTransactionHash}:${resource.outpointIndex}`);
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})`);
@@ -57,7 +63,10 @@ export const handleResourceCommand = async (
if (!subCommand) {
deps.io.verbose("No sub-command provided");
printResourceHelp(deps.io);
throw new CommandError("resource.subcommand.missing", "No sub-command provided");
throw new CommandError(
"resource.subcommand.missing",
"No sub-command provided",
);
}
switch (subCommand) {
@@ -80,9 +89,13 @@ export const handleResourceCommand = async (
}
const showReserved = qualifier === "all" || qualifier === "reserved";
const formattedResources = filtered.map((r) => formatResource(r, showReserved));
const formattedResources = filtered.map((r) =>
formatResource(r, showReserved),
);
deps.io.out(formattedResources.join("\n"));
deps.io.out(`Total satoshis: ${filtered.reduce((acc, r) => acc + r.valueSatoshis, 0)}`);
deps.io.out(
`Total satoshis: ${filtered.reduce((acc, r) => acc + r.valueSatoshis, 0)}`,
);
deps.io.out(`Total resources: ${filtered.length}`);
return { count: filtered.length };
}
@@ -92,20 +105,33 @@ export const handleResourceCommand = async (
if (!outpointArg) {
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.");
throw new CommandError(
"resource.unreserve.outpoint_missing",
"Please provide a UTXO in <txhash>:<vout> format.",
);
}
const separatorIndex = outpointArg.lastIndexOf(":");
if (separatorIndex === -1) {
deps.io.err(`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
throw new CommandError("resource.unreserve.outpoint_invalid", `Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
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)) {
deps.io.err(`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
throw new CommandError("resource.unreserve.outpoint_invalid", `Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
deps.io.err(
`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`,
);
throw new CommandError(
"resource.unreserve.outpoint_invalid",
`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`,
);
}
const allResources = await deps.app.engine.listUnspentOutputsData();
@@ -115,7 +141,10 @@ export const handleResourceCommand = async (
if (!target) {
deps.io.err(`UTXO not found: ${txHash}:${vout}`);
throw new CommandError("resource.unreserve.utxo_missing", `UTXO not found: ${txHash}:${vout}`);
throw new CommandError(
"resource.unreserve.utxo_missing",
`UTXO not found: ${txHash}:${vout}`,
);
}
if (!target.reservedBy) {
@@ -125,9 +154,11 @@ export const handleResourceCommand = async (
await deps.app.engine.unreserveResources(
[{ outpointTransactionHash: hexToBin(txHash), outpointIndex: vout }],
target.reservedBy ,
target.reservedBy,
);
deps.io.out(
`Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.reservedBy})`,
);
deps.io.out(`Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.reservedBy})`);
return {};
}
@@ -144,7 +175,10 @@ export const handleResourceCommand = async (
default: {
deps.io.verbose(`Unknown resource sub-command: ${subCommand}`);
printResourceHelp(deps.io);
throw new CommandError("resource.subcommand.unknown", `Unknown resource sub-command: ${subCommand}`);
throw new CommandError(
"resource.subcommand.unknown",
`Unknown resource sub-command: ${subCommand}`,
);
}
}
};

View File

@@ -14,7 +14,7 @@ import { resolveTemplate } from "../utils.js";
*/
export const printTemplateHelp = (io: CommandIO): void => {
io.out(
`
`
${bold("Usage:")} xo-cli template <sub-command>
${bold("Sub-commands:")}
@@ -23,7 +23,8 @@ ${bold("Sub-commands:")}
- 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")}
`);
`,
);
};
/**
@@ -41,8 +42,11 @@ export const handleTemplateListCommand = async (
if (!templateCategory) {
const templates = await deps.app.engine.listImportedTemplates();
const formattedTemplates = templates.map((template: XOTemplate) => `${bold(generateTemplateIdentifier(template))} - ${dim(template.name)} ${dim(template.description)}`);
deps.io.out(formattedTemplates.join('\n'));
const formattedTemplates = templates.map(
(template: XOTemplate) =>
`${bold(generateTemplateIdentifier(template))} - ${dim(template.name)} ${dim(template.description)}`,
);
deps.io.out(formattedTemplates.join("\n"));
return { count: templates.length };
}
@@ -51,13 +55,19 @@ export const handleTemplateListCommand = async (
if (!templateIdentifier) {
deps.io.err("No template identifier provided");
throw new CommandError("template.list.identifier_missing", "No template identifier provided");
throw new CommandError(
"template.list.identifier_missing",
"No template identifier provided",
);
}
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
if (!rawTemplate) {
deps.io.err(`No template found: ${templateIdentifier}`);
throw new CommandError("template.list.not_found", `No template found: ${templateIdentifier}`);
throw new CommandError(
"template.list.not_found",
`No template found: ${templateIdentifier}`,
);
}
const template = await resolveTemplateReferences(rawTemplate);
@@ -66,47 +76,65 @@ export const handleTemplateListCommand = async (
switch (templateCategory) {
case "action": {
const actions = template.actions;
const formattedActions = Object.entries(actions).map(([actionIdentifier, action]) => `${bold(actionIdentifier)} ${dim(action.name)} ${dim(action.description)}`);
deps.io.out(formattedActions.join('\n'));
const formattedActions = Object.entries(actions).map(
([actionIdentifier, action]) =>
`${bold(actionIdentifier)} ${dim(action.name)} ${dim(action.description)}`,
);
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)}`);
deps.io.out(formattedTransactions.join('\n'));
const formattedTransactions = Object.entries(transactions).map(
([transactionIdentifier, transaction]) =>
`${bold(transactionIdentifier)} ${dim(transaction.name)} ${dim(transaction.description)}`,
);
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)}`);
deps.io.out(formattedOutputs.join('\n'));
const formattedOutputs = Object.entries(outputs).map(
([outputIdentifier, output]) =>
`${bold(outputIdentifier)} ${dim(output.name)} ${dim(output.description)}`,
);
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)}`);
deps.io.out(formattedLockingscripts.join('\n'));
const formattedLockingscripts = Object.entries(lockingscripts).map(
([lockingScriptIdentifier, lockingScript]) =>
`${bold(lockingScriptIdentifier)} ${dim(lockingScript.name)} ${dim(lockingScript.description)}`,
);
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)}`);
deps.io.out(formattedVariables.join('\n'));
const formattedVariables = Object.entries(variables).map(
([variableIdentifier, variable]) =>
`${bold(variableIdentifier)} ${dim(variable.name)} ${dim(variable.description)}`,
);
deps.io.out(formattedVariables.join("\n"));
return {};
}
default: {
deps.io.verbose(`Unknown template category: ${templateCategory}`);
throw new CommandError("template.list.category_unknown", `Unknown template category: ${templateCategory}`);
throw new CommandError(
"template.list.category_unknown",
`Unknown template category: ${templateCategory}`,
);
}
}
}
};
/**
* Prints the help message for the template inspect command
*/
export const printTemplateInspectHelp = (io: CommandIO): void => {
io.out(
`
`
${bold("Usage:")} xo-cli template inspect <category> <identifier> <field>
${bold("Arguments:")}
@@ -120,7 +148,8 @@ ${bold("Categories:")}
- output <output-identifier> ${dim("Inspect an output")}
- lockingscript <lockingscript-identifier> ${dim("Inspect a lockingscript")}
- variable <variable-identifier> ${dim("Inspect a variable")}
`);
`,
);
};
/**
@@ -137,12 +166,17 @@ export const handleTemplateInspectCommand = async (
const templateQuery = args[1];
const templateField = args[2];
deps.io.verbose(`Template inspect args - category: ${templateCategory}, identifier: ${templateQuery}, field: ${templateField}`);
deps.io.verbose(
`Template inspect args - category: ${templateCategory}, identifier: ${templateQuery}, field: ${templateField}`,
);
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");
throw new CommandError(
"template.inspect.arguments_missing",
"No template category, identifier, or field provided",
);
}
const originalTemplate = await resolveTemplate(deps, templateQuery);
@@ -156,7 +190,10 @@ export const handleTemplateInspectCommand = async (
const action = template.actions[templateField];
if (!action) {
deps.io.err(`No action found: ${templateField}`);
throw new CommandError("template.inspect.action_missing", `No action found: ${templateField}`);
throw new CommandError(
"template.inspect.action_missing",
`No action found: ${templateField}`,
);
}
deps.io.out(formatObject(action));
return {};
@@ -165,7 +202,10 @@ export const handleTemplateInspectCommand = async (
const transaction = template.transactions?.[templateField];
if (!transaction) {
deps.io.err(`No transaction found: ${templateField}`);
throw new CommandError("template.inspect.transaction_missing", `No transaction found: ${templateField}`);
throw new CommandError(
"template.inspect.transaction_missing",
`No transaction found: ${templateField}`,
);
}
deps.io.out(formatObject(transaction));
return {};
@@ -174,7 +214,10 @@ export const handleTemplateInspectCommand = async (
const output = template.outputs[templateField];
if (!output) {
deps.io.err(`No output found: ${templateField}`);
throw new CommandError("template.inspect.output_missing", `No output found: ${templateField}`);
throw new CommandError(
"template.inspect.output_missing",
`No output found: ${templateField}`,
);
}
deps.io.out(formatObject(output));
return {};
@@ -183,7 +226,10 @@ export const handleTemplateInspectCommand = async (
const lockingscript = template.lockingScripts[templateField];
if (!lockingscript) {
deps.io.err(`No lockingscript found: ${templateField}`);
throw new CommandError("template.inspect.lockingscript_missing", `No lockingscript found: ${templateField}`);
throw new CommandError(
"template.inspect.lockingscript_missing",
`No lockingscript found: ${templateField}`,
);
}
deps.io.out(formatObject(lockingscript));
return {};
@@ -192,17 +238,23 @@ export const handleTemplateInspectCommand = async (
const variable = template.variables?.[templateField];
if (!variable) {
deps.io.err(`No variable found: ${templateField}`);
throw new CommandError("template.inspect.variable_missing", `No variable found: ${templateField}`);
throw new CommandError(
"template.inspect.variable_missing",
`No variable found: ${templateField}`,
);
}
deps.io.out(formatObject(variable));
return {};
}
default: {
deps.io.verbose(`Unknown template category: ${templateCategory}`);
throw new CommandError("template.inspect.category_unknown", `Unknown template category: ${templateCategory}`);
throw new CommandError(
"template.inspect.category_unknown",
`Unknown template category: ${templateCategory}`,
);
}
}
}
};
/**
* Handles the template command.
@@ -221,7 +273,10 @@ export const handleTemplateCommand = async (
if (!subCommand) {
deps.io.verbose("No sub-command provided");
printTemplateHelp(deps.io);
throw new CommandError("template.subcommand.missing", "No sub-command provided");
throw new CommandError(
"template.subcommand.missing",
"No sub-command provided",
);
}
switch (subCommand) {
@@ -232,7 +287,10 @@ export const handleTemplateCommand = async (
if (!templateFile) {
deps.io.verbose("No template file provided");
printTemplateHelp(deps.io);
throw new CommandError("template.import.file_missing", "No template file provided");
throw new CommandError(
"template.import.file_missing",
"No template file provided",
);
}
const templatePath = path.resolve(`${process.cwd()}/${templateFile}`);
@@ -241,7 +299,10 @@ export const handleTemplateCommand = async (
if (!existsSync(templatePath)) {
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}`);
throw new CommandError(
"template.import.file_not_found",
`Template file does not exist: ${templatePath}`,
);
}
const template = await readFileSync(templatePath, "utf8");
@@ -262,17 +323,31 @@ export const handleTemplateCommand = async (
const outputIdentifier = args[2];
const roleIdentifier = args[3];
if (!templateFile || !outputIdentifier || !roleIdentifier) {
deps.io.verbose("No template file, output identifier, or role identifier provided");
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");
throw new CommandError(
"template.default.arguments_missing",
"No template file, output identifier, or role identifier provided",
);
}
deps.io.verbose(`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`);
await deps.app.engine.setDefaultLockingParameters(templateFile, outputIdentifier, roleIdentifier);
deps.io.verbose(
`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`,
);
await deps.app.engine.setDefaultLockingParameters(
templateFile,
outputIdentifier,
roleIdentifier,
);
return {};
}
default:
deps.io.verbose(`Unknown template sub-command: ${subCommand}`);
printTemplateHelp(deps.io);
throw new CommandError("template.subcommand.unknown", `Unknown template sub-command: ${subCommand}`);
throw new CommandError(
"template.subcommand.unknown",
`Unknown template sub-command: ${subCommand}`,
);
}
};