Formatting
This commit is contained in:
@@ -11,11 +11,11 @@ There are two global commands after install:
|
||||
|
||||
Wallet state lives under **`~/.config/xo-cli/`** (XDG-style), so you can run commands from any directory:
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `~/.config/xo-cli/mnemonics/` | Mnemonic files (`mnemonic-*`) |
|
||||
| `~/.config/xo-cli/data/` | Engine DB (`xo-wallet.db`) and invitation storage (`xo-invitations.db`) |
|
||||
| `~/.config/xo-cli/.wallet` | Last-used mnemonic reference (so `-m` can be omitted) |
|
||||
| Path | Purpose |
|
||||
| ----------------------------- | ----------------------------------------------------------------------- |
|
||||
| `~/.config/xo-cli/mnemonics/` | Mnemonic files (`mnemonic-*`) |
|
||||
| `~/.config/xo-cli/data/` | Engine DB (`xo-wallet.db`) and invitation storage (`xo-invitations.db`) |
|
||||
| `~/.config/xo-cli/.wallet` | Last-used mnemonic reference (so `-m` can be omitted) |
|
||||
|
||||
**Local to your shell’s current directory:** template JSON paths, invitation JSON you create/import, and any path you pass explicitly (e.g. `-m /abs/path/to/file`).
|
||||
|
||||
@@ -41,11 +41,11 @@ npx tsx src/index.ts # TUI
|
||||
|
||||
### Environment variables (TUI / `xo-tui`)
|
||||
|
||||
| Variable | Default |
|
||||
|----------|---------|
|
||||
| `SYNC_SERVER_URL` | `http://localhost:3000` |
|
||||
| `DB_PATH` | `~/.config/xo-cli/data` |
|
||||
| `DB_FILENAME` | `xo-wallet.db` |
|
||||
| Variable | Default |
|
||||
| ------------------------- | ----------------------------------------- |
|
||||
| `SYNC_SERVER_URL` | `http://localhost:3000` |
|
||||
| `DB_PATH` | `~/.config/xo-cli/data` |
|
||||
| `DB_FILENAME` | `xo-wallet.db` |
|
||||
| `INVITATION_STORAGE_PATH` | `~/.config/xo-cli/data/xo-invitations.db` |
|
||||
|
||||
## Getting Started
|
||||
@@ -71,8 +71,8 @@ The first time you pass `-m <name>`, that reference is saved to `~/.config/xo-cl
|
||||
|
||||
Mnemonic resolution order:
|
||||
|
||||
1. Absolute path, if the file exists
|
||||
2. Path relative to the current working directory
|
||||
1. Absolute path, if the file exists
|
||||
2. Path relative to the current working directory
|
||||
3. `~/.config/xo-cli/mnemonics/<basename>`
|
||||
|
||||
```bash
|
||||
@@ -82,11 +82,11 @@ xo-cli resource list
|
||||
|
||||
## Global Options (`xo-cli`)
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| Flag | Description |
|
||||
| ------------------------------ | --------------------------------------------------- |
|
||||
| `-m`, `--mnemonic-file <file>` | Mnemonic file (basename, cwd-relative, or absolute) |
|
||||
| `-v`, `--verbose` | Verbose output |
|
||||
| `-h`, `--help` | Help |
|
||||
| `-v`, `--verbose` | Verbose output |
|
||||
| `-h`, `--help` | Help |
|
||||
|
||||
Advanced: you can pass `--database-path`, `--database-filename`, and `--invitation-storage-path` to override the defaults under `~/.config/xo-cli/data/` (see `src/cli/index.ts`).
|
||||
|
||||
@@ -144,15 +144,15 @@ xo-cli invitation list
|
||||
|
||||
**Create / append options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-var-<name> <value>` | Template variable |
|
||||
| `--add-input <txhash:vout>` | Inputs (comma-separated) |
|
||||
| `--add-output <id>` | Override outputs (omit to auto-discover) |
|
||||
| `--auto-inputs` | Auto-select UTXOs |
|
||||
| `-role <role>` | Role for variables / bytecode |
|
||||
| `--sign` | Auto-sign when complete |
|
||||
| `--broadcast` | Auto-broadcast (implies `--sign`) |
|
||||
| Flag | Description |
|
||||
| --------------------------- | ---------------------------------------- |
|
||||
| `-var-<name> <value>` | Template variable |
|
||||
| `--add-input <txhash:vout>` | Inputs (comma-separated) |
|
||||
| `--add-output <id>` | Override outputs (omit to auto-discover) |
|
||||
| `--auto-inputs` | Auto-select UTXOs |
|
||||
| `-role <role>` | Role for variables / bytecode |
|
||||
| `--sign` | Auto-sign when complete |
|
||||
| `--broadcast` | Auto-broadcast (implies `--sign`) |
|
||||
|
||||
Invitation JSON files from `create` / `append` are written to the **current working directory**.
|
||||
|
||||
@@ -186,7 +186,7 @@ xo-cli completions fish | source
|
||||
|
||||
## File Conventions
|
||||
|
||||
| Location | Purpose |
|
||||
|----------|---------|
|
||||
| `~/.config/xo-cli/` | Global wallet state |
|
||||
| `./` (cwd) | Templates, invitation JSON, explicit paths |
|
||||
| Location | Purpose |
|
||||
| ------------------- | ------------------------------------------ |
|
||||
| `~/.config/xo-cli/` | Global wallet state |
|
||||
| `./` (cwd) | Templates, invitation JSON, explicit paths |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* CLI Argument extraction and validation.
|
||||
*
|
||||
*
|
||||
* Converts `-${key}` or `--${key}` to `key` in the args object.
|
||||
*/
|
||||
import { z } from "zod";
|
||||
@@ -15,28 +15,31 @@ import { z } from "zod";
|
||||
* verbose: "true",
|
||||
* },
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @param args - The CLI args to convert.
|
||||
* @returns The key-value object.
|
||||
*/
|
||||
export function convertArgsToObject(args: string[]): { args: string[], options: Record<string, string> } {
|
||||
export function convertArgsToObject(args: string[]): {
|
||||
args: string[];
|
||||
options: Record<string, string>;
|
||||
} {
|
||||
// Map of single-character short flags to their canonical long names
|
||||
const shortToFull: Record<string, string> = {
|
||||
'm': 'mnemonicFile',
|
||||
'o': 'output',
|
||||
'v': 'verbose',
|
||||
'h': 'help',
|
||||
m: "mnemonicFile",
|
||||
o: "output",
|
||||
v: "verbose",
|
||||
h: "help",
|
||||
};
|
||||
|
||||
// Flags that are always boolean and never consume the next argument as a value.
|
||||
// Uses the canonical (expanded) names so the check works after short-form resolution.
|
||||
const booleanFlags = new Set<string>([
|
||||
'verbose',
|
||||
'help',
|
||||
'autoInputs',
|
||||
'sign',
|
||||
'broadcast',
|
||||
'install',
|
||||
"verbose",
|
||||
"help",
|
||||
"autoInputs",
|
||||
"sign",
|
||||
"broadcast",
|
||||
"install",
|
||||
]);
|
||||
|
||||
const positionalArgs: string[] = [];
|
||||
@@ -55,7 +58,9 @@ export function convertArgsToObject(args: string[]): { args: string[], options:
|
||||
// - Remove the leading `-`s
|
||||
// - Convert kebab-case to camelCase
|
||||
// - Expand known short forms to their full names
|
||||
let key = arg.replace(/^-+/, "").replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
let key = arg
|
||||
.replace(/^-+/, "")
|
||||
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
key = shortToFull[key] ?? key;
|
||||
|
||||
// Known boolean flags never take a value
|
||||
@@ -78,4 +83,4 @@ export function convertArgsToObject(args: string[]): { args: string[], options:
|
||||
}
|
||||
|
||||
return { args: positionalArgs, options: optionsObject };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { getDataDir, getMnemonicsDir, getWalletConfigPath } from "../../utils/paths.js";
|
||||
import {
|
||||
getDataDir,
|
||||
getMnemonicsDir,
|
||||
getWalletConfigPath,
|
||||
} from "../../utils/paths.js";
|
||||
import { loadMnemonic } from "../mnemonic.js";
|
||||
import { Storage } from "../../services/storage.js";
|
||||
import { COMMAND_TREE } from "./completions.js";
|
||||
@@ -56,7 +60,9 @@ async function getEngineModule() {
|
||||
*/
|
||||
function outputCompletions(items: readonly string[], prefix?: string): void {
|
||||
const filtered = prefix
|
||||
? items.filter((item) => item.toLowerCase().startsWith(prefix.toLowerCase()))
|
||||
? items.filter((item) =>
|
||||
item.toLowerCase().startsWith(prefix.toLowerCase()),
|
||||
)
|
||||
: items;
|
||||
|
||||
for (const item of filtered) {
|
||||
@@ -71,7 +77,9 @@ function outputCompletions(items: readonly string[], prefix?: string): void {
|
||||
function listMnemonics(prefix?: string): void {
|
||||
try {
|
||||
const mnemonicsDir = getMnemonicsDir();
|
||||
const files = readdirSync(mnemonicsDir).filter((f) => f.startsWith("mnemonic-"));
|
||||
const files = readdirSync(mnemonicsDir).filter((f) =>
|
||||
f.startsWith("mnemonic-"),
|
||||
);
|
||||
outputCompletions(files, prefix);
|
||||
} catch {
|
||||
// Silently fail - no completions available
|
||||
@@ -155,7 +163,13 @@ async function listTemplates(prefix?: string): Promise<void> {
|
||||
* Resolves a template by name or ID.
|
||||
*/
|
||||
async function resolveTemplate(
|
||||
engine: Awaited<ReturnType<Awaited<ReturnType<typeof getOfflineEngineModule>>["tryCreateOfflineEngine"]>>,
|
||||
engine: Awaited<
|
||||
ReturnType<
|
||||
Awaited<
|
||||
ReturnType<typeof getOfflineEngineModule>
|
||||
>["tryCreateOfflineEngine"]
|
||||
>
|
||||
>,
|
||||
templateQuery: string,
|
||||
) {
|
||||
if (!engine) return null;
|
||||
@@ -165,7 +179,9 @@ async function resolveTemplate(
|
||||
|
||||
// Try exact match on name or ID
|
||||
let template = templates.find(
|
||||
(t) => t.name === templateQuery || generateTemplateIdentifier(t) === templateQuery,
|
||||
(t) =>
|
||||
t.name === templateQuery ||
|
||||
generateTemplateIdentifier(t) === templateQuery,
|
||||
);
|
||||
|
||||
// Try partial match on name
|
||||
@@ -181,7 +197,10 @@ async function resolveTemplate(
|
||||
/**
|
||||
* Lists actions for a specific template.
|
||||
*/
|
||||
async function listActions(templateQuery: string, prefix?: string): Promise<void> {
|
||||
async function listActions(
|
||||
templateQuery: string,
|
||||
prefix?: string,
|
||||
): Promise<void> {
|
||||
const mnemonic = getCurrentMnemonic();
|
||||
if (!mnemonic) return;
|
||||
|
||||
@@ -210,7 +229,11 @@ async function listActions(templateQuery: string, prefix?: string): Promise<void
|
||||
* Lists fields (actions, transactions, outputs, etc.) for a specific template category.
|
||||
* Used for completing the 3rd argument of `template inspect <category> <template> <field>`.
|
||||
*/
|
||||
async function listFields(category: string, templateQuery: string, prefix?: string): Promise<void> {
|
||||
async function listFields(
|
||||
category: string,
|
||||
templateQuery: string,
|
||||
prefix?: string,
|
||||
): Promise<void> {
|
||||
const mnemonic = getCurrentMnemonic();
|
||||
if (!mnemonic) return;
|
||||
|
||||
@@ -300,7 +323,9 @@ async function listResources(prefix?: string): Promise<void> {
|
||||
|
||||
try {
|
||||
const utxos = await engine.listUnspentOutputsData();
|
||||
const outpoints = utxos.map((u) => `${u.outpointTransactionHash}:${u.outpointIndex}`);
|
||||
const outpoints = utxos.map(
|
||||
(u) => `${u.outpointTransactionHash}:${u.outpointIndex}`,
|
||||
);
|
||||
outputCompletions(outpoints, prefix);
|
||||
} finally {
|
||||
await engine.stop();
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
* xo-cli completions fish --install
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
appendFileSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { homedir } from "node:os";
|
||||
@@ -40,7 +45,16 @@ const MNEMONIC_SUBS = ["create", "import", "list", "expose"];
|
||||
/** Subcommands for the template command */
|
||||
const TEMPLATE_SUBS = ["import", "list", "inspect", "set-default"];
|
||||
/** Subcommands for the invitation command */
|
||||
const INVITATION_SUBS = ["create", "append", "sign", "broadcast", "requirements", "import", "inspect", "list"];
|
||||
const INVITATION_SUBS = [
|
||||
"create",
|
||||
"append",
|
||||
"sign",
|
||||
"broadcast",
|
||||
"requirements",
|
||||
"import",
|
||||
"inspect",
|
||||
"list",
|
||||
];
|
||||
/** Subcommands for the resource command */
|
||||
const RESOURCE_SUBS = ["list", "unreserve", "unreserve-all"];
|
||||
/** Subcommands for the completions command */
|
||||
@@ -57,7 +71,16 @@ export const COMMAND_TREE = {
|
||||
} as const;
|
||||
|
||||
/** Global option flags available on every command. */
|
||||
const GLOBAL_OPTIONS = ["-h", "--help", "-v", "--verbose", "-m", "--mnemonic-file", "-o", "--output"];
|
||||
const GLOBAL_OPTIONS = [
|
||||
"-h",
|
||||
"--help",
|
||||
"-v",
|
||||
"--verbose",
|
||||
"-m",
|
||||
"--mnemonic-file",
|
||||
"-o",
|
||||
"--output",
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the path to the scripts directory containing shell templates.
|
||||
@@ -92,13 +115,22 @@ function loadAndProcessTemplate(templateName: string, binName: string): string {
|
||||
content = content.replace(/\{\{OPTIONS\}\}/g, options);
|
||||
content = content.replace(/\{\{MNEMONIC_SUBS\}\}/g, MNEMONIC_SUBS.join(" "));
|
||||
content = content.replace(/\{\{TEMPLATE_SUBS\}\}/g, TEMPLATE_SUBS.join(" "));
|
||||
content = content.replace(/\{\{INVITATION_SUBS\}\}/g, INVITATION_SUBS.join(" "));
|
||||
content = content.replace(
|
||||
/\{\{INVITATION_SUBS\}\}/g,
|
||||
INVITATION_SUBS.join(" "),
|
||||
);
|
||||
content = content.replace(/\{\{RESOURCE_SUBS\}\}/g, RESOURCE_SUBS.join(" "));
|
||||
|
||||
// Fish-specific placeholders
|
||||
if (templateName.endsWith(".fish")) {
|
||||
content = content.replace(/\{\{TOP_LEVEL_COMMANDS\}\}/g, generateFishTopLevelCommands(binName));
|
||||
content = content.replace(/\{\{STATIC_SUBCOMMANDS\}\}/g, generateFishStaticSubcommands(binName));
|
||||
content = content.replace(
|
||||
/\{\{TOP_LEVEL_COMMANDS\}\}/g,
|
||||
generateFishTopLevelCommands(binName),
|
||||
);
|
||||
content = content.replace(
|
||||
/\{\{STATIC_SUBCOMMANDS\}\}/g,
|
||||
generateFishStaticSubcommands(binName),
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
@@ -110,7 +142,9 @@ function loadAndProcessTemplate(templateName: string, binName: string): string {
|
||||
function generateFishTopLevelCommands(binName: string): string {
|
||||
const lines: string[] = [];
|
||||
for (const cmd of Object.keys(COMMAND_TREE)) {
|
||||
lines.push(`complete -c ${binName} -n "__fish_use_subcommand" -a "${cmd}" -d "${cmd} command"`);
|
||||
lines.push(
|
||||
`complete -c ${binName} -n "__fish_use_subcommand" -a "${cmd}" -d "${cmd} command"`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -122,7 +156,9 @@ function generateFishStaticSubcommands(binName: string): string {
|
||||
const lines: string[] = [];
|
||||
for (const [cmd, subs] of Object.entries(COMMAND_TREE)) {
|
||||
for (const sub of subs) {
|
||||
lines.push(`complete -c ${binName} -n "__fish_seen_subcommand_from ${cmd}; and not __fish_seen_subcommand_from ${subs.join(" ")}" -a "${sub}" -d "${cmd} ${sub}"`);
|
||||
lines.push(
|
||||
`complete -c ${binName} -n "__fish_seen_subcommand_from ${cmd}; and not __fish_seen_subcommand_from ${subs.join(" ")}" -a "${sub}" -d "${cmd} ${sub}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
@@ -163,7 +199,10 @@ const generators: Record<ShellType, (binName: string) => string> = {
|
||||
/**
|
||||
* Shell config file paths and eval commands for each shell type.
|
||||
*/
|
||||
const shellConfigs: Record<ShellType, { configFile: string; evalCommand: (binName: string) => string }> = {
|
||||
const shellConfigs: Record<
|
||||
ShellType,
|
||||
{ configFile: string; evalCommand: (binName: string) => string }
|
||||
> = {
|
||||
bash: {
|
||||
configFile: join(homedir(), ".bashrc"),
|
||||
evalCommand: (binName) => `eval "$(${binName} completions bash)"`,
|
||||
@@ -199,7 +238,8 @@ function installCompletions(shell: ShellType, binName: string): boolean {
|
||||
}
|
||||
|
||||
// Append the completion line
|
||||
const newLine = existingContent.endsWith("\n") || existingContent === "" ? "" : "\n";
|
||||
const newLine =
|
||||
existingContent.endsWith("\n") || existingContent === "" ? "" : "\n";
|
||||
const completionBlock = `${newLine}\n# ${binName} shell completions\n${evalCommand}\n`;
|
||||
|
||||
appendFileSync(config.configFile, completionBlock);
|
||||
@@ -227,14 +267,26 @@ export function handleCompletionsCommand(
|
||||
console.error(`Usage: ${binName} completions <${supported}> [--install]`);
|
||||
console.error("");
|
||||
console.error("Examples:");
|
||||
console.error(` eval "$(${binName} completions bash)" # Output to stdout (add to ~/.bashrc)`);
|
||||
console.error(` eval "$(${binName} completions zsh)" # Output to stdout (add to ~/.zshrc)`);
|
||||
console.error(` ${binName} completions fish | source # Output to stdout (add to fish config)`);
|
||||
console.error(
|
||||
` eval "$(${binName} completions bash)" # Output to stdout (add to ~/.bashrc)`,
|
||||
);
|
||||
console.error(
|
||||
` eval "$(${binName} completions zsh)" # Output to stdout (add to ~/.zshrc)`,
|
||||
);
|
||||
console.error(
|
||||
` ${binName} completions fish | source # Output to stdout (add to fish config)`,
|
||||
);
|
||||
console.error("");
|
||||
console.error("Install directly to shell config:");
|
||||
console.error(` ${binName} completions bash --install # Appends to ~/.bashrc`);
|
||||
console.error(` ${binName} completions zsh --install # Appends to ~/.zshrc`);
|
||||
console.error(` ${binName} completions fish --install # Appends to ~/.config/fish/config.fish`);
|
||||
console.error(
|
||||
` ${binName} completions bash --install # Appends to ~/.bashrc`,
|
||||
);
|
||||
console.error(
|
||||
` ${binName} completions zsh --install # Appends to ~/.zshrc`,
|
||||
);
|
||||
console.error(
|
||||
` ${binName} completions fish --install # Appends to ~/.config/fish/config.fish`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
* and instead constructs the engine directly with an in-memory blockchain provider.
|
||||
*/
|
||||
|
||||
import { BlockchainMonitor, Engine, InMemoryBlockchainProvider } from "@xo-cash/engine";
|
||||
import {
|
||||
BlockchainMonitor,
|
||||
Engine,
|
||||
InMemoryBlockchainProvider,
|
||||
} from "@xo-cash/engine";
|
||||
import { createStorageAdapter, State, StorageType } from "@xo-cash/state";
|
||||
import { convertMnemonicToSeedBytes } from "@xo-cash/crypto";
|
||||
import { binToHex, hash256 } from "@bitauth/libauth";
|
||||
|
||||
@@ -2,9 +2,9 @@ import util from "node:util";
|
||||
|
||||
/**
|
||||
* Text formatting utilities for the CLI.
|
||||
*
|
||||
*
|
||||
* Uses ANSI escape codes to format text.
|
||||
*
|
||||
*
|
||||
* AI Generated links:
|
||||
* @see https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
|
||||
@@ -30,7 +30,8 @@ const HIDDEN = "\x1b[8m";
|
||||
export const hidden = (text: string) => `${HIDDEN}${text}${RESET}`;
|
||||
|
||||
const STRIKETHROUGH = "\x1b[9m";
|
||||
export const strikethrough = (text: string) => `${STRIKETHROUGH}${text}${RESET}`;
|
||||
export const strikethrough = (text: string) =>
|
||||
`${STRIKETHROUGH}${text}${RESET}`;
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
export const reset = (text: string) => `${RESET}${text}${RESET}`;
|
||||
@@ -39,6 +40,6 @@ export const formatObject = (obj: unknown) => {
|
||||
return util.inspect(obj, {
|
||||
depth: null,
|
||||
colors: true,
|
||||
compact: false
|
||||
compact: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI entry point.
|
||||
*
|
||||
*
|
||||
* TODO: Decide the best way to handle CLI arguments. We have the option of:
|
||||
* - Handling it in the `bin` folder
|
||||
* - Switch / if statements in here
|
||||
* - Dedicated command parser
|
||||
* - Separate files?
|
||||
*
|
||||
*
|
||||
* What kind of commands do we want to support?
|
||||
* Worth noting that we shouldn't need to list invitations? Maybe we will though? If we do, then we will need to reuse the storage + xo-invitations.db file. I think this is fine to do though?
|
||||
* Nah, lets use the storage + xo-invitations.db file. Will allow us to persist invitations.
|
||||
* How do we want to import invitations though? Should we just take in the ID still? Probably makes more sense to allow for reading from a file though...
|
||||
* But thats an entirely different flow to what we have already. And how would we handle writing the invitation? Do we just overwrite the file? Probably... Just take in an -o option; default to overwrite?
|
||||
*
|
||||
*
|
||||
* Commands:
|
||||
* xo-cli mnemonic create [mnemonic seed]
|
||||
* xo-cli mnemonic list
|
||||
*
|
||||
*
|
||||
* xo-cli template import <template-file>
|
||||
* xo-cli template list
|
||||
* xo-cli template set-default <template-file> <output-identifier> <role-identifier>
|
||||
@@ -27,9 +27,9 @@
|
||||
* xo-cli invitation import <invitation-file>
|
||||
* xo-cli invitation sign <invitation-file>
|
||||
* xo-cli invitation broadcast <invitation-file>
|
||||
*
|
||||
*
|
||||
* xo-cli resource list
|
||||
*
|
||||
*
|
||||
* universal Args:
|
||||
* -h --help
|
||||
* -m --mnemonic-file <mnemonic-file>
|
||||
@@ -42,15 +42,19 @@ import { AppService } from "../services/app.js";
|
||||
import { convertArgsToObject } from "./arguments.js";
|
||||
import { bold, dim, formatObject } from "./cli-utils.js";
|
||||
import { listGlobalMnemonicFiles, loadMnemonic } from "./mnemonic.js";
|
||||
import { getDataDir, getMnemonicsDir, getWalletConfigPath } from "../utils/paths.js";
|
||||
import {
|
||||
getDataDir,
|
||||
getMnemonicsDir,
|
||||
getWalletConfigPath,
|
||||
} from "../utils/paths.js";
|
||||
|
||||
import {
|
||||
import {
|
||||
type CommandDependencies,
|
||||
type CommandIO,
|
||||
type CommandPaths,
|
||||
CommandError,
|
||||
handleMnemonicCommand,
|
||||
handleTemplateCommand,
|
||||
handleMnemonicCommand,
|
||||
handleTemplateCommand,
|
||||
handleInvitationCommand,
|
||||
handleReceiveCommand,
|
||||
handleResourceCommand,
|
||||
@@ -72,7 +76,7 @@ const createCommandIO = (verbose: boolean): CommandIO => ({
|
||||
|
||||
/**
|
||||
* Main entry point.
|
||||
* We will:
|
||||
* We will:
|
||||
* - Initialize the app service?
|
||||
* - Extract the command being called
|
||||
* - Extract CLI Args (Depends on the command being called. Eww. But we can probably use Zod to validate the args in a decent way?)
|
||||
@@ -91,7 +95,7 @@ async function main(): Promise<void> {
|
||||
// Log the parsed app args
|
||||
io.verbose(`Parsed args: ${formatObject(args)}`);
|
||||
io.verbose(`Parsed options: ${formatObject(options)}`);
|
||||
|
||||
|
||||
// Handle the command
|
||||
const command = args[0];
|
||||
io.verbose(`Command: ${command}`);
|
||||
@@ -138,8 +142,12 @@ async function main(): Promise<void> {
|
||||
}
|
||||
if (!mnemonicFile) {
|
||||
io.err("No mnemonic file provided");
|
||||
io.out(`You can create a mnemonic file with the following command: xo-cli mnemonic create <mnemonic-seed> or use one of the following files: \n${listGlobalMnemonicFiles().join("\n")}`);
|
||||
io.out(`\nTip: pass -m <file> once and it will be remembered in ${paths.walletConfigPath}`);
|
||||
io.out(
|
||||
`You can create a mnemonic file with the following command: xo-cli mnemonic create <mnemonic-seed> or use one of the following files: \n${listGlobalMnemonicFiles().join("\n")}`,
|
||||
);
|
||||
io.out(
|
||||
`\nTip: pass -m <file> once and it will be remembered in ${paths.walletConfigPath}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -148,7 +156,7 @@ async function main(): Promise<void> {
|
||||
|
||||
const mnemonic = loadMnemonic(paths.mnemonicsDir, mnemonicFile);
|
||||
io.verbose(`Loaded mnemonic from file: ${mnemonicFile} ${mnemonic}`);
|
||||
|
||||
|
||||
// Create an App instance
|
||||
io.verbose("Creating app instance...");
|
||||
const app = await AppService.create(mnemonic, {
|
||||
@@ -158,7 +166,8 @@ async function main(): Promise<void> {
|
||||
databaseFilename: options["databaseFilename"] ?? "xo-wallet.db",
|
||||
},
|
||||
invitationStoragePath:
|
||||
options["invitationStoragePath"] ?? join(paths.dataDir, "xo-invitations.db"),
|
||||
options["invitationStoragePath"] ??
|
||||
join(paths.dataDir, "xo-invitations.db"),
|
||||
});
|
||||
io.verbose("App instance created");
|
||||
|
||||
@@ -179,23 +188,42 @@ async function main(): Promise<void> {
|
||||
let result: unknown;
|
||||
switch (command) {
|
||||
case "template":
|
||||
result = await handleTemplateCommand(commandDependencies, subArgs, options);
|
||||
result = await handleTemplateCommand(
|
||||
commandDependencies,
|
||||
subArgs,
|
||||
options,
|
||||
);
|
||||
break;
|
||||
case "invitation":
|
||||
result = await handleInvitationCommand(commandDependencies, subArgs, options);
|
||||
result = await handleInvitationCommand(
|
||||
commandDependencies,
|
||||
subArgs,
|
||||
options,
|
||||
);
|
||||
break;
|
||||
case "receive":
|
||||
result = await handleReceiveCommand(commandDependencies, subArgs, options);
|
||||
result = await handleReceiveCommand(
|
||||
commandDependencies,
|
||||
subArgs,
|
||||
options,
|
||||
);
|
||||
break;
|
||||
case "resource":
|
||||
result = await handleResourceCommand(commandDependencies, subArgs, options);
|
||||
result = await handleResourceCommand(
|
||||
commandDependencies,
|
||||
subArgs,
|
||||
options,
|
||||
);
|
||||
break;
|
||||
case "help":
|
||||
result = await handleHelpCommand(commandDependencies, subArgs, options);
|
||||
break;
|
||||
default:
|
||||
io.err(`Unknown command: ${command}`);
|
||||
throw new CommandError("cli.command.unknown", `Unknown command: ${command}`);
|
||||
throw new CommandError(
|
||||
"cli.command.unknown",
|
||||
`Unknown command: ${command}`,
|
||||
);
|
||||
}
|
||||
|
||||
// console.log(result);
|
||||
@@ -217,7 +245,7 @@ const handleHelpCommand = async (
|
||||
_options: Record<string, string>,
|
||||
): Promise<Record<string, never>> => {
|
||||
deps.io.out(
|
||||
`${bold("XO-CLI Help:")}
|
||||
`${bold("XO-CLI Help:")}
|
||||
|
||||
${bold("Usage:")} xo-cli <command> [options]
|
||||
|
||||
@@ -232,7 +260,7 @@ Commands:
|
||||
Options:
|
||||
-h, --help ${dim("Show this help message")}
|
||||
-m, --mnemonic-file <mnemonic-file> ${dim("Use a specific mnemonic file")}
|
||||
-v, --verbose ${dim("Show verbose output")}`
|
||||
-v, --verbose ${dim("Show verbose output")}`,
|
||||
);
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -28,9 +28,11 @@ export const createMnemonicFile = (
|
||||
|
||||
let fileName = outputFilename;
|
||||
if (!fileName) {
|
||||
const firstWord = mnemonic.split(' ')[0]?.toLowerCase();
|
||||
const firstWord = mnemonic.split(" ")[0]?.toLowerCase();
|
||||
if (!firstWord) {
|
||||
throw new Error("Failed to create mnemonic file: Unable to extract first word from the mnemonic");
|
||||
throw new Error(
|
||||
"Failed to create mnemonic file: Unable to extract first word from the mnemonic",
|
||||
);
|
||||
}
|
||||
fileName = `mnemonic-${firstWord}`;
|
||||
}
|
||||
@@ -80,9 +82,14 @@ export const resolveMnemonicFilePath = (
|
||||
* @param mnemonicFile - The filename of the mnemonic file
|
||||
* @returns The mnemonic seed
|
||||
*/
|
||||
export const loadMnemonic = (mnemonicsDir: string, mnemonicFile: string): string => {
|
||||
export const loadMnemonic = (
|
||||
mnemonicsDir: string,
|
||||
mnemonicFile: string,
|
||||
): string => {
|
||||
const resolvedPath = resolveMnemonicFilePath(mnemonicsDir, mnemonicFile);
|
||||
const mnemonicUrl = BCHMnemonicURL.fromURL(readFileSync(resolvedPath, "utf8"));
|
||||
const mnemonicUrl = BCHMnemonicURL.fromURL(
|
||||
readFileSync(resolvedPath, "utf8"),
|
||||
);
|
||||
const { entropy } = mnemonicUrl.toObject();
|
||||
|
||||
const mnemonic = encodeBip39Mnemonic(entropy);
|
||||
|
||||
@@ -9,14 +9,17 @@ import { generateTemplateIdentifier } from "@xo-cash/engine";
|
||||
* First, check the id of every template
|
||||
* Then, check the name of every template. If multiple names match, throw an error.
|
||||
* If no match is found, throw an error.
|
||||
*
|
||||
*
|
||||
* @param deps - The command dependencies.
|
||||
* @param query - The id or name of the template to resolve.
|
||||
* @returns The template object.
|
||||
* @throws CommandError if no template is found.
|
||||
* @throws CommandError if multiple templates are found.
|
||||
*/
|
||||
export const resolveTemplate = async (deps: CommandDependencies, query: string): Promise<XOTemplate> => {
|
||||
export const resolveTemplate = async (
|
||||
deps: CommandDependencies,
|
||||
query: string,
|
||||
): Promise<XOTemplate> => {
|
||||
const templates = await deps.app.engine.listImportedTemplates();
|
||||
|
||||
const matches = new Set<XOTemplate>();
|
||||
@@ -36,7 +39,12 @@ export const resolveTemplate = async (deps: CommandDependencies, query: string):
|
||||
if (matches.size > 1) {
|
||||
throw new CommandError(
|
||||
"template.resolve.multiple_matches",
|
||||
`Multiple templates found for "${query}": ${Array.from(matches).map(template => `${template.name} (${generateTemplateIdentifier(template)})`).join(", ")}`,
|
||||
`Multiple templates found for "${query}": ${Array.from(matches)
|
||||
.map(
|
||||
(template) =>
|
||||
`${template.name} (${generateTemplateIdentifier(template)})`,
|
||||
)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,5 +52,8 @@ export const resolveTemplate = async (deps: CommandDependencies, query: string):
|
||||
return matches.values().next().value!;
|
||||
}
|
||||
|
||||
throw new CommandError("template.resolve.not_found", `Template not found: ${query}`);
|
||||
}
|
||||
throw new CommandError(
|
||||
"template.resolve.not_found",
|
||||
`Template not found: ${query}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -74,8 +74,20 @@ export class AppService extends EventEmitter<AppEventMap> {
|
||||
// Import the default P2PKH template
|
||||
const { templateIdentifier } = await engine.importTemplate(p2pkhTemplate);
|
||||
|
||||
engine.subscribeToLockingBytecodesForTemplate(templateIdentifier).catch(err => console.error(`Error subscribing to locking bytecodes for template ${templateIdentifier}: ${err}`));
|
||||
engine.updateUnspentOutputsForTemplate(templateIdentifier).catch(err => console.error(`Error updating unspent outputs for template ${templateIdentifier}: ${err}`));
|
||||
engine
|
||||
.subscribeToLockingBytecodesForTemplate(templateIdentifier)
|
||||
.catch((err) =>
|
||||
console.error(
|
||||
`Error subscribing to locking bytecodes for template ${templateIdentifier}: ${err}`,
|
||||
),
|
||||
);
|
||||
engine
|
||||
.updateUnspentOutputsForTemplate(templateIdentifier)
|
||||
.catch((err) =>
|
||||
console.error(
|
||||
`Error updating unspent outputs for template ${templateIdentifier}: ${err}`,
|
||||
),
|
||||
);
|
||||
|
||||
// Set default locking parameters for P2PKH
|
||||
// To my knowledge, this doesnt generate any lockscript, so discovery of funds will not work automatically.
|
||||
@@ -135,10 +147,10 @@ export class AppService extends EventEmitter<AppEventMap> {
|
||||
|
||||
// Create the invitation
|
||||
const invitationInstance = await Invitation.create(invitation, deps);
|
||||
|
||||
|
||||
// Add the invitation to the invitations array
|
||||
await this.addInvitation(invitationInstance);
|
||||
|
||||
|
||||
return invitationInstance;
|
||||
}
|
||||
|
||||
@@ -209,10 +221,7 @@ export class AppService extends EventEmitter<AppEventMap> {
|
||||
if (!trackedInvitation || !cleanup) return;
|
||||
|
||||
trackedInvitation.off("invitation-updated", cleanup.onUpdated);
|
||||
trackedInvitation.off(
|
||||
"invitation-status-changed",
|
||||
cleanup.onStatusChanged,
|
||||
);
|
||||
trackedInvitation.off("invitation-status-changed", cleanup.onStatusChanged);
|
||||
this.invitationEventCleanup.delete(invitationIdentifier);
|
||||
}
|
||||
|
||||
@@ -259,7 +268,9 @@ export class AppService extends EventEmitter<AppEventMap> {
|
||||
|
||||
await Promise.all(
|
||||
invitations.map(async ({ key }) => {
|
||||
await this.createInvitation(key).catch(err => console.error(`Error creating invitation ${key}: ${err}`));
|
||||
await this.createInvitation(key).catch((err) =>
|
||||
console.error(`Error creating invitation ${key}: ${err}`),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { compileCashAssemblyString } from "@xo-cash/engine";
|
||||
export type InvitationEventMap = {
|
||||
"invitation-updated": XOInvitation;
|
||||
"invitation-status-changed": string;
|
||||
"error": Error;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type InvitationDependencies = {
|
||||
@@ -153,25 +153,25 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
this.syncServer.connect(),
|
||||
this.syncServer.getInvitation(this.data.invitationIdentifier),
|
||||
]);
|
||||
|
||||
|
||||
// There is a chance we get SSE messages before the invitation is returned, so we want to combine any commits
|
||||
const sseCommits = this.data.commits;
|
||||
|
||||
|
||||
// Merge the commits
|
||||
const combinedCommits = this.mergeCommits(
|
||||
sseCommits,
|
||||
invitation?.commits ?? [],
|
||||
);
|
||||
|
||||
|
||||
// Set the invitation data with the combined commits
|
||||
this.data = { ...this.data, ...invitation, commits: combinedCommits };
|
||||
|
||||
|
||||
// Store the invitation in the storage
|
||||
await this.storage.set(this.data.invitationIdentifier, this.data);
|
||||
|
||||
|
||||
// Publish the invitation to the sync server
|
||||
this.publishInvitation(this.data);
|
||||
|
||||
|
||||
// Compute and emit initial status
|
||||
await this.updateStatus();
|
||||
} catch (err) {
|
||||
@@ -215,7 +215,9 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
/**
|
||||
* Publish the invitation to the sync server
|
||||
*/
|
||||
private async publishInvitation(invitation: XOInvitation = this.data): Promise<void> {
|
||||
private async publishInvitation(
|
||||
invitation: XOInvitation = this.data,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.syncServer.publishInvitation(invitation);
|
||||
} catch (err) {
|
||||
|
||||
@@ -90,7 +90,7 @@ export class BCHMnemonicURL {
|
||||
static fromSeed(seed: string): BCHMnemonicURL {
|
||||
// Encode the seed to a Uint8Array
|
||||
const entropy = decodeBip39Mnemonic(seed);
|
||||
|
||||
|
||||
// If the decode failed, throw an error
|
||||
if (typeof entropy === "string") {
|
||||
throw new Error(`Invalid seed: ${entropy}`);
|
||||
|
||||
Reference in New Issue
Block a user