Skip to content

Commit bc5fab4

Browse files
committed
update NIP-60 implementation
1 parent 2cf2670 commit bc5fab4

File tree

17 files changed

+241
-91
lines changed

17 files changed

+241
-91
lines changed

docs/mobile/index.md

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,39 +25,27 @@ When using this library don't import `@nostr-dev-kit/ndk` directly, instead impo
2525

2626
Initialize NDK using the init function, probably when your app loads.
2727

28-
```tsx
29-
function App() {
30-
const { ndk, init: initializeNDK } = useNDK();
31-
32-
useEffect(() => {
33-
initializeNDK({
34-
/* Any parameter you'd want to pass to NDK */
35-
explicitRelayUrls: [...],
36-
// ...
37-
}
38-
}, []);
39-
}
40-
```
41-
42-
### Settings
43-
4428
Throughout the use of a normal app, you will probably want to store some settings, such us, the user that is logged in. `ndk-mobile` can take care of this for you automatically if you pass a `settingsStore` to the initialization. For example, using `expo-secure-store` you can:
4529

4630
```tsx
4731
import * as SecureStore from 'expo-secure-store';
4832

33+
const ndk = new NDK({
34+
/* Any parameter you'd want to pass to NDK */
35+
explicitRelayUrls: [...],
36+
// ...
37+
})
38+
4939
const settingsStore = {
5040
get: SecureStore.getItemAsync,
5141
set: SecureStore.setItemAsync,
5242
delete: SecureStore.deleteItemAsync,
5343
getSync: SecureStore.getItem,
5444
};
5545

56-
// and then, when you initialiaze NDK:
57-
initializeNDK({
58-
......,
59-
settingsStore
60-
})
46+
function App() {
47+
useNDKInit(ndk, settingsStore);
48+
}
6149
```
6250

