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