@0xio/sdk 2.5.0 → 2.7.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.
package/dist/index.umd.js CHANGED
@@ -41,8 +41,9 @@
41
41
  */
42
42
  once(eventType, listener) {
43
43
  const onceListener = (event) => {
44
- listener(event);
44
+ // Remove BEFORE calling so a throwing listener doesn't stay registered
45
45
  this.off(eventType, onceListener);
46
+ listener(event);
46
47
  };
47
48
  this.on(eventType, onceListener);
48
49
  }
@@ -143,6 +144,135 @@
143
144
  }
144
145
  }
145
146
 
147
+ /**
148
+ * 0xio Wallet transport adapter.
149
+ *
150
+ * Implements the postMessage protocol used by the 0xio browser extension (>= v2.4.0).
151
+ *
152
+ * Outbound wire format:
153
+ * window.postMessage({ source: '0xio-sdk-request', request: { id, method, params, timestamp } }, origin)
154
+ *
155
+ * Inbound wire format:
156
+ * window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
157
+ * window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
158
+ *
159
+ * H-2: Session nonce validation — injected.ts broadcasts the nonce received from the
160
+ * isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
161
+ * mismatched nonce is rejected, preventing response injection by malicious page scripts.
162
+ */
163
+ /**
164
+ * Creates a 0xio adapter. The factory accepts optional extra trusted parent origins
165
+ * so the communicator can forward its own trustedOrigins setting to origin validation.
166
+ */
167
+ function createZeroXIOAdapter(extraTrustedOrigins = []) {
168
+ return {
169
+ name: '0xio',
170
+ displayName: '0xio Wallet',
171
+ detect() {
172
+ if (typeof window === 'undefined')
173
+ return false;
174
+ const win = window;
175
+ return !!(win.wallet0xio ||
176
+ win.ZeroXIOWallet ||
177
+ // 0xio extension also exposes window.octra with isOctra=true; match it here
178
+ // so this adapter (postMessage protocol) takes priority over OctraProviderAdapter
179
+ (win.octra?.isOctra && (win.wallet0xio || win.ZeroXIOWallet)) ||
180
+ win.chrome?.runtime?.id ||
181
+ document.querySelector('meta[name="0xio-dapp"]') ||
182
+ document.querySelector('[data-0xio-sdk-bridge]'));
183
+ },
184
+ postRequest(request) {
185
+ window.postMessage({ source: '0xio-sdk-request', request }, window.location.origin);
186
+ },
187
+ postRequestToParent(request, parentOrigin) {
188
+ try {
189
+ window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
190
+ }
191
+ catch {
192
+ // Do not fall back to '*' — silent failure is safer
193
+ }
194
+ },
195
+ listen(handler, options) {
196
+ const allowedOrigin = window.location.origin;
197
+ const trustedParentOrigins = new Set([
198
+ allowedOrigin,
199
+ 'tauri://localhost',
200
+ 'https://tauri.localhost',
201
+ 'http://localhost',
202
+ 'https://localhost',
203
+ ...(extraTrustedOrigins),
204
+ ...(options?.trustedParentOrigins ?? []),
205
+ ]);
206
+ let _sessionNonce = null;
207
+ // H-2: receive session nonce from injected.ts (MAIN world content script)
208
+ const nonceListener = (e) => {
209
+ if (e.origin !== allowedOrigin)
210
+ return;
211
+ if (e.data?.source === '0xio-sdk-nonce-init' && typeof e.data.nonce === 'string') {
212
+ _sessionNonce = e.data.nonce;
213
+ window.removeEventListener('message', nonceListener);
214
+ }
215
+ };
216
+ window.addEventListener('message', nonceListener);
217
+ const msgListener = (e) => {
218
+ const isFromSameOrigin = e.origin === allowedOrigin;
219
+ const isLocalhost = e.origin.startsWith('http://localhost:') ||
220
+ e.origin.startsWith('http://127.0.0.1:');
221
+ const isFromTrustedParent = e.source === window.parent &&
222
+ window.parent !== window &&
223
+ (trustedParentOrigins.has(e.origin) || isLocalhost);
224
+ if (!isFromSameOrigin && !isFromTrustedParent)
225
+ return;
226
+ if (e.source !== window && e.source !== window.parent)
227
+ return;
228
+ if (!e.data || e.data.source !== '0xio-sdk-bridge')
229
+ return;
230
+ // H-2: session nonce validation.
231
+ // Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
232
+ // Fallback path: if the init broadcast was missed (race between document_start
233
+ // content script and page script load), capture nonce from the first same-origin
234
+ // response so that all subsequent responses are validated.
235
+ if (!_sessionNonce && isFromSameOrigin && typeof e.data.sessionNonce === 'string') {
236
+ _sessionNonce = e.data.sessionNonce;
237
+ }
238
+ if (_sessionNonce && e.data.sessionNonce !== _sessionNonce)
239
+ return;
240
+ if (e.data.response) {
241
+ const r = e.data.response;
242
+ handler({
243
+ requestId: r.id,
244
+ success: r.success,
245
+ data: r.data,
246
+ error: r.error,
247
+ });
248
+ }
249
+ else if (e.data.event) {
250
+ handler({
251
+ eventType: e.data.event.type,
252
+ eventData: e.data.event.data ?? e.data.event,
253
+ });
254
+ }
255
+ };
256
+ window.addEventListener('message', msgListener);
257
+ return () => {
258
+ window.removeEventListener('message', nonceListener);
259
+ window.removeEventListener('message', msgListener);
260
+ };
261
+ },
262
+ listenForReady(onReady) {
263
+ const handler = () => onReady();
264
+ window.addEventListener('0xioWalletReady', handler);
265
+ window.addEventListener('wallet0xioReady', handler);
266
+ return () => {
267
+ window.removeEventListener('0xioWalletReady', handler);
268
+ window.removeEventListener('wallet0xioReady', handler);
269
+ };
270
+ },
271
+ };
272
+ }
273
+ /** Default 0xio adapter instance (no extra trusted origins). */
274
+ const ZeroXIOAdapter = createZeroXIOAdapter();
275
+
146
276
  /**
147
277
  * 0xio Wallet SDK - Utilities
148
278
  * Helper functions for validation, formatting, and common operations
@@ -162,9 +292,15 @@
162
292
  return addressRegex.test(address);
163
293
  }
164
294
  /**
165
- * Validate transaction amount
295
+ * Validate transaction amount.
296
+ * Accepts both number and string representations.
297
+ * String amounts avoid JS number precision loss for very large values.
166
298
  */
