@getalby/cli 0.6.1 → 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.
@@ -0,0 +1,111 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { runCli } from "./helpers.js";
3
+ // Exercises the shared amount model (src/amount.ts) wired into every
4
+ // amount-bearing command. Every check here resolves *before* network I/O or
5
+ // wallet load: structural validation (currency/network/unit rules) and the
6
+ // BTC sats/BTC arithmetic are all synchronous. We sanitize the environment so
7
+ // that, when an input is fully valid, the command fails only at wallet load
8
+ // ("No wallet connection found") — proving the input cleared every gate.
9
+ //
10
+ // The two cross-rail *currency* mismatches that need a downstream resolver to
11
+ // surface (a token on the lightning rail → rate lookup; a fiat code's live
12
+ // rate) are intentionally covered by the live suites, not here, since the
13
+ // model is network-first (the rail is chosen by --network alone, with no
14
+ // hardcoded currency/token catalog).
15
+ const SANITIZED_ENV = {
16
+ HOME: "/tmp/nonexistent-alby-cli-test-home",
17
+ NWC_URL: "",
18
+ };
19
+ function run(command) {
20
+ return runCli(command, SANITIZED_ENV);
21
+ }
22
+ describe("amount model — currency and network are always required", () => {
23
+ test("amount without --currency is rejected", () => {
24
+ const result = run("pay alice@getalby.com --amount 100 --network lightning");
25
+ expect(result.success).toBe(false);
26
+ expect(result.output.error).toContain("--currency");
27
+ });
28
+ test("amount without --network is rejected", () => {
29
+ const result = run("pay alice@getalby.com --amount 100 --currency BTC --unit sats");
30
+ expect(result.success).toBe(false);
31
+ expect(result.output.error).toContain("--network");
32
+ });
33
+ });
34
+ describe("amount model — --unit is required for BTC, rejected otherwise", () => {
35
+ test("BTC without --unit is rejected (sats vs BTC must be explicit)", () => {
36
+ const result = run("pay alice@getalby.com --amount 1 --currency BTC --network lightning");
37
+ expect(result.success).toBe(false);
38
+ expect(result.output.error).toContain("--unit");
39
+ });
40
+ test("--unit on a fiat currency is rejected", () => {
41
+ const result = run("pay alice@getalby.com --amount 5 --currency USD --unit sats --network lightning");
42
+ expect(result.success).toBe(false);
43
+ expect(result.output.error).toContain("--unit is not valid");
44
+ });
45
+ test('an invalid --unit value is rejected', () => {
46
+ const result = run("pay alice@getalby.com --amount 1 --currency BTC --unit bits --network lightning");
47
+ expect(result.success).toBe(false);
48
+ expect(result.output.error).toContain('--unit must be "sats" or "BTC"');
49
+ });
50
+ });
51
+ describe("amount model — BTC --unit sats is whole-number only", () => {
52
+ test.each(["1.5", "1abc", "0"])("make-invoice rejects --amount %s for BTC/sats", (value) => {
53
+ const result = run(`make-invoice --amount ${value} --currency BTC --unit sats --network lightning`);
54
+ expect(result.success).toBe(false);
55
+ // "1.5" is rejected by the sats whole-number check; "1abc"/"0" by the
56
+ // strict --amount parser at parse time.
57
+ expect(result.output.error).toMatch(/whole number|Amount must be a positive number/);
58
+ });
59
+ });
60
+ describe("amount model — BTC --unit BTC converts to sats", () => {
61
+ test("a fractional-sat BTC amount is rejected", () => {
62
+ // 0.000000001 BTC = 0.1 sats — not a whole number of sats.
63
+ const result = run("make-invoice --amount 0.000000001 --currency BTC --unit BTC --network lightning");
64
+ expect(result.success).toBe(false);
65
+ expect(result.output.error).toContain("whole number of sats");
66
+ });
67
+ test("a whole-sat BTC amount clears validation (fails only at wallet load)", () => {
68
+ // 0.000001 BTC = 100 sats. Resolution is synchronous, so the only thing
69
+ // left to fail is the (absent) wallet.
70
+ const result = run("make-invoice --amount 0.000001 --currency BTC --unit BTC --network lightning");
71
+ expect(result.success).toBe(false);
72
+ expect(result.output.error).toContain("No wallet connection found");
73
+ });
74
+ });
75
+ describe("amount model — rail mismatch on a lightning-only command", () => {
76
+ test("BTC on a chain network is rejected", () => {
77
+ const result = run("make-invoice --amount 1 --currency BTC --network arbitrum");
78
+ expect(result.success).toBe(false);
79
+ expect(result.output.error).toContain("only supported on --network lightning");
80
+ });
81
+ test("a chain network is rejected (invoices settle over Lightning)", () => {
82
+ const result = run("make-invoice --amount 10 --currency USDC --network arbitrum");
83
+ expect(result.success).toBe(false);
84
+ expect(result.output.error).toContain("lightning");
85
+ });
86
+ });
87
+ // Representative coverage proving the shared helper is wired into more than
88
+ // one command, not just make-invoice / pay.
89
+ describe("amount model — wired across commands", () => {
90
+ test("make-hold-invoice enforces --unit for BTC", () => {
91
+ const result = run("make-hold-invoice --amount 1 --currency BTC --network lightning --payment-hash " +
92
+ "a".repeat(64));
93
+ expect(result.success).toBe(false);
94
+ expect(result.output.error).toContain("--unit");
95
+ });
96
+ test("pay-keysend enforces --unit for BTC", () => {
97
+ const result = run(`pay-keysend -p 02${"a".repeat(64)} --amount 1 --currency BTC --network lightning`);
98
+ expect(result.success).toBe(false);
99
+ expect(result.output.error).toContain("--unit");
100
+ });
101
+ test("request-invoice-from-lightning-address rejects --unit for fiat", () => {
102
+ const result = run("request-invoice-from-lightning-address -a a@b.com --amount 5 --currency USD --unit sats --network lightning");
103
+ expect(result.success).toBe(false);
104
+ expect(result.output.error).toContain("--unit is not valid");
105
+ });
106
+ test("sats-to-fiat rejects an invalid --unit", () => {
107
+ const result = run("sats-to-fiat --amount 1000 --unit bits --currency USD");
108
+ expect(result.success).toBe(false);
109
+ expect(result.output.error).toContain('--unit must be "sats" or "BTC"');
110
+ });
111
+ });
@@ -50,12 +50,12 @@ describe("Connection Secret Handling", () => {
50
50
  NWC_URL: "",
51
51
  });
