@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 +9 -127
- package/build/amount.js +141 -0
- package/build/commands/fetch.js +50 -1
- package/build/commands/fiat-to-sats.js +2 -1
- package/build/commands/make-hold-invoice.js +15 -3
- package/build/commands/make-invoice.js +16 -3
- package/build/commands/pay-crypto.js +16 -5
- package/build/commands/pay-invoice.js +21 -3
- package/build/commands/pay-keysend.js +19 -3
- package/build/commands/pay.js +91 -42
- package/build/commands/receive.js +28 -12
- package/build/commands/request-invoice-from-lightning-address.js +15 -3
- package/build/commands/sats-to-fiat.js +23 -4
- package/build/index.js +5 -4
- package/build/lendaswap/swap.js +5 -5
- package/build/test/amount-model.test.js +111 -0
- package/build/test/fetch-max-amount.test.js +30 -0
- package/build/test/lightning-tools.test.js +16 -3
- package/build/test/nwc-hold-invoices.test.js +3 -3
- package/build/test/nwc-payments.test.js +3 -3
- package/build/test/pay-command.test.js +63 -16
- package/build/test/pay-crypto.test.js +10 -3
- package/build/test/receive-command.test.js +44 -7
- package/build/tools/lightning/discover.js +1 -1
- package/build/tools/lightning/fetch.js +1 -1
- package/package.json +2 -2
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 -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/build/amount.js
ADDED
|
@@ -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
|
+
}
|
package/build/commands/fetch.js
CHANGED
|
@@ -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 <
|
|
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("
|
|
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("
|
|
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:
|
|
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("
|
|
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:
|
|
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("
|
|
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
|
-
|
|
18
|
-
|
|
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(
|
|
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("
|
|
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:
|
|
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("
|
|
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:
|
|
32
|
+
amount_in_sats: resolved.sats,
|
|
21
33
|
preimage: options.preimage,
|
|
22
34
|
tlv_records: tlvRecords,
|
|
23
35
|
});
|
|
24
|
-
output(
|
|
36
|
+
output({
|
|
37
|
+
...result,
|
|
38
|
+
amount_in_sats: resolved.sats,
|
|
39
|
+
...(resolved.fiat && { fiat: resolved.fiat }),
|
|
40
|
+
});
|
|
25
41
|
});
|
|
26
42
|
});
|
|
27
43
|
}
|