add ConnectURI class

This commit is contained in:
tiero
2023-01-03 00:52:04 +01:00
parent 140190699f
commit 0a85d00deb
9 changed files with 259 additions and 506 deletions

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { NostrRPC } from '../src/request'; import { NostrRPC } from '../src/rpc';
class Server extends NostrRPC { class Server extends NostrRPC {

View File

@@ -1,142 +1,183 @@
import { Event, nip04, relayInit } from 'nostr-tools'; import { getPublicKey, Event, nip04 } from 'nostr-tools';
import { prepareRequest } from './event'; import { isValidRequest, NostrRPC } from './rpc';
import { Session, SessionStatus } from './session'; import EventEmitter from 'events';
export interface ConnectMessage { export interface Metadata {
type: ConnectMessageType; name: string;
value?: any; url: string;
requestID?: string; description?: string;
icons?: string[];
} }
export enum ConnectMessageType { export enum SessionStatus {
PAIRED = 'paired', Paired = 'paired',
UNPAIRED = 'unpaired', Unpaired = 'unpaired',
GET_PUBLIC_KEY_REQUEST = 'getPublicKeyRequest',
GET_PUBLIC_KEY_RESPONSE = 'getPublicKeyResponse',
} }
export interface PairingACK extends ConnectMessage { export class ConnectURI {
type: ConnectMessageType.PAIRED; status: SessionStatus = SessionStatus.Unpaired;
value: { target: string;
pubkey: string; metadata: Metadata;
}; relayURL: string;
}
export interface PairingNACK extends ConnectMessage { static fromURI(uri: string): ConnectURI {
type: ConnectMessageType.UNPAIRED; const url = new URL(uri);
} const target = url.searchParams.get('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');
}
const metadata = url.searchParams.get('metadata');
if (!metadata) {
throw new Error('Invalid connect URI: missing metadata');
}
export interface GetPublicKeyRequest extends ConnectMessage { try {
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST; const md = JSON.parse(metadata);
} return new ConnectURI({ target: target, metadata: md, relayURL: relay });
} catch (ignore) {
export interface GetPublicKeyResponse extends ConnectMessage { throw new Error('Invalid connect URI: metadata is not valid JSON');
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE; }
value: {
pubkey: string;
};
}
export function responseTypeForRequestType(
type: ConnectMessageType
): ConnectMessageType {
switch (type) {
case ConnectMessageType.GET_PUBLIC_KEY_REQUEST:
return ConnectMessageType.GET_PUBLIC_KEY_RESPONSE;
default:
throw new Error('Invalid request type');
} }
constructor({
target,
metadata,
relayURL,
}: {
target: string;
metadata: Metadata;
relayURL: string;
}) {
this.target = target;
this.metadata = metadata;
this.relayURL = relayURL;
}
toString() {
return `nostr://connect?target=${this.target}&metadata=${JSON.stringify(
this.metadata
)}&relay=${this.relayURL}`;
}
async approve(secretKey: string): Promise<void> {
const rpc = new NostrRPC({
relay: this.relayURL,
secretKey,
});
const response = await rpc.call({
target: this.target,
request: {
method: 'connect',
params: [getPublicKey(secretKey)],
},
});
if (!response) throw new Error('Invalid response from remote');
return;
}
async reject(secretKey: string): Promise<void> {
const rpc = new NostrRPC({
relay: this.relayURL,
secretKey,
});
const response = await rpc.call({
target: this.target,
request: {
method: 'disconnect',
params: [],
},
});
if (!response) throw new Error('Invalid response from remote');
return;
}
}
export enum ConnectStatus {
Connected = 'connected',
Disconnected = 'disconnected',
} }
export class Connect { export class Connect {
session: Session; rpc: NostrRPC;
private targetPrivateKey: string; target?: string;
events = new EventEmitter();
status = ConnectStatus.Disconnected;
constructor({ constructor({
session, target,
targetPrivateKey, relay,
secretKey,
}: { }: {
session: Session; secretKey: string;
targetPrivateKey: string; target?: string;
relay?: string;
}) { }) {
this.session = session; this.rpc = new NostrRPC({ relay, secretKey });
this.targetPrivateKey = targetPrivateKey; if (target) {
this.target = target;
this.status = ConnectStatus.Connected;
}
} }
async sendMessage(message: ConnectMessage): Promise<ConnectMessage> { async init() {
if (this.session.status !== SessionStatus.PAIRED) const sub = await this.rpc.listen();
throw new Error('Session is not paired'); sub.on('event', async (event: Event) => {
if (!this.session.target) throw new Error('Target is required'); let payload;
if (!this.session.remote) throw new Error('Remote is required'); try {
const { target, remote } = this.session;
// send request to remote
const { event, requestID } = await prepareRequest(
target,
remote,
message,
this.targetPrivateKey
);
console.log(`sending message ${message.type} with requestID ${requestID}`);
const id = await this.session.sendEvent(event, this.targetPrivateKey);
if (!id) throw new Error('Failed to send message ' + message.type);
console.log('sent message with nostr id', id);
const relay = relayInit(this.session.relay);
await relay.connect();
return new Promise((resolve, reject) => {
relay.on('error', () => {
reject(`failed to connect to ${relay.url}`);
});
// waiting for response from remote
let sub = relay.sub([
{
kinds: [4],
authors: [remote],
//since: now,
'#p': [target],
limit: 1,
},
]);
sub.on('event', async (event: Event) => {
const plaintext = await nip04.decrypt( const plaintext = await nip04.decrypt(
this.targetPrivateKey, this.rpc.self.secret,
event.pubkey, event.pubkey,
event.content event.content
); );
console.log('plaintext', plaintext); if (!plaintext) throw new Error('failed to decrypt event');
console.log('requestID', requestID); payload = JSON.parse(plaintext);
const payload = JSON.parse(plaintext); } catch (ignore) {
if (!payload) return; return;
if ( }
!Object.keys(payload).includes('requestID') || // ignore all the events that are not NostrRPCRequest events
!Object.keys(payload).includes('message') if (!isValidRequest(payload)) return;
)
return;
if (payload.requestID !== requestID) return;
const msg = payload.message as ConnectMessage;
const responseType = responseTypeForRequestType(msg.type);
if (msg.type !== responseType) return;
resolve(msg);
});
sub.on('eose', () => { // ignore all the request that are not connect
sub.unsub(); 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);
}); });
} }
on(evt: 'connect' | 'disconnect', cb: (...args: any[]) => void) {
this.events.on(evt, cb);
}
off(evt: 'connect' | 'disconnect', cb: (...args: any[]) => void) {
this.events.off(evt, cb);
}
private isConnected() {
return this.status === ConnectStatus.Connected;
}
async getPublicKey(): Promise<string> { async getPublicKey(): Promise<string> {
const response: ConnectMessage = await this.sendMessage({ if (!this.target || !this.isConnected()) throw new Error('Not connected');
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST,
const response = await this.rpc.call({
target: this.target,
request: {
method: 'get_public_key',
params: [],
},
}); });
if (response.type !== ConnectMessageType.GET_PUBLIC_KEY_RESPONSE) return response as string;
throw new Error('Invalid response type');
return response.value.pubkey;
} }
async signEvent(_event: Event): Promise<Event> { async signEvent(_event: Event): Promise<Event> {
@@ -157,8 +198,4 @@ export class Connect {
throw new Error('Not implemented'); throw new Error('Not implemented');
}, },
}; };
async request(_opts: { method: string; params: any }): Promise<any> {
throw new Error('Not implemented');
}
} }

View File

@@ -1,56 +0,0 @@
import { nip04, Event } from 'nostr-tools';
import { ConnectMessage } from './connect';
export async function prepareRequest(
from: string,
to: string,
request: ConnectMessage,
fromSecretKey: string
) {
const now = Math.floor(Date.now() / 1000);
const requestID = Math.random()
.toString()
.slice(2);
const cipherText = await nip04.encrypt(
fromSecretKey,
to,
JSON.stringify({
requestID: requestID,
request,
})
);
const event: Event = {
kind: 4,
created_at: now,
pubkey: from,
tags: [['p', to]],
content: cipherText,
};
return { event, requestID };
}
export async function prepareResponse(
requestID: string,
from: string,
to: string,
response: ConnectMessage,
fromSecretKey: string
) {
const now = Math.floor(Date.now() / 1000);
const cipherText = await nip04.encrypt(
fromSecretKey,
to,
JSON.stringify({
requestID: requestID,
response,
})
);
const event: Event = {
kind: 4,
created_at: now,
pubkey: from,
tags: [['p', to]],
content: cipherText,
};
return { event, requestID };
}

View File

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

View File

@@ -35,6 +35,7 @@ export class NostrRPC {
secret: opts.secretKey, secret: opts.secretKey,
}; };
} }
async call({ async call({
target, target,
request: { id = randomID(), method, params = [] }, request: { id = randomID(), method, params = [] },
@@ -46,28 +47,32 @@ export class NostrRPC {
params?: any[]; params?: any[];
}; };
}): Promise<any> { }): Promise<any> {
// prepare request to be sent
const request = prepareRequest(id, method, params);
const event = await prepareEvent(this.self.secret, target, request);
// connect to relay // connect to relay
await this.relay.connect(); await this.relay.connect();
this.relay.on('error', () => {
throw new Error(`failed to connect to ${this.relay.url}`);
});
// prepare request to be sent
const body = prepareRequest(id, method, params);
const event = await prepareEvent(this.self.secret, target, body);
// send request via relay // send request via relay
await new Promise<void>((resolve, reject) => { try {
const pub = this.relay.publish(event); await new Promise<void>(async (resolve, reject) => {
pub.on('failed', (reason: any) => { this.relay.on('error', () => {
reject(reason); reject(`failed to connect to ${this.relay.url}`);
});
const pub = this.relay.publish(event);
pub.on('failed', (reason: any) => {
reject(reason);
});
pub.on('seen', () => {
console.log(`seen`, event.id, request);
resolve();
});
}); });
pub.on('seen', () => { } catch (err) {
resolve(); throw err;
}); }
});
// TODO: reject after a timeout
// waiting for response from remote // waiting for response from remote
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const queries = [ const queries = [
@@ -75,10 +80,9 @@ export class NostrRPC {
kinds: [4], kinds: [4],
authors: [target], authors: [target],
'#p': [this.self.pubkey], '#p': [this.self.pubkey],
since: event.created_at - 1, since: event.created_at,
}, },
]; ];
let sub = this.relay.sub(queries); let sub = this.relay.sub(queries);
sub.on('event', async (event: Event) => { sub.on('event', async (event: Event) => {
let payload; let payload;
@@ -100,8 +104,7 @@ export class NostrRPC {
// ignore all the events that are not for this request // ignore all the events that are not for this request
if (payload.id !== id) return; if (payload.id !== id) return;
// unsubscribe from the stream console.log(`response`, event.id, payload);
sub.unsub();
// if the response is an error, reject the promise // if the response is an error, reject the promise
if (payload.error) { if (payload.error) {
@@ -113,25 +116,25 @@ export class NostrRPC {
resolve(payload.result); resolve(payload.result);
} }
}); });
sub.on('eose', () => {
sub.unsub();
});
}); });
} }
async listen(): Promise<Sub> { async listen(): Promise<Sub> {
await this.relay.connect(); await this.relay.connect();
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
this.relay.on('connect', resolve); this.relay.on('connect', () => {
this.relay.on('error', reject); resolve();
});
this.relay.on('error', () => {
reject(`not possible to connect to ${this.relay.url}`);
});
}); });
let sub = this.relay.sub([ let sub = this.relay.sub([
{ {
kinds: [4], kinds: [4],
'#p': [this.self.pubkey], '#p': [this.self.pubkey],
since: now(), since: now() - 1,
}, },
]); ]);
@@ -153,12 +156,15 @@ export class NostrRPC {
if (!isValidRequest(payload)) return; if (!isValidRequest(payload)) return;
// handle request // handle request
if (!this.hasOwnProperty(payload.method)) return;
const response = await this.handleRequest(payload); const response = await this.handleRequest(payload);
const body = prepareResponse( const body = prepareResponse(
response.id, response.id,
response.result, response.result,
response.error response.error
); );
const responseEvent = await prepareEvent( const responseEvent = await prepareEvent(
this.self.secret, this.self.secret,
event.pubkey, event.pubkey,
@@ -166,8 +172,15 @@ export class NostrRPC {
); );
// send response via relay // send response via relay
this.relay.publish(responseEvent); await new Promise<void>((resolve, reject) => {
// TODO: handle errors when event is not seen const pub = this.relay.publish(responseEvent);
pub.on('failed', (reason: any) => {
reject(reason);
});
pub.on('seen', () => {
resolve();
});
});
}); });
return sub; return sub;
@@ -250,7 +263,7 @@ export async function prepareEvent(
return signedEvent; return signedEvent;
} }
function isValidRequest(payload: any): boolean { export function isValidRequest(payload: any): boolean {
if (!payload) return false; if (!payload) return false;
const keys = Object.keys(payload); const keys = Object.keys(payload);
@@ -264,7 +277,7 @@ function isValidRequest(payload: any): boolean {
return true; return true;
} }
function isValidResponse(payload: any): boolean { export function isValidResponse(payload: any): boolean {
if (!payload) return false; if (!payload) return false;
const keys = Object.keys(payload); const keys = Object.keys(payload);

View File

@@ -1,228 +0,0 @@
import {
validateEvent,
verifySignature,
signEvent,
getEventHash,
Event,
relayInit,
nip04,
getPublicKey,
} from 'nostr-tools';
import { ConnectMessage, ConnectMessageType, PairingACK } from './connect';
import { prepareResponse } from './event';
export interface Metadata {
name: string;
url: string;
description?: string;
icons?: string[];
}
export enum SessionStatus {
PROPOSED = 'PROPOSED',
PAIRED = 'PAIRED',
UNPAIRED = 'UNPAIRED',
}
export class Session {
metadata: Metadata;
relay: string;
target: string;
remote?: string;
connectURI: string;
status: SessionStatus = SessionStatus.PROPOSED;
listeners: Record<string, { [type: string]: Array<(value: any) => any> }>;
static fromConnectURI(uri: string): Session {
const url = new URL(uri);
const target = url.searchParams.get('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');
}
const metadata = url.searchParams.get('metadata');
if (!metadata) {
throw new Error('Invalid connect URI: missing metadata');
}
try {
const md = JSON.parse(metadata);
return new Session({ target: target, metadata: md, relay });
} catch (ignore) {
throw new Error('Invalid connect URI: metadata is not valid JSON');
}
}
constructor({
target,
metadata,
relay,
}: {
target: string;
relay: string;
metadata: Metadata;
}) {
this.listeners = {};
this.target = target;
this.metadata = metadata;
this.relay = relay;
this.connectURI = `nostr://connect?target=${
this.target
}&metadata=${JSON.stringify(this.metadata)}&relay=${this.relay}`;
}
on(type: ConnectMessageType, cb: (value: any) => any): void {
const id = Math.random()
.toString()
.slice(2);
this.listeners[id] = this.listeners[id] || emptyListeners();
this.listeners[id][type].push(cb);
}
off(type: ConnectMessageType, cb: (value: any) => any): void {
for (const id in this.listeners) {
const idx = this.listeners[id][type].indexOf(cb);
if (idx > -1) {
this.listeners[id][type].splice(idx, 1);
}
}
}
// this is used to process messages to the given key and emit to listeners
async listen(secretKey: string): Promise<void> {
if (!secretKey) throw new Error('secret key is required');
const pubkey = getPublicKey(secretKey);
const relay = relayInit(this.relay);
await relay.connect();
relay.on('connect', () => {
console.log(`connected to ${relay.url}`);
});
relay.on('error', () => {
console.error(`failed to connect to ${relay.url}`);
});
let sub = relay.sub([
{
kinds: [4],
'#p': [pubkey],
},
]);
sub.on('event', async (event: Event) => {
const plaintext = await nip04.decrypt(
secretKey,
event.pubkey,
event.content
);
const payload = JSON.parse(plaintext);
if (!payload) return;
if (
!Object.keys(payload).includes('requestID') ||
!Object.keys(payload).includes('message')
)
return;
const msg = payload.message as ConnectMessage;
switch (msg.type) {
case ConnectMessageType.PAIRED: {
if (this.status === SessionStatus.PAIRED) return;
if (!msg.value) return;
if (!Object.keys(msg.value).includes('pubkey')) return;
this.status = SessionStatus.PAIRED;
const remote = msg.value.pubkey;
this.remote = remote;
this.emit(ConnectMessageType.PAIRED, msg);
break;
}
case ConnectMessageType.UNPAIRED: {
if (this.status !== SessionStatus.PAIRED) return;
this.status = SessionStatus.UNPAIRED;
this.emit(ConnectMessageType.UNPAIRED);
break;
}
case ConnectMessageType.GET_PUBLIC_KEY_REQUEST: {
if (this.status !== SessionStatus.PAIRED) return;
this.emit(ConnectMessageType.GET_PUBLIC_KEY_REQUEST, msg);
break;
}
case ConnectMessageType.GET_PUBLIC_KEY_RESPONSE: {
this.emit(ConnectMessageType.GET_PUBLIC_KEY_RESPONSE, msg);
break;
}
}
});
sub.on('eose', () => {
sub.unsub();
});
}
emit(type: ConnectMessageType, value?: any): void {
Object.values(this.listeners).forEach(listeners => {
if (listeners[type]) {
listeners[type].forEach(cb => cb(value));
}
});
}
async pair(remoteSignerPrivateKey: string): Promise<void> {
if (!remoteSignerPrivateKey)
throw new Error('Signer private key is required');
const remoteSignerPubKey = getPublicKey(remoteSignerPrivateKey);
this.remote = remoteSignerPubKey;
const message: PairingACK = {
type: ConnectMessageType.PAIRED,
value: { pubkey: this.remote },
};
const randomID = Math.random()
.toString()
.slice(2);
const { event } = await prepareResponse(
randomID,
this.remote,
this.target,
message,
remoteSignerPrivateKey
);
const id = await this.sendEvent(event, remoteSignerPrivateKey);
console.log('sent pairing response from mobile', id);
}
async sendEvent(event: Event, secretKey: string): Promise<string> {
const id = getEventHash(event);
const sig = signEvent(event, secretKey);
const signedEvent = { ...event, id, sig };
let ok = validateEvent(signedEvent);
let veryOk = verifySignature(signedEvent);
if (!ok || !veryOk) {
throw new Error('Event is not valid');
}
const relay = relayInit(this.relay);
await relay.connect();
relay.on('error', () => {
throw new Error(`failed to connect to ${relay.url}`);
});
return new Promise((resolve, reject) => {
const pub = relay.publish(signedEvent);
pub.on('failed', (reason: any) => reject(reason));
pub.on('seen', () => resolve(id));
});
}
}
function emptyListeners(): {} {
let data: any = {};
Object.values(ConnectMessageType).forEach(type => {
data[type] = [];
});
return data;
}

View File

@@ -1,26 +1,42 @@
import { getPublicKey } from 'nostr-tools'; import { getPublicKey } from 'nostr-tools';
import { import { Connect, ConnectURI, NostrRPC } from '../src';
Connect, import { sleep } from './utils';
ConnectMessageType,
GetPublicKeyResponse,
Session,
} from '../src/index';
jest.setTimeout(5000); jest.setTimeout(8000);
// web app (this is ephemeral and represents the currention session)
const webSK =
'5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3';
const webPK = getPublicKey(webSK);
console.log('webPk', webPK);
// mobile app with keys with the nostr identity
const mobileSK =
'ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c';
const mobilePK = getPublicKey(mobileSK);
console.log('mobilePK', mobilePK);
class MobileHandler extends NostrRPC {
async get_public_key(): Promise<string> {
return getPublicKey(this.self.secret);
}
}
describe('Nostr Connect', () => { describe('Nostr Connect', () => {
it('connect', async () => { it('connect', async () => {
let resolvePaired: (arg0: boolean) => void; const testHandler = jest.fn();
let resolveGetPublicKey: (arg0: boolean) => void;
// web app (this is ephemeral and represents the currention session)
const webSK =
'5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3';
const webPK = getPublicKey(webSK);
console.log('webPk', webPK);
const sessionWeb = new Session({ // start listening for connect messages on the web app
const connect = new Connect({ secretKey: webSK });
connect.events.on('connect', testHandler);
await connect.init();
await sleep(100);
// send the connect message to the web app from the mobile
const connectURI = new ConnectURI({
target: webPK, target: webPK,
relay: 'wss://nostr.vulpem.com', relayURL: 'wss://nostr.vulpem.com',
metadata: { metadata: {
name: 'My Website', name: 'My Website',
description: 'lorem ipsum dolor sit amet', description: 'lorem ipsum dolor sit amet',
@@ -28,60 +44,34 @@ describe('Nostr Connect', () => {
icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'], icons: ['https://vulpem.com/1000x860-p-500.422be1bc.png'],
}, },
}); });
sessionWeb.on(ConnectMessageType.PAIRED, (msg: any) => { await connectURI.approve(mobileSK);
expect(msg).toBeDefined();
resolvePaired(true);
});
await sessionWeb.listen(webSK);
// mobile app (this can be a child key) expect(testHandler).toBeCalledTimes(1);
const sessionMobile = Session.fromConnectURI(sessionWeb.connectURI); // 'nostr://connect?target=...&metadata=...' });
const mobileSK =
'ed779ff047f99c95f732b22c9f8f842afb870c740aab591776ebc7b64e83cf6c';
const mobilePK = getPublicKey(mobileSK);
console.log('mobilePK', mobilePK);
await sessionMobile.pair(mobileSK);
// we define the behavior of the mobile app for each requests it('returns pubkey', async () => {
sessionMobile.on(ConnectMessageType.GET_PUBLIC_KEY_REQUEST, async () => { // start listening for connect messages on the mobile app
const message: GetPublicKeyResponse = { const remoteHandler = new MobileHandler({ secretKey: mobileSK });
type: ConnectMessageType.GET_PUBLIC_KEY_RESPONSE, await remoteHandler.listen();
value: {
pubkey: mobilePK,
},
};
const event = await sessionMobile.eventToBeSentToTarget(
message,
mobileSK
);
await sessionMobile.sendEvent(event, mobileSK);
resolveGetPublicKey(true);
});
await sessionMobile.listen(mobileSK);
// The WebApp send the request and wait for the response await sleep(1000);
// The WebApp fetch the public key sending request via session
// start listening for connect messages on the web app
const connect = new Connect({ const connect = new Connect({
session: sessionWeb, secretKey: webSK,
targetPrivateKey: webSK, target: mobilePK,
}); });
const response = await connect.sendMessage({ await connect.init();
type: ConnectMessageType.GET_PUBLIC_KEY_REQUEST,
});
expect(response).toBeDefined();
return expect( await sleep(1000);
Promise.all([
new Promise(resolve => {
resolvePaired = resolve;
}),
new Promise(resolve => {
resolveGetPublicKey = resolve;
}),
])
).resolves.toEqual([true, true]);
/* // send the get_public_key message to the mobile app from the web
const pubkey = await connect.getPublicKey();
expect(pubkey).toBe(webPK);
});
});
/*
expect(handler).toBeCalledTimes(1); expect(handler).toBeCalledTimes(1);
expect(handler).toBeCalledWith({ expect(handler).toBeCalledWith({
type: ConnectMessageType.PAIRED, type: ConnectMessageType.PAIRED,
@@ -114,5 +104,3 @@ describe('Nostr Connect', () => {
}); });
*/ */
});
});

View File

@@ -1,4 +1,5 @@
import { NostrRPC } from '../src/request'; import { NostrRPC } from '../src/rpc';
import { sleep } from './utils';
class Server extends NostrRPC { class Server extends NostrRPC {
async ping(): Promise<string> { async ping(): Promise<string> {
@@ -6,7 +7,7 @@ class Server extends NostrRPC {
} }
} }
jest.setTimeout(10000); jest.setTimeout(5000);
describe('Nostr RPC', () => { describe('Nostr RPC', () => {
it('starts a server', async () => { it('starts a server', async () => {
@@ -20,7 +21,6 @@ describe('Nostr RPC', () => {
secretKey: secretKey:
'5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3', '5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3',
}); });
console.log(`from: ` + client.self.pubkey, `to: ` + server.self.pubkey);
await sleep(2000); await sleep(2000);
@@ -28,10 +28,6 @@ describe('Nostr RPC', () => {
target: server.self.pubkey, target: server.self.pubkey,
request: { method: 'ping' }, request: { method: 'ping' },
}); });
console.log(result); expect(result).toBe('pong');
}); });
}); });
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

3
test/utils.ts Normal file
View File

@@ -0,0 +1,3 @@
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}