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: add nip 44 and versioning support #273

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
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
105 changes: 99 additions & 6 deletions src/NWCClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
nip04,
nip19,
nip44,
finalizeEvent,
generateSecretKey,
getEventHash,
Expand Down Expand Up @@ -206,6 +207,7 @@ export class Nip47ResponseDecodingError extends Nip47Error {}
export class Nip47ResponseValidationError extends Nip47Error {}
export class Nip47UnexpectedResponseError extends Nip47Error {}
export class Nip47NetworkError extends Nip47Error {}
export class Nip47UnsupportedVersionError extends Nip47Error {}

export const NWCs: Record<string, NWCOptions> = {
alby: {
Expand All @@ -232,6 +234,9 @@ export class NWCClient {
lud16: string | undefined;
walletPubkey: string;
options: NWCOptions;
version: string | undefined;

static SUPPORTED_VERSIONS = ["0.0", "1.0"];

static parseWalletConnectUrl(walletConnectUrl: string): NWCOptions {
// makes it possible to parse with URL in the different environments (browser/node/...)
Expand Down Expand Up @@ -328,6 +333,13 @@ export class NWCClient {
return getPublicKey(hexToBytes(this.secret));
}

get supportedVersion(): string {
if (!this.version) {
throw new Error("Missing version");
}
return this.version;
}

getPublicKey(): Promise<string> {
return Promise.resolve(this.publicKey);
}
Expand All @@ -352,15 +364,27 @@ export class NWCClient {
if (!this.secret) {
throw new Error("Missing secret");
}
const encrypted = await nip04.encrypt(this.secret, pubkey, content);
let encrypted;
if (this.supportedVersion === "0.0") {
encrypted = await nip04.encrypt(this.secret, pubkey, content);
} else {
const key = nip44.getConversationKey(hexToBytes(this.secret), pubkey);
encrypted = nip44.encrypt(content, key);
}
return encrypted;
}

async decrypt(pubkey: string, content: string) {
if (!this.secret) {
throw new Error("Missing secret");
}
const decrypted = await nip04.decrypt(this.secret, pubkey, content);
let decrypted;
if (this.supportedVersion === "0.0") {
decrypted = await nip04.decrypt(this.secret, pubkey, content);
} else {
const key = nip44.getConversationKey(hexToBytes(this.secret), pubkey);
decrypted = nip44.decrypt(content, key);
}
return decrypted;
}

Expand Down Expand Up @@ -467,6 +491,7 @@ export class NWCClient {
}

async getWalletServiceInfo(): Promise<{
versions: string[];
capabilities: Nip47Capability[];
notifications: Nip47NotificationType[];
}> {
Expand Down Expand Up @@ -503,7 +528,9 @@ export class NWCClient {
const notificationsTag = events[0].tags.find(
(t) => t[0] === "notifications",
);
const versionsTag = events[0].tags.find((t) => t[0] === "v");
return {
versions: versionsTag ? versionsTag[1]?.split(" ") : ["0.0"],
// delimiter is " " per spec, but Alby NWC originally returned ","
capabilities: content.split(/[ |,]/g) as Nip47Method[],
notifications: (notificationsTag?.[1]?.split(" ") ||
Expand Down Expand Up @@ -715,11 +742,13 @@ export class NWCClient {
while (subscribed) {
try {
await this._checkConnected();

await this._checkCompatibility();
sub = this.relay.subscribe(
[
{
kinds: [23196],
kinds: [
...(this.supportedVersion === "0.0" ? [23196] : [23197]),
],
authors: [this.walletPubkey],
"#p": [this.publicKey],
},
Expand Down Expand Up @@ -792,6 +821,7 @@ export class NWCClient {
resultValidator: (result: T) => boolean,
): Promise<T> {
await this._checkConnected();
await this._checkCompatibility();
return new Promise<T>((resolve, reject) => {
(async () => {
const command = {
Expand All @@ -805,7 +835,10 @@ export class NWCClient {
const eventTemplate: EventTemplate = {
kind: 23194,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", this.walletPubkey]],
tags: [
["p", this.walletPubkey],
["v", this.supportedVersion],
],
content: encryptedCommand,
};

Expand Down Expand Up @@ -924,6 +957,7 @@ export class NWCClient {
resultValidator: (result: T) => boolean,
): Promise<(T & { dTag: string })[]> {
await this._checkConnected();
await this._checkCompatibility();
rolznz marked this conversation as resolved.
Show resolved Hide resolved
const results: (T & { dTag: string })[] = [];
return new Promise<(T & { dTag: string })[]>((resolve, reject) => {
(async () => {
Expand All @@ -938,7 +972,10 @@ export class NWCClient {
const eventTemplate: EventTemplate = {
kind: 23194,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", this.walletPubkey]],
tags: [
["p", this.walletPubkey],
["v", this.supportedVersion],
],
content: encryptedCommand,
};

Expand Down Expand Up @@ -1065,6 +1102,7 @@ export class NWCClient {
})();
});
}

private async _checkConnected() {
if (!this.secret) {
throw new Error("Missing secret key");
Expand All @@ -1084,4 +1122,59 @@ export class NWCClient {
);
}
}

private async _checkCompatibility() {
if (!this.version) {
const walletServiceInfo = await this.getWalletServiceInfo();
const compatibleVersion = this.selectHighestCompatibleVersion(
walletServiceInfo.versions,
);
if (!compatibleVersion) {
throw new Nip47UnsupportedVersionError(
`no compatible version found between wallet and client`,
"UNSUPPORTED_VERSION",
);
}
this.version = compatibleVersion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the version is 0.0 should we log a warning?

}
}

private selectHighestCompatibleVersion(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is nice, but I am wondering if such a complex algorithm is needed?

What about just:
if the wallet supports version 1.0, use it. Otherwise use 0.0.

As Bumi mentioned:

I do not expect many versions (and rather bigger changes like the switch to nip-44) because too many versions (of a spec) lead to incompatibilities which must be avoided.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I was also thinking about this, but then thought to add this as this is an sdk and can face any possibility... (a weird case where the wallet service info gives "0.5" or "1.1" for example)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we leave it as-is, please add a few tests to cover it

walletVersions: string[],
): string | null {
const parseVersions = (versions: string[]) =>
versions.map((v) => v.split(".").map(Number));

const walletParsed = parseVersions(walletVersions);
const clientParsed = parseVersions(NWCClient.SUPPORTED_VERSIONS);

const walletMajors: number[] = walletParsed
.map(([major]) => major)
.filter((value, index, self) => self.indexOf(value) === index);

const clientMajors: number[] = clientParsed
.map(([major]) => major)
.filter((value, index, self) => self.indexOf(value) === index);

const commonMajors = walletMajors
.filter((major) => clientMajors.includes(major))
.sort((a, b) => b - a);

for (const major of commonMajors) {
const walletMinors = walletParsed
.filter(([m]) => m === major)
.map(([, minor]) => minor);
const clientMinors = clientParsed
.filter(([m]) => m === major)
.map(([, minor]) => minor);

const highestMinor = Math.min(
Math.max(...walletMinors),
Math.max(...clientMinors),
);

return `${major}.${highestMinor}`;
}
return null;
}
}
Loading