mirror of
https://github.com/nostr-connect/connect.git
synced 2025-12-17 05:04:20 +01:00
add events and wrap NostrRPC as NostrSigner
This commit is contained in:
104
README.md
104
README.md
@@ -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);
|
||||
```
|
||||
@@ -72,7 +72,7 @@ export class ConnectURI {
|
||||
params: [getPublicKey(secretKey)],
|
||||
},
|
||||
},
|
||||
{ skipResponse: true }
|
||||
{ skipResponse: false }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './connect';
|
||||
export * from './rpc';
|
||||
export * from './nostr';
|
||||
|
||||
23
src/nostr.ts
Normal file
23
src/nostr.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user