Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: NWA #298

Merged
merged 27 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cd6fe4a
feat: add create_connection method to NWCClient
rolznz Dec 20, 2024
b45bd70
feat: new NWC deeplink flow to support other relays and wallet pubkey…
rolznz Jan 18, 2025
6f098bb
feat: add NWA (WIP)
rolznz Jan 18, 2025
4218004
Merge branch 'feat/nwc-create-connection' into feat/nwa
rolznz Jan 20, 2025
61903bc
feat: accept NWA connections
rolznz Jan 20, 2025
8cf4d15
fix: read explicit nwc deeplink success parameters
rolznz Feb 1, 2025
df1560a
Merge branch 'feat/nwc-new-deeplink-flow' into feat/nwa
rolznz Feb 1, 2025
58b42d7
chore: use nwc info event instead of nwa event
rolznz Feb 1, 2025
eecf0f7
feat: finish NWA implementation
rolznz Feb 25, 2025
f8c8b34
Merge remote-tracking branch 'origin/master' into feat/nwa
rolznz Feb 25, 2025
253e186
fix: add missing options to NWCClient
rolznz Feb 25, 2025
568371a
fix: rename nwa budget renewal option to be consistent
rolznz Feb 25, 2025
06db3fc
fix: do not read lud16 from tag
rolznz Feb 25, 2025
1753fbf
feat: add fetching lud16 to NWA subscription event handler
rolznz Feb 26, 2025
6a171a8
chore: bump beta version
rolznz Feb 26, 2025
ea5eba2
fix: add missing name and icon to NWAClient
rolznz Feb 26, 2025
9f31c7f
fix: naming of budget renewal field
rolznz Feb 26, 2025
4208466
feat: add return_to to nwa
rolznz Feb 26, 2025
1920e4d
feat: get nwa connection uri with custom scheme suffix
rolznz Feb 26, 2025
b39bbe5
docs: add NWA section to README
rolznz Feb 26, 2025
290f415
fix: notification types field in Nip47CreateConnectionRequest
rolznz Feb 27, 2025
02efe17
fix: close child client in test
rolznz Feb 28, 2025
567cc8c
fix: parse different nwa scheme formats, DRY code, remove appSecretKe…
rolznz Feb 28, 2025
2bcc58f
chore: print QR code in nwa example for better devX
im-adithya Feb 28, 2025
ba4e037
fix: nwa client test
im-adithya Feb 28, 2025
ce7194b
fix: order of fields in NWAOptions
rolznz Feb 28, 2025
4ea816e
docs: remove duplicated documentation
rolznz Feb 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,20 @@ const nwc = await webln.NostrWebLNProvider.fromAuthorizationUrl(

The same options can be provided to getAuthorizationUrl() as fromAuthorizationUrl() - see [Manual Auth example](./examples/nwc/auth_manual.html)

### Nostr Wallet Auth

NWA is an alternative flow for lightning apps to easily initialize an NWC connection to mobile-first or self-custodial wallets, using a client-created secret.

The app will generate an NWA URI which should be opened in the wallet, where the user can approve the connection.

#### Generating an NWA URI

See [NWA example](examples/nwc/client/nwa.js)

### Accepting and creating a connection from an NWA URI

See [NWA accept example](examples/nwc/client/nwa.js) for NWA URI parsing and handling. The implementation of actually creating the connection and showing a confirmation page to the user is wallet-specific. In the example, a connection will be created via the `create_connection` NWC command.

## OAuth API Documentation

Please have a look a the Alby OAuth2 Wallet API:
Expand Down
53 changes: 53 additions & 0 deletions examples/nwc/client/create-connection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import "websocket-polyfill"; // required in node.js

import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { nwc } from "../../../dist/index.module.js";
import { generateSecretKey, getPublicKey } from "nostr-tools";

import { bytesToHex } from "@noble/hashes/utils";

const rl = readline.createInterface({ input, output });

const nwcUrl =
process.env.NWC_URL ||
(await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): "));
rl.close();

const client = new nwc.NWCClient({
nostrWalletConnectUrl: nwcUrl,
});

let secretKey = generateSecretKey();
let pubkey = getPublicKey(secretKey);

const response = await client.createConnection({
pubkey,
name: "Test created app from JS SDK " + new Date().toISOString(),
request_methods: [
"get_info",
"get_balance",
"get_budget",
"make_invoice",
"pay_invoice",
"lookup_invoice",
"list_transactions",
"sign_message",
],
});

console.info(response);

client.close();

const childClient = new nwc.NWCClient({
relayUrl: client.relayUrl,
secret: bytesToHex(secretKey),
walletPubkey: response.wallet_pubkey,
});

const info = await childClient.getInfo();
console.info("Got info from created app", info);

childClient.close();
49 changes: 49 additions & 0 deletions examples/nwc/client/nwa-accept.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import "websocket-polyfill"; // required in node.js

import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { nwa, nwc } from "../../../dist/index.module.js";

const rl = readline.createInterface({ input, output });

const nwcUrl =
process.env.NWC_URL ||
(await rl.question(
"Nostr Wallet Connect URL (nostr+walletconnect://...) with create_connection method: ",
));

const client = new nwc.NWCClient({
nostrWalletConnectUrl: nwcUrl,
});
const infoResponse = await client.getInfo();

if (infoResponse.methods.indexOf("create_connection") < 0) {
console.error("this connection does not support NWC create_connection");
process.exit(1);
}

const nwaUrl = await rl.question(
"Nostr Wallet Auth URL (nostr+walletauth://...): ",
);

const nwaOptions = nwa.NWAClient.parseWalletAuthUrl(nwaUrl);

// (here the user would choose to accept the connection)

const createAppResponse = await client.createConnection({
pubkey: nwaOptions.appPubkey,
name: nwaOptions.name || "NWA test " + new Date().toISOString(),
request_methods: nwaOptions.requestMethods,
notification_types: nwaOptions.notificationTypes,
max_amount: nwaOptions.maxAmount,
budget_renewal: nwaOptions.budgetRenewal,
expires_at: nwaOptions.expiresAt,
isolated: nwaOptions.isolated,
metadata: nwaOptions.metadata,
});

console.info(createAppResponse);

rl.close();
client.close();
41 changes: 41 additions & 0 deletions examples/nwc/client/nwa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "websocket-polyfill"; // required in node.js
import qrcode from "qrcode-terminal";

import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { nwa } from "../../../dist/index.module.js";

const rl = readline.createInterface({ input, output });

const DEFAULT_RELAY_URL = "wss://relay.getalby.com/v1";

const relayUrl =
(await rl.question(`Relay URL (${DEFAULT_RELAY_URL}): `)) ||
DEFAULT_RELAY_URL;
rl.close();

const nwaClient = new nwa.NWAClient({
relayUrl,
requestMethods: ["get_info"],
});

console.info("Scan or enter the following NWA connection URI in your wallet:");

// this prints the QR code
qrcode.generate(nwaClient.connectionUri, { small: true });

console.info(nwaClient.connectionUri);

console.info("\nWaiting for connection...");

await nwaClient.subscribe({
onSuccess: async (nwcClient) => {
console.info("NWA successful", nwcClient.options);
const response = await nwcClient.getInfo();

console.info(response);

nwcClient.close();
},
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@getalby/sdk",
"version": "4.0.0",
"version": "4.1.0-beta.9",
"description": "The SDK to integrate with Nostr Wallet Connect and the Alby API",
"repository": "https://github.com/getAlby/js-sdk.git",
"bugs": "https://github.com/getAlby/js-sdk/issues",
Expand Down Expand Up @@ -57,6 +57,7 @@
"lint-staged": "^15.0.1",
"microbundle": "^0.15.1",
"prettier": "^3.0.1",
"qrcode-terminal": "^0.12.0",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"typescript": "^5.1.6",
Expand Down
99 changes: 99 additions & 0 deletions src/NWAClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import "websocket-polyfill";
import { NWAClient } from "./NWAClient";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { generateSecretKey, getPublicKey } from "nostr-tools";
import { Nip47Method, Nip47NotificationType } from "./NWCClient";

describe("NWA URI", () => {
test("constructs correct connection URI with custom app secret key", () => {
const appSecretKey = bytesToHex(generateSecretKey());
const appPubkey = getPublicKey(hexToBytes(appSecretKey));

const nwaClient = new NWAClient({
relayUrl: "wss://relay.getalby.com/v1",
appSecretKey,
requestMethods: ["get_info"],
});

expect(nwaClient.connectionUri).toEqual(
`nostr+walletauth://${appPubkey}?relay=${encodeURIComponent(nwaClient.options.relayUrl)}&request_methods=get_info`,
);
});
test("constructs correct connection URI", () => {
const expiresAt = Date.now() + 1000 * 60 * 60 * 24 * 30; // 30 days
const maxAmount = 1000_000; // 1000 sats
const nwaClient = new NWAClient({
name: "App Name",
icon: "https://example.com/image.png",
relayUrl: "wss://relay.getalby.com/v1",
requestMethods: ["get_info", "pay_invoice"],
notificationTypes: ["payment_received", "payment_sent"],
expiresAt,
budgetRenewal: "monthly",
maxAmount,
isolated: true,
metadata: { message: "hello world" },
returnTo: "https://example.com",
});

expect(nwaClient.connectionUri).toEqual(
`nostr+walletauth://${nwaClient.options.appPubkey}?relay=wss%3A%2F%2Frelay.getalby.com%2Fv1&request_methods=get_info%20pay_invoice&name=App%20Name&icon=https%3A%2F%2Fexample.com%2Fimage.png&return_to=https%3A%2F%2Fexample.com&notification_types=payment_received%20payment_sent&max_amount=${maxAmount}&budget_renewal=monthly&expires_at=${expiresAt}&isolated=true&metadata=%7B%22message%22%3A%22hello%20world%22%7D`,
);
});

test("constructs correct connection URI for specific app", () => {
const nwaClient = new NWAClient({
relayUrl: "wss://relay.getalby.com/v1",
requestMethods: ["get_info"],
});

expect(nwaClient.getConnectionUri("alby")).toEqual(
`nostr+walletauth+alby://${nwaClient.options.appPubkey}?relay=wss%3A%2F%2Frelay.getalby.com%2Fv1&request_methods=get_info`,
);
});

for (const scheme of [
"nostr+walletauth://",
"nostr+walletauth:",
"nostr+walletauth+alby://",
"nostr+walletauth+alby:",
]) {
test(`parses connection URI (${scheme})`, () => {
const nwaOptions = NWAClient.parseWalletAuthUrl(
`${scheme}e73575d76c731102aefd4eb6fb0ddfaaf335eabe60255a22e6ca5e7074eb4992?relay=wss%3A%2F%2Frelay.getalby.com%2Fv1&request_methods=get_info%20pay_invoice&name=App%20Name&icon=https%3A%2F%2Fexample.com%2Fimage.png&return_to=https%3A%2F%2Fexample.com&notification_types=payment_received%20payment_sent&max_amount=1000000&budget_renewal=monthly&expires_at=1740470142968&isolated=true&metadata=%7B%22message%22%3A%22hello%20world%22%7D`,
);

expect(nwaOptions.appPubkey).toEqual(
"e73575d76c731102aefd4eb6fb0ddfaaf335eabe60255a22e6ca5e7074eb4992",
);
expect(nwaOptions.relayUrl).toEqual("wss://relay.getalby.com/v1");
expect(nwaOptions.requestMethods).toEqual([
"get_info",
"pay_invoice",
] satisfies Nip47Method[]);
expect(nwaOptions.notificationTypes).toEqual([
"payment_received",
"payment_sent",
] satisfies Nip47NotificationType[]);
expect(nwaOptions.expiresAt).toBe(1740470142968);
expect(nwaOptions.maxAmount).toBe(1000_000);
expect(nwaOptions.budgetRenewal).toBe("monthly");
expect(nwaOptions.isolated).toBe(true);
expect(nwaOptions.metadata).toEqual({ message: "hello world" });
expect(nwaOptions.name).toBe("App Name");
expect(nwaOptions.icon).toBe("https://example.com/image.png");
expect(nwaOptions.returnTo).toBe("https://example.com");
});
}

test("incorrect scheme", () => {
try {
NWAClient.parseWalletAuthUrl("asd://");
fail("should not pass");
} catch (error) {
expect("" + error).toBe(
"Error: Unexpected scheme. Should be nostr+walletauth:// or nostr+walletauth+specificapp://",
);
}
});
});
Loading