add events and wrap NostrRPC as NostrSigner

This commit is contained in:
tiero
2023-01-14 03:10:54 +01:00
parent 52f6b6ced8
commit aa74344030
6 changed files with 196 additions and 6 deletions

104
README.md
View File

@@ -15,7 +15,11 @@ yarn add @nostr-connect/connect
## 📖 Usage
1. [👩‍💻 For Apps developers](#-for-apps-developers)
2. [🔐 For Remote Signer developers](#-for-wallet-developers)
2. [🔐 For Wallet & Remote Signer developers](#-for-wallet-developers)
## 👩‍💻 For Apps developers
### Create an ephemeral key
To use the SDK, you need to create an ephemeral key. This key is used to authenticate your user and to create a session.
@@ -48,7 +52,7 @@ await connect.init();
```typescript
const connectURI = new ConnectURI({
target: webPK,
relayURL: 'wss://nostr.vulpem.com',
relay: 'wss://nostr.vulpem.com',
metadata: {
name: 'My Website',
description: 'lorem ipsum dolor sit amet',
@@ -70,3 +74,99 @@ const pubkey = await connect.getPublicKey();
// send the sign_event message to the mobile app
const sig = await connect.signEvent(event);
```
## 🔐 For Wallet developers
### 🤓 1. Define your methods
As per [NIP-46](https://github.com/nostr-connect/nips/blob/nostr-connect/46.md), the Signer app **MUST** implement the following RPC methods:
- `get_public_key`
- `sign_event`
You need to define these methods in your app, each method must return a `Promise` that resolves to the expected result.
The NostrRPC class provides access to the Nostr public key via `this.self.pubkey`, the Nostr private key via `this.self.secret` and the full Nostr event that originated the current request. You can access the event using the `this.event` property.
It's best to ask approval from the user before signing an event. To do so, you can emit an event to the UI and wait for the user to approve or reject the request.
```typescript
import { NostrRPC } from '@nostr-connect/connect';
import { getPublicKey, signEvent, nip06 } from 'nostr-tools';
const sk = nip06.privateKeyFromSeedWords(myWords);
class MobileHandler extends NostrRPC {
async get_public_key(): Promise<string> {
return getPublicKey(sk);
}
async sign_event(event: Event): Promise<string> {
if (!this.event) throw new Error('No origin event');
// emit event to the UI to show a modal
this.events.emit('sign_event_request', {
sender: this.event.pubkey,
event,
});
// wait for the user to approve or reject the request
return new Promise((resolve, reject) => {
// listen for user accept
this.events.on('sign_event_approve', () => {
resolve(signEvent(event, this.self.secret));
});
// or reject
this.events.on('sign_event_reject', () => {
reject(new Error('User rejected request'));
});
});
}
}
```
2. 🎒 Create a MobileHandler instance
Generate a key to identify the remote signer, it is used to be reached by the apps.
```typescript
// random key
const secretKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
// create a new instance of the MobileHandler
const handler = new MobileHandler({ secretKey });
// define how to consume the sign_event_request events
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');
// or reject
//this.events.emit('sign_event_reject');
}
);
// 📡 Listen for incoming requests
await remoteHandler.listen();
```
4. 🚏 Intercept ConnectURI
Allow user to scan the QR code and extract the ConnectURI, intercept it via deep linking or let use manually copy-paste the URI.
```typescript
// Show to the user the pubkey
const { target, relay, metadata } = ConnectURI.fromURI(text);
// if he consents send the connect message
await connectURI.approve(key);
// if rejects could be polite to notify the app of the rejection
await connectURI.reject(key);
```

View File

@@ -72,7 +72,7 @@ export class ConnectURI {
params: [getPublicKey(secretKey)],
},
},
{ skipResponse: true }
{ skipResponse: false }
);
}

View File

@@ -1,2 +1,3 @@
export * from './connect';
export * from './rpc';
export * from './nostr';

23
src/nostr.ts Normal file
View File

@@ -0,0 +1,23 @@
import { NostrRPC } from './rpc';
export class NostrSigner extends NostrRPC {
connectedAppIDs: string[];
constructor(opts: { relay?: string | undefined; secretKey: string }) {
super(opts);
this.connectedAppIDs = [];
}
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);
}
}

View File

@@ -1,3 +1,4 @@
import EventEmitter from 'events';
import {
Event,
Filter,
@@ -29,6 +30,8 @@ export class NostrRPC {
event: Event | undefined;
// this is for implementing the response handlers for each method
[key: string]: any;
// events
events = new EventEmitter();
constructor(opts: { relay?: string; secretKey: string }) {
this.relay = opts.relay || 'wss://nostr.vulpem.com';

View File

@@ -1,5 +1,5 @@
import { getPublicKey } from 'nostr-tools';
import { Connect, ConnectURI, NostrRPC } from '../src';
import { getPublicKey, signEvent, Event } from 'nostr-tools';
import { Connect, ConnectURI, NostrSigner } from '../src';
import { sleep } from './utils';
jest.setTimeout(5000);
@@ -16,10 +16,29 @@ const mobileSK =
const mobilePK = getPublicKey(mobileSK);
console.log('mobilePK', mobilePK);
class MobileHandler extends NostrRPC {
class MobileHandler extends NostrSigner {
async get_public_key(): Promise<string> {
return getPublicKey(this.self.secret);
}
async sign_event(event: any): Promise<string> {
if (!this.event) throw new Error('No origin event');
// emit event to the UI to show a modal
this.events.emit('sign_event_request', event);
// wait for the user to approve or reject the request
return new Promise((resolve, reject) => {
// listen for user acceptance
this.events.on('sign_event_approve', () => {
resolve(signEvent(event, this.self.secret));
});
// or rejection
this.events.on('sign_event_reject', () => {
reject(new Error('User rejected request'));
});
});
}
}
describe('Nostr Connect', () => {
@@ -72,6 +91,50 @@ describe('Nostr Connect', () => {
const pubkey = await connect.getPublicKey();
expect(pubkey).toBe(mobilePK);
});
it('returns a signed event', async () => {
// start listening for connect messages on the mobile app
const remoteHandler = new MobileHandler({
secretKey: mobileSK,
relay: 'wss://nostr.vulpem.com',
});
// define how to comnsume the event
remoteHandler.events.on('sign_event_request', (event: Event) => {
// ⚠️⚠️⚠️ IMPORTANT: always check if the app is connected
console.log(remoteHandler.connectedAppIDs);
if (!remoteHandler.isConnected(event.pubkey)) return;
// assume user clicks on approve button on the UI
remoteHandler.events.emit('sign_event_approve');
});
// add app as connected app
remoteHandler.addConnectedApp(webPK);
// start listening for request messages on the mobile app
await remoteHandler.listen();
await sleep(1000);
// start listening for connect messages on the web app
const connect = new Connect({
secretKey: webSK,
target: mobilePK,
});
await connect.init();
await sleep(1000);
const event = await connect.signEvent({
kind: 1,
pubkey: mobilePK,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: '🏃‍♀️ Testing Nostr Connect',
});
expect(event).toBeDefined();
});
});
/*