@getalby/cli 0.2.4 → 0.4.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 +76 -62
- package/build/commands/auth.js +66 -0
- package/build/commands/cancel-hold-invoice.js +1 -1
- package/build/commands/connect.js +46 -0
- package/build/commands/{fetch-l402.js → fetch.js} +8 -6
- package/build/commands/get-balance.js +1 -1
- package/build/commands/get-budget.js +1 -1
- package/build/commands/get-info.js +1 -1
- package/build/commands/get-wallet-service-info.js +1 -1
- package/build/commands/list-transactions.js +1 -1
- package/build/commands/lookup-invoice.js +1 -1
- package/build/commands/make-hold-invoice.js +1 -1
- package/build/commands/make-invoice.js +1 -1
- package/build/commands/pay-invoice.js +1 -1
- package/build/commands/pay-keysend.js +1 -1
- package/build/commands/settle-hold-invoice.js +1 -1
- package/build/commands/sign-message.js +1 -1
- package/build/commands/wait-for-payment.js +1 -1
- package/build/index.js +38 -12
- package/build/test/connection-secret.test.js +5 -2
- package/build/tools/lightning/{fetch_l402.js → fetch.js} +10 -11
- package/build/utils.js +101 -12
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -6,17 +6,64 @@ Built for agents - use with the [Alby Bitcoin Payments CLI Skill](https://github
|
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
+
### First-time setup
|
|
10
|
+
|
|
11
|
+
The CLI is an interface to a wallet and therefore needs a connection secret.
|
|
12
|
+
|
|
13
|
+
**Option 1: `auth` — for wallets that support it (e.g. Alby Hub)**
|
|
14
|
+
|
|
9
15
|
```bash
|
|
10
|
-
#
|
|
11
|
-
|
|
16
|
+
# Step 1: generate a connection URL and open it in your wallet to approve
|
|
17
|
+
# --app-name is the name of the agent/app that will use the wallet via the CLI (e.g. "Claude Code", "OpenClaw")
|
|
18
|
+
npx @getalby/cli auth https://my.albyhub.com --app-name "Claude Code"
|
|
12
19
|
|
|
13
|
-
#
|
|
14
|
-
npx @getalby/cli
|
|
20
|
+
# Step 2: after approving in the wallet, complete the connection
|
|
21
|
+
npx @getalby/cli auth --complete
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Option 2: `connect` — paste a NWC connection secret directly**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @getalby/cli connect "nostr+walletconnect://..."
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Multiple wallets
|
|
31
|
+
|
|
32
|
+
Use `--wallet-name` when setting up to save named connections:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @getalby/cli connect "nostr+walletconnect://..." --wallet-name work
|
|
36
|
+
npx @getalby/cli auth https://my.albyhub.com --app-name "Claude Code" --wallet-name personal
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then pass `--wallet-name` to any command to use that wallet:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @getalby/cli --wallet-name work get-balance
|
|
43
|
+
npx @getalby/cli --wallet-name personal pay-invoice --invoice lnbc...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Connection secret resolution (in order of priority)
|
|
47
|
+
|
|
48
|
+
1. `--connection-secret` flag (value or path to file)
|
|
49
|
+
2. `--wallet-name` flag (`~/.alby-cli/connection-secret-<name>.key`)
|
|
50
|
+
3. `NWC_URL` environment variable
|
|
51
|
+
4. `~/.alby-cli/connection-secret.key` (default file location)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Use the default saved wallet connection (preferred)
|
|
55
|
+
npx @getalby/cli <command> [options]
|
|
56
|
+
|
|
57
|
+
# Use a named wallet
|
|
58
|
+
npx @getalby/cli --wallet-name alice <command> [options]
|
|
59
|
+
|
|
60
|
+
# Or pass a connection secret directly
|
|
61
|
+
npx @getalby/cli -c /path/to/secret.txt <command> [options]
|
|
15
62
|
```
|
|
16
63
|
|
|
17
64
|
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)).
|
|
18
65
|
|
|
19
|
-
You can also
|
|
66
|
+
You can also set the `NWC_URL` environment variable instead of using the `-c` option:
|
|
20
67
|
|
|
21
68
|
```txt
|
|
22
69
|
NWC_URL="nostr+walletconnect://..."
|
|
@@ -42,44 +89,50 @@ curl -X POST "https://faucet.nwc.dev/wallets/<username>/topup?amount=5000"
|
|
|
42
89
|
|
|
43
90
|
### Wallet Commands
|
|
44
91
|
|
|
45
|
-
These commands require
|
|
92
|
+
These commands require a wallet connection - either default connection, or specify a custom connection with `-w`, '-c', or `NWC_URL` environment variable:
|
|
46
93
|
|
|
47
94
|
```bash
|
|
48
95
|
# Get wallet balance
|
|
49
|
-
npx @getalby/cli
|
|
96
|
+
npx @getalby/cli get-balance
|
|
50
97
|
|
|
51
98
|
# Get wallet info
|
|
52
|
-
npx @getalby/cli
|
|
99
|
+
npx @getalby/cli get-info
|
|
53
100
|
|
|
54
101
|
# Get wallet service capabilities
|
|
55
|
-
npx @getalby/cli
|
|
102
|
+
npx @getalby/cli get-wallet-service-info
|
|
56
103
|
|
|
57
104
|
# Create an invoice
|
|
58
|
-
npx @getalby/cli
|
|
105
|
+
npx @getalby/cli make-invoice --amount 1000 --description "Payment"
|
|
59
106
|
|
|
60
107
|
# Pay an invoice
|
|
61
|
-
npx @getalby/cli
|
|
108
|
+
npx @getalby/cli pay-invoice --invoice "lnbc..."
|
|
62
109
|
|
|
63
110
|
# Send a keysend payment
|
|
64
|
-
npx @getalby/cli
|
|
111
|
+
npx @getalby/cli pay-keysend --pubkey "02abc..." --amount 100
|
|
65
112
|
|
|
66
113
|
# Look up an invoice by payment hash
|
|
67
|
-
npx @getalby/cli
|
|
114
|
+
npx @getalby/cli lookup-invoice --payment-hash "abc123..."
|
|
68
115
|
|
|
69
116
|
# List transactions
|
|
70
|
-
npx @getalby/cli
|
|
117
|
+
npx @getalby/cli list-transactions --limit 10
|
|
71
118
|
|
|
72
119
|
# Get wallet budget
|
|
73
|
-
npx @getalby/cli
|
|
120
|
+
npx @getalby/cli get-budget
|
|
74
121
|
|
|
75
122
|
# Sign a message
|
|
76
|
-
npx @getalby/cli
|
|
123
|
+
npx @getalby/cli sign-message --message "Hello, World!"
|
|
77
124
|
|
|
78
|
-
# Fetch
|
|
79
|
-
npx @getalby/cli
|
|
125
|
+
# Fetch a payment-protected resource (auto-detects L402, X402, MPP)
|
|
126
|
+
npx @getalby/cli fetch --url "https://example.com/api"
|
|
127
|
+
|
|
128
|
+
# Fetch with custom method, headers, and body
|
|
129
|
+
npx @getalby/cli fetch --url "https://example.com/api" --method POST --body '{"query":"hello"}' --headers '{"Accept":"application/json"}'
|
|
130
|
+
|
|
131
|
+
# Fetch with a custom max amount (default: 5000 sats, 0 = no limit)
|
|
132
|
+
npx @getalby/cli fetch --url "https://example.com/api" --max-amount 1000
|
|
80
133
|
|
|
81
134
|
# Wait for a payment notification
|
|
82
|
-
npx @getalby/cli
|
|
135
|
+
npx @getalby/cli wait-for-payment --payment-hash "abc123..."
|
|
83
136
|
```
|
|
84
137
|
|
|
85
138
|
### HOLD Invoices
|
|
@@ -88,13 +141,13 @@ HOLD invoices allow you to accept payments conditionally - the payment is held u
|
|
|
88
141
|
|
|
89
142
|
```bash
|
|
90
143
|
# Create a HOLD invoice (you provide the payment hash)
|
|
91
|
-
npx @getalby/cli
|
|
144
|
+
npx @getalby/cli make-hold-invoice --amount 1000 --payment-hash "abc123..."
|
|
92
145
|
|
|
93
146
|
# Settle a HOLD invoice (claim the payment)
|
|
94
|
-
npx @getalby/cli
|
|
147
|
+
npx @getalby/cli settle-hold-invoice --preimage "def456..."
|
|
95
148
|
|
|
96
149
|
# Cancel a HOLD invoice (reject the payment)
|
|
97
|
-
npx @getalby/cli
|
|
150
|
+
npx @getalby/cli cancel-hold-invoice --payment-hash "abc123..."
|
|
98
151
|
```
|
|
99
152
|
|
|
100
153
|
### Lightning Tools
|
|
@@ -120,46 +173,7 @@ npx @getalby/cli request-invoice-from-lightning-address --address "hello@getalby
|
|
|
120
173
|
|
|
121
174
|
## Command Reference
|
|
122
175
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
These require `-c` or `--connection-secret`:
|
|
126
|
-
|
|
127
|
-
| Command | Description | Required Options |
|
|
128
|
-
| ------------------------- | ------------------------------ | ------------------------------- |
|
|
129
|
-
| `get-balance` | Get wallet balance | - |
|
|
130
|
-
| `get-info` | Get wallet info | - |
|
|
131
|
-
| `get-wallet-service-info` | Get wallet capabilities | - |
|
|
132
|
-
| `get-budget` | Get wallet budget | - |
|
|
133
|
-
| `make-invoice` | Create a lightning invoice | `--amount` |
|
|
134
|
-
| `pay-invoice` | Pay a lightning invoice | `--invoice` |
|
|
135
|
-
| `pay-keysend` | Send a keysend payment | `--pubkey`, `--amount` |
|
|
136
|
-
| `lookup-invoice` | Look up an invoice | `--payment-hash` or `--invoice` |
|
|
137
|
-
| `list-transactions` | List transactions | - |
|
|
138
|
-
| `sign-message` | Sign a message with wallet key | `--message` |
|
|
139
|
-
| `wait-for-payment` | Wait for payment notification | `--payment-hash` |
|
|
140
|
-
| `fetch-l402` | Fetch L402-protected resource | `--url` |
|
|
141
|
-
|
|
142
|
-
### HOLD Invoice Commands
|
|
143
|
-
|
|
144
|
-
These require `-c` or `--connection-secret`:
|
|
145
|
-
|
|
146
|
-
| Command | Description | Required Options |
|
|
147
|
-
| --------------------- | --------------------- | ---------------------------- |
|
|
148
|
-
| `make-hold-invoice` | Create a HOLD invoice | `--amount`, `--payment-hash` |
|
|
149
|
-
| `settle-hold-invoice` | Settle a HOLD invoice | `--preimage` |
|
|
150
|
-
| `cancel-hold-invoice` | Cancel a HOLD invoice | `--payment-hash` |
|
|
151
|
-
|
|
152
|
-
### Lightning Tools
|
|
153
|
-
|
|
154
|
-
These don't require a wallet connection:
|
|
155
|
-
|
|
156
|
-
| Command | Description | Required Options |
|
|
157
|
-
| ---------------------------------------- | -------------------------------------- | ------------------------- |
|
|
158
|
-
| `fiat-to-sats` | Convert fiat to sats | `--currency`, `--amount` |
|
|
159
|
-
| `sats-to-fiat` | Convert sats to fiat | `--amount`, `--currency` |
|
|
160
|
-
| `parse-invoice` | Parse a BOLT-11 invoice | `--invoice` |
|
|
161
|
-
| `verify-preimage` | Verify preimage against invoice | `--invoice`, `--preimage` |
|
|
162
|
-
| `request-invoice-from-lightning-address` | Request invoice from lightning address | `--address`, `--amount` |
|
|
176
|
+
Run `npx @getalby/cli help` for a full list of commands and possible arguments.
|
|
163
177
|
|
|
164
178
|
## Output
|
|
165
179
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NWCClient } from "@getalby/sdk";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getConnectionSecretPath, getPendingConnectionRelayPath, getPendingConnectionSecretPath, handleError, } from "../utils.js";
|
|
6
|
+
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
|
7
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
8
|
+
export function registerAuthCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("auth <wallet-url>")
|
|
11
|
+
.description("Securely connect a wallet with human confirmation via the browser\n\n" +
|
|
12
|
+
" Step 1: npx @getalby/cli auth https://my.albyhub.com --app-name MyApp\n" +
|
|
13
|
+
" Step 2: after human confirmation, run any command to finalize the connection")
|
|
14
|
+
.option("--app-name <name>", 'Name of the agent or app that will use this wallet (e.g. "Claude Code")')
|
|
15
|
+
.option("--relay-url <url>", "Relay URL for the pending connection", "wss://relay.getalby.com/v1")
|
|
16
|
+
.option("--force", "Overwrite existing connection secret")
|
|
17
|
+
.option("--remove-pending", "Remove a pending connection and start fresh")
|
|
18
|
+
.action(async (walletUrl, options) => {
|
|
19
|
+
await handleError(async () => {
|
|
20
|
+
const walletName = program.opts().walletName;
|
|
21
|
+
const connectionSecretPath = getConnectionSecretPath(walletName);
|
|
22
|
+
const pendingSecretPath = getPendingConnectionSecretPath(walletName);
|
|
23
|
+
const pendingRelayPath = getPendingConnectionRelayPath(walletName);
|
|
24
|
+
// Remove pending connection
|
|
25
|
+
if (options.removePending) {
|
|
26
|
+
if (!existsSync(pendingSecretPath)) {
|
|
27
|
+
console.log(`No pending connection found at ${pendingSecretPath}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
rmSync(pendingSecretPath);
|
|
31
|
+
console.log(`Removed pending connection at ${pendingSecretPath}`);
|
|
32
|
+
}
|
|
33
|
+
if (existsSync(pendingRelayPath)) {
|
|
34
|
+
rmSync(pendingRelayPath);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Generate auth URL
|
|
39
|
+
if (!options.appName) {
|
|
40
|
+
console.error(`Error: No app name provided.\n` +
|
|
41
|
+
`Add --app-name <name> to identify the app in the wallet.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(connectionSecretPath) && !options.force) {
|
|
45
|
+
console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
|
|
46
|
+
`To overwrite, use --force.`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const secret = bytesToHex(generateSecretKey());
|
|
50
|
+
const pubkey = getPublicKey(hexToBytes(secret));
|
|
51
|
+
const authUrl = NWCClient.getAuthorizationUrl(`${walletUrl}/apps/new`, { name: options.appName }, pubkey).toString();
|
|
52
|
+
const dir = join(homedir(), ".alby-cli");
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(pendingSecretPath, secret, { mode: 0o600 });
|
|
57
|
+
writeFileSync(pendingRelayPath, options.relayUrl, { mode: 0o600 });
|
|
58
|
+
console.log("Click the following URL to approve the connection in your wallet:\n" +
|
|
59
|
+
authUrl);
|
|
60
|
+
const retryCmd = walletName
|
|
61
|
+
? `npx @getalby/cli get-balance --wallet-name ${walletName}`
|
|
62
|
+
: `npx @getalby/cli get-balance`;
|
|
63
|
+
console.log(`\nOnce approved, run any command, e.g.:\n ${retryCmd}`);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -7,7 +7,7 @@ export function registerCancelHoldInvoiceCommand(program) {
|
|
|
7
7
|
.requiredOption("--payment-hash <hex>", "Payment hash (32 bytes hex)")
|
|
8
8
|
.action(async (options) => {
|
|
9
9
|
await handleError(async () => {
|
|
10
|
-
const client = getClient(program);
|
|
10
|
+
const client = await getClient(program);
|
|
11
11
|
const result = await cancelHoldInvoice(client, {
|
|
12
12
|
payment_hash: options.paymentHash,
|
|
13
13
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NWCClient } from "@getalby/sdk";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { getConnectionSecretPath, handleError, saveConnectionSecret, testAndLogConnection, } from "../utils.js";
|
|
4
|
+
export function registerConnectCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('connect "[connection-secret]"')
|
|
7
|
+
.description("Connect to a Nostr Wallet Connect wallet")
|
|
8
|
+
.option("--force", "Overwrite existing connection secret")
|
|
9
|
+
.action(async (connectionSecret, options) => {
|
|
10
|
+
await handleError(async () => {
|
|
11
|
+
const connectionSecretPath = getConnectionSecretPath(program.opts().walletName);
|
|
12
|
+
if (existsSync(connectionSecretPath) && !options.force) {
|
|
13
|
+
console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
|
|
14
|
+
`To overwrite, use --force.`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
if (!connectionSecret) {
|
|
18
|
+
console.error(`Usage: npx @getalby/cli connect "<connection-secret>"\n` +
|
|
19
|
+
`Provide a NWC connection secret (nostr+walletconnect://...)`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (!connectionSecret.startsWith("nostr+walletconnect://")) {
|
|
23
|
+
console.error(`Error: Invalid connection secret. Expected format: nostr+walletconnect://...`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const client = new NWCClient({
|
|
27
|
+
nostrWalletConnectUrl: connectionSecret,
|
|
28
|
+
});
|
|
29
|
+
if (!client.secret || !/^[0-9a-f]{64}$/i.test(client.secret)) {
|
|
30
|
+
console.error(`Error: Invalid connection secret. Missing or invalid secret key.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
if (!client.walletPubkey ||
|
|
34
|
+
!/^[0-9a-f]{64}$/i.test(client.walletPubkey)) {
|
|
35
|
+
console.error(`Error: Invalid connection secret. Missing or invalid wallet pubkey.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (!client.relayUrls?.length) {
|
|
39
|
+
console.error(`Error: Invalid connection secret. Missing relay URL.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
await testAndLogConnection(client);
|
|
43
|
+
saveConnectionSecret(connectionSecretPath, connectionSecret, program.opts().verbose);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetch402 } from "../tools/lightning/fetch.js";
|
|
2
2
|
import { getClient, handleError, output } from "../utils.js";
|
|
3
|
-
export function
|
|
3
|
+
export function registerFetch402Command(program) {
|
|
4
4
|
program
|
|
5
|
-
.command("fetch
|
|
6
|
-
.description("Fetch
|
|
5
|
+
.command("fetch")
|
|
6
|
+
.description("Fetch a payment-protected resource (auto-detects L402, X402, MPP)")
|
|
7
7
|
.requiredOption("-u, --url <url>", "URL to fetch")
|
|
8
8
|
.option("-m, --method <method>", "HTTP method (GET, POST, etc.)")
|
|
9
9
|
.option("-b, --body <json>", "Request body (JSON string)")
|
|
10
10
|
.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)
|
|
11
12
|
.action(async (options) => {
|
|
12
13
|
await handleError(async () => {
|
|
13
|
-
const client = getClient(program);
|
|
14
|
-
const result = await
|
|
14
|
+
const client = await getClient(program);
|
|
15
|
+
const result = await fetch402(client, {
|
|
15
16
|
url: options.url,
|
|
16
17
|
method: options.method,
|
|
17
18
|
body: options.body,
|
|
18
19
|
headers: options.headers ? JSON.parse(options.headers) : undefined,
|
|
20
|
+
maxAmountSats: options.maxAmount,
|
|
19
21
|
});
|
|
20
22
|
output(result);
|
|
21
23
|
});
|
|
@@ -6,7 +6,7 @@ export function registerGetBalanceCommand(program) {
|
|
|
6
6
|
.description("Get wallet balance")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getBalance(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -6,7 +6,7 @@ export function registerGetBudgetCommand(program) {
|
|
|
6
6
|
.description("Get wallet budget information")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getBudget(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -6,7 +6,7 @@ export function registerGetInfoCommand(program) {
|
|
|
6
6
|
.description("Get wallet info")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getInfo(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -6,7 +6,7 @@ export function registerGetWalletServiceInfoCommand(program) {
|
|
|
6
6
|
.description("Get wallet service capabilities")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getWalletServiceInfo(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -12,7 +12,7 @@ export function registerListTransactionsCommand(program) {
|
|
|
12
12
|
.option("-t, --type <type>", "Filter by type (incoming|outgoing)")
|
|
13
13
|
.action(async (options) => {
|
|
14
14
|
await handleError(async () => {
|
|
15
|
-
const client = getClient(program);
|
|
15
|
+
const client = await getClient(program);
|
|
16
16
|
const result = await listTransactions(client, {
|
|
17
17
|
from: options.from,
|
|
18
18
|
until: options.until,
|
|
@@ -12,7 +12,7 @@ export function registerLookupInvoiceCommand(program) {
|
|
|
12
12
|
console.error("Error: --payment-hash or --invoice is required");
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
-
const client = getClient(program);
|
|
15
|
+
const client = await getClient(program);
|
|
16
16
|
const result = await lookupInvoice(client, {
|
|
17
17
|
payment_hash: options.paymentHash,
|
|
18
18
|
invoice: options.invoice,
|
|
@@ -10,7 +10,7 @@ export function registerMakeHoldInvoiceCommand(program) {
|
|
|
10
10
|
.option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
|
|
11
11
|
.action(async (options) => {
|
|
12
12
|
await handleError(async () => {
|
|
13
|
-
const client = getClient(program);
|
|
13
|
+
const client = await getClient(program);
|
|
14
14
|
const result = await makeHoldInvoice(client, {
|
|
15
15
|
amount_in_sats: options.amount,
|
|
16
16
|
payment_hash: options.paymentHash,
|
|
@@ -9,7 +9,7 @@ export function registerMakeInvoiceCommand(program) {
|
|
|
9
9
|
.option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
|
|
10
10
|
.action(async (options) => {
|
|
11
11
|
await handleError(async () => {
|
|
12
|
-
const client = getClient(program);
|
|
12
|
+
const client = await getClient(program);
|
|
13
13
|
const result = await makeInvoice(client, {
|
|
14
14
|
amount_in_sats: options.amount,
|
|
15
15
|
description: options.description,
|
|
@@ -8,7 +8,7 @@ export function registerPayInvoiceCommand(program) {
|
|
|
8
8
|
.option("-a, --amount <sats>", "Amount (for zero-amount invoices)", parseInt)
|
|
9
9
|
.action(async (options) => {
|
|
10
10
|
await handleError(async () => {
|
|
11
|
-
const client = getClient(program);
|
|
11
|
+
const client = await getClient(program);
|
|
12
12
|
const result = await payInvoice(client, {
|
|
13
13
|
invoice: options.invoice,
|
|
14
14
|
amount_in_sats: options.amount,
|
|
@@ -10,7 +10,7 @@ export function registerPayKeysendCommand(program) {
|
|
|
10
10
|
.option("--tlv-records <json>", "TLV records as JSON array [{type, value}]")
|
|
11
11
|
.action(async (options) => {
|
|
12
12
|
await handleError(async () => {
|
|
13
|
-
const client = getClient(program);
|
|
13
|
+
const client = await getClient(program);
|
|
14
14
|
let tlvRecords;
|
|
15
15
|
if (options.tlvRecords) {
|
|
16
16
|
tlvRecords = JSON.parse(options.tlvRecords);
|
|
@@ -7,7 +7,7 @@ export function registerSettleHoldInvoiceCommand(program) {
|
|
|
7
7
|
.requiredOption("--preimage <hex>", "Preimage (32 bytes hex)")
|
|
8
8
|
.action(async (options) => {
|
|
9
9
|
await handleError(async () => {
|
|
10
|
-
const client = getClient(program);
|
|
10
|
+
const client = await getClient(program);
|
|
11
11
|
const result = await settleHoldInvoice(client, {
|
|
12
12
|
preimage: options.preimage,
|
|
13
13
|
});
|
|
@@ -7,7 +7,7 @@ export function registerSignMessageCommand(program) {
|
|
|
7
7
|
.requiredOption("-m, --message <text>", "Message to sign")
|
|
8
8
|
.action(async (options) => {
|
|
9
9
|
await handleError(async () => {
|
|
10
|
-
const client = getClient(program);
|
|
10
|
+
const client = await getClient(program);
|
|
11
11
|
const result = await signMessage(client, {
|
|
12
12
|
message: options.message,
|
|
13
13
|
});
|
|
@@ -9,7 +9,7 @@ export function registerWaitForPaymentCommand(program) {
|
|
|
9
9
|
.option("--timeout <seconds>", "Timeout in seconds", parseInt)
|
|
10
10
|
.action(async (options) => {
|
|
11
11
|
await handleError(async () => {
|
|
12
|
-
const client = getClient(program);
|
|
12
|
+
const client = await getClient(program);
|
|
13
13
|
const result = await waitForPayment(client, {
|
|
14
14
|
payment_hash: options.paymentHash,
|
|
15
15
|
type: options.type,
|
package/build/index.js
CHANGED
|
@@ -19,38 +19,64 @@ import { registerSatsToFiatCommand } from "./commands/sats-to-fiat.js";
|
|
|
19
19
|
import { registerParseInvoiceCommand } from "./commands/parse-invoice.js";
|
|
20
20
|
import { registerVerifyPreimageCommand } from "./commands/verify-preimage.js";
|
|
21
21
|
import { registerRequestInvoiceFromLightningAddressCommand } from "./commands/request-invoice-from-lightning-address.js";
|
|
22
|
-
import {
|
|
22
|
+
import { registerFetch402Command } from "./commands/fetch.js";
|
|
23
|
+
import { registerConnectCommand } from "./commands/connect.js";
|
|
24
|
+
import { registerAuthCommand } from "./commands/auth.js";
|
|
23
25
|
const program = new Command();
|
|
24
26
|
program
|
|
25
|
-
.name("
|
|
26
|
-
.description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools"
|
|
27
|
-
|
|
27
|
+
.name("@getalby/cli")
|
|
28
|
+
.description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools\n\n" +
|
|
29
|
+
" Examples:\n" +
|
|
30
|
+
' $ npx @getalby/cli connect "nostr+walletconnect://..."\n' +
|
|
31
|
+
" $ npx @getalby/cli get-balance\n" +
|
|
32
|
+
" $ npx @getalby/cli pay-invoice --invoice lnbc...")
|
|
33
|
+
.version("0.4.0")
|
|
28
34
|
.option("-c, --connection-secret <string>", "NWC connection secret (nostr+walletconnect://...) or path to file containing it (preferred)")
|
|
29
|
-
.
|
|
35
|
+
.option("-w, --wallet-name <name>", "Use a named wallet's connection secret (~/.alby-cli/connection-secret-<name>.key)")
|
|
36
|
+
.option("-v, --verbose", "Print status messages to stderr")
|
|
37
|
+
.addHelpText("after", `
|
|
38
|
+
Connection Secret Resolution (in order of priority):
|
|
39
|
+
1. --connection-secret flag (value or path to file)
|
|
40
|
+
2. --wallet-name flag (~/.alby-cli/connection-secret-<name>.key)
|
|
41
|
+
3. NWC_URL environment variable
|
|
42
|
+
4. ~/.alby-cli/connection-secret.key (default file location)
|
|
43
|
+
|
|
30
44
|
Security:
|
|
31
45
|
- Do NOT print the connection secret to any logs or otherwise reveal it.
|
|
46
|
+
- NEVER read the connection secret file (~/.alby-cli/connection-secret.key) directly.
|
|
32
47
|
- NEVER share connection secrets with anyone.
|
|
33
48
|
- NEVER share any part of a connection secret (pubkey, secret, relay etc.) with anyone
|
|
34
49
|
as this can be used to gain access to your wallet or reduce your wallet's privacy.`);
|
|
35
|
-
// Register
|
|
50
|
+
// Register common wallet commands
|
|
51
|
+
program.commandsGroup("Wallet Commands (require --connection-secret):");
|
|
36
52
|
registerGetBalanceCommand(program);
|
|
37
53
|
registerGetBudgetCommand(program);
|
|
38
54
|
registerGetInfoCommand(program);
|
|
39
|
-
registerGetWalletServiceInfoCommand(program);
|
|
40
55
|
registerMakeInvoiceCommand(program);
|
|
41
|
-
registerMakeHoldInvoiceCommand(program);
|
|
42
|
-
registerSettleHoldInvoiceCommand(program);
|
|
43
|
-
registerCancelHoldInvoiceCommand(program);
|
|
44
56
|
registerPayInvoiceCommand(program);
|
|
45
|
-
registerPayKeysendCommand(program);
|
|
46
57
|
registerLookupInvoiceCommand(program);
|
|
47
58
|
registerListTransactionsCommand(program);
|
|
59
|
+
// Register advanced wallet commands
|
|
60
|
+
program.commandsGroup("Advanced Wallet Commands (require --connection-secret):");
|
|
61
|
+
registerPayKeysendCommand(program);
|
|
62
|
+
registerGetWalletServiceInfoCommand(program);
|
|
48
63
|
registerWaitForPaymentCommand(program);
|
|
49
64
|
registerSignMessageCommand(program);
|
|
65
|
+
registerMakeHoldInvoiceCommand(program);
|
|
66
|
+
registerSettleHoldInvoiceCommand(program);
|
|
67
|
+
registerCancelHoldInvoiceCommand(program);
|
|
68
|
+
// Register lightning tool commands
|
|
69
|
+
program.commandsGroup("Lightning Tools (no --connection-secret required):");
|
|
50
70
|
registerFiatToSatsCommand(program);
|
|
51
71
|
registerSatsToFiatCommand(program);
|
|
52
72
|
registerParseInvoiceCommand(program);
|
|
53
73
|
registerVerifyPreimageCommand(program);
|
|
54
74
|
registerRequestInvoiceFromLightningAddressCommand(program);
|
|
55
|
-
|
|
75
|
+
// Register fetch command for payment-protected resources
|
|
76
|
+
program.commandsGroup("HTTP 402 Payments (require --connection-secret):");
|
|
77
|
+
registerFetch402Command(program);
|
|
78
|
+
// Register setup commands
|
|
79
|
+
program.commandsGroup("Setup:");
|
|
80
|
+
registerAuthCommand(program);
|
|
81
|
+
registerConnectCommand(program);
|
|
56
82
|
program.parse();
|
|
@@ -45,9 +45,12 @@ describe("Connection Secret Handling", () => {
|
|
|
45
45
|
expect(result.output.error).toContain("Invalid connection secret");
|
|
46
46
|
});
|
|
47
47
|
test("errors when no connection secret provided", () => {
|
|
48
|
-
const result = runCli("get-balance"
|
|
48
|
+
const result = runCli("get-balance", {
|
|
49
|
+
HOME: "/nonexistent-test-home",
|
|
50
|
+
NWC_URL: "",
|
|
51
|
+
});
|
|
49
52
|
expect(result.success).toBe(false);
|
|
50
|
-
expect(result.output.error).toContain("
|
|
53
|
+
expect(result.output.error).toContain("No connection secret provided");
|
|
51
54
|
});
|
|
52
55
|
test("errors when connection string is malformed", () => {
|
|
53
56
|
const result = runCli(`-c "nostr+walletconnect://asdf" get-balance`);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { fetch402 as fetch402Lib } from "@getalby/lightning-tools/402";
|
|
2
|
+
const DEFAULT_MAX_AMOUNT_SATS = 5000;
|
|
3
|
+
export async function fetch402(client, params) {
|
|
4
|
+
const method = params.method?.toUpperCase();
|
|
3
5
|
const requestOptions = {
|
|
4
|
-
method
|
|
6
|
+
method,
|
|
5
7
|
};
|
|
6
|
-
if (
|
|
8
|
+
if (method && method !== "GET" && method !== "HEAD") {
|
|
7
9
|
requestOptions.body = params.body;
|
|
8
10
|
requestOptions.headers = {
|
|
9
11
|
"Content-Type": "application/json",
|
|
@@ -13,13 +15,10 @@ export async function fetchL402(client, params) {
|
|
|
13
15
|
else if (params.headers) {
|
|
14
16
|
requestOptions.headers = params.headers;
|
|
15
17
|
}
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return { preimage: result.preimage };
|
|
21
|
-
},
|
|
22
|
-
},
|
|
18
|
+
const maxAmountSats = params.maxAmountSats ?? DEFAULT_MAX_AMOUNT_SATS;
|
|
19
|
+
const result = await fetch402Lib(params.url, requestOptions, {
|
|
20
|
+
wallet: client,
|
|
21
|
+
maxAmount: maxAmountSats || undefined,
|
|
23
22
|
});
|
|
24
23
|
const responseContent = await result.text();
|
|
25
24
|
if (!result.ok) {
|
package/build/utils.js
CHANGED
|
@@ -1,30 +1,119 @@
|
|
|
1
|
-
import { NWCClient } from "@getalby/sdk";
|
|
2
|
-
import {
|
|
1
|
+
import { NWAClient, NWCClient } from "@getalby/sdk";
|
|
2
|
+
import { getInfo } from "./tools/nwc/get_info.js";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
+
function sanitizeWalletName(name) {
|
|
7
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
8
|
+
}
|
|
9
|
+
export function getConnectionSecretPath(name) {
|
|
10
|
+
const filename = name
|
|
11
|
+
? `connection-secret-${sanitizeWalletName(name)}.key`
|
|
12
|
+
: "connection-secret.key";
|
|
13
|
+
return join(homedir(), ".alby-cli", filename);
|
|
14
|
+
}
|
|
15
|
+
export function getPendingConnectionSecretPath(name) {
|
|
16
|
+
const filename = name
|
|
17
|
+
? `pending-connection-secret-${sanitizeWalletName(name)}.key`
|
|
18
|
+
: "pending-connection-secret.key";
|
|
19
|
+
return join(homedir(), ".alby-cli", filename);
|
|
20
|
+
}
|
|
21
|
+
export function getPendingConnectionRelayPath(name) {
|
|
22
|
+
const filename = name
|
|
23
|
+
? `pending-connection-relay-${sanitizeWalletName(name)}.txt`
|
|
24
|
+
: "pending-connection-relay.txt";
|
|
25
|
+
return join(homedir(), ".alby-cli", filename);
|
|
26
|
+
}
|
|
27
|
+
export function saveConnectionSecret(path, secret, verbose) {
|
|
28
|
+
const alreadyExists = existsSync(path);
|
|
29
|
+
const dir = join(homedir(), ".alby-cli");
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
writeFileSync(path, secret, { mode: 0o600 });
|
|
34
|
+
if (alreadyExists) {
|
|
35
|
+
chmodSync(path, 0o600);
|
|
36
|
+
}
|
|
37
|
+
if (verbose) {
|
|
38
|
+
console.error(`Connection saved to ${path}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function testAndLogConnection(client) {
|
|
42
|
+
console.log("Testing connection...");
|
|
43
|
+
const info = await getInfo(client);
|
|
44
|
+
console.log(`Connected to ${info.alias || "wallet"} (${info.network || "unknown network"})`);
|
|
45
|
+
}
|
|
46
|
+
export async function completePendingConnection(pendingSecretPath, connectionSecretPath, relayUrl, verbose, pendingRelayPath) {
|
|
47
|
+
const secret = readFileSync(pendingSecretPath, "utf-8").trim();
|
|
48
|
+
const DEFAULT_RELAY = "wss://relay.getalby.com/v1";
|
|
49
|
+
if (!relayUrl && pendingRelayPath && existsSync(pendingRelayPath)) {
|
|
50
|
+
relayUrl = readFileSync(pendingRelayPath, "utf-8").trim();
|
|
51
|
+
}
|
|
52
|
+
const resolvedRelay = relayUrl ?? DEFAULT_RELAY;
|
|
53
|
+
const nwaClient = new NWAClient({
|
|
54
|
+
appSecretKey: secret,
|
|
55
|
+
relayUrls: [resolvedRelay],
|
|
56
|
+
requestMethods: [],
|
|
57
|
+
});
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let settled = false;
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
unsub?.();
|
|
65
|
+
reject(new Error("Timed out waiting for wallet approval.\n\nTo retry, run the command again.\nTo cancel: npx @getalby/cli auth --remove-pending"));
|
|
66
|
+
}, 5000);
|
|
67
|
+
let unsub;
|
|
68
|
+
nwaClient
|
|
69
|
+
.subscribe({
|
|
70
|
+
onSuccess: async (nwcClient) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
unsub?.();
|
|
76
|
+
saveConnectionSecret(connectionSecretPath, nwcClient.getNostrWalletConnectUrl(), verbose);
|
|
77
|
+
rmSync(pendingSecretPath);
|
|
78
|
+
if (pendingRelayPath && existsSync(pendingRelayPath)) {
|
|
79
|
+
rmSync(pendingRelayPath);
|
|
80
|
+
}
|
|
81
|
+
resolve(nwcClient);
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.then(({ unsub: u }) => {
|
|
85
|
+
unsub = u;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function getClient(program) {
|
|
6
90
|
const opts = program.opts();
|
|
7
91
|
let connectionSecret = opts.connectionSecret;
|
|
8
|
-
|
|
9
|
-
|
|
92
|
+
const walletName = opts.walletName;
|
|
93
|
+
const connectionPath = getConnectionSecretPath(walletName);
|
|
94
|
+
const pendingPath = getPendingConnectionSecretPath(walletName);
|
|
95
|
+
if (!connectionSecret && !walletName) {
|
|
10
96
|
connectionSecret = process.env.NWC_URL;
|
|
11
97
|
}
|
|
12
98
|
if (!connectionSecret) {
|
|
13
|
-
const defaultPath = join(homedir(), ".alby-cli", "connection-secret.key");
|
|
14
99
|
try {
|
|
15
|
-
connectionSecret = readFileSync(
|
|
100
|
+
connectionSecret = readFileSync(connectionPath, "utf-8").trim();
|
|
16
101
|
}
|
|
17
102
|
catch (error) {
|
|
18
103
|
const err = error;
|
|
19
|
-
if (err.code !== "ENOENT")
|
|
20
|
-
// only throw an error if it's not a file not found error
|
|
104
|
+
if (err.code !== "ENOENT")
|
|
21
105
|
throw err;
|
|
22
|
-
}
|
|
23
106
|
}
|
|
24
107
|
}
|
|
108
|
+
if (!connectionSecret && existsSync(pendingPath)) {
|
|
109
|
+
if (opts.verbose) {
|
|
110
|
+
console.error("Pending connection found. Waiting for wallet approval...");
|
|
111
|
+
}
|
|
112
|
+
const pendingRelayPath = getPendingConnectionRelayPath(walletName);
|
|
113
|
+
return await completePendingConnection(pendingPath, connectionPath, undefined, opts.verbose, pendingRelayPath);
|
|
114
|
+
}
|
|
25
115
|
if (!connectionSecret) {
|
|
26
|
-
|
|
27
|
-
process.exit(1);
|
|
116
|
+
throw new Error("No connection secret provided. Pass -c <secret or file path>, set NWC_URL, use --wallet-name <name>, or create ~/.alby-cli/connection-secret.key");
|
|
28
117
|
}
|
|
29
118
|
// Auto-detect: if it doesn't start with the protocol, treat as file path
|
|
30
119
|
if (!connectionSecret.startsWith("nostr+walletconnect://")) {
|
package/package.json
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
"name": "@getalby/cli",
|
|
3
3
|
"description": "CLI for Nostr Wallet Connect (NIP-47) with a few additional useful lightning tools",
|
|
4
4
|
"repository": "https://github.com/getAlby/cli.git",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "build/index.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"cli": "build/index.js"
|
|
10
|
-
"alby-cli": "build/index.js"
|
|
9
|
+
"cli": "build/index.js"
|
|
11
10
|
},
|
|
12
11
|
"files": [
|
|
13
12
|
"build/**/*"
|
|
@@ -33,10 +32,15 @@
|
|
|
33
32
|
],
|
|
34
33
|
"author": "Alby contributors",
|
|
35
34
|
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
36
38
|
"dependencies": {
|
|
37
|
-
"@getalby/lightning-tools": "^
|
|
39
|
+
"@getalby/lightning-tools": "^8.0.0",
|
|
38
40
|
"@getalby/sdk": "^7.0.0",
|
|
39
|
-
"
|
|
41
|
+
"@noble/hashes": "^2.0.1",
|
|
42
|
+
"commander": "^14.0.3",
|
|
43
|
+
"nostr-tools": "^2.23.3"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
46
|
"@types/node": "^25.2.0",
|