Skip to content

Commit

Permalink
feat: new NWC deeplink flow to support other relays and wallet pubkey…
Browse files Browse the repository at this point in the history
…s (WIP)
  • Loading branch information
rolznz committed Jan 18, 2025
1 parent 943acf3 commit b45bd70
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 92 deletions.
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ There are two interfaces you can use to access NWC:
- `secret`: secret key to sign the request event (if not available window.nostr will be used)
- `authorizationUrl`: URL to the NWC interface for the user to and the app connection

#### `static newClientFromAuthorizationUrl()`

Initialized a new `NWCClient` instance but generates a new random secret. The pubkey of that secret then needs to be authorized by the user (this can be initiated by redirecting the user to the `getAuthorizationUrl()` URL or calling `fromAuthorizationUrl()` to open an authorization popup.

##### Example

```js
const nwcClient = await nwc.NWCClient.fromAuthorizationUrl(
"https://my.albyhub.com/apps/new",
{
name: "My app name",
},
);
```

#### Quick start example

```js
Expand Down Expand Up @@ -115,17 +130,6 @@ if (!window.webln) {

The goal of the Nostr Wallet Connect provider is to be API compatible with [webln](https://www.webln.guide/). Currently not all methods are supported - see the examples/nwc directory for a list of supported methods.

#### `static withNewSecret()`

Initialized a new `NostrWebLNProvider` instance but generates a new random secret. The pubkey of that secret then needs to be authorized by the user (this can be initiated by redirecting the user to the `getAuthorizationUrl()` URL or calling `initNWC()` to open an authorization popup.

##### Example

```js
const nwc = NostrWebLNProvider.withNewSecret();
await nwc.initNWC();
```

#### sendPayment(invice: string)

Takes a bolt11 invoice and calls the NWC `pay_invoice` function.
Expand Down
20 changes: 20 additions & 0 deletions examples/nwc/client/auth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script type="module">
// TODO: change to proper package
import { nwc } from "https://esm.sh/[email protected]"; // jsdelivr.net, skypack.dev also work
window.launchNwc = async () => {
try {
// TODO: change default to my.albyhub.com
const authUrl = prompt("Auth URL", "http://localhost:5173/apps/new");
const nwcClient = await nwc.NWCClient.fromAuthorizationUrl(authUrl, {
name: "Deeplink " + Date.now(),
});
const result = await nwcClient.getInfo();
alert("Info response: " + JSON.stringify(result));
} catch (error) {
console.error(error);
alert("Something went wrong: " + error);
}
};
</script>

<button onclick="window.launchNwc()">Connect NWC</button>
41 changes: 41 additions & 0 deletions src/NWCClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,44 @@ describe("NWCClient", () => {
expect(nwcClient.options.lud16).toBe("[email protected]");
});
});

describe("getAuthorizationUrl", () => {
test("standard url", () => {
const pubkey =
"c5dc47856f533dad6c016b979ee3b21f83f88ae0f0058001b67a4b348339fe94";

expect(
NWCClient.getAuthorizationUrl(
"https://nwc.getalby.com/apps/new",
{
budgetRenewal: "weekly",
editable: false,
expiresAt: new Date("2023-07-21"),
maxAmount: 100,
name: "TestApp",
returnTo: "https://example.com",
requestMethods: ["pay_invoice", "get_balance"],
},
pubkey,
).toString(),
).toEqual(
`https://nwc.getalby.com/apps/new?name=TestApp&pubkey=${pubkey}&return_to=https%3A%2F%2Fexample.com&budget_renewal=weekly&expires_at=1689897600&max_amount=100&editable=false&request_methods=pay_invoice+get_balance`,
);
});

test("hash router url is not supported", () => {
const pubkey =
"c5dc47856f533dad6c016b979ee3b21f83f88ae0f0058001b67a4b348339fe94";

try {
NWCClient.getAuthorizationUrl(
"https://my.albyhub.com/#/apps/new",
{},
pubkey,
);
fail("error should have been thrown");
} catch (error) {
expect("" + error).toEqual("Error: hash router paths not supported");
}
});
});
86 changes: 49 additions & 37 deletions src/NWCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ export type Nip47SignMessageResponse = {
};

