diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2ea904e..2ab5465 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,5 +1,9 @@
name: CI
-on: [push]
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
jobs:
build:
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
@@ -7,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- node: ['14.x', '16.x', '18.x']
+ node: ['14.x', '16.x']
os: [ubuntu-latest]
steps:
@@ -25,8 +29,5 @@ jobs:
- name: Lint
run: yarn lint
- #- name: Test
- # run: yarn test --ci --coverage --maxWorkers=2
-
- name: Build
run: yarn build
diff --git a/example/index.tsx b/example/index.tsx
index 034fa8e..abe7dd5 100644
--- a/example/index.tsx
+++ b/example/index.tsx
@@ -3,20 +3,20 @@ import { useEffect, useState } from 'react';
import { useStatePersist } from 'use-state-persist';
import * as ReactDOM from 'react-dom';
-import { broadcastToRelay, Connect, connectToRelay, ConnectURI } from '@nostr-connect/connect';
+import { broadcastToRelay, Connect, connectToRelay, ConnectURI, TimeRanges } from '../src/index';
import { QRCodeSVG } from 'qrcode.react';
-import { getEventHash, getPublicKey, Event } from 'nostr-tools';
+import { getEventHash, getPublicKey, Event, nip19 } from 'nostr-tools';
const secretKey = "5acff99d1ad3e1706360d213fd69203312d9b5e91a2d5f2e06100cc6f686e5b3";
const connectURI = new ConnectURI({
target: getPublicKey(secretKey),
relay: 'wss://nostr.vulpem.com',
metadata: {
- name: 'Example',
- description: '🔉🔉🔉',
- url: 'https://example.com',
- icons: ['https://example.com/icon.png'],
+ name: 'Vulpem',
+ description: 'Bitcoin company',
+ url: 'https://vulpem.com',
+ icons: ['https://vulpem.com/favicon.ico'],
},
});
@@ -54,7 +54,7 @@ const App = () => {
target: pubkey,
});
const pk = await connect.getPublicKey();
- setGetPublicKeyReply(pk);
+ setGetPublicKeyReply(nip19.npubEncode(pk));
}
const sendMessage = async () => {
@@ -116,19 +116,13 @@ const App = () => {
target: pubkey,
});
- const sig = await connect.rpc.call({
- target: pubkey,
- request: {
- method: 'delegate',
- params: [
- getPublicKey(secretKey),
- {
- kind: 0,
- until: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365,
- }
- ],
+ const sig = await connect.delegate(
+ getPublicKey(secretKey),
+ {
+ kind: 1,
+ until: TimeRanges.ONE_DAY,
}
- });
+ );
setDelegateSig(sig);
} catch (error) {
console.error(error);
@@ -158,6 +152,7 @@ const App = () => {
});
}
+ const appEpehemeralPubKey = nip19.npubEncode(getPublicKey(secretKey));
return (
@@ -165,7 +160,7 @@ const App = () => {
Nostr Connect Playground
-
Nostr ID {getPublicKey(secretKey)}
+
Nostr ID (ephemeral) {appEpehemeralPubKey}
Status {isConnected() ? '🟢 Connected' : '🔴 Disconnected'}
diff --git a/src/connect.ts b/src/connect.ts
index c9043ac..95aa23f 100644
--- a/src/connect.ts
+++ b/src/connect.ts
@@ -10,6 +10,23 @@ export interface Metadata {
icons?: string[];
}
+export enum TimeRanges {
+ FIVE_MINS = '5mins',
+ ONE_HR = '1hour',
+ ONE_DAY = '1day',
+ ONE_WEEK = '1week',
+ ONE_MONTH = '1month',
+ ONE_YEAR = '1year',
+}
+export const TimeRangeToUnix: Record
= {
+ [TimeRanges.FIVE_MINS]: Math.round(Date.now() / 1000) + 60 * 5,
+ [TimeRanges.ONE_HR]: Math.round(Date.now() / 1000) + 60 * 60,
+ [TimeRanges.ONE_DAY]: Math.round(Date.now() / 1000) + 60 * 60 * 24,
+ [TimeRanges.ONE_WEEK]: Math.round(Date.now() / 1000) + 60 * 60 * 24 * 7,
+ [TimeRanges.ONE_MONTH]: Math.round(Date.now() / 1000) + 60 * 60 * 24 * 30,
+ [TimeRanges.ONE_YEAR]: Math.round(Date.now() / 1000) + 60 * 60 * 24 * 365,
+};
+
export class ConnectURI {
target: string;
metadata: Metadata;
@@ -209,6 +226,54 @@ export class Connect {
return signature as string;
}
+ async describe(): Promise {
+ if (!this.target) throw new Error('Not connected');
+
+ const response = await this.rpc.call({
+ target: this.target,
+ request: {
+ method: 'describe',
+ params: [],
+ },
+ });
+ return response as string[];
+ }
+
+ async delegate(
+ delegatee: string = this.rpc.self.pubkey,
+ conditions: {
+ kind?: number;
+ until?: number | TimeRanges;
+ since?: number | TimeRanges;
+ }
+ ): Promise {
+ if (!this.target) throw new Error('Not connected');
+
+ if (conditions.until && typeof conditions.until !== 'number') {
+ if (!Object.keys(TimeRangeToUnix).includes(conditions.until))
+ throw new Error(
+ 'conditions.until must be either a number or a valid TimeRange'
+ );
+ conditions.until = TimeRangeToUnix[conditions.until];
+ }
+ if (conditions.since && typeof conditions.since !== 'number') {
+ if (!Object.keys(TimeRangeToUnix).includes(conditions.since))
+ throw new Error(
+ 'conditions.since must be either a number or a valid TimeRange'
+ );
+ conditions.since = TimeRangeToUnix[conditions.since];
+ }
+
+ const sig = await this.rpc.call({
+ target: this.target,
+ request: {
+ method: 'delegate',
+ params: [delegatee, conditions],
+ },
+ });
+ return sig as string;
+ }
+
async getRelays(): Promise<{
[url: string]: { read: boolean; write: boolean };
}> {
diff --git a/test/connect.test.ts b/test/connect.test.ts
index 5a0cd58..874f12e 100644
--- a/test/connect.test.ts
+++ b/test/connect.test.ts
@@ -1,8 +1,8 @@
-import { getPublicKey, signEvent, Event } from 'nostr-tools';
-import { Connect, ConnectURI, NostrSigner } from '../src';
+import { getPublicKey, signEvent, Event, nip26 } from 'nostr-tools';
+import { Connect, ConnectURI, NostrSigner, TimeRanges } from '../src';
import { sleep } from './utils';
-jest.setTimeout(5000);
+jest.setTimeout(7500);
// web app (this is ephemeral and represents the currention session)
const webSK =
@@ -21,27 +21,34 @@ class MobileHandler extends NostrSigner {
return getPublicKey(this.self.secret);
}
async sign_event(event: any): Promise {
- 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'));
- });
- });
+ const sigEvt = signEvent(event, this.self.secret);
+ return Promise.resolve(sigEvt);
+ }
+ async delegate(
+ delegatee: string,
+ conditions: {
+ kind?: number;
+ until?: number;
+ since?: number;
+ }
+ ): Promise {
+ const delegateParameters: nip26.Parameters = {
+ pubkey: delegatee,
+ kind: conditions.kind,
+ since: conditions.since || Math.round(Date.now() / 1000),
+ until:
+ conditions.until ||
+ Math.round(Date.now() / 1000) + 60 * 60 * 24 * 30 /* 30 days */,
+ };
+ const delegation = nip26.createDelegation(
+ this.self.secret,
+ delegateParameters
+ );
+ return Promise.resolve(delegation.sig);
}
}
-describe('Nostr Connect', () => {
+describe('ConnectURI', () => {
it('roundtrip connectURI', async () => {
const connectURI = new ConnectURI({
target: `b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4`,
@@ -70,6 +77,45 @@ describe('Nostr Connect', () => {
'https://vulpem.com/1000x860-p-500.422be1bc.png'
);
});
+});
+
+describe('Connect', () => {
+ beforeAll(async () => {
+ try {
+ // start listening for connect messages on the mobile app
+ const remoteHandler = new MobileHandler({
+ secretKey: mobileSK,
+ relay: 'wss://nostr.vulpem.com',
+ });
+ await remoteHandler.listen();
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+ });
+
+ it('returns pubkey and delegation', async () => {
+ // start listening for connect messages on the web app
+ const connect = new Connect({
+ secretKey: webSK,
+ target: mobilePK,
+ });
+ await connect.init();
+
+ sleep(1000);
+
+ // send the get_public_key message to the mobile app from the web
+ const pubkey = await connect.getPublicKey();
+ expect(pubkey).toBe(mobilePK);
+
+ // send the delegate message to the mobile app from the web to ask for permission to sign kind 1 notes on behalf of the user for 5 mins
+ const sig = await connect.delegate(webPK, {
+ kind: 1,
+ until: TimeRanges.FIVE_MINS,
+ });
+ expect(sig).toBeTruthy();
+ });
+
it.skip('connect', async () => {
const testHandler = jest.fn();
@@ -95,31 +141,6 @@ describe('Nostr Connect', () => {
expect(testHandler).toBeCalledTimes(1);
});
-
- it('returns pubkey', async () => {
- // start listening for connect messages on the mobile app
- const remoteHandler = new MobileHandler({
- secretKey: mobileSK,
- relay: 'wss://nostr.vulpem.com',
- });
- 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);
-
- // send the get_public_key message to the mobile app from the web
- const pubkey = await connect.getPublicKey();
- expect(pubkey).toBe(mobilePK);
- });
-
it.skip('returns a signed event', async () => {
// start listening for connect messages on the mobile app
const remoteHandler = new MobileHandler({
@@ -164,36 +185,3 @@ describe('Nostr Connect', () => {
expect(event).toBeDefined();
});
});
-
-/*
- expect(handler).toBeCalledTimes(1);
- expect(handler).toBeCalledWith({
- type: ConnectMessageType.PAIRED,
- value: {
- pubkey: mobilePK,
- }
- });
-
- const pubkey = await connect.getPublicKey();
- expect(pubkey).toBe(mobilePK);
- const signedEvt = await connect.signEvent({});
- const relays = await connect.getRelays();
-
- const plainText = "hello 🌍";
- const cipherText = await connect.nip04.encrypt(childPK, plainText);
- const plainText2 = await connect.nip04.decrypt(childPK, cipherText);
- expect(plainText === plainText2).toBeTruthy();
-
- await connect.request({
- method: 'signSchnorr',
- params: [
- '0x000000',
- '0x000000'
- ]
- });
-
-
- sessionWeb.on(ConnectMessageType.UNPAIRED, () => {
- });
-
- */