@unicitylabs/sphere-sdk 0.3.8 → 0.3.9

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.
Files changed (43) hide show
  1. package/dist/connect/index.cjs +770 -0
  2. package/dist/connect/index.cjs.map +1 -0
  3. package/dist/connect/index.d.cts +312 -0
  4. package/dist/connect/index.d.ts +312 -0
  5. package/dist/connect/index.js +747 -0
  6. package/dist/connect/index.js.map +1 -0
  7. package/dist/core/index.cjs +87 -7
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +57 -1
  10. package/dist/core/index.d.ts +57 -1
  11. package/dist/core/index.js +87 -7
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/impl/browser/connect/index.cjs +271 -0
  14. package/dist/impl/browser/connect/index.cjs.map +1 -0
  15. package/dist/impl/browser/connect/index.d.cts +137 -0
  16. package/dist/impl/browser/connect/index.d.ts +137 -0
  17. package/dist/impl/browser/connect/index.js +248 -0
  18. package/dist/impl/browser/connect/index.js.map +1 -0
  19. package/dist/impl/browser/index.cjs +167 -32
  20. package/dist/impl/browser/index.cjs.map +1 -1
  21. package/dist/impl/browser/index.js +170 -33
  22. package/dist/impl/browser/index.js.map +1 -1
  23. package/dist/impl/browser/ipfs.cjs.map +1 -1
  24. package/dist/impl/browser/ipfs.js.map +1 -1
  25. package/dist/impl/nodejs/connect/index.cjs +372 -0
  26. package/dist/impl/nodejs/connect/index.cjs.map +1 -0
  27. package/dist/impl/nodejs/connect/index.d.cts +178 -0
  28. package/dist/impl/nodejs/connect/index.d.ts +178 -0
  29. package/dist/impl/nodejs/connect/index.js +333 -0
  30. package/dist/impl/nodejs/connect/index.js.map +1 -0
  31. package/dist/impl/nodejs/index.cjs +114 -4
  32. package/dist/impl/nodejs/index.cjs.map +1 -1
  33. package/dist/impl/nodejs/index.d.cts +49 -0
  34. package/dist/impl/nodejs/index.d.ts +49 -0
  35. package/dist/impl/nodejs/index.js +117 -5
  36. package/dist/impl/nodejs/index.js.map +1 -1
  37. package/dist/index.cjs +87 -7
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +57 -1
  40. package/dist/index.d.ts +57 -1
  41. package/dist/index.js +87 -7
  42. package/dist/index.js.map +1 -1
  43. package/package.json +31 -1
