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