@getalby/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  CLI for Nostr Wallet Connect (NIP-47) with lightning tools.
4
4
 
5
- Built for agents - use with the [Alby Bitcoin Payments CLI Skill](https://github.com/getAlby/alby-cli-skill)
5
+ Built for agents - best used with the [Alby Bitcoin Payments CLI Skill](https://github.com/getAlby/alby-cli-skill)
6
+
7
+ ## What this CLI can do
8
+
9
+ Bitcoin lightning wallet operations using Nostr Wallet Connect (NIP-47). Use when the user needs to send/receive bitcoin payments, pay to crypto/stablecoin addresses, check wallet balance, create invoices, convert between fiat and sats, work with lightning addresses, when an HTTP request returns a 402 Payment Required status code and the user wants to pay for and retry the request, or discover paid API services.
6
10
 
7
11
  ## Usage
8
12
 
@@ -27,6 +31,8 @@ npx @getalby/cli auth --complete
27
31
  npx @getalby/cli connect "nostr+walletconnect://..."
28
32
  ```
29
33
 
34
+ Already have a connection secret? Pass it per-command with `-c <secret-or-file>`, or set the `NWC_URL` environment variable.
35
+
30
36
  ### Multiple wallets
31
37
 
32
38
  Use `--wallet-name` when setting up to save named connections:
@@ -49,32 +55,6 @@ List the wallets you've configured (names and connection status only, never the
49
55
  npx @getalby/cli list-wallets
50
56
  ```
51
57
 
52
- ### Connection secret resolution (in order of priority)
53
-
54
- 1. `--connection-secret` flag (value or path to file)
55
- 2. `--wallet-name` flag (`~/.alby-cli/connection-secret-<name>.key`)
56
- 3. `NWC_URL` environment variable
57
- 4. `~/.alby-cli/connection-secret.key` (default file location)
58
-
59
- ```bash
60
- # Use the default saved wallet connection (preferred)
61
- npx @getalby/cli <command> [options]
62
-
63
- # Use a named wallet
64
- npx @getalby/cli --wallet-name alice <command> [options]
65
-
66
- # Or pass a connection secret directly
67
- npx @getalby/cli -c /path/to/secret.txt <command> [options]
68
- ```
69
-
70
- The `-c` option auto-detects whether you're passing a connection string or a file path. You can get a connection string from your NWC-compatible wallet (e.g., [Alby](https://getalby.com)).
71
-
72
- You can also set the `NWC_URL` environment variable instead of using the `-c` option:
73
-
74
- ```txt
75
- NWC_URL="nostr+walletconnect://..."
76
- ```
77
-
78
58
  ## Testing Wallet
79
59
 
80
60
  For testing the CLI without using real funds, you can create a test wallet using the [NWC Faucet](https://faucet.nwc.dev):
@@ -93,107 +73,9 @@ curl -X POST "https://faucet.nwc.dev/wallets/<username>/topup?amount=5000"
93
73
 
94
74
  ## Commands
95
75
 
96
- ### Wallet Commands
97
-
98
- These commands require a wallet connection - either default connection, or specify a custom connection with `-w`, '-c', or `NWC_URL` environment variable:
99
-
100
- ```bash
101
- # Get wallet balance
102
- npx @getalby/cli get-balance
103
-
104
- # Get wallet info
105
- npx @getalby/cli get-info
106
-
107
- # Get wallet service capabilities
108
- npx @getalby/cli get-wallet-service-info
109
-
110
- # Create an invoice
111
- npx @getalby/cli make-invoice --amount 1000 --description "Payment"
112
-
113
- # Get paid — returns the wallet's lightning address, or a BOLT-11 invoice if --amount is given.
114
- # - With no args: returns the wallet's lightning address (errors if the wallet has none)
115
- npx @getalby/cli receive
116
- # - With --amount: returns a BOLT-11 invoice for that amount; --description is optional
117
- npx @getalby/cli receive --amount 100 --description "coffee"
118
-
119
- # Pay any supported destination — auto-detects type from the destination string.
120
- # Required args depend on the destination type:
121
- # - BOLT-11 invoice (lnbc...): no extra args (use --amount only for zero-amount invoices)
122
- npx @getalby/cli pay "lnbc..."
123
- # - Lightning address (user@domain): requires --amount (sats); optional --comment
124
- npx @getalby/cli pay alice@getalby.com --amount 100 --comment "hi"
125
- # - Node pubkey (66-char hex, compressed secp256k1): keysend, requires --amount (sats)
126
- npx @getalby/cli pay 02abc... --amount 100
127
- # - EVM address (0x...): pay crypto/stablecoin, requires --amount, --currency, and --network
128
- npx @getalby/cli pay 0xabc... --amount 10 --currency USDC --network arbitrum
129
-
130
- # The dedicated `pay-invoice`, `pay-keysend`, and `pay-crypto` commands are
131
- # still available if you want to constrain the destination type explicitly.
132
-
133
- # Look up an invoice by payment hash
134
- npx @getalby/cli lookup-invoice --payment-hash "abc123..."
135
-
136
- # List transactions
137
- npx @getalby/cli list-transactions --limit 10
138
-
139
- # Get wallet budget
140
- npx @getalby/cli get-budget
141
-
142
- # Sign a message
143
- npx @getalby/cli sign-message --message "Hello, World!"
144
-
145
- # Fetch a payment-protected resource (auto-detects L402, X402, MPP)
146
- npx @getalby/cli fetch "https://example.com/api"
147
-
148
- # Fetch with custom method, headers, and body
149
- npx @getalby/cli fetch "https://example.com/api" --method POST --body '{"query":"hello"}' --headers '{"Accept":"application/json"}'
150
-
151
- # Fetch with a custom max amount (default: 5000 sats, 0 = no limit)
152
- npx @getalby/cli fetch "https://example.com/api" --max-amount 1000
153
-
154
- # Wait for a payment notification
155
- npx @getalby/cli wait-for-payment --payment-hash "abc123..."
156
- ```
157
-
158
- ### HOLD Invoices
159
-
160
- HOLD invoices allow you to accept payments conditionally - the payment is held until you settle or cancel it.
161
-
162
- ```bash
163
- # Create a HOLD invoice (you provide the payment hash)
164
- npx @getalby/cli make-hold-invoice --amount 1000 --payment-hash "abc123..."
165
-
166
- # Settle a HOLD invoice (claim the payment)
167
- npx @getalby/cli settle-hold-invoice --preimage "def456..."
168
-
169
- # Cancel a HOLD invoice (reject the payment)
170
- npx @getalby/cli cancel-hold-invoice --payment-hash "abc123..."
171
- ```
172
-
173
- ### Lightning Tools
174
-
175
- These commands don't require a wallet connection:
176
-
177
- ```bash
178
- # Convert USD to sats
179
- npx @getalby/cli fiat-to-sats --currency USD --amount 10
180
-
181
- # Convert sats to USD
182
- npx @getalby/cli sats-to-fiat --amount 1000 --currency USD
183
-
184
- # Parse a BOLT-11 invoice
185
- npx @getalby/cli parse-invoice --invoice "lnbc..."
186
-
187
- # Verify a preimage against an invoice
188
- npx @getalby/cli verify-preimage --invoice "lnbc..." --preimage "abc123..."
189
-
190
- # Request invoice from lightning address
191
- npx @getalby/cli request-invoice-from-lightning-address --address "hello@getalby.com" --amount 1000
192
- ```
193
-
194
- ## Command Reference
76
+ Run `npx @getalby/cli help` for the full list of commands and their arguments, or `npx @getalby/cli help <command>` for one command.
195
77
 
196
- Run `npx @getalby/cli help` for a full list of commands and possible arguments.
78
+ Amounts are always given as `--amount` with `--currency` and `--network`; `--currency BTC` additionally requires `--unit sats|BTC`.
197
79
 
198
80
  ## Output
199
81
 
@@ -0,0 +1,141 @@
1
+ import { InvalidArgumentError } from "commander";
2
+ import { getSatoshiValue } from "@getalby/lightning-tools";
3
+ /**
4
+ * Shared amount model for every amount-bearing command. One axis each for
5
+ * *what* (`--currency`), *where* (`--network`), and — for BTC only — the
6
+ * sub-unit (`--unit`): denomination and rail are never guessed, and the
7
+ * BTC/sats split is made explicit so a bare `--amount 1 --currency BTC` can
8
+ * never be silently interpreted as 1 sat vs 1 whole bitcoin.
9
+ *
10
+ * Rail dispatch is keyed purely on `--network`, so no catalog of currencies or
11
+ * tokens is hardcoded here. BTC is the only special-cased currency (a protocol
12
+ * constant with sub-units); every other code is resolved by the rail it lands
13
+ * on — a fiat code by the rate converter on the lightning rail, a token symbol
14
+ * by the Lendaswap catalog on a chain rail. A currency that doesn't belong on
15
+ * the chosen rail therefore surfaces its error from that downstream resolver.
16
+ */
17
+ export const SATS_PER_BTC = 100_000_000;
18
+ /**
19
+ * Commander coercion for `--amount`: a strict positive number. Rejects `NaN`,
20
+ * unit-suffixed input (`"10abc"`), and values `<= 0` instead of silently
21
+ * truncating them. Replaces the ad-hoc `Number`/`parseInt`/`parseFloat`
22
+ * coercions that previously let the same flag resolve to different values
23
+ * across commands.
24
+ */
25
+ export function parseAmountNumber(value) {
26
+ const num = Number(value);
27
+ if (!Number.isFinite(num) || num <= 0) {
28
+ throw new InvalidArgumentError(`Amount must be a positive number (got "${value}")`);
29
+ }
30
+ return num;
31
+ }
32
+ const LIGHTNING_NETWORK = "lightning";
33
+ /**
34
+ * Validate a (currency, unit, network) triple and decide the payment rail.
35
+ * Keyed on `--network`, which selects the *destination*: `lightning` pays a
36
+ * lightning invoice/address (amount denominated in BTC, or in a fiat code
37
+ * converted to sats); any other value is the chain of a crypto/stablecoin
38
+ * address — still funded from the lightning wallet, then swapped to the token.
39
+ * The payer always pays with lightning. The only currency interpreted here is
40
+ * `BTC` — every other code is passed through for the downstream resolver (rate
41
+ * converter for fiat, Lendaswap catalog for tokens) to validate. Pure /
42
+ * synchronous — no network I/O — so the structural checks run at validation
43
+ * time before any wallet load.
44
+ */
45
+ export function classifyRail({ currency, unit, network, }) {
46
+ if (!currency) {
47
+ throw new Error("An amount requires --currency <BTC|USD|EUR|USDC|…> so the denomination is never guessed");
48
+ }
49
+ if (!network) {
50
+ throw new Error('An amount requires --network <name>. Use "lightning" to pay a lightning ' +
51
+ "invoice or address (amount in --currency BTC, or a fiat code like USD " +
52
+ "that's converted to sats). Use a chain name (e.g. arbitrum) to pay a " +
53
+ "crypto/stablecoin address on that chain — still paid from your lightning " +
54
+ "wallet, then swapped to the token.");
55
+ }
56
+ const code = currency.toUpperCase();
57
+ const isBtc = code === "BTC";
58
+ const isLightning = network.toLowerCase() === LIGHTNING_NETWORK;
59
+ // --unit is meaningful only for BTC (the one currency with sub-units).
60
+ if (!isBtc && unit !== undefined) {
61
+ throw new Error(`--unit is not valid for --currency ${code} — only BTC has sub-units (sats/BTC). Drop --unit.`);
62
+ }
63
+ if (isLightning) {
64
+ if (isBtc) {
65
+ return { kind: "bitcoin", currency: "BTC", unit: parseUnit(unit), network };
66
+ }
67
+ // Fiat code (or, if mis-routed, a token) — the rate converter validates it.
68
+ return { kind: "fiat", currency: code, network };
69
+ }
70
+ // Chain network → crypto swap rail. BTC has no chain rail today.
71
+ if (isBtc) {
72
+ throw new Error(`--currency BTC is only supported on --network lightning, not "${network}"`);
73
+ }
74
+ // Token symbol (or, if mis-routed, a fiat code) — the catalog validates it.
75
+ return { kind: "crypto", currency: code, network };
76
+ }
77
+ /**
78
+ * Normalize and validate `--unit` for BTC. Case-insensitive; canonical forms
79
+ * are `sats` and `BTC`. Required (no default) because the sats/BTC split is
80
+ * dangerous and must be stated explicitly.
81
+ */
82
+ function parseUnit(unit) {
83
+ if (unit === undefined) {
84
+ throw new Error("--unit <sats|BTC> is required when --currency is BTC (1 BTC = 100,000,000 sats, so the amount can't be guessed)");
85
+ }
86
+ const normalized = unit.toLowerCase();
87
+ if (normalized === "sats")
88
+ return "sats";
89
+ if (normalized === "btc")
90
+ return "BTC";
91
+ throw new Error(`--unit must be "sats" or "BTC" (got "${unit}")`);
92
+ }
93
+ /**
94
+ * Resolve a bitcoin- or fiat-denominated amount to whole sats. For BTC, does
95
+ * the sats/BTC arithmetic and enforces a whole-sat result. For fiat, converts
96
+ * at the live rate and surfaces the resolved sats alongside the original fiat
97
+ * amount. Crypto-token amounts are handled by the swap path, not here.
98
+ */
99
+ export async function resolveToSats({ amount, currency, unit, }) {
100
+ if (currency.toUpperCase() === "BTC") {
101
+ if (unit === "sats") {
102
+ if (!Number.isInteger(amount)) {
103
+ throw new Error(`Amount in sats must be a whole number (got ${amount}). Use --unit BTC for fractional bitcoin.`);
104
+ }
105
+ return { sats: amount };
106
+ }
107
+ // unit === "BTC"
108
+ const raw = amount * SATS_PER_BTC;
109
+ const sats = Math.round(raw);
110
+ if (Math.abs(raw - sats) > 1e-6) {
111
+ throw new Error(`Amount ${amount} BTC is not a whole number of sats (1 BTC = 100,000,000 sats)`);
112
+ }
113
+ if (sats < 1) {
114
+ throw new Error(`Amount ${amount} BTC is less than 1 sat`);
115
+ }
116
+ return { sats };
117
+ }
118
+ // Fiat → sats at the live rate. A non-fiat code (e.g. a token mistakenly put
119
+ // on the lightning rail) surfaces the converter's own rate-lookup error.
120
+ const sats = await getSatoshiValue({ amount, currency });
121
+ return { sats, fiat: { amount, currency: currency.toUpperCase() } };
122
+ }
123
+ /**
124
+ * For commands that can only settle over lightning (invoices, lightning-address
125
+ * / keysend payments): classify the rail, reject a chain (crypto) rail with a
126
+ * pointer to `pay-crypto`, and resolve BTC/fiat to whole sats.
127
+ */
128
+ export async function resolveLightningSats({ amount, currency, unit, network, }) {
129
+ const rail = classifyRail({ currency, unit, network });
130
+ if (rail.kind === "crypto") {
131
+ throw new Error(`--network "${network}" is a chain network for crypto-token payments. ` +
132
+ "This command settles over lightning — use --network lightning with " +
133
+ "--currency BTC (and --unit sats|BTC) or a fiat code (e.g. --currency USD). " +
134
+ "For on-chain crypto, see the pay-crypto command.");
135
+ }
136
+ return resolveToSats({
137
+ amount,
138
+ currency: rail.currency,
139
+ unit: rail.kind === "bitcoin" ? rail.unit : undefined,
140
+ });
141
+ }
@@ -1,5 +1,28 @@
1
+ import { InvalidArgumentError } from "commander";
1
2
  import { fetch402 } from "../tools/lightning/fetch.js";
2
3
  import { getClient, handleError, output } from "../utils.js";
4
+ import { classifyRail } from "../amount.js";
5
+ /**
6
+ * Commander coercion for `--max-amount`, fetch's spend cap. The value is in
7
+ * sats (`--unit` is restricted to sats), so only a positive base-10 whole
8
+ * number is accepted. Unlike `parseInt`, this rejects partial/odd input —
9
+ * `"1abc"`, `"1e3"`, `"1.5"`, `"0x10"`, `"abc"`, `"0"` — instead of silently
10
+ * coercing it (`parseInt("abc")` → `NaN`, `parseInt("0.5")` → `0`), which would
11
+ * weaken the cap.
12
+ */
13
+ function parseMaxAmountSats(value) {
14
+ if (!/^\d+$/.test(value.trim())) {
15
+ throw new InvalidArgumentError(`Sats must be a whole number (got "${value}")`);
16
+ }
17
+ const sats = Number(value);
18
+ if (!Number.isSafeInteger(sats)) {
19
+ throw new InvalidArgumentError(`Sats value is too large (got "${value}")`);
20
+ }
21
+ if (sats === 0) {
22
+ throw new InvalidArgumentError("Sats must be greater than 0");
23
+ }
24
+ return sats;
25
+ }
3
26
  export function registerFetch402Command(program) {
4
27
  program
5
28
  .command("fetch")
@@ -8,15 +31,41 @@ export function registerFetch402Command(program) {
8
31
  .option("-X, --method <method>", "HTTP method (GET, POST, etc.)")
9
32
  .option("-b, --body <json>", "Request body (JSON string)")
10
33
  .option("-H, --headers <json>", "Additional headers (JSON string)")
11
- .option("--max-amount <sats>", "Maximum amount in sats to pay per request. Aborts if the endpoint requests more. (default: 5000, 0 = no limit)", parseInt)
34
+ .option("--max-amount <amount>", "Maximum amount to auto-pay per request. Aborts if the endpoint requests more. " +
35
+ "When set, requires --currency BTC --unit sats --network lightning. (default: 5000 sats)", parseMaxAmountSats)
36
+ .option("--currency <code>", "Denomination of --max-amount — currently must be BTC")
37
+ .option("--unit <sats|BTC>", "Sub-unit of --max-amount — currently must be sats")
38
+ .option("--network <name>", "Rail for --max-amount — currently must be lightning")
39
+ .addHelpText("after", "\nExample:\n" +
40
+ ' $ npx @getalby/cli fetch "https://example.com/api" --max-amount 1000 --currency BTC --unit sats --network lightning\n')
12
41
  .action(async (url, options) => {
13
42
  await handleError(async () => {
43
+ // A cap must state its denomination, like every other amount. For now
44
+ // the only supported rail is BTC/sats over lightning — the cap is
45
+ // inherently a sats spend limit — but it goes through the shared
46
+ // classifier so the surface matches the rest of the CLI and can grow.
47
+ if (options.maxAmount !== undefined) {
48
+ const rail = classifyRail({
49
+ currency: options.currency,
50
+ unit: options.unit,
51
+ network: options.network,
52
+ });
53
+ if (rail.kind !== "bitcoin" || rail.unit !== "sats") {
54
+ throw new Error("fetch's --max-amount spend cap currently supports only " +
55
+ "--currency BTC --unit sats --network lightning");
56
+ }
57
+ }
58
+ else if (options.currency || options.unit || options.network) {
59
+ throw new Error("--currency/--unit/--network only apply together with a positive --max-amount");
60
+ }
14
61
  const client = await getClient(program);
15
62
  const result = await fetch402(client, {
16
63
  url: url,
17
64
  method: options.method,
18
65
  body: options.body,
19
66
  headers: options.headers ? JSON.parse(options.headers) : undefined,
67
+ // --unit is restricted to sats above, so --max-amount is already the
68
+ // sats cap. When omitted, the tool applies its default.
20
69
  maxAmountSats: options.maxAmount,
21
70
  });
22
71
  output(result);
@@ -1,11 +1,12 @@
1
1
  import { fiatToSats } from "../tools/lightning/fiat_to_sats.js";
2
2
  import { handleError, output } from "../utils.js";
3
+ import { parseAmountNumber } from "../amount.js";
3
4
  export function registerFiatToSatsCommand(program) {
4
5
  program
5
6
  .command("fiat-to-sats")
6
7
  .description("Convert fiat to sats")
7
8
  .requiredOption("--currency <code>", "Currency code (e.g., USD, EUR)")
8
- .requiredOption("-a, --amount <n>", "Fiat amount", parseFloat)
9
+ .requiredOption("--amount <n>", "Fiat amount", parseAmountNumber)
9
10
  .action(async (options) => {
10
11
  await handleError(async () => {
11
12
  const result = await fiatToSats({
@@ -1,23 +1,35 @@
1
1
  import { makeHoldInvoice } from "../tools/nwc/make_hold_invoice.js";
2
2
  import { getClient, handleError, output } from "../utils.js";
3
+ import { parseAmountNumber, resolveLightningSats } from "../amount.js";
3
4
  export function registerMakeHoldInvoiceCommand(program) {
4
5
  program
5
6
  .command("make-hold-invoice")
6
7
  .description("Create a HOLD invoice that requires manual settlement")
7
- .requiredOption("-a, --amount <sats>", "Amount in sats", parseInt)
8
+ .requiredOption("--amount <number>", "Invoice amount", parseAmountNumber)
9
+ .requiredOption("--currency <code>", "Denomination: BTC, or a fiat code (USD, EUR, …) converted to sats at the current rate")
10
+ .requiredOption("--network <name>", 'Payment network — must be "lightning" for invoices')
11
+ .option("--unit <sats|BTC>", "Sub-unit (required when --currency is BTC)")
8
12
  .requiredOption("--payment-hash <hex>", "Payment hash (32 bytes hex)")
9
13
  .option("-d, --description <text>", "Invoice description")
10
14
  .option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
15
+ .addHelpText("after", "\nExample:\n" +
16
+ " $ npx @getalby/cli make-hold-invoice --amount 1000 --currency BTC --unit sats --network lightning --payment-hash abc123...\n")
11
17
  .action(async (options) => {
12
18
  await handleError(async () => {
19
+ const resolved = await resolveLightningSats({
20
+ amount: options.amount,
21
+ currency: options.currency,
22
+ unit: options.unit,
23
+ network: options.network,
24
+ });
13
25
  const client = await getClient(program);
14
26
  const result = await makeHoldInvoice(client, {
15
- amount_in_sats: options.amount,
27
+ amount_in_sats: resolved.sats,
16
28
  payment_hash: options.paymentHash,
17
29
  description: options.description,
18
30
  expiry: options.expiry,
19
31
  });
20
- output(result);
32
+ output({ ...result, ...(resolved.fiat && { fiat: resolved.fiat }) });
21
33
  });
22
34
  });
23
35
  }
@@ -1,21 +1,34 @@
1
1
  import { makeInvoice } from "../tools/nwc/make_invoice.js";
2
2
  import { getClient, handleError, output } from "../utils.js";
3
+ import { parseAmountNumber, resolveLightningSats } from "../amount.js";
3
4
  export function registerMakeInvoiceCommand(program) {
4
5
  program
5
6
  .command("make-invoice")
6
7
  .description("Create a lightning invoice")
7
- .requiredOption("-a, --amount <sats>", "Amount in sats", parseInt)
8
+ .requiredOption("--amount <number>", "Invoice amount", parseAmountNumber)
9
+ .requiredOption("--currency <code>", "Denomination: BTC, or a fiat code (USD, EUR, …) converted to sats at the current rate")
10
+ .requiredOption("--network <name>", 'Payment network — must be "lightning" for invoices')
11
+ .option("--unit <sats|BTC>", "Sub-unit (required when --currency is BTC)")
8
12
  .option("-d, --description <text>", "Invoice description")
9
13
  .option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
14
+ .addHelpText("after", "\nExamples:\n" +
15
+ " $ npx @getalby/cli make-invoice --amount 1000 --currency BTC --unit sats --network lightning\n" +
16
+ " $ npx @getalby/cli make-invoice --amount 5 --currency USD --network lightning\n")
10
17
  .action(async (options) => {
11
18
  await handleError(async () => {
19
+ const resolved = await resolveLightningSats({
20
+ amount: options.amount,
21
+ currency: options.currency,
22
+ unit: options.unit,
23
+ network: options.network,
24
+ });
12
25
  const client = await getClient(program);
13
26
  const result = await makeInvoice(client, {
14
- amount_in_sats: options.amount,
27
+ amount_in_sats: resolved.sats,
15
28
  description: options.description,
16
29
  expiry: options.expiry,
17
30
  });
18
- output(result);
31
+ output({ ...result, ...(resolved.fiat && { fiat: resolved.fiat }) });
19
32
  });
20
33
  });
21
34
  }
@@ -1,28 +1,39 @@
1
1
  import { payInvoice } from "../tools/nwc/pay_invoice.js";
2
2
  import { getClient, handleError, output } from "../utils.js";
3
3
  import { isPlausibleEvmAddress, payCrypto, findSupportedPair, } from "../lendaswap/swap.js";
4
+ import { parseAmountNumber, classifyRail } from "../amount.js";
4
5
  export function registerPayCryptoCommand(program) {
5
6
  program
6
7
  .command("pay-crypto")
7
8
  .description("Pay any supported crypto or stablecoin address from your bitcoin lightning wallet.\n\n" +
8
9
  "If the requested currency/network pair isn't supported you'll get an error listing the pairs that are.")
9
10
  .argument("<address>", "Recipient address on the target network")
10
- .requiredOption("-a, --amount <number>", "Amount to send in target-currency units (e.g. 10 = 10 USDC)", Number)
11
+ .requiredOption("--amount <number>", "Amount to send in target-currency units (e.g. 10 = 10 USDC)", parseAmountNumber)
11
12
  .requiredOption("--currency <name>", "Target currency (e.g. USDC)")
12
- .requiredOption("--network <name>", "Target network (chain name or id, e.g. arbitrum / 42161)")
13
+ .requiredOption("--network <name>", "Target chain network (chain name or id, e.g. arbitrum / 42161)")
13
14
  .addHelpText("after", "\nExample:\n" +
14
15
  " $ npx @getalby/cli pay-crypto 0xabc... --amount 10 --currency USDC --network arbitrum\n")
15
16
  .action(async (address, options) => {
16
17
  await handleError(async () => {
17
- if (!Number.isFinite(options.amount) || options.amount <= 0) {
18
- throw new Error(`Invalid --amount: ${options.amount}`);
18
+ // Shared rail classifier: rejects --unit, rejects --network lightning,
19
+ // rejects BTC/fiat on a chain network — leaving only a crypto token on
20
+ // a chain network, which findSupportedPair then validates.
21
+ const rail = classifyRail({
22
+ currency: options.currency,
23
+ unit: options.unit,
24
+ network: options.network,
25
+ });
26
+ if (rail.kind !== "crypto") {
27
+ throw new Error("pay-crypto only sends crypto tokens over a chain network " +
28
+ "(e.g. --currency USDC --network arbitrum). For BTC/fiat over " +
29
+ "lightning, use the pay command.");
19
30
  }
20
31
  if (!isPlausibleEvmAddress(address)) {
21
32
  throw new Error(`Recipient address does not look valid (expected 0x + 40 hex chars): ${address}`);
22
33
  }
23
34
  // Validate the pair against the live Lendaswap catalog before
24
35
  // asking the user for their wallet — fast feedback on typos.
25
- const pair = await findSupportedPair(options.currency, options.network);
36
+ const pair = await findSupportedPair(rail.currency, rail.network);
26
37
  const nwc = await getClient(program);
27
38
  const { swapId } = await payCrypto({
28
39
  pair,
@@ -1,19 +1,37 @@
1
1
  import { payInvoice } from "../tools/nwc/pay_invoice.js";
2
2
  import { getClient, handleError, output } from "../utils.js";
3
+ import { parseAmountNumber, resolveLightningSats } from "../amount.js";
3
4
  export function registerPayInvoiceCommand(program) {
4
5
  program
5
6
  .command("pay-invoice")
6
7
  .description("Pay a lightning invoice")
7
8
  .argument("<bolt11>", "Invoice to pay")
8
- .option("-a, --amount <sats>", "Amount (for zero-amount invoices)", parseInt)
9
+ .option("--amount <number>", "Amount (only for zero-amount invoices)", parseAmountNumber)
10
+ .option("--currency <code>", "Denomination: BTC, or a fiat code (USD, EUR, …) converted to sats at the current rate — required with --amount")
11
+ .option("--network <name>", 'Payment network — must be "lightning" (required with --amount)')
12
+ .option("--unit <sats|BTC>", "Sub-unit (required when --currency is BTC)")
13
+ .addHelpText("after", "\nExample (zero-amount invoice):\n" +
14
+ " $ npx @getalby/cli pay-invoice lnbc1... --amount 1000 --currency BTC --unit sats --network lightning\n")
9
15
  .action(async (invoice, options) => {
10
16
  await handleError(async () => {
17
+ let amountInSats;
18
+ let fiat;
19
+ if (options.amount !== undefined) {
20
+ const resolved = await resolveLightningSats({
21
+ amount: options.amount,
22
+ currency: options.currency,
23
+ unit: options.unit,
24
+ network: options.network,
25
+ });
26
+ amountInSats = resolved.sats;
27
+ fiat = resolved.fiat;
28
+ }
11
29
  const client = await getClient(program);
12
30
  const result = await payInvoice(client, {
13
31
  invoice,
14
- amount_in_sats: options.amount,
32
+ amount_in_sats: amountInSats,
15
33
  });
16
- output(result);
34
+ output({ ...result, ...(fiat && { fiat }) });
17
35
  });
18
36
  });
19
37
  }
@@ -1,15 +1,27 @@
1
1
  import { payKeysend } from "../tools/nwc/pay_keysend.js";
2
2
  import { getClient, handleError, output } from "../utils.js";
3
+ import { parseAmountNumber, resolveLightningSats } from "../amount.js";
3
4
  export function registerPayKeysendCommand(program) {
4
5
  program
5
6
  .command("pay-keysend")
6
7
  .description("Send a keysend payment to a node")
7
8
  .requiredOption("-p, --pubkey <hex>", "Destination node public key")
8
- .requiredOption("-a, --amount <sats>", "Amount in sats", parseInt)
9
+ .requiredOption("--amount <number>", "Amount", parseAmountNumber)
10
+ .requiredOption("--currency <code>", "Denomination: BTC, or a fiat code (USD, EUR, …) converted to sats at the current rate")
11
+ .requiredOption("--network <name>", 'Payment network — must be "lightning"')
12
+ .option("--unit <sats|BTC>", "Sub-unit (required when --currency is BTC)")
9
13
  .option("--preimage <hex>", "Preimage (optional, will be generated if not provided)")
10
14
  .option("--tlv-records <json>", "TLV records as JSON array [{type, value}]")
15
+ .addHelpText("after", "\nExample:\n" +
16
+ " $ npx @getalby/cli pay-keysend -p 02abc... --amount 100 --currency BTC --unit sats --network lightning\n")
11
17
  .action(async (options) => {
12
18
  await handleError(async () => {
19
+ const resolved = await resolveLightningSats({
20
+ amount: options.amount,
21
+ currency: options.currency,
22
+ unit: options.unit,
23
+ network: options.network,
24
+ });
13
25
  const client = await getClient(program);
14
26
  let tlvRecords;
15
27
  if (options.tlvRecords) {
@@ -17,11 +29,15 @@ export function registerPayKeysendCommand(program) {
17
29
  }
18
30
  const result = await payKeysend(client, {
19
31
  pubkey: options.pubkey,
20
- amount_in_sats: options.amount,
32
+ amount_in_sats: resolved.sats,
21
33
  preimage: options.preimage,
22
34
  tlv_records: tlvRecords,
23
35
  });
24
- output(result);
36
+ output({
37
+ ...result,
38
+ amount_in_sats: resolved.sats,
39
+ ...(resolved.fiat && { fiat: resolved.fiat }),
40
+ });
25
41
  });
26
42
  });
27
43
  }