@@ -0,0 +1,747 @@
1
+ // constants.ts
2
+ var STORAGE_KEYS_GLOBAL = {
3
+ /** Encrypted BIP39 mnemonic */
4
+ MNEMONIC: "mnemonic",
5
+ /** Encrypted master private key */
6
+ MASTER_KEY: "master_key",
7
+ /** BIP32 chain code */
8
+ CHAIN_CODE: "chain_code",
9
+ /** HD derivation path (full path like m/44'/0'/0'/0/0) */
10
+ DERIVATION_PATH: "derivation_path",
11
+ /** Base derivation path (like m/44'/0'/0' without chain/index) */
12
+ BASE_PATH: "base_path",
13
+ /** Derivation mode: bip32, wif_hmac, legacy_hmac */
14
+ DERIVATION_MODE: "derivation_mode",
15
+ /** Wallet source: mnemonic, file, unknown */
16
+ WALLET_SOURCE: "wallet_source",
17
+ /** Wallet existence flag */
18
+ WALLET_EXISTS: "wallet_exists",
19
+ /** Current active address index */
20
+ CURRENT_ADDRESS_INDEX: "current_address_index",
21
+ /** Nametag cache per address (separate from tracked addresses registry) */
22
+ ADDRESS_NAMETAGS: "address_nametags",
23
+ /** Active addresses registry (JSON: TrackedAddressesStorage) */
24
+ TRACKED_ADDRESSES: "tracked_addresses",
25
+ /** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
26
+ LAST_WALLET_EVENT_TS: "last_wallet_event_ts",
27
+ /** Group chat: joined groups */
28
+ GROUP_CHAT_GROUPS: "group_chat_groups",
29
+ /** Group chat: messages */
30
+ GROUP_CHAT_MESSAGES: "group_chat_messages",
31
+ /** Group chat: members */
32
+ GROUP_CHAT_MEMBERS: "group_chat_members",
33
+ /** Group chat: processed event IDs for deduplication */
34
+ GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
35
+ /** Group chat: last used relay URL (stale data detection) */
36
+ GROUP_CHAT_RELAY_URL: "group_chat_relay_url",
37
+ /** Cached token registry JSON (fetched from remote) */
38
+ TOKEN_REGISTRY_CACHE: "token_registry_cache",
39
+ /** Timestamp of last token registry cache update (ms since epoch) */
40
+ TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
41
+ /** Cached price data JSON (from CoinGecko or other provider) */
42
+ PRICE_CACHE: "price_cache",
43
+ /** Timestamp of last price cache update (ms since epoch) */
44
+ PRICE_CACHE_TS: "price_cache_ts"
45
+ };
46
+ var STORAGE_KEYS_ADDRESS = {
47
+ /** Pending transfers for this address */
48
+ PENDING_TRANSFERS: "pending_transfers",
49
+ /** Transfer outbox for this address */
50
+ OUTBOX: "outbox",
51
+ /** Conversations for this address */
52
+ CONVERSATIONS: "conversations",
53
+ /** Messages for this address */
54
+ MESSAGES: "messages",
55
+ /** Transaction history for this address */
56
+ TRANSACTION_HISTORY: "transaction_history",
57
+ /** Pending V5 finalization tokens (unconfirmed instant split tokens) */
58
+ PENDING_V5_TOKENS: "pending_v5_tokens"
59
+ };
60
+ var STORAGE_KEYS = {
61
+ ...STORAGE_KEYS_GLOBAL,
62
+ ...STORAGE_KEYS_ADDRESS
63
+ };
64
+ var DEFAULT_BASE_PATH = "m/44'/0'/0'";
65
+ var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
66
+ var HOST_READY_TYPE = "sphere-connect:host-ready";
67
+ var HOST_READY_TIMEOUT = 3e4;
68
+
69
+ // connect/protocol.ts
70
+ var SPHERE_CONNECT_NAMESPACE = "sphere-connect";
71
+ var SPHERE_CONNECT_VERSION = "1.0";
72
+ var RPC_METHODS = {
73
+ GET_IDENTITY: "sphere_getIdentity",
74
+ GET_BALANCE: "sphere_getBalance",
75
+ GET_ASSETS: "sphere_getAssets",
76
+ GET_FIAT_BALANCE: "sphere_getFiatBalance",
77
+ GET_TOKENS: "sphere_getTokens",
78
+ GET_HISTORY: "sphere_getHistory",
79
+ L1_GET_BALANCE: "sphere_l1GetBalance",
80
+ L1_GET_HISTORY: "sphere_l1GetHistory",
81
+ RESOLVE: "sphere_resolve",
82
+ SUBSCRIBE: "sphere_subscribe",
83
+ UNSUBSCRIBE: "sphere_unsubscribe",
84
+ DISCONNECT: "sphere_disconnect"
85
+ };
86
+ var INTENT_ACTIONS = {
87
+ SEND: "send",
88
+ L1_SEND: "l1_send",
89
+ DM: "dm",
90
+ PAYMENT_REQUEST: "payment_request",
91
+ RECEIVE: "receive",
92
+ SIGN_MESSAGE: "sign_message"
93
+ };
94
+ var ERROR_CODES = {
95
+ // Standard JSON-RPC
96
+ PARSE_ERROR: -32700,
97
+ INVALID_REQUEST: -32600,
98
+ METHOD_NOT_FOUND: -32601,
99
+ INVALID_PARAMS: -32602,
100
+ INTERNAL_ERROR: -32603,
101
+ // Sphere Connect (4xxx)
102
+ NOT_CONNECTED: 4001,
103
+ PERMISSION_DENIED: 4002,
104
+ USER_REJECTED: 4003,
105
+ SESSION_EXPIRED: 4004,
106
+ ORIGIN_BLOCKED: 4005,
107
+ RATE_LIMITED: 4006,
108
+ INSUFFICIENT_BALANCE: 4100,
109
+ INVALID_RECIPIENT: 4101,
110
+ TRANSFER_FAILED: 4102,
111
+ INTENT_CANCELLED: 4200
112
+ };
113
+ function isSphereConnectMessage(msg) {
114
+ if (!msg || typeof msg !== "object") return false;
115
+ const m = msg;
116
+ return m.ns === SPHERE_CONNECT_NAMESPACE && m.v === SPHERE_CONNECT_VERSION;
117
+ }
118
+ function createRequestId() {
119
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
120
+ return crypto.randomUUID();
121
+ }
122
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
123
+ }
124
+
125
+ // connect/permissions.ts
126
+ var PERMISSION_SCOPES = {
127
+ IDENTITY_READ: "identity:read",
128
+ BALANCE_READ: "balance:read",
129
+ TOKENS_READ: "tokens:read",
130
+ HISTORY_READ: "history:read",
131
+ L1_READ: "l1:read",
132
+ EVENTS_SUBSCRIBE: "events:subscribe",
133
+ RESOLVE_PEER: "resolve:peer",
134
+ TRANSFER_REQUEST: "transfer:request",
135
+ L1_TRANSFER: "l1:transfer",
136
+ DM_REQUEST: "dm:request",
137
+ PAYMENT_REQUEST: "payment:request",
138
+ SIGN_REQUEST: "sign:request"
139
+ };
140
+ var ALL_PERMISSIONS = Object.values(PERMISSION_SCOPES);
141
+ var DEFAULT_PERMISSIONS = [
142
+ PERMISSION_SCOPES.IDENTITY_READ
143
+ ];
144
+ var METHOD_PERMISSIONS = {
145
+ [RPC_METHODS.GET_IDENTITY]: PERMISSION_SCOPES.IDENTITY_READ,
146
+ [RPC_METHODS.GET_BALANCE]: PERMISSION_SCOPES.BALANCE_READ,
147
+ [RPC_METHODS.GET_ASSETS]: PERMISSION_SCOPES.BALANCE_READ,
148
+ [RPC_METHODS.GET_FIAT_BALANCE]: PERMISSION_SCOPES.BALANCE_READ,
149
+ [RPC_METHODS.GET_TOKENS]: PERMISSION_SCOPES.TOKENS_READ,
150
+ [RPC_METHODS.GET_HISTORY]: PERMISSION_SCOPES.HISTORY_READ,
151
+ [RPC_METHODS.L1_GET_BALANCE]: PERMISSION_SCOPES.L1_READ,
152
+ [RPC_METHODS.L1_GET_HISTORY]: PERMISSION_SCOPES.L1_READ,
153
+ [RPC_METHODS.RESOLVE]: PERMISSION_SCOPES.RESOLVE_PEER,
154
+ [RPC_METHODS.SUBSCRIBE]: PERMISSION_SCOPES.EVENTS_SUBSCRIBE,
155
+ [RPC_METHODS.UNSUBSCRIBE]: PERMISSION_SCOPES.EVENTS_SUBSCRIBE
156
+ };
157
+ var INTENT_PERMISSIONS = {
158
+ [INTENT_ACTIONS.SEND]: PERMISSION_SCOPES.TRANSFER_REQUEST,
159
+ [INTENT_ACTIONS.L1_SEND]: PERMISSION_SCOPES.L1_TRANSFER,
160
+ [INTENT_ACTIONS.DM]: PERMISSION_SCOPES.DM_REQUEST,
161
+ [INTENT_ACTIONS.PAYMENT_REQUEST]: PERMISSION_SCOPES.PAYMENT_REQUEST,
162
+ [INTENT_ACTIONS.RECEIVE]: PERMISSION_SCOPES.IDENTITY_READ,
163
+ [INTENT_ACTIONS.SIGN_MESSAGE]: PERMISSION_SCOPES.SIGN_REQUEST
164
+ };
165
+ function hasMethodPermission(granted, method) {
166
+ const required = METHOD_PERMISSIONS[method];
167
+ if (!required) return false;
168
+ return granted.has(required);
169
+ }
170
+ function hasIntentPermission(granted, action) {
171
+ const required = INTENT_PERMISSIONS[action];
172
+ if (!required) return false;
173
+ return granted.has(required);
174
+ }
175
+ function validatePermissions(permissions) {
176
+ const validScopes = new Set(ALL_PERMISSIONS);
177
+ return permissions.every((p) => validScopes.has(p));
178
+ }
179
+
180
+ // connect/host/ConnectHost.ts
181
+ var DEFAULT_SESSION_TTL_MS = 864e5;
182
+ var DEFAULT_MAX_RPS = 20;
183
+ var ConnectHost = class {
184
+ sphere;
185
+ transport;
186
+ config;
187
+ session = null;
188
+ grantedPermissions = /* @__PURE__ */ new Set();
189
+ // Event subscription management
190
+ eventSubscriptions = /* @__PURE__ */ new Map();
191
+ // eventName → unsub
192
+ // Rate limiting
193
+ rateLimitCounter = 0;
194
+ rateLimitResetAt = 0;
195
+ unsubscribeTransport = null;
196
+ constructor(config) {
197
+ this.sphere = config.sphere;
198
+ this.transport = config.transport;
199
+ this.config = config;
200
+ this.unsubscribeTransport = this.transport.onMessage(this.handleMessage.bind(this));
201
+ }
202
+ /** Get current active session */
203
+ getSession() {
204
+ return this.session;
205
+ }
206
+ /** Revoke the current session */
207
+ revokeSession() {
208
+ if (this.session) {
209
+ this.session.active = false;
210
+ this.cleanupEventSubscriptions();
211
+ this.session = null;
212
+ this.grantedPermissions.clear();
213
+ }
214
+ }
215
+ /** Destroy the host, clean up all resources */
216
+ destroy() {
217
+ this.revokeSession();
218
+ if (this.unsubscribeTransport) {
219
+ this.unsubscribeTransport();
220
+ this.unsubscribeTransport = null;
221
+ }
222
+ }
223
+ // ===========================================================================
224
+ // Message Handling
225
+ // ===========================================================================
226
+ async handleMessage(msg) {
227
+ try {
228
+ if (msg.type === "handshake" && msg.direction === "request") {
229
+ await this.handleHandshake(msg);
230
+ return;
231
+ }
232
+ if (msg.type === "request") {
233
+ await this.handleRpcRequest(msg);
234
+ return;
235
+ }
236
+ if (msg.type === "intent") {
237
+ await this.handleIntentRequest(msg);
238
+ return;
239
+ }
240
+ } catch (error) {
241
+ console.warn("[ConnectHost] Error handling message:", error);
242
+ }
243
+ }
244
+ // ===========================================================================
245
+ // Handshake
246
+ // ===========================================================================
247
+ async handleHandshake(msg) {
248
+ const dapp = msg.dapp;
249
+ if (!dapp) {
250
+ this.sendHandshakeResponse([], void 0, void 0);
251
+ return;
252
+ }
253
+ const requestedPermissions = msg.permissions;
254
+ const { approved, grantedPermissions } = await this.config.onConnectionRequest(
255
+ dapp,
256
+ requestedPermissions
257
+ );
258
+ if (!approved) {
259
+ this.sendHandshakeResponse([], void 0, void 0);
260
+ return;
261
+ }
262
+ const sessionId = createRequestId();
263
+ const allPermissions = [.../* @__PURE__ */ new Set([...DEFAULT_PERMISSIONS, ...grantedPermissions])];
264
+ const ttl = this.config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
265
+ this.session = {
266
+ id: sessionId,
267
+ dapp,
268
+ permissions: allPermissions,
269
+ createdAt: Date.now(),
270
+ expiresAt: ttl > 0 ? Date.now() + ttl : 0,
271
+ active: true
272
+ };
273
+ this.grantedPermissions = new Set(allPermissions);
274
+ const identity = this.getPublicIdentity();
275
+ this.sendHandshakeResponse(allPermissions, sessionId, identity);
276
+ }
277
+ sendHandshakeResponse(permissions, sessionId, identity) {
278
+ this.transport.send({
279
+ ns: SPHERE_CONNECT_NAMESPACE,
280
+ v: SPHERE_CONNECT_VERSION,
281
+ type: "handshake",
282
+ direction: "response",
283
+ permissions,
284
+ sessionId,
285
+ identity
286
+ });
287
+ }
288
+ // ===========================================================================
289
+ // RPC Requests (query)
290
+ // ===========================================================================
291
+ async handleRpcRequest(msg) {
292
+ if (!this.session?.active) {
293
+ this.sendError(msg.id, ERROR_CODES.NOT_CONNECTED, "Not connected");
294
+ return;
295
+ }
296
+ if (this.session.expiresAt > 0 && Date.now() > this.session.expiresAt) {
297
+ this.revokeSession();
298
+ this.sendError(msg.id, ERROR_CODES.SESSION_EXPIRED, "Session expired");
299
+ return;
300
+ }
301
+ if (!this.checkRateLimit()) {
302
+ this.sendError(msg.id, ERROR_CODES.RATE_LIMITED, "Too many requests");
303
+ return;
304
+ }
305
+ if (msg.method === RPC_METHODS.DISCONNECT) {
306
+ this.revokeSession();
307
+ this.sendResult(msg.id, { disconnected: true });
308
+ return;
309
+ }
310
+ if (!hasMethodPermission(this.grantedPermissions, msg.method)) {
311
+ this.sendError(msg.id, ERROR_CODES.PERMISSION_DENIED, `Permission denied for ${msg.method}`);
312
+ return;
313
+ }
314
+ try {
315
+ const result = await this.executeMethod(msg.method, msg.params ?? {});
316
+ this.sendResult(msg.id, result);
317
+ } catch (error) {
318
+ this.sendError(msg.id, ERROR_CODES.INTERNAL_ERROR, error.message);
319
+ }
320
+ }
321
+ // ===========================================================================
322
+ // Intent Requests
323
+ // ===========================================================================
324
+ async handleIntentRequest(msg) {
325
+ if (!this.session?.active) {
326
+ this.sendIntentError(msg.id, ERROR_CODES.NOT_CONNECTED, "Not connected");
327
+ return;
328
+ }
329
+ if (this.session.expiresAt > 0 && Date.now() > this.session.expiresAt) {
330
+ this.revokeSession();
331
+ this.sendIntentError(msg.id, ERROR_CODES.SESSION_EXPIRED, "Session expired");
332
+ return;
333
+ }
334
+ if (!hasIntentPermission(this.grantedPermissions, msg.action)) {
335
+ this.sendIntentError(msg.id, ERROR_CODES.PERMISSION_DENIED, `Permission denied for intent: ${msg.action}`);
336
+ return;
337
+ }
338
+ const response = await this.config.onIntent(msg.action, msg.params, this.session);
339
+ if (response.error) {
340
+ this.sendIntentError(msg.id, response.error.code, response.error.message);
341
+ } else {
342
+ this.sendIntentResult(msg.id, response.result);
343
+ }
344
+ }
345
+ // ===========================================================================
346
+ // Method Router
347
+ // ===========================================================================
348
+ async executeMethod(method, params) {
349
+ switch (method) {
350
+ case RPC_METHODS.GET_IDENTITY:
351
+ return this.getPublicIdentity();
352
+ case RPC_METHODS.GET_BALANCE:
353
+ return this.sphere.payments.getBalance(params.coinId);
354
+ case RPC_METHODS.GET_ASSETS:
355
+ return this.sphere.payments.getAssets(params.coinId);
356
+ case RPC_METHODS.GET_FIAT_BALANCE:
357
+ return { fiatBalance: await this.sphere.payments.getFiatBalance() };
358
+ case RPC_METHODS.GET_TOKENS:
359
+ return this.stripTokenSdkData(
360
+ this.sphere.payments.getTokens(
361
+ params.coinId ? { coinId: params.coinId } : void 0
362
+ )
363
+ );
364
+ case RPC_METHODS.GET_HISTORY:
365
+ return this.sphere.payments.getHistory();
366
+ case RPC_METHODS.L1_GET_BALANCE:
367
+ if (!this.sphere.payments.l1) {
368
+ throw new Error("L1 module not available");
369
+ }
370
+ return this.sphere.payments.l1.getBalance();
371
+ case RPC_METHODS.L1_GET_HISTORY:
372
+ if (!this.sphere.payments.l1) {
373
+ throw new Error("L1 module not available");
374
+ }
375
+ return this.sphere.payments.l1.getHistory(params.limit);
376
+ case RPC_METHODS.RESOLVE:
377
+ if (!params.identifier) {
378
+ throw new Error("Missing required parameter: identifier");
379
+ }
380
+ return this.sphere.resolve(params.identifier);
381
+ case RPC_METHODS.SUBSCRIBE:
382
+ return this.handleSubscribe(params.event);
383
+ case RPC_METHODS.UNSUBSCRIBE:
384
+ return this.handleUnsubscribe(params.event);
385
+ default:
386
+ throw new Error(`Unknown method: ${method}`);
387
+ }
388
+ }
389
+ // ===========================================================================
390
+ // Event Subscriptions
391
+ // ===========================================================================
392
+ handleSubscribe(eventName) {
393
+ if (!eventName) throw new Error("Missing required parameter: event");
394
+ if (this.eventSubscriptions.has(eventName)) {
395
+ return { subscribed: true, event: eventName };
396
+ }
397
+ const unsub = this.sphere.on(eventName, (data) => {
398
+ this.transport.send({
399
+ ns: SPHERE_CONNECT_NAMESPACE,
400
+ v: SPHERE_CONNECT_VERSION,
401
+ type: "event",
402
+ event: eventName,
403
+ data
404
+ });
405
+ });
406
+ this.eventSubscriptions.set(eventName, unsub);
407
+ return { subscribed: true, event: eventName };
408
+ }
409
+ handleUnsubscribe(eventName) {
410
+ if (!eventName) throw new Error("Missing required parameter: event");
411
+ const unsub = this.eventSubscriptions.get(eventName);
412
+ if (unsub) {
413
+ unsub();
414
+ this.eventSubscriptions.delete(eventName);
415
+ }
416
+ return { unsubscribed: true, event: eventName };
417
+ }
418
+ cleanupEventSubscriptions() {
419
+ for (const [, unsub] of this.eventSubscriptions) {
420
+ unsub();
421
+ }
422
+ this.eventSubscriptions.clear();
423
+ }
424
+ // ===========================================================================
425
+ // Helpers
426
+ // ===========================================================================
427
+ getPublicIdentity() {
428
+ const id = this.sphere.identity;
429
+ if (!id) return void 0;
430
+ return {
431
+ chainPubkey: id.chainPubkey,
432
+ l1Address: id.l1Address,
433
+ directAddress: id.directAddress,
434
+ nametag: id.nametag
435
+ };
436
+ }
437
+ stripTokenSdkData(tokens) {
438
+ return tokens.map((t) => {
439
+ const token = t;
440
+ const { sdkData, ...publicFields } = token;
441
+ return publicFields;
442
+ });
443
+ }
444
+ sendResult(id, result) {
445
+ this.transport.send({
446
+ ns: SPHERE_CONNECT_NAMESPACE,
447
+ v: SPHERE_CONNECT_VERSION,
448
+ type: "response",
449
+ id,
450
+ result
451
+ });
452
+ }
453
+ sendError(id, code, message) {
454
+ this.transport.send({
455
+ ns: SPHERE_CONNECT_NAMESPACE,
456
+ v: SPHERE_CONNECT_VERSION,
457
+ type: "response",
458
+ id,
459
+ error: { code, message }
460
+ });
461
+ }
462
+ sendIntentResult(id, result) {
463
+ this.transport.send({
464
+ ns: SPHERE_CONNECT_NAMESPACE,
465
+ v: SPHERE_CONNECT_VERSION,
466
+ type: "intent_result",
467
+ id,
468
+ result
469
+ });
470
+ }
471
+ sendIntentError(id, code, message) {
472
+ this.transport.send({
473
+ ns: SPHERE_CONNECT_NAMESPACE,
474
+ v: SPHERE_CONNECT_VERSION,
475
+ type: "intent_result",
476
+ id,
477
+ error: { code, message }
478
+ });
479
+ }
480
+ checkRateLimit() {
481
+ const maxRps = this.config.maxRequestsPerSecond ?? DEFAULT_MAX_RPS;
482
+ const now = Date.now();
483
+ if (now > this.rateLimitResetAt) {
484
+ this.rateLimitCounter = 0;
485
+ this.rateLimitResetAt = now + 1e3;
486
+ }
487
+ this.rateLimitCounter++;
488
+ return this.rateLimitCounter <= maxRps;
489
+ }
490
+ };
491
+
492
+ // connect/client/ConnectClient.ts
493
+ var DEFAULT_TIMEOUT = 3e4;
494
+ var DEFAULT_INTENT_TIMEOUT = 12e4;
495
+ var ConnectClient = class {
496
+ transport;
497
+ dapp;
498
+ requestedPermissions;
499
+ timeout;
500
+ intentTimeout;
501
+ sessionId = null;
502
+ grantedPermissions = [];
503
+ identity = null;
504
+ connected = false;
505
+ pendingRequests = /* @__PURE__ */ new Map();
506
+ eventHandlers = /* @__PURE__ */ new Map();
507
+ unsubscribeTransport = null;
508
+ // Handshake resolver (one-shot)
509
+ handshakeResolver = null;
510
+ constructor(config) {
511
+ this.transport = config.transport;
512
+ this.dapp = config.dapp;
513
+ this.requestedPermissions = config.permissions ?? [...ALL_PERMISSIONS];
514
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
515
+ this.intentTimeout = config.intentTimeout ?? DEFAULT_INTENT_TIMEOUT;
516
+ }
517
+ // ===========================================================================
518
+ // Connection
519
+ // ===========================================================================
520
+ /** Connect to the wallet. Returns session info and public identity. */
521
+ async connect() {
522
+ this.unsubscribeTransport = this.transport.onMessage(this.handleMessage.bind(this));
523
+ return new Promise((resolve, reject) => {
524
+ const timer = setTimeout(() => {
525
+ this.handshakeResolver = null;
526
+ reject(new Error("Connection timeout"));
527
+ }, this.timeout);
528
+ this.handshakeResolver = { resolve, reject, timer };
529
+ this.transport.send({
530
+ ns: SPHERE_CONNECT_NAMESPACE,
531
+ v: SPHERE_CONNECT_VERSION,
532
+ type: "handshake",
533
+ direction: "request",
534
+ permissions: this.requestedPermissions,
535
+ dapp: this.dapp
536
+ });
537
+ });
538
+ }
539
+ /** Disconnect from the wallet */
540
+ async disconnect() {
541
+ if (this.connected) {
542
+ try {
543
+ await this.query(RPC_METHODS.DISCONNECT);
544
+ } catch {
545
+ }
546
+ }
547
+ this.cleanup();
548
+ }
549
+ /** Whether currently connected */
550
+ get isConnected() {
551
+ return this.connected;
552
+ }
553
+ /** Granted permission scopes */
554
+ get permissions() {
555
+ return this.grantedPermissions;
556
+ }
557
+ /** Current session ID */
558
+ get session() {
559
+ return this.sessionId;
560
+ }
561
+ /** Public identity received during handshake */
562
+ get walletIdentity() {
563
+ return this.identity;
564
+ }
565
+ // ===========================================================================
566
+ // Query (read data)
567
+ // ===========================================================================
568
+ /** Send a query request and return the result */
569
+ async query(method, params) {
570
+ if (!this.connected) throw new Error("Not connected");
571
+ const id = createRequestId();
572
+ return new Promise((resolve, reject) => {
573
+ const timer = setTimeout(() => {
574
+ this.pendingRequests.delete(id);
575
+ reject(new Error(`Query timeout: ${method}`));
576
+ }, this.timeout);
577
+ this.pendingRequests.set(id, {
578
+ resolve,
579
+ reject,
580
+ timer
581
+ });
582
+ this.transport.send({
583
+ ns: SPHERE_CONNECT_NAMESPACE,
584
+ v: SPHERE_CONNECT_VERSION,
585
+ type: "request",
586
+ id,
587
+ method,
588
+ params
589
+ });
590
+ });
591
+ }
592
+ // ===========================================================================
593
+ // Intent (trigger wallet UI)
594
+ // ===========================================================================
595
+ /** Send an intent request. The wallet will open its UI for user confirmation. */
596
+ async intent(action, params) {
597
+ if (!this.connected) throw new Error("Not connected");
598
+ const id = createRequestId();
599
+ return new Promise((resolve, reject) => {
600
+ const timer = setTimeout(() => {
601
+ this.pendingRequests.delete(id);
602
+ reject(new Error(`Intent timeout: ${action}`));
603
+ }, this.intentTimeout);
604
+ this.pendingRequests.set(id, {
605
+ resolve,
606
+ reject,
607
+ timer
608
+ });
609
+ this.transport.send({
610
+ ns: SPHERE_CONNECT_NAMESPACE,
611
+ v: SPHERE_CONNECT_VERSION,
612
+ type: "intent",
613
+ id,
614
+ action,
615
+ params
616
+ });
617
+ });
618
+ }
619
+ // ===========================================================================
620
+ // Events
621
+ // ===========================================================================
622
+ /** Subscribe to a wallet event. Returns unsubscribe function. */
623
+ on(event, handler) {
624
+ if (!this.eventHandlers.has(event)) {
625
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
626
+ if (this.connected) {
627
+ this.query(RPC_METHODS.SUBSCRIBE, { event }).catch(() => {
628
+ });
629
+ }
630
+ }
631
+ this.eventHandlers.get(event).add(handler);
632
+ return () => {
633
+ const handlers = this.eventHandlers.get(event);
634
+ if (handlers) {
635
+ handlers.delete(handler);
636
+ if (handlers.size === 0) {
637
+ this.eventHandlers.delete(event);
638
+ if (this.connected) {
639
+ this.query(RPC_METHODS.UNSUBSCRIBE, { event }).catch(() => {
640
+ });
641
+ }
642
+ }
643
+ }
644
+ };
645
+ }
646
+ // ===========================================================================
647
+ // Message Handling
648
+ // ===========================================================================
649
+ handleMessage(msg) {
650
+ if (msg.type === "handshake" && msg.direction === "response") {
651
+ this.handleHandshakeResponse(msg);
652
+ return;
653
+ }
654
+ if (msg.type === "response") {
655
+ this.handlePendingResponse(msg.id, msg.result, msg.error);
656
+ return;
657
+ }
658
+ if (msg.type === "intent_result") {
659
+ this.handlePendingResponse(msg.id, msg.result, msg.error);
660
+ return;
661
+ }
662
+ if (msg.type === "event") {
663
+ const handlers = this.eventHandlers.get(msg.event);
664
+ if (handlers) {
665
+ for (const handler of handlers) {
666
+ try {
667
+ handler(msg.data);
668
+ } catch {
669
+ }
670
+ }
671
+ }
672
+ }
673
+ }
674
+ handleHandshakeResponse(msg) {
675
+ if (!this.handshakeResolver) return;
676
+ clearTimeout(this.handshakeResolver.timer);
677
+ if (msg.sessionId && msg.identity) {
678
+ this.sessionId = msg.sessionId;
679
+ this.grantedPermissions = msg.permissions;
680
+ this.identity = msg.identity;
681
+ this.connected = true;
682
+ this.handshakeResolver.resolve({
683
+ sessionId: msg.sessionId,
684
+ permissions: this.grantedPermissions,
685
+ identity: msg.identity
686
+ });
687
+ } else {
688
+ this.handshakeResolver.reject(new Error("Connection rejected by wallet"));
689
+ }
690
+ this.handshakeResolver = null;
691
+ }
692
+ handlePendingResponse(id, result, error) {
693
+ const pending = this.pendingRequests.get(id);
694
+ if (!pending) return;
695
+ clearTimeout(pending.timer);
696
+ this.pendingRequests.delete(id);
697
+ if (error) {
698
+ const err = new Error(error.message);
699
+ err.code = error.code;
700
+ err.data = error.data;
701
+ pending.reject(err);
702
+ } else {
703
+ pending.resolve(result);
704
+ }
705
+ }
706
+ // ===========================================================================
707
+ // Cleanup
708
+ // ===========================================================================
709
+ cleanup() {
710
+ if (this.unsubscribeTransport) {
711
+ this.unsubscribeTransport();
712
+ this.unsubscribeTransport = null;
713
+ }
714
+ for (const [, pending] of this.pendingRequests) {
715
+ clearTimeout(pending.timer);
716
+ pending.reject(new Error("Disconnected"));
717
+ }
718
+ this.pendingRequests.clear();
719
+ this.eventHandlers.clear();
720
+ this.connected = false;
721
+ this.sessionId = null;
722
+ this.grantedPermissions = [];
723
+ this.identity = null;
724
+ }
725
+ };
726
+ export {
727
+ ALL_PERMISSIONS,
728
+ ConnectClient,
729
+ ConnectHost,
730
+ DEFAULT_PERMISSIONS,
731
+ ERROR_CODES,
732
+ HOST_READY_TIMEOUT,
733
+ HOST_READY_TYPE,
734
+ INTENT_ACTIONS,
735
+ INTENT_PERMISSIONS,
736
+ METHOD_PERMISSIONS,
737
+ PERMISSION_SCOPES,
738
+ RPC_METHODS,
739
+ SPHERE_CONNECT_NAMESPACE,
740
+ SPHERE_CONNECT_VERSION,
741
+ createRequestId,
742
+ hasIntentPermission,
743
+ hasMethodPermission,
744
+ isSphereConnectMessage,
745
+ validatePermissions
746
+ };
747
+ //# sourceMappingURL=index.js.map