167
299
  function isValidAmount(amount) {
300
+ if (typeof amount === 'string') {
301
+ const n = parseFloat(amount);
302
+ return !isNaN(n) && n > 0 && Number.isFinite(n);
303
+ }
168
304
  return typeof amount === 'number' &&
169
305
  amount > 0 &&
170
306
  Number.isFinite(amount) &&
@@ -174,11 +310,13 @@
174
310
  * Validate transaction message
175
311
  */
176
312
  function isValidMessage(message) {
177
- if (!message) {
178
- return true; // Empty messages are valid
179
- }
313
+ // Type check first — falsy non-strings (0, false, null) are NOT valid messages
180
314
  if (typeof message !== 'string') {
181
- return false;
315
+ return message === undefined || message === null ? true : false;
316
+ }
317
+ // Empty string is valid (optional field)
318
+ if (message.length === 0) {
319
+ return true;
182
320
  }
183
321
  // 100KB limit — contract call params can be large (serialized JSON)
184
322
  return message.length <= 100000;
@@ -190,16 +328,64 @@
190
328
  return feeLevel === 1 || feeLevel === 3;
191
329
  }
192
330
  // ===================
331
+ // ADDRESS DERIVATION
332
+ // ===================
333
+ const _B58_ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
334
+ function _base58Encode(buf) {
335
+ let zeros = 0;
336
+ for (let i = 0; i < buf.length && buf[i] === 0; i++)
337
+ zeros++;
338
+ const digits = [];
339
+ for (let i = zeros; i < buf.length; i++) {
340
+ let carry = buf[i];
341
+ for (let j = 0; j < digits.length; j++) {
342
+ carry += digits[j] << 8;
343
+ digits[j] = carry % 58;
344
+ carry = Math.floor(carry / 58);
345
+ }
346
+ while (carry > 0) {
347
+ digits.push(carry % 58);
348
+ carry = Math.floor(carry / 58);
349
+ }
350
+ }
351
+ let out = '';
352
+ for (let i = 0; i < zeros; i++)
353
+ out += _B58_ALPHA[0];
354
+ for (let i = digits.length - 1; i >= 0; i--)
355
+ out += _B58_ALPHA[digits[i]];
356
+ return out;
357
+ }
358
+ /**
359
+ * Derive the canonical Octra address from a base64-encoded Ed25519 public key.
360
+ * Algorithm: SHA-256(pubkey_bytes) → base58 → prepend "oct"
361
+ * Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
362
+ */
363
+ async function deriveOctraAddress(publicKeyBase64) {
364
+ if (!publicKeyBase64 || typeof publicKeyBase64 !== 'string') {
365
+ throw new Error('publicKeyBase64 must be a non-empty string');
366
+ }
367
+ if (typeof crypto === 'undefined' || !crypto.subtle) {
368
+ throw new Error('Web Crypto API is not available in this environment');
369
+ }
370
+ const binStr = atob(publicKeyBase64);
371
+ const bytes = new Uint8Array(binStr.length);
372
+ for (let i = 0; i < binStr.length; i++)
373
+ bytes[i] = binStr.charCodeAt(i);
374
+ const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
375
+ return 'oct' + _base58Encode(new Uint8Array(hashBuf));
376
+ }
377
+ // ===================
193
378
  // FORMATTING UTILITIES
194
379
  // ===================
195
380
  /**
196
381
  * Format OCT amount for display
197
382
  */
198
383
  function formatOCT(amount, decimals = 6) {
199
- if (!isValidAmount(amount)) {
384
+ const n = typeof amount === 'string' ? parseFloat(amount) : amount;
385
+ if (!isValidAmount(n)) {
200
386
  return '0';
201
387
  }
202
- return amount.toLocaleString(undefined, {
388
+ return n.toLocaleString(undefined, {
203
389
  minimumFractionDigits: 0,
204
390
  maximumFractionDigits: decimals
205
391
  });
@@ -401,7 +587,7 @@
401
587
  networkInfo: {
402
588
  id: 'mainnet',
403
589
  name: 'Octra Mainnet',
404
- rpcUrl: 'http://46.101.86.250:8080',
590
+ rpcUrl: 'https://octra.network',
405
591
  explorerUrl: 'https://lite.octrascan.io/tx.html?hash=',
406
592
  explorerAddressUrl: 'https://lite.octrascan.io/address.html?addr=',
407
593
  indexerUrl: 'https://lite.octrascan.io',
@@ -474,108 +660,54 @@
474
660
  * to ensure secure wallet interactions.
475
661
  *
476
662
  * @module communication
477
- * @version 2.5.0
663
+ * @version 2.7.0
478
664
  * @license MIT
479
665
  */
480
- /**
481
- * ExtensionCommunicator - Manages communication with the 0xio Wallet browser extension
482
- *
483
- * @class
484
- * @extends EventEmitter
485
- *
486
- * @description
487
- * Handles all communication between the SDK and wallet extension including:
488
- * - Request/response message passing with origin validation
489
- * - Rate limiting to prevent DoS attacks
490
- * - Automatic retry logic with exponential backoff
491
- * - Extension detection and availability monitoring
492
- * - Cryptographically secure request ID generation
493
- *
494
- * @example
495
- * ```typescript
496
- * const communicator = new ExtensionCommunicator(true); // debug mode
497
- * await communicator.initialize();
498
- *
499
- * const response = await communicator.sendRequest('get_balance', {});
500
- * console.log(response);
501
- * ```
502
- */
503
666
  class ExtensionCommunicator extends EventEmitter {
504
- /**
505
- * Creates a new ExtensionCommunicator instance
506
- *
507
- * @param {boolean} debug - Enable debug logging
508
- */
509
- constructor(debug = false, trustedOrigins = []) {
667
+ constructor(debug = false, trustedOrigins = [], adapter) {
510
668
  super(debug);
511
- /** Legacy request counter (deprecated, kept for fallback) */
512
- this.requestId = 0;
513
- /** Map of pending requests awaiting responses */
514
669
  this.pendingRequests = new Map();
515
- /** Initialization state flag */
516
670
  this.isInitialized = false;
517
- /** Interval handle for periodic extension detection */
518
671
  this.extensionDetectionInterval = null;
519
- /** Current extension availability state */
520
672
  this.isExtensionAvailableState = false;
521
- /** Message listener reference for cleanup */
522
- this.messageListener = null;
523
- /** Trusted parent origins for iframe communication */
524
673
  this.trustedOrigins = [];
525
- /** Parent origin learned from walletReady signal */
526
674
  this._parentOrigin = null;
527
- // Rate limiting configuration
528
- /** Maximum number of concurrent pending requests */
675
+ /** Teardown fn returned by adapter.listen() */
676
+ this._adapterTeardown = null;
677
+ /** Teardown fn returned by adapter.listenForReady() */
678
+ this._adapterReadyTeardown = null;
679
+ /**
680
+ * Set when a trusted walletReady has been received from window.parent.
681
+ * The polling fallback must NOT clear this flag.
682
+ */
683
+ this._parentTrusted = false;
684
+ /** walletReady postMessage listener stored for cleanup */
685
+ this._walletReadyMessageListener = null;
686
+ /**
687
+ * In-flight interactive request lock.
688
+ * Methods that open approval popups are serialized — only one at a time.
689
+ */
690
+ this._interactiveInFlight = false;
529
691
  this.MAX_CONCURRENT_REQUESTS = 50;
530
- /** Time window for rate limiting (milliseconds) */
531
692
  this.RATE_LIMIT_WINDOW = 1000;
532
- /** Maximum requests allowed per time window */
533
693
  this.MAX_REQUESTS_PER_WINDOW = 20;
534
- /** Timestamps of recent requests for rate limiting */
535
694
  this.requestTimestamps = [];
536
695
  this.logger = createLogger('ExtensionCommunicator', debug);
537
696
  this.trustedOrigins = trustedOrigins;
697
+ this.adapter = adapter ?? createZeroXIOAdapter(trustedOrigins);
538
698
  this.setupMessageListener();
539
699
  this.startExtensionDetection();
540
700
  }
541
- /**
542
- * Add trusted origins for iframe/bridge communication
543
- * Call this before connecting if your dApp runs inside a trusted frame
544
- */
545
701
  setTrustedOrigins(origins) {
546
702
  this.trustedOrigins = origins;
547
703
  }
548
- /**
549
- * Initialize communication with the wallet extension
550
- *
551
- * @description
552
- * Performs initial setup and verification:
553
- * 1. Waits for extension to become available
554
- * 2. Sends ping to verify communication
555
- * 3. Establishes message handlers
556
- *
557
- * Must be called before any other methods.
558
- *
559
- * @returns {Promise<boolean>} True if initialization succeeded, false otherwise
560
- * @throws {ZeroXIOWalletError} If extension is not available after timeout
561
- *
562
- * @example
563
- * ```typescript
564
- * const success = await communicator.initialize();
565
- * if (!success) {
566
- * console.error('Failed to initialize wallet connection');
567
- * }
568
- * ```
569
- */
570
704
  async initialize() {
571
705
  if (this.isInitialized) {
572
706
  return true;
573
707
  }
574
708
  try {
575
- // Wait for extension detection with timeout
576
709
  const available = await this.waitForExtensionAvailability(10000);
577
710
  if (available) {
578
- // Verify with ping
579
711
  await withTimeout(this.sendRequestWithRetry('ping', {}, 3, 2000), 8000, 'Extension ping timeout during initialization');
580
712
  this.isInitialized = true;
581
713
  this.logger.log('Extension communication initialized successfully');
@@ -590,149 +722,106 @@
590
722
  return false;
591
723
  }
592
724
  }
593
- /**
594
- * Check if extension is available
595
- */
596
725
  isExtensionAvailable() {
597
726
  return this.isExtensionAvailableState && this.hasExtensionContext();
598
727
  }
599
- /**
600
- * Send request to extension
601
- */
602
728
  async sendRequest(method, params = {}, timeout = 30000) {
603
729
  const isInteractive = ExtensionCommunicator.NO_RETRY_METHODS.has(method);
604
730
  const maxRetries = isInteractive ? 0 : 1;
605
- // Give users 3 minutes for interactive approvals (review + sign + FHE proof generation)
606
731
  const effectiveTimeout = isInteractive ? Math.max(timeout, 180000) : timeout;
607
732
  return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
608
733
  }
609
- /**
610
- * Send request to extension with automatic retry logic
611
- */
612
734
  async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
613
735
  if (!this.hasExtensionContext()) {
614
- throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, '0xio Wallet extension is not installed or available', {
615
- method,
616
- params,
617
- browserContext: this.getBrowserDiagnostics()
618
- });
736
+ throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, '0xio Wallet extension is not installed or available', { method, browserContext: this.getBrowserDiagnostics() });
619
737
  }
620
738
  if (!this.isExtensionAvailableState) {
621
- // Wait a bit for extension to become available
622
739
  await this.waitForExtensionAvailability(5000);
623
740
  if (!this.isExtensionAvailableState) {
624
- throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Extension not available for communication', {
625
- method,
626
- params,
627
- extensionState: this.getExtensionDiagnostics()
628
- });
741
+ throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Extension not available for communication', { method, extensionState: this.getExtensionDiagnostics() });
629
742
  }
630
743
  }
631
- // SECURITY: Check rate limits before processing
744
+ // Enforce one-at-a-time for interactive popup methods
745
+ const isInteractive = ExtensionCommunicator.INTERACTIVE_METHODS.has(method);
746
+ if (isInteractive) {
747
+ if (this._interactiveInFlight) {
748
+ throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, 'Another approval popup is already open. Please wait for it to complete.');
749
+ }
750
+ this._interactiveInFlight = true;
751
+ }
632
752
  this.checkRateLimit();
633
- return retry(async () => {
634
- const requestId = this.generateRequestId();
635
- const request = {
636
- id: requestId,
637
- method,
638
- params,
639
- timestamp: Date.now()
640
- };
641
- this.logger.log(`Sending request (${method}):`, { id: requestId, params });
642
- return new Promise((resolve, reject) => {
643
- // Set up timeout
644
- const timeoutHandle = setTimeout(() => {
645
- const pending = this.pendingRequests.get(requestId);
646
- if (pending) {
753
+ try {
754
+ return await retry(async () => {
755
+ const requestId = this.generateRequestId();
756
+ const request = {
757
+ id: requestId,
758
+ method,
759
+ params,
760
+ timestamp: Date.now()
761
+ };
762
+ this.logger.log(`Sending request (${method}):`, { id: requestId });
763
+ return new Promise((resolve, reject) => {
764
+ const timeoutHandle = setTimeout(() => {
765
+ const pending = this.pendingRequests.get(requestId);
766
+ if (pending) {
767
+ this.pendingRequests.delete(requestId);
768
+ reject(new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, `Request timeout after ${timeout}ms`, { method, requestId, retryCount: pending.retryCount }));
769
+ }
770
+ }, timeout);
771
+ this.pendingRequests.set(requestId, {
772
+ resolve,
773
+ reject,
774
+ timeout: timeoutHandle,
775
+ retryCount: 0
776
+ });
777
+ // Wrap postMessage so a DataCloneError cleans up the pending entry
778
+ try {
779
+ this.postMessageToExtension(request);
780
+ }
781
+ catch (cloneErr) {
782
+ clearTimeout(timeoutHandle);
647
783
  this.pendingRequests.delete(requestId);
648
- reject(new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, `Request timeout after ${timeout}ms`, {
649
- method,
650
- params,
651
- requestId,
652
- retryCount: pending.retryCount,
653
- extensionState: this.getExtensionDiagnostics()
654
- }));
784
+ reject(new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Request params are not serializable', { method, requestId }));
655
785
  }
656
- }, timeout);
657
- // Store request handlers
658
- this.pendingRequests.set(requestId, {
659
- resolve,
660
- reject,
661
- timeout: timeoutHandle,
662
- retryCount: 0
663
786
  });
664
- // Send message to extension via content script bridge
665
- this.postMessageToExtension(request);
666
- });
667
- }, maxRetries, 1000);
668
- }
669
- /**
670
- * Setup message listener for responses from extension
671
- */
672
- setupMessageListener() {
673
- if (typeof window === 'undefined') {
674
- return; // Not in browser environment
675
- }
676
- const allowedOrigin = window.location.origin;
677
- // Store allowed origins for parent frame communication
678
- // Desktop (Tauri): tauri://localhost or https://tauri.localhost
679
- // Mobile (Expo): about:blank or custom scheme
680
- const trustedParentOrigins = new Set([
681
- allowedOrigin,
682
- 'tauri://localhost',
683
- 'https://tauri.localhost',
684
- 'http://localhost',
685
- 'https://localhost',
686
- ]);
687
- // Allow dApps to register additional trusted origins
688
- if (this.trustedOrigins) {
689
- for (const origin of this.trustedOrigins) {
690
- trustedParentOrigins.add(origin);
691
- }
787
+ }, maxRetries, 1000);
692
788
  }
693
- this.messageListener = (event) => {
694
- // Strict origin validation
695
- const isFromSameOrigin = event.origin === allowedOrigin;
696
- const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
697
- const isFromTrustedParent = event.source === window.parent
698
- && window.parent !== window
699
- && (trustedParentOrigins.has(event.origin) || isLocalhost);
700
- if (!isFromSameOrigin && !isFromTrustedParent) {
701
- return;
789
+ finally {
790
+ if (isInteractive) {
791
+ this._interactiveInFlight = false;
702
792
  }
703
- // Only accept from same window (extension content script) or trusted parent frame
704
- if (event.source !== window && event.source !== window.parent) {
705
- return;
706
- }
707
- // Verify message structure
708
- if (!event.data || event.data.source !== '0xio-sdk-bridge') {
709
- return;
710
- }
711
- // Validate response has a pending request (prevents forged responses)
712
- if (event.data.response) {
713
- const response = event.data.response;
714
- if (response && response.id && this.pendingRequests.has(response.id)) {
715
- this.handleExtensionResponse(response);
793
+ }
794
+ }
795
+ setupMessageListener() {
796
+ if (typeof window === 'undefined')
797
+ return;
798
+ this._adapterTeardown = this.adapter.listen((msg) => {
799
+ if (msg.requestId !== undefined) {
800
+ // response — map AdapterIncomingMessage → ExtensionResponse shape
801
+ if (this.pendingRequests.has(msg.requestId)) {
802
+ this.handleExtensionResponse({
803
+ id: msg.requestId,
804
+ success: msg.success ?? false,
805
+ data: msg.data,
806
+ error: msg.error,
807
+ timestamp: Date.now(),
808
+ });
716
809
  }
717
810
  }
718
- else if (event.data.event) {
719
- this.handleExtensionEvent(event.data.event);
811
+ else if (msg.eventType) {
812
+ this.handleExtensionEvent({ type: msg.eventType, data: msg.eventData });
720
813
  }
721
- };
722
- window.addEventListener('message', this.messageListener);
723
- this.logger.log('Message listener setup complete');
814
+ }, { trustedParentOrigins: this.trustedOrigins });
815
+ this.logger.log(`Message listener setup complete (adapter: ${this.adapter.name})`);
724
816
  }
725
- /**
726
- * Handle extension event
727
- */
728
817
  handleExtensionEvent(event) {
818
+ if (!event?.type || !ExtensionCommunicator.VALID_EVENT_TYPES.has(event.type)) {
819
+ this.logger.warn(`Received unknown event type from bridge: ${event?.type}`);
820
+ return;
821
+ }
729
822
  this.logger.log('Received extension event:', event.type);
730
- // Forward the event to listeners
731
823
  this.emit(event.type, event.data);
732
824
  }
733
- /**
734
- * Handle response from extension
735
- */
736
825
  handleExtensionResponse(response) {
737
826
  this.logger.log(`Received response:`, { id: response.id, success: response.success });
738
827
  const pending = this.pendingRequests.get(response.id);
@@ -740,192 +829,131 @@
740
829
  this.logger.warn(`Received response for unknown request ID: ${response.id}`);
741
830
  return;
742
831
  }
743
- // Clear timeout and remove from pending
744
832
  clearTimeout(pending.timeout);
745
833
  this.pendingRequests.delete(response.id);
746
- // Handle response
747
- if (response.success) {
834
+ // Require strict boolean true — "false" string or other truthy values are failures
835
+ if (response.success === true) {
748
836
  pending.resolve(response.data);
749
837
  }
750
838
  else {
751
839
  const error = response.error;
752
840
  if (error) {
753
- const enhancedError = new ZeroXIOWalletError(error.code, error.message, {
754
- ...error.details,
755
- requestId: response.id,
756
- retryCount: pending.retryCount,
757
- timestamp: Date.now(),
758
- extensionState: this.getExtensionDiagnostics()
759
- });
841
+ // Handle both object {code, message} and legacy plain-string error formats
842
+ const isObj = error !== null && typeof error === 'object';
843
+ const code = isObj && error.code ? error.code : exports.ErrorCode.UNKNOWN_ERROR;
844
+ const message = isObj
845
+ ? (error.message ?? 'Unknown error')
846
+ : (typeof error === 'string' ? error : 'Unknown error');
847
+ const enhancedError = new ZeroXIOWalletError(code, message,
848
+ // Redact bridge-supplied error details; only keep non-sensitive metadata
849
+ { requestId: response.id, retryCount: pending.retryCount, timestamp: Date.now() });
760
850
  pending.reject(enhancedError);
761
851
  }
762
852
  else {
763
- pending.reject(new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Unknown error occurred', {
764
- requestId: response.id,
765
- retryCount: pending.retryCount,
766
- extensionState: this.getExtensionDiagnostics()
767
- }));
853
+ pending.reject(new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Unknown error occurred', { requestId: response.id, retryCount: pending.retryCount }));
768
854
  }
769
855
  }
770
856
  }
771
- /**
772
- * Post message to extension via content script
773
- */
774
857
  postMessageToExtension(request) {
775
- const msg = { source: '0xio-sdk-request', request };
776
- // Post to same window (extension content script picks it up)
777
- window.postMessage(msg, window.location.origin);
778
- // Also post to parent frame if in an iframe (desktop/mobile bridge)
779
- if (window.parent !== window) {
780
- // Use the parent origin we learned from the walletReady signal, or wildcard
781
- // Response validation is done by request ID matching, not origin
782
- const parentOrigin = this._parentOrigin || '*';
783
- try {
784
- window.parent.postMessage(msg, parentOrigin);
785
- }
786
- catch {
787
- // Fallback to wildcard if specific origin fails
788
- try {
789
- window.parent.postMessage(msg, '*');
790
- }
791
- catch { }
858
+ this.adapter.postRequest(request);
859
+ // Parent bridge (iframe/desktop mode) only when a trusted origin is established.
860
+ // Sending with '*' would leak method + params to any intercepting frame.
861
+ if (window.parent !== window && this._parentOrigin) {
862
+ if (this.adapter.postRequestToParent) {
863
+ this.adapter.postRequestToParent(request, this._parentOrigin);
792
864
  }
793
865
  }
794
866
  }
795
- /**
796
- * Check if we're in a context that can communicate with extension
797
- */
798
867
  hasExtensionContext() {
799
868
  return typeof window !== 'undefined' &&
800
869
  typeof window.postMessage === 'function';
801
870
  }
802
- /**
803
- * Check and enforce rate limits to prevent denial-of-service attacks
804
- *
805
- * @private
806
- * @throws {ZeroXIOWalletError} RATE_LIMIT_EXCEEDED if limits are exceeded
807
- *
808
- * @description
809
- * Implements two-tier rate limiting:
810
- * 1. Concurrent requests: Maximum 50 pending requests at once
811
- * 2. Request frequency: Maximum 20 requests per second
812
- *
813
- * Rate limiting protects both the SDK and extension from:
814
- * - Accidental infinite loops in dApp code
815
- * - Malicious DoS attacks
816
- * - Resource exhaustion
817
- *
818
- * @security Critical security function - enforces resource limits
819
- */
820
871
  checkRateLimit() {
821
872
  const now = Date.now();
822
- // ✅ SECURITY: Check concurrent request limit
823
873
  if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
824
874
  throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
825
875
  }
826
- // SECURITY: Check requests per time window
876
+ // Trim expired timestamps cap array size to prevent unbounded growth in idle tabs
827
877
  this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
878
+ if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
879
+ this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
880
+ }
828
881
  if (this.requestTimestamps.length >= this.MAX_REQUESTS_PER_WINDOW) {
829
882
  throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, `Too many requests per second (max: ${this.MAX_REQUESTS_PER_WINDOW} per ${this.RATE_LIMIT_WINDOW}ms)`);
830
883
  }
831
884
  this.requestTimestamps.push(now);
832
885
  }
833
- /**
834
- * Generate cryptographically secure unique request ID
835
- *
836
- * @private
837
- * @returns {string} A unique, unpredictable request identifier
838
- *
839
- * @description
840
- * Uses Web Crypto API for secure random ID generation:
841
- * 1. Primary: crypto.randomUUID() - UUID v4 format
842
- * 2. Fallback: crypto.getRandomValues() - 128-bit random hex
843
- * 3. Last resort: timestamp + counter (logs warning)
844
- *
845
- * Security importance:
846
- * - Prevents request ID prediction attacks
847
- * - Mitigates replay attacks
848
- * - Makes session hijacking more difficult
849
- *
850
- * @security Critical - IDs must be cryptographically unpredictable
851
- */
852
886
  generateRequestId() {
853
- // ✅ SECURITY: Use crypto.randomUUID() for secure, unpredictable IDs
854
887
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
855
888
  return `0xio-sdk-${crypto.randomUUID()}`;
856
889
  }
857
- // Fallback to crypto.getRandomValues for older browsers
858
890
  if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
859
891
  const array = new Uint8Array(16);
860
892
  crypto.getRandomValues(array);
861
893
  const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
862
894
  return `0xio-sdk-${hex}`;
863
895
  }
864
- // Last resort fallback (not recommended for production)
865
- this.logger.warn('Crypto API not available, using less secure ID generation');
866
- return `0xio-sdk-${++this.requestId}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
896
+ // Crypto API unavailable throw rather than produce a guessable ID that
897
+ // could allow response spoofing via a known requestId.
898
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
867
899
  }
868
- /**
869
- * Start continuous extension detection
870
- */
871
900
  startExtensionDetection() {
872
901
  if (typeof window === 'undefined')
873
902
  return;
874
- // Listen for extension ready event (instant detection)
875
- window.addEventListener('0xioWalletReady', () => {
876
- this.logger.log('Received 0xioWalletReady event');
877
- this.isExtensionAvailableState = true;
878
- });
879
- // Also listen for legacy event names
880
- window.addEventListener('wallet0xioReady', () => {
881
- this.logger.log('Received wallet0xioReady event');
882
- this.isExtensionAvailableState = true;
883
- });
884
- window.addEventListener('octraWalletReady', () => {
885
- this.logger.log('Received octraWalletReady event');
886
- this.isExtensionAvailableState = true;
887
- });
888
- // Listen for desktop/mobile bridge walletReady via postMessage (with origin validation)
889
- window.addEventListener('message', (event) => {
890
- if (event.data?.source === '0xio-sdk-bridge' && event.data?.event?.type === 'walletReady') {
891
- const isSameOrigin = event.origin === window.location.origin;
892
- const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
893
- const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
894
- const isTrusted = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhost;
895
- if (isSameOrigin || isTrusted) {
896
- this.logger.log('Received walletReady via postMessage from trusted origin');
897
- this.isExtensionAvailableState = true;
898
- // Capture parent origin for cross-origin reply targeting
899
- if (event.data.parentOrigin) {
900
- this._parentOrigin = event.data.parentOrigin;
901
- }
902
- else if (event.origin && event.origin !== 'null') {
903
- this._parentOrigin = event.origin;
904
- }
903
+ // Adapter-provided wallet-ready events (e.g. '0xioWalletReady', 'exampleWalletReady')
904
+ if (this.adapter.listenForReady) {
905
+ this._adapterReadyTeardown = this.adapter.listenForReady(() => {
906
+ this.logger.log(`Received wallet-ready event (adapter: ${this.adapter.name})`);
907
+ this.isExtensionAvailableState = true;
908
+ });
909
+ }
910
+ // walletReady via postMessage (desktop/mobile iframe bridge) — store ref for cleanup
911
+ this._walletReadyMessageListener = (event) => {
912
+ if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
913
+ return;
914
+ }
915
+ const isSameOrigin = event.origin === window.location.origin;
916
+ const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
917
+ const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
918
+ const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhost;
919
+ // In iframe mode, only trust the actual parent window
920
+ const inIframe = window.parent !== window;
921
+ if (inIframe && event.source !== window.parent) {
922
+ this.logger.warn(`Ignored walletReady from non-parent source in iframe mode`);
923
+ return;
924
+ }
925
+ if (isSameOrigin || isTrustedOrigin) {
926
+ this.logger.log('Received walletReady via postMessage from trusted origin');
927
+ this.isExtensionAvailableState = true;
928
+ this._parentTrusted = true; // Mark parent-bridge readiness separately
929
+ if (event.data.parentOrigin) {
930
+ this._parentOrigin = event.data.parentOrigin;
905
931
  }
906
- else {
907
- this.logger.warn(`Ignored walletReady from untrusted origin: ${event.origin}`);
932
+ else if (event.origin && event.origin !== 'null') {
933
+ this._parentOrigin = event.origin;
908
934
  }
909
935
  }
910
- });
911
- // If running inside a frame, do NOT auto-trust the parent.
912
- // Wait for a walletReady message from a trusted origin instead.
936
+ else {
937
+ this.logger.warn(`Ignored walletReady from untrusted origin: ${event.origin}`);
938
+ }
939
+ };
940
+ window.addEventListener('message', this._walletReadyMessageListener);
913
941
  if (window.parent !== window) {
914
942
  this.logger.log('Running inside a frame — waiting for trusted walletReady signal');
915
943
  }
916
- // Initial check (in case extension was already injected)
917
944
  this.checkExtensionAvailability();
918
- // Set up periodic checks as fallback
919
945
  this.extensionDetectionInterval = setInterval(() => {
920
946
  this.checkExtensionAvailability();
921
947
  }, 2000);
922
948
  }
923
- /**
924
- * Check if extension is currently available
925
- */
926
949
  checkExtensionAvailability() {
950
+ // If parent-bridge readiness was established via a trusted walletReady handshake,
951
+ // preserve that state — the polling fallback (detectExtensionSignals) does not
952
+ // consider the iframe parent signal and would incorrectly flip state back
953
+ if (this._parentTrusted) {
954
+ return;
955
+ }
927
956
  const wasAvailable = this.isExtensionAvailableState;
928
- // Basic checks for extension context
929
957
  this.isExtensionAvailableState = this.hasExtensionContext() && this.detectExtensionSignals();
930
958
  if (!wasAvailable && this.isExtensionAvailableState) {
931
959
  this.logger.log('Extension became available');
@@ -934,25 +962,9 @@
934
962
  this.logger.warn('Extension became unavailable');
935
963
  }
936
964
  }
937
- /**
938
- * Detect extension signals/indicators
939
- */
940
965
  detectExtensionSignals() {
941
- if (typeof window === 'undefined')
942
- return false;
943
- // Check for extension-injected indicators
944
- const win = window;
945
- // Look for extension-injected globals (matches injected.ts)
946
- return !!(win.wallet0xio ||
947
- win.ZeroXIOWallet ||
948
- win.octraWallet ||
949
- (win.chrome?.runtime?.id) ||
950
- document.querySelector('meta[name="0xio-dapp"]') ||
951
- document.querySelector('[data-0xio-sdk-bridge]'));
966
+ return this.adapter.detect();
952
967
  }
953
- /**
954
- * Wait for extension to become available
955
- */
956
968
  async waitForExtensionAvailability(timeoutMs) {
957
969
  if (this.isExtensionAvailableState) {
958
970
  return true;
@@ -960,14 +972,11 @@
960
972
  return new Promise((resolve) => {
961
973
  let resolved = false;
962
974
  const startTime = Date.now();
963
- // Cleanup function
975
+ let adapterReadyTeardown = null;
964
976
  const cleanup = () => {
965
977
  clearInterval(checkInterval);
966
- window.removeEventListener('0xioWalletReady', onReady);
967
- window.removeEventListener('wallet0xioReady', onReady);
968
- window.removeEventListener('octraWalletReady', onReady);
978
+ adapterReadyTeardown?.();
969
979
  };
970
- // Event handler for instant resolution
971
980
  const onReady = () => {
972
981
  if (resolved)
973
982
  return;
@@ -976,11 +985,10 @@
976
985
  cleanup();
977
986
  resolve(true);
978
987
  };
979
- // Listen for extension ready events
980
- window.addEventListener('0xioWalletReady', onReady);
981
- window.addEventListener('wallet0xioReady', onReady);
982
- window.addEventListener('octraWalletReady', onReady);
983
- // Polling fallback with shorter interval
988
+ // Use adapter's listenForReady for fast resolution; polling is the fallback
989
+ if (this.adapter.listenForReady) {
990
+ adapterReadyTeardown = this.adapter.listenForReady(onReady);
991
+ }
984
992
  const checkInterval = setInterval(() => {
985
993
  if (resolved)
986
994
  return;
@@ -998,9 +1006,6 @@
998
1006
  }, 100);
999
1007
  });
1000
1008
  }
1001
- /**
1002
- * Get browser diagnostics for error reporting
1003
- */
1004
1009
  getBrowserDiagnostics() {
1005
1010
  if (typeof window === 'undefined') {
1006
1011
  return { environment: 'non-browser' };
@@ -1012,29 +1017,21 @@
1012
1017
  hasChromeRuntime: !!(win.chrome?.runtime),
1013
1018
  hasPostMessage: typeof window.postMessage === 'function',
1014
1019
  origin: window.location?.origin,
1015
- extensionDetection: {
1016
- hasWallet0xio: !!win.wallet0xio,
1017
- hasZeroXIOWallet: !!win.ZeroXIOWallet,
1018
- hasOctraWallet: !!win.octraWallet,
1019
- hasChromeRuntimeId: !!(win.chrome?.runtime?.id),
1020
- hasSdkBridge: !!document.querySelector('[data-0xio-sdk-bridge]')
1021
- }
1022
1020
  };
1023
1021
  }
1024
- /**
1025
- * Get extension state diagnostics
1026
- */
1027
1022
  getExtensionDiagnostics() {
1028
1023
  return {
1029
1024
  initialized: this.isInitialized,
1030
1025
  available: this.isExtensionAvailableState,
1026
+ parentTrusted: this._parentTrusted,
1031
1027
  pendingRequests: this.pendingRequests.size,
1032
1028
  hasExtensionContext: this.hasExtensionContext(),
1033
- browserDiagnostics: this.getBrowserDiagnostics()
1034
1029
  };
1035
1030
  }
1036
1031
  /**
1037
- * Cleanup pending requests
1032
+ * Clean up SDK resources.
1033
+ * After cleanup() the instance is terminal — do not call initialize() again.
1034
+ * Construct a new instance instead.
1038
1035
  */
1039
1036
  cleanup() {
1040
1037
  if (this.extensionDetectionInterval) {
@@ -1048,18 +1045,22 @@
1048
1045
  this.pendingRequests.clear();
1049
1046
  this.isInitialized = false;
1050
1047
  this.isExtensionAvailableState = false;
1051
- // Remove message listener to prevent accumulation
1052
- if (this.messageListener) {
1053
- window.removeEventListener('message', this.messageListener);
1054
- this.messageListener = null;
1048
+ this._parentTrusted = false;
1049
+ this._parentOrigin = null;
1050
+ this._interactiveInFlight = false;
1051
+ // Tear down adapter listeners (response/event + wallet-ready)
1052
+ this._adapterTeardown?.();
1053
+ this._adapterTeardown = null;
1054
+ this._adapterReadyTeardown?.();
1055
+ this._adapterReadyTeardown = null;
1056
+ // Remove walletReady postMessage listener (parent iframe bridge)
1057
+ if (typeof window !== 'undefined' && this._walletReadyMessageListener) {
1058
+ window.removeEventListener('message', this._walletReadyMessageListener);
1059
+ this._walletReadyMessageListener = null;
1055
1060
  }
1056
- // Call parent cleanup
1057
1061
  this.removeAllListeners();
1058
1062
  this.logger.log('Communication cleanup complete');
1059
1063
  }
1060
- /**
1061
- * Get debug information
1062
- */
1063
1064
  getDebugInfo() {
1064
1065
  return {
1065
1066
  initialized: this.isInitialized,
@@ -1070,23 +1071,30 @@
1070
1071
  };
1071
1072
  }
1072
1073
  }
1073
- // Methods that trigger user-facing popups — NEVER retry these
1074
+ // Methods that trigger user-facing popups — NEVER retry these.
1074
1075
  // Retrying sends a second request while the first popup is still open,
1075
- // causing double popups where the second tx fails (stale nonce/state)
1076
+ // causing double popups where the second tx fails (stale nonce/state).
1076
1077
  ExtensionCommunicator.NO_RETRY_METHODS = new Set([
1077
1078
  'connect', 'send_transaction', 'call_contract', 'signMessage',
1078
1079
  'send_private_transfer', 'claim_private_transfer',
1079
1080
  'encrypt_balance', 'decrypt_balance',
1080
1081
  ]);
1082
+ ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
1083
+ // only forward known event types
1084
+ ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
1085
+ 'connect', 'disconnect', 'accountChanged', 'balanceChanged',
1086
+ 'networkChanged', 'transactionConfirmed', 'error',
1087
+ 'extensionLocked', 'extensionUnlocked'
1088
+ ]);
1081
1089
 
1082
1090
  /**
1083
1091
  * Network configuration for 0xio SDK
1084
1092
  */
1085
- const NETWORKS = {
1093
+ const _NETWORKS = {
1086
1094
  'mainnet': {
1087
1095
  id: 'mainnet',
1088
1096
  name: 'Octra Mainnet',
1089
- rpcUrl: 'http://46.101.86.250:8080',
1097
+ rpcUrl: 'https://octra.network',
1090
1098
  explorerUrl: 'https://lite.octrascan.io/tx.html?hash=',
1091
1099
  explorerAddressUrl: 'https://lite.octrascan.io/address.html?addr=',
1092
1100
  indexerUrl: 'https://lite.octrascan.io',
@@ -1117,49 +1125,106 @@
1117
1125
  isTestnet: false
1118
1126
  }
1119
1127
  };
1128
+ /**
1129
+ * Immutable public copy of the built-in network table.
1130
+ * Modifications to returned objects do not affect SDK-internal state.
1131
+ */
1132
+ const NETWORKS = Object.freeze(Object.fromEntries(Object.entries(_NETWORKS).map(([k, v]) => [k, Object.freeze({ ...v })])));
1120
1133
  const DEFAULT_NETWORK_ID = 'mainnet';
1121
1134
  /**
1122
- * Get network configuration by ID
1135
+ * Get network configuration by ID.
1136
+ * Returns a frozen copy — callers cannot mutate SDK-internal state.
1123
1137
  */
1124
1138
  function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
1125
- const network = NETWORKS[networkId];
1126
- if (!network) {
1139
+ if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
1127
1140
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, `Unknown network ID: ${networkId}`);
1128
1141
  }
1129
- return network;
1142
+ return Object.freeze({ ..._NETWORKS[networkId] });
1130
1143
  }
1131
1144
  /**
1132
- * Get all available networks
1145
+ * Get all available networks.
1146
+ * Returns frozen copies — callers cannot mutate SDK-internal state.
1133
1147
  */
1134
1148
  function getAllNetworks() {
1135
- return Object.values(NETWORKS);
1149
+ return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
1136
1150
  }
1137
1151
  /**
1138
- * Check if network ID is valid
1152
+ * Check if network ID is valid (own property check, prevents prototype pollution).
1139
1153
  */
1140
1154
  function isValidNetworkId(networkId) {
1141
- return networkId in NETWORKS;
1155
+ return typeof networkId === 'string' && Object.prototype.hasOwnProperty.call(_NETWORKS, networkId);
1156
+ }
1157
+ /**
1158
+ * Validate a NetworkInfo shape from an untrusted source (bridge response).
1159
+ * Returns a frozen copy if valid, null otherwise.
1160
+ */
1161
+ function validateNetworkInfo(raw) {
1162
+ if (!raw || typeof raw !== 'object')
1163
+ return null;
1164
+ if (typeof raw.id !== 'string' || !raw.id)
1165
+ return null;
1166
+ if (typeof raw.name !== 'string')
1167
+ return null;
1168
+ if (typeof raw.rpcUrl !== 'string' || (!raw.rpcUrl && raw.id !== 'custom'))
1169
+ return null;
1170
+ if (typeof raw.supportsPrivacy !== 'boolean')
1171
+ return null;
1172
+ return Object.freeze({
1173
+ id: raw.id,
1174
+ name: raw.name,
1175
+ rpcUrl: raw.rpcUrl,
1176
+ explorerUrl: typeof raw.explorerUrl === 'string' ? raw.explorerUrl : undefined,
1177
+ explorerAddressUrl: typeof raw.explorerAddressUrl === 'string' ? raw.explorerAddressUrl : undefined,
1178
+ indexerUrl: typeof raw.indexerUrl === 'string' ? raw.indexerUrl : undefined,
1179
+ supportsPrivacy: raw.supportsPrivacy,
1180
+ color: typeof raw.color === 'string' ? raw.color : '#64748b',
1181
+ isTestnet: typeof raw.isTestnet === 'boolean' ? raw.isTestnet : false,
1182
+ });
1142
1183
  }
1143
1184
 
1144
1185
  /**
1145
1186
  * SDK Configuration
1146
1187
  */
1147
1188
  /**
1148
- * Default balance structure
1189
+ * Default balance structure.
1190
+ * Accepts a numeric total or undefined — never pass a Balance object here.
1149
1191
  */
1150
1192
  function createDefaultBalance(total = 0) {
1193
+ const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
1151
1194
  return {
1152
- total,
1153
- public: total,
1195
+ total: safeTotal,
1196
+ public: safeTotal,
1154
1197
  private: 0,
1155
1198
  currency: 'OCT'
1156
1199
  };
1157
1200
  }
1201
+ /**
1202
+ * Validate and normalise a Balance from an untrusted source (bridge response).
1203
+ * Returns null if the payload cannot be coerced into a valid Balance.
1204
+ */
1205
+ function validateBalance(raw) {
1206
+ if (raw === null || raw === undefined)
1207
+ return null;
1208
+ // If it's already a Balance-shaped object, extract numeric fields
1209
+ // Use Number() not parseFloat() — parseFloat('10abc') silently returns 10
1210
+ const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
1211
+ const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
1212
+ if (!Number.isFinite(pub) || pub < 0)
1213
+ return null;
1214
+ if (!Number.isFinite(priv) || priv < 0)
1215
+ return null;
1216
+ return {
1217
+ public: pub,
1218
+ private: priv,
1219
+ total: pub + priv,
1220
+ currency: 'OCT'
1221
+ };
1222
+ }
1158
1223
  /**
1159
1224
  * SDK Configuration constants
1160
1225
  */
1161
1226
  const SDK_CONFIG = {
1162
- version: '2.5.0',
1227
+ version: '2.7.0',
1163
1228
  defaultNetworkId: DEFAULT_NETWORK_ID,
1164
1229
  communicationTimeout: 30000, // 30 seconds
1165
1230
  retryAttempts: 3,
@@ -1181,6 +1246,9 @@
1181
1246
  super(config.debug);
1182
1247
  this.connectionInfo = { isConnected: false };
1183
1248
  this.isInitialized = false;
1249
+ this._initPromise = null;
1250
+ // session version — stale write detection
1251
+ this._sessionVersion = 0;
1184
1252
  this.config = {
1185
1253
  ...config,
1186
1254
  appVersion: config.appVersion || '1.0.0',
@@ -1188,7 +1256,7 @@
1188
1256
  debug: config.debug || false
1189
1257
  };
1190
1258
  this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
1191
- this.communicator = new ExtensionCommunicator(this.config.debug);
1259
+ this.communicator = new ExtensionCommunicator(this.config.debug, [], this.config.adapter);
1192
1260
  this.logger.log('Wallet instance created with config:', this.config);
1193
1261
  }
1194
1262
  // ===================
@@ -1202,35 +1270,45 @@
1202
1270
  if (this.isInitialized) {
1203
1271
  return true;
1204
1272
  }
1205
- try {
1206
- // Initialize extension communication
1207
- const communicationReady = await this.communicator.initialize();
1208
- if (!communicationReady) {
1209
- throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
1210
- }
1211
- // Register this DApp with the extension
1212
- await this.communicator.sendRequest('register_dapp', {
1213
- appName: this.config.appName,
1214
- appDescription: this.config.appDescription,
1215
- appVersion: this.config.appVersion,
1216
- appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
1217
- appIcon: this.config.appIcon,
1218
- requiredPermissions: this.config.requiredPermissions,
1219
- networkId: this.config.networkId
1220
- });
1221
- // Setup event forwarding from extension
1222
- this.setupExtensionEventListeners();
1223
- this.isInitialized = true;
1224
- this.logger.log('SDK initialized successfully');
1225
- return true;
1273
+ // single-flight init
1274
+ if (this._initPromise) {
1275
+ return this._initPromise;
1226
1276
  }
1227
- catch (error) {
1228
- this.logger.error('Failed to initialize:', error);
1229
- if (error instanceof ZeroXIOWalletError) {
1230
- throw error;
1277
+ this._initPromise = (async () => {
1278
+ try {
1279
+ // Initialize extension communication
1280
+ const communicationReady = await this.communicator.initialize();
1281
+ if (!communicationReady) {
1282
+ throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
1283
+ }
1284
+ // Register this DApp with the extension
1285
+ await this.communicator.sendRequest('register_dapp', {
1286
+ appName: this.config.appName,
1287
+ appDescription: this.config.appDescription,
1288
+ appVersion: this.config.appVersion,
1289
+ appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
1290
+ appIcon: this.config.appIcon,
1291
+ requiredPermissions: this.config.requiredPermissions,
1292
+ networkId: this.config.networkId
1293
+ });
1294
+ // Setup event forwarding from extension
1295
+ this.setupExtensionEventListeners();
1296
+ this.isInitialized = true;
1297
+ this.logger.log('SDK initialized successfully');
1298
+ return true;
1231
1299
  }
1232
- throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Failed to initialize SDK', error);
1233
- }
1300
+ catch (error) {
1301
+ this.logger.error('Failed to initialize:', error);
1302
+ if (error instanceof ZeroXIOWalletError) {
1303
+ throw error;
1304
+ }
1305
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Failed to initialize SDK', error);
1306
+ }
1307
+ finally {
1308
+ this._initPromise = null;
1309
+ }
1310
+ })();
1311
+ return this._initPromise;
1234
1312
  }
1235
1313
  /**
1236
1314
  * Check if SDK is initialized
@@ -1248,31 +1326,53 @@
1248
1326
  this.ensureInitialized();
1249
1327
  try {
1250
1328
  this.logger.log('Attempting to connect with options:', options);
1329
+ // filter to declared perms only
1330
+ const declaredPermissions = this.config.requiredPermissions || [];
1331
+ const requestPermissions = options.requestPermissions
1332
+ ? options.requestPermissions.filter(p => declaredPermissions.includes(p))
1333
+ : declaredPermissions;
1251
1334
  const result = await this.communicator.sendRequest('connect', {
1252
- requestPermissions: options.requestPermissions || this.config.requiredPermissions,
1335
+ requestPermissions,
1253
1336
  networkId: options.networkId || this.config.networkId
1254
1337
  });
1255
- // Use networkInfo from extension response, fallback to config-based
1256
- const networkInfo = result.networkInfo || getNetworkConfig(result.networkId || this.config.networkId);
1257
- // Update connection info
1338
+ // verify pubkey→addr binding
1339
+ if (result.publicKey && result.address) {
1340
+ try {
1341
+ const derived = await deriveOctraAddress(result.publicKey);
1342
+ if (derived !== result.address) {
1343
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed — the reported public key does not derive to the reported address');
1344
+ }
1345
+ }
1346
+ catch (e) {
1347
+ if (e instanceof ZeroXIOWalletError)
1348
+ throw e;
1349
+ this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
1350
+ }
1351
+ }
1352
+ // Use networkInfo from extension response — validate before caching
1353
+ const networkInfo = validateNetworkInfo(result.networkInfo)
1354
+ ?? getNetworkConfig(result.networkId || this.config.networkId);
1355
+ const permissions = result.permissions || [];
1356
+ // Update connection info — including permissions
1258
1357
  this.connectionInfo = {
1259
1358
  isConnected: true,
1260
1359
  address: result.address,
1261
1360
  publicKey: result.publicKey,
1262
1361
  balance: result.balance,
1263
1362
  networkInfo,
1264
- connectedAt: Date.now()
1363
+ connectedAt: Date.now(),
1364
+ permissions
1265
1365
  };
1266
1366
  const connectEvent = {
1267
1367
  address: result.address,
1268
1368
  publicKey: result.publicKey,
1269
1369
  balance: result.balance,
1270
1370
  networkInfo,
1271
- permissions: result.permissions || []
1371
+ permissions
1272
1372
  };
1273
1373
  // Emit connect event
1274
1374
  this.emit('connect', connectEvent);
1275
- this.logger.log('Connected successfully:', connectEvent);
1375
+ this.logger.log('Connected successfully:', { address: connectEvent.address, network: networkInfo.id });
1276
1376
  return connectEvent;
1277
1377
  }
1278
1378
  catch (error) {
@@ -1290,6 +1390,7 @@
1290
1390
  this.ensureInitialized();
1291
1391
  try {
1292
1392
  await this.communicator.sendRequest('disconnect');
1393
+ ++this._sessionVersion;
1293
1394
  this.connectionInfo = { isConnected: false };
1294
1395
  const disconnectEvent = {
1295
1396
  reason: 'user_action'
@@ -1320,30 +1421,55 @@
1320
1421
  async getConnectionStatus() {
1321
1422
  this.ensureInitialized();
1322
1423
  try {
1424
+ const sv = this._sessionVersion;
1323
1425
  const result = await this.communicator.sendRequest('getConnectionStatus');
1426
+ // skip if session changed mid-flight
1427
+ if (this._sessionVersion !== sv)
1428
+ return { ...this.connectionInfo };
1324
1429
  if (result.isConnected && result.address) {
1325
- // Update internal state if we discover an existing connection
1326
- const balanceInfo = createDefaultBalance(result.balance);
1327
- // Use network from extension response (actual active network), fallback to config
1328
- const activeNetworkId = result.networkId || this.config.networkId;
1329
- const networkInfo = getNetworkConfig(activeNetworkId);
1430
+ // verify pubkey→addr binding
1431
+ if (result.publicKey) {
1432
+ try {
1433
+ const derived = await deriveOctraAddress(result.publicKey);
1434
+ if (derived !== result.address) {
1435
+ this.logger.warn('Address-key binding mismatch on session restore — ignoring stale session');
1436
+ this.connectionInfo = { isConnected: false };
1437
+ return { ...this.connectionInfo };
1438
+ }
1439
+ }
1440
+ catch (e) {
1441
+ this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
1442
+ }
1443
+ }
1444
+ // validate untrusted balance/networkInfo before caching
1445
+ const balanceInfo = validateBalance(result.balance) ?? createDefaultBalance();
1446
+ const networkInfo = validateNetworkInfo(result.networkInfo)
1447
+ ?? getNetworkConfig(result.networkId || this.config.networkId);
1448
+ const wasConnected = this.connectionInfo.isConnected;
1449
+ const permissions = result.permissions || [];
1450
+ // preserve existing connectedAt
1451
+ const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
1330
1452
  this.connectionInfo = {
1331
1453
  isConnected: true,
1332
1454
  address: result.address,
1333
1455
  publicKey: result.publicKey,
1334
1456
  balance: balanceInfo,
1335
1457
  networkInfo,
1336
- connectedAt: result.connectedAt || Date.now()
1337
- };
1338
- this.logger.log('Discovered existing connection:', this.connectionInfo);
1339
- // Emit connect event to notify the wrapper
1340
- const connectEvent = {
1341
- address: result.address,
1342
- balance: balanceInfo,
1343
- networkInfo,
1344
- permissions: result.permissions || []
1458
+ connectedAt,
1459
+ permissions
1345
1460
  };
1346
- this.emit('connect', connectEvent);
1461
+ this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
1462
+ // only emit on disconnected→connected transition
1463
+ if (!wasConnected) {
1464
+ const connectEvent = {
1465
+ address: result.address,
1466
+ publicKey: result.publicKey,
1467
+ balance: balanceInfo,
1468
+ networkInfo,
1469
+ permissions
1470
+ };
1471
+ this.emit('connect', connectEvent);
1472
+ }
1347
1473
  }
1348
1474
  else {
1349
1475
  // No existing connection
@@ -1363,10 +1489,14 @@
1363
1489
  * The extension broadcasts 'networkChanged' event to all connected dApps.
1364
1490
  */
1365
1491
  async switchNetwork(networkId) {
1366
- this.ensureInitialized();
1492
+ this.ensureConnected();
1367
1493
  try {
1494
+ const sv = this._sessionVersion;
1368
1495
  const result = await this.communicator.sendRequest('switch_network', { networkId });
1369
1496
  this.logger.log(`Network switch result:`, result);
1497
+ // skip if session changed mid-flight
1498
+ if (this._sessionVersion !== sv)
1499
+ return { network: result.network || networkId, switched: result.switched ?? false };
1370
1500
  if (result.switched) {
1371
1501
  // Update internal state
1372
1502
  const networkInfo = getNetworkConfig(networkId);
@@ -1409,22 +1539,28 @@
1409
1539
  // Fetch balance from extension (bypasses CORS, has access to private balance)
1410
1540
  let publicBalance = 0;
1411
1541
  let privateBalance = 0;
1542
+ const sv = this._sessionVersion;
1412
1543
  const extResult = await this.communicator.sendRequest('getBalance', { forceRefresh });
1413
1544
  publicBalance = parseFloat(extResult.balance || '0');
1414
1545
  privateBalance = parseFloat(extResult.privateBalance || '0');
1415
- this.logger.log('Balance fetched from extension:', { public: publicBalance, private: privateBalance });
1546
+ this.logger.log('Balance fetched from extension:', { public: publicBalance });
1416
1547
  const result = {
1417
1548
  public: publicBalance,
1418
1549
  private: privateBalance,
1419
1550
  total: publicBalance + privateBalance,
1420
1551
  currency: 'OCT'
1421
1552
  };
1553
+ // skip if session changed mid-flight
1554
+ if (this._sessionVersion !== sv)
1555
+ return result;
1422
1556
  // Update cached balance
1423
1557
  if (this.connectionInfo.balance) {
1424
1558
  const previousBalance = this.connectionInfo.balance;
1425
1559
  this.connectionInfo.balance = result;
1426
- // Emit balance changed event if different
1427
- if (previousBalance.total !== result.total) {
1560
+ // emit on total or pub/priv split change
1561
+ if (previousBalance.total !== result.total ||
1562
+ previousBalance.public !== result.public ||
1563
+ previousBalance.private !== result.private) {
1428
1564
  const balanceChangedEvent = {
1429
1565
  address: this.connectionInfo.address,
1430
1566
  previousBalance,
@@ -1439,6 +1575,8 @@
1439
1575
  return result;
1440
1576
  }
1441
1577
  catch (error) {
1578
+ if (error instanceof ZeroXIOWalletError)
1579
+ throw error;
1442
1580
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
1443
1581
  }
1444
1582
  }
@@ -1448,26 +1586,37 @@
1448
1586
  async getNetworkInfo() {
1449
1587
  this.ensureInitialized();
1450
1588
  try {
1589
+ const sv = this._sessionVersion;
1451
1590
  const result = await this.communicator.sendRequest('get_network_info');
1591
+ // validate network info before caching
1592
+ const networkInfo = validateNetworkInfo(result);
1593
+ if (!networkInfo) {
1594
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Extension returned invalid network info');
1595
+ }
1596
+ // skip if session changed mid-flight
1597
+ if (this._sessionVersion !== sv)
1598
+ return networkInfo;
1452
1599
  // Update cached network info
1453
1600
  if (this.connectionInfo.networkInfo) {
1454
1601
  const previousNetwork = this.connectionInfo.networkInfo;
1455
- this.connectionInfo.networkInfo = result;
1602
+ this.connectionInfo.networkInfo = networkInfo;
1456
1603
  // Emit network changed event if different
1457
- if (previousNetwork.id !== result.id) {
1604
+ if (previousNetwork.id !== networkInfo.id) {
1458
1605
  const networkChangedEvent = {
1459
1606
  previousNetwork,
1460
- newNetwork: result
1607
+ newNetwork: networkInfo
1461
1608
  };
1462
1609
  this.emit('networkChanged', networkChangedEvent);
1463
1610
  }
1464
1611
  }
1465
1612
  else {
1466
- this.connectionInfo.networkInfo = result;
1613
+ this.connectionInfo.networkInfo = networkInfo;
1467
1614
  }
1468
- return result;
1615
+ return networkInfo;
1469
1616
  }
1470
1617
  catch (error) {
1618
+ if (error instanceof ZeroXIOWalletError)
1619
+ throw error;
1471
1620
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
1472
1621
  }
1473
1622
  }
@@ -1479,8 +1628,20 @@
1479
1628
  */
1480
1629
  async sendTransaction(txData) {
1481
1630
  this.ensureConnected();
1631
+ // validate inputs
1632
+ if (!isValidAddress(txData.to)) {
1633
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1634
+ }
1635
+ if (!isValidAmount(txData.amount)) {
1636
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1637
+ }
1638
+ // bound memo
1639
+ if (txData.message && txData.message.length > 1000) {
1640
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1641
+ }
1482
1642
  try {
1483
- this.logger.log('Sending transaction:', txData);
1643
+ // log non-sensitive only
1644
+ this.logger.log('Sending transaction:', { to: txData.to });
1484
1645
  const result = await this.communicator.sendRequest('send_transaction', txData);
1485
1646
  this.logger.log('Transaction result:', result);
1486
1647
  // Refresh balance after successful transaction
@@ -1507,8 +1668,30 @@
1507
1668
  */
1508
1669
  async callContract(callData) {
1509
1670
  this.ensureConnected();
1671
+ // validate inputs
1672
+ if (!isValidAddress(callData.contract)) {
1673
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1674
+ }
1675
+ if (!callData.method || typeof callData.method !== 'string') {
1676
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method is required');
1677
+ }
1678
+ // bound method + params size
1679
+ if (callData.method.length > 200) {
1680
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
1681
+ }
1682
+ try {
1683
+ if (JSON.stringify(callData.params).length > 65536) {
1684
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
1685
+ }
1686
+ }
1687
+ catch (e) {
1688
+ if (e instanceof ZeroXIOWalletError)
1689
+ throw e;
1690
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params are not serialisable');
1691
+ }
1510
1692
  try {
1511
- this.logger.log('Calling contract:', callData);
1693
+ // log non-sensitive only
1694
+ this.logger.log('Calling contract:', { contract: callData.contract, method: callData.method });
1512
1695
  const result = await this.communicator.sendRequest('call_contract', {
1513
1696
  contract: callData.contract,
1514
1697
  method: callData.method,
@@ -1533,13 +1716,36 @@
1533
1716
  */
1534
1717
  async contractCallView(viewData) {
1535
1718
  this.ensureInitialized();
1719
+ // validate inputs
1720
+ if (!isValidAddress(viewData.contract)) {
1721
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1722
+ }
1723
+ if (!viewData.method || typeof viewData.method !== 'string') {
1724
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method is required');
1725
+ }
1726
+ // bound method + params size
1727
+ if (viewData.method.length > 200) {
1728
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method name too long (max 200 characters)');
1729
+ }
1730
+ try {
1731
+ if (JSON.stringify(viewData.params).length > 65536) {
1732
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract params too large (max 64 KB)');
1733
+ }
1734
+ }
1735
+ catch (e) {
1736
+ if (e instanceof ZeroXIOWalletError)
1737
+ throw e;
1738
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract params are not serialisable');
1739
+ }
1536
1740
  try {
1537
- this.logger.log('Contract view call:', viewData);
1741
+ // log non-sensitive only
1742
+ this.logger.log('Contract view call:', { contract: viewData.contract, method: viewData.method });
1538
1743
  const result = await this.communicator.sendRequest('contract_call_view', {
1539
1744
  contract: viewData.contract,
1540
1745
  method: viewData.method,
1541
1746
  params: viewData.params,
1542
- caller: viewData.caller || this.getAddress() || '',
1747
+ // only include caller if explicit
1748
+ ...(viewData.caller != null ? { caller: viewData.caller } : {}),
1543
1749
  });
1544
1750
  this.logger.log('Contract view result:', result);
1545
1751
  return result;
@@ -1557,6 +1763,16 @@
1557
1763
  */
1558
1764
  async getContractStorage(contract, key) {
1559
1765
  this.ensureInitialized();
1766
+ // validate inputs
1767
+ if (!isValidAddress(contract)) {
1768
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1769
+ }
1770
+ if (!key || typeof key !== 'string') {
1771
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Storage key is required');
1772
+ }
1773
+ if (key.length > 200) {
1774
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Storage key too long (max 200 characters)');
1775
+ }
1560
1776
  try {
1561
1777
  this.logger.log('Getting contract storage:', { contract, key });
1562
1778
  const result = await this.communicator.sendRequest('get_contract_storage', {
@@ -1610,13 +1826,17 @@
1610
1826
  */
1611
1827
  async encryptBalance(amount) {
1612
1828
  this.ensureConnected();
1829
+ // validate amount
1830
+ if (!isValidAmount(amount)) {
1831
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1832
+ }
1613
1833
  try {
1614
1834
  const result = await this.communicator.sendRequest('encrypt_balance', { amount });
1615
1835
  // Refresh balance after encryption
1616
1836
  setTimeout(() => {
1617
1837
  this.getBalance(true).catch(() => { });
1618
1838
  }, 1000);
1619
- return result.success;
1839
+ return result;
1620
1840
  }
1621
1841
  catch (error) {
1622
1842
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to encrypt balance', error);
@@ -1627,23 +1847,43 @@
1627
1847
  */
1628
1848
  async decryptBalance(amount) {
1629
1849
  this.ensureConnected();
1850
+ // validate amount
1851
+ if (!isValidAmount(amount)) {
1852
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1853
+ }
1630
1854
  try {
1631
1855
  const result = await this.communicator.sendRequest('decrypt_balance', { amount });
1632
1856
  // Refresh balance after decryption
1633
1857
  setTimeout(() => {
1634
1858
  this.getBalance(true).catch(() => { });
1635
1859
  }, 1000);
1636
- return result.success;
1860
+ return result;
1637
1861
  }
1638
1862
  catch (error) {
1639
1863
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to decrypt balance', error);
1640
1864
  }
1641
1865
  }
1642
1866
  /**
1643
- * Send private transfer
1867
+ * Send a private (encrypted) transfer to another address.
1868
+ * The extension builds the PVAC ciphertext subtraction + range proof + zero proof,
1869
+ * then submits the encrypted transaction to the network. The recipient's encrypted
1870
+ * balance is updated by the node using re-encryption under their public key.
1871
+ * Requires 'private_transfers' permission.
1872
+ * @since 2.6.0
1644
1873
  */
1645
1874
  async sendPrivateTransfer(transferData) {
1646
1875
  this.ensureConnected();
1876
+ // validate inputs
1877
+ if (!isValidAddress(transferData.to)) {
1878
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1879
+ }
1880
+ if (!isValidAmount(transferData.amount)) {
1881
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transfer amount');
1882
+ }
1883
+ // bound msg size
1884
+ if (transferData.message && transferData.message.length > 1000) {
1885
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
1886
+ }
1647
1887
  try {
1648
1888
  const result = await this.communicator.sendRequest('send_private_transfer', transferData);
1649
1889
  // Refresh balance after transfer
@@ -1659,7 +1899,9 @@
1659
1899
  }
1660
1900
  }
1661
1901
  /**
1662
- * Get pending private transfers
1902
+ * Get pending private transfers that can be claimed by this wallet.
1903
+ * Returns transfers where the connected address is the recipient.
1904
+ * @since 2.6.0
1663
1905
  */
1664
1906
  async getPendingPrivateTransfers() {
1665
1907
  this.ensureConnected();
@@ -1672,10 +1914,15 @@
1672
1914
  }
1673
1915
  }
1674
1916
  /**
1675
- * Claim private transfer
1917
+ * Claim a pending private transfer, adding it to the wallet's encrypted balance.
1918
+ * @since 2.6.0
1676
1919
  */
1677
1920
  async claimPrivateTransfer(transferId) {
1678
1921
  this.ensureConnected();
1922
+ // validate transfer ID
1923
+ if (!transferId || typeof transferId !== 'string') {
1924
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transfer ID');
1925
+ }
1679
1926
  try {
1680
1927
  const result = await this.communicator.sendRequest('claim_private_transfer', {
1681
1928
  transferId
@@ -1733,6 +1980,30 @@
1733
1980
  }
1734
1981
  }
1735
1982
  // ===================
1983
+ // AUTHENTICATION HELPERS
1984
+ // ===================
1985
+ /**
1986
+ * Sign a domain-separated authentication message.
1987
+ * Unlike `signMessage()`, this prepends a standard header that binds the signature
1988
+ * to the calling service and a one-time nonce, preventing cross-service replay attacks.
1989
+ *
1990
+ * @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
1991
+ * @param nonce - Unique one-time value — use a server-generated UUID or challenge
1992
+ * @returns Promise resolving to the base64-encoded Ed25519 signature
1993
+ */
1994
+ async signAuthMessage(service, nonce) {
1995
+ this.ensureConnected();
1996
+ if (!service || typeof service !== 'string') {
1997
+ throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Service name is required');
1998
+ }
1999
+ if (!nonce || typeof nonce !== 'string') {
2000
+ throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
2001
+ }
2002
+ const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
2003
+ const domainSeparated = `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
2004
+ return this.signMessage(domainSeparated);
2005
+ }
2006
+ // ===================
1736
2007
  // PRIVATE METHODS
1737
2008
  // ===================
1738
2009
  ensureInitialized() {
@@ -1772,28 +2043,47 @@
1772
2043
  * Handle account changed event from extension
1773
2044
  */
1774
2045
  handleAccountChanged(data) {
2046
+ ++this._sessionVersion;
1775
2047
  const previousAddress = this.connectionInfo.address;
1776
2048
  this.connectionInfo.address = data.address;
2049
+ // clear stale pubkey on acct change
2050
+ this.connectionInfo.publicKey = data.publicKey;
1777
2051
  if (data.balance) {
1778
- this.connectionInfo.balance = data.balance;
2052
+ // validate balance before caching
2053
+ const validated = validateBalance(data.balance);
2054
+ if (validated) {
2055
+ this.connectionInfo.balance = validated;
2056
+ }
2057
+ else {
2058
+ this.connectionInfo.balance = undefined; // clear stale balance
2059
+ }
1779
2060
  }
1780
2061
  const accountChangedEvent = {
1781
2062
  previousAddress,
1782
2063
  newAddress: data.address,
2064
+ publicKey: data.publicKey,
1783
2065
  balance: data.balance ?? this.connectionInfo.balance
1784
2066
  };
1785
2067
  this.emit('accountChanged', accountChangedEvent);
1786
- this.logger.log('Account changed:', accountChangedEvent);
2068
+ this.logger.log('Account changed:', { newAddress: accountChangedEvent.newAddress });
1787
2069
  }
1788
2070
  /**
1789
2071
  * Handle network changed event from extension
1790
2072
  */
1791
2073
  handleNetworkChanged(data) {
1792
2074
  const previousNetwork = this.connectionInfo.networkInfo;
1793
- this.connectionInfo.networkInfo = data.networkInfo;
2075
+ // validate networkInfo drop invalid
2076
+ const networkInfo = validateNetworkInfo(data.networkInfo);
2077
+ if (!networkInfo) {
2078
+ this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
2079
+ return;
2080
+ }
2081
+ this.connectionInfo.networkInfo = networkInfo;
2082
+ // invalidate balance on network change
2083
+ this.connectionInfo.balance = undefined;
1794
2084
  const networkChangedEvent = {
1795
2085
  previousNetwork,
1796
- newNetwork: data.networkInfo
2086
+ newNetwork: networkInfo
1797
2087
  };
1798
2088
  this.emit('networkChanged', networkChangedEvent);
1799
2089
  this.logger.log('Network changed:', networkChangedEvent);
@@ -1802,21 +2092,30 @@
1802
2092
  * Handle balance changed event from extension
1803
2093
  */
1804
2094
  handleBalanceChanged(data) {
2095
+ // validate balance before caching
2096
+ const balance = validateBalance(data.balance);
2097
+ if (!balance) {
2098
+ this.logger.warn('Received invalid balance in balanceChanged event, ignoring');
2099
+ return;
2100
+ }
1805
2101
  const previousBalance = this.connectionInfo.balance;
1806
- this.connectionInfo.balance = data.balance;
2102
+ this.connectionInfo.balance = balance;
1807
2103
  const balanceChangedEvent = {
1808
2104
  address: this.connectionInfo.address,
1809
2105
  previousBalance,
1810
- newBalance: data.balance
2106
+ newBalance: balance
1811
2107
  };
1812
2108
  this.emit('balanceChanged', balanceChangedEvent);
1813
- this.logger.log('Balance changed:', balanceChangedEvent);
2109
+ this.logger.log('Balance changed:', { public: balance.public });
1814
2110
  }
1815
2111
  /**
1816
2112
  * Handle extension locked event
1817
2113
  */
1818
2114
  handleExtensionLocked() {
2115
+ ++this._sessionVersion;
1819
2116
  this.connectionInfo = { isConnected: false };
2117
+ // emit extensionLocked then disconnect
2118
+ this.emit('extensionLocked', {});
1820
2119
  const disconnectEvent = {
1821
2120
  reason: 'extension_locked'
1822
2121
  };
@@ -1827,6 +2126,8 @@
1827
2126
  * Handle extension unlocked event
1828
2127
  */
1829
2128
  handleExtensionUnlocked() {
2129
+ // emit extensionUnlocked
2130
+ this.emit('extensionUnlocked', {});
1830
2131
  // Attempt to restore connection
1831
2132
  this.getConnectionStatus().catch(() => {
1832
2133
  this.logger.warn('Could not restore connection after unlock');
@@ -1859,6 +2160,8 @@
1859
2160
  this.removeAllListeners();
1860
2161
  this.connectionInfo = { isConnected: false };
1861
2162
  this.isInitialized = false;
2163
+ this._initPromise = null;
2164
+ ++this._sessionVersion;
1862
2165
  this.logger.log('SDK cleanup complete');
1863
2166
  }
1864
2167
  }
@@ -1868,6 +2171,200 @@
1868
2171
  ZeroXIOWallet: ZeroXIOWallet
1869
2172
  });
1870
2173
 
2174
+ /**
2175
+ * RFC-O-1 OctraProvider transport adapter.
2176
+ *
2177
+ * Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
2178
+ * window.octra.isOctra === true
2179
+ * window.octra.request({ method, params }) → Promise<unknown>
2180
+ * window.octra.on(event, listener) / removeListener(event, listener)
2181
+ *
2182
+ * This adapter translates the SDK's internal method names into RFC-O-1 method
2183
+ * names and maps events back to the SDK event vocabulary.
2184
+ *
2185
+ * Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
2186
+ * are passed through as-is; the wallet's request() handles or rejects them.
2187
+ */
2188
+ /** SDK method → RFC-O-1 method name */
2189
+ const SDK_TO_RFC = {
2190
+ get_network_info: 'octra_networkInfo',
2191
+ switch_network: 'octra_switchNetwork',
2192
+ signMessage: 'octra_signMessage',
2193
+ send_transaction: 'octra_sendTransaction',
2194
+ call_contract: 'octra_callContract',
2195
+ contract_call_view: 'octra_callContract',
2196
+ get_private_balance_info: 'octra_getEncryptedBalance',
2197
+ encrypt_balance: 'octra_encryptBalance',
2198
+ decrypt_balance: 'octra_decryptBalance',
2199
+ send_private_transfer: 'octra_sendPrivateTransfer',
2200
+ claim_private_transfer: 'octra_claimStealth',
2201
+ };
2202
+ /** RFC-O-1 error code → SDK ErrorCode string */
2203
+ const RFC_TO_SDK_ERROR = {
2204
+ 4001: 'USER_REJECTED',
2205
+ 4100: 'PERMISSION_DENIED',
2206
+ 4200: 'NETWORK_ERROR',
2207
+ 4900: 'CONNECTION_REFUSED',
2208
+ 4901: 'NETWORK_ERROR',
2209
+ };
2210
+ function getProvider() {
2211
+ return typeof window !== 'undefined' ? window.octra : null;
2212
+ }
2213
+ function mapError(err) {
2214
+ const code = RFC_TO_SDK_ERROR[err?.code] ?? 'UNKNOWN_ERROR';
2215
+ return { code, message: err?.message ?? 'Request failed' };
2216
+ }
2217
+ /**
2218
+ * Build a connect response compatible with what the SDK expects
2219
+ * ({ address, networkInfo, permissions, balance }) by making
2220
+ * three RFC-O-1 calls: octra_requestAccounts, octra_networkInfo, octra_permissions.
2221
+ */
2222
+ async function rfcConnect(provider, params) {
2223
+ const requestPerms = params?.requestPermissions ?? [];
2224
+ const accounts = (await provider.request({
2225
+ method: 'octra_requestAccounts',
2226
+ params: [{ permissions: requestPerms }],
2227
+ }));
2228
+ const address = accounts?.[0] ?? null;
2229
+ const [networkInfo, permissions] = await Promise.all([
2230
+ provider.request({ method: 'octra_networkInfo' }),
2231
+ provider.request({ method: 'octra_permissions' }),
2232
+ ]);
2233
+ return { address, networkInfo, permissions, balance: null };
2234
+ }
2235
+ /**
2236
+ * Build a getConnectionStatus response by checking octra_accounts.
2237
+ */
2238
+ async function rfcConnectionStatus(provider) {
2239
+ const accounts = (await provider.request({ method: 'octra_accounts' }));
2240
+ const address = accounts?.[0] ?? null;
2241
+ if (!address)
2242
+ return { isConnected: false };
2243
+ const [networkInfo, permissions] = await Promise.all([
2244
+ provider.request({ method: 'octra_networkInfo' }),
2245
+ provider.request({ method: 'octra_permissions' }),
2246
+ ]);
2247
+ return { isConnected: true, address, networkInfo, permissions };
2248
+ }
2249
+ function createOctraProviderAdapter() {
2250
+ let _handler = null;
2251
+ return {
2252
+ name: 'octra-provider',
2253
+ displayName: 'Octra Wallet (RFC-O-1)',
2254
+ detect() {
2255
+ const p = getProvider();
2256
+ return p?.isOctra === true;
2257
+ },
2258
+ postRequest(request) {
2259
+ const { id, method, params } = request;
2260
+ const provider = getProvider();
2261
+ if (!provider) {
2262
+ _handler?.({
2263
+ requestId: id,
2264
+ success: false,
2265
+ error: { code: 'EXTENSION_NOT_FOUND', message: 'No RFC-O-1 provider found on window.octra' },
2266
+ });
2267
+ return;
2268
+ }
2269
+ (async () => {
2270
+ try {
2271
+ let data;
2272
+ if (method === 'ping') {
2273
+ data = { available: true };
2274
+ }
2275
+ else if (method === 'register_dapp') {
2276
+ data = { success: true };
2277
+ }
2278
+ else if (method === 'connect') {
2279
+ data = await rfcConnect(provider, params);
2280
+ }
2281
+ else if (method === 'disconnect') {
2282
+ await provider.request({ method: 'disconnect' }).catch(() => { });
2283
+ data = { success: true };
2284
+ }
2285
+ else if (method === 'getConnectionStatus') {
2286
+ data = await rfcConnectionStatus(provider);
2287
+ }
2288
+ else {
2289
+ const rfcMethod = SDK_TO_RFC[method] ?? method;
2290
+ data = await provider.request({ method: rfcMethod, params });
2291
+ }
2292
+ _handler?.({ requestId: id, success: true, data });
2293
+ }
2294
+ catch (err) {
2295
+ _handler?.({ requestId: id, success: false, error: mapError(err) });
2296
+ }
2297
+ })();
2298
+ },
2299
+ listen(handler) {
2300
+ _handler = handler;
2301
+ const provider = getProvider();
2302
+ if (!provider)
2303
+ return () => { _handler = null; };
2304
+ // RFC-O-1 event → SDK event name + data shape
2305
+ const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
2306
+ const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
2307
+ const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
2308
+ const onNetworkChanged = (data) => handler({ eventType: 'networkChanged', eventData: { networkInfo: data } });
2309
+ const onBalanceChanged = (data) => handler({ eventType: 'balanceChanged', eventData: data });
2310
+ const onTransactionChanged = (data) => handler({ eventType: 'transactionConfirmed', eventData: data });
2311
+ provider.on('connect', onConnect);
2312
+ provider.on('disconnect', onDisconnect);
2313
+ provider.on('accountsChanged', onAccountsChanged);
2314
+ provider.on('networkChanged', onNetworkChanged);
2315
+ provider.on('balanceChanged', onBalanceChanged);
2316
+ provider.on('transactionChanged', onTransactionChanged);
2317
+ const cleanup = () => {
2318
+ provider.removeListener('connect', onConnect);
2319
+ provider.removeListener('disconnect', onDisconnect);
2320
+ provider.removeListener('accountsChanged', onAccountsChanged);
2321
+ provider.removeListener('networkChanged', onNetworkChanged);
2322
+ provider.removeListener('balanceChanged', onBalanceChanged);
2323
+ provider.removeListener('transactionChanged', onTransactionChanged);
2324
+ _handler = null;
2325
+ };
2326
+ return cleanup;
2327
+ },
2328
+ listenForReady(onReady) {
2329
+ const handler = () => onReady();
2330
+ window.addEventListener('octraWalletReady', handler);
2331
+ return () => window.removeEventListener('octraWalletReady', handler);
2332
+ },
2333
+ };
2334
+ }
2335
+ /** Default RFC-O-1 adapter instance. */
2336
+ const OctraProviderAdapter = createOctraProviderAdapter();
2337
+
2338
+ /**
2339
+ * 0xio SDK — Wallet Adapter Registry
2340
+ *
2341
+ * Add new wallet adapters here. Detection order determines which wallet takes
2342
+ * priority when multiple wallets are installed at the same time.
2343
+ */
2344
+ const REGISTERED_ADAPTERS = [
2345
+ ZeroXIOAdapter, // 0xio extension (postMessage protocol) — highest priority
2346
+ OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
2347
+ // Add new wallet adapters here — detection runs in order, first match wins
2348
+ ];
2349
+ /**
2350
+ * Auto-detects the first available wallet in the current page.
2351
+ * Returns null if no supported wallet is found.
2352
+ *
2353
+ * @example
2354
+ * const adapter = detectWalletAdapter();
2355
+ * if (!adapter) throw new Error('No supported wallet found');
2356
+ * const wallet = new ZeroXIOWallet({ appName: 'My DApp', adapter });
2357
+ */
2358
+ function detectWalletAdapter() {
2359
+ if (typeof window === 'undefined')
2360
+ return null;
2361
+ return REGISTERED_ADAPTERS.find((a) => a.detect()) ?? null;
2362
+ }
2363
+ /** Returns all registered adapter instances. */
2364
+ function getAllAdapters() {
2365
+ return [...REGISTERED_ADAPTERS];
2366
+ }
2367
+
1871
2368
  /**
1872
2369
  * 0xio Wallet SDK - Main Entry Point
1873
2370
  * Official SDK for integrating with 0xio Wallet Extension
@@ -1894,7 +2391,7 @@
1894
2391
  */
1895
2392
  // Main exports
1896
2393
  // Version information
1897
- const SDK_VERSION = '2.5.0';
2394
+ const SDK_VERSION = '2.7.0';
1898
2395
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
1899
2396
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
1900
2397
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -1904,7 +2401,8 @@
1904
2401
  const wallet$1 = new ZeroXIOWallet({
1905
2402
  appName: config.appName,
1906
2403
  appDescription: config.appDescription,
1907
- debug: config.debug || false
2404
+ debug: config.debug || false,
2405
+ ...(config.adapter ? { adapter: config.adapter } : {}),
1908
2406
  });
1909
2407
  await wallet$1.initialize();
1910
2408
  if (config.autoConnect) {
@@ -1918,23 +2416,35 @@
1918
2416
  }
1919
2417
  return wallet$1;
1920
2418
  }
1921
- // Legacy alias for backward compatibility
1922
- const createOctraWallet = createZeroXIOWallet;
1923
2419
  // Browser detection and compatibility check
1924
2420
  function checkSDKCompatibility() {
1925
2421
  const issues = [];
1926
2422
  const recommendations = [];
1927
- // Basic browser support check
2423
+ // Hard blockers SDK cannot function without these
1928
2424
  if (typeof window === 'undefined') {
1929
2425
  issues.push('Window object not available');
1930
2426
  recommendations.push('SDK must be used in a browser environment');
2427
+ return { compatible: false, issues, recommendations };
2428
+ }
2429
+ if (typeof window.postMessage !== 'function') {
2430
+ issues.push('postMessage API not available');
2431
+ recommendations.push('Your browser environment must support postMessage');
2432
+ }
2433
+ if (typeof window.addEventListener !== 'function') {
2434
+ issues.push('addEventListener not available');
2435
+ }
2436
+ if (typeof Promise === 'undefined') {
2437
+ issues.push('Promise not available');
1931
2438
  }
1932
- // Check for extension APIs
1933
- if (typeof window !== 'undefined') {
2439
+ // Informational: note which transport is likely active
2440
+ if (issues.length === 0) {
1934
2441
  const win = window;
1935
- if (!win.chrome || !win.chrome.runtime) {
1936
- issues.push('Chrome extension APIs not available');
1937
- recommendations.push('This SDK requires a Chromium-based browser (Chrome, Edge, Brave, etc.)');
2442
+ const hasExtension = !!(win.wallet0xio || win.ZeroXIOWallet || win.chrome?.runtime?.id ||
2443
+ document.querySelector('meta[name="0xio-dapp"]') || document.querySelector('[data-0xio-sdk-bridge]'));
2444
+ const hasParentBridge = window.parent !== window;
2445
+ if (!hasExtension && !hasParentBridge) {
2446
+ recommendations.push('No 0xio transport detected yet. Install the 0xio Wallet browser extension, ' +
2447
+ 'or run inside the 0xio Desktop/Mobile app iframe bridge.');
1938
2448
  }
1939
2449
  }
1940
2450
  return {
@@ -1996,9 +2506,11 @@
1996
2506
  exports.MIN_EXTENSION_VERSION = MIN_EXTENSION_VERSION;
1997
2507
  exports.MIN_EXTENSION_VERSION_DEVNET = MIN_EXTENSION_VERSION_DEVNET;
1998
2508
  exports.NETWORKS = NETWORKS;
2509
+ exports.OctraProviderAdapter = OctraProviderAdapter;
1999
2510
  exports.SDK_CONFIG = SDK_CONFIG;
2000
2511
  exports.SDK_VERSION = SDK_VERSION;
2001
2512
  exports.SUPPORTED_EXTENSION_VERSIONS = SUPPORTED_EXTENSION_VERSIONS;
2513
+ exports.ZeroXIOAdapter = ZeroXIOAdapter;
2002
2514
  exports.ZeroXIOWallet = ZeroXIOWallet;
2003
2515
  exports.ZeroXIOWalletError = ZeroXIOWalletError;
2004
2516
  exports.checkBrowserSupport = checkBrowserSupport;
@@ -2006,9 +2518,12 @@
2006
2518
  exports.createDefaultBalance = createDefaultBalance;
2007
2519
  exports.createErrorMessage = createErrorMessage;
2008
2520
  exports.createLogger = createLogger;
2009
- exports.createOctraWallet = createOctraWallet;
2521
+ exports.createOctraProviderAdapter = createOctraProviderAdapter;
2522
+ exports.createZeroXIOAdapter = createZeroXIOAdapter;
2010
2523
  exports.createZeroXIOWallet = createZeroXIOWallet;
2011
2524
  exports.delay = delay;
2525
+ exports.deriveOctraAddress = deriveOctraAddress;
2526
+ exports.detectWalletAdapter = detectWalletAdapter;
2012
2527
  exports.formatAddress = formatAddress;
2013
2528
  exports.formatOCT = formatOCT;
2014
2529
  exports.formatTimestamp = formatTimestamp;
@@ -2017,6 +2532,7 @@
2017
2532
  exports.fromMicroOCT = fromMicroOCT;
2018
2533
  exports.fromMicroZeroXIO = fromMicroOCT;
2019
2534
  exports.generateMockData = generateMockData;
2535
+ exports.getAllAdapters = getAllAdapters;
2020
2536
  exports.getAllNetworks = getAllNetworks;
2021
2537
  exports.getDefaultNetwork = getDefaultNetwork;
2022
2538
  exports.getNetworkConfig = getNetworkConfig;
@@ -2027,10 +2543,8 @@
2027
2543
  exports.isValidFeeLevel = isValidFeeLevel;
2028
2544
  exports.isValidMessage = isValidMessage;
2029
2545
  exports.isValidNetworkId = isValidNetworkId;
2030
- exports.retry = retry;
2031
2546
  exports.toMicroOCT = toMicroOCT;
2032
2547
  exports.toMicroZeroXIO = toMicroOCT;
2033
- exports.withTimeout = withTimeout;
2034
2548
 
2035
2549
  }));
2036
2550
  //# sourceMappingURL=index.umd.js.map