@getalby/cli 0.0.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/LICENSE +201 -0
- package/README.md +94 -0
- package/build/auth.js +20 -0
- package/build/commands/cancel-hold-invoice.js +17 -0
- package/build/commands/fetch-l402.js +23 -0
- package/build/commands/fiat-to-sats.js +18 -0
- package/build/commands/get-balance.js +14 -0
- package/build/commands/get-budget.js +14 -0
- package/build/commands/get-info.js +14 -0
- package/build/commands/get-wallet-service-info.js +14 -0
- package/build/commands/list-transactions.js +27 -0
- package/build/commands/lookup-invoice.js +23 -0
- package/build/commands/make-hold-invoice.js +23 -0
- package/build/commands/make-invoice.js +21 -0
- package/build/commands/parse-invoice.js +14 -0
- package/build/commands/pay-invoice.js +19 -0
- package/build/commands/pay-keysend.js +27 -0
- package/build/commands/request-invoice.js +20 -0
- package/build/commands/sats-to-fiat.js +18 -0
- package/build/commands/settle-hold-invoice.js +17 -0
- package/build/commands/sign-message.js +17 -0
- package/build/commands/verify-preimage.js +18 -0
- package/build/commands/wait-for-payment.js +21 -0
- package/build/index.js +50 -0
- package/build/mcp_server.js +36 -0
- package/build/sse.js +50 -0
- package/build/streamable_http.js +49 -0
- package/build/test/fetch-l402.test.js +7 -0
- package/build/test/helpers.js +61 -0
- package/build/test/lightning-tools.test.js +36 -0
- package/build/test/nwc-hold-invoices.test.js +79 -0
- package/build/test/nwc-payments.test.js +42 -0
- package/build/test/nwc-readonly.test.js +41 -0
- package/build/tools/lightning/fetch_l402.js +32 -0
- package/build/tools/lightning/fiat_to_sats.js +10 -0
- package/build/tools/lightning/parse_invoice.js +8 -0
- package/build/tools/lightning/request_invoice.js +14 -0
- package/build/tools/lightning/sats_to_fiat.js +11 -0
- package/build/tools/lightning/schemas/invoice.js +20 -0
- package/build/tools/lightning/verify_preimage.js +9 -0
- package/build/tools/nwc/cancel_hold_invoice.js +8 -0
- package/build/tools/nwc/get_balance.js +6 -0
- package/build/tools/nwc/get_budget.js +14 -0
- package/build/tools/nwc/get_info.js +3 -0
- package/build/tools/nwc/get_wallet_service_info.js +3 -0
- package/build/tools/nwc/list_transactions.js +18 -0
- package/build/tools/nwc/lookup_invoice.js +11 -0
- package/build/tools/nwc/make_hold_invoice.js +12 -0
- package/build/tools/nwc/make_invoice.js +14 -0
- package/build/tools/nwc/pay_invoice.js +12 -0
- package/build/tools/nwc/pay_keysend.js +12 -0
- package/build/tools/nwc/schemas/transaction.js +26 -0
- package/build/tools/nwc/settle_hold_invoice.js +8 -0
- package/build/tools/nwc/sign_message.js +6 -0
- package/build/tools/nwc/wait_for_payment.js +43 -0
- package/build/utils.js +23 -0
- package/package.json +47 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { signMessage } from "../tools/nwc/sign_message.js";
|
|
2
|
+
import { getClient, handleError, output } from "../utils.js";
|
|
3
|
+
export function registerSignMessageCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command("sign-message")
|
|
6
|
+
.description("Sign a message with the wallet's key")
|
|
7
|
+
.requiredOption("-m, --message <text>", "Message to sign")
|
|
8
|
+
.action(async (options) => {
|
|
9
|
+
await handleError(async () => {
|
|
10
|
+
const client = getClient(program);
|
|
11
|
+
const result = await signMessage(client, {
|
|
12
|
+
message: options.message,
|
|
13
|
+
});
|
|
14
|
+
output(result);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { verifyPreimage } from "../tools/lightning/verify_preimage.js";
|
|
2
|
+
import { handleError, output } from "../utils.js";
|
|
3
|
+
export function registerVerifyPreimageCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command("verify-preimage")
|
|
6
|
+
.description("Verify a preimage against an invoice")
|
|
7
|
+
.requiredOption("-i, --invoice <bolt11>", "BOLT-11 invoice")
|
|
8
|
+
.requiredOption("--preimage <hex>", "Preimage to verify (32 bytes hex)")
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
await handleError(async () => {
|
|
11
|
+
const result = verifyPreimage({
|
|
12
|
+
invoice: options.invoice,
|
|
13
|
+
preimage: options.preimage,
|
|
14
|
+
});
|
|
15
|
+
output(result);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { waitForPayment } from "../tools/nwc/wait_for_payment.js";
|
|
2
|
+
import { getClient, handleError, output } from "../utils.js";
|
|
3
|
+
export function registerWaitForPaymentCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command("wait-for-payment")
|
|
6
|
+
.description("Wait for a payment notification")
|
|
7
|
+
.requiredOption("--payment-hash <hex>", "Payment hash to wait for")
|
|
8
|
+
.option("--type <type>", "Notification type filter: payment_received, payment_sent, hold_invoice_accepted")
|
|
9
|
+
.option("--timeout <seconds>", "Timeout in seconds", parseInt)
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
await handleError(async () => {
|
|
12
|
+
const client = getClient(program);
|
|
13
|
+
const result = await waitForPayment(client, {
|
|
14
|
+
payment_hash: options.paymentHash,
|
|
15
|
+
type: options.type,
|
|
16
|
+
timeout: options.timeout,
|
|
17
|
+
});
|
|
18
|
+
output(result);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerGetBalanceCommand } from "./commands/get-balance.js";
|
|
4
|
+
import { registerGetBudgetCommand } from "./commands/get-budget.js";
|
|
5
|
+
import { registerGetInfoCommand } from "./commands/get-info.js";
|
|
6
|
+
import { registerGetWalletServiceInfoCommand } from "./commands/get-wallet-service-info.js";
|
|
7
|
+
import { registerMakeInvoiceCommand } from "./commands/make-invoice.js";
|
|
8
|
+
import { registerMakeHoldInvoiceCommand } from "./commands/make-hold-invoice.js";
|
|
9
|
+
import { registerSettleHoldInvoiceCommand } from "./commands/settle-hold-invoice.js";
|
|
10
|
+
import { registerCancelHoldInvoiceCommand } from "./commands/cancel-hold-invoice.js";
|
|
11
|
+
import { registerPayInvoiceCommand } from "./commands/pay-invoice.js";
|
|
12
|
+
import { registerPayKeysendCommand } from "./commands/pay-keysend.js";
|
|
13
|
+
import { registerLookupInvoiceCommand } from "./commands/lookup-invoice.js";
|
|
14
|
+
import { registerListTransactionsCommand } from "./commands/list-transactions.js";
|
|
15
|
+
import { registerWaitForPaymentCommand } from "./commands/wait-for-payment.js";
|
|
16
|
+
import { registerSignMessageCommand } from "./commands/sign-message.js";
|
|
17
|
+
import { registerFiatToSatsCommand } from "./commands/fiat-to-sats.js";
|
|
18
|
+
import { registerSatsToFiatCommand } from "./commands/sats-to-fiat.js";
|
|
19
|
+
import { registerParseInvoiceCommand } from "./commands/parse-invoice.js";
|
|
20
|
+
import { registerVerifyPreimageCommand } from "./commands/verify-preimage.js";
|
|
21
|
+
import { registerRequestInvoiceCommand } from "./commands/request-invoice.js";
|
|
22
|
+
import { registerFetchL402Command } from "./commands/fetch-l402.js";
|
|
23
|
+
const program = new Command();
|
|
24
|
+
program
|
|
25
|
+
.name("alby-cli")
|
|
26
|
+
.description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools")
|
|
27
|
+
.version("0.0.0")
|
|
28
|
+
.option("-c, --connection-secret <string>", "NWC connection secret (nostr+walletconnect://...)");
|
|
29
|
+
// Register all commands
|
|
30
|
+
registerGetBalanceCommand(program);
|
|
31
|
+
registerGetBudgetCommand(program);
|
|
32
|
+
registerGetInfoCommand(program);
|
|
33
|
+
registerGetWalletServiceInfoCommand(program);
|
|
34
|
+
registerMakeInvoiceCommand(program);
|
|
35
|
+
registerMakeHoldInvoiceCommand(program);
|
|
36
|
+
registerSettleHoldInvoiceCommand(program);
|
|
37
|
+
registerCancelHoldInvoiceCommand(program);
|
|
38
|
+
registerPayInvoiceCommand(program);
|
|
39
|
+
registerPayKeysendCommand(program);
|
|
40
|
+
registerLookupInvoiceCommand(program);
|
|
41
|
+
registerListTransactionsCommand(program);
|
|
42
|
+
registerWaitForPaymentCommand(program);
|
|
43
|
+
registerSignMessageCommand(program);
|
|
44
|
+
registerFiatToSatsCommand(program);
|
|
45
|
+
registerSatsToFiatCommand(program);
|
|
46
|
+
registerParseInvoiceCommand(program);
|
|
47
|
+
registerVerifyPreimageCommand(program);
|
|
48
|
+
registerRequestInvoiceCommand(program);
|
|
49
|
+
registerFetchL402Command(program);
|
|
50
|
+
program.parse();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { webln } from "@getalby/sdk";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { registerGetInfoTool } from "./tools/nwc/get_info.js";
|
|
4
|
+
import { registerGetWalletServiceInfoTool } from "./tools/nwc/get_wallet_service_info.js";
|
|
5
|
+
import { registerLookupInvoiceTool } from "./tools/nwc/lookup_invoice.js";
|
|
6
|
+
import { registerMakeInvoiceTool } from "./tools/nwc/make_invoice.js";
|
|
7
|
+
import { registerPayInvoiceTool } from "./tools/nwc/pay_invoice.js";
|
|
8
|
+
import { registerGetBalanceTool } from "./tools/nwc/get_balance.js";
|
|
9
|
+
import { registerListTransactionsTool } from "./tools/nwc/list_transactions.js";
|
|
10
|
+
import { registerFetchL402Tool } from "./tools/lightning/fetch_l402.js";
|
|
11
|
+
import { registerFiatToSatsTool } from "./tools/lightning/fiat_to_sats.js";
|
|
12
|
+
import { registerParseInvoiceTool } from "./tools/lightning/parse_invoice.js";
|
|
13
|
+
import { registerRequestInvoiceFromLightningAddressTool } from "./tools/lightning/request_invoice.js";
|
|
14
|
+
export function createMCPServer(client) {
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: "@getalby/mcp",
|
|
17
|
+
version: "1.1.0",
|
|
18
|
+
title: "Alby MCP Server",
|
|
19
|
+
});
|
|
20
|
+
// NWC
|
|
21
|
+
registerGetWalletServiceInfoTool(server, client);
|
|
22
|
+
registerGetInfoTool(server, client);
|
|
23
|
+
registerMakeInvoiceTool(server, client);
|
|
24
|
+
registerPayInvoiceTool(server, client);
|
|
25
|
+
registerGetBalanceTool(server, client);
|
|
26
|
+
registerLookupInvoiceTool(server, client);
|
|
27
|
+
registerListTransactionsTool(server, client);
|
|
28
|
+
// Lightning tools
|
|
29
|
+
registerFetchL402Tool(server, new webln.NostrWebLNProvider({
|
|
30
|
+
client,
|
|
31
|
+
}));
|
|
32
|
+
registerFiatToSatsTool(server);
|
|
33
|
+
registerParseInvoiceTool(server);
|
|
34
|
+
registerRequestInvoiceFromLightningAddressTool(server);
|
|
35
|
+
return server;
|
|
36
|
+
}
|
package/build/sse.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { nwc } from "@getalby/sdk";
|
|
2
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
3
|
+
import { createMCPServer } from "./mcp_server.js";
|
|
4
|
+
import { getConnectionSecret } from "./auth.js";
|
|
5
|
+
export function addSSEEndpoints(app) {
|
|
6
|
+
// Store transports for each session type
|
|
7
|
+
// SSE is deprecated so we do not put much effort in here (e.g. cleaning up unused sessions)
|
|
8
|
+
const sessions = {};
|
|
9
|
+
app.get("/sse", async (req, res) => {
|
|
10
|
+
const nostrWalletConnectUrl = getConnectionSecret(req.header("Authorization"), req.query.nwc);
|
|
11
|
+
if (!nostrWalletConnectUrl) {
|
|
12
|
+
res
|
|
13
|
+
.status(400)
|
|
14
|
+
.send("Bearer auth with NWC connection secret or nwc query parameter not provided");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const client = new nwc.NWCClient({
|
|
18
|
+
nostrWalletConnectUrl,
|
|
19
|
+
});
|
|
20
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
21
|
+
const server = createMCPServer(client);
|
|
22
|
+
sessions[transport.sessionId] = {
|
|
23
|
+
server,
|
|
24
|
+
transport,
|
|
25
|
+
};
|
|
26
|
+
console.info("Created new SSE session", transport.sessionId);
|
|
27
|
+
if (req.query.sessionId) {
|
|
28
|
+
console.info("Request provided its own session ID: " + req.query.sessionId);
|
|
29
|
+
sessions[req.query.sessionId] = {
|
|
30
|
+
server,
|
|
31
|
+
transport,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
server.connect(transport);
|
|
35
|
+
});
|
|
36
|
+
app.post("/messages", (req, res) => {
|
|
37
|
+
const sessionId = req.query.sessionId;
|
|
38
|
+
console.info("SSE messages request", sessionId);
|
|
39
|
+
const session = sessions[sessionId];
|
|
40
|
+
if (session) {
|
|
41
|
+
console.info("Found session for messages request", sessionId);
|
|
42
|
+
session.transport.handlePostMessage(req, res);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
res
|
|
46
|
+
.status(400)
|
|
47
|
+
.send("No transport found for sessionId: " + req.query.sessionId);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createMCPServer } from "./mcp_server.js";
|
|
2
|
+
import { nwc } from "@getalby/sdk";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { getConnectionSecret } from "./auth.js";
|
|
5
|
+
import { json } from "express";
|
|
6
|
+
export function addStreamableHttpEndpoints(app) {
|
|
7
|
+
app.post("/mcp", json(), async (req, res) => {
|
|
8
|
+
// In stateless mode, create a new instance of transport and server for each request
|
|
9
|
+
// to ensure complete isolation. A single instance would cause request ID collisions
|
|
10
|
+
// when multiple clients connect concurrently.
|
|
11
|
+
try {
|
|
12
|
+
const nostrWalletConnectUrl = getConnectionSecret(req.header("Authorization"), req.query.nwc);
|
|
13
|
+
if (!nostrWalletConnectUrl) {
|
|
14
|
+
res
|
|
15
|
+
.status(400)
|
|
16
|
+
.send("Bearer auth with NWC connection secret or nwc query parameter not provided");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const client = new nwc.NWCClient({
|
|
20
|
+
nostrWalletConnectUrl,
|
|
21
|
+
});
|
|
22
|
+
const server = createMCPServer(client);
|
|
23
|
+
const transport = new StreamableHTTPServerTransport({
|
|
24
|
+
sessionIdGenerator: undefined,
|
|
25
|
+
});
|
|
26
|
+
res.on("close", () => {
|
|
27
|
+
console.log("Request closed");
|
|
28
|
+
transport.close();
|
|
29
|
+
server.close();
|
|
30
|
+
client.close();
|
|
31
|
+
});
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
await transport.handleRequest(req, res, req.body);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error("Error handling MCP request:", error);
|
|
37
|
+
if (!res.headersSent) {
|
|
38
|
+
res.status(500).json({
|
|
39
|
+
jsonrpc: "2.0",
|
|
40
|
+
error: {
|
|
41
|
+
code: -32603,
|
|
42
|
+
message: "Internal server error",
|
|
43
|
+
},
|
|
44
|
+
id: null,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
export function generateHoldInvoiceParams() {
|
|
4
|
+
const preimage = randomBytes(32).toString("hex");
|
|
5
|
+
const paymentHash = createHash("sha256")
|
|
6
|
+
.update(Buffer.from(preimage, "hex"))
|
|
7
|
+
.digest("hex");
|
|
8
|
+
return { preimage, paymentHash };
|
|
9
|
+
}
|
|
10
|
+
export async function createTestWallet(retries = 5) {
|
|
11
|
+
for (let i = 0; i < retries; i++) {
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch("https://faucet.nwc.dev?balance=10000", {
|
|
14
|
+
method: "POST",
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
if (i < retries - 1) {
|
|
18
|
+
await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Faucet request failed: ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
const nwcUrl = (await response.text()).trim();
|
|
24
|
+
const lud16Match = nwcUrl.match(/lud16=([^&\s]+)/);
|
|
25
|
+
if (!lud16Match) {
|
|
26
|
+
throw new Error("No lud16 in NWC URL");
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
nwcUrl,
|
|
30
|
+
lightningAddress: decodeURIComponent(lud16Match[1]),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (i < retries - 1) {
|
|
35
|
+
await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw new Error("Failed to create test wallet");
|
|
42
|
+
}
|
|
43
|
+
export function runCli(args) {
|
|
44
|
+
try {
|
|
45
|
+
const result = execSync(`node build/index.js ${args}`, {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
});
|
|
49
|
+
return { success: true, output: JSON.parse(result) };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const err = error;
|
|
53
|
+
const errorOutput = err.stderr || err.stdout || "{}";
|
|
54
|
+
try {
|
|
55
|
+
return { success: false, output: JSON.parse(errorOutput) };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { success: false, output: { error: errorOutput } };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { runCli } from "./helpers.js";
|
|
3
|
+
const exampleInvoice = "lnbc1u1p5hlrr8dqqnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp55utq6s8u5407namwt4jvhgsaf9fyszppjfwyxp7qsw6cyc8vxukqsp583usez9yhmkcavvvjz8cq56v3nglh2q37xkf4ufrgwxfrfjkm54s9qyysgqcqzp2xqyz5vqgtyysw64zt9sj6kfpqnekzwc37y2uyg0xdapgxqqth4uahff0x89sjfsvukjlllasg5dn05u2uha6qcvxz2y3ye5k7958qtes4pv4ggqtnjyky";
|
|
4
|
+
const exampleLightningAddress = "nwc1769966844@getalby.com";
|
|
5
|
+
describe("Lightning Tools (no wallet required)", () => {
|
|
6
|
+
test("fiat-to-sats converts USD to sats", () => {
|
|
7
|
+
const result = runCli("fiat-to-sats -a 1 --currency USD");
|
|
8
|
+
expect(result.success).toBe(true);
|
|
9
|
+
expect(result.output.amount_in_sats).toBeTypeOf("number");
|
|
10
|
+
expect(result.output.amount_in_sats).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
test("sats-to-fiat converts sats to USD", () => {
|
|
13
|
+
const result = runCli("sats-to-fiat -a 1000 --currency USD");
|
|
14
|
+
expect(result.success).toBe(true);
|
|
15
|
+
expect(result.output.amount).toBeTypeOf("number");
|
|
16
|
+
expect(result.output.amount).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
test("parse-invoice parses a BOLT-11 invoice", () => {
|
|
19
|
+
const result = runCli(`parse-invoice -i "${exampleInvoice}"`);
|
|
20
|
+
expect(result.success).toBe(true);
|
|
21
|
+
expect(result.output.paymentHash).toBeDefined();
|
|
22
|
+
expect(result.output.amount_in_sats).toBe(100);
|
|
23
|
+
});
|
|
24
|
+
test("verify-preimage returns false for invalid preimage", () => {
|
|
25
|
+
// Use a fake preimage (32 bytes of zeros)
|
|
26
|
+
const fakePreimage = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
27
|
+
const result = runCli(`verify-preimage -i "${exampleInvoice}" --preimage "${fakePreimage}"`);
|
|
28
|
+
expect(result.success).toBe(true);
|
|
29
|
+
expect(result.output.valid).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
test("request-invoice requests invoice from lightning address", async () => {
|
|
32
|
+
const result = runCli(`request-invoice -a "${exampleLightningAddress}" -s 100`);
|
|
33
|
+
expect(result.success).toBe(true);
|
|
34
|
+
expect(result.output.paymentRequest.toLowerCase()).toMatch(/^lnbc/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "vitest";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { createTestWallet, runCli, generateHoldInvoiceParams, } from "./helpers.js";
|
|
4
|
+
describe("NWC HOLD Invoice Commands", () => {
|
|
5
|
+
let sender;
|
|
6
|
+
let receiver;
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
// Create wallets sequentially to avoid faucet rate limiting
|
|
9
|
+
receiver = await createTestWallet();
|
|
10
|
+
sender = await createTestWallet();
|
|
11
|
+
}, 60000);
|
|
12
|
+
test("make-hold-invoice creates hold invoice", () => {
|
|
13
|
+
const { paymentHash } = generateHoldInvoiceParams();
|
|
14
|
+
const result = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
|
|
15
|
+
expect(result.success).toBe(true);
|
|
16
|
+
expect(result.output.invoice).toBeDefined();
|
|
17
|
+
expect(result.output.payment_hash).toBe(paymentHash);
|
|
18
|
+
});
|
|
19
|
+
test("settle-hold-invoice settles with preimage", async () => {
|
|
20
|
+
const { preimage, paymentHash } = generateHoldInvoiceParams();
|
|
21
|
+
// Create a hold invoice
|
|
22
|
+
const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
|
|
23
|
+
expect(holdResult.success).toBe(true);
|
|
24
|
+
// Start wait-for-payment in background
|
|
25
|
+
const waitProcess = spawn("node", [
|
|
26
|
+
"build/index.js",
|
|
27
|
+
"-c",
|
|
28
|
+
receiver.nwcUrl,
|
|
29
|
+
"wait-for-payment",
|
|
30
|
+
"-p",
|
|
31
|
+
paymentHash,
|
|
32
|
+
"-t",
|
|
33
|
+
"30",
|
|
34
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
35
|
+
// Give it time to start listening
|
|
36
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
37
|
+
// Pay the invoice from sender (this will be held)
|
|
38
|
+
const payProcess = spawn("node", [
|
|
39
|
+
"build/index.js",
|
|
40
|
+
"-c",
|
|
41
|
+
sender.nwcUrl,
|
|
42
|
+
"pay-invoice",
|
|
43
|
+
"-i",
|
|
44
|
+
holdResult.output.invoice,
|
|
45
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
46
|
+
// Wait for the hold invoice to be accepted
|
|
47
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
48
|
+
// Settle the hold invoice
|
|
49
|
+
const settleResult = runCli(`-c "${receiver.nwcUrl}" settle-hold-invoice --preimage "${preimage}"`);
|
|
50
|
+
expect(settleResult.success).toBe(true);
|
|
51
|
+
expect(settleResult.output.preimage).toBe(preimage);
|
|
52
|
+
// Cleanup
|
|
53
|
+
waitProcess.kill();
|
|
54
|
+
payProcess.kill();
|
|
55
|
+
}, 60000);
|
|
56
|
+
test("cancel-hold-invoice cancels hold invoice", async () => {
|
|
57
|
+
const { paymentHash } = generateHoldInvoiceParams();
|
|
58
|
+
// Create a hold invoice
|
|
59
|
+
const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
|
|
60
|
+
expect(holdResult.success).toBe(true);
|
|
61
|
+
// Pay the invoice from sender (this will put it in held state)
|
|
62
|
+
const payProcess = spawn("node", [
|
|
63
|
+
"build/index.js",
|
|
64
|
+
"-c",
|
|
65
|
+
sender.nwcUrl,
|
|
66
|
+
"pay-invoice",
|
|
67
|
+
"-i",
|
|
68
|
+
holdResult.output.invoice,
|
|
69
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
70
|
+
// Wait for the hold invoice to be in held state
|
|
71
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
72
|
+
// Cancel the hold invoice (while it's held)
|
|
73
|
+
const cancelResult = runCli(`-c "${receiver.nwcUrl}" cancel-hold-invoice --payment-hash "${paymentHash}"`);
|
|
74
|
+
expect(cancelResult.success).toBe(true);
|
|
75
|
+
expect(cancelResult.output.payment_hash).toBe(paymentHash);
|
|
76
|
+
// Cleanup
|
|
77
|
+
payProcess.kill();
|
|
78
|
+
}, 60000);
|
|
79
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "vitest";
|
|
2
|
+
import { createTestWallet, runCli } from "./helpers.js";
|
|
3
|
+
describe("NWC Payment Commands", () => {
|
|
4
|
+
let sender;
|
|
5
|
+
let receiver;
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
// Create wallets sequentially to avoid faucet rate limiting
|
|
8
|
+
sender = await createTestWallet();
|
|
9
|
+
receiver = await createTestWallet();
|
|
10
|
+
}, 60000);
|
|
11
|
+
test("make-invoice and pay-invoice", () => {
|
|
12
|
+
// Create invoice with receiver wallet
|
|
13
|
+
const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 100`);
|
|
14
|
+
expect(invoiceResult.success).toBe(true);
|
|
15
|
+
expect(invoiceResult.output.invoice).toBeDefined();
|
|
16
|
+
// Pay with sender wallet
|
|
17
|
+
const paymentResult = runCli(`-c "${sender.nwcUrl}" pay-invoice -i "${invoiceResult.output.invoice}"`);
|
|
18
|
+
expect(paymentResult.success).toBe(true);
|
|
19
|
+
expect(paymentResult.output.preimage).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
test("lookup-invoice finds paid invoice", () => {
|
|
22
|
+
// Create an invoice
|
|
23
|
+
const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 50`);
|
|
24
|
+
expect(invoiceResult.success).toBe(true);
|
|
25
|
+
// Pay the invoice first (unpaid invoices may not be found)
|
|
26
|
+
const payResult = runCli(`-c "${sender.nwcUrl}" pay-invoice -i "${invoiceResult.output.invoice}"`);
|
|
27
|
+
expect(payResult.success).toBe(true);
|
|
28
|
+
// Lookup the paid invoice using the invoice string
|
|
29
|
+
const lookupResult = runCli(`-c "${receiver.nwcUrl}" lookup-invoice -i "${invoiceResult.output.invoice}"`);
|
|
30
|
+
expect(lookupResult.success).toBe(true);
|
|
31
|
+
expect(lookupResult.output.payment_hash).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
test("pay-keysend sends keysend payment", () => {
|
|
34
|
+
// Get receiver's pubkey
|
|
35
|
+
const infoResult = runCli(`-c "${receiver.nwcUrl}" get-info`);
|
|
36
|
+
expect(infoResult.success).toBe(true);
|
|
37
|
+
// Send keysend payment
|
|
38
|
+
const keysendResult = runCli(`-c "${sender.nwcUrl}" pay-keysend -p "${infoResult.output.pubkey}" -a 100`);
|
|
39
|
+
expect(keysendResult.success).toBe(true);
|
|
40
|
+
expect(keysendResult.output.preimage).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "vitest";
|
|
2
|
+
import { createTestWallet, runCli } from "./helpers.js";
|
|
3
|
+
describe("NWC Read-only Commands", () => {
|
|
4
|
+
let wallet;
|
|
5
|
+
beforeAll(async () => {
|
|
6
|
+
wallet = await createTestWallet();
|
|
7
|
+
}, 60000);
|
|
8
|
+
test("get-balance returns wallet balance", () => {
|
|
9
|
+
const result = runCli(`-c "${wallet.nwcUrl}" get-balance`);
|
|
10
|
+
expect(result.success).toBe(true);
|
|
11
|
+
expect(result.output.amount_in_sats).toBeTypeOf("number");
|
|
12
|
+
});
|
|
13
|
+
test("get-budget returns budget info", () => {
|
|
14
|
+
const result = runCli(`-c "${wallet.nwcUrl}" get-budget`);
|
|
15
|
+
expect(result.success).toBe(true);
|
|
16
|
+
expect(result.output).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
test("get-info returns wallet info", () => {
|
|
19
|
+
const result = runCli(`-c "${wallet.nwcUrl}" get-info`);
|
|
20
|
+
expect(result.success).toBe(true);
|
|
21
|
+
expect(result.output.alias).toBeDefined();
|
|
22
|
+
expect(result.output.pubkey).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
test("get-wallet-service-info returns service capabilities", () => {
|
|
25
|
+
const result = runCli(`-c "${wallet.nwcUrl}" get-wallet-service-info`);
|
|
26
|
+
expect(result.success).toBe(true);
|
|
27
|
+
expect(Array.isArray(result.output.capabilities)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
test("list-transactions returns transaction list", () => {
|
|
30
|
+
const result = runCli(`-c "${wallet.nwcUrl}" list-transactions`);
|
|
31
|
+
expect(result.success).toBe(true);
|
|
32
|
+
expect(Array.isArray(result.output.transactions)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
// NOTE: Faucet wallets don't have sign_message scope
|
|
35
|
+
test.skip("sign-message signs a message", () => {
|
|
36
|
+
const testMessage = "Hello, World!";
|
|
37
|
+
const result = runCli(`-c "${wallet.nwcUrl}" sign-message -m "${testMessage}"`);
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
expect(result.output.signature).toBeTypeOf("string");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { fetchWithL402 } from "@getalby/lightning-tools";
|
|
2
|
+
export async function fetchL402(client, params) {
|
|
3
|
+
const requestOptions = {
|
|
4
|
+
method: params.method,
|
|
5
|
+
};
|
|
6
|
+
if (params.method && params.method !== "GET" && params.method !== "HEAD") {
|
|
7
|
+
requestOptions.body = params.body;
|
|
8
|
+
requestOptions.headers = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
...params.headers,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
else if (params.headers) {
|
|
14
|
+
requestOptions.headers = params.headers;
|
|
15
|
+
}
|
|
16
|
+
const webln = {
|
|
17
|
+
sendPayment: async (invoice) => {
|
|
18
|
+
const result = await client.payInvoice({ invoice });
|
|
19
|
+
return { preimage: result.preimage };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const result = await fetchWithL402(params.url, requestOptions, {
|
|
23
|
+
webln: webln,
|
|
24
|
+
});
|
|
25
|
+
const responseContent = await result.text();
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
throw new Error(`fetch returned non-OK status: ${result.status} ${responseContent}`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
content: responseContent,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LightningAddress } from "@getalby/lightning-tools";
|
|
2
|
+
export async function requestInvoice(params) {
|
|
3
|
+
const ln = new LightningAddress(params.lightning_address);
|
|
4
|
+
await ln.fetch();
|
|
5
|
+
const { satoshi, ...invoice } = await ln.requestInvoice({
|
|
6
|
+
satoshi: params.amount_in_sats,
|
|
7
|
+
comment: params.comment,
|
|
8
|
+
payerdata: params.payer_data,
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
...invoice,
|
|
12
|
+
amount_in_sats: satoshi,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getFiatValue } from "@getalby/lightning-tools";
|
|
2
|
+
export async function satsToFiat(params) {
|
|
3
|
+
const fiat = await getFiatValue({
|
|
4
|
+
satoshi: params.amount_in_sats,
|
|
5
|
+
currency: params.currency,
|
|
6
|
+
});
|
|
7
|
+
return {
|
|
8
|
+
amount: fiat,
|
|
9
|
+
currency: params.currency,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const invoiceSchema = {
|
|
3
|
+
paymentRequest: z.string().describe("The BOLT-11 payment request"),
|
|
4
|
+
paymentHash: z.string().describe("Payment hash"),
|
|
5
|
+
preimage: z.string().nullable().describe("Payment preimage if available"),
|
|
6
|
+
verify: z
|
|
7
|
+
.string()
|
|
8
|
+
.nullable()
|
|
9
|
+
.describe("URL to verify if the email was paid (LNURL-verify)"),
|
|
10
|
+
amount_in_sats: z.number().describe("Amount in sats"),
|
|
11
|
+
expiry: z.number().nullish().describe("Expiry time in seconds"),
|
|
12
|
+
timestamp: z.number().describe("Creation unix timestamp"),
|
|
13
|
+
createdDate: z.string().describe("Creation date string"),
|
|
14
|
+
expiryDate: z.string().nullish().describe("Expiry date string"),
|
|
15
|
+
description: z.string().nullable().describe("Invoice description"),
|
|
16
|
+
successAction: z
|
|
17
|
+
.unknown()
|
|
18
|
+
.nullable()
|
|
19
|
+
.describe("Success action to initiate after the invoice has been paid"),
|
|
20
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Invoice } from "@getalby/lightning-tools";
|
|
2
|
+
export function verifyPreimage(params) {
|
|
3
|
+
const invoice = new Invoice({ pr: params.invoice });
|
|
4
|
+
const valid = invoice.validatePreimage(params.preimage);
|
|
5
|
+
return {
|
|
6
|
+
valid,
|
|
7
|
+
payment_hash: invoice.paymentHash,
|
|
8
|
+
};
|
|
9
|
+
}
|