@getalby/cli 0.7.0 → 0.9.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.
@@ -11,7 +11,7 @@ describe("NWC HOLD Invoice Commands", () => {
11
11
  }, 60000);
12
12
  test("make-hold-invoice creates hold invoice", () => {
13
13
  const { paymentHash } = generateHoldInvoiceParams();
14
- const result = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
14
+ const result = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice --amount 100 --currency BTC --unit sats --network lightning --payment-hash "${paymentHash}"`);
15
15
  expect(result.success).toBe(true);
16
16
  expect(result.output.invoice).toBeDefined();
17
17
  expect(result.output.payment_hash).toBe(paymentHash);
@@ -19,7 +19,7 @@ describe("NWC HOLD Invoice Commands", () => {
19
19
  test("settle-hold-invoice settles with preimage", async () => {
20
20
  const { preimage, paymentHash } = generateHoldInvoiceParams();
21
21
  // Create a hold invoice
22
- const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
22
+ const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice --amount 100 --currency BTC --unit sats --network lightning --payment-hash "${paymentHash}"`);
23
23
  expect(holdResult.success).toBe(true);
24
24
  // Start wait-for-payment in background
25
25
  const waitProcess = spawn("node", [
@@ -55,7 +55,7 @@ describe("NWC HOLD Invoice Commands", () => {
55
55
  test("cancel-hold-invoice cancels hold invoice", async () => {
56
56
  const { paymentHash } = generateHoldInvoiceParams();
57
57
  // Create a hold invoice
58
- const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
58
+ const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice --amount 100 --currency BTC --unit sats --network lightning --payment-hash "${paymentHash}"`);
59
59
  expect(holdResult.success).toBe(true);
60
60
  // Pay the invoice from sender (this will put it in held state)
61
61
  const payProcess = spawn("node", [
@@ -10,7 +10,7 @@ describe("NWC Payment Commands", () => {
10
10
  }, 60000);
11
11
  test("make-invoice and pay-invoice", () => {
12
12
  // Create invoice with receiver wallet
13
- const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 100`);
13
+ const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice --amount 100 --currency BTC --unit sats --network lightning`);
14
14
  expect(invoiceResult.success).toBe(true);
15
15
  expect(invoiceResult.output.invoice).toBeDefined();
16
16
  // Pay with sender wallet
@@ -20,7 +20,7 @@ describe("NWC Payment Commands", () => {
20
20
  });
21
21
  test("lookup-invoice finds paid invoice", () => {
22
22
  // Create an invoice
23
- const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 50`);
23
+ const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice --amount 50 --currency BTC --unit sats --network lightning`);
24
24
  expect(invoiceResult.success).toBe(true);
25
25
  // Pay the invoice first (unpaid invoices may not be found)
26
26
  const payResult = runCli(`-c "${sender.nwcUrl}" pay-invoice "${invoiceResult.output.invoice}"`);
@@ -35,7 +35,7 @@ describe("NWC Payment Commands", () => {
35
35
  const infoResult = runCli(`-c "${receiver.nwcUrl}" get-info`);
36
36
  expect(infoResult.success).toBe(true);
37
37
  // Send keysend payment
38
- const keysendResult = runCli(`-c "${sender.nwcUrl}" pay-keysend -p "${infoResult.output.pubkey}" -a 100`);
38
+ const keysendResult = runCli(`-c "${sender.nwcUrl}" pay-keysend -p "${infoResult.output.pubkey}" --amount 100 --currency BTC --unit sats --network lightning`);
39
39
  expect(keysendResult.success).toBe(true);
40
40
  expect(keysendResult.output.preimage).toBeDefined();
41
41
  });
@@ -1,5 +1,7 @@
1
1
  import { describe, test, expect, beforeAll } from "vitest";
2
2
  import { createTestWallet, runCli } from "./helpers.js";
3
+ const pubkey = "02" + "a".repeat(64);
4
+ const evm = "0x000000000000000000000000000000000000dead";
3
5
  describe("pay command — destination detection", () => {
4
6
  test("unknown destination format lists all 4 accepted shapes", () => {
5
7
  const result = runCli(`pay notavaliddestination`);
@@ -16,44 +18,82 @@ describe("pay command — destination detection", () => {
16
18
  expect(result.output.error).toContain("--amount");
17
19
  });
18
20
  test("keysend pubkey without --amount is rejected before wallet load", () => {
19
- const pubkey = "02" + "a".repeat(64);
20
21
  const result = runCli(`pay ${pubkey}`);
21
22
  expect(result.success).toBe(false);
22
23
  expect(result.output.error).toContain("--amount");
23
24
  });
24
25
  test("EVM address without --amount is rejected before wallet load", () => {
25
- const result = runCli(`pay 0x000000000000000000000000000000000000dead`);
26
+ const result = runCli(`pay ${evm}`);
26
27
  expect(result.success).toBe(false);
27
28
  expect(result.output.error).toContain("--amount");
28
29
  });
30
+ });
31
+ describe("pay command — unified amount model validation", () => {
32
+ test("lightning address with --amount but no --currency is rejected", () => {
33
+ const result = runCli(`pay alice@getalby.com --amount 100 --network lightning`);
34
+ expect(result.success).toBe(false);
35
+ expect(result.output.error).toContain("--currency");
36
+ });
37
+ test("lightning address with --amount but no --network is rejected", () => {
38
+ const result = runCli(`pay alice@getalby.com --amount 100 --currency BTC`);
39
+ expect(result.success).toBe(false);
40
+ expect(result.output.error).toContain("--network");
41
+ });
42
+ test("--currency BTC without --unit is rejected (sats/BTC ambiguity)", () => {
43
+ const result = runCli(`pay alice@getalby.com --amount 100 --currency BTC --network lightning`);
44
+ expect(result.success).toBe(false);
45
+ expect(result.output.error).toContain("--unit");
46
+ });
47
+ test("--unit on a fiat currency is rejected", () => {
48
+ const result = runCli(`pay alice@getalby.com --amount 5 --currency USD --unit sats --network lightning`);
49
+ expect(result.success).toBe(false);
50
+ expect(result.output.error).toContain("--unit is not valid");
51
+ });
52
+ test("a lightning address on a chain network is rejected (lightning-only)", () => {
53
+ const result = runCli(`pay alice@getalby.com --amount 10 --currency USDC --network arbitrum`);
54
+ expect(result.success).toBe(false);
55
+ expect(result.output.error).toContain("lightning");
56
+ });
57
+ test("a non-numeric --amount is rejected at parse time", () => {
58
+ const result = runCli(`pay alice@getalby.com --amount 123usd --currency BTC --unit sats --network lightning`);
59
+ expect(result.success).toBe(false);
60
+ expect(result.output.error).toContain("Amount must be a positive number");
61
+ });
62
+ test("EVM address with --currency BTC --network lightning is rejected", () => {
63
+ const result = runCli(`pay ${evm} --amount 10 --currency BTC --network lightning`);
64
+ expect(result.success).toBe(false);
65
+ expect(result.output.error).toContain("EVM address");
66
+ });
29
67
  test("EVM address without --currency is rejected", () => {
30
- const result = runCli(`pay 0x000000000000000000000000000000000000dead --amount 10 --network arbitrum`);
68
+ const result = runCli(`pay ${evm} --amount 10 --network arbitrum`);
31
69
  expect(result.success).toBe(false);
32
70
  expect(result.output.error).toContain("--currency");
33
71
  });
34
72
  test("EVM address without --network is rejected", () => {
35
- const result = runCli(`pay 0x000000000000000000000000000000000000dead --amount 10 --currency USDC`);
73
+ const result = runCli(`pay ${evm} --amount 10 --currency USDC`);
36
74
  expect(result.success).toBe(false);
37
75
  expect(result.output.error).toContain("--network");
38
76
  });
39
- test("--currency on a BOLT-11 invoice is rejected as not applicable", () => {
77
+ test("amount flags on a BOLT-11 invoice without --amount are rejected", () => {
40
78
  const result = runCli(`pay lnbc1junk --currency USDT`);
41
79
  expect(result.success).toBe(false);
42
- expect(result.output.error).toContain("not applicable to invoice payment");
80
+ expect(result.output.error).toContain("zero-amount invoice");
43
81
  });
44
82
  test("testnet/signet invoice prefixes (lntb...) are recognized as invoices", () => {
45
- // Same path as the lnbc test above — exercises that lntb is treated as
46
- // an invoice (not falling through to the unknown-destination error).
47
83
  const result = runCli(`pay lntb1junk --currency USDT`);
48
84
  expect(result.success).toBe(false);
49
- expect(result.output.error).toContain("not applicable to invoice payment");
85
+ expect(result.output.error).toContain("zero-amount invoice");
50
86
  });
51
87
  test("--comment on a keysend pubkey is rejected as not applicable", () => {
52
- const pubkey = "02" + "a".repeat(64);
53
- const result = runCli(`pay ${pubkey} --amount 100 --comment hi`);
88
+ const result = runCli(`pay ${pubkey} --amount 100 --currency BTC --unit sats --network lightning --comment hi`);
54
89
  expect(result.success).toBe(false);
55
90
  expect(result.output.error).toContain("not applicable to keysend payment");
56
91
  });
92
+ test("--unit on an EVM (crypto) destination is rejected", () => {
93
+ const result = runCli(`pay ${evm} --amount 10 --currency USDC --unit sats --network arbitrum`);
94
+ expect(result.success).toBe(false);
95
+ expect(result.output.error).toContain("--unit");
96
+ });
57
97
  });
58
98
  describe("pay command — live integration", () => {
59
99
  let sender;
@@ -63,21 +103,28 @@ describe("pay command — live integration", () => {
63
103
  receiver = await createTestWallet();
64
104
  }, 60000);
65
105
  test("pay <bolt11> pays an invoice end-to-end", () => {
66
- const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 100`);
106
+ const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice --amount 100 --currency BTC --unit sats --network lightning`);
67
107
  expect(invoiceResult.success).toBe(true);
68
108
  const paymentResult = runCli(`-c "${sender.nwcUrl}" pay "${invoiceResult.output.invoice}"`);
69
109
  expect(paymentResult.success).toBe(true);
70
110
  expect(paymentResult.output.preimage).toBeDefined();
71
111
  });
72
- test("pay <lightning-address> --amount fetches an invoice and pays it", () => {
73
- const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${receiver.lightningAddress} --amount 100`);
112
+ test("pay <lightning-address> --currency BTC --unit sats pays it", () => {
113
+ const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${receiver.lightningAddress} --amount 100 --currency BTC --unit sats --network lightning`);
114
+ expect(paymentResult.success).toBe(true);
115
+ expect(paymentResult.output.preimage).toBeDefined();
116
+ });
117
+ test("pay <lightning-address> --currency USD resolves fiat to sats and pays", () => {
118
+ const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${receiver.lightningAddress} --amount 1 --currency USD --network lightning`);
74
119
  expect(paymentResult.success).toBe(true);
75
120
  expect(paymentResult.output.preimage).toBeDefined();
121
+ expect(paymentResult.output.amount_in_sats).toBeGreaterThan(0);
122
+ expect(paymentResult.output.fiat).toEqual({ amount: 1, currency: "USD" });
76
123
  });
77
- test("pay <pubkey> --amount sends a keysend payment", () => {
124
+ test("pay <pubkey> --currency BTC --unit sats sends a keysend payment", () => {
78
125
  const infoResult = runCli(`-c "${receiver.nwcUrl}" get-info`);
79
126
  expect(infoResult.success).toBe(true);
80
- const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${infoResult.output.pubkey} --amount 100`);
127
+ const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${infoResult.output.pubkey} --amount 100 --currency BTC --unit sats --network lightning`);
81
128
  expect(paymentResult.success).toBe(true);
82
129
  expect(paymentResult.output.preimage).toBeDefined();
83
130
  });
@@ -129,17 +129,24 @@ describe("pay-crypto validation", () => {
129
129
  test("--amount 0 is rejected", async () => {
130
130
  const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 0 --currency USDC --network arbitrum");
131
131
  expect(result.success).toBe(false);
132
- expect(result.output.error).toContain("Invalid --amount");
132
+ expect(result.output.error).toContain("Amount must be a positive number");
133
133
  });
134
134
  test("--amount -1 is rejected", async () => {
135
135
  const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount -1 --currency USDC --network arbitrum");
136
136
  expect(result.success).toBe(false);
137
- expect(result.output.error).toContain("Invalid --amount");
137
+ expect(result.output.error).toContain("Amount must be a positive number");
138
138
  });
139
139
  test("--amount abc (NaN) is rejected", async () => {
140
140
  const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount abc --currency USDC --network arbitrum");
141
141
  expect(result.success).toBe(false);
142
- expect(result.output.error).toContain("Invalid --amount");
142
+ expect(result.output.error).toContain("Amount must be a positive number");
143
+ });
144
+ // Unit-suffixed input must not be truncated to its leading digits
145
+ // (Number("123usd") is NaN, unlike parseFloat which would yield 123).
146
+ test("--amount 123usd is rejected", async () => {
147
+ const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 123usd --currency USDC --network arbitrum");
148
+ expect(result.success).toBe(false);
149
+ expect(result.output.error).toContain("Amount must be a positive number");
143
150
  });
144
151
  });
145
152
  describe("missing required options", () => {
@@ -6,15 +6,40 @@ describe("receive command — validation", () => {
6
6
  expect(result.success).toBe(false);
7
7
  expect(result.output.error).toContain("--description requires --amount");
8
8
  });
9
- test("--amount 0 is rejected", () => {
9
+ test("--currency without --amount is rejected", () => {
10
+ const result = runCli(`receive --currency USD`);
11
+ expect(result.success).toBe(false);
12
+ expect(result.output.error).toContain("--currency requires --amount");
13
+ });
14
+ test("--amount 0 is rejected at parse time", () => {
10
15
  const result = runCli(`receive --amount 0`);
11
16
  expect(result.success).toBe(false);
12
- expect(result.output.error).toContain("Invalid --amount");
17
+ expect(result.output.error).toContain("Amount must be a positive number");
13
18
  });
14
- test("--amount abc (NaN) is rejected", () => {
19
+ test("--amount abc (NaN) is rejected at parse time", () => {
15
20
  const result = runCli(`receive --amount abc`);
16
21
  expect(result.success).toBe(false);
17
- expect(result.output.error).toContain("Invalid --amount");
22
+ expect(result.output.error).toContain("Amount must be a positive number");
23
+ });
24
+ test("--amount without --currency is rejected", () => {
25
+ const result = runCli(`receive --amount 100 --network lightning`);
26
+ expect(result.success).toBe(false);
27
+ expect(result.output.error).toContain("--currency");
28
+ });
29
+ test("--amount --currency BTC without --unit is rejected", () => {
30
+ const result = runCli(`receive --amount 100 --currency BTC --network lightning`);
31
+ expect(result.success).toBe(false);
32
+ expect(result.output.error).toContain("--unit");
33
+ });
34
+ test("--unit on a fiat currency is rejected", () => {
35
+ const result = runCli(`receive --amount 5 --currency USD --unit sats --network lightning`);
36
+ expect(result.success).toBe(false);
37
+ expect(result.output.error).toContain("--unit is not valid");
38
+ });
39
+ test("a chain network is rejected (invoices are lightning-only)", () => {
40
+ const result = runCli(`receive --amount 10 --currency USDC --network arbitrum`);
41
+ expect(result.success).toBe(false);
42
+ expect(result.output.error).toContain("lightning");
18
43
  });
19
44
  });
20
45
  describe("receive command — live integration", () => {
@@ -27,14 +52,26 @@ describe("receive command — live integration", () => {
27
52
  expect(result.success).toBe(true);
28
53
  expect(result.output.lightning_address).toBe(wallet.lightningAddress);
29
54
  });
30
- test("receive --amount returns a BOLT-11 invoice", () => {
31
- const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 100`);
55
+ test("receive --amount --currency BTC --unit sats returns a BOLT-11 invoice", () => {
56
+ const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 100 --currency BTC --unit sats --network lightning`);
32
57
  expect(result.success).toBe(true);
33
58
  expect(result.output.invoice).toMatch(/^lnbc/i);
34
59
  expect(result.output.amount_in_sats).toBe(100);
35
60
  });
61
+ test("receive --unit BTC converts to sats", () => {
62
+ const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 0.000001 --currency BTC --unit BTC --network lightning`);
63
+ expect(result.success).toBe(true);
64
+ expect(result.output.amount_in_sats).toBe(100);
65
+ });
66
+ test("receive --amount --currency USD resolves fiat to sats", () => {
67
+ const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 5 --currency USD --network lightning`);
68
+ expect(result.success).toBe(true);
69
+ expect(result.output.invoice).toMatch(/^lnbc/i);
70
+ expect(result.output.amount_in_sats).toBeGreaterThan(0);
71
+ expect(result.output.fiat).toEqual({ amount: 5, currency: "USD" });
72
+ });
36
73
  test("receive --amount --description produces an invoice", () => {
37
- const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 100 --description "test"`);
74
+ const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 100 --currency BTC --unit sats --network lightning --description "test"`);
38
75
  expect(result.success).toBe(true);
39
76
  expect(result.output.invoice).toMatch(/^lnbc/i);
40
77
  expect(result.output.amount_in_sats).toBe(100);
@@ -9,7 +9,7 @@ export async function discover(params) {
9
9
  url.searchParams.set("health", params.health);
10
10
  if (params.sort)
11
11
  url.searchParams.set("sort", params.sort);
12
- // Filter to BTC (Lightning) services server-side
12
+ // Filter to BTC (lightning) services server-side
13
13
  url.searchParams.set("payment_asset", "BTC");
14
14
  url.searchParams.set("limit", String(requestedLimit));
15
15
  const controller = new AbortController();
@@ -1,4 +1,4 @@
1
- import { fetch402 as fetch402Lib } from "@getalby/lightning-tools/402";
1
+ import { fetch402 as fetch402Lib, } from "@getalby/lightning-tools/402";
2
2
  const DEFAULT_MAX_AMOUNT_SATS = 5000;
3
3
  export async function fetch402(client, params) {
4
4
  const method = params.method?.toUpperCase();
@@ -7,10 +7,15 @@ export async function fetch402(client, params) {
7
7
  };
8
8
  if (method && method !== "GET" && method !== "HEAD") {
9
9
  requestOptions.body = params.body;
10
- requestOptions.headers = {
11
- "Content-Type": "application/json",
12
- ...params.headers,
13
- };
10
+ // A body is always passed as JSON, so default the Content-Type. The agent
11
+ // isn't required to set the header itself but might do so anyway, so only
12
+ // add our default when it's absent to avoid a duplicate Content-Type.
13
+ const headers = { ...params.headers };
14
+ const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
15
+ if (!hasContentType) {
16
+ headers["Content-Type"] = "application/json";
17
+ }
18
+ requestOptions.headers = headers;
14
19
  }
15
20
  else if (params.headers) {
16
21
  requestOptions.headers = params.headers;
@@ -18,7 +23,8 @@ export async function fetch402(client, params) {
18
23
  const maxAmountSats = params.maxAmountSats ?? DEFAULT_MAX_AMOUNT_SATS;
19
24
  const result = await fetch402Lib(params.url, requestOptions, {
20
25
  wallet: client,
21
- maxAmount: maxAmountSats || undefined,
26
+ maxAmount: maxAmountSats,
27
+ credentials: params.credentials,
22
28
  });
23
29
  const responseContent = await result.text();
24
30
  if (!result.ok) {
@@ -26,5 +32,11 @@ export async function fetch402(client, params) {
26
32
  }
27
33
  return {
28
34
  content: responseContent,
35
+ // Payment metadata attached by the 402 helper: whether a payment was made,
36
+ // the amount, routing fees (feesPaid, in millisatoshis), and the reusable
37
+ // credential. Pass `credentials` back via --credentials on a follow-up
38
+ // request to authorize it without paying again. Absent when no 402 payment
39
+ // was involved (e.g. an already-open resource).
40
+ payment: result.payment,
29
41
  };
30
42
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
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.7.0",
5
+ "version": "0.9.0",
6
6
  "type": "module",
7
7
  "main": "build/index.js",
8
8
  "bin": {
@@ -36,9 +36,9 @@
36
36
  "node": ">=20"
37
37
  },
38
38
  "dependencies": {
39
- "@getalby/lightning-tools": "^8.1.1",
40
- "@getalby/sdk": "^8.0.1",
41
- "@lendasat/lendaswap-sdk-pure": "^0.2.36",
39
+ "@getalby/lightning-tools": "^8.2.0",
40
+ "@getalby/sdk": "^8.0.3",
41
+ "@lendasat/lendaswap-sdk-pure": "^0.2.38",
42
42
  "@noble/hashes": "^2.0.1",
43
43
  "commander": "^14.0.3",
44
44
  "nostr-tools": "^2.23.3"