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

This commit is contained in:
2026-04-06 11:56:09 +00:00
parent b475b23beb
commit 55c75501d5
24 changed files with 3284 additions and 77 deletions
+145
View File
@@ -0,0 +1,145 @@
import { hexToBin } from "@bitauth/libauth";
import { bold, dim } from "../cli-utils.js";
import type { CommandDependencies } from "./types.js";
/**
* Prints the help message for the resource command.
*/
export const printResourceHelp = () => {
console.log(
`
${bold("Usage:")} xo-cli resource <sub-command>
${bold("Sub-commands:")}
- list ${dim("List all unreserved resources")}
- list reserved ${dim("List reserved resources")}
- 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: { outpointTransactionHash: string; outpointIndex: number; valueSatoshis: number; outputIdentifier: string; minedAtHeight: number; reserved?: boolean; invitationIdentifier?: string }, 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}`);
return `${outpoint} ${value} ${output} ${height} ${inv}`;
}
return `${outpoint} ${value} ${output} ${height}`;
}
/**
* Handles the resource command.
* @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> => {
const subCommand = args[0];
deps.verboseLogger(`Resource sub-command: ${subCommand}`);
if (!subCommand) {
deps.verboseLogger("No sub-command provided");
printResourceHelp();
return;
}
switch (subCommand) {
case "list": {
const qualifier = args[1]; // "reserved", "all", or undefined (defaults to unreserved)
const allResources = await deps.app.engine.listUnspentOutputsData();
let filtered;
if (qualifier === "reserved") {
filtered = allResources.filter((r) => r.reserved);
} else if (qualifier === "all") {
filtered = allResources;
} else {
// Default: show only unreserved (selectable) resources
filtered = allResources.filter((r) => !r.reserved);
}
if (filtered.length === 0) {
console.log(dim("No resources found."));
return;
}
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;
}
case "unreserve": {
const outpointArg = args[1];
if (!outpointArg) {
console.error("Please provide a UTXO in <txhash>:<vout> format.");
printResourceHelp();
return;
}
const separatorIndex = outpointArg.lastIndexOf(":");
if (separatorIndex === -1) {
console.error(`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`);
return;
}
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;
}
// 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;
}
if (!target.reserved) {
console.log(dim("UTXO is not reserved. Nothing to do."));
return;
}
await deps.app.engine.unreserveResources(
[{ outpointTransactionHash: hexToBin(txHash), outpointIndex: vout }],
target.invitationIdentifier,
);
console.log(`Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.invitationIdentifier})`);
break;
}
case "unreserve-all": {
const count = await deps.app.unreserveAllResources();
if (count === 0) {
console.log(dim("No reserved resources to unreserve."));
} else {
console.log(`Unreserved ${bold(String(count))} resource(s).`);
}
break;
}
default: {
deps.verboseLogger(`Unknown resource sub-command: ${subCommand}`);
printResourceHelp();
return;
}
}
};