diff --git a/example/index.html b/example/index.html
index 19850f0..07635e5 100644
--- a/example/index.html
+++ b/example/index.html
@@ -5,9 +5,10 @@
Playground
+
-
+
diff --git a/example/index.old.tsx b/example/index.old.tsx
deleted file mode 100644
index d0937eb..0000000
--- a/example/index.old.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import 'react-app-polyfill/ie11';
-import * as React from 'react';
-import { useEffect } from 'react';
-import * as ReactDOM from 'react-dom';
-import { Session } from '../src/index';
-import { generatePrivateKey, getPublicKey, nip04, relayInit } from 'nostr-tools'
-
-const App = () => {
- /* useEffect(() => {
- (async () => {
- })();
- }, []); */
-
- const [walletKey, setWalletKey] = React.useState<{ pk: string; sk: string }>();
- const [sessionHolder, setSession] = React.useState();
- const [request, setRequest] = React.useState();
-
-
- const newWallet = () => {
- //this is the wallet public key
- let sk = generatePrivateKey()
- let pk = getPublicKey(sk)
- setWalletKey({ pk, sk });
- }
-
- const newSession = async () => {
-
- const session = new Session({
- name: 'Auth',
- description: 'lorem ipsum dolor sit amet',
- url: 'https://vulpem.com',
- icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'],
- });
-
- await session.pair(walletKey?.pk);
- setSession(session);
- };
-
- const listen = async () => {
- if (!sessionHolder) return;
-
- // let's query for an event to this wallet pub key
- const relay = relayInit('wss://nostr.vulpem.com');
- await relay.connect()
-
- relay.on('connect', () => {
- console.log(`wallet: connected to ${relay.url}`)
- })
- relay.on('error', () => {
- console.log(`wallet: failed to connect to ${relay.url}`)
- })
-
- let sub = relay.sub([{ kinds: [4] }])
- // on the receiver side
- sub.on('event', async (event) => {
- if (!walletKey) return;
-
- const mention = event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1]
- if (mention !== walletKey.pk) return;
-
- const plaintext = await nip04.decrypt(walletKey?.sk, sessionHolder.pubKey, event.content);
- console.log('wallet', event.id, event.pubkey, JSON.parse(plaintext));
- setRequest(JSON.parse(plaintext));
- })
-
- sub.on('eose', () => {
- sub.unsub()
- })
- }
-
- return (
-
-
💸 Wallet
- {walletKey &&
🔎 Wallet Pub: {walletKey?.pk}
}
-
-
- {request &&
-
📨 Incoming Request
-

-
- Name {request?.name}
-
-
- Description {request?.description}
-
-
- URL: {request?.url}
-
-
}
-
-
App
-
-
- );
-};
-
-ReactDOM.render(, document.getElementById('root'));
diff --git a/example/index.tsx b/example/index.tsx
index d9b4aee..b01648b 100644
--- a/example/index.tsx
+++ b/example/index.tsx
@@ -1,57 +1,151 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
+import { useStatePersist } from 'use-state-persist';
import * as ReactDOM from 'react-dom';
-import { NostrRPC } from '../src/rpc';
+import { broadcastToRelay, Connect, connectToRelay, ConnectURI } from '../src';
+import { QRCodeSVG } from 'qrcode.react';
+import { getEventHash, getPublicKey, Event } from 'nostr-tools';
-class Server extends NostrRPC {
- async ping(): Promise {
- return 'pong';
- }
-}
-const server = new Server({
- secretKey:
- 'ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c',
-});
-const client = new NostrRPC({
- secretKey:
- '5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3',
+const secretKey = "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3";
+const connectURI = new ConnectURI({
+ target: getPublicKey(secretKey),
+ relayURL: 'wss://nostr.vulpem.com',
+ metadata: {
+ name: 'Example',
+ description: '🔉🔉🔉',
+ url: 'https://example.com',
+ icons: ['https://example.com/icon.png'],
+ },
});
const App = () => {
-
- const [response, setResponse] = useState('');
+ const [pubkey, setPubkey] = useStatePersist('@pubkey', '');
+ const [getPublicKeyReply, setGetPublicKeyReply] = useState('');
+ const [eventWithSig, setEvent] = useState({});
useEffect(() => {
(async () => {
- const sub = await server.listen();
- sub.on('event', (evt) => {
- console.log('server received message', evt);
+ const target = pubkey.length > 0 ? pubkey : undefined;
+ const connect = new Connect({
+ secretKey,
+ 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('');
+ });
+ await connect.init();
})();
}, []);
- const makeCall = async () => {
- const result = await client.call({
- target: server.self.pubkey,
- request: { method: 'ping' },
+ const getPub = async () => {
+ if (pubkey.length === 0) return;
+ const connect = new Connect({
+ secretKey,
+ target: pubkey,
});
- setResponse(result);
+ const pk = await connect.getPublicKey();
+ setGetPublicKeyReply(pk);
}
+ const sendMessage = async () => {
+ if (pubkey.length === 0) return;
+
+ 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);
+
+ setEvent(event);
+ }
+
+ const isConnected = () => {
+ return pubkey.length > 0;
+ }
return (
-
-
Nostr Connect Playground
-
Server pubkey: {server.self.pubkey}
-
Client pubkey: {client.self.pubkey}
-
-
-
-
{response}
-
+ <>
+
+
+
Nostr Connect Playground
+
+
+
Nostr ID {getPublicKey(secretKey)}
+
+
+
Status {isConnected() ? '🟢 Connected' : '🔴 Disconnected'}
+
+ {!isConnected() &&
+
+
Connect with Nostr
+
+
+
+
}
+
+
+ >
+
+
)
}
ReactDOM.render(, document.getElementById('root'));
diff --git a/example/package.json b/example/package.json
index 66e6518..269fc44 100644
--- a/example/package.json
+++ b/example/package.json
@@ -8,7 +8,9 @@
"build": "parcel build index.html"
},
"dependencies": {
- "react-app-polyfill": "^1.0.0"
+ "qrcode.react": "^3.1.0",
+ "react-app-polyfill": "^1.0.0",
+ "use-state-persist": "^0.3.1"
},
"alias": {
"react": "../node_modules/react",
@@ -18,7 +20,10 @@
"devDependencies": {
"@types/react": "^16.9.11",
"@types/react-dom": "^16.8.4",
+ "events": "^3.1.0",
"parcel": "^2.8.2",
+ "path-browserify": "^1.0.0",
+ "process": "^0.11.10",
"typescript": "^3.4.5"
}
}
diff --git a/example/yarn.lock b/example/yarn.lock
index 34fb76c..438994e 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -1002,6 +1002,16 @@ 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:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
get-port@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119"
@@ -1265,6 +1275,11 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
+path-browserify@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
+ integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -1314,6 +1329,11 @@ posthtml@^0.16.4, posthtml@^0.16.5:
posthtml-parser "^0.11.0"
posthtml-render "^3.0.0"
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
promise@^8.0.3:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a"
@@ -1321,6 +1341,11 @@ promise@^8.0.3:
dependencies:
asap "~2.0.6"
+qrcode.react@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8"
+ integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==
+
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
@@ -1458,6 +1483,13 @@ update-browserslist-db@^1.0.9:
escalade "^3.1.1"
picocolors "^1.0.0"
+use-state-persist@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/use-state-persist/-/use-state-persist-0.3.1.tgz#ab65aacfdeb4adcba96a7bba60d5bdd7c1531d7c"
+ integrity sha512-bNU/9uZMNZDhFGSFfC2DOtsLvNU41FU2/87AVsC2kJuEKfqD4ksCdqGD9mUVfoGkahu4nZAMYvvmOVwpT2kTYw==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+
utility-types@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
diff --git a/package.json b/package.json
index c8d75f5..366be69 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
"typescript": "^4.9.4"
},
"dependencies": {
+ "events": "^3.3.0",
"nostr-tools": "^1.0.1"
}
}
diff --git a/src/connect.ts b/src/connect.ts
index 65834f2..c425512 100644
--- a/src/connect.ts
+++ b/src/connect.ts
@@ -9,13 +9,7 @@ export interface Metadata {
icons?: string[];
}
-export enum SessionStatus {
- Paired = 'paired',
- Unpaired = 'unpaired',
-}
-
export class ConnectURI {
- status: SessionStatus = SessionStatus.Unpaired;
target: string;
metadata: Metadata;
relayURL: string;
@@ -68,14 +62,13 @@ export class ConnectURI {
relay: this.relayURL,
secretKey,
});
- const response = await rpc.call({
+ await rpc.call({
target: this.target,
request: {
method: 'connect',
params: [getPublicKey(secretKey)],
},
- });
- if (!response) throw new Error('Invalid response from remote');
+ }, { skipResponse: true });
return;
}
@@ -85,29 +78,22 @@ export class ConnectURI {
relay: this.relayURL,
secretKey,
});
- const response = await rpc.call({
+ await rpc.call({
target: this.target,
request: {
method: 'disconnect',
params: [],
},
- });
- if (!response) throw new Error('Invalid response from remote');
+ }, { skipResponse: true });
return;
}
}
-export enum ConnectStatus {
- Connected = 'connected',
- Disconnected = 'disconnected',
-}
-
export class Connect {
rpc: NostrRPC;
target?: string;
events = new EventEmitter();
- status = ConnectStatus.Disconnected;
constructor({
target,
@@ -121,7 +107,6 @@ export class Connect {
this.rpc = new NostrRPC({ relay, secretKey });
if (target) {
this.target = target;
- this.status = ConnectStatus.Connected;
}
}
@@ -140,19 +125,24 @@ export class Connect {
} catch (ignore) {
return;
}
+
// ignore all the events that are not NostrRPCRequest events
if (!isValidRequest(payload)) return;
- // ignore all the request that are not connect
- if (payload.method !== 'connect') return;
-
- // ignore all the request that are not for us
- if (!payload.params || payload.params.length !== 1) return;
- const [pubkey] = payload.params;
-
- this.status = ConnectStatus.Connected;
- this.target = pubkey;
- this.events.emit('connect', pubkey);
+ switch (payload.method) {
+ case 'connect':
+ if (!payload.params || payload.params.length !== 1) return;
+ const [pubkey] = payload.params;
+ this.target = pubkey;
+ this.events.emit('connect', pubkey);
+ return
+ case 'disconnect':
+ this.target = undefined;
+ this.events.emit('disconnect');
+ return;
+ default:
+ return;
+ }
});
}
@@ -163,12 +153,8 @@ export class Connect {
this.events.off(evt, cb);
}
- private isConnected() {
- return this.status === ConnectStatus.Connected;
- }
-
async getPublicKey(): Promise {
- if (!this.target || !this.isConnected()) throw new Error('Not connected');
+ if (!this.target) throw new Error('Not connected');
const response = await this.rpc.call({
target: this.target,
@@ -180,8 +166,19 @@ export class Connect {
return response as string;
}
- async signEvent(_event: Event): Promise {
- throw new Error('Not implemented');
+ async signEvent(event: Event): Promise {
+ if (!this.target) throw new Error('Not connected');
+
+ const signature = await this.rpc.call({
+ target: this.target,
+ request: {
+ method: 'sign_event',
+ params: [event],
+ },
+ });
+ console.log('signature', signature);
+
+ return signature as string;
}
async getRelays(): Promise<{
diff --git a/src/rpc.ts b/src/rpc.ts
index 1f1e42c..e554a22 100644
--- a/src/rpc.ts
+++ b/src/rpc.ts
@@ -9,6 +9,7 @@ import {
Event,
Sub,
Filter,
+ Relay,
} from 'nostr-tools';
export interface NostrRPCRequest {
@@ -46,38 +47,19 @@ export class NostrRPC {
method: string;
params?: any[];
};
- }): Promise {
- const relay = await relayInit(this.relay);
+ }, opts?: { skipResponse?: boolean, timeout?: number }): Promise {
+ // connect to relay
+ const relay = await connectToRelay(this.relay);
+
// prepare request to be sent
const request = prepareRequest(id, method, params);
const event = await prepareEvent(this.self.secret, target, request);
- // connect to relay
- await relay.connect();
- await new Promise((resolve, reject) => {
- relay.on('connect', () => {
- resolve();
- });
- relay.on('error', () => {
- reject(`not possible to connect to ${relay.url}`);
- });
- });
-
- // send request via relay
- await new Promise((resolve, reject) => {
- relay.on('error', () => {
- reject(`failed to connect to ${relay.url}`);
- });
- const pub = relay.publish(event);
- pub.on('failed', (reason: any) => {
- reject(reason);
- });
- pub.on('seen', () => {
- resolve();
- });
- });
+ await broadcastToRelay(relay, event);
// waiting for response from remote
+ if (opts && opts.skipResponse === true) return Promise.resolve();
+
return new Promise((resolve, reject) => {
let sub = relay.sub([
{
@@ -122,16 +104,7 @@ export class NostrRPC {
}
async listen(): Promise {
- const relay = relayInit(this.relay);
- await relay.connect();
- await new Promise((resolve, reject) => {
- relay.on('connect', () => {
- resolve();
- });
- relay.on('error', () => {
- reject(`not possible to connect to ${relay.url}`);
- });
- });
+ const relay = await connectToRelay(this.relay);
let sub = relay.sub([
{
@@ -159,6 +132,7 @@ export class NostrRPC {
if (!isValidRequest(payload)) return;
// handle request
+ if (typeof this[payload.method] !== 'function') Promise.resolve();
const response = await this.handleRequest(payload);
const body = prepareResponse(
@@ -292,3 +266,33 @@ export function isValidResponse(payload: any): boolean {
return true;
}
+
+export async function connectToRelay(realayURL: string) {
+ const relay = relayInit(realayURL);
+ await relay.connect();
+ await new Promise((resolve, reject) => {
+ relay.on('connect', () => {
+ resolve();
+ });
+ relay.on('error', () => {
+ reject(`not possible to connect to ${relay.url}`);
+ });
+ });
+ return relay;
+}
+export async function broadcastToRelay(relay: Relay, event: Event) {
+ // send request via relay
+ return await new Promise((resolve, reject) => {
+ relay.on('error', () => {
+ reject(`failed to connect to ${relay.url}`);
+ });
+ const pub = relay.publish(event);
+ pub.on('failed', (reason: any) => {
+ reject(reason);
+ });
+ pub.on('seen', () => {
+ resolve();
+ });
+ });
+}
+
diff --git a/yarn.lock b/yarn.lock
index bb701cf..76b0880 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2984,6 +2984,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+events@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
exec-sh@^0.3.2:
version "0.3.6"
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc"