From e797a6994e04fdfe9c8c82fe472b326ec904d6c0 Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Sat, 14 Jan 2023 19:34:21 +0100 Subject: [PATCH] disconnect from App (#2) * disconnect from App * example --- README.md | 8 ++-- example/index.tsx | 100 ++++++++++++++++++++++++++----------------- example/package.json | 2 +- example/yarn.lock | 13 ++---- src/connect.ts | 39 +++++++++++++---- src/nostr.ts | 22 +++------- test/connect.test.ts | 20 ++++++--- 7 files changed, 117 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 414f272..7bd1705 100644 --- a/README.md +++ b/README.md @@ -127,9 +127,7 @@ class MobileHandler extends NostrSigner { ### 📱 Create a MobileHandler instance -Generate a key to identify the remote signer, it is used to be reached by the apps. - -An instance that extends `NostrSigner` has access to `isConnected(pubkey:string): boolean` method that tells you if the current request is from an app that has been granted the connection by the current signer app. +Generate a key to identify the remote signer, it is used to be reached by the apps. At the moment it's your duty to persist locally a list of the apps that are allowed to connect to your remote signer. ```typescript // random key @@ -141,9 +139,9 @@ const handler = new MobileHandler({ secretKey }); remoteHandler.events.on('sign_event_request', (event: Event) => { // ⚠️⚠️⚠️ IMPORTANT: always check if the app is connected - if (!remoteHandler.isConnected(event.pubkey)) return; + // do your UI stuff here to ask the user to approve or reject the request - + // UI components can accept the sign //this.events.emit('sign_event_approve'); diff --git a/example/index.tsx b/example/index.tsx index 863a48a..535a471 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -33,11 +33,9 @@ const App = () => { target, }); connect.events.on('connect', (pubkey: string) => { - console.log('We are connected to ' + pubkey) setPubkey(pubkey); }); connect.events.on('disconnect', () => { - console.log('We got disconnected') setEvent({}); setPubkey(''); setGetPublicKeyReply(''); @@ -58,56 +56,79 @@ const App = () => { } const sendMessage = async () => { - if (pubkey.length === 0) return; + try { + if (pubkey.length === 0) return; - const connect = new Connect({ - secretKey, - target: pubkey, - }); + const connect = new Connect({ + secretKey, + target: pubkey, + }); - let event: Event = { - kind: 1, - pubkey: pubkey, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: "Running Nostr Connect 🔌" - }; - event.id = getEventHash(event) - event.sig = await connect.signEvent(event); - const relay = await connectToRelay('wss://relay.damus.io'); - await broadcastToRelay(relay, event); + let event: Event = { + kind: 1, + pubkey: pubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: "Running Nostr Connect 🔌" + }; + event.id = getEventHash(event) + event.sig = await connect.signEvent(event); + const relay = await connectToRelay('wss://relay.damus.io'); + await broadcastToRelay(relay, event); + + setEvent(event); + } catch (error) { + console.error(error); + } - setEvent(event); } const isConnected = () => { return pubkey.length > 0; } - const copyToClipboard = () => { - navigator.clipboard.writeText(connectURI.toString()).then(function() { - alert('Copied!'); - }, function(err) { - console.error('Async: Could not copy text: ', err); + const disconnect = async () => { + const connect = new Connect({ + secretKey, + target: pubkey, }); + await connect.disconnect(); + //cleanup + setEvent({}); + setPubkey(''); + setGetPublicKeyReply(''); + } + + const copyToClipboard = () => { + navigator.clipboard.writeText(connectURI.toString()).then(undefined, + function (err) { + console.error('Async: Could not copy text: ', err); + }); } return ( - <> +
-

Nostr Connect Playground

+

Nostr Connect Playground

-

Nostr ID {getPublicKey(secretKey)}

+

Nostr ID {getPublicKey(secretKey)}

-

Status {isConnected() ? '🟢 Connected' : '🔴 Disconnected'}

+

Status {isConnected() ? '🟢 Connected' : '🔴 Disconnected'}

+ { + isConnected() &&
+ +
+ } {!isConnected() &&
-
+

Connect with Nostr

- + { Copy to clipboard
-
} -
-
+
+ } { isConnected() && - <> +
-

Get Public Key

- {getPublicKeyReply.length > 0 && { />}
-

Send a message with text Running Nostr Connect 🔌

+

Post a message on Damus relay with text Running Nostr Connect 🔌

{ Object.keys(eventWithSig).length > 0 && @@ -152,10 +172,10 @@ const App = () => { /> }
- +
} - + ) diff --git a/example/package.json b/example/package.json index 07216af..82c65bc 100644 --- a/example/package.json +++ b/example/package.json @@ -8,7 +8,7 @@ "build": "parcel build --public-url /connect index.html" }, "dependencies": { - "@nostr-connect/connect": "^0.1.3", + "@nostr-connect/connect": "@nostr-connect/connect", "nostr-tools": "^1.1.1", "qrcode.react": "^3.1.0", "react": "^18.2.0", diff --git a/example/yarn.lock b/example/yarn.lock index a05b05a..52ced4c 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -159,13 +159,8 @@ resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1" integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw== -"@nostr-connect/connect@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@nostr-connect/connect/-/connect-0.1.3.tgz#427d43cd799fd253009aab490573aba698774b7f" - integrity sha512-8BEolEbzWngU4CAGmWfHPxbnyAMXSR2hlww4da3a5NzGlySu22m/fxpy5ZuFybIbOgyerjY26KOuN67xx4Ai4A== - dependencies: - events "^3.3.0" - nostr-tools "^1.0.1" +"@nostr-connect/connect@file:../src": + version "0.0.0" "@parcel/bundler-default@2.8.2": version "2.8.2" @@ -1047,7 +1042,7 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -events@^3.1.0, events@^3.3.0: +events@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -1268,7 +1263,7 @@ node-releases@^2.0.6: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== -nostr-tools@^1.0.1, nostr-tools@^1.1.1: +nostr-tools@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.1.1.tgz#2be4cd650bc0a4d20650b6cf46fee451c9f565b8" integrity sha512-mxgjbHR6nx2ACBNa2tBpeM/glsPWqxHPT1Kszx/XfzL+kUdi1Gm3Xz1UcaODQ2F84IFtCKNLO+aF31ZfTAhSYQ== diff --git a/src/connect.ts b/src/connect.ts index c915f62..c9043ac 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -18,8 +18,7 @@ export class ConnectURI { static fromURI(uri: string): ConnectURI { const url = new URL(uri); const target = url.hostname || url.pathname.substring(2); - if (!target) - throw new Error('Invalid connect URI: missing target'); + if (!target) throw new Error('Invalid connect URI: missing target'); const relay = url.searchParams.get('relay'); if (!relay) { throw new Error('Invalid connect URI: missing relay'); @@ -53,9 +52,9 @@ export class ConnectURI { } toString() { - return `nostrconnect://${this.target}?metadata=${JSON.stringify( - this.metadata - )}&relay=${this.relay}`; + return `nostrconnect://${this.target}?metadata=${encodeURIComponent( + JSON.stringify(this.metadata) + )}&relay=${encodeURIComponent(this.relay)}`; } async approve(secretKey: string): Promise { @@ -71,7 +70,7 @@ export class ConnectURI { params: [getPublicKey(secretKey)], }, }, - { skipResponse: false } + { skipResponse: true } ); } @@ -135,7 +134,8 @@ export class Connect { switch (payload.method) { case 'connect': { - if (!payload.params || payload.params.length !== 1) return; + if (!payload.params || payload.params.length !== 1) + throw new Error('connect: missing pubkey'); const [pubkey] = payload.params; this.target = pubkey; this.events.emit('connect', pubkey); @@ -158,6 +158,30 @@ export class Connect { this.events.off(evt, cb); } + async disconnect(): Promise { + if (!this.target) throw new Error('Not connected'); + + // notify the UI that we are disconnecting + this.events.emit('disconnect'); + + try { + await this.rpc.call( + { + target: this.target, + request: { + method: 'disconnect', + params: [], + }, + }, + { skipResponse: true } + ); + } catch (error) { + throw new Error('Failed to disconnect'); + } + + this.target = undefined; + } + async getPublicKey(): Promise { if (!this.target) throw new Error('Not connected'); @@ -181,7 +205,6 @@ export class Connect { params: [event], }, }); - console.log('signature', signature); return signature as string; } diff --git a/src/nostr.ts b/src/nostr.ts index a2bba8a..f5bff58 100644 --- a/src/nostr.ts +++ b/src/nostr.ts @@ -1,23 +1,11 @@ import { NostrRPC } from './rpc'; export class NostrSigner extends NostrRPC { - connectedAppIDs: string[]; - constructor(opts: { relay?: string | undefined; secretKey: string }) { - super(opts); - this.connectedAppIDs = []; + async disconnect(): Promise { + this.events.emit('disconnect'); + return null; } - - addConnectedApp = (pubkey: string) => { - this.connectedAppIDs.push(pubkey); - }; - - removeConnectedApp = (pubkey: string) => { - this.connectedAppIDs = this.connectedAppIDs.filter( - (id: string) => id !== pubkey - ); - }; - - isConnected(pubkey: string): boolean { - return this.connectedAppIDs.includes(pubkey); + isConnected(): boolean { + throw new Error('Method not implemented yet.'); } } diff --git a/test/connect.test.ts b/test/connect.test.ts index 210128c..276a684 100644 --- a/test/connect.test.ts +++ b/test/connect.test.ts @@ -48,20 +48,27 @@ describe('Nostr Connect', () => { relay: 'wss://nostr.vulpem.com', metadata: { name: 'Vulpem', - description: 'Enabling the next generation of bitcoin-native financial services', + description: + 'Enabling the next generation of bitcoin-native financial services', url: 'https://vulpem.com', icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'], }, }); const url = ConnectURI.fromURI(connectURI.toString()); - expect(url.target).toBe('b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4'); + expect(url.target).toBe( + 'b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4' + ); expect(url.relay).toBe('wss://nostr.vulpem.com'); expect(url.metadata.name).toBe('Vulpem'); - expect(url.metadata.description).toBe('Enabling the next generation of bitcoin-native financial services'); + expect(url.metadata.description).toBe( + 'Enabling the next generation of bitcoin-native financial services' + ); expect(url.metadata.url).toBe('https://vulpem.com'); expect(url.metadata.icons).toBeDefined(); - expect(url.metadata.icons!.length).toBe(1); - expect(url.metadata.icons![0]).toBe('https://vulpem.com/1000x860-p-500.422be1bc.png'); + expect(url.metadata.icons!.length).toBe(1); + expect(url.metadata.icons![0]).toBe( + 'https://vulpem.com/1000x860-p-500.422be1bc.png' + ); }); it.skip('connect', async () => { const testHandler = jest.fn(); @@ -124,7 +131,7 @@ describe('Nostr Connect', () => { remoteHandler.events.on('sign_event_request', (event: Event) => { // ⚠️⚠️⚠️ IMPORTANT: always check if the app is connected - if (!remoteHandler.isConnected(event.pubkey)) return; + //if (!remoteHandler.isConnected(event.pubkey)) return; // assume user clicks on approve button on the UI remoteHandler.events.emit('sign_event_approve'); }); @@ -186,7 +193,6 @@ describe('Nostr Connect', () => { sessionWeb.on(ConnectMessageType.UNPAIRED, () => { - console.log('unpaired'); }); */