6351
Now, once your user logs in, their login information will be stored locally so when your app restarts, the user will be logged in automatically.
@@ -81,4 +69,4 @@ function LoginScreen() {
8169

8270
## Example
8371

84-
For a real application using this look at [Olas](https://github.com/pablof7z/snapstr).
72+
For a real application using this look at [Olas](https://github.com/pablof7z/olas).

docs/wallet/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ As a developer, the first thing you need to do to use a wallet in your app is to
1515
Once you instantiate the desired wallet, you simply pass it to ndk.
1616

1717
```ts
18-
const wallet = new NDKNWCWallet(ndk);
19-
await wallet.initWithPairingCode("nostr+walletconnect:....");
18+
const wallet = new NDKNWCWallet(ndk, { timeout: 5000, pairingCode: "nostr+walletconnect:...." });
2019
ndk.wallet = wallet;
20+
wallet.on("timeout", (method: string) => console.log('Unable to complete the operation in time', { method }))
2121
```
2222

2323
Now whenever you want to pay something, the wallet will be called. Refer to the Nutsack adapter to see more details of the interface.

ndk-mobile/src/cache-adapter/migrations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,16 @@ export const migrations = [
120120
},
121121
},
122122

123+
{
124+
version: 6,
125+
up: async (db: SQLite.SQLiteDatabase) => {
126+
await db.execAsync(`DROP TABLE IF EXISTS wallet_nutzaps;`); // XXX
127+
await db.execAsync(`CREATE TABLE IF NOT EXISTS wallet_nutzaps (
128+
event_id TEXT PRIMARY KEY UNIQUE,
129+
status TEXT,
130+
claimed_at INTEGER,
131+
tx_event_id TEXT
132+
);`);
133+
},
134+
},
123135
];

ndk-mobile/src/cache-adapter/sqlite.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,15 @@ type PendingCallback = (...arg: any) => any;
5050
function filterForCache(subscription: NDKSubscription) {
5151
if (!subscription.cacheUnconstrainFilter) return subscription.filters;
5252

53-
const filterCopy = [...subscription.filters];
53+
const filterCopy = subscription.filters.map(filter => ({ ...filter }));
5454

5555
// remove the keys that are in the cacheUnconstrainFilter
56-
filterCopy.filter((filter) => {
56+
return filterCopy.filter((filter) => {
5757
for (const key of subscription.cacheUnconstrainFilter) {
5858
delete filter[key];
5959
}
6060
return Object.keys(filter).length > 0;
6161
});
62-
63-
return filterCopy;
6462
}
6563

6664
export class NDKCacheAdapterSqlite implements NDKCacheAdapter {
@@ -84,12 +82,15 @@ export class NDKCacheAdapterSqlite implements NDKCacheAdapter {
8482
constructor(dbName: string, maxProfiles: number = 200) {
8583
this.dbName = dbName ?? 'ndk-cache';
8684
this.profileCache = new LRUCache({ maxSize: maxProfiles });
87-
this.initialize();
88-
}
89-
90-
private async initialize() {
9185
this.db = SQLite.openDatabaseSync(this.dbName);
86+
}
9287

88+
/**
89+
* Initialize the cache adapter.
90+
*
91+
* This should be called before using it.
92+
*/
93+
public async initialize() {
9394
let { user_version: schemaVersion } = (this.db.getFirstSync(`PRAGMA user_version;`)) as { user_version: number };
9495

9596
if (!schemaVersion) {

ndk-mobile/src/db/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as nutzaps from './wallet/nutzaps.js';
2+
3+
const wallet = {
4+
nutzaps
5+
}
6+
7+
export {
8+
wallet
9+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import NDK, { NDKNutzap } from "@nostr-dev-kit/ndk";
2+
import * as SQLite from "expo-sqlite";
3+
import { NDKCacheAdapterSqlite } from "../../cache-adapter/sqlite.js";
4+
5+
function withDb(ndk: NDK) {
6+
const db = (ndk.cacheAdapter as NDKCacheAdapterSqlite).db;
7+
if (!(db instanceof SQLite.SQLiteDatabase)) {
8+
throw new Error("Database is not an instance of SQLiteDatabase");
9+
}
10+
return db;
11+
}
12+
13+
export interface NDKDBNutzap {
14+
event_id: string;
15+
status: string;
16+
claim_at: number;
17+
claimed_at: number;
18+
}
19+
20+
export function getNutzaps(ndk: NDK): NDKDBNutzap[] {
21+
const db = withDb(ndk);
22+
const nutzaps = db.getAllSync("SELECT * FROM wallet_nutzaps") as NDKDBNutzap[];
23+
24+
console.log('known nutzaps', nutzaps.length);
25+
console.table(nutzaps);
26+
27+
return nutzaps;
28+
}
29+
30+
export function saveNutzap(ndk: NDK, nutzap: NDKNutzap, status?: string, claimedAt?: number, txEventId?: string) {
31+
const db = withDb(ndk);
32+
33+
console.log('[DB] saving nutzap', nutzap.id, status, claimedAt, txEventId);
34+
35+
db.runSync(
36+
`INSERT OR REPLACE INTO wallet_nutzaps (event_id, status, claimed_at, tx_event_id)
37+
VALUES (?, ?, ?, ?)`,
38+
[nutzap.id, status, claimedAt, txEventId]
39+
);
40+
}

ndk-mobile/src/hooks/ndk.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import { NDKEvent } from '@nostr-dev-kit/ndk';
1+
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk';
22
import { useCallback, useEffect, useRef, useState } from 'react';
33
import { useNDKStore } from '../stores/ndk.js';
4+
import { SettingsStore } from '../types.js';
45

56
export const useNDK = () => {
67
const ndk = useNDKStore(s => s.ndk);
7-
const init = useNDKStore(s => s.init);
88
const login = useNDKStore(s => s.login);
99
const logout = useNDKStore(s => s.logout);
1010

11-
return { ndk, init, login, logout };
11+
return { ndk, login, logout };
12+
}
13+
14+
export const useNDKInit = (ndk: NDK, settingsStore: SettingsStore) => {
15+
const storeInit = useNDKStore(s => s.init);
16+
17+
useEffect(() => {
18+
storeInit(ndk, settingsStore);
19+
}, []);
1220
}
1321

1422
export const useNDKCurrentUser = () => useNDKStore(s => s.currentUser);
15-
export const useNDKCacheInitialized = () => useNDKStore(s => s.cacheInitialized);
1623

1724
export function useNDKUnpublishedEvents() {
1825
const { ndk } = useNDK();

ndk-mobile/src/hooks/session.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
11
import NDK, { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
2+
import { useState, useEffect } from 'react';
23
import { useNDK } from './ndk.js';
3-
import { NDKEventWithFrom } from './subscribe.js';
4+
import { NDKEventWithFrom, NDKEventWithAsyncFrom } from './subscribe.js';
45
import { useNDKSession } from '../stores/session/index.js';
56
import { useNDKWallet } from './wallet.js';
67
import { walletFromLoadingString } from '@nostr-dev-kit/ndk-wallet';
78
import { SessionInitOpts, SessionInitCallbacks } from '../stores/session/types.js';
89
import { SettingsStore } from '../types.js';
910

1011
const useNDKSessionInit = () => {
11-
const init = useNDKSession(s => s.init);
12+
return useNDKSession(s => s.init);
13+
}
1214

15+
const useNDKSessionInitWallet = () => {
1316
const { setActiveWallet } = useNDKWallet();
1417

15-
const wrappedInit = (ndk: NDK, user: NDKUser, settingsStore: SettingsStore, opts: SessionInitOpts, on: SessionInitCallbacks) => {
16-
init(ndk, user, settingsStore, opts, on);
17-
18+
const initWallet = (ndk: NDK, settingsStore: SettingsStore) => {
1819
const walletString = settingsStore?.getSync('wallet');
1920
if (walletString) {
2021
walletFromLoadingString(ndk, walletString).then((wallet) => {
2122
if (wallet) setActiveWallet(wallet);
22-
}).catch((e) => {
23-
console.error('error setting active wallet', e);
2423
});
2524
}
2625
}
2726

28-
return wrappedInit;
27+
return initWallet;
2928
}
3029

3130
const useFollows = () => useNDKSession(s => s.follows);
@@ -51,9 +50,10 @@ const useWOT = () => useNDKSession(s => s.wot);
5150
*/
5251
const useNDKSessionEventKind = <T extends NDKEvent>(
5352
EventClass: NDKEventWithFrom<any>,
54-
kind: NDKKind,
53+
kind?: NDKKind,
5554
{ create }: { create: boolean } = { create: false }
5655
): T | undefined => {
56+
kind ??= EventClass.kind;
5757
const { ndk } = useNDK();
5858
const events = useNDKSession(s => s.events);
5959
const kindEvents = events.get(kind) || [];
@@ -69,6 +69,27 @@ const useNDKSessionEventKind = <T extends NDKEvent>(
6969
return firstEvent ? EventClass.from(firstEvent) : undefined;
7070
};
7171

72+
const useNDKSessionEventKindAsync = <T>(
73+
EventClass: NDKEventWithAsyncFrom<any>,
74+
kind?: NDKKind,
75+
{ create }: { create: boolean } = { create: false }
76+
): T | undefined => {
77+
kind ??= EventClass.kind;
78+
const events = useNDKSession(s => s.events);
79+
const kindEvents = events.get(kind) || [];
80+
const firstEvent = kindEvents[0];
81+
const [res, setRes] = useState<T | undefined>(undefined);
82+
83+
useEffect(() => {
84+
if (!firstEvent) return;
85+
EventClass.from(firstEvent).then((event) => {
86+
setRes(event);
87+
});
88+
}, [firstEvent]);
89+
90+
return res;
91+
};
92+
7293
const useNDKSessionEvents = <T extends NDKEvent>(
7394
kinds: NDKKind[],
7495
eventClass?: NDKEventWithFrom<any>,
@@ -92,4 +113,6 @@ export {
92113
useNDKSessionEventKind,
93114
useNDKSessionEvents,
94115
useNDKSessionInit,
116+
useNDKSessionInitWallet,
117+
useNDKSessionEventKindAsync,
95118
};

ndk-mobile/src/hooks/subscribe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useNDKSession } from '../stores/session/index.js';
1010
* Extends NDKEvent with a 'from' method to wrap events with a kind-specific handler
1111
*/
1212
export type NDKEventWithFrom<T extends NDKEvent> = T & { from: (event: NDKEvent) => T };
13+
export type NDKEventWithAsyncFrom<T extends NDKEvent> = T & { from: (event: NDKEvent) => Promise<T> };
1314

1415
export type UseSubscribeOptions = NDKSubscriptionOptions & {
1516
/**
@@ -303,7 +304,6 @@ export function useMuteFilter() {
303304
mutedHashtags.forEach(h => _mutedHashtags.add(h.toLowerCase()));
304305

305306
return (event: NDKEvent) => {
306-
const start = performance.now();
307307
const tags = new Set(event.getMatchingTags('t').map(tag => tag[1].toLowerCase()));
308308
const taggedEvents = new Set(event.getMatchingTags('e').map(tag => tag[1]));
309309
taggedEvents.add(event.id);

ndk-mobile/src/hooks/wallet.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ import { useWalletStore } from '../stores/wallet.js';
33
import { useNDK, useNDKCurrentUser } from './ndk.js';
44
import { useNDKStore } from '../stores/ndk.js';
55
import { useEffect, useCallback } from 'react';
6+
import { getNutzaps, saveNutzap } from '../db/wallet/nutzaps.js';
7+
import NDK from '@nostr-dev-kit/ndk';
8+
9+
const getKnownNutzaps = (ndk: NDK) => {
10+
const nutzaps = getNutzaps(ndk);
11+
return new Set(nutzaps
12+
.filter(n => n.status === 'redeemed' || n.status === 'spent')
13+
.map(n => n.event_id)
14+
);
15+
}
616

717
/**
818
* @param start - Whether to start the nutzap monitor if it hasn't been started yet.
@@ -22,11 +32,33 @@ const useNDKNutzapMonitor = (start: boolean = true) => {
2232
if (!activeWallet?.walletId) return;
2333
if (nutzapMonitor) return;
2434

35+
const knownNutzaps = getKnownNutzaps(ndk);
2536
const monitor = new NDKNutzapMonitor(ndk, currentUser);
26-
console.log("[NDK-WALLET] Starting nutzap monitor");
37+
2738
setNutzapMonitor(monitor);
28-
if (activeWallet instanceof NDKCashuWallet) monitor.addWallet(activeWallet);
29-
monitor.start();
39+
40+
if (activeWallet instanceof NDKCashuWallet) monitor.wallet = activeWallet;
41+
42+
monitor.on("seen", (event) => {
43+
saveNutzap(ndk, event);
44+
});
45+
46+
monitor.on("redeem", (event) => {
47+
saveNutzap(ndk, event, "redeemed", Math.floor(Date.now()/1000));
48+
});
49+
50+
monitor.on("spent", (event) => {
51+
saveNutzap(ndk, event, "spent");
52+
});
53+
54+
monitor.on("failed", (event) => {
55+
saveNutzap(ndk, event, "failed");
56+
});
57+
58+
monitor.start(undefined, {
59+
knownNutzaps: knownNutzaps,
60+
pageSize: 10,
61+
});
3062
}, [ nutzapMonitor, setNutzapMonitor, activeWallet?.walletId, currentUser?.pubkey, ndk, start ])
3163

3264
return { nutzapMonitor, setNutzapMonitor };
@@ -43,8 +75,7 @@ const useNDKWallet = () => {
4375
const storeLastUpdatedAt = useCallback((wallet: NDKWallet) => {
4476
if (!(wallet instanceof NDKCashuWallet)) return;
4577
const now = Math.floor(Date.now()/1000);
46-
settingsStore.set('wallet_'+wallet.walletId+'_last_updated_at', now.toString());
47-
console.log("[NDK-WALLET] Stored last updated at for wallet", wallet.walletId, "as", now);
78+
settingsStore.set('wallet_last_updated_at', now.toString());
4879
}, [ settingsStore ]);
4980

5081
const setActiveWallet = useCallback((wallet: NDKWallet) => {
@@ -69,18 +100,22 @@ const useNDKWallet = () => {
69100
wallet.on('ready', updateBalance);
70101
wallet.on('balance_updated', updateBalance);
71102
} else {
103+
settingsStore.delete('wallet');
104+
settingsStore.delete('wallet_last_updated_at');
105+
72106
setBalance(null);
73107
}
74108

75109
if (wallet instanceof NDKCashuWallet) {
76-
const lastUpdatedAt = settingsStore?.getSync('wallet_' + wallet.walletId + '_last_updated_at');
77-
console.log("[NDK-WALLET] Starting wallet", wallet.walletId, "with since", lastUpdatedAt);
110+
const lastUpdatedAt = settingsStore?.getSync('wallet_last_updated_at');
78111
wallet.start({ subId: 'wallet', since: lastUpdatedAt ? parseInt(lastUpdatedAt) : undefined });
79112
}
80113

81-
if (wallet) wallet.updateBalance?.();
114+
if (wallet) {
115+
wallet.updateBalance?.();
116+
loadingString = wallet.toLoadingString?.();
117+
}
82118

83-
if (wallet) loadingString = wallet.toLoadingString?.();
84119
if (loadingString)
85120
settingsStore.set('wallet', loadingString);
86121
else

0 commit comments

Comments
 (0)