Breaking-Change: Extremely rough update to work with Kioks wallet

This commit is contained in:
2026-05-22 14:11:07 +02:00
parent 3d6518e465
commit def261b568
17 changed files with 422 additions and 107 deletions

View File

@@ -11,13 +11,21 @@ import {
resolveProvidedLockingBytecodeHex,
mapUnspentOutputsToSelectable,
autoSelectGreedyUtxos,
hasMissingRequirements,
} from "../../utils/invitation-flow.js";
import { encodeExtendedJson } from "../../utils/ext-json.js";
import { deserializeInvitation, serializeInvitation } from "@xo-cash/engine";
import type { XOInvitation } from "@xo-cash/types";
import { resolveTemplate } from "../utils.js";
const DEFAULT_FEE = 500n;
const DUST_THRESHOLD = 546n;
/**
* Serializes an invitation to pretty-printed JSON for file export.
*/
const formatInvitationForFile = (invitation: XOInvitation, indent = 2): string =>
JSON.stringify(JSON.parse(serializeInvitation(invitation)), null, indent);
/**
* Result of parsing CLI options into inputs and outputs for an append call.
* A `null` return signals a fatal error that was already logged to stderr.
@@ -286,9 +294,13 @@ ${bold("Sub-commands:")}
- broadcast <invitation-id> ${dim("Broadcast an invitation")}
- requirements <invitation-id> ${dim("Show requirements for an invitation")}
- import <invitation-file> ${dim("Import an invitation from a file")}
- export <invitation-id> [output-file] ${dim("Export an invitation to stdout or a file")}
- inspect <invitation-id | invitation-file> ${dim("Inspect an invitation")}
- list ${dim("List all invitations")}
${bold("Export options:")}
-o --output <output-filename> ${dim("Output filename for the exported invitation")}
${bold("Create / Append options:")}
-var-<name> <value> ${dim("Set a variable (e.g. -var-requested-satoshis 1000)")}
--add-input <txhash:vout> ${dim("Add UTXO input(s), comma-separated (e.g. abc123:0,def456:1)")}
@@ -311,6 +323,7 @@ export type InvitationCommandResult = {
invitationIdentifier?: string;
txHash?: string;
count?: number;
outputFile?: string;
templateName?: string;
actionIdentifier?: string;
status?: string;
@@ -320,6 +333,66 @@ export type InvitationCommandResult = {
variables?: unknown[];
};
/**
* Handles the invitation export command.
* Throws CommandError on failure, returns result data on success.
* @param deps - The command dependencies.
* @param args - Positional args after "export", e.g. ["invitation-id"] or ["invitation-id", "invitation.json"].
* @param options - Parsed option flags.
*/
export const handleInvitationExportCommand = async (
deps: CommandDependencies,
args: string[],
options: Record<string, string>,
): Promise<{ invitationIdentifier?: string; outputFile?: string }> => {
const invitationIdentifier = args[0];
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
if (!invitationIdentifier) {
deps.io.verbose("No invitation identifier provided");
printInvitationHelp(deps.io);
throw new CommandError(
"invitation.export.identifier_missing",
"No invitation identifier provided",
);
}
const invitation = deps.app.invitations.find(
(candidate) =>
candidate.data.invitationIdentifier === invitationIdentifier,
);
if (!invitation) {
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
throw new CommandError(
"invitation.export.not_found",
`Invitation not found: ${invitationIdentifier}`,
);
}
const serializedInvitation = serializeInvitation(invitation.data);
const outputFile = options["output"] ?? args[1];
if (!outputFile) {
deps.io.out(serializedInvitation);
return { invitationIdentifier };
}
const outputPath = path.resolve(process.cwd(), outputFile);
try {
writeFileSync(outputPath, serializedInvitation);
} catch (error) {
throw new CommandError(
"invitation.export.write_failed",
`Failed to export invitation to file: ${outputPath} (${error instanceof Error ? error.message : "unknown error"})`,
);
}
deps.io.out(`Invitation exported to: ${outputPath}`);
return { invitationIdentifier, outputFile: outputPath };
};
/**
* Handles the invitation command.
* Throws CommandError on failure, returns result data on success.
@@ -411,7 +484,7 @@ export const handleInvitationCommand = async (
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
writeFileSync(
invitationFilePath,
encodeExtendedJson(invitationInstance.data, 2),
formatInvitationForFile(invitationInstance.data),
);
deps.io.out(
`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`,
@@ -421,11 +494,8 @@ export const handleInvitationCommand = async (
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);
hasMissingRequirements(missingRequirements.templateRequirements) ||
missingRequirements.inputsMissingSignatures.length > 0;
// If there are missing requirements, print them out
if (hasMissing) {
@@ -532,7 +602,10 @@ export const handleInvitationCommand = async (
// Write the invitation to a file in the working directory
// TODO: Support the -o flag to specify the output path
const invitationFilePath = `${deps.paths.workingDir}/inv-${invitation.data.invitationIdentifier}.json`;
writeFileSync(invitationFilePath, encodeExtendedJson(invitation.data, 2));
writeFileSync(
invitationFilePath,
formatInvitationForFile(invitation.data),
);
deps.io.out(
`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`,
);
@@ -540,11 +613,8 @@ export const handleInvitationCommand = async (
// Get the missing requirements for the invitation. This will tell us if we are missing any variables, inputs, outputs, or roles.
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);
hasMissingRequirements(missingRequirements.templateRequirements) ||
missingRequirements.inputsMissingSignatures.length > 0;
// If there are missing requirements, print them out
if (hasMissing) {
@@ -721,11 +791,10 @@ export const handleInvitationCommand = async (
}
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
const invitationFile = await readFileSync(invitationFilePath, "utf8");
const invitationFile = readFileSync(invitationFilePath, "utf8");
deps.io.verbose(`Invitation file: ${invitationFile}`);
// Parse the invitation file
const invitation = JSON.parse(invitationFile);
const invitation = deserializeInvitation(invitationFile);
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
// Create the invitation instance (NOTE: Not an engine compatible invitation. This is the wrapped format)
@@ -839,19 +908,27 @@ export const handleInvitationCommand = async (
}
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
const invitationFile = await readFileSync(invitationFilePath, "utf8");
const invitationFile = readFileSync(invitationFilePath, "utf8");
deps.io.verbose(`Invitation file: ${invitationFile}`);
// Parse the invitation file
const invitation = JSON.parse(invitationFile);
const invitation = deserializeInvitation(invitationFile);
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
// "Creates" the invitiation in the engine. This method acts as both creation or import depending on the data that is being passed in
const xoInvitation = await deps.app.engine.createInvitation(invitation);
deps.io.verbose(`XOInvitation: ${formatObject(xoInvitation)}`);
const template = await deps.app.engine.getTemplate(
invitation.templateIdentifier,
);
if (!template) {
throw new CommandError(
"invitation.import.template_not_found",
`Template not found: ${invitation.templateIdentifier}. ` +
`Import the matching template first with: xo-cli template import <template-file>`,
);
}
// Create the invitation instance (NOTE: Not an engine compatible invitation. This is the wrapped format)
const invitationInstance = await deps.app.createInvitation(xoInvitation);
// Accept and track the invitation. Invitation.create calls acceptInvitation
// internally — do not call engine.createInvitation here, that creates a new
// invitation and discards the imported commits.
const invitationInstance = await deps.app.createInvitation(invitation);
deps.io.verbose(
`Invitation created: ${formatObject(invitationInstance.data)}`,
);
@@ -862,6 +939,10 @@ export const handleInvitationCommand = async (
};
}
case "export": {
return handleInvitationExportCommand(deps, args.slice(1), options);
}
case "list": {
// List all the invitations
const invitations = await Promise.all(