export interface NWCOptions {
authorizationUrl?: string; // the URL to the NWC interface for the user to confirm the session
relayUrl: string;
walletPubkey: string;
secret?: string;
Expand Down Expand Up @@ -209,18 +208,7 @@ export class Nip47UnexpectedResponseError extends Nip47Error {}
export class Nip47NetworkError extends Nip47Error {}
export class Nip47UnsupportedVersionError extends Nip47Error {}

export const NWCs: Record<string, NWCOptions> = {
alby: {
authorizationUrl: "https://nwc.getalby.com/apps/new",
relayUrl: "wss://relay.getalby.com/v1",
walletPubkey:
"69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9",
},
};

export type NewNWCClientOptions = {
providerName?: string;
authorizationUrl?: string;
relayUrl?: string;
secret?: string;
walletPubkey?: string;
Expand Down Expand Up @@ -267,22 +255,14 @@ export class NWCClient {
return options;
}

static withNewSecret(options?: ConstructorParameters<typeof NWCClient>[0]) {
options = options || {};
options.secret = bytesToHex(generateSecretKey());
return new NWCClient(options);
}

constructor(options?: NewNWCClientOptions) {
if (options && options.nostrWalletConnectUrl) {
options = {
...NWCClient.parseWalletConnectUrl(options.nostrWalletConnectUrl),
...options,
};
}
const providerOptions = NWCs[options?.providerName || "alby"] as NWCOptions;
this.options = {
...providerOptions,
...(options || {}),
} as NWCOptions;

Expand Down Expand Up @@ -388,49 +368,70 @@ export class NWCClient {
return decrypted;
}

getAuthorizationUrl(options?: NWCAuthorizationUrlOptions): URL {
if (!this.options.authorizationUrl) {
throw new Error("Missing authorizationUrl option");
static getAuthorizationUrl(
authorizationBasePath: string,
options: NWCAuthorizationUrlOptions = {},
pubkey: string,
): URL {
if (authorizationBasePath.indexOf("/#/") > -1) {
throw new Error("hash router paths not supported");
}
const url = new URL(this.options.authorizationUrl);
if (options?.name) {
url.searchParams.set("name", options?.name);
const url = new URL(authorizationBasePath);
if (options.name) {
url.searchParams.set("name", options.name);
}
url.searchParams.set("pubkey", this.publicKey);
if (options?.returnTo) {
url.searchParams.set("pubkey", pubkey);
if (options.returnTo) {
url.searchParams.set("return_to", options.returnTo);
}

if (options?.budgetRenewal) {
if (options.budgetRenewal) {
url.searchParams.set("budget_renewal", options.budgetRenewal);
}
if (options?.expiresAt) {
if (options.expiresAt) {
url.searchParams.set(
"expires_at",
Math.floor(options.expiresAt.getTime() / 1000).toString(),
);
}
if (options?.maxAmount) {
if (options.maxAmount) {
url.searchParams.set("max_amount", options.maxAmount.toString());
}
if (options?.editable !== undefined) {
if (options.editable !== undefined) {
url.searchParams.set("editable", options.editable.toString());
}

if (options?.requestMethods) {
if (options.requestMethods) {
url.searchParams.set("request_methods", options.requestMethods.join(" "));
}

return url;
}

initNWC(options: NWCAuthorizationUrlOptions = {}) {
/**
* create a new client-initiated NWC connection via HTTP deeplink
*
* @authorizationBasePath the deeplink path e.g. https://my.albyhub.com/apps/new
* @options configure the created app (e.g. the name, budget, expiration)
* @secret optionally pass a secret, otherwise one will be generated.
*/
static fromAuthorizationUrl(
authorizationBasePath: string,
options: NWCAuthorizationUrlOptions = {},
secret?: string,
): Promise<NWCClient> {
secret = secret || bytesToHex(generateSecretKey());

// here we assume an browser context and window/document is available
// we set the location.host as a default name if none is given
if (!options.name) {
options.name = document.location.host;
}
const url = this.getAuthorizationUrl(options);
const url = this.getAuthorizationUrl(
authorizationBasePath,
options,
getPublicKey(hexToBytes(secret)),
);
const height = 600;
const width = 400;
const top = window.outerHeight / 2 + window.screenY - height / 2;
Expand All @@ -456,7 +457,10 @@ export class NWCClient {
};

const onMessage = (message: {
data?: { type: "nwc:success" | unknown };
data?: {
type: "nwc:success" | unknown;
nostrWalletConnectUrl?: string;
};
origin: string;
}) => {
const data = message.data;
Expand All @@ -465,7 +469,15 @@ export class NWCClient {
data.type === "nwc:success" &&
message.origin === `${url.protocol}//${url.host}`
) {
resolve(data);
if (!data.nostrWalletConnectUrl) {
reject(new Error("no nostrWalletConnectUrl in response"));
}
resolve(
new NWCClient({
nostrWalletConnectUrl: data.nostrWalletConnectUrl,
secret,
}),
);
clearInterval(popupChecker);
window.removeEventListener("message", onMessage);
if (popup) {
Expand Down
25 changes: 0 additions & 25 deletions src/webln/NostrWeblnProvider.test.ts

This file was deleted.

19 changes: 0 additions & 19 deletions src/webln/NostrWeblnProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
MakeInvoiceResponse,
} from "@webbtc/webln-types";
import { GetInfoResponse } from "@webbtc/webln-types";
import { NWCAuthorizationUrlOptions } from "../types";
import {
NWCClient,
NWCOptions,
Expand Down Expand Up @@ -226,24 +225,6 @@ export class NostrWebLNProvider implements WebLNProvider, Nip07Provider {
return this.client.decrypt(pubkey, content);
}

/**
* @deprecated please use client.getAuthorizationUrl. Deprecated since v3.2.3. Will be removed in v4.0.0.
*/
getAuthorizationUrl(options?: NWCAuthorizationUrlOptions) {
console.warn(
"getAuthorizationUrl is deprecated. Please use client.getAuthorizationUrl instead.",
);
return this.client.getAuthorizationUrl(options);
}

/**
* @deprecated please use client.initNWC. Deprecated since v3.2.3. Will be removed in v4.0.0.
*/
initNWC(options: NWCAuthorizationUrlOptions = {}) {
console.warn("initNWC is deprecated. Please use client.initNWC instead.");
return this.client.initNWC(options);
}

async getInfo(): Promise<GetInfoResponse> {
await this.checkEnabled();

Expand Down

0 comments on commit b45bd70

Please sign in to comment.