@unicitylabs/sphere-sdk 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -69
- package/dist/core/index.cjs +167 -62
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +16 -5
- package/dist/core/index.d.ts +16 -5
- package/dist/core/index.js +167 -62
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +89 -12
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +89 -12
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +3 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +3 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +77 -12
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +21 -0
- package/dist/impl/nodejs/index.d.ts +21 -0
- package/dist/impl/nodejs/index.js +77 -12
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +167 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.js +167 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -552,6 +552,8 @@ interface IncomingPaymentRequest {
|
|
|
552
552
|
id: string;
|
|
553
553
|
/** Transport-specific pubkey of sender */
|
|
554
554
|
senderTransportPubkey: string;
|
|
555
|
+
/** Sender's nametag (if included in encrypted content) */
|
|
556
|
+
senderNametag?: string;
|
|
555
557
|
/** Parsed request data */
|
|
556
558
|
request: {
|
|
557
559
|
requestId: string;
|
|
@@ -675,6 +677,14 @@ type UUIDGenerator = () => string;
|
|
|
675
677
|
* WebSocket is injected via factory for cross-platform support
|
|
676
678
|
*/
|
|
677
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Minimal key-value storage interface for transport persistence.
|
|
682
|
+
* Used to persist the last processed event timestamp across sessions.
|
|
683
|
+
*/
|
|
684
|
+
interface TransportStorageAdapter {
|
|
685
|
+
get(key: string): Promise<string | null>;
|
|
686
|
+
set(key: string, value: string): Promise<void>;
|
|
687
|
+
}
|
|
678
688
|
interface NostrTransportProviderConfig {
|
|
679
689
|
/** Nostr relay URLs */
|
|
680
690
|
relays?: string[];
|
|
@@ -692,6 +702,8 @@ interface NostrTransportProviderConfig {
|
|
|
692
702
|
createWebSocket: WebSocketFactory;
|
|
693
703
|
/** UUID generator (optional, defaults to crypto.randomUUID) */
|
|
694
704
|
generateUUID?: UUIDGenerator;
|
|
705
|
+
/** Optional storage adapter for persisting subscription timestamps */
|
|
706
|
+
storage?: TransportStorageAdapter;
|
|
695
707
|
}
|
|
696
708
|
declare class NostrTransportProvider implements TransportProvider {
|
|
697
709
|
readonly id = "nostr";
|
|
@@ -699,6 +711,9 @@ declare class NostrTransportProvider implements TransportProvider {
|
|
|
699
711
|
readonly type: "p2p";
|
|
700
712
|
readonly description = "P2P messaging via Nostr protocol";
|
|
701
713
|
private config;
|
|
714
|
+
private storage;
|
|
715
|
+
/** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
|
|
716
|
+
private lastEventTs;
|
|
702
717
|
private identity;
|
|
703
718
|
private keyManager;
|
|
704
719
|
private status;
|
|
@@ -800,6 +815,12 @@ declare class NostrTransportProvider implements TransportProvider {
|
|
|
800
815
|
publishBroadcast(content: string, tags?: string[]): Promise<string>;
|
|
801
816
|
onEvent(callback: TransportEventCallback): () => void;
|
|
802
817
|
private handleEvent;
|
|
818
|
+
/**
|
|
819
|
+
* Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
|
|
820
|
+
* Uses in-memory `lastEventTs` to avoid read-before-write race conditions
|
|
821
|
+
* when multiple events arrive in quick succession.
|
|
822
|
+
*/
|
|
823
|
+
private updateLastEventTimestamp;
|
|
803
824
|
private handleDirectMessage;
|
|
804
825
|
private handleGiftWrap;
|
|
805
826
|
private handleTokenTransfer;
|
|
@@ -552,6 +552,8 @@ interface IncomingPaymentRequest {
|
|
|
552
552
|
id: string;
|
|
553
553
|
/** Transport-specific pubkey of sender */
|
|
554
554
|
senderTransportPubkey: string;
|
|
555
|
+
/** Sender's nametag (if included in encrypted content) */
|
|
556
|
+
senderNametag?: string;
|
|
555
557
|
/** Parsed request data */
|
|
556
558
|
request: {
|
|
557
559
|
requestId: string;
|
|
@@ -675,6 +677,14 @@ type UUIDGenerator = () => string;
|
|
|
675
677
|
* WebSocket is injected via factory for cross-platform support
|
|
676
678
|
*/
|
|
677
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Minimal key-value storage interface for transport persistence.
|
|
682
|
+
* Used to persist the last processed event timestamp across sessions.
|
|
683
|
+
*/
|
|
684
|
+
interface TransportStorageAdapter {
|
|
685
|
+
get(key: string): Promise<string | null>;
|
|
686
|
+
set(key: string, value: string): Promise<void>;
|
|
687
|
+
}
|
|
678
688
|
interface NostrTransportProviderConfig {
|
|
679
689
|
/** Nostr relay URLs */
|
|
680
690
|
relays?: string[];
|
|
@@ -692,6 +702,8 @@ interface NostrTransportProviderConfig {
|
|
|
692
702
|
createWebSocket: WebSocketFactory;
|
|
693
703
|
/** UUID generator (optional, defaults to crypto.randomUUID) */
|
|
694
704
|
generateUUID?: UUIDGenerator;
|
|
705
|
+
/** Optional storage adapter for persisting subscription timestamps */
|
|
706
|
+
storage?: TransportStorageAdapter;
|
|
695
707
|
}
|
|
696
708
|
declare class NostrTransportProvider implements TransportProvider {
|
|
697
709
|
readonly id = "nostr";
|
|
@@ -699,6 +711,9 @@ declare class NostrTransportProvider implements TransportProvider {
|
|
|
699
711
|
readonly type: "p2p";
|
|
700
712
|
readonly description = "P2P messaging via Nostr protocol";
|
|
701
713
|
private config;
|
|
714
|
+
private storage;
|
|
715
|
+
/** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
|
|
716
|
+
private lastEventTs;
|
|
702
717
|
private identity;
|
|
703
718
|
private keyManager;
|
|
704
719
|
private status;
|
|
@@ -800,6 +815,12 @@ declare class NostrTransportProvider implements TransportProvider {
|
|
|
800
815
|
publishBroadcast(content: string, tags?: string[]): Promise<string>;
|
|
801
816
|
onEvent(callback: TransportEventCallback): () => void;
|
|
802
817
|
private handleEvent;
|
|
818
|
+
/**
|
|
819
|
+
* Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
|
|
820
|
+
* Uses in-memory `lastEventTs` to avoid read-before-write race conditions
|
|
821
|
+
* when multiple events arrive in quick succession.
|
|
822
|
+
*/
|
|
823
|
+
private updateLastEventTimestamp;
|
|
803
824
|
private handleDirectMessage;
|
|
804
825
|
private handleGiftWrap;
|
|
805
826
|
private handleTokenTransfer;
|
|
@@ -25,7 +25,9 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
25
25
|
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
26
26
|
ADDRESS_NAMETAGS: "address_nametags",
|
|
27
27
|
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
28
|
-
TRACKED_ADDRESSES: "tracked_addresses"
|
|
28
|
+
TRACKED_ADDRESSES: "tracked_addresses",
|
|
29
|
+
/** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
|
|
30
|
+
LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
|
|
29
31
|
};
|
|
30
32
|
var STORAGE_KEYS_ADDRESS = {
|
|
31
33
|
/** Pending transfers for this address */
|
|
@@ -1101,6 +1103,9 @@ var NostrTransportProvider = class {
|
|
|
1101
1103
|
type = "p2p";
|
|
1102
1104
|
description = "P2P messaging via Nostr protocol";
|
|
1103
1105
|
config;
|
|
1106
|
+
storage = null;
|
|
1107
|
+
/** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
|
|
1108
|
+
lastEventTs = 0;
|
|
1104
1109
|
identity = null;
|
|
1105
1110
|
keyManager = null;
|
|
1106
1111
|
status = "disconnected";
|
|
@@ -1126,6 +1131,7 @@ var NostrTransportProvider = class {
|
|
|
1126
1131
|
createWebSocket: config.createWebSocket,
|
|
1127
1132
|
generateUUID: config.generateUUID ?? defaultUUIDGenerator
|
|
1128
1133
|
};
|
|
1134
|
+
this.storage = config.storage ?? null;
|
|
1129
1135
|
}
|
|
1130
1136
|
// ===========================================================================
|
|
1131
1137
|
// BaseProvider Implementation
|
|
@@ -1164,7 +1170,14 @@ var NostrTransportProvider = class {
|
|
|
1164
1170
|
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
1165
1171
|
}
|
|
1166
1172
|
});
|
|
1167
|
-
await
|
|
1173
|
+
await Promise.race([
|
|
1174
|
+
this.nostrClient.connect(...this.config.relays),
|
|
1175
|
+
new Promise(
|
|
1176
|
+
(_, reject) => setTimeout(() => reject(new Error(
|
|
1177
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
1178
|
+
)), this.config.timeout)
|
|
1179
|
+
)
|
|
1180
|
+
]);
|
|
1168
1181
|
if (!this.nostrClient.isConnected()) {
|
|
1169
1182
|
throw new Error("Failed to connect to any relay");
|
|
1170
1183
|
}
|
|
@@ -1172,7 +1185,7 @@ var NostrTransportProvider = class {
|
|
|
1172
1185
|
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
1173
1186
|
this.log("Connected to", this.nostrClient.getConnectedRelays().size, "relays");
|
|
1174
1187
|
if (this.identity) {
|
|
1175
|
-
this.subscribeToEvents();
|
|
1188
|
+
await this.subscribeToEvents();
|
|
1176
1189
|
}
|
|
1177
1190
|
} catch (error) {
|
|
1178
1191
|
this.status = "error";
|
|
@@ -1325,11 +1338,18 @@ var NostrTransportProvider = class {
|
|
|
1325
1338
|
this.log("NostrClient reconnected to relay:", url);
|
|
1326
1339
|
}
|
|
1327
1340
|
});
|
|
1328
|
-
await
|
|
1329
|
-
|
|
1341
|
+
await Promise.race([
|
|
1342
|
+
this.nostrClient.connect(...this.config.relays),
|
|
1343
|
+
new Promise(
|
|
1344
|
+
(_, reject) => setTimeout(() => reject(new Error(
|
|
1345
|
+
`Transport reconnection timed out after ${this.config.timeout}ms`
|
|
1346
|
+
)), this.config.timeout)
|
|
1347
|
+
)
|
|
1348
|
+
]);
|
|
1349
|
+
await this.subscribeToEvents();
|
|
1330
1350
|
oldClient.disconnect();
|
|
1331
1351
|
} else if (this.isConnected()) {
|
|
1332
|
-
this.subscribeToEvents();
|
|
1352
|
+
await this.subscribeToEvents();
|
|
1333
1353
|
}
|
|
1334
1354
|
}
|
|
1335
1355
|
/**
|
|
@@ -1857,10 +1877,31 @@ var NostrTransportProvider = class {
|
|
|
1857
1877
|
this.handleBroadcast(event);
|
|
1858
1878
|
break;
|
|
1859
1879
|
}
|
|
1880
|
+
if (event.created_at && this.storage && this.keyManager) {
|
|
1881
|
+
const kind = event.kind;
|
|
1882
|
+
if (kind === EVENT_KINDS.DIRECT_MESSAGE || kind === EVENT_KINDS.TOKEN_TRANSFER || kind === EVENT_KINDS.PAYMENT_REQUEST || kind === EVENT_KINDS.PAYMENT_REQUEST_RESPONSE) {
|
|
1883
|
+
this.updateLastEventTimestamp(event.created_at);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1860
1886
|
} catch (error) {
|
|
1861
1887
|
this.log("Failed to handle event:", error);
|
|
1862
1888
|
}
|
|
1863
1889
|
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Save the max event timestamp to storage (fire-and-forget, no await needed by caller).
|
|
1892
|
+
* Uses in-memory `lastEventTs` to avoid read-before-write race conditions
|
|
1893
|
+
* when multiple events arrive in quick succession.
|
|
1894
|
+
*/
|
|
1895
|
+
updateLastEventTimestamp(createdAt) {
|
|
1896
|
+
if (!this.storage || !this.keyManager) return;
|
|
1897
|
+
if (createdAt <= this.lastEventTs) return;
|
|
1898
|
+
this.lastEventTs = createdAt;
|
|
1899
|
+
const pubkey = this.keyManager.getPublicKeyHex();
|
|
1900
|
+
const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${pubkey.slice(0, 16)}`;
|
|
1901
|
+
this.storage.set(storageKey, createdAt.toString()).catch((err) => {
|
|
1902
|
+
this.log("Failed to save last event timestamp:", err);
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1864
1905
|
async handleDirectMessage(event) {
|
|
1865
1906
|
this.log("Ignoring NIP-04 kind 4 event (DMs use NIP-17):", event.id?.slice(0, 12));
|
|
1866
1907
|
}
|
|
@@ -1939,6 +1980,7 @@ var NostrTransportProvider = class {
|
|
|
1939
1980
|
const request = {
|
|
1940
1981
|
id: event.id,
|
|
1941
1982
|
senderTransportPubkey: event.pubkey,
|
|
1983
|
+
senderNametag: requestData.recipientNametag,
|
|
1942
1984
|
request: {
|
|
1943
1985
|
requestId: requestData.requestId,
|
|
1944
1986
|
amount: requestData.amount,
|
|
@@ -2093,7 +2135,7 @@ var NostrTransportProvider = class {
|
|
|
2093
2135
|
// Track subscription IDs for cleanup
|
|
2094
2136
|
walletSubscriptionId = null;
|
|
2095
2137
|
chatSubscriptionId = null;
|
|
2096
|
-
subscribeToEvents() {
|
|
2138
|
+
async subscribeToEvents() {
|
|
2097
2139
|
this.log("subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
|
|
2098
2140
|
if (!this.identity || !this.keyManager || !this.nostrClient) {
|
|
2099
2141
|
this.log("subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
|
|
@@ -2113,6 +2155,27 @@ var NostrTransportProvider = class {
|
|
|
2113
2155
|
}
|
|
2114
2156
|
const nostrPubkey = this.keyManager.getPublicKeyHex();
|
|
2115
2157
|
this.log("Subscribing with Nostr pubkey:", nostrPubkey);
|
|
2158
|
+
let since;
|
|
2159
|
+
if (this.storage) {
|
|
2160
|
+
const storageKey = `${STORAGE_KEYS_GLOBAL.LAST_WALLET_EVENT_TS}_${nostrPubkey.slice(0, 16)}`;
|
|
2161
|
+
try {
|
|
2162
|
+
const stored = await this.storage.get(storageKey);
|
|
2163
|
+
if (stored) {
|
|
2164
|
+
since = parseInt(stored, 10);
|
|
2165
|
+
this.lastEventTs = since;
|
|
2166
|
+
this.log("Resuming from stored event timestamp:", since);
|
|
2167
|
+
} else {
|
|
2168
|
+
since = Math.floor(Date.now() / 1e3);
|
|
2169
|
+
this.log("No stored timestamp, starting from now:", since);
|
|
2170
|
+
}
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
this.log("Failed to read last event timestamp, falling back to now:", err);
|
|
2173
|
+
since = Math.floor(Date.now() / 1e3);
|
|
2174
|
+
}
|
|
2175
|
+
} else {
|
|
2176
|
+
since = Math.floor(Date.now() / 1e3) - 86400;
|
|
2177
|
+
this.log("No storage adapter, using 24h fallback");
|
|
2178
|
+
}
|
|
2116
2179
|
const walletFilter = new Filter();
|
|
2117
2180
|
walletFilter.kinds = [
|
|
2118
2181
|
EVENT_KINDS.DIRECT_MESSAGE,
|
|
@@ -2121,7 +2184,7 @@ var NostrTransportProvider = class {
|
|
|
2121
2184
|
EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
|
|
2122
2185
|
];
|
|
2123
2186
|
walletFilter["#p"] = [nostrPubkey];
|
|
2124
|
-
walletFilter.since =
|
|
2187
|
+
walletFilter.since = since;
|
|
2125
2188
|
this.walletSubscriptionId = this.nostrClient.subscribe(walletFilter, {
|
|
2126
2189
|
onEvent: (event) => {
|
|
2127
2190
|
this.log("Received wallet event kind:", event.kind, "id:", event.id?.slice(0, 12));
|
|
@@ -2918,10 +2981,11 @@ function createNodeProviders(config) {
|
|
|
2918
2981
|
const oracleConfig = resolveOracleConfig(network, config?.oracle);
|
|
2919
2982
|
const l1Config = resolveL1Config(network, config?.l1);
|
|
2920
2983
|
const priceConfig = resolvePriceConfig(config?.price);
|
|
2984
|
+
const storage = createFileStorageProvider({
|
|
2985
|
+
dataDir: config?.dataDir ?? "./sphere-data"
|
|
2986
|
+
});
|
|
2921
2987
|
return {
|
|
2922
|
-
storage
|
|
2923
|
-
dataDir: config?.dataDir ?? "./sphere-data"
|
|
2924
|
-
}),
|
|
2988
|
+
storage,
|
|
2925
2989
|
tokenStorage: createFileTokenStorageProvider({
|
|
2926
2990
|
tokensDir: config?.tokensDir ?? "./sphere-tokens"
|
|
2927
2991
|
}),
|
|
@@ -2929,7 +2993,8 @@ function createNodeProviders(config) {
|
|
|
2929
2993
|
relays: transportConfig.relays,
|
|
2930
2994
|
timeout: transportConfig.timeout,
|
|
2931
2995
|
autoReconnect: transportConfig.autoReconnect,
|
|
2932
|
-
debug: transportConfig.debug
|
|
2996
|
+
debug: transportConfig.debug,
|
|
2997
|
+
storage
|
|
2933
2998
|
}),
|
|
2934
2999
|
oracle: createUnicityAggregatorProvider({
|
|
2935
3000
|
url: oracleConfig.url,
|