52
52
  expect(result.success).toBe(false);
53
- expect(result.output.error).toContain("No connection secret provided");
53
+ expect(result.output.error).toContain("No wallet connection found");
54
54
  });
55
55
  test("errors when connection string is malformed", () => {
56
56
  const result = runCli(`-c "nostr+walletconnect://asdf" get-balance`);
57
57
  expect(result.success).toBe(false);
58
- expect(result.output.error).toContain("Invalid connection secret");
58
+ expect(result.output.error).toContain("Invalid NWC URL");
59
59
  });
60
60
  test("reads connection secret from NWC_URL environment variable", () => {
61
61
  const result = runCli("get-balance", { NWC_URL: wallet.nwcUrl });
@@ -0,0 +1,30 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { runCli } from "./helpers.js";
3
+ // fetch's `--max-amount` is the unified spend-cap flag. Its value is a strict
4
+ // sats integer (see `parseMaxAmountSats` in src/commands/fetch.ts) because the
5
+ // cap is currently restricted to BTC/sats over lightning; the denomination flags are
6
+ // validated through the shared amount model (src/amount.ts). Parsing happens at
7
+ // commander parse time — before any network I/O — so these assertions are
8
+ // deterministic and need no wallet. Payment/invoice amounts are covered by
9
+ // amount-model.test.ts.
10
+ describe("fetch --max-amount strict parsing", () => {
11
+ // The cap is a positive whole number of sats. Malformed input must be
12
+ // rejected, not coerced (parseInt("abc") → NaN, parseInt("0.5") → 0), so a
13
+ // typo can't silently weaken the limit. Negatives are rejected by the
14
+ // digits-only check, and 0 is no longer a "no limit" escape hatch.
15
+ test.each(["0.5", "abc", "-1", "1e3", "0"])("rejects malformed/non-positive --max-amount %s", (value) => {
16
+ const result = runCli(`fetch http://example.invalid --max-amount ${value}`);
17
+ expect(result.success).toBe(false);
18
+ expect(result.output.error).toContain("Sats must be");
19
+ });
20
+ test("a positive --max-amount requires its denomination (--currency)", () => {
21
+ const result = runCli("fetch http://example.invalid --max-amount 1000");
22
+ expect(result.success).toBe(false);
23
+ expect(result.output.error).toContain("--currency");
24
+ });
25
+ test("a positive --max-amount rejects a non-BTC/sats/lightning denomination", () => {
26
+ const result = runCli("fetch http://example.invalid --max-amount 5 --currency USD --network lightning");
27
+ expect(result.success).toBe(false);
28
+ expect(result.output.error).toContain("currently supports only --currency BTC --unit sats --network lightning");
29
+ });
30
+ });
@@ -1,16 +1,29 @@
1
1
  import { describe, test, expect } from "vitest";
2
2
  import { runCli } from "./helpers.js";
3
3
  const exampleInvoice = "lnbc1u1p5hlrr8dqqnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp55utq6s8u5407namwt4jvhgsaf9fyszppjfwyxp7qsw6cyc8vxukqsp583usez9yhmkcavvvjz8cq56v3nglh2q37xkf4ufrgwxfrfjkm54s9qyysgqcqzp2xqyz5vqgtyysw64zt9sj6kfpqnekzwc37y2uyg0xdapgxqqth4uahff0x89sjfsvukjlllasg5dn05u2uha6qcvxz2y3ye5k7958qtes4pv4ggqtnjyky";
4
- const exampleLightningAddress = "nwc1769966844@getalby.com";
4
+ const exampleLightningAddress = "nwc1779952113427@getalby.com";
5
5
  describe("Lightning Tools (no wallet required)", () => {
6
6
  test("fiat-to-sats converts USD to sats", () => {
7
- const result = runCli("fiat-to-sats -a 1 --currency USD");
7
+ const result = runCli("fiat-to-sats --amount 1 --currency USD");
8
8
  expect(result.success).toBe(true);
9
9
  expect(result.output.amount_in_sats).toBeTypeOf("number");
10
10
  expect(result.output.amount_in_sats).toBeGreaterThan(0);
11
11
  });
12
+ test("fiat-to-sats accepts a decimal --amount", () => {
13
+ const result = runCli("fiat-to-sats --amount 10.5 --currency USD");
14
+ expect(result.success).toBe(true);
15
+ expect(result.output.amount_in_sats).toBeGreaterThan(0);
16
+ });
17
+ // --amount is parsed by the shared strict parser (parseAmountNumber), so
18
+ // partial/invalid input is rejected rather than silently truncated (e.g.
19
+ // "10abc" → 10), as are non-positive values.
20
+ test.each(["10abc", "abc", "0", "-5"])("fiat-to-sats rejects invalid --amount %s", (value) => {
21
+ const result = runCli(`fiat-to-sats --amount ${value} --currency USD`);
22
+ expect(result.success).toBe(false);
23
+ expect(result.output.error).toContain("Amount must be a positive number");
24
+ });
12
25
  test("sats-to-fiat converts sats to USD", () => {
13
- const result = runCli("sats-to-fiat -a 1000 --currency USD");
26
+ const result = runCli("sats-to-fiat --amount 1000 --unit sats --currency USD");
14
27
  expect(result.success).toBe(true);
15
28
  expect(result.output.amount).toBeTypeOf("number");
16
29
  expect(result.output.amount).toBeGreaterThan(0);
@@ -29,7 +42,7 @@ describe("Lightning Tools (no wallet required)", () => {
29
42
  expect(result.output.valid).toBe(false);
30
43
  });
31
44
  test("request-invoice-from-lightning-address requests invoice from lightning address", async () => {
32
- const result = runCli(`request-invoice-from-lightning-address -a "${exampleLightningAddress}" -s 100`);
45
+ const result = runCli(`request-invoice-from-lightning-address -a "${exampleLightningAddress}" --amount 100 --currency BTC --unit sats --network lightning`);
33
46
  expect(result.success).toBe(true);
34
47
  expect(result.output.paymentRequest.toLowerCase()).toMatch(/^lnbc/);
35
48
  });
@@ -0,0 +1,75 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
+ import { runCli } from "./helpers.js";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ describe("list-wallets command", () => {
7
+ let testHome;
8
+ let albyDir;
9
+ beforeEach(() => {
10
+ testHome = mkdtempSync(join(tmpdir(), "alby-cli-test-"));
11
+ albyDir = join(testHome, ".alby-cli");
12
+ });
13
+ afterEach(() => {
14
+ rmSync(testHome, { recursive: true, force: true });
15
+ });
16
+ function writeWallet(filename) {
17
+ mkdirSync(albyDir, { recursive: true });
18
+ writeFileSync(join(albyDir, filename), "nostr+walletconnect://test");
19
+ }
20
+ test("returns empty list when no .alby-cli directory exists", () => {
21
+ const result = runCli("list-wallets", { HOME: testHome });
22
+ expect(result.success).toBe(true);
23
+ expect(result.output.wallets).toEqual([]);
24
+ });
25
+ test("lists the default (unnamed) wallet", () => {
26
+ writeWallet("connection-secret.key");
27
+ const result = runCli("list-wallets", { HOME: testHome });
28
+ expect(result.success).toBe(true);
29
+ expect(result.output.wallets).toEqual([
30
+ { name: null, isDefault: true, status: "connected" },
31
+ ]);
32
+ });
33
+ test("lists named wallets sorted with default first", () => {
34
+ writeWallet("connection-secret.key");
35
+ writeWallet("connection-secret-work.key");
36
+ writeWallet("connection-secret-personal.key");
37
+ const result = runCli("list-wallets", { HOME: testHome });
38
+ expect(result.success).toBe(true);
39
+ expect(result.output.wallets).toEqual([
40
+ { name: null, isDefault: true, status: "connected" },
41
+ { name: "personal", isDefault: false, status: "connected" },
42
+ { name: "work", isDefault: false, status: "connected" },
43
+ ]);
44
+ });
45
+ test("reports pending connections", () => {
46
+ writeWallet("pending-connection-secret-test.key");
47
+ const result = runCli("list-wallets", { HOME: testHome });
48
+ expect(result.success).toBe(true);
49
+ expect(result.output.wallets).toEqual([
50
+ { name: "test", isDefault: false, status: "pending" },
51
+ ]);
52
+ });
53
+ test("connected status takes precedence over pending for the same wallet", () => {
54
+ writeWallet("connection-secret-dual.key");
55
+ writeWallet("pending-connection-secret-dual.key");
56
+ const result = runCli("list-wallets", { HOME: testHome });
57
+ expect(result.success).toBe(true);
58
+ expect(result.output.wallets).toEqual([
59
+ { name: "dual", isDefault: false, status: "connected" },
60
+ ]);
61
+ });
62
+ test("does not reveal secret contents", () => {
63
+ writeWallet("connection-secret.key");
64
+ const result = runCli("list-wallets", { HOME: testHome });
65
+ expect(JSON.stringify(result.output)).not.toContain("nostr+walletconnect://");
66
+ });
67
+ test("ignores unrelated files", () => {
68
+ mkdirSync(albyDir, { recursive: true });
69
+ writeFileSync(join(albyDir, "pending-connection-relay-test.txt"), "wss://relay");
70
+ writeFileSync(join(albyDir, "notes.txt"), "hello");
71
+ const result = runCli("list-wallets", { HOME: testHome });
72
+ expect(result.success).toBe(true);
73
+ expect(result.output.wallets).toEqual([]);
74
+ });
75
+ });
@@ -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", [
@@ -40,7 +40,6 @@ describe("NWC HOLD Invoice Commands", () => {
40
40
  "-c",
41
41
  sender.nwcUrl,
42
42
  "pay-invoice",
43
- "-i",
44
43
  holdResult.output.invoice,
45
44
  ], { stdio: ["ignore", "pipe", "pipe"] });
46
45
  // Wait for the hold invoice to be accepted
@@ -56,7 +55,7 @@ describe("NWC HOLD Invoice Commands", () => {
56
55
  test("cancel-hold-invoice cancels hold invoice", async () => {
57
56
  const { paymentHash } = generateHoldInvoiceParams();
58
57
  // Create a hold invoice
59
- 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}"`);
60
59
  expect(holdResult.success).toBe(true);
61
60
  // Pay the invoice from sender (this will put it in held state)
62
61
  const payProcess = spawn("node", [
@@ -64,7 +63,6 @@ describe("NWC HOLD Invoice Commands", () => {
64
63
  "-c",
65
64
  sender.nwcUrl,
66
65
  "pay-invoice",
67
- "-i",
68
66
  holdResult.output.invoice,
69
67
  ], { stdio: ["ignore", "pipe", "pipe"] });
70
68
  // Wait for the hold invoice to be in held state
@@ -10,20 +10,20 @@ 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
17
- const paymentResult = runCli(`-c "${sender.nwcUrl}" pay-invoice -i "${invoiceResult.output.invoice}"`);
17
+ const paymentResult = runCli(`-c "${sender.nwcUrl}" pay-invoice "${invoiceResult.output.invoice}"`);
18
18
  expect(paymentResult.success).toBe(true);
19
19
  expect(paymentResult.output.preimage).toBeDefined();
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
- const payResult = runCli(`-c "${sender.nwcUrl}" pay-invoice -i "${invoiceResult.output.invoice}"`);
26
+ const payResult = runCli(`-c "${sender.nwcUrl}" pay-invoice "${invoiceResult.output.invoice}"`);
27
27
  expect(payResult.success).toBe(true);
28
28
  // Lookup the paid invoice using the invoice string
29
29
  const lookupResult = runCli(`-c "${receiver.nwcUrl}" lookup-invoice -i "${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
  });
@@ -0,0 +1,131 @@
1
+ import { describe, test, expect, beforeAll } from "vitest";
2
+ import { createTestWallet, runCli } from "./helpers.js";
3
+ const pubkey = "02" + "a".repeat(64);
4
+ const evm = "0x000000000000000000000000000000000000dead";
5
+ describe("pay command — destination detection", () => {
6
+ test("unknown destination format lists all 4 accepted shapes", () => {
7
+ const result = runCli(`pay notavaliddestination`);
8
+ expect(result.success).toBe(false);
9
+ expect(result.output.error).toContain("Could not detect destination type");
10
+ expect(result.output.error).toContain("BOLT-11 invoice");
11
+ expect(result.output.error).toContain("Lightning address");
12
+ expect(result.output.error).toContain("Node pubkey");
13
+ expect(result.output.error).toContain("EVM address");
14
+ });
15
+ test("lightning address without --amount is rejected before wallet load", () => {
16
+ const result = runCli(`pay alice@getalby.com`);
17
+ expect(result.success).toBe(false);
18
+ expect(result.output.error).toContain("--amount");
19
+ });
20
+ test("keysend pubkey without --amount is rejected before wallet load", () => {
21
+ const result = runCli(`pay ${pubkey}`);
22
+ expect(result.success).toBe(false);
23
+ expect(result.output.error).toContain("--amount");
24
+ });
25
+ test("EVM address without --amount is rejected before wallet load", () => {
26
+ const result = runCli(`pay ${evm}`);
27
+ expect(result.success).toBe(false);
28
+ expect(result.output.error).toContain("--amount");
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
+ });
67
+ test("EVM address without --currency is rejected", () => {
68
+ const result = runCli(`pay ${evm} --amount 10 --network arbitrum`);
69
+ expect(result.success).toBe(false);
70
+ expect(result.output.error).toContain("--currency");
71
+ });
72
+ test("EVM address without --network is rejected", () => {
73
+ const result = runCli(`pay ${evm} --amount 10 --currency USDC`);
74
+ expect(result.success).toBe(false);
75
+ expect(result.output.error).toContain("--network");
76
+ });
77
+ test("amount flags on a BOLT-11 invoice without --amount are rejected", () => {
78
+ const result = runCli(`pay lnbc1junk --currency USDT`);
79
+ expect(result.success).toBe(false);
80
+ expect(result.output.error).toContain("zero-amount invoice");
81
+ });
82
+ test("testnet/signet invoice prefixes (lntb...) are recognized as invoices", () => {
83
+ const result = runCli(`pay lntb1junk --currency USDT`);
84
+ expect(result.success).toBe(false);
85
+ expect(result.output.error).toContain("zero-amount invoice");
86
+ });
87
+ test("--comment on a keysend pubkey is rejected as not applicable", () => {
88
+ const result = runCli(`pay ${pubkey} --amount 100 --currency BTC --unit sats --network lightning --comment hi`);
89
+ expect(result.success).toBe(false);
90
+ expect(result.output.error).toContain("not applicable to keysend payment");
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
+ });
97
+ });
98
+ describe("pay command — live integration", () => {
99
+ let sender;
100
+ let receiver;
101
+ beforeAll(async () => {
102
+ sender = await createTestWallet();
103
+ receiver = await createTestWallet();
104
+ }, 60000);
105
+ test("pay <bolt11> pays an invoice end-to-end", () => {
106
+ const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice --amount 100 --currency BTC --unit sats --network lightning`);
107
+ expect(invoiceResult.success).toBe(true);
108
+ const paymentResult = runCli(`-c "${sender.nwcUrl}" pay "${invoiceResult.output.invoice}"`);
109
+ expect(paymentResult.success).toBe(true);
110
+ expect(paymentResult.output.preimage).toBeDefined();
111
+ });
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`);
119
+ expect(paymentResult.success).toBe(true);
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" });
123
+ });
124
+ test("pay <pubkey> --currency BTC --unit sats sends a keysend payment", () => {
125
+ const infoResult = runCli(`-c "${receiver.nwcUrl}" get-info`);
126
+ expect(infoResult.success).toBe(true);
127
+ const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${infoResult.output.pubkey} --amount 100 --currency BTC --unit sats --network lightning`);
128
+ expect(paymentResult.success).toBe(true);
129
+ expect(paymentResult.output.preimage).toBeDefined();
130
+ });
131
+ });