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,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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user