Breaking-Change: Extremely rough update to work with Kioks wallet
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user