Add documentation to the commands for the CLI
This commit is contained in:
@@ -23,7 +23,6 @@ import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
appendFileSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
# bash completion for {{BIN_NAME}}
|
||||
# Add to ~/.bashrc: eval "$({{BIN_NAME}} completions bash)"
|
||||
# ------------------------------------------------------------------------------
|
||||
# Bash completion template for {{BIN_NAME}}
|
||||
# ------------------------------------------------------------------------------
|
||||
# Installation:
|
||||
# eval "$({{BIN_NAME}} completions bash)"
|
||||
#
|
||||
# This file is generated from a template. Placeholders (for example `{{OPTIONS}}`)
|
||||
# are replaced at build/runtime with concrete command data from the CLI.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Find xo-complete in the same directory as xo-cli
|
||||
# Prefer a globally-installed helper, but fall back to a helper co-located with
|
||||
# the CLI binary. This lets completions work in both "installed via PATH" and
|
||||
# "single extracted directory" workflows.
|
||||
__xo_complete_bin=""
|
||||
if command -v xo-complete &>/dev/null; then
|
||||
__xo_complete_bin="xo-complete"
|
||||
@@ -9,16 +18,28 @@ elif command -v {{BIN_NAME}} &>/dev/null; then
|
||||
__xo_complete_bin="$(dirname "$(command -v {{BIN_NAME}})")/xo-complete"
|
||||
fi
|
||||
|
||||
# Wrapper to call xo-complete helper
|
||||
# @description
|
||||
# Calls the dynamic completion helper and suppresses helper stderr so the shell
|
||||
# completion menu stays clean even when the helper is unavailable or errors.
|
||||
# @param "$@" Arguments forwarded to xo-complete.
|
||||
__xo_complete() {
|
||||
[[ -n "${__xo_complete_bin}" ]] && "${__xo_complete_bin}" "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
# @description
|
||||
# Main completion dispatcher invoked by bash's `complete -F`.
|
||||
# It determines context (command/subcommand/argument position) and then mixes:
|
||||
# - static completions (known command words)
|
||||
# - dynamic completions (resolved by xo-complete)
|
||||
# - filesystem completions (when a subcommand expects file paths)
|
||||
_{{FUNC_NAME}}_completions() {
|
||||
local cur prev words cword
|
||||
# Populates `cur`, `prev`, `words`, and `cword`.
|
||||
# `_init_completion` is provided by bash-completion.
|
||||
_init_completion || return
|
||||
|
||||
# Handle -m/--mnemonic-file argument (previous word was -m)
|
||||
# If the previous token is `-m/--mnemonic-file`, this argument expects a
|
||||
# mnemonic file alias/path. Ask the helper for mnemonic suggestions.
|
||||
if [[ "${prev}" == "-m" || "${prev}" == "--mnemonic-file" ]]; then
|
||||
local mnemonics
|
||||
mnemonics=$(__xo_complete mnemonics "${cur}")
|
||||
@@ -30,13 +51,14 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# If the current word starts with "-", offer option flags
|
||||
# Option context: show global options when the current token starts with `-`.
|
||||
if [[ "${cur}" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "{{OPTIONS}}" -- "${cur}"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Find the command and subcommand positions
|
||||
# Parse command/subcommand from non-option tokens before the current cursor.
|
||||
# We track indices so argument-position logic can be computed later.
|
||||
local cmd="" subcmd="" cmd_idx=0 subcmd_idx=0
|
||||
for ((i=1; i < cword; i++)); do
|
||||
if [[ "${words[i]}" != -* ]]; then
|
||||
@@ -51,13 +73,13 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
done
|
||||
|
||||
# No command yet — offer the top-level commands
|
||||
# No command selected yet: complete top-level commands.
|
||||
if [[ -z "${cmd}" ]]; then
|
||||
COMPREPLY=($(compgen -W "{{COMMANDS}}" -- "${cur}"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle each command's completion
|
||||
# Command-specific completion rules.
|
||||
case "${cmd}" in
|
||||
mnemonic)
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
@@ -69,7 +91,9 @@ _{{FUNC_NAME}}_completions() {
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
COMPREPLY=($(compgen -W "{{TEMPLATE_SUBS}}" -- "${cur}"))
|
||||
elif [[ "${subcmd}" == "list" || "${subcmd}" == "inspect" ]]; then
|
||||
# template list/inspect <category> <template> [field] - category first, then template, then field
|
||||
# template list/inspect <category> <template> [field]
|
||||
# Position is computed relative to the subcommand token:
|
||||
# 1 => category, 2 => template, 3 => field (inspect only)
|
||||
local pos=$((cword - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
COMPREPLY=($(compgen -W "action transaction output lockingscript variable" -- "${cur}"))
|
||||
@@ -82,7 +106,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
done <<< "${templates}"
|
||||
fi
|
||||
elif [[ $pos -eq 3 && "${subcmd}" == "inspect" ]]; then
|
||||
# Get the category and template from previous args
|
||||
# Field names depend on both selected category and template.
|
||||
local category="${words[subcmd_idx + 1]}"
|
||||
local template_arg="${words[subcmd_idx + 2]}"
|
||||
local fields
|
||||
@@ -94,7 +118,8 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
fi
|
||||
elif [[ "${subcmd}" == "set-default" ]]; then
|
||||
# template set-default <template> <output> <role> - template first
|
||||
# template set-default <template> <output> <role>
|
||||
# We only complete the first positional argument (template) here.
|
||||
local pos=$((cword - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local templates
|
||||
@@ -114,7 +139,8 @@ _{{FUNC_NAME}}_completions() {
|
||||
else
|
||||
case "${subcmd}" in
|
||||
create)
|
||||
# invitation create <template> <action> - offer templates then actions
|
||||
# invitation create <template> <action>
|
||||
# The available actions depend on the selected template.
|
||||
local pos=$((cword - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local templates
|
||||
@@ -136,7 +162,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
;;
|
||||
append|sign|broadcast|requirements|inspect)
|
||||
# These take an invitation ID
|
||||
# These subcommands expect an invitation identifier as first arg.
|
||||
local pos=$((cword - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local invitations
|
||||
@@ -149,7 +175,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
;;
|
||||
import)
|
||||
# import takes a file path - use default file completion
|
||||
# File import path: delegate to bash's built-in file completion.
|
||||
COMPREPLY=($(compgen -f -- "${cur}"))
|
||||
;;
|
||||
esac
|
||||
@@ -160,7 +186,8 @@ _{{FUNC_NAME}}_completions() {
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
COMPREPLY=($(compgen -W "{{RESOURCE_SUBS}}" -- "${cur}"))
|
||||
elif [[ "${subcmd}" == "unreserve" ]]; then
|
||||
# resource unreserve <txhash:vout> - offer resources
|
||||
# resource unreserve <txhash:vout>
|
||||
# Suggest known reserved outpoints from the helper.
|
||||
local pos=$((cword - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local resources
|
||||
@@ -175,7 +202,8 @@ _{{FUNC_NAME}}_completions() {
|
||||
;;
|
||||
|
||||
receive)
|
||||
# receive <template> [output] - offer templates
|
||||
# receive <template> [output]
|
||||
# Template is the first positional argument after `receive`.
|
||||
local pos=$((cword - cmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local templates
|
||||
@@ -189,6 +217,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
;;
|
||||
|
||||
completions)
|
||||
# Shell target for generating completion scripts.
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
COMPREPLY=($(compgen -W "bash zsh fish" -- "${cur}"))
|
||||
fi
|
||||
@@ -196,4 +225,5 @@ _{{FUNC_NAME}}_completions() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Register the completion function for the CLI binary.
|
||||
complete -F _{{FUNC_NAME}}_completions {{BIN_NAME}}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
# fish completion for {{BIN_NAME}}
|
||||
# Add to fish config: {{BIN_NAME}} completions fish | source
|
||||
# ------------------------------------------------------------------------------
|
||||
# Fish completion template for {{BIN_NAME}}
|
||||
# ------------------------------------------------------------------------------
|
||||
# Installation:
|
||||
# {{BIN_NAME}} completions fish | source
|
||||
#
|
||||
# This file is generated from a template. Placeholders (for example
|
||||
# `{{TOP_LEVEL_COMMANDS}}`) are replaced with concrete completion definitions.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Disable file completions by default
|
||||
# Fish offers file completion by default. Disable that globally first so command
|
||||
# words are preferred, then selectively re-enable `-F` where paths are expected.
|
||||
complete -c {{BIN_NAME}} -f
|
||||
|
||||
# Helper function to get dynamic completions
|
||||
# Finds xo-complete in the same directory as {{BIN_NAME}}
|
||||
# @description
|
||||
# Resolves and calls `xo-complete` for dynamic values (templates, invitations,
|
||||
# fields, etc.). We first try PATH, then a helper next to `{{BIN_NAME}}`.
|
||||
# @param $argv Arguments forwarded directly to xo-complete.
|
||||
function __{{FUNC_NAME}}_complete_dynamic
|
||||
set -l xo_complete_bin ""
|
||||
if command -q xo-complete
|
||||
@@ -18,7 +28,7 @@ function __{{FUNC_NAME}}_complete_dynamic
|
||||
end
|
||||
end
|
||||
|
||||
# Global options
|
||||
# Global option flags available across top-level command contexts.
|
||||
complete -c {{BIN_NAME}} -s h -d "Show help"
|
||||
complete -c {{BIN_NAME}} -l help -d "Show help"
|
||||
complete -c {{BIN_NAME}} -s v -d "Verbose output"
|
||||
@@ -26,45 +36,61 @@ complete -c {{BIN_NAME}} -l verbose -d "Verbose output"
|
||||
complete -c {{BIN_NAME}} -s o -d "Output file"
|
||||
complete -c {{BIN_NAME}} -l output -d "Output file"
|
||||
|
||||
# Dynamic mnemonic file completion for -m
|
||||
# Dynamic completion for `-m/--mnemonic-file`.
|
||||
complete -c {{BIN_NAME}} -s m -l mnemonic-file -xa '(__{{FUNC_NAME}}_complete_dynamic mnemonics)'
|
||||
|
||||
# Top-level commands
|
||||
# Top-level command registrations inserted by template expansion.
|
||||
{{TOP_LEVEL_COMMANDS}}
|
||||
|
||||
# Static sub-commands
|
||||
# Static subcommand registrations inserted by template expansion.
|
||||
{{STATIC_SUBCOMMANDS}}
|
||||
|
||||
# Dynamic completions
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dynamic completions by command/subcommand.
|
||||
#
|
||||
# Fish condition notes:
|
||||
# - `__fish_seen_subcommand_from <name>` checks whether `<name>` exists in the
|
||||
# current tokenized command line.
|
||||
# - `count (commandline -opc)` returns how many tokens were entered.
|
||||
# We use this to infer positional argument index.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# invitation create: template names
|
||||
# invitation create <template> <action>
|
||||
# Position 3 => template argument.
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from create; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic templates)'
|
||||
|
||||
# invitation create: action names (2nd arg)
|
||||
# invitation create <template> <action>
|
||||
# Position 4 => action argument, filtered by selected template token.
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from create; and test (count (commandline -opc)) -eq 4" -xa '(__{{FUNC_NAME}}_complete_dynamic actions (commandline -opc)[4])'
|
||||
|
||||
# invitation append/sign/broadcast/requirements/inspect: invitation IDs
|
||||
# invitation append/sign/broadcast/requirements/inspect <invitation-id>
|
||||
# Position 3 => invitation identifier.
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from append; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from sign; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from broadcast; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from requirements; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from inspect; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic invitations)'
|
||||
|
||||
# invitation import: file completion
|
||||
# invitation import <path>
|
||||
# Re-enable default filesystem completion for path argument.
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from invitation; and __fish_seen_subcommand_from import" -F
|
||||
|
||||
# template list/inspect: category first (pos 3), then template (pos 4), then field (pos 5 for inspect)
|
||||
# template list/inspect <category> <template> [field]
|
||||
# Position 3 => category, 4 => template, 5 => field (inspect only).
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from template; and __fish_seen_subcommand_from list; and test (count (commandline -opc)) -eq 3" -xa 'action transaction output lockingscript variable'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from template; and __fish_seen_subcommand_from list; and test (count (commandline -opc)) -eq 4" -xa '(__{{FUNC_NAME}}_complete_dynamic templates)'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from template; and __fish_seen_subcommand_from inspect; and test (count (commandline -opc)) -eq 3" -xa 'action transaction output lockingscript variable'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from template; and __fish_seen_subcommand_from inspect; and test (count (commandline -opc)) -eq 4" -xa '(__{{FUNC_NAME}}_complete_dynamic templates)'
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from template; and __fish_seen_subcommand_from inspect; and test (count (commandline -opc)) -eq 5" -xa '(__{{FUNC_NAME}}_complete_dynamic fields (commandline -opc)[4] (commandline -opc)[5])'
|
||||
|
||||
# template set-default: template first
|
||||
# template set-default <template> <output> <role>
|
||||
# Position 3 => template argument.
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from template; and __fish_seen_subcommand_from set-default; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic templates)'
|
||||
|
||||
# resource unreserve: UTXO outpoints
|
||||
# resource unreserve <txhash:vout>
|
||||
# Position 3 => outpoint to unreserve.
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from resource; and __fish_seen_subcommand_from unreserve; and test (count (commandline -opc)) -eq 3" -xa '(__{{FUNC_NAME}}_complete_dynamic resources)'
|
||||
|
||||
# receive: template names
|
||||
# receive <template> [output]
|
||||
# Position 2 => template argument.
|
||||
complete -c {{BIN_NAME}} -n "__fish_seen_subcommand_from receive; and test (count (commandline -opc)) -eq 2" -xa '(__{{FUNC_NAME}}_complete_dynamic templates)'
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
# zsh completion for {{BIN_NAME}}
|
||||
# Add to ~/.zshrc: eval "$({{BIN_NAME}} completions zsh)"
|
||||
# ------------------------------------------------------------------------------
|
||||
# Zsh completion template for {{BIN_NAME}}
|
||||
# ------------------------------------------------------------------------------
|
||||
# Installation:
|
||||
# eval "$({{BIN_NAME}} completions zsh)"
|
||||
#
|
||||
# This file is generated from a template. Placeholders (for example
|
||||
# `{{MNEMONIC_SUBS}}`) are replaced with concrete command values.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Find xo-complete in the same directory as xo-cli
|
||||
# Prefer a helper on PATH; otherwise fall back to helper next to the CLI binary.
|
||||
# This keeps dynamic completion functional in both installed and portable layouts.
|
||||
__xo_complete_bin=""
|
||||
if (( $+commands[xo-complete] )); then
|
||||
__xo_complete_bin="xo-complete"
|
||||
@@ -9,16 +17,25 @@ elif (( $+commands[{{BIN_NAME}}] )); then
|
||||
__xo_complete_bin="${commands[{{BIN_NAME}}]:h}/xo-complete"
|
||||
fi
|
||||
|
||||
# Wrapper to call xo-complete helper
|
||||
# @description
|
||||
# Calls the dynamic helper while silencing helper stderr to avoid noisy
|
||||
# completion menus if helper lookup fails.
|
||||
# @param "$@" Arguments forwarded to xo-complete.
|
||||
__xo_complete() {
|
||||
[[ -n "${__xo_complete_bin}" ]] && "${__xo_complete_bin}" "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
# @description
|
||||
# Main zsh completion dispatcher registered via `compdef`.
|
||||
# It resolves command context from `$words`/`$CURRENT` and serves:
|
||||
# - static command words via `compadd`
|
||||
# - dynamic values from `xo-complete`
|
||||
# - filesystem completions where file paths are expected
|
||||
_{{FUNC_NAME}}_completions() {
|
||||
local -a commands
|
||||
commands=({{COMMANDS}})
|
||||
|
||||
# Handle -m/--mnemonic-file argument (previous word was -m)
|
||||
# If previous token is `-m/--mnemonic-file`, complete mnemonic sources.
|
||||
if [[ "${words[CURRENT-1]}" == "-m" || "${words[CURRENT-1]}" == "--mnemonic-file" ]]; then
|
||||
local mnemonics
|
||||
mnemonics=("${(@f)$(__xo_complete mnemonics "${words[CURRENT]}")}")
|
||||
@@ -28,13 +45,14 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# If typing an option flag, complete options
|
||||
# Option context: if current token starts with `-`, complete known options.
|
||||
if [[ "${words[${CURRENT}]}" == -* ]]; then
|
||||
compadd -- {{OPTIONS}}
|
||||
return
|
||||
fi
|
||||
|
||||
# Find the command and subcommand
|
||||
# Find first and second non-option tokens before the cursor.
|
||||
# `cmd_idx` and `subcmd_idx` are used for positional argument calculations.
|
||||
local cmd="" subcmd="" cmd_idx=0 subcmd_idx=0
|
||||
for ((i=2; i < CURRENT; i++)); do
|
||||
if [[ "${words[i]}" != -* ]]; then
|
||||
@@ -49,13 +67,13 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
done
|
||||
|
||||
# No command yet — offer top-level commands
|
||||
# No command token yet: offer top-level commands.
|
||||
if [[ -z "${cmd}" ]]; then
|
||||
compadd -- ${commands[@]}
|
||||
return
|
||||
fi
|
||||
|
||||
# Handle each command's completion
|
||||
# Command-specific completion behavior.
|
||||
case "${cmd}" in
|
||||
mnemonic)
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
@@ -67,7 +85,9 @@ _{{FUNC_NAME}}_completions() {
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
compadd -- {{TEMPLATE_SUBS}}
|
||||
elif [[ "${subcmd}" == "list" || "${subcmd}" == "inspect" ]]; then
|
||||
# template list/inspect <category> <template> [field] - category first, then template, then field
|
||||
# template list/inspect <category> <template> [field]
|
||||
# Relative positions from subcommand:
|
||||
# 1 => category, 2 => template, 3 => field (inspect only)
|
||||
local pos=$((CURRENT - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
compadd -- action transaction output lockingscript variable
|
||||
@@ -78,7 +98,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
compadd -- "${templates[@]}"
|
||||
fi
|
||||
elif [[ $pos -eq 3 && "${subcmd}" == "inspect" ]]; then
|
||||
# Get the category and template from previous args
|
||||
# Field suggestions depend on selected category and template.
|
||||
local category="${words[subcmd_idx + 1]}"
|
||||
local template_arg="${words[subcmd_idx + 2]}"
|
||||
local fields
|
||||
@@ -88,7 +108,8 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
fi
|
||||
elif [[ "${subcmd}" == "set-default" ]]; then
|
||||
# template set-default <template> <output> <role> - template first
|
||||
# template set-default <template> <output> <role>
|
||||
# First positional argument is template name.
|
||||
local pos=$((CURRENT - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local templates
|
||||
@@ -106,6 +127,8 @@ _{{FUNC_NAME}}_completions() {
|
||||
else
|
||||
case "${subcmd}" in
|
||||
create)
|
||||
# invitation create <template> <action>
|
||||
# Action list is template-specific.
|
||||
local pos=$((CURRENT - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local templates
|
||||
@@ -123,6 +146,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
;;
|
||||
append|sign|broadcast|requirements|inspect)
|
||||
# These subcommands take invitation ID as first argument.
|
||||
local pos=$((CURRENT - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local invitations
|
||||
@@ -133,6 +157,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
;;
|
||||
import)
|
||||
# invitation import <path>: delegate to zsh file completion.
|
||||
_files
|
||||
;;
|
||||
esac
|
||||
@@ -143,6 +168,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
compadd -- {{RESOURCE_SUBS}}
|
||||
elif [[ "${subcmd}" == "unreserve" ]]; then
|
||||
# resource unreserve <txhash:vout>
|
||||
local pos=$((CURRENT - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local resources
|
||||
@@ -155,6 +181,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
;;
|
||||
|
||||
receive)
|
||||
# receive <template> [output]
|
||||
local pos=$((CURRENT - cmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
local templates
|
||||
@@ -166,6 +193,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
;;
|
||||
|
||||
completions)
|
||||
# Shell target for completion generation.
|
||||
if [[ -z "${subcmd}" ]]; then
|
||||
compadd -- bash zsh fish
|
||||
fi
|
||||
@@ -173,4 +201,5 @@ _{{FUNC_NAME}}_completions() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Register completion function for the executable name.
|
||||
compdef _{{FUNC_NAME}}_completions {{BIN_NAME}}
|
||||
|
||||
@@ -42,6 +42,7 @@ function parseVariablesFromOptions(
|
||||
): { variableIdentifier: string; value: string; roleIdentifier?: string }[] {
|
||||
const roleIdentifier = options["role"];
|
||||
|
||||
// Parse the variables from the options by checking if its starts with "var"
|
||||
return Object.entries(options)
|
||||
.filter(([key]) => key.startsWith("var"))
|
||||
.map(([key, value]) => ({
|
||||
@@ -81,10 +82,12 @@ async function buildAppendParams(
|
||||
const suitableResources = await invitation.findSuitableResources();
|
||||
const selectable = mapUnspentOutputsToSelectable(suitableResources);
|
||||
|
||||
// Get the required sats out with the default fee
|
||||
const requiredWithFee =
|
||||
(await invitation.getSatsOut().catch(() => 0n)) + DEFAULT_FEE;
|
||||
autoSelectGreedyUtxos(selectable, requiredWithFee);
|
||||
|
||||
// Get the inputs from the selectable UTXOs
|
||||
inputs = selectable
|
||||
.filter((u) => u.selected)
|
||||
.map((u) => ({
|
||||
@@ -92,12 +95,15 @@ async function buildAppendParams(
|
||||
outpointIndex: u.outpointIndex,
|
||||
}));
|
||||
|
||||
// If no inputs are found, print a message and return null
|
||||
if (inputs.length === 0) {
|
||||
deps.io.err("No suitable UTXOs found for auto-input selection.");
|
||||
return null;
|
||||
}
|
||||
deps.io.verbose(`Auto-selected ${inputs.length} input(s)`);
|
||||
} else if (options["addInput"]) {
|
||||
}
|
||||
// If the add input option is provided, parse the inputs from the options
|
||||
else if (options["addInput"]) {
|
||||
inputs = options["addInput"].split(",").map((entry) => {
|
||||
const separatorIndex = entry.lastIndexOf(":");
|
||||
if (separatorIndex === -1) {
|
||||
@@ -105,8 +111,12 @@ async function buildAppendParams(
|
||||
`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the tx hash and vout from the entry
|
||||
const txHash = entry.substring(0, separatorIndex);
|
||||
const vout = parseInt(entry.substring(separatorIndex + 1), 10);
|
||||
|
||||
// If the tx hash or vout is not a string or isNaN, print a message and throw an error
|
||||
if (!txHash || isNaN(vout)) {
|
||||
throw new Error(
|
||||
`Invalid input format "${entry}". Expected <txhash>:<vout> (e.g. abc123:0)`,
|
||||
@@ -161,10 +171,12 @@ async function buildAppendParams(
|
||||
}
|
||||
}
|
||||
|
||||
// Get the template from the engine
|
||||
const template = await deps.app.engine.getTemplate(
|
||||
invitation.data.templateIdentifier,
|
||||
);
|
||||
|
||||
// Get the outputs from the template
|
||||
const outputs: any[] = await Promise.all(
|
||||
outputIdentifiers.map(async (outputId) => {
|
||||
// Try variable-based resolution first (e.g. sendSatoshis → recipientLockingscript)
|
||||
@@ -205,28 +217,37 @@ async function buildAppendParams(
|
||||
]),
|
||||
);
|
||||
|
||||
// Sum the total input sats
|
||||
let totalInputSats = 0n;
|
||||
// Iterate through the inputs and sum the valueSatoshis
|
||||
for (const input of inputs) {
|
||||
// Get the tx hash hex
|
||||
const txHashHex = binToHex(input.outpointTransactionHash);
|
||||
// Get the utxo from the utxo map
|
||||
const utxo = utxoMap.get(`${txHashHex}:${input.outpointIndex}`);
|
||||
if (!utxo) {
|
||||
// If the utxo is not found, print a message and return null
|
||||
deps.io.err(
|
||||
`UTXO not found: ${txHashHex}:${input.outpointIndex}. Make sure it exists in your wallet.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// Sum the valueSatoshis
|
||||
totalInputSats += BigInt(utxo.valueSatoshis);
|
||||
}
|
||||
deps.io.verbose(`Total input value: ${totalInputSats} satoshis`);
|
||||
|
||||
// Get the required sats out
|
||||
const requiredSats = await invitation.getSatsOut();
|
||||
deps.io.verbose(`Required output value: ${requiredSats} satoshis`);
|
||||
|
||||
// Get the change amount by subtracting the required sats out from the total input sats and the default fee
|
||||
const changeAmount = totalInputSats - requiredSats - DEFAULT_FEE;
|
||||
deps.io.verbose(
|
||||
`Change amount: ${changeAmount} satoshis (fee: ${DEFAULT_FEE})`,
|
||||
);
|
||||
|
||||
// If the change amount is less than 0, print a message and return null
|
||||
if (changeAmount < 0n) {
|
||||
deps.io.err(
|
||||
`Insufficient funds. Inputs total ${totalInputSats} sats, but need ${requiredSats + DEFAULT_FEE} sats (${requiredSats} required + ${DEFAULT_FEE} fee).`,
|
||||
@@ -234,10 +255,13 @@ async function buildAppendParams(
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the change amount is greater than or equal to the dust threshold, add the change output
|
||||
if (changeAmount >= DUST_THRESHOLD) {
|
||||
outputs.push({ valueSatoshis: changeAmount });
|
||||
deps.io.out(`Auto-adding change output: ${changeAmount} satoshis`);
|
||||
} else if (changeAmount > 0n) {
|
||||
}
|
||||
// If the change amount is greater than 0, print a message
|
||||
else if (changeAmount > 0n) {
|
||||
deps.io.out(
|
||||
`Change ${changeAmount} sats is below dust threshold (${DUST_THRESHOLD} sats), donating to miners as fee.`,
|
||||
);
|
||||
@@ -311,6 +335,7 @@ export const handleInvitationCommand = async (
|
||||
const subCommand = args[0];
|
||||
deps.io.verbose(`Invitation sub-command: ${subCommand}`);
|
||||
|
||||
// If there was no subcommand provided, print the help message and throw an error
|
||||
if (!subCommand) {
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -320,14 +345,18 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Switch statement to handle the different subcommands
|
||||
switch (subCommand) {
|
||||
case "create": {
|
||||
// Get the template query and action identifier from the arguments
|
||||
const templateQuery = args[1];
|
||||
const actionIdentifier = args[2];
|
||||
deps.io.verbose(
|
||||
`Template query: ${templateQuery}, action identifier: ${actionIdentifier}`,
|
||||
);
|
||||
|
||||
// If they didnt provide us with a template query or action identifier, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
if (!templateQuery || !actionIdentifier) {
|
||||
deps.io.verbose("No template file or action identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -337,25 +366,31 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the template, this will check both filepath and identifier. Because we are flexible here, we will need to generate the identifier again after
|
||||
const template = await resolveTemplate(deps, templateQuery);
|
||||
const templateIdentifier = generateTemplateIdentifier(template);
|
||||
|
||||
// Create an XOInvitation. We will convert this into our own invitation instance afterwards
|
||||
const rawInvitation = await deps.app.engine.createInvitation({
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
});
|
||||
deps.io.verbose(`XOInvitation created: ${formatObject(rawInvitation)}`);
|
||||
|
||||
// Create our own invitation instance out of the raw XOInvitation. This will also initate the SSE Session
|
||||
const invitationInstance = await deps.app.createInvitation(rawInvitation);
|
||||
deps.io.verbose(
|
||||
`Invitation created: ${formatObject(invitationInstance.data)}`,
|
||||
);
|
||||
|
||||
// Read the variables that were passed in via `-var-<name> <value>`
|
||||
const variables = parseVariablesFromOptions(options);
|
||||
deps.io.verbose(`Variables: ${formatObject(variables)}`);
|
||||
if (variables.length > 0) {
|
||||
await invitationInstance.addVariables(variables);
|
||||
}
|
||||
|
||||
// Build the parameters for the append call. This will resolve the inputs and outputs for the invitation.
|
||||
const params = await buildAppendParams(deps, invitationInstance, options);
|
||||
if (!params) {
|
||||
throw new CommandError(
|
||||
@@ -364,11 +399,14 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Append the inputs and outputs to the invitation
|
||||
const { inputs, outputs } = params;
|
||||
if (inputs.length > 0 || outputs.length > 0) {
|
||||
await invitationInstance.append({ inputs, outputs });
|
||||
}
|
||||
|
||||
// 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-${invitationInstance.data.invitationIdentifier}.json`;
|
||||
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
||||
writeFileSync(
|
||||
@@ -379,6 +417,7 @@ export const handleInvitationCommand = async (
|
||||
`Invitation created: ${path.basename(invitationFilePath)} (${invitationInstance.data.invitationIdentifier})`,
|
||||
);
|
||||
|
||||
// Get the missing requirements for the invitation. This will tell us if we are missing any variables, inputs, outputs, or roles.
|
||||
const missingRequirements =
|
||||
await invitationInstance.getMissingRequirements();
|
||||
const hasMissing =
|
||||
@@ -388,14 +427,17 @@ export const handleInvitationCommand = async (
|
||||
(missingRequirements.roles !== undefined &&
|
||||
Object.keys(missingRequirements.roles).length > 0);
|
||||
|
||||
// If there are missing requirements, print them out
|
||||
if (hasMissing) {
|
||||
deps.io.out(`\n${bold("Remaining requirements:")}`);
|
||||
deps.io.out(formatObject(missingRequirements));
|
||||
} else {
|
||||
// If there are no missing requirements, sign the invitation if the user has requested it
|
||||
const shouldSign =
|
||||
options["sign"] === "true" || options["broadcast"] === "true";
|
||||
const shouldBroadcast = options["broadcast"] === "true";
|
||||
|
||||
// Sign the invitation if the user has requested it
|
||||
if (shouldSign) {
|
||||
await invitationInstance.sign();
|
||||
deps.io.out(
|
||||
@@ -403,6 +445,7 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Broadcast the transaction if the user has requested it
|
||||
if (shouldBroadcast) {
|
||||
const txHash = await invitationInstance.broadcast();
|
||||
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
|
||||
@@ -412,15 +455,20 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the invitation identifier
|
||||
return {
|
||||
invitationIdentifier: invitationInstance.data.invitationIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
case "append": {
|
||||
// Get the invitation identifier from the arguments
|
||||
const invitationIdentifier = args[1];
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// If they didnt provide us with an invitation identifier, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
if (!invitationIdentifier) {
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -430,9 +478,12 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Find the invitation instance in our list of invitations
|
||||
const invitation = deps.app.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
|
||||
// If the invitation is not found, print an error and throw an error
|
||||
if (!invitation) {
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError(
|
||||
@@ -442,12 +493,14 @@ export const handleInvitationCommand = async (
|
||||
}
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Parse the variables that were passed in via `-var-<name> <value>`
|
||||
const variables = parseVariablesFromOptions(options);
|
||||
deps.io.verbose(`Variables to append: ${formatObject(variables)}`);
|
||||
if (variables.length > 0) {
|
||||
await invitation.addVariables(variables);
|
||||
}
|
||||
|
||||
// Build the parameters for the append call. This will resolve the inputs and outputs for the invitation.
|
||||
const params = await buildAppendParams(deps, invitation, options);
|
||||
if (!params) {
|
||||
throw new CommandError(
|
||||
@@ -456,6 +509,7 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// If there are no variables, inputs, or outputs, print an error and throw an error
|
||||
const { inputs, outputs } = params;
|
||||
if (
|
||||
variables.length === 0 &&
|
||||
@@ -468,18 +522,22 @@ export const handleInvitationCommand = async (
|
||||
throw new CommandError("invitation.append.empty", error);
|
||||
}
|
||||
|
||||
// Append the inputs and outputs to the invitation
|
||||
if (inputs.length > 0 || outputs.length > 0) {
|
||||
await invitation.append({ inputs, outputs });
|
||||
}
|
||||
deps.io.verbose(`Invitation appended: ${formatObject(invitation.data)}`);
|
||||
deps.io.out(`Invitation appended: ${invitationIdentifier}`);
|
||||
|
||||
// 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));
|
||||
deps.io.out(
|
||||
`Invitation updated: ${path.basename(invitationFilePath)} (${invitation.data.invitationIdentifier})`,
|
||||
);
|
||||
|
||||
// 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 ||
|
||||
@@ -488,19 +546,23 @@ export const handleInvitationCommand = async (
|
||||
(missingRequirements.roles !== undefined &&
|
||||
Object.keys(missingRequirements.roles).length > 0);
|
||||
|
||||
// If there are missing requirements, print them out
|
||||
if (hasMissing) {
|
||||
deps.io.out(`\n${bold("Remaining requirements:")}`);
|
||||
deps.io.out(formatObject(missingRequirements));
|
||||
} else {
|
||||
// If there are no missing requirements, sign the invitation if the user has requested it
|
||||
const shouldSign =
|
||||
options["sign"] === "true" || options["broadcast"] === "true";
|
||||
const shouldBroadcast = options["broadcast"] === "true";
|
||||
|
||||
// Sign the invitation if the user has requested it
|
||||
if (shouldSign) {
|
||||
await invitation.sign();
|
||||
deps.io.out(`Invitation signed: ${invitationIdentifier}`);
|
||||
}
|
||||
|
||||
// Broadcast the transaction if the user has requested it
|
||||
if (shouldBroadcast) {
|
||||
const txHash = await invitation.broadcast();
|
||||
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
|
||||
@@ -514,8 +576,12 @@ export const handleInvitationCommand = async (
|
||||
}
|
||||
|
||||
case "sign": {
|
||||
// Get the invitation identifier from the arguments
|
||||
const invitationIdentifier = args[1];
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// If they didnt provide us with an invitation identifier, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
if (!invitationIdentifier) {
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -525,10 +591,13 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Find the invitation instance in our list of invitations
|
||||
const invitation = deps.app.invitations.find(
|
||||
(candidate) =>
|
||||
candidate.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
|
||||
// If the invitation is not found, print an error and throw an error
|
||||
if (!invitation) {
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError(
|
||||
@@ -538,15 +607,22 @@ export const handleInvitationCommand = async (
|
||||
}
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Sign the invitation
|
||||
await invitation.sign();
|
||||
deps.io.verbose(`Invitation signed: ${formatObject(invitation.data)}`);
|
||||
deps.io.out(`Invitation signed: ${invitationIdentifier}`);
|
||||
|
||||
// Return the invitation identifier
|
||||
return { invitationIdentifier };
|
||||
}
|
||||
|
||||
case "broadcast": {
|
||||
// Get the invitation identifier from the arguments
|
||||
const invitationIdentifier = args[1];
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// If they didnt provide us with an invitation identifier, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
if (!invitationIdentifier) {
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -556,10 +632,13 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Find the invitation instance in our list of invitations
|
||||
const invitation = deps.app.invitations.find(
|
||||
(candidate) =>
|
||||
candidate.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
|
||||
// If the invitation is not found, print an error and throw an error
|
||||
if (!invitation) {
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError(
|
||||
@@ -569,17 +648,24 @@ export const handleInvitationCommand = async (
|
||||
}
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Broadcast the transaction
|
||||
const txHash = await invitation.broadcast();
|
||||
deps.io.verbose(
|
||||
`Invitation broadcasted: ${formatObject(invitation.data)}`,
|
||||
);
|
||||
deps.io.out(`Transaction broadcast: ${bold(txHash)}`);
|
||||
|
||||
// Return the invitation identifier and transaction hash
|
||||
return { invitationIdentifier, txHash };
|
||||
}
|
||||
|
||||
case "requirements": {
|
||||
// Get the invitation identifier from the arguments
|
||||
const invitationIdentifier = args[1];
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// If they didnt provide us with an invitation identifier, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
if (!invitationIdentifier) {
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -589,10 +675,13 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Find the invitation instance in our list of invitations
|
||||
const invitation = deps.app.invitations.find(
|
||||
(candidate) =>
|
||||
candidate.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
|
||||
// If the invitation is not found, print an error and throw an error
|
||||
if (!invitation) {
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError(
|
||||
@@ -602,18 +691,26 @@ export const handleInvitationCommand = async (
|
||||
}
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// List the requirements for the invitation
|
||||
const requirements = await deps.app.engine.listRequirements(
|
||||
invitation.data,
|
||||
);
|
||||
deps.io.verbose(`Requirements: ${formatObject(requirements)}`);
|
||||
deps.io.out(formatObject(requirements));
|
||||
|
||||
// Return the invitation identifier
|
||||
return { invitationIdentifier };
|
||||
}
|
||||
|
||||
case "inspect": {
|
||||
// Get the invitation file path from the arguments
|
||||
const invitationFilePath = args[1];
|
||||
|
||||
// If they didnt provide us with an invitation file path, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
||||
|
||||
// Read the invitation file
|
||||
if (!invitationFilePath) {
|
||||
deps.io.verbose("No invitation file provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -623,24 +720,31 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
|
||||
const invitationFile = await readFileSync(invitationFilePath, "utf8");
|
||||
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
||||
|
||||
// Parse the invitation file
|
||||
const invitation = JSON.parse(invitationFile);
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation)}`);
|
||||
|
||||
// Create the invitation instance (NOTE: Not an engine compatible invitation. This is the wrapped format)
|
||||
const invitationInstance = await deps.app.createInvitation(invitation);
|
||||
deps.io.verbose(
|
||||
`Invitation created: ${formatObject(invitationInstance.data)}`,
|
||||
);
|
||||
|
||||
// Get the template for the invitation
|
||||
const template = await deps.app.engine.getTemplate(
|
||||
invitationInstance.data.templateIdentifier,
|
||||
);
|
||||
|
||||
// Get the action for the invitation
|
||||
const action =
|
||||
template?.actions[invitationInstance.data.actionIdentifier];
|
||||
deps.io.verbose(`Action: ${formatObject(action)}`);
|
||||
|
||||
// If the action is not found, print an error and throw an error
|
||||
if (!action) {
|
||||
deps.io.err(
|
||||
`Action not found: ${invitationInstance.data.actionIdentifier}`,
|
||||
@@ -651,9 +755,11 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Get the status for the invitation
|
||||
const status = invitationInstance.status;
|
||||
deps.io.verbose(`Status: ${status}`);
|
||||
|
||||
// Get the entities for the invitation
|
||||
const entities = Array.from(
|
||||
new Set(
|
||||
invitationInstance.data.commits.map(
|
||||
@@ -663,6 +769,7 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
deps.io.verbose(`Entities: ${formatObject(entities)}`);
|
||||
|
||||
// Get the entities with roles for the invitation
|
||||
const entitiesWithRoles = entities.map((entity) => {
|
||||
return {
|
||||
entityIdentifier: entity,
|
||||
@@ -685,21 +792,25 @@ export const handleInvitationCommand = async (
|
||||
};
|
||||
});
|
||||
|
||||
// Get the inputs for the invitation
|
||||
const inputs = invitationInstance.data.commits.flatMap(
|
||||
(commit) => commit.data.inputs ?? [],
|
||||
);
|
||||
deps.io.verbose(`Inputs: ${formatObject(inputs)}`);
|
||||
|
||||
// Get the outputs for the invitation
|
||||
const outputs = invitationInstance.data.commits.flatMap(
|
||||
(commit) => commit.data.outputs ?? [],
|
||||
);
|
||||
deps.io.verbose(`Outputs: ${formatObject(outputs)}`);
|
||||
|
||||
// Get the variables for the invitation
|
||||
const variables = invitationInstance.data.commits.flatMap(
|
||||
(commit) => commit.data.variables ?? [],
|
||||
);
|
||||
deps.io.verbose(`Variables: ${formatObject(variables)}`);
|
||||
|
||||
// Return the invitation details
|
||||
return {
|
||||
templateName: template?.name ?? "Unknown",
|
||||
actionIdentifier: invitationInstance.data.actionIdentifier,
|
||||
@@ -712,9 +823,12 @@ export const handleInvitationCommand = async (
|
||||
}
|
||||
|
||||
case "import": {
|
||||
// Get the invitation file path from the arguments
|
||||
const invitationFilePath = args[1];
|
||||
deps.io.verbose(`Invitation file path: ${invitationFilePath}`);
|
||||
|
||||
// If they didnt provide us with an invitation file path, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
if (!invitationFilePath) {
|
||||
deps.io.verbose("No invitation file provided");
|
||||
printInvitationHelp(deps.io);
|
||||
@@ -724,26 +838,41 @@ export const handleInvitationCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Read the invitation file (XOInvitation format, can be passed to the engine directly)
|
||||
const invitationFile = await readFileSync(invitationFilePath, "utf8");
|
||||
deps.io.verbose(`Invitation file: ${invitationFile}`);
|
||||
|
||||
// Parse the invitation file
|
||||
const invitation = JSON.parse(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)}`);
|
||||
|
||||
// Create the invitation instance (NOTE: Not an engine compatible invitation. This is the wrapped format)
|
||||
const invitationInstance = await deps.app.createInvitation(xoInvitation);
|
||||
deps.io.verbose(
|
||||
`Invitation created: ${formatObject(invitationInstance.data)}`,
|
||||
);
|
||||
|
||||
// Return the invitation identifier
|
||||
return {
|
||||
invitationIdentifier: invitationInstance.data.invitationIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
case "list": {
|
||||
// List all the invitations
|
||||
const invitations = await Promise.all(
|
||||
// Iterate over the invitations and compile them into a list of data that we can use to display them with another loop later.
|
||||
deps.app.invitations.map(async (invitation) => {
|
||||
// Get the template for the invitation
|
||||
const template = await deps.app.engine.getTemplate(
|
||||
invitation.data.templateIdentifier,
|
||||
);
|
||||
|
||||
// Get the role identifier for the invitation
|
||||
return {
|
||||
invitationIdentifier: invitation.data.invitationIdentifier,
|
||||
templateIdentifier: invitation.data.templateIdentifier,
|
||||
@@ -755,15 +884,22 @@ export const handleInvitationCommand = async (
|
||||
}),
|
||||
);
|
||||
deps.io.verbose(`Invitations: ${formatObject(invitations)}`);
|
||||
|
||||
// Format the invitations into a list of strings that we can display to the user
|
||||
const formattedInvitations = invitations.map(
|
||||
(invitation) =>
|
||||
`${bold(invitation.templateName)} ${dim(invitation.status)} ${dim(invitation.invitationIdentifier)} ${dim(invitation.actionIdentifier)} (${dim(invitation.roleIdentifier)})`,
|
||||
);
|
||||
|
||||
// Display the invitations to the user
|
||||
deps.io.out(formattedInvitations.join("\n"));
|
||||
|
||||
// Return the number of invitations
|
||||
return { count: invitations.length };
|
||||
}
|
||||
|
||||
default:
|
||||
// If the sub-command is not found, print an error and throw an error
|
||||
deps.io.verbose(`Unknown invitation sub-command: ${subCommand}`);
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError(
|
||||
|
||||
@@ -41,9 +41,11 @@ export const handleMnemonicCommand = async (
|
||||
args: string[],
|
||||
options: Record<string, string>,
|
||||
): Promise<{ savedAs?: string; count?: number; mnemonic?: string }> => {
|
||||
// Get the sub-command from the arguments
|
||||
const subCommand = args[0];
|
||||
const { mnemonicsDir } = deps.paths;
|
||||
|
||||
// If no sub-command is provided, print the help message and throw an error
|
||||
if (!subCommand) {
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printMnemonicHelp(deps.io);
|
||||
@@ -53,22 +55,29 @@ export const handleMnemonicCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the sub-command
|
||||
switch (subCommand) {
|
||||
case "create": {
|
||||
// Create a new mnemonic seed
|
||||
const mnemonicSeed = createMnemonicSeed();
|
||||
|
||||
// Create a new mnemonic file
|
||||
const savedAs = createMnemonicFile(
|
||||
mnemonicsDir,
|
||||
mnemonicSeed,
|
||||
options["output"],
|
||||
);
|
||||
|
||||
// Display the mnemonic file to the user
|
||||
deps.io.out(`Mnemonic file created: ${savedAs} (${mnemonicSeed})`);
|
||||
return { savedAs };
|
||||
}
|
||||
|
||||
case "import": {
|
||||
// Get the mnemonic seed from the arguments
|
||||
const mnemonicSeed = args.slice(1).join(" ");
|
||||
|
||||
// If no mnemonic seed is provided, print the help message and throw an error
|
||||
if (!mnemonicSeed) {
|
||||
deps.io.verbose("No mnemonic seed provided");
|
||||
printMnemonicHelp(deps.io);
|
||||
@@ -78,25 +87,33 @@ export const handleMnemonicCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new mnemonic file
|
||||
deps.io.verbose(`Mnemonic seed: ${mnemonicSeed}`);
|
||||
const savedAs = createMnemonicFile(
|
||||
mnemonicsDir,
|
||||
mnemonicSeed,
|
||||
options["output"],
|
||||
);
|
||||
|
||||
// Display the mnemonic file to the user
|
||||
deps.io.out(`Mnemonic file created: ${savedAs}`);
|
||||
return { savedAs };
|
||||
}
|
||||
|
||||
case "list": {
|
||||
// List all the mnemonic files
|
||||
const mnemonicFiles = listMnemonicFiles(mnemonicsDir);
|
||||
deps.io.out(mnemonicFiles.join("\n"));
|
||||
|
||||
// Return the number of mnemonic files
|
||||
return { count: mnemonicFiles.length };
|
||||
}
|
||||
|
||||
case "expose": {
|
||||
// Get the mnemonic file from the arguments
|
||||
const mnemonicFile = args[1];
|
||||
|
||||
// If no mnemonic file is provided, print the help message and throw an error
|
||||
if (!mnemonicFile) {
|
||||
deps.io.verbose("No mnemonic file provided");
|
||||
printMnemonicHelp(deps.io);
|
||||
@@ -106,11 +123,15 @@ export const handleMnemonicCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Try to load the mnemonic file
|
||||
try {
|
||||
const mnemonic = loadMnemonic(mnemonicsDir, mnemonicFile);
|
||||
deps.io.out(mnemonic);
|
||||
|
||||
// Return the mnemonic
|
||||
return { mnemonic };
|
||||
} catch (error) {
|
||||
// If the mnemonic file is not found, print an error and throw an error
|
||||
throw new CommandError(
|
||||
"mnemonic.expose.file_not_found",
|
||||
`Mnemonic file not found: ${mnemonicFile}`,
|
||||
@@ -119,6 +140,7 @@ export const handleMnemonicCommand = async (
|
||||
}
|
||||
|
||||
default:
|
||||
// If the sub-command is not found, print an error and throw an error
|
||||
deps.io.err(`Unknown sub-command: ${subCommand}`);
|
||||
printMnemonicHelp(deps.io);
|
||||
throw new CommandError(
|
||||
|
||||
@@ -44,14 +44,17 @@ export const handleReceiveCommand = async (
|
||||
args: string[],
|
||||
_options: Record<string, string>,
|
||||
): Promise<{ address: string }> => {
|
||||
// Get the template query, output identifier, and role identifier from the arguments
|
||||
const templateQuery = args[0];
|
||||
const outputIdentifier = args[1];
|
||||
const roleIdentifier = args[2];
|
||||
|
||||
// Log the receive args
|
||||
deps.io.verbose(
|
||||
`Receive args - template: ${templateQuery}, output: ${outputIdentifier}, role: ${roleIdentifier}`,
|
||||
);
|
||||
|
||||
// If no template query or output identifier is provided, print the help message and throw an error
|
||||
if (!templateQuery || !outputIdentifier) {
|
||||
deps.io.verbose("Missing required arguments");
|
||||
printReceiveHelp(deps.io);
|
||||
|
||||
@@ -30,18 +30,27 @@ function formatResource(
|
||||
resource: UnspentOutputData,
|
||||
showReserved = false,
|
||||
): string {
|
||||
// Format the outpoint
|
||||
const outpoint = bold(
|
||||
`${resource.outpointTransactionHash}:${resource.outpointIndex}`,
|
||||
);
|
||||
|
||||
// Format the value
|
||||
const value = dim(`${resource.valueSatoshis} sats`);
|
||||
|
||||
// Format the output
|
||||
const output = dim(resource.outputIdentifier);
|
||||
|
||||
// Format the height
|
||||
const height = dim(`(height ${resource.minedAtHeight})`);
|
||||
|
||||
// If the resource is reserved, format the reservation info
|
||||
if (showReserved && resource.reservedBy) {
|
||||
const inv = dim(`reserved for ${resource.reservedBy}`);
|
||||
return `${outpoint} ${value} ${output} ${height} ${inv}`;
|
||||
}
|
||||
|
||||
// Otherwise, format the resource without reservation info
|
||||
return `${outpoint} ${value} ${output} ${height}`;
|
||||
}
|
||||
|
||||
@@ -60,6 +69,7 @@ export const handleResourceCommand = async (
|
||||
const subCommand = args[0];
|
||||
deps.io.verbose(`Resource sub-command: ${subCommand}`);
|
||||
|
||||
// If no sub-command is provided, print the help message and throw an error
|
||||
if (!subCommand) {
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printResourceHelp(deps.io);
|
||||
@@ -69,39 +79,59 @@ export const handleResourceCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the sub-command
|
||||
switch (subCommand) {
|
||||
case "list": {
|
||||
// Get the qualifier from the arguments - This could be "reserved", "all", or omitted (which defaults to "unreserved")
|
||||
const qualifier = args[1];
|
||||
|
||||
// List all the unspent outputs data
|
||||
const allResources = await deps.app.engine.listUnspentOutputsData();
|
||||
|
||||
let filtered;
|
||||
// If the qualifier is "reserved", return only the reserved resources
|
||||
if (qualifier === "reserved") {
|
||||
filtered = allResources.filter((r) => r.reservedBy);
|
||||
} else if (qualifier === "all") {
|
||||
}
|
||||
// If the qualifier is "all", return all the resources
|
||||
else if (qualifier === "all") {
|
||||
filtered = allResources;
|
||||
} else {
|
||||
}
|
||||
// If the qualifier is not "reserved" or "all", return only the unreserved resources
|
||||
else {
|
||||
filtered = allResources.filter((r) => !r.reservedBy);
|
||||
}
|
||||
|
||||
// If no resources are found, print a message and return 0
|
||||
if (filtered.length === 0) {
|
||||
deps.io.out(dim("No resources found."));
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
// Format the resources into a list of strings that we can display to the user
|
||||
const showReserved = qualifier === "all" || qualifier === "reserved";
|
||||
const formattedResources = filtered.map((r) =>
|
||||
formatResource(r, showReserved),
|
||||
);
|
||||
|
||||
// Display the resources to the user
|
||||
deps.io.out(formattedResources.join("\n"));
|
||||
|
||||
// Display the total satoshis
|
||||
deps.io.out(
|
||||
`Total satoshis: ${filtered.reduce((acc, r) => acc + r.valueSatoshis, 0)}`,
|
||||
);
|
||||
|
||||
// Display the total resources
|
||||
deps.io.out(`Total resources: ${filtered.length}`);
|
||||
return { count: filtered.length };
|
||||
}
|
||||
|
||||
case "unreserve": {
|
||||
// Get the outpoint from the arguments
|
||||
const outpointArg = args[1];
|
||||
|
||||
// If no outpoint is provided, print a message and throw an error
|
||||
if (!outpointArg) {
|
||||
deps.io.err("Please provide a UTXO in <txhash>:<vout> format.");
|
||||
printResourceHelp(deps.io);
|
||||
@@ -111,8 +141,10 @@ export const handleResourceCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Get the separator index
|
||||
const separatorIndex = outpointArg.lastIndexOf(":");
|
||||
if (separatorIndex === -1) {
|
||||
// If the separator index is -1 (not found), print a message and throw an error
|
||||
deps.io.err(
|
||||
`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`,
|
||||
);
|
||||
@@ -122,8 +154,11 @@ export const handleResourceCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Get the tx hash and vout
|
||||
const txHash = outpointArg.substring(0, separatorIndex);
|
||||
const vout = parseInt(outpointArg.substring(separatorIndex + 1), 10);
|
||||
|
||||
// If the tx hash or vout is not a string or isNaN, print a message and throw an error
|
||||
if (!txHash || isNaN(vout)) {
|
||||
deps.io.err(
|
||||
`Invalid format "${outpointArg}". Expected <txhash>:<vout>.`,
|
||||
@@ -134,11 +169,15 @@ export const handleResourceCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Gather all of our resources
|
||||
const allResources = await deps.app.engine.listUnspentOutputsData();
|
||||
|
||||
// Find the target resource
|
||||
const target = allResources.find(
|
||||
(r) => r.outpointTransactionHash === txHash && r.outpointIndex === vout,
|
||||
);
|
||||
|
||||
// If the target resource is not found, print a message and throw an error
|
||||
if (!target) {
|
||||
deps.io.err(`UTXO not found: ${txHash}:${vout}`);
|
||||
throw new CommandError(
|
||||
@@ -147,28 +186,39 @@ export const handleResourceCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// If the target resource is not reserved, print a message and return
|
||||
if (!target.reservedBy) {
|
||||
deps.io.out(dim("UTXO is not reserved. Nothing to do."));
|
||||
return {};
|
||||
}
|
||||
|
||||
// Unreserve the resources
|
||||
await deps.app.engine.unreserveResources(
|
||||
[{ outpointTransactionHash: hexToBin(txHash), outpointIndex: vout }],
|
||||
target.reservedBy,
|
||||
);
|
||||
|
||||
deps.io.out(
|
||||
`Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.reservedBy})`,
|
||||
);
|
||||
|
||||
// TODO: What do I want to return here?
|
||||
return {};
|
||||
}
|
||||
|
||||
case "unreserve-all": {
|
||||
// Unreserve all the resources
|
||||
const count = await deps.app.unreserveAllResources();
|
||||
|
||||
// If no resources are reserved, print a message and return
|
||||
if (count === 0) {
|
||||
deps.io.out(dim("No reserved resources to unreserve."));
|
||||
} else {
|
||||
}
|
||||
// If some resources were unreserved, print a message and return the count
|
||||
else {
|
||||
deps.io.out(`Unreserved ${bold(String(count))} resource(s).`);
|
||||
}
|
||||
|
||||
return { count };
|
||||
}
|
||||
|
||||
|
||||
@@ -37,22 +37,33 @@ export const handleTemplateListCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
): Promise<{ count?: number }> => {
|
||||
// Get the template category from the arguments - This could be "action", "transaction", "output", "lockingscript", or "variable"
|
||||
const templateCategory = args[0];
|
||||
deps.io.verbose(`Template list category: ${templateCategory}`);
|
||||
|
||||
// If no template category is provided, list all the imported templates
|
||||
if (!templateCategory) {
|
||||
// List all the imported templates
|
||||
const templates = await deps.app.engine.listImportedTemplates();
|
||||
|
||||
// Format the templates into a list of strings that we can display to the user
|
||||
const formattedTemplates = templates.map(
|
||||
(template: XOTemplate) =>
|
||||
`${bold(generateTemplateIdentifier(template))} - ${dim(template.name)} ${dim(template.description)}`,
|
||||
);
|
||||
|
||||
// Display the templates to the user
|
||||
deps.io.out(formattedTemplates.join("\n"));
|
||||
|
||||
// Return the number of templates
|
||||
return { count: templates.length };
|
||||
}
|
||||
|
||||
// Get the template identifier from the arguments
|
||||
const templateIdentifier = args[1];
|
||||
deps.io.verbose(`Template identifier: ${templateIdentifier}`);
|
||||
|
||||
// If no template identifier is provided, print a message and throw an error
|
||||
if (!templateIdentifier) {
|
||||
deps.io.err("No template identifier provided");
|
||||
throw new CommandError(
|
||||
@@ -61,7 +72,10 @@ export const handleTemplateListCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Get the raw template from the engine
|
||||
const rawTemplate = await deps.app.engine.getTemplate(templateIdentifier);
|
||||
|
||||
// If the raw template is not found, print a message and throw an error
|
||||
if (!rawTemplate) {
|
||||
deps.io.err(`No template found: ${templateIdentifier}`);
|
||||
throw new CommandError(
|
||||
@@ -70,53 +84,91 @@ export const handleTemplateListCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the template deeply - Deeply nested objects instead of shallow objects referencing keys at the top level.
|
||||
// Reduces the load of having to call multiple lookups just to get some resolved value like the outputIdentifer that comes from calling an action.
|
||||
const template = await resolveTemplateReferences(rawTemplate);
|
||||
deps.io.verbose(`Template: ${formatObject(template)}`);
|
||||
|
||||
// Handle the template category
|
||||
switch (templateCategory) {
|
||||
case "action": {
|
||||
// Get the actions from the template
|
||||
const actions = template.actions;
|
||||
|
||||
// Format the actions into a list of strings that we can display to the user
|
||||
const formattedActions = Object.entries(actions).map(
|
||||
([actionIdentifier, action]) =>
|
||||
`${bold(actionIdentifier)} ${dim(action.name)} ${dim(action.description)}`,
|
||||
);
|
||||
|
||||
// Display the actions to the user
|
||||
deps.io.out(formattedActions.join("\n"));
|
||||
|
||||
// Return the number of actions
|
||||
return {};
|
||||
}
|
||||
case "transaction": {
|
||||
// Get the transactions from the template
|
||||
const transactions = template.transactions;
|
||||
|
||||
// Format the transactions into a list of strings that we can display to the user
|
||||
const formattedTransactions = Object.entries(transactions).map(
|
||||
([transactionIdentifier, transaction]) =>
|
||||
`${bold(transactionIdentifier)} ${dim(transaction.name)} ${dim(transaction.description)}`,
|
||||
);
|
||||
|
||||
// Display the transactions to the user
|
||||
deps.io.out(formattedTransactions.join("\n"));
|
||||
|
||||
// Return the number of transactions
|
||||
return {};
|
||||
}
|
||||
case "output": {
|
||||
// Get the outputs from the template
|
||||
const outputs = template.outputs;
|
||||
|
||||
// Format the outputs into a list of strings that we can display to the user
|
||||
const formattedOutputs = Object.entries(outputs).map(
|
||||
([outputIdentifier, output]) =>
|
||||
`${bold(outputIdentifier)} ${dim(output.name)} ${dim(output.description)}`,
|
||||
);
|
||||
|
||||
// Display the outputs to the user
|
||||
deps.io.out(formattedOutputs.join("\n"));
|
||||
|
||||
// Return the number of outputs
|
||||
return {};
|
||||
}
|
||||
case "lockingscript": {
|
||||
// Get the lockingscripts from the template
|
||||
const lockingscripts = template.lockingScripts;
|
||||
|
||||
// Format the lockingscripts into a list of strings that we can display to the user
|
||||
const formattedLockingscripts = Object.entries(lockingscripts).map(
|
||||
([lockingScriptIdentifier, lockingScript]) =>
|
||||
`${bold(lockingScriptIdentifier)} ${dim(lockingScript.name)} ${dim(lockingScript.description)}`,
|
||||
);
|
||||
|
||||
// Display the lockingscripts to the user
|
||||
deps.io.out(formattedLockingscripts.join("\n"));
|
||||
|
||||
// Return the number of lockingscripts
|
||||
return {};
|
||||
}
|
||||
case "variable": {
|
||||
// Get the variables from the template
|
||||
const variables = template.variables || {};
|
||||
|
||||
// Format the variables into a list of strings that we can display to the user
|
||||
const formattedVariables = Object.entries(variables).map(
|
||||
([variableIdentifier, variable]) =>
|
||||
`${bold(variableIdentifier)} ${dim(variable.name)} ${dim(variable.description)}`,
|
||||
);
|
||||
|
||||
// Display the variables to the user
|
||||
deps.io.out(formattedVariables.join("\n"));
|
||||
|
||||
// Return the number of variables
|
||||
return {};
|
||||
}
|
||||
default: {
|
||||
@@ -162,6 +214,7 @@ export const handleTemplateInspectCommand = async (
|
||||
deps: CommandDependencies,
|
||||
args: string[],
|
||||
): Promise<Record<string, never>> => {
|
||||
// Get the template category, identifier, and field from the arguments
|
||||
const templateCategory = args[0];
|
||||
const templateQuery = args[1];
|
||||
const templateField = args[2];
|
||||
@@ -170,6 +223,7 @@ export const handleTemplateInspectCommand = async (
|
||||
`Template inspect args - category: ${templateCategory}, identifier: ${templateQuery}, field: ${templateField}`,
|
||||
);
|
||||
|
||||
// If no template category, identifier, or field is provided, print a message and throw an error
|
||||
if (!templateCategory || !templateQuery || !templateField) {
|
||||
deps.io.err("No template category, identifier, or field provided");
|
||||
printTemplateInspectHelp(deps.io);
|
||||
@@ -179,15 +233,21 @@ export const handleTemplateInspectCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the template
|
||||
const originalTemplate = await resolveTemplate(deps, templateQuery);
|
||||
deps.io.verbose(`Original Template: ${formatObject(originalTemplate)}`);
|
||||
|
||||
// Resolve the template references
|
||||
const template = await resolveTemplateReferences(originalTemplate);
|
||||
deps.io.verbose(`Extended Template: ${formatObject(template)}`);
|
||||
|
||||
// Handle the template category
|
||||
switch (templateCategory) {
|
||||
case "action": {
|
||||
// Get the action from the template
|
||||
const action = template.actions[templateField];
|
||||
|
||||
// If the action is not found, print a message and throw an error
|
||||
if (!action) {
|
||||
deps.io.err(`No action found: ${templateField}`);
|
||||
throw new CommandError(
|
||||
@@ -195,11 +255,16 @@ export const handleTemplateInspectCommand = async (
|
||||
`No action found: ${templateField}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Display the action to the user
|
||||
deps.io.out(formatObject(action));
|
||||
return {};
|
||||
}
|
||||
case "transaction": {
|
||||
// Get the transaction from the template
|
||||
const transaction = template.transactions?.[templateField];
|
||||
|
||||
// If the transaction is not found, print a message and throw an error
|
||||
if (!transaction) {
|
||||
deps.io.err(`No transaction found: ${templateField}`);
|
||||
throw new CommandError(
|
||||
@@ -207,11 +272,16 @@ export const handleTemplateInspectCommand = async (
|
||||
`No transaction found: ${templateField}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Display the transaction to the user
|
||||
deps.io.out(formatObject(transaction));
|
||||
return {};
|
||||
}
|
||||
case "output": {
|
||||
// Get the output from the template
|
||||
const output = template.outputs[templateField];
|
||||
|
||||
// If the output is not found, print a message and throw an error
|
||||
if (!output) {
|
||||
deps.io.err(`No output found: ${templateField}`);
|
||||
throw new CommandError(
|
||||
@@ -219,11 +289,16 @@ export const handleTemplateInspectCommand = async (
|
||||
`No output found: ${templateField}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Display the output to the user
|
||||
deps.io.out(formatObject(output));
|
||||
return {};
|
||||
}
|
||||
case "lockingscript": {
|
||||
// Get the lockingscript from the template
|
||||
const lockingscript = template.lockingScripts[templateField];
|
||||
|
||||
// If the lockingscript is not found, print a message and throw an error
|
||||
if (!lockingscript) {
|
||||
deps.io.err(`No lockingscript found: ${templateField}`);
|
||||
throw new CommandError(
|
||||
@@ -231,11 +306,16 @@ export const handleTemplateInspectCommand = async (
|
||||
`No lockingscript found: ${templateField}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Display the lockingscript to the user
|
||||
deps.io.out(formatObject(lockingscript));
|
||||
return {};
|
||||
}
|
||||
case "variable": {
|
||||
// Get the variable from the template
|
||||
const variable = template.variables?.[templateField];
|
||||
|
||||
// If the variable is not found, print a message and throw an error
|
||||
if (!variable) {
|
||||
deps.io.err(`No variable found: ${templateField}`);
|
||||
throw new CommandError(
|
||||
@@ -243,6 +323,8 @@ export const handleTemplateInspectCommand = async (
|
||||
`No variable found: ${templateField}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Display the variable to the user
|
||||
deps.io.out(formatObject(variable));
|
||||
return {};
|
||||
}
|
||||
@@ -268,8 +350,10 @@ export const handleTemplateCommand = async (
|
||||
args: string[],
|
||||
_options: Record<string, string>,
|
||||
): Promise<{ templateFile?: string; count?: number }> => {
|
||||
// Get the sub-command from the arguments
|
||||
const subCommand = args[0];
|
||||
|
||||
// If no sub-command is provided, print a message and throw an error
|
||||
if (!subCommand) {
|
||||
deps.io.verbose("No sub-command provided");
|
||||
printTemplateHelp(deps.io);
|
||||
@@ -279,9 +363,13 @@ export const handleTemplateCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the sub-command
|
||||
switch (subCommand) {
|
||||
case "import": {
|
||||
// Get the template file from the arguments
|
||||
const templateFile = args[1];
|
||||
|
||||
// If no template file is provided, print a message and throw an error
|
||||
deps.io.verbose(`Template file: ${templateFile}`);
|
||||
|
||||
if (!templateFile) {
|
||||
@@ -293,9 +381,11 @@ export const handleTemplateCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the template path
|
||||
const templatePath = path.resolve(`${process.cwd()}/${templateFile}`);
|
||||
deps.io.verbose(`Template path: ${templatePath}`);
|
||||
|
||||
// If the template file does not exist, print a message and throw an error
|
||||
if (!existsSync(templatePath)) {
|
||||
deps.io.err(`Template file does not exist: ${templatePath}`);
|
||||
printTemplateHelp(deps.io);
|
||||
@@ -305,23 +395,32 @@ export const handleTemplateCommand = async (
|
||||
);
|
||||
}
|
||||
|
||||
// Read the template file
|
||||
const template = await readFileSync(templatePath, "utf8");
|
||||
|
||||
deps.io.verbose(`Importing template: ${templateFile}`);
|
||||
|
||||
// Import the template
|
||||
await deps.app.engine.importTemplate(template);
|
||||
deps.io.verbose(`Template imported: ${templateFile}`);
|
||||
|
||||
// Return the template file
|
||||
return { templateFile };
|
||||
}
|
||||
case "list": {
|
||||
// Handle the template list command, We offload here as it has lots of arguments and is quite long
|
||||
return handleTemplateListCommand(deps, args.slice(1));
|
||||
}
|
||||
case "inspect": {
|
||||
// Handle the template inspect command, We offload here as it has lots of arguments and is quite long
|
||||
return handleTemplateInspectCommand(deps, args.slice(1));
|
||||
}
|
||||
case "set-default": {
|
||||
// Get the template file, output identifier, and role identifier from the arguments
|
||||
const templateFile = args[1];
|
||||
const outputIdentifier = args[2];
|
||||
const roleIdentifier = args[3];
|
||||
|
||||
// If no template file, output identifier, or role identifier is provided, print a message and throw an error
|
||||
if (!templateFile || !outputIdentifier || !roleIdentifier) {
|
||||
deps.io.verbose(
|
||||
"No template file, output identifier, or role identifier provided",
|
||||
@@ -332,17 +431,24 @@ export const handleTemplateCommand = async (
|
||||
"No template file, output identifier, or role identifier provided",
|
||||
);
|
||||
}
|
||||
|
||||
// Set the default locking parameters
|
||||
deps.io.verbose(
|
||||
`Template file: ${templateFile}, output identifier: ${outputIdentifier}, role identifier: ${roleIdentifier}`,
|
||||
);
|
||||
|
||||
// Set the default locking parameters
|
||||
await deps.app.engine.setDefaultLockingParameters(
|
||||
templateFile,
|
||||
outputIdentifier,
|
||||
roleIdentifier,
|
||||
);
|
||||
|
||||
// Return an empty object
|
||||
return {};
|
||||
}
|
||||
default:
|
||||
// If the sub-command is not found, print a message and throw an error
|
||||
deps.io.verbose(`Unknown template sub-command: ${subCommand}`);
|
||||
printTemplateHelp(deps.io);
|
||||
throw new CommandError(
|
||||
|
||||
@@ -57,20 +57,24 @@ export const resolveMnemonicFilePath = (
|
||||
mnemonicsDir: string,
|
||||
mnemonicRef: string,
|
||||
): string => {
|
||||
// Try to resolve the mnemonic file as an absolute path
|
||||
if (isAbsolute(mnemonicRef) && existsSync(mnemonicRef)) {
|
||||
return mnemonicRef;
|
||||
}
|
||||
|
||||
// Try to resolve the mnemonic file relative to the current working directory
|
||||
const relativeToCwd = resolve(process.cwd(), mnemonicRef);
|
||||
if (existsSync(relativeToCwd)) {
|
||||
return relativeToCwd;
|
||||
}
|
||||
|
||||
// Try to resolve the mnemonic file in the mnemonics directory
|
||||
const inMnemonics = join(mnemonicsDir, basename(mnemonicRef));
|
||||
if (existsSync(inMnemonics)) {
|
||||
return inMnemonics;
|
||||
}
|
||||
|
||||
// If the mnemonic file is not found, throw an error
|
||||
throw new Error(
|
||||
`Mnemonic file not found: ${mnemonicRef}. Run "xo-cli mnemonic list" to see available files.`,
|
||||
);
|
||||
@@ -86,18 +90,26 @@ export const loadMnemonic = (
|
||||
mnemonicsDir: string,
|
||||
mnemonicFile: string,
|
||||
): string => {
|
||||
// Resolve the mnemonic file path
|
||||
const resolvedPath = resolveMnemonicFilePath(mnemonicsDir, mnemonicFile);
|
||||
|
||||
// Read the mnemonic file
|
||||
const mnemonicUrl = BCHMnemonicURL.fromURL(
|
||||
readFileSync(resolvedPath, "utf8"),
|
||||
);
|
||||
|
||||
// Get the entropy from the mnemonic url
|
||||
const { entropy } = mnemonicUrl.toObject();
|
||||
|
||||
// Encode the entropy to a mnemonic
|
||||
const mnemonic = encodeBip39Mnemonic(entropy);
|
||||
|
||||
// If the mnemonic is not a string, throw an error
|
||||
if (typeof mnemonic === "string") {
|
||||
throw new Error(`Failed to convert entropy to mnemonic: ${mnemonic}`);
|
||||
}
|
||||
|
||||
// Return the mnemonic phrase
|
||||
return mnemonic.phrase;
|
||||
};
|
||||
|
||||
@@ -107,9 +119,12 @@ export const loadMnemonic = (
|
||||
* @returns Basenames suitable for `-m <name>`
|
||||
*/
|
||||
export const listMnemonicFiles = (mnemonicsDir: string): string[] => {
|
||||
// List the mnemonic files in the given directory
|
||||
const filenames = readdirSync(mnemonicsDir).filter((f: string) =>
|
||||
f.startsWith("mnemonic-"),
|
||||
);
|
||||
|
||||
// Return the mnemonic files
|
||||
return filenames;
|
||||
};
|
||||
|
||||
@@ -119,5 +134,6 @@ export const listMnemonicFiles = (mnemonicsDir: string): string[] => {
|
||||
* @returns Basenames suitable for `-m <name>`
|
||||
*/
|
||||
export const listGlobalMnemonicFiles = (): string[] => {
|
||||
// List the mnemonic files in the global mnemonics directory
|
||||
return listMnemonicFiles(getGlobalMnemonicsDir());
|
||||
};
|
||||
|
||||
@@ -23,22 +23,28 @@ export const resolveTemplate = async (
|
||||
deps: CommandDependencies,
|
||||
query: string,
|
||||
): Promise<XOTemplate> => {
|
||||
// Gather all of our imported templates
|
||||
const templates = await deps.app.engine.listImportedTemplates();
|
||||
|
||||
// Create a set to store the matches
|
||||
const matches = new Set<XOTemplate>();
|
||||
|
||||
// Iterate through the templates and check if the identifier matches the query
|
||||
for (const template of templates) {
|
||||
if (generateTemplateIdentifier(template) === query) {
|
||||
// Return early if we got a match since identifiers are always unique by content
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through the templates and check if the name matches the query
|
||||
for (const template of templates) {
|
||||
if (template.name === query) {
|
||||
matches.add(template);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are multiple matches, throw an error
|
||||
if (matches.size > 1) {
|
||||
throw new CommandError(
|
||||
"template.resolve.multiple_matches",
|
||||
@@ -51,10 +57,12 @@ export const resolveTemplate = async (
|
||||
);
|
||||
}
|
||||
|
||||
// If there is one match, return the match
|
||||
if (matches.size === 1) {
|
||||
return matches.values().next().value!;
|
||||
}
|
||||
|
||||
// If there are no matches, throw an error
|
||||
throw new CommandError(
|
||||
"template.resolve.not_found",
|
||||
`Template not found: ${query}`,
|
||||
|
||||
Reference in New Issue
Block a user