disconnect from App (#2)

* disconnect from App

* example
This commit is contained in:
Marco Argentieri
2023-01-14 19:34:21 +01:00
committed by GitHub
parent 3c79cbbeec
commit e797a6994e
7 changed files with 117 additions and 87 deletions

View File

@@ -127,9 +127,7 @@ class MobileHandler extends NostrSigner {
### 📱 Create a MobileHandler instance ### 📱 Create a MobileHandler instance
Generate a key to identify the remote signer, it is used to be reached by the apps. 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.
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.
```typescript ```typescript
// random key // random key
@@ -141,9 +139,9 @@ const handler = new MobileHandler({ secretKey });
remoteHandler.events.on('sign_event_request', remoteHandler.events.on('sign_event_request',
(event: Event) => { (event: Event) => {
// ⚠️⚠️⚠️ IMPORTANT: always check if the app is connected // ⚠️⚠️⚠️ 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 // do your UI stuff here to ask the user to approve or reject the request
// UI components can accept the sign // UI components can accept the sign
//this.events.emit('sign_event_approve'); //this.events.emit('sign_event_approve');

View File

@@ -33,11 +33,9 @@ const App = () => {
target, target,
}); });
connect.events.on('connect', (pubkey: string) => { connect.events.on('connect', (pubkey: string) => {
console.log('We are connected to ' + pubkey)
setPubkey(pubkey); setPubkey(pubkey);
}); });
connect.events.on('disconnect', () => { connect.events.on('disconnect', () => {
console.log('We got disconnected')
setEvent({}); setEvent({});
setPubkey(''); setPubkey('');
setGetPublicKeyReply(''); setGetPublicKeyReply('');
@@ -58,56 +56,79 @@ const App = () => {
} }
const sendMessage = async () => { const sendMessage = async () => {
if (pubkey.length === 0) return; try {
if (pubkey.length === 0) return;
const connect = new Connect({ const connect = new Connect({
secretKey, secretKey,
target: pubkey, target: pubkey,
}); });
let event: Event = { let event: Event = {
kind: 1, kind: 1,
pubkey: pubkey, pubkey: pubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [], tags: [],
content: "Running Nostr Connect 🔌" content: "Running Nostr Connect 🔌"
}; };
event.id = getEventHash(event) event.id = getEventHash(event)
event.sig = await connect.signEvent(event); event.sig = await connect.signEvent(event);
const relay = await connectToRelay('wss://relay.damus.io'); const relay = await connectToRelay('wss://relay.damus.io');
await broadcastToRelay(relay, event); await broadcastToRelay(relay, event);
setEvent(event);
} catch (error) {
console.error(error);
}
setEvent(event);
} }
const isConnected = () => { const isConnected = () => {
return pubkey.length > 0; return pubkey.length > 0;
} }
const copyToClipboard = () => { const disconnect = async () => {
navigator.clipboard.writeText(connectURI.toString()).then(function() { const connect = new Connect({
alert('Copied!'); secretKey,
}, function(err) { target: pubkey,
console.error('Async: Could not copy text: ', err);
}); });
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 ( return (
<> <div className='hero is-fullheight has-background-black has-text-white'>
<section className="container"> <section className="container">
<div className='content'> <div className='content'>
<h1 className='title'>Nostr Connect Playground</h1> <h1 className='title has-text-white'>Nostr Connect Playground</h1>
</div> </div>
<div className='content'> <div className='content'>
<p className='subtitle is-6'><strong>Nostr ID</strong> {getPublicKey(secretKey)}</p> <p className='subtitle is-6 has-text-white'><b>Nostr ID</b> {getPublicKey(secretKey)}</p>
</div> </div>
<div className='content'> <div className='content'>
<p className='subtitle is-6'><strong>Status</strong> {isConnected() ? '🟢 Connected' : '🔴 Disconnected'}</p> <p className='subtitle is-6 has-text-white'><b>Status</b> {isConnected() ? '🟢 Connected' : '🔴 Disconnected'}</p>
</div> </div>
{
isConnected() && <div className='content'>
<button className='button is-danger' onClick={disconnect}>
<p className='subtitle is-6 has-text-white'>💤 <i>Disconnect</i></p>
</button>
</div>
}
{!isConnected() && <div className='content has-text-centered'> {!isConnected() && <div className='content has-text-centered'>
<div className='notification is-info'> <div className='notification is-dark'>
<h2 className='title is-5'>Connect with Nostr</h2> <h2 className='title is-5'>Connect with Nostr</h2>
<QRCodeSVG value={connectURI.toString()} /> <QRCodeSVG value={connectURI.toString()} />
<input <input
className='input is-info' className='input is-info'
@@ -119,15 +140,14 @@ const App = () => {
Copy to clipboard Copy to clipboard
</button> </button>
</div> </div>
</div>} </div>
</section> }
<section className="container mt-6">
{ {
isConnected() && isConnected() &&
<> <div className='notification is-dark'>
<div className='content'> <div className='content'>
<h2 className='title is-5'>Get Public Key</h2> <h2 className='title is-5 has-text-white'>Get Public Key</h2>
<button className='button is-info' onClick={getPub}> <button className='button is-info has-text-white' onClick={getPub}>
Get public key Get public key
</button> </button>
{getPublicKeyReply.length > 0 && <input {getPublicKeyReply.length > 0 && <input
@@ -138,9 +158,9 @@ const App = () => {
/>} />}
</div> </div>
<div className='content'> <div className='content'>
<h2 className='title is-5'>Send a message with text <b>Running Nostr Connect 🔌</b></h2> <h2 className='title is-5 has-text-white'>Post a message on Damus relay with text <b>Running Nostr Connect 🔌</b></h2>
<button className='button is-info' onClick={sendMessage}> <button className='button is-info' onClick={sendMessage}>
Send message to Nostr Sign Event
</button> </button>
{ {
Object.keys(eventWithSig).length > 0 && Object.keys(eventWithSig).length > 0 &&
@@ -152,10 +172,10 @@ const App = () => {
/> />
} }
</div> </div>
</> </div>
} }
</section> </section>
</> </div>
) )

View File

@@ -8,7 +8,7 @@
"build": "parcel build --public-url /connect index.html" "build": "parcel build --public-url /connect index.html"
}, },
"dependencies": { "dependencies": {
"@nostr-connect/connect": "^0.1.3", "@nostr-connect/connect": "@nostr-connect/connect",
"nostr-tools": "^1.1.1", "nostr-tools": "^1.1.1",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -159,13 +159,8 @@
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1"
integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw== integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==
"@nostr-connect/connect@^0.1.3": "@nostr-connect/connect@file:../src":
version "0.1.3" version "0.0.0"
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"
"@parcel/bundler-default@2.8.2": "@parcel/bundler-default@2.8.2":
version "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" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
events@^3.1.0, events@^3.3.0: events@^3.1.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== 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" 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== 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" version "1.1.1"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.1.1.tgz#2be4cd650bc0a4d20650b6cf46fee451c9f565b8" resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.1.1.tgz#2be4cd650bc0a4d20650b6cf46fee451c9f565b8"
integrity sha512-mxgjbHR6nx2ACBNa2tBpeM/glsPWqxHPT1Kszx/XfzL+kUdi1Gm3Xz1UcaODQ2F84IFtCKNLO+aF31ZfTAhSYQ== integrity sha512-mxgjbHR6nx2ACBNa2tBpeM/glsPWqxHPT1Kszx/XfzL+kUdi1Gm3Xz1UcaODQ2F84IFtCKNLO+aF31ZfTAhSYQ==

View File

@@ -18,8 +18,7 @@ export class ConnectURI {
static fromURI(uri: string): ConnectURI { static fromURI(uri: string): ConnectURI {
const url = new URL(uri); const url = new URL(uri);
const target = url.hostname || url.pathname.substring(2); const target = url.hostname || url.pathname.substring(2);
if (!target) if (!target) throw new Error('Invalid connect URI: missing target');
throw new Error('Invalid connect URI: missing target');
const relay = url.searchParams.get('relay'); const relay = url.searchParams.get('relay');
if (!relay) { if (!relay) {
throw new Error('Invalid connect URI: missing relay'); throw new Error('Invalid connect URI: missing relay');
@@ -53,9 +52,9 @@ export class ConnectURI {
} }
toString() { toString() {
return `nostrconnect://${this.target}?metadata=${JSON.stringify( return `nostrconnect://${this.target}?metadata=${encodeURIComponent(
this.metadata JSON.stringify(this.metadata)
)}&relay=${this.relay}`; )}&relay=${encodeURIComponent(this.relay)}`;
} }
async approve(secretKey: string): Promise<void> { async approve(secretKey: string): Promise<void> {
@@ -71,7 +70,7 @@ export class ConnectURI {
params: [getPublicKey(secretKey)], params: [getPublicKey(secretKey)],
}, },
}, },
{ skipResponse: false } { skipResponse: true }
); );
} }
@@ -135,7 +134,8 @@ export class Connect {
switch (payload.method) { switch (payload.method) {
case 'connect': { 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; const [pubkey] = payload.params;
this.target = pubkey; this.target = pubkey;
this.events.emit('connect', pubkey); this.events.emit('connect', pubkey);
@@ -158,6 +158,30 @@ export class Connect {
this.events.off(evt, cb); this.events.off(evt, cb);
} }
async disconnect(): Promise<void> {
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<string> { async getPublicKey(): Promise<string> {
if (!this.target) throw new Error('Not connected'); if (!this.target) throw new Error('Not connected');
@@ -181,7 +205,6 @@ export class Connect {
params: [event], params: [event],
}, },
}); });
console.log('signature', signature);
return signature as string; return signature as string;
} }

View File

@@ -1,23 +1,11 @@
import { NostrRPC } from './rpc'; import { NostrRPC } from './rpc';
export class NostrSigner extends NostrRPC { export class NostrSigner extends NostrRPC {
connectedAppIDs: string[]; async disconnect(): Promise<null> {
constructor(opts: { relay?: string | undefined; secretKey: string }) { this.events.emit('disconnect');
super(opts); return null;
this.connectedAppIDs = [];
} }
isConnected(): boolean {
addConnectedApp = (pubkey: string) => { throw new Error('Method not implemented yet.');
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);
} }
} }

View File

@@ -48,20 +48,27 @@ describe('Nostr Connect', () => {
relay: 'wss://nostr.vulpem.com', relay: 'wss://nostr.vulpem.com',
metadata: { metadata: {
name: 'Vulpem', 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', url: 'https://vulpem.com',
icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'], icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'],
}, },
}); });
const url = ConnectURI.fromURI(connectURI.toString()); 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.relay).toBe('wss://nostr.vulpem.com');
expect(url.metadata.name).toBe('Vulpem'); 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.url).toBe('https://vulpem.com');
expect(url.metadata.icons).toBeDefined(); expect(url.metadata.icons).toBeDefined();
expect(url.metadata.icons!.length).toBe(1); 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![0]).toBe(
'https://vulpem.com/1000x860-p-500.422be1bc.png'
);
}); });
it.skip('connect', async () => { it.skip('connect', async () => {
const testHandler = jest.fn(); const testHandler = jest.fn();
@@ -124,7 +131,7 @@ describe('Nostr Connect', () => {
remoteHandler.events.on('sign_event_request', (event: Event) => { remoteHandler.events.on('sign_event_request', (event: Event) => {
// ⚠️⚠️⚠️ IMPORTANT: always check if the app is connected // ⚠️⚠️⚠️ 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 // assume user clicks on approve button on the UI
remoteHandler.events.emit('sign_event_approve'); remoteHandler.events.emit('sign_event_approve');
}); });
@@ -186,7 +193,6 @@ describe('Nostr Connect', () => {
sessionWeb.on(ConnectMessageType.UNPAIRED, () => { sessionWeb.on(ConnectMessageType.UNPAIRED, () => {
console.log('unpaired');
}); });
*/ */