@unicitylabs/sphere-sdk 0.1.0

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.
@@ -0,0 +1,1722 @@
1
+ // impl/nodejs/storage/FileStorageProvider.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var FileStorageProvider = class {
5
+ id = "file-storage";
6
+ name = "File Storage";
7
+ type = "local";
8
+ dataDir;
9
+ filePath;
10
+ data = {};
11
+ status = "disconnected";
12
+ _identity = null;
13
+ constructor(config) {
14
+ if (typeof config === "string") {
15
+ this.dataDir = config;
16
+ this.filePath = path.join(config, "wallet.json");
17
+ } else {
18
+ this.dataDir = config.dataDir;
19
+ this.filePath = path.join(config.dataDir, config.fileName ?? "wallet.json");
20
+ }
21
+ }
22
+ setIdentity(identity) {
23
+ this._identity = identity;
24
+ }
25
+ getIdentity() {
26
+ return this._identity;
27
+ }
28
+ async connect() {
29
+ if (!fs.existsSync(this.dataDir)) {
30
+ fs.mkdirSync(this.dataDir, { recursive: true });
31
+ }
32
+ if (fs.existsSync(this.filePath)) {
33
+ try {
34
+ const content = fs.readFileSync(this.filePath, "utf-8");
35
+ this.data = JSON.parse(content);
36
+ } catch {
37
+ this.data = {};
38
+ }
39
+ }
40
+ this.status = "connected";
41
+ }
42
+ async disconnect() {
43
+ await this.save();
44
+ this.status = "disconnected";
45
+ }
46
+ isConnected() {
47
+ return this.status === "connected";
48
+ }
49
+ getStatus() {
50
+ return this.status;
51
+ }
52
+ async get(key) {
53
+ return this.data[key] ?? null;
54
+ }
55
+ async set(key, value) {
56
+ this.data[key] = value;
57
+ await this.save();
58
+ }
59
+ async remove(key) {
60
+ delete this.data[key];
61
+ await this.save();
62
+ }
63
+ async has(key) {
64
+ return key in this.data;
65
+ }
66
+ async keys(prefix) {
67
+ const allKeys = Object.keys(this.data);
68
+ if (prefix) {
69
+ return allKeys.filter((k) => k.startsWith(prefix));
70
+ }
71
+ return allKeys;
72
+ }
73
+ async clear(prefix) {
74
+ if (prefix) {
75
+ const keysToDelete = Object.keys(this.data).filter((k) => k.startsWith(prefix));
76
+ for (const key of keysToDelete) {
77
+ delete this.data[key];
78
+ }
79
+ } else {
80
+ this.data = {};
81
+ }
82
+ await this.save();
83
+ }
84
+ async save() {
85
+ if (!fs.existsSync(this.dataDir)) {
86
+ fs.mkdirSync(this.dataDir, { recursive: true });
87
+ }
88
+ fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
89
+ }
90
+ };
91
+ function createFileStorageProvider(config) {
92
+ return new FileStorageProvider(config);
93
+ }
94
+
95
+ // impl/nodejs/storage/FileTokenStorageProvider.ts
96
+ import * as fs2 from "fs";
97
+ import * as path2 from "path";
98
+ var FileTokenStorageProvider = class {
99
+ id = "file-token-storage";
100
+ name = "File Token Storage";
101
+ type = "local";
102
+ tokensDir;
103
+ status = "disconnected";
104
+ identity = null;
105
+ constructor(config) {
106
+ this.tokensDir = typeof config === "string" ? config : config.tokensDir;
107
+ }
108
+ setIdentity(identity) {
109
+ this.identity = identity;
110
+ }
111
+ async initialize() {
112
+ if (!fs2.existsSync(this.tokensDir)) {
113
+ fs2.mkdirSync(this.tokensDir, { recursive: true });
114
+ }
115
+ this.status = "connected";
116
+ return true;
117
+ }
118
+ async shutdown() {
119
+ this.status = "disconnected";
120
+ }
121
+ async connect() {
122
+ await this.initialize();
123
+ }
124
+ async disconnect() {
125
+ this.status = "disconnected";
126
+ }
127
+ isConnected() {
128
+ return this.status === "connected";
129
+ }
130
+ getStatus() {
131
+ return this.status;
132
+ }
133
+ async load() {
134
+ const data = {
135
+ _meta: {
136
+ version: 1,
137
+ address: this.identity?.address ?? "",
138
+ formatVersion: "2.0",
139
+ updatedAt: Date.now()
140
+ }
141
+ };
142
+ try {
143
+ const files = fs2.readdirSync(this.tokensDir).filter((f) => f.endsWith(".json") && f !== "_meta.json");
144
+ for (const file of files) {
145
+ try {
146
+ const content = fs2.readFileSync(path2.join(this.tokensDir, file), "utf-8");
147
+ const token = JSON.parse(content);
148
+ const key = `_${path2.basename(file, ".json")}`;
149
+ data[key] = token;
150
+ } catch {
151
+ }
152
+ }
153
+ return {
154
+ success: true,
155
+ data,
156
+ source: "local",
157
+ timestamp: Date.now()
158
+ };
159
+ } catch (error) {
160
+ return {
161
+ success: false,
162
+ error: error instanceof Error ? error.message : "Unknown error",
163
+ source: "local",
164
+ timestamp: Date.now()
165
+ };
166
+ }
167
+ }
168
+ async save(data) {
169
+ try {
170
+ fs2.writeFileSync(
171
+ path2.join(this.tokensDir, "_meta.json"),
172
+ JSON.stringify(data._meta, null, 2)
173
+ );
174
+ for (const [key, value] of Object.entries(data)) {
175
+ if (key.startsWith("_") && key !== "_meta" && key !== "_tombstones" && key !== "_outbox" && key !== "_sent" && key !== "_invalid") {
176
+ const tokenId = key.slice(1);
177
+ fs2.writeFileSync(
178
+ path2.join(this.tokensDir, `${tokenId}.json`),
179
+ JSON.stringify(value, null, 2)
180
+ );
181
+ }
182
+ }
183
+ if (data._tombstones) {
184
+ for (const tombstone of data._tombstones) {
185
+ const filePath = path2.join(this.tokensDir, `${tombstone.tokenId}.json`);
186
+ if (fs2.existsSync(filePath)) {
187
+ fs2.unlinkSync(filePath);
188
+ }
189
+ }
190
+ }
191
+ return {
192
+ success: true,
193
+ timestamp: Date.now()
194
+ };
195
+ } catch (error) {
196
+ return {
197
+ success: false,
198
+ error: error instanceof Error ? error.message : "Unknown error",
199
+ timestamp: Date.now()
200
+ };
201
+ }
202
+ }
203
+ async sync(localData) {
204
+ const saveResult = await this.save(localData);
205
+ return {
206
+ success: saveResult.success,
207
+ merged: localData,
208
+ added: 0,
209
+ removed: 0,
210
+ conflicts: 0,
211
+ error: saveResult.error
212
+ };
213
+ }
214
+ async deleteToken(tokenId) {
215
+ const filePath = path2.join(this.tokensDir, `${tokenId}.json`);
216
+ if (fs2.existsSync(filePath)) {
217
+ fs2.unlinkSync(filePath);
218
+ }
219
+ }
220
+ async saveToken(tokenId, tokenData) {
221
+ fs2.writeFileSync(
222
+ path2.join(this.tokensDir, `${tokenId}.json`),
223
+ JSON.stringify(tokenData, null, 2)
224
+ );
225
+ }
226
+ async getToken(tokenId) {
227
+ const filePath = path2.join(this.tokensDir, `${tokenId}.json`);
228
+ if (!fs2.existsSync(filePath)) {
229
+ return null;
230
+ }
231
+ try {
232
+ const content = fs2.readFileSync(filePath, "utf-8");
233
+ return JSON.parse(content);
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ async listTokenIds() {
239
+ const files = fs2.readdirSync(this.tokensDir).filter((f) => f.endsWith(".json") && f !== "_meta.json");
240
+ return files.map((f) => path2.basename(f, ".json"));
241
+ }
242
+ };
243
+ function createFileTokenStorageProvider(config) {
244
+ return new FileTokenStorageProvider(config);
245
+ }
246
+
247
+ // impl/nodejs/transport/index.ts
248
+ import WebSocket from "ws";
249
+
250
+ // transport/NostrTransportProvider.ts
251
+ import { Buffer } from "buffer";
252
+ import {
253
+ NostrKeyManager,
254
+ NIP04,
255
+ Event as NostrEventClass,
256
+ hashNametag
257
+ } from "@unicitylabs/nostr-js-sdk";
258
+
259
+ // transport/websocket.ts
260
+ var WebSocketReadyState = {
261
+ CONNECTING: 0,
262
+ OPEN: 1,
263
+ CLOSING: 2,
264
+ CLOSED: 3
265
+ };
266
+ function defaultUUIDGenerator() {
267
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
268
+ return crypto.randomUUID();
269
+ }
270
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
271
+ const r = Math.random() * 16 | 0;
272
+ const v = c === "x" ? r : r & 3 | 8;
273
+ return v.toString(16);
274
+ });
275
+ }
276
+
277
+ // constants.ts
278
+ var STORAGE_PREFIX = "sphere_";
279
+ var STORAGE_KEYS = {
280
+ /** Encrypted BIP39 mnemonic */
281
+ MNEMONIC: `${STORAGE_PREFIX}mnemonic`,
282
+ /** Encrypted master private key */
283
+ MASTER_KEY: `${STORAGE_PREFIX}master_key`,
284
+ /** BIP32 chain code */
285
+ CHAIN_CODE: `${STORAGE_PREFIX}chain_code`,
286
+ /** HD derivation path (full path like m/44'/0'/0'/0/0) */
287
+ DERIVATION_PATH: `${STORAGE_PREFIX}derivation_path`,
288
+ /** Base derivation path (like m/44'/0'/0' without chain/index) */
289
+ BASE_PATH: `${STORAGE_PREFIX}base_path`,
290
+ /** Derivation mode: bip32, wif_hmac, legacy_hmac */
291
+ DERIVATION_MODE: `${STORAGE_PREFIX}derivation_mode`,
292
+ /** Wallet source: mnemonic, file, unknown */
293
+ WALLET_SOURCE: `${STORAGE_PREFIX}wallet_source`,
294
+ /** Wallet existence flag */
295
+ WALLET_EXISTS: `${STORAGE_PREFIX}wallet_exists`,
296
+ /** Registered nametag (legacy - single address) */
297
+ NAMETAG: `${STORAGE_PREFIX}nametag`,
298
+ /** Current active address index */
299
+ CURRENT_ADDRESS_INDEX: `${STORAGE_PREFIX}current_address_index`,
300
+ /** Address nametags map (JSON: { "0": "alice", "1": "bob" }) */
301
+ ADDRESS_NAMETAGS: `${STORAGE_PREFIX}address_nametags`,
302
+ /** Token data */
303
+ TOKENS: `${STORAGE_PREFIX}tokens`,
304
+ /** Pending transfers */
305
+ PENDING_TRANSFERS: `${STORAGE_PREFIX}pending_transfers`,
306
+ /** Transfer outbox */
307
+ OUTBOX: `${STORAGE_PREFIX}outbox`,
308
+ /** Conversations */
309
+ CONVERSATIONS: `${STORAGE_PREFIX}conversations`,
310
+ /** Messages */
311
+ MESSAGES: `${STORAGE_PREFIX}messages`,
312
+ /** Transaction history */
313
+ TRANSACTION_HISTORY: `${STORAGE_PREFIX}transaction_history`,
314
+ /** Archived tokens (spent token history) */
315
+ ARCHIVED_TOKENS: `${STORAGE_PREFIX}archived_tokens`,
316
+ /** Tombstones (records of deleted/spent tokens) */
317
+ TOMBSTONES: `${STORAGE_PREFIX}tombstones`,
318
+ /** Forked tokens (alternative histories) */
319
+ FORKED_TOKENS: `${STORAGE_PREFIX}forked_tokens`
320
+ };
321
+ var DEFAULT_NOSTR_RELAYS = [
322
+ "wss://relay.unicity.network",
323
+ "wss://relay.damus.io",
324
+ "wss://nos.lol",
325
+ "wss://relay.nostr.band"
326
+ ];
327
+ var NOSTR_EVENT_KINDS = {
328
+ /** NIP-04 encrypted direct message */
329
+ DIRECT_MESSAGE: 4,
330
+ /** Token transfer (Unicity custom - 31113) */
331
+ TOKEN_TRANSFER: 31113,
332
+ /** Payment request (Unicity custom - 31115) */
333
+ PAYMENT_REQUEST: 31115,
334
+ /** Payment request response (Unicity custom - 31116) */
335
+ PAYMENT_REQUEST_RESPONSE: 31116,
336
+ /** Nametag binding (NIP-78 app-specific data) */
337
+ NAMETAG_BINDING: 30078,
338
+ /** Public broadcast */
339
+ BROADCAST: 1
340
+ };
341
+ var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
342
+ var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
343
+ var TEST_AGGREGATOR_URL = "https://goggregator-test.unicity.network";
344
+ var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
345
+ var DEFAULT_IPFS_GATEWAYS = [
346
+ "https://ipfs.unicity.network",
347
+ "https://dweb.link",
348
+ "https://ipfs.io"
349
+ ];
350
+ var DEFAULT_BASE_PATH = "m/44'/0'/0'";
351
+ var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
352
+ var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
353
+ var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
354
+ var TEST_NOSTR_RELAYS = [
355
+ "wss://nostr-relay.testnet.unicity.network"
356
+ ];
357
+ var NETWORKS = {
358
+ mainnet: {
359
+ name: "Mainnet",
360
+ aggregatorUrl: DEFAULT_AGGREGATOR_URL,
361
+ nostrRelays: DEFAULT_NOSTR_RELAYS,
362
+ ipfsGateways: DEFAULT_IPFS_GATEWAYS,
363
+ electrumUrl: DEFAULT_ELECTRUM_URL
364
+ },
365
+ testnet: {
366
+ name: "Testnet",
367
+ aggregatorUrl: TEST_AGGREGATOR_URL,
368
+ nostrRelays: TEST_NOSTR_RELAYS,
369
+ ipfsGateways: DEFAULT_IPFS_GATEWAYS,
370
+ electrumUrl: TEST_ELECTRUM_URL
371
+ },
372
+ dev: {
373
+ name: "Development",
374
+ aggregatorUrl: DEV_AGGREGATOR_URL,
375
+ nostrRelays: TEST_NOSTR_RELAYS,
376
+ ipfsGateways: DEFAULT_IPFS_GATEWAYS,
377
+ electrumUrl: TEST_ELECTRUM_URL
378
+ }
379
+ };
380
+ var TIMEOUTS = {
381
+ /** WebSocket connection timeout */
382
+ WEBSOCKET_CONNECT: 1e4,
383
+ /** Nostr relay reconnect delay */
384
+ NOSTR_RECONNECT_DELAY: 3e3,
385
+ /** Max reconnect attempts */
386
+ MAX_RECONNECT_ATTEMPTS: 5,
387
+ /** Proof polling interval */
388
+ PROOF_POLL_INTERVAL: 1e3,
389
+ /** Sync interval */
390
+ SYNC_INTERVAL: 6e4
391
+ };
392
+
393
+ // transport/NostrTransportProvider.ts
394
+ var EVENT_KINDS = NOSTR_EVENT_KINDS;
395
+ var NostrTransportProvider = class {
396
+ id = "nostr";
397
+ name = "Nostr Transport";
398
+ type = "p2p";
399
+ description = "P2P messaging via Nostr protocol";
400
+ config;
401
+ identity = null;
402
+ keyManager = null;
403
+ status = "disconnected";
404
+ // WebSocket connections to relays
405
+ connections = /* @__PURE__ */ new Map();
406
+ reconnectAttempts = /* @__PURE__ */ new Map();
407
+ // Event handlers
408
+ messageHandlers = /* @__PURE__ */ new Set();
409
+ transferHandlers = /* @__PURE__ */ new Set();
410
+ paymentRequestHandlers = /* @__PURE__ */ new Set();
411
+ paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
412
+ broadcastHandlers = /* @__PURE__ */ new Map();
413
+ eventCallbacks = /* @__PURE__ */ new Set();
414
+ // Subscriptions
415
+ subscriptions = /* @__PURE__ */ new Map();
416
+ // subId -> relays
417
+ constructor(config) {
418
+ this.config = {
419
+ relays: config.relays ?? [...DEFAULT_NOSTR_RELAYS],
420
+ timeout: config.timeout ?? TIMEOUTS.WEBSOCKET_CONNECT,
421
+ autoReconnect: config.autoReconnect ?? true,
422
+ reconnectDelay: config.reconnectDelay ?? TIMEOUTS.NOSTR_RECONNECT_DELAY,
423
+ maxReconnectAttempts: config.maxReconnectAttempts ?? TIMEOUTS.MAX_RECONNECT_ATTEMPTS,
424
+ debug: config.debug ?? false,
425
+ createWebSocket: config.createWebSocket,
426
+ generateUUID: config.generateUUID ?? defaultUUIDGenerator
427
+ };
428
+ }
429
+ // ===========================================================================
430
+ // BaseProvider Implementation
431
+ // ===========================================================================
432
+ async connect() {
433
+ if (this.status === "connected") return;
434
+ this.status = "connecting";
435
+ try {
436
+ const connectPromises = this.config.relays.map(
437
+ (relay) => this.connectToRelay(relay)
438
+ );
439
+ await Promise.allSettled(connectPromises);
440
+ if (this.connections.size === 0) {
441
+ throw new Error("Failed to connect to any relay");
442
+ }
443
+ this.status = "connected";
444
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
445
+ this.log("Connected to", this.connections.size, "relays");
446
+ if (this.identity) {
447
+ this.subscribeToEvents();
448
+ }
449
+ } catch (error) {
450
+ this.status = "error";
451
+ throw error;
452
+ }
453
+ }
454
+ async disconnect() {
455
+ for (const [url, ws] of this.connections) {
456
+ ws.close();
457
+ this.connections.delete(url);
458
+ }
459
+ this.subscriptions.clear();
460
+ this.status = "disconnected";
461
+ this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
462
+ this.log("Disconnected from all relays");
463
+ }
464
+ isConnected() {
465
+ return this.status === "connected" && this.connections.size > 0;
466
+ }
467
+ getStatus() {
468
+ return this.status;
469
+ }
470
+ // ===========================================================================
471
+ // Dynamic Relay Management
472
+ // ===========================================================================
473
+ /**
474
+ * Get list of configured relay URLs
475
+ */
476
+ getRelays() {
477
+ return [...this.config.relays];
478
+ }
479
+ /**
480
+ * Get list of currently connected relay URLs
481
+ */
482
+ getConnectedRelays() {
483
+ return Array.from(this.connections.keys());
484
+ }
485
+ /**
486
+ * Add a new relay dynamically
487
+ * Will connect immediately if provider is already connected
488
+ */
489
+ async addRelay(relayUrl) {
490
+ if (this.config.relays.includes(relayUrl)) {
491
+ this.log("Relay already configured:", relayUrl);
492
+ return false;
493
+ }
494
+ this.config.relays.push(relayUrl);
495
+ if (this.status === "connected") {
496
+ try {
497
+ await this.connectToRelay(relayUrl);
498
+ this.log("Added and connected to relay:", relayUrl);
499
+ this.emitEvent({
500
+ type: "transport:relay_added",
501
+ timestamp: Date.now(),
502
+ data: { relay: relayUrl, connected: true }
503
+ });
504
+ return true;
505
+ } catch (error) {
506
+ this.log("Failed to connect to new relay:", relayUrl, error);
507
+ this.emitEvent({
508
+ type: "transport:relay_added",
509
+ timestamp: Date.now(),
510
+ data: { relay: relayUrl, connected: false, error: String(error) }
511
+ });
512
+ return false;
513
+ }
514
+ }
515
+ this.emitEvent({
516
+ type: "transport:relay_added",
517
+ timestamp: Date.now(),
518
+ data: { relay: relayUrl, connected: false }
519
+ });
520
+ return true;
521
+ }
522
+ /**
523
+ * Remove a relay dynamically
524
+ * Will disconnect from the relay if connected
525
+ */
526
+ async removeRelay(relayUrl) {
527
+ const index = this.config.relays.indexOf(relayUrl);
528
+ if (index === -1) {
529
+ this.log("Relay not found:", relayUrl);
530
+ return false;
531
+ }
532
+ this.config.relays.splice(index, 1);
533
+ const ws = this.connections.get(relayUrl);
534
+ if (ws) {
535
+ ws.close();
536
+ this.connections.delete(relayUrl);
537
+ this.reconnectAttempts.delete(relayUrl);
538
+ this.log("Removed and disconnected from relay:", relayUrl);
539
+ }
540
+ this.emitEvent({
541
+ type: "transport:relay_removed",
542
+ timestamp: Date.now(),
543
+ data: { relay: relayUrl }
544
+ });
545
+ if (this.connections.size === 0 && this.status === "connected") {
546
+ this.status = "error";
547
+ this.emitEvent({
548
+ type: "transport:error",
549
+ timestamp: Date.now(),
550
+ data: { error: "No connected relays remaining" }
551
+ });
552
+ }
553
+ return true;
554
+ }
555
+ /**
556
+ * Check if a relay is configured
557
+ */
558
+ hasRelay(relayUrl) {
559
+ return this.config.relays.includes(relayUrl);
560
+ }
561
+ /**
562
+ * Check if a relay is currently connected
563
+ */
564
+ isRelayConnected(relayUrl) {
565
+ const ws = this.connections.get(relayUrl);
566
+ return ws !== void 0 && ws.readyState === WebSocketReadyState.OPEN;
567
+ }
568
+ // ===========================================================================
569
+ // TransportProvider Implementation
570
+ // ===========================================================================
571
+ setIdentity(identity) {
572
+ this.identity = identity;
573
+ const secretKey = Buffer.from(identity.privateKey, "hex");
574
+ this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
575
+ const nostrPubkey = this.keyManager.getPublicKeyHex();
576
+ this.log("Identity set, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
577
+ if (this.isConnected()) {
578
+ this.subscribeToEvents();
579
+ }
580
+ }
581
+ /**
582
+ * Get the Nostr-format public key (32 bytes / 64 hex chars)
583
+ * This is the x-coordinate only, without the 02/03 prefix.
584
+ */
585
+ getNostrPubkey() {
586
+ if (!this.keyManager) {
587
+ throw new Error("KeyManager not initialized - call setIdentity first");
588
+ }
589
+ return this.keyManager.getPublicKeyHex();
590
+ }
591
+ async sendMessage(recipientPubkey, content) {
592
+ this.ensureReady();
593
+ const event = await this.createEncryptedEvent(
594
+ EVENT_KINDS.DIRECT_MESSAGE,
595
+ content,
596
+ [["p", recipientPubkey]]
597
+ );
598
+ await this.publishEvent(event);
599
+ this.emitEvent({
600
+ type: "message:sent",
601
+ timestamp: Date.now(),
602
+ data: { recipient: recipientPubkey }
603
+ });
604
+ return event.id;
605
+ }
606
+ onMessage(handler) {
607
+ this.messageHandlers.add(handler);
608
+ return () => this.messageHandlers.delete(handler);
609
+ }
610
+ async sendTokenTransfer(recipientPubkey, payload) {
611
+ this.ensureReady();
612
+ const content = "token_transfer:" + JSON.stringify(payload);
613
+ const event = await this.createEncryptedEvent(
614
+ EVENT_KINDS.TOKEN_TRANSFER,
615
+ content,
616
+ [
617
+ ["p", recipientPubkey],
618
+ ["d", "token-transfer"],
619
+ ["type", "token_transfer"]
620
+ ]
621
+ );
622
+ await this.publishEvent(event);
623
+ this.emitEvent({
624
+ type: "transfer:sent",
625
+ timestamp: Date.now(),
626
+ data: { recipient: recipientPubkey }
627
+ });
628
+ return event.id;
629
+ }
630
+ onTokenTransfer(handler) {
631
+ this.transferHandlers.add(handler);
632
+ return () => this.transferHandlers.delete(handler);
633
+ }
634
+ async sendPaymentRequest(recipientPubkey, payload) {
635
+ this.ensureReady();
636
+ const requestId = this.config.generateUUID();
637
+ const amount = typeof payload.amount === "bigint" ? payload.amount.toString() : payload.amount;
638
+ const requestContent = {
639
+ requestId,
640
+ amount,
641
+ coinId: payload.coinId,
642
+ message: payload.message,
643
+ recipientNametag: payload.recipientNametag,
644
+ deadline: Date.now() + 5 * 60 * 1e3
645
+ // 5 minutes default
646
+ };
647
+ const content = "payment_request:" + JSON.stringify(requestContent);
648
+ const tags = [
649
+ ["p", recipientPubkey],
650
+ ["type", "payment_request"],
651
+ ["amount", amount]
652
+ ];
653
+ if (payload.recipientNametag) {
654
+ tags.push(["recipient", payload.recipientNametag]);
655
+ }
656
+ const event = await this.createEncryptedEvent(
657
+ EVENT_KINDS.PAYMENT_REQUEST,
658
+ content,
659
+ tags
660
+ );
661
+ await this.publishEvent(event);
662
+ this.log("Sent payment request:", event.id);
663
+ return event.id;
664
+ }
665
+ onPaymentRequest(handler) {
666
+ this.paymentRequestHandlers.add(handler);
667
+ return () => this.paymentRequestHandlers.delete(handler);
668
+ }
669
+ async sendPaymentRequestResponse(recipientPubkey, payload) {
670
+ this.ensureReady();
671
+ const responseContent = {
672
+ requestId: payload.requestId,
673
+ responseType: payload.responseType,
674
+ message: payload.message,
675
+ transferId: payload.transferId
676
+ };
677
+ const content = "payment_response:" + JSON.stringify(responseContent);
678
+ const event = await this.createEncryptedEvent(
679
+ EVENT_KINDS.PAYMENT_REQUEST_RESPONSE,
680
+ content,
681
+ [
682
+ ["p", recipientPubkey],
683
+ ["e", payload.requestId],
684
+ // Reference to original request
685
+ ["d", "payment-request-response"],
686
+ ["type", "payment_response"]
687
+ ]
688
+ );
689
+ await this.publishEvent(event);
690
+ this.log("Sent payment request response:", event.id, "type:", payload.responseType);
691
+ return event.id;
692
+ }
693
+ onPaymentRequestResponse(handler) {
694
+ this.paymentRequestResponseHandlers.add(handler);
695
+ return () => this.paymentRequestResponseHandlers.delete(handler);
696
+ }
697
+ async resolveNametag(nametag) {
698
+ this.ensureReady();
699
+ const hashedNametag = hashNametag(nametag);
700
+ let events = await this.queryEvents({
701
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
702
+ "#t": [hashedNametag],
703
+ limit: 1
704
+ });
705
+ if (events.length === 0) {
706
+ events = await this.queryEvents({
707
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
708
+ "#d": [hashedNametag],
709
+ limit: 1
710
+ });
711
+ }
712
+ if (events.length === 0) return null;
713
+ const bindingEvent = events[0];
714
+ if (bindingEvent.pubkey) {
715
+ return bindingEvent.pubkey;
716
+ }
717
+ const pubkeyTag = bindingEvent.tags.find((t) => t[0] === "p");
718
+ if (pubkeyTag?.[1]) return pubkeyTag[1];
719
+ return null;
720
+ }
721
+ async publishNametag(nametag, address) {
722
+ this.ensureReady();
723
+ const hashedNametag = hashNametag(nametag);
724
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, address, [
725
+ ["d", hashedNametag],
726
+ ["a", address]
727
+ ]);
728
+ await this.publishEvent(event);
729
+ this.log("Published nametag binding:", nametag);
730
+ }
731
+ async registerNametag(nametag, _publicKey) {
732
+ this.ensureReady();
733
+ const nostrPubkey = this.getNostrPubkey();
734
+ const existing = await this.resolveNametag(nametag);
735
+ this.log("registerNametag:", nametag, "existing:", existing, "myPubkey:", nostrPubkey);
736
+ if (existing && existing !== nostrPubkey) {
737
+ this.log("Nametag already taken:", nametag, "- owner:", existing);
738
+ return false;
739
+ }
740
+ const hashedNametag = hashNametag(nametag);
741
+ const content = JSON.stringify({
742
+ nametag_hash: hashedNametag,
743
+ address: nostrPubkey,
744
+ verified: Date.now()
745
+ });
746
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, [
747
+ ["d", hashedNametag],
748
+ ["nametag", hashedNametag],
749
+ ["t", hashedNametag],
750
+ ["address", nostrPubkey]
751
+ ]);
752
+ await this.publishEvent(event);
753
+ this.log("Registered nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...");
754
+ return true;
755
+ }
756
+ subscribeToBroadcast(tags, handler) {
757
+ const key = tags.sort().join(":");
758
+ if (!this.broadcastHandlers.has(key)) {
759
+ this.broadcastHandlers.set(key, /* @__PURE__ */ new Set());
760
+ if (this.isConnected()) {
761
+ this.subscribeToTags(tags);
762
+ }
763
+ }
764
+ this.broadcastHandlers.get(key).add(handler);
765
+ return () => {
766
+ this.broadcastHandlers.get(key)?.delete(handler);
767
+ if (this.broadcastHandlers.get(key)?.size === 0) {
768
+ this.broadcastHandlers.delete(key);
769
+ }
770
+ };
771
+ }
772
+ async publishBroadcast(content, tags) {
773
+ this.ensureReady();
774
+ const eventTags = tags?.map((t) => ["t", t]) ?? [];
775
+ const event = await this.createEvent(EVENT_KINDS.BROADCAST, content, eventTags);
776
+ await this.publishEvent(event);
777
+ return event.id;
778
+ }
779
+ // ===========================================================================
780
+ // Event Subscription
781
+ // ===========================================================================
782
+ onEvent(callback) {
783
+ this.eventCallbacks.add(callback);
784
+ return () => this.eventCallbacks.delete(callback);
785
+ }
786
+ // ===========================================================================
787
+ // Private: Connection Management
788
+ // ===========================================================================
789
+ async connectToRelay(url) {
790
+ return new Promise((resolve, reject) => {
791
+ const ws = this.config.createWebSocket(url);
792
+ const timeout = setTimeout(() => {
793
+ ws.close();
794
+ reject(new Error(`Connection timeout: ${url}`));
795
+ }, this.config.timeout);
796
+ ws.onopen = () => {
797
+ clearTimeout(timeout);
798
+ this.connections.set(url, ws);
799
+ this.reconnectAttempts.set(url, 0);
800
+ this.log("Connected to relay:", url);
801
+ resolve();
802
+ };
803
+ ws.onerror = (error) => {
804
+ clearTimeout(timeout);
805
+ this.log("Relay error:", url, error);
806
+ reject(error);
807
+ };
808
+ ws.onclose = () => {
809
+ this.connections.delete(url);
810
+ if (this.config.autoReconnect && this.status === "connected") {
811
+ this.scheduleReconnect(url);
812
+ }
813
+ };
814
+ ws.onmessage = (event) => {
815
+ this.handleRelayMessage(url, event.data);
816
+ };
817
+ });
818
+ }
819
+ scheduleReconnect(url) {
820
+ const attempts = this.reconnectAttempts.get(url) ?? 0;
821
+ if (attempts >= this.config.maxReconnectAttempts) {
822
+ this.log("Max reconnect attempts reached for:", url);
823
+ return;
824
+ }
825
+ this.reconnectAttempts.set(url, attempts + 1);
826
+ const delay = this.config.reconnectDelay * Math.pow(2, attempts);
827
+ this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
828
+ setTimeout(() => {
829
+ this.connectToRelay(url).catch(() => {
830
+ });
831
+ }, delay);
832
+ }
833
+ // ===========================================================================
834
+ // Private: Message Handling
835
+ // ===========================================================================
836
+ handleRelayMessage(relay, data) {
837
+ try {
838
+ const message = JSON.parse(data);
839
+ const [type, ...args] = message;
840
+ switch (type) {
841
+ case "EVENT":
842
+ this.handleEvent(args[1]);
843
+ break;
844
+ case "EOSE":
845
+ break;
846
+ case "OK":
847
+ break;
848
+ case "NOTICE":
849
+ this.log("Relay notice:", relay, args[0]);
850
+ break;
851
+ }
852
+ } catch (error) {
853
+ this.log("Failed to parse relay message:", error);
854
+ }
855
+ }
856
+ async handleEvent(event) {
857
+ try {
858
+ switch (event.kind) {
859
+ case EVENT_KINDS.DIRECT_MESSAGE:
860
+ await this.handleDirectMessage(event);
861
+ break;
862
+ case EVENT_KINDS.TOKEN_TRANSFER:
863
+ await this.handleTokenTransfer(event);
864
+ break;
865
+ case EVENT_KINDS.PAYMENT_REQUEST:
866
+ await this.handlePaymentRequest(event);
867
+ break;
868
+ case EVENT_KINDS.PAYMENT_REQUEST_RESPONSE:
869
+ await this.handlePaymentRequestResponse(event);
870
+ break;
871
+ case EVENT_KINDS.BROADCAST:
872
+ this.handleBroadcast(event);
873
+ break;
874
+ }
875
+ } catch (error) {
876
+ this.log("Failed to handle event:", error);
877
+ }
878
+ }
879
+ async handleDirectMessage(event) {
880
+ if (!this.identity || !this.keyManager) return;
881
+ if (event.pubkey === this.keyManager.getPublicKeyHex()) return;
882
+ const content = await this.decryptContent(event.content, event.pubkey);
883
+ const message = {
884
+ id: event.id,
885
+ senderPubkey: event.pubkey,
886
+ content,
887
+ timestamp: event.created_at * 1e3,
888
+ encrypted: true
889
+ };
890
+ this.emitEvent({ type: "message:received", timestamp: Date.now() });
891
+ for (const handler of this.messageHandlers) {
892
+ try {
893
+ handler(message);
894
+ } catch (error) {
895
+ this.log("Message handler error:", error);
896
+ }
897
+ }
898
+ }
899
+ async handleTokenTransfer(event) {
900
+ if (!this.identity) return;
901
+ const content = await this.decryptContent(event.content, event.pubkey);
902
+ const payload = JSON.parse(content);
903
+ const transfer = {
904
+ id: event.id,
905
+ senderPubkey: event.pubkey,
906
+ payload,
907
+ timestamp: event.created_at * 1e3
908
+ };
909
+ this.emitEvent({ type: "transfer:received", timestamp: Date.now() });
910
+ for (const handler of this.transferHandlers) {
911
+ try {
912
+ handler(transfer);
913
+ } catch (error) {
914
+ this.log("Transfer handler error:", error);
915
+ }
916
+ }
917
+ }
918
+ async handlePaymentRequest(event) {
919
+ if (!this.identity) return;
920
+ try {
921
+ const content = await this.decryptContent(event.content, event.pubkey);
922
+ const requestData = JSON.parse(content);
923
+ const request = {
924
+ id: event.id,
925
+ senderPubkey: event.pubkey,
926
+ request: {
927
+ requestId: requestData.requestId,
928
+ amount: requestData.amount,
929
+ coinId: requestData.coinId,
930
+ message: requestData.message,
931
+ recipientNametag: requestData.recipientNametag,
932
+ metadata: requestData.metadata
933
+ },
934
+ timestamp: event.created_at * 1e3
935
+ };
936
+ this.log("Received payment request:", request.id);
937
+ for (const handler of this.paymentRequestHandlers) {
938
+ try {
939
+ handler(request);
940
+ } catch (error) {
941
+ this.log("Payment request handler error:", error);
942
+ }
943
+ }
944
+ } catch (error) {
945
+ this.log("Failed to handle payment request:", error);
946
+ }
947
+ }
948
+ async handlePaymentRequestResponse(event) {
949
+ if (!this.identity) return;
950
+ try {
951
+ const content = await this.decryptContent(event.content, event.pubkey);
952
+ const responseData = JSON.parse(content);
953
+ const response = {
954
+ id: event.id,
955
+ responderPubkey: event.pubkey,
956
+ response: {
957
+ requestId: responseData.requestId,
958
+ responseType: responseData.responseType,
959
+ message: responseData.message,
960
+ transferId: responseData.transferId
961
+ },
962
+ timestamp: event.created_at * 1e3
963
+ };
964
+ this.log("Received payment request response:", response.id, "type:", responseData.responseType);
965
+ for (const handler of this.paymentRequestResponseHandlers) {
966
+ try {
967
+ handler(response);
968
+ } catch (error) {
969
+ this.log("Payment request response handler error:", error);
970
+ }
971
+ }
972
+ } catch (error) {
973
+ this.log("Failed to handle payment request response:", error);
974
+ }
975
+ }
976
+ handleBroadcast(event) {
977
+ const tags = event.tags.filter((t) => t[0] === "t").map((t) => t[1]);
978
+ const broadcast = {
979
+ id: event.id,
980
+ authorPubkey: event.pubkey,
981
+ content: event.content,
982
+ tags,
983
+ timestamp: event.created_at * 1e3
984
+ };
985
+ for (const [key, handlers] of this.broadcastHandlers) {
986
+ const subscribedTags = key.split(":");
987
+ if (tags.some((t) => subscribedTags.includes(t))) {
988
+ for (const handler of handlers) {
989
+ try {
990
+ handler(broadcast);
991
+ } catch (error) {
992
+ this.log("Broadcast handler error:", error);
993
+ }
994
+ }
995
+ }
996
+ }
997
+ }
998
+ // ===========================================================================
999
+ // Private: Event Creation & Publishing
1000
+ // ===========================================================================
1001
+ async createEvent(kind, content, tags) {
1002
+ if (!this.identity) throw new Error("Identity not set");
1003
+ if (!this.keyManager) throw new Error("KeyManager not initialized");
1004
+ const signedEvent = NostrEventClass.create(this.keyManager, {
1005
+ kind,
1006
+ content,
1007
+ tags
1008
+ });
1009
+ const event = {
1010
+ id: signedEvent.id,
1011
+ kind: signedEvent.kind,
1012
+ content: signedEvent.content,
1013
+ tags: signedEvent.tags,
1014
+ pubkey: signedEvent.pubkey,
1015
+ created_at: signedEvent.created_at,
1016
+ sig: signedEvent.sig
1017
+ };
1018
+ return event;
1019
+ }
1020
+ async createEncryptedEvent(kind, content, tags) {
1021
+ if (!this.keyManager) throw new Error("KeyManager not initialized");
1022
+ const recipientTag = tags.find((t) => t[0] === "p");
1023
+ if (!recipientTag || !recipientTag[1]) {
1024
+ throw new Error("No recipient pubkey in tags for encryption");
1025
+ }
1026
+ const recipientPubkey = recipientTag[1];
1027
+ const encrypted = await NIP04.encryptHex(
1028
+ content,
1029
+ this.keyManager.getPrivateKeyHex(),
1030
+ recipientPubkey
1031
+ );
1032
+ return this.createEvent(kind, encrypted, tags);
1033
+ }
1034
+ async publishEvent(event) {
1035
+ const message = JSON.stringify(["EVENT", event]);
1036
+ const publishPromises = Array.from(this.connections.values()).map((ws) => {
1037
+ return new Promise((resolve, reject) => {
1038
+ if (ws.readyState !== WebSocketReadyState.OPEN) {
1039
+ reject(new Error("WebSocket not open"));
1040
+ return;
1041
+ }
1042
+ ws.send(message);
1043
+ resolve();
1044
+ });
1045
+ });
1046
+ await Promise.any(publishPromises);
1047
+ }
1048
+ async queryEvents(filter) {
1049
+ if (this.connections.size === 0) {
1050
+ throw new Error("No connected relays");
1051
+ }
1052
+ const queryPromises = Array.from(this.connections.values()).map(
1053
+ (ws) => this.queryEventsFromRelay(ws, filter)
1054
+ );
1055
+ const results = await Promise.allSettled(queryPromises);
1056
+ for (const result of results) {
1057
+ if (result.status === "fulfilled" && result.value.length > 0) {
1058
+ return result.value;
1059
+ }
1060
+ }
1061
+ return [];
1062
+ }
1063
+ async queryEventsFromRelay(ws, filter) {
1064
+ const subId = this.config.generateUUID().slice(0, 8);
1065
+ const events = [];
1066
+ return new Promise((resolve) => {
1067
+ const timeout = setTimeout(() => {
1068
+ this.unsubscribeFromRelay(ws, subId);
1069
+ resolve(events);
1070
+ }, 5e3);
1071
+ const originalHandler = ws.onmessage;
1072
+ ws.onmessage = (event) => {
1073
+ const message = JSON.parse(event.data);
1074
+ const [type, sid, data] = message;
1075
+ if (sid !== subId) {
1076
+ originalHandler?.call(ws, event);
1077
+ return;
1078
+ }
1079
+ if (type === "EVENT") {
1080
+ events.push(data);
1081
+ } else if (type === "EOSE") {
1082
+ clearTimeout(timeout);
1083
+ ws.onmessage = originalHandler;
1084
+ this.unsubscribeFromRelay(ws, subId);
1085
+ resolve(events);
1086
+ }
1087
+ };
1088
+ ws.send(JSON.stringify(["REQ", subId, filter]));
1089
+ });
1090
+ }
1091
+ unsubscribeFromRelay(ws, subId) {
1092
+ if (ws.readyState === WebSocketReadyState.OPEN) {
1093
+ ws.send(JSON.stringify(["CLOSE", subId]));
1094
+ }
1095
+ }
1096
+ // ===========================================================================
1097
+ // Private: Subscriptions
1098
+ // ===========================================================================
1099
+ subscribeToEvents() {
1100
+ if (!this.identity || !this.keyManager) return;
1101
+ const subId = "main";
1102
+ const nostrPubkey = this.keyManager.getPublicKeyHex();
1103
+ const filter = {
1104
+ kinds: [
1105
+ EVENT_KINDS.DIRECT_MESSAGE,
1106
+ EVENT_KINDS.TOKEN_TRANSFER,
1107
+ EVENT_KINDS.PAYMENT_REQUEST,
1108
+ EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
1109
+ ],
1110
+ "#p": [nostrPubkey],
1111
+ since: Math.floor(Date.now() / 1e3) - 86400
1112
+ // Last 24h
1113
+ };
1114
+ const message = JSON.stringify(["REQ", subId, filter]);
1115
+ for (const ws of this.connections.values()) {
1116
+ if (ws.readyState === WebSocketReadyState.OPEN) {
1117
+ ws.send(message);
1118
+ }
1119
+ }
1120
+ this.subscriptions.set(subId, Array.from(this.connections.keys()));
1121
+ this.log("Subscribed to events");
1122
+ }
1123
+ subscribeToTags(tags) {
1124
+ const subId = `tags:${tags.join(":")}`;
1125
+ const filter = {
1126
+ kinds: [EVENT_KINDS.BROADCAST],
1127
+ "#t": tags,
1128
+ since: Math.floor(Date.now() / 1e3) - 3600
1129
+ // Last hour
1130
+ };
1131
+ const message = JSON.stringify(["REQ", subId, filter]);
1132
+ for (const ws of this.connections.values()) {
1133
+ if (ws.readyState === WebSocketReadyState.OPEN) {
1134
+ ws.send(message);
1135
+ }
1136
+ }
1137
+ this.subscriptions.set(subId, Array.from(this.connections.keys()));
1138
+ }
1139
+ // ===========================================================================
1140
+ // Private: Encryption
1141
+ // ===========================================================================
1142
+ async decryptContent(content, senderPubkey) {
1143
+ if (!this.keyManager) throw new Error("KeyManager not initialized");
1144
+ const decrypted = await NIP04.decryptHex(
1145
+ content,
1146
+ this.keyManager.getPrivateKeyHex(),
1147
+ senderPubkey
1148
+ );
1149
+ return this.stripContentPrefix(decrypted);
1150
+ }
1151
+ /**
1152
+ * Strip known content prefixes (nostr-js-sdk compatibility)
1153
+ * Handles: payment_request:, token_transfer:, etc.
1154
+ */
1155
+ stripContentPrefix(content) {
1156
+ const prefixes = [
1157
+ "payment_request:",
1158
+ "token_transfer:",
1159
+ "payment_response:"
1160
+ ];
1161
+ for (const prefix of prefixes) {
1162
+ if (content.startsWith(prefix)) {
1163
+ return content.slice(prefix.length);
1164
+ }
1165
+ }
1166
+ return content;
1167
+ }
1168
+ // ===========================================================================
1169
+ // Private: Helpers
1170
+ // ===========================================================================
1171
+ ensureReady() {
1172
+ if (!this.isConnected()) {
1173
+ throw new Error("NostrTransportProvider not connected");
1174
+ }
1175
+ if (!this.identity) {
1176
+ throw new Error("Identity not set");
1177
+ }
1178
+ }
1179
+ emitEvent(event) {
1180
+ for (const callback of this.eventCallbacks) {
1181
+ try {
1182
+ callback(event);
1183
+ } catch (error) {
1184
+ this.log("Event callback error:", error);
1185
+ }
1186
+ }
1187
+ }
1188
+ log(...args) {
1189
+ if (this.config.debug) {
1190
+ console.log("[NostrTransportProvider]", ...args);
1191
+ }
1192
+ }
1193
+ };
1194
+
1195
+ // impl/nodejs/transport/index.ts
1196
+ function createNodeWebSocketFactory() {
1197
+ return (url) => {
1198
+ return new WebSocket(url);
1199
+ };
1200
+ }
1201
+ function createNostrTransportProvider(config) {
1202
+ return new NostrTransportProvider({
1203
+ ...config,
1204
+ createWebSocket: createNodeWebSocketFactory()
1205
+ });
1206
+ }
1207
+
1208
+ // impl/nodejs/oracle/index.ts
1209
+ import * as fs3 from "fs";
1210
+
1211
+ // oracle/UnicityAggregatorProvider.ts
1212
+ import { StateTransitionClient } from "@unicitylabs/state-transition-sdk/lib/StateTransitionClient";
1213
+ import { AggregatorClient } from "@unicitylabs/state-transition-sdk/lib/api/AggregatorClient";
1214
+ import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/bft/RootTrustBase";
1215
+ import { Token as SdkToken } from "@unicitylabs/state-transition-sdk/lib/token/Token";
1216
+ import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
1217
+ var UnicityAggregatorProvider = class {
1218
+ id = "unicity-aggregator";
1219
+ name = "Unicity Aggregator";
1220
+ type = "network";
1221
+ description = "Unicity state transition aggregator (oracle implementation)";
1222
+ config;
1223
+ status = "disconnected";
1224
+ eventCallbacks = /* @__PURE__ */ new Set();
1225
+ // SDK clients
1226
+ aggregatorClient = null;
1227
+ stateTransitionClient = null;
1228
+ trustBase = null;
1229
+ /** Get the current trust base */
1230
+ getTrustBase() {
1231
+ return this.trustBase;
1232
+ }
1233
+ /** Get the state transition client */
1234
+ getStateTransitionClient() {
1235
+ return this.stateTransitionClient;
1236
+ }
1237
+ /** Get the aggregator client */
1238
+ getAggregatorClient() {
1239
+ return this.aggregatorClient;
1240
+ }
1241
+ // Cache for spent states (immutable)
1242
+ spentCache = /* @__PURE__ */ new Map();
1243
+ constructor(config) {
1244
+ this.config = {
1245
+ url: config.url,
1246
+ apiKey: config.apiKey ?? "",
1247
+ timeout: config.timeout ?? DEFAULT_AGGREGATOR_TIMEOUT,
1248
+ skipVerification: config.skipVerification ?? false,
1249
+ debug: config.debug ?? false,
1250
+ trustBaseLoader: config.trustBaseLoader
1251
+ };
1252
+ }
1253
+ // ===========================================================================
1254
+ // BaseProvider Implementation
1255
+ // ===========================================================================
1256
+ async connect() {
1257
+ if (this.status === "connected") return;
1258
+ this.status = "connecting";
1259
+ this.status = "connected";
1260
+ this.emitEvent({ type: "oracle:connected", timestamp: Date.now() });
1261
+ this.log("Connected to oracle:", this.config.url);
1262
+ }
1263
+ async disconnect() {
1264
+ this.status = "disconnected";
1265
+ this.emitEvent({ type: "oracle:disconnected", timestamp: Date.now() });
1266
+ this.log("Disconnected from oracle");
1267
+ }
1268
+ isConnected() {
1269
+ return this.status === "connected";
1270
+ }
1271
+ getStatus() {
1272
+ return this.status;
1273
+ }
1274
+ // ===========================================================================
1275
+ // OracleProvider Implementation
1276
+ // ===========================================================================
1277
+ async initialize(trustBase) {
1278
+ this.aggregatorClient = new AggregatorClient(
1279
+ this.config.url,
1280
+ this.config.apiKey || null
1281
+ );
1282
+ this.stateTransitionClient = new StateTransitionClient(this.aggregatorClient);
1283
+ if (trustBase) {
1284
+ this.trustBase = trustBase;
1285
+ } else if (!this.config.skipVerification && this.config.trustBaseLoader) {
1286
+ try {
1287
+ const trustBaseJson = await this.config.trustBaseLoader.load();
1288
+ if (trustBaseJson) {
1289
+ this.trustBase = RootTrustBase.fromJSON(trustBaseJson);
1290
+ }
1291
+ } catch (error) {
1292
+ this.log("Failed to load trust base:", error);
1293
+ }
1294
+ }
1295
+ await this.connect();
1296
+ this.log("Initialized with trust base:", !!this.trustBase);
1297
+ }
1298
+ /**
1299
+ * Submit a transfer commitment to the aggregator.
1300
+ * Accepts either an SDK TransferCommitment or a simple commitment object.
1301
+ */
1302
+ async submitCommitment(commitment) {
1303
+ this.ensureConnected();
1304
+ try {
1305
+ let requestId;
1306
+ if (this.isSdkTransferCommitment(commitment)) {
1307
+ const response = await this.stateTransitionClient.submitTransferCommitment(commitment);
1308
+ requestId = commitment.requestId?.toString() ?? response.status;
1309
+ } else {
1310
+ const response = await this.rpcCall("submitCommitment", {
1311
+ sourceToken: commitment.sourceToken,
1312
+ recipient: commitment.recipient,
1313
+ salt: Array.from(commitment.salt),
1314
+ data: commitment.data
1315
+ });
1316
+ requestId = response.requestId ?? "";
1317
+ }
1318
+ this.emitEvent({
1319
+ type: "commitment:submitted",
1320
+ timestamp: Date.now(),
1321
+ data: { requestId }
1322
+ });
1323
+ return {
1324
+ success: true,
1325
+ requestId,
1326
+ timestamp: Date.now()
1327
+ };
1328
+ } catch (error) {
1329
+ const errorMsg = error instanceof Error ? error.message : String(error);
1330
+ return {
1331
+ success: false,
1332
+ error: errorMsg,
1333
+ timestamp: Date.now()
1334
+ };
1335
+ }
1336
+ }
1337
+ /**
1338
+ * Submit a mint commitment to the aggregator (SDK only)
1339
+ * @param commitment - SDK MintCommitment instance
1340
+ */
1341
+ async submitMintCommitment(commitment) {
1342
+ this.ensureConnected();
1343
+ try {
1344
+ const response = await this.stateTransitionClient.submitMintCommitment(commitment);
1345
+ const requestId = commitment.requestId?.toString() ?? response.status;
1346
+ this.emitEvent({
1347
+ type: "commitment:submitted",
1348
+ timestamp: Date.now(),
1349
+ data: { requestId }
1350
+ });
1351
+ return {
1352
+ success: true,
1353
+ requestId,
1354
+ timestamp: Date.now()
1355
+ };
1356
+ } catch (error) {
1357
+ const errorMsg = error instanceof Error ? error.message : String(error);
1358
+ return {
1359
+ success: false,
1360
+ error: errorMsg,
1361
+ timestamp: Date.now()
1362
+ };
1363
+ }
1364
+ }
1365
+ isSdkTransferCommitment(commitment) {
1366
+ return commitment !== null && typeof commitment === "object" && "requestId" in commitment && typeof commitment.requestId?.toString === "function";
1367
+ }
1368
+ async getProof(requestId) {
1369
+ this.ensureConnected();
1370
+ try {
1371
+ const response = await this.rpcCall("getInclusionProof", { requestId });
1372
+ if (!response.proof) {
1373
+ return null;
1374
+ }
1375
+ return {
1376
+ requestId,
1377
+ roundNumber: response.roundNumber ?? 0,
1378
+ proof: response.proof,
1379
+ timestamp: Date.now()
1380
+ };
1381
+ } catch {
1382
+ return null;
1383
+ }
1384
+ }
1385
+ async waitForProof(requestId, options) {
1386
+ const timeout = options?.timeout ?? this.config.timeout;
1387
+ const pollInterval = options?.pollInterval ?? TIMEOUTS.PROOF_POLL_INTERVAL;
1388
+ const startTime = Date.now();
1389
+ let attempt = 0;
1390
+ while (Date.now() - startTime < timeout) {
1391
+ options?.onPoll?.(++attempt);
1392
+ const proof = await this.getProof(requestId);
1393
+ if (proof) {
1394
+ this.emitEvent({
1395
+ type: "proof:received",
1396
+ timestamp: Date.now(),
1397
+ data: { requestId, roundNumber: proof.roundNumber }
1398
+ });
1399
+ return proof;
1400
+ }
1401
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
1402
+ }
1403
+ throw new Error(`Timeout waiting for proof: ${requestId}`);
1404
+ }
1405
+ async validateToken(tokenData) {
1406
+ this.ensureConnected();
1407
+ try {
1408
+ if (this.trustBase && !this.config.skipVerification) {
1409
+ try {
1410
+ const sdkToken = await SdkToken.fromJSON(tokenData);
1411
+ const verifyResult = await sdkToken.verify(this.trustBase);
1412
+ const stateHash = await sdkToken.state.calculateHash();
1413
+ const stateHashStr = stateHash.toJSON();
1414
+ const valid2 = verifyResult.isSuccessful;
1415
+ this.emitEvent({
1416
+ type: "validation:completed",
1417
+ timestamp: Date.now(),
1418
+ data: { valid: valid2 }
1419
+ });
1420
+ return {
1421
+ valid: valid2,
1422
+ spent: false,
1423
+ // Spend check is separate
1424
+ stateHash: stateHashStr,
1425
+ error: valid2 ? void 0 : "SDK verification failed"
1426
+ };
1427
+ } catch (sdkError) {
1428
+ this.log("SDK validation failed, falling back to RPC:", sdkError);
1429
+ }
1430
+ }
1431
+ const response = await this.rpcCall("validateToken", { token: tokenData });
1432
+ const valid = response.valid ?? false;
1433
+ const spent = response.spent ?? false;
1434
+ this.emitEvent({
1435
+ type: "validation:completed",
1436
+ timestamp: Date.now(),
1437
+ data: { valid }
1438
+ });
1439
+ if (response.stateHash && spent) {
1440
+ this.spentCache.set(response.stateHash, true);
1441
+ }
1442
+ return {
1443
+ valid,
1444
+ spent,
1445
+ stateHash: response.stateHash,
1446
+ error: response.error
1447
+ };
1448
+ } catch (error) {
1449
+ return {
1450
+ valid: false,
1451
+ spent: false,
1452
+ error: error instanceof Error ? error.message : String(error)
1453
+ };
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Wait for inclusion proof using SDK (for SDK commitments)
1458
+ */
1459
+ async waitForProofSdk(commitment, signal) {
1460
+ this.ensureConnected();
1461
+ if (!this.trustBase) {
1462
+ throw new Error("Trust base not initialized");
1463
+ }
1464
+ return await waitInclusionProof(
1465
+ this.trustBase,
1466
+ this.stateTransitionClient,
1467
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1468
+ commitment,
1469
+ signal
1470
+ );
1471
+ }
1472
+ async isSpent(stateHash) {
1473
+ if (this.spentCache.has(stateHash)) {
1474
+ return this.spentCache.get(stateHash);
1475
+ }
1476
+ this.ensureConnected();
1477
+ try {
1478
+ const response = await this.rpcCall("isSpent", { stateHash });
1479
+ const spent = response.spent ?? false;
1480
+ if (spent) {
1481
+ this.spentCache.set(stateHash, true);
1482
+ }
1483
+ return spent;
1484
+ } catch {
1485
+ return false;
1486
+ }
1487
+ }
1488
+ async getTokenState(tokenId) {
1489
+ this.ensureConnected();
1490
+ try {
1491
+ const response = await this.rpcCall("getTokenState", { tokenId });
1492
+ if (!response.state) {
1493
+ return null;
1494
+ }
1495
+ return {
1496
+ tokenId,
1497
+ stateHash: response.state.stateHash ?? "",
1498
+ spent: response.state.spent ?? false,
1499
+ roundNumber: response.state.roundNumber,
1500
+ lastUpdated: Date.now()
1501
+ };
1502
+ } catch {
1503
+ return null;
1504
+ }
1505
+ }
1506
+ async getCurrentRound() {
1507
+ if (this.aggregatorClient) {
1508
+ const blockHeight = await this.aggregatorClient.getBlockHeight();
1509
+ return Number(blockHeight);
1510
+ }
1511
+ return 0;
1512
+ }
1513
+ async mint(params) {
1514
+ this.ensureConnected();
1515
+ try {
1516
+ const response = await this.rpcCall("mint", {
1517
+ coinId: params.coinId,
1518
+ amount: params.amount,
1519
+ recipientAddress: params.recipientAddress,
1520
+ recipientPubkey: params.recipientPubkey
1521
+ });
1522
+ return {
1523
+ success: true,
1524
+ requestId: response.requestId,
1525
+ tokenId: response.tokenId
1526
+ };
1527
+ } catch (error) {
1528
+ return {
1529
+ success: false,
1530
+ error: error instanceof Error ? error.message : String(error)
1531
+ };
1532
+ }
1533
+ }
1534
+ // ===========================================================================
1535
+ // Event Subscription
1536
+ // ===========================================================================
1537
+ onEvent(callback) {
1538
+ this.eventCallbacks.add(callback);
1539
+ return () => this.eventCallbacks.delete(callback);
1540
+ }
1541
+ // ===========================================================================
1542
+ // Private: RPC
1543
+ // ===========================================================================
1544
+ async rpcCall(method, params) {
1545
+ const controller = new AbortController();
1546
+ const timeout = setTimeout(() => controller.abort(), this.config.timeout);
1547
+ try {
1548
+ const response = await fetch(this.config.url, {
1549
+ method: "POST",
1550
+ headers: {
1551
+ "Content-Type": "application/json"
1552
+ },
1553
+ body: JSON.stringify({
1554
+ jsonrpc: "2.0",
1555
+ id: Date.now(),
1556
+ method,
1557
+ params
1558
+ }),
1559
+ signal: controller.signal
1560
+ });
1561
+ if (!response.ok) {
1562
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1563
+ }
1564
+ const result = await response.json();
1565
+ if (result.error) {
1566
+ throw new Error(result.error.message ?? "RPC error");
1567
+ }
1568
+ return result.result ?? {};
1569
+ } finally {
1570
+ clearTimeout(timeout);
1571
+ }
1572
+ }
1573
+ // ===========================================================================
1574
+ // Private: Helpers
1575
+ // ===========================================================================
1576
+ ensureConnected() {
1577
+ if (this.status !== "connected") {
1578
+ throw new Error("UnicityAggregatorProvider not connected");
1579
+ }
1580
+ }
1581
+ emitEvent(event) {
1582
+ for (const callback of this.eventCallbacks) {
1583
+ try {
1584
+ callback(event);
1585
+ } catch (error) {
1586
+ this.log("Event callback error:", error);
1587
+ }
1588
+ }
1589
+ }
1590
+ log(...args) {
1591
+ if (this.config.debug) {
1592
+ console.log("[UnicityAggregatorProvider]", ...args);
1593
+ }
1594
+ }
1595
+ };
1596
+ var UnicityOracleProvider = UnicityAggregatorProvider;
1597
+
1598
+ // impl/nodejs/oracle/index.ts
1599
+ var NodeTrustBaseLoader = class {
1600
+ filePath;
1601
+ constructor(filePath = "./trustbase-testnet.json") {
1602
+ this.filePath = filePath;
1603
+ }
1604
+ async load() {
1605
+ try {
1606
+ if (fs3.existsSync(this.filePath)) {
1607
+ const content = fs3.readFileSync(this.filePath, "utf-8");
1608
+ return JSON.parse(content);
1609
+ }
1610
+ } catch {
1611
+ }
1612
+ return null;
1613
+ }
1614
+ };
1615
+ function createNodeTrustBaseLoader(filePath) {
1616
+ return new NodeTrustBaseLoader(filePath);
1617
+ }
1618
+ function createUnicityAggregatorProvider(config) {
1619
+ const { trustBasePath, ...restConfig } = config;
1620
+ return new UnicityAggregatorProvider({
1621
+ ...restConfig,
1622
+ trustBaseLoader: createNodeTrustBaseLoader(trustBasePath)
1623
+ });
1624
+ }
1625
+ var createUnicityOracleProvider = createUnicityAggregatorProvider;
1626
+
1627
+ // impl/shared/resolvers.ts
1628
+ function getNetworkConfig(network = "mainnet") {
1629
+ return NETWORKS[network];
1630
+ }
1631
+ function resolveTransportConfig(network, config) {
1632
+ const networkConfig = getNetworkConfig(network);
1633
+ let relays;
1634
+ if (config?.relays) {
1635
+ relays = config.relays;
1636
+ } else {
1637
+ relays = [...networkConfig.nostrRelays];
1638
+ if (config?.additionalRelays) {
1639
+ relays = [...relays, ...config.additionalRelays];
1640
+ }
1641
+ }
1642
+ return {
1643
+ relays,
1644
+ timeout: config?.timeout,
1645
+ autoReconnect: config?.autoReconnect,
1646
+ debug: config?.debug,
1647
+ // Browser-specific
1648
+ reconnectDelay: config?.reconnectDelay,
1649
+ maxReconnectAttempts: config?.maxReconnectAttempts
1650
+ };
1651
+ }
1652
+ function resolveOracleConfig(network, config) {
1653
+ const networkConfig = getNetworkConfig(network);
1654
+ return {
1655
+ url: config?.url ?? networkConfig.aggregatorUrl,
1656
+ apiKey: config?.apiKey,
1657
+ timeout: config?.timeout,
1658
+ skipVerification: config?.skipVerification,
1659
+ debug: config?.debug,
1660
+ // Node.js-specific
1661
+ trustBasePath: config?.trustBasePath
1662
+ };
1663
+ }
1664
+ function resolveL1Config(network, config) {
1665
+ if (config === void 0) {
1666
+ return void 0;
1667
+ }
1668
+ const networkConfig = getNetworkConfig(network);
1669
+ return {
1670
+ electrumUrl: config.electrumUrl ?? networkConfig.electrumUrl,
1671
+ defaultFeeRate: config.defaultFeeRate,
1672
+ enableVesting: config.enableVesting
1673
+ };
1674
+ }
1675
+
1676
+ // impl/nodejs/index.ts
1677
+ function createNodeProviders(config) {
1678
+ const network = config?.network ?? "mainnet";
1679
+ const transportConfig = resolveTransportConfig(network, config?.transport);
1680
+ const oracleConfig = resolveOracleConfig(network, config?.oracle);
1681
+ const l1Config = resolveL1Config(network, config?.l1);
1682
+ return {
1683
+ storage: createFileStorageProvider({
1684
+ dataDir: config?.dataDir ?? "./sphere-data"
1685
+ }),
1686
+ tokenStorage: createFileTokenStorageProvider({
1687
+ tokensDir: config?.tokensDir ?? "./sphere-tokens"
1688
+ }),
1689
+ transport: createNostrTransportProvider({
1690
+ relays: transportConfig.relays,
1691
+ timeout: transportConfig.timeout,
1692
+ autoReconnect: transportConfig.autoReconnect,
1693
+ debug: transportConfig.debug
1694
+ }),
1695
+ oracle: createUnicityAggregatorProvider({
1696
+ url: oracleConfig.url,
1697
+ apiKey: oracleConfig.apiKey,
1698
+ timeout: oracleConfig.timeout,
1699
+ trustBasePath: oracleConfig.trustBasePath,
1700
+ skipVerification: oracleConfig.skipVerification,
1701
+ debug: oracleConfig.debug
1702
+ }),
1703
+ l1: l1Config
1704
+ };
1705
+ }
1706
+ export {
1707
+ FileStorageProvider,
1708
+ FileTokenStorageProvider,
1709
+ NodeTrustBaseLoader,
1710
+ NostrTransportProvider,
1711
+ UnicityAggregatorProvider,
1712
+ UnicityOracleProvider,
1713
+ createFileStorageProvider,
1714
+ createFileTokenStorageProvider,
1715
+ createNodeProviders,
1716
+ createNodeTrustBaseLoader,
1717
+ createNodeWebSocketFactory,
1718
+ createNostrTransportProvider,
1719
+ createUnicityAggregatorProvider,
1720
+ createUnicityOracleProvider
1721
+ };
1722
+ //# sourceMappingURL=index.js.map