@0xio/sdk 2.4.0 → 2.4.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to the 0xio Wallet SDK will be documented in this file.
4
4
 
5
+ ## [2.4.1] - 2026-04-14
6
+
7
+ ### Security
8
+ - **[CRITICAL] postMessage origin validation**: Parent-frame messages now validated against a strict trusted origins set instead of accepting all origins. Prevents malicious pages from intercepting wallet requests via iframe embedding.
9
+ - **[CRITICAL] Removed auto-trust for iframes**: SDK no longer assumes any iframe parent is a wallet bridge. Must receive a `walletReady` signal from a trusted origin first.
10
+ - **[CRITICAL] Response binding**: Only responses with matching pending request IDs are processed. Forged responses from injected scripts are rejected.
11
+ - **[HIGH] Removed `simulateExtensionEvent`**: Dev utility that could be exploited on staging builds to inject fake wallet events has been removed.
12
+ - **[HIGH] No wildcard postMessage**: `postMessageToExtension` now uses specific trusted origins (`tauri://localhost`, etc.) instead of `'*'` for parent-frame communication.
13
+
14
+ ### Fixed
15
+ - **No retry on user rejection**: `retry()` detects rejection/denied/cancelled errors and throws immediately. Prevents double confirmation popups.
16
+ - **`withTimeout` timer leak**: Timer is now cleared via `.finally()` when the promise resolves, preventing 30s memory retention per request.
17
+ - **`retry` off-by-one**: `maxRetries=1` now correctly means 1 initial + 1 retry = 2 total (was 3).
18
+ - **Message listener cleanup**: `cleanup()` now removes the `window.addEventListener('message')` listener, preventing accumulation on re-instantiation.
19
+ - **Type compatibility**: Replaced `NodeJS.Timeout` with `ReturnType<typeof setTimeout>` for browser-only environments.
20
+ - **`process.env` guard**: `createLogger` now uses optional chaining for `process.env.NODE_ENV`, preventing ReferenceError when imported in browser without bundler.
21
+ - **Duplicate `isValidNetworkId`**: Removed duplicate export from `utils.ts`, canonical version in `config/networks.ts`.
22
+
23
+ ### Added
24
+ - **`setTrustedOrigins(origins)`**: New method to configure allowed parent-frame origins for iframe/bridge communication.
25
+
26
+ ### Documentation
27
+ - Fixed all event listener examples to use `event.data.xxx` (WalletEvent wrapper)
28
+ - Fixed `TransactionHistory` type (`totalCount`/`hasMore` instead of `total`/`limit`/`totalPages`)
29
+ - Fixed `retry()` docs (positional args, not options object)
30
+ - Fixed `formatZeroXIO` return value (no " OCT" suffix)
31
+ - Fixed `toMicroZeroXIO` return type (string, not number)
32
+ - Removed nonexistent `ConnectOptions.timeout`, `.requestPrivateAccess`
33
+ - Removed nonexistent `ConnectionInfo.permissions`
34
+ - Changed `ErrorCode.TIMEOUT` to `ErrorCode.NETWORK_ERROR`
35
+ - Updated mainnet privacy support to "Yes"
36
+
5
37
  ## [2.4.0] - 2026-03-24
6
38
 
7
39
  ### Added
package/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # 0xio Wallet SDK
2
2
 
3
- **Version:** 2.4.0
3
+ **Version:** 2.4.1
4
4
 
5
5
  Official TypeScript SDK for integrating DApps with 0xio Wallet on Octra Network.
6
6
 
7
- ## What's New in v2.4.0
7
+ ## What's New in v2.4.1
8
+
9
+ - **No retry on user rejection**: Transactions, contract calls, and sign requests rejected by the user no longer trigger automatic retry. Prevents double confirmation popups.
10
+ - **Type fixes**: Replaced `NodeJS.Timeout` with `ReturnType<typeof setTimeout>` for browser compatibility.
11
+
12
+ ## v2.4.0
8
13
 
9
14
  - **Desktop/Mobile DApp Bridge**: SDK now supports running inside iframes (0xio Desktop) and WebViews (0xio App). Requests are relayed to the parent frame automatically.
10
15
  - **Auto Frame Detection**: When `window.parent !== window`, the SDK assumes a wallet bridge is available and marks the wallet as detected.
@@ -163,11 +168,11 @@ console.log('Signature:', signature);
163
168
  ### Events
164
169
 
165
170
  ```typescript
166
- wallet.on('connect', (event) => console.log('Connected:', event.address));
171
+ wallet.on('connect', (event) => console.log('Connected:', event.data.address));
167
172
  wallet.on('disconnect', (event) => console.log('Disconnected'));
168
- wallet.on('balanceChanged', (event) => console.log('New balance:', event.newBalance.total));
169
- wallet.on('accountChanged', (event) => console.log('Account changed:', event.newAddress));
170
- wallet.on('networkChanged', (event) => console.log('Network:', event.newNetwork.name));
173
+ wallet.on('balanceChanged', (event) => console.log('New balance:', event.data.newBalance.total));
174
+ wallet.on('accountChanged', (event) => console.log('Account changed:', event.data.newAddress));
175
+ wallet.on('networkChanged', (event) => console.log('Network:', event.data.newNetwork.name));
171
176
  ```
172
177
 
173
178
  ## Error Handling
@@ -227,7 +232,7 @@ console.log(mainnet.supportsPrivacy); // true
227
232
 
228
233
  | Network | Privacy (FHE) | Explorer |
229
234
  |---------|:---:|---|
230
- | Mainnet Alpha | No | [octrascan.io](https://octrascan.io) |
235
+ | Mainnet Alpha | Yes | [octrascan.io](https://octrascan.io) |
231
236
  | Devnet | Yes | [devnet.octrascan.io](https://devnet.octrascan.io) |
232
237
 
233
238
  ### NetworkInfo Type
package/dist/index.d.ts CHANGED
@@ -412,7 +412,7 @@ declare class ZeroXIOWallet extends EventEmitter {
412
412
  * to ensure secure wallet interactions.
413
413
  *
414
414
  * @module communication
415
- * @version 2.2.0
415
+ * @version 2.4.1
416
416
  * @license MIT
417
417
  */
418
418
 
@@ -452,6 +452,10 @@ declare class ExtensionCommunicator extends EventEmitter {
452
452
  private extensionDetectionInterval;
453
453
  /** Current extension availability state */
454
454
  private isExtensionAvailableState;
455
+ /** Message listener reference for cleanup */
456
+ private messageListener;
457
+ /** Trusted parent origins for iframe communication */
458
+ private trustedOrigins;
455
459
  /** Maximum number of concurrent pending requests */
456
460
  private readonly MAX_CONCURRENT_REQUESTS;
457
461
  /** Time window for rate limiting (milliseconds) */
@@ -465,7 +469,12 @@ declare class ExtensionCommunicator extends EventEmitter {
465
469
  *
466
470
  * @param {boolean} debug - Enable debug logging
467
471
  */
468
- constructor(debug?: boolean);
472
+ constructor(debug?: boolean, trustedOrigins?: string[]);
473
+ /**
474
+ * Add trusted origins for iframe/bridge communication
475
+ * Call this before connecting if your dApp runs inside a trusted frame
476
+ */
477
+ setTrustedOrigins(origins: string[]): void;
469
478
  /**
470
479
  * Initialize communication with the wallet extension
471
480
  *
@@ -614,6 +623,10 @@ declare function getNetworkConfig(networkId?: string): NetworkInfo;
614
623
  * Get all available networks
615
624
  */
616
625
  declare function getAllNetworks(): NetworkInfo[];
626
+ /**
627
+ * Check if network ID is valid
628
+ */
629
+ declare function isValidNetworkId(networkId: string): boolean;
617
630
 
618
631
  /**
619
632
  * Default balance structure
@@ -623,7 +636,7 @@ declare function createDefaultBalance(total?: number): Balance;
623
636
  * SDK Configuration constants
624
637
  */
625
638
  declare const SDK_CONFIG: {
626
- readonly version: "2.3.0";
639
+ readonly version: "2.4.1";
627
640
  readonly defaultNetworkId: "mainnet";
628
641
  readonly communicationTimeout: 30000;
629
642
  readonly retryAttempts: 3;
@@ -647,10 +660,6 @@ declare function isValidAddress(address: string): boolean;
647
660
  * Validate transaction amount
648
661
  */
649
662
  declare function isValidAmount(amount: number): boolean;
650
- /**
651
- * Validate network ID
652
- */
653
- declare function isValidNetworkId(networkId: string): boolean;
654
663
  /**
655
664
  * Validate transaction message
656
665
  */
@@ -750,7 +759,7 @@ declare function createLogger(prefix: string, debug: boolean): {
750
759
  groupEnd: () => void;
751
760
  };
752
761
 
753
- declare const SDK_VERSION = "2.3.0";
762
+ declare const SDK_VERSION = "2.4.1";
754
763
  declare const MIN_EXTENSION_VERSION = "2.0.1";
755
764
  declare const MIN_EXTENSION_VERSION_DEVNET = "2.2.1";
756
765
  declare const SUPPORTED_EXTENSION_VERSIONS = "^2.0.1";
package/dist/index.esm.js CHANGED
@@ -164,16 +164,6 @@ function isValidAmount(amount) {
164
164
  Number.isFinite(amount) &&
165
165
  amount <= Number.MAX_SAFE_INTEGER;
166
166
  }
167
- /**
168
- * Validate network ID
169
- */
170
- function isValidNetworkId(networkId) {
171
- if (!networkId || typeof networkId !== 'string') {
172
- return false;
173
- }
174
- const validNetworks = ['mainnet', 'devnet', 'custom'];
175
- return validNetworks.includes(networkId.toLowerCase());
176
- }
177
167
  /**
178
168
  * Validate transaction message
179
169
  */
@@ -317,14 +307,21 @@ function delay(ms) {
317
307
  */
318
308
  async function retry(operation, maxRetries = 3, baseDelay = 1000) {
319
309
  let lastError;
320
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
310
+ // maxRetries = number of retries AFTER the first attempt
311
+ // Total attempts = 1 (initial) + maxRetries
312
+ for (let attempt = 0; attempt < 1 + maxRetries; attempt++) {
321
313
  try {
322
314
  return await operation();
323
315
  }
324
316
  catch (error) {
325
317
  lastError = error;
326
- if (attempt === maxRetries) {
327
- break; // Last attempt failed
318
+ // Never retry user rejections — these are intentional
319
+ const msg = lastError.message?.toLowerCase() || '';
320
+ if (msg.includes('rejected') || msg.includes('denied') || msg.includes('cancelled') || msg.includes('user refused')) {
321
+ throw lastError;
322
+ }
323
+ if (attempt >= maxRetries) {
324
+ break; // All retries exhausted
328
325
  }
329
326
  // Exponential backoff: 1s, 2s, 4s, etc.
330
327
  const delayMs = baseDelay * Math.pow(2, attempt);
@@ -337,12 +334,15 @@ async function retry(operation, maxRetries = 3, baseDelay = 1000) {
337
334
  * Timeout wrapper for promises
338
335
  */
339
336
  function withTimeout(promise, timeoutMs, timeoutMessage = 'Operation timed out') {
337
+ let timer;
340
338
  const timeoutPromise = new Promise((_, reject) => {
341
- setTimeout(() => {
339
+ timer = setTimeout(() => {
342
340
  reject(new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, timeoutMessage));
343
341
  }, timeoutMs);
344
342
  });
345
- return Promise.race([promise, timeoutPromise]);
343
+ return Promise.race([promise, timeoutPromise]).finally(() => {
344
+ clearTimeout(timer);
345
+ });
346
346
  }
347
347
  // ===================
348
348
  // BROWSER UTILITIES
@@ -409,7 +409,8 @@ function generateMockData() {
409
409
  * Create development logger
410
410
  */
411
411
  function createLogger(prefix, debug) {
412
- const isDevelopment = typeof window !== 'undefined' && (window.location.hostname === 'localhost' ||
412
+ const isDevelopment = typeof window !== 'undefined' && ((typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ||
413
+ window.location.hostname === 'localhost' ||
413
414
  window.location.hostname === '127.0.0.1' ||
414
415
  window.__OCTRA_SDK_DEBUG__);
415
416
  // Only enable logging in development mode AND when debug is explicitly enabled
@@ -467,7 +468,7 @@ function createLogger(prefix, debug) {
467
468
  * to ensure secure wallet interactions.
468
469
  *
469
470
  * @module communication
470
- * @version 2.2.0
471
+ * @version 2.4.1
471
472
  * @license MIT
472
473
  */
473
474
  /**
@@ -499,7 +500,7 @@ class ExtensionCommunicator extends EventEmitter {
499
500
  *
500
501
  * @param {boolean} debug - Enable debug logging
501
502
  */
502
- constructor(debug = false) {
503
+ constructor(debug = false, trustedOrigins = []) {
503
504
  super(debug);
504
505
  /** Legacy request counter (deprecated, kept for fallback) */
505
506
  this.requestId = 0;
@@ -511,6 +512,10 @@ class ExtensionCommunicator extends EventEmitter {
511
512
  this.extensionDetectionInterval = null;
512
513
  /** Current extension availability state */
513
514
  this.isExtensionAvailableState = false;
515
+ /** Message listener reference for cleanup */
516
+ this.messageListener = null;
517
+ /** Trusted parent origins for iframe communication */
518
+ this.trustedOrigins = [];
514
519
  // Rate limiting configuration
515
520
  /** Maximum number of concurrent pending requests */
516
521
  this.MAX_CONCURRENT_REQUESTS = 50;
@@ -521,9 +526,17 @@ class ExtensionCommunicator extends EventEmitter {
521
526
  /** Timestamps of recent requests for rate limiting */
522
527
  this.requestTimestamps = [];
523
528
  this.logger = createLogger('ExtensionCommunicator', debug);
529
+ this.trustedOrigins = trustedOrigins;
524
530
  this.setupMessageListener();
525
531
  this.startExtensionDetection();
526
532
  }
533
+ /**
534
+ * Add trusted origins for iframe/bridge communication
535
+ * Call this before connecting if your dApp runs inside a trusted frame
536
+ */
537
+ setTrustedOrigins(origins) {
538
+ this.trustedOrigins = origins;
539
+ }
527
540
  /**
528
541
  * Initialize communication with the wallet extension
529
542
  *
@@ -649,34 +662,51 @@ class ExtensionCommunicator extends EventEmitter {
649
662
  return; // Not in browser environment
650
663
  }
651
664
  const allowedOrigin = window.location.origin;
652
- window.addEventListener('message', (event) => {
653
- // Accept messages from same origin OR from parent frame (desktop/mobile bridge)
665
+ // Store allowed origins for parent frame communication
666
+ // Desktop (Tauri): tauri://localhost or https://tauri.localhost
667
+ // Mobile (Expo): about:blank or custom scheme
668
+ const trustedParentOrigins = new Set([
669
+ allowedOrigin,
670
+ 'tauri://localhost',
671
+ 'https://tauri.localhost',
672
+ 'http://localhost',
673
+ 'https://localhost',
674
+ ]);
675
+ // Allow dApps to register additional trusted origins
676
+ if (this.trustedOrigins) {
677
+ for (const origin of this.trustedOrigins) {
678
+ trustedParentOrigins.add(origin);
679
+ }
680
+ }
681
+ this.messageListener = (event) => {
682
+ // Strict origin validation
654
683
  const isFromSameOrigin = event.origin === allowedOrigin;
655
- const isFromParent = event.source === window.parent && window.parent !== window;
656
- if (!isFromSameOrigin && !isFromParent) {
684
+ const isFromTrustedParent = event.source === window.parent
685
+ && window.parent !== window
686
+ && trustedParentOrigins.has(event.origin);
687
+ if (!isFromSameOrigin && !isFromTrustedParent) {
657
688
  return;
658
689
  }
659
- // Accept from same window or parent frame
690
+ // Only accept from same window (extension content script) or trusted parent frame
660
691
  if (event.source !== window && event.source !== window.parent) {
661
692
  return;
662
693
  }
663
- // Check if it's a 0xio SDK response
694
+ // Verify message structure
664
695
  if (!event.data || event.data.source !== '0xio-sdk-bridge') {
665
696
  return;
666
697
  }
667
- // Handle different types of messages
698
+ // Validate response has a pending request (prevents forged responses)
668
699
  if (event.data.response) {
669
- // Regular request/response
670
700
  const response = event.data.response;
671
- if (response && response.id) {
701
+ if (response && response.id && this.pendingRequests.has(response.id)) {
672
702
  this.handleExtensionResponse(response);
673
703
  }
674
704
  }
675
705
  else if (event.data.event) {
676
- // Event notification from extension
677
706
  this.handleExtensionEvent(event.data.event);
678
707
  }
679
- });
708
+ };
709
+ window.addEventListener('message', this.messageListener);
680
710
  this.logger.log('Message listener setup complete');
681
711
  }
682
712
  /**
@@ -732,9 +762,28 @@ class ExtensionCommunicator extends EventEmitter {
732
762
  const msg = { source: '0xio-sdk-request', request };
733
763
  // Post to same window (extension content script picks it up)
734
764
  window.postMessage(msg, window.location.origin);
735
- // Also post to parent frame if in an iframe (desktop/mobile bridge picks it up)
765
+ // Also post to parent frame if in an iframe (desktop/mobile bridge)
766
+ // Use specific origin instead of wildcard to prevent interception
736
767
  if (window.parent !== window) {
737
- window.parent.postMessage(msg, '*');
768
+ try {
769
+ // Try same-origin first (works for Tauri, same-domain iframes)
770
+ window.parent.postMessage(msg, window.location.origin);
771
+ }
772
+ catch {
773
+ // Cross-origin parent (e.g. tauri://localhost) — use known trusted origins only
774
+ const trustedOrigins = ['tauri://localhost', 'https://tauri.localhost'];
775
+ if (this.trustedOrigins) {
776
+ trustedOrigins.push(...this.trustedOrigins);
777
+ }
778
+ for (const origin of trustedOrigins) {
779
+ try {
780
+ window.parent.postMessage(msg, origin);
781
+ }
782
+ catch {
783
+ // Origin not matching, skip
784
+ }
785
+ }
786
+ }
738
787
  }
739
788
  }
740
789
  /**
@@ -830,17 +879,26 @@ class ExtensionCommunicator extends EventEmitter {
830
879
  this.logger.log('Received octraWalletReady event');
831
880
  this.isExtensionAvailableState = true;
832
881
  });
833
- // Listen for desktop/mobile bridge walletReady via postMessage
882
+ // Listen for desktop/mobile bridge walletReady via postMessage (with origin validation)
834
883
  window.addEventListener('message', (event) => {
835
884
  if (event.data?.source === '0xio-sdk-bridge' && event.data?.event?.type === 'walletReady') {
836
- this.logger.log('Received walletReady via postMessage (desktop/mobile bridge)');
837
- this.isExtensionAvailableState = true;
885
+ const isSameOrigin = event.origin === window.location.origin;
886
+ const isTrusted = this.trustedOrigins.includes(event.origin)
887
+ || event.origin === 'tauri://localhost'
888
+ || event.origin === 'https://tauri.localhost';
889
+ if (isSameOrigin || isTrusted) {
890
+ this.logger.log('Received walletReady via postMessage from trusted origin');
891
+ this.isExtensionAvailableState = true;
892
+ }
893
+ else {
894
+ this.logger.warn(`Ignored walletReady from untrusted origin: ${event.origin}`);
895
+ }
838
896
  }
839
897
  });
840
- // Also detect if running inside a frame with a wallet bridge parent
898
+ // If running inside a frame, do NOT auto-trust the parent.
899
+ // Wait for a walletReady message from a trusted origin instead.
841
900
  if (window.parent !== window) {
842
- this.logger.log('Running inside a frame — checking for parent wallet bridge');
843
- this.isExtensionAvailableState = true;
901
+ this.logger.log('Running inside a frame — waiting for trusted walletReady signal');
844
902
  }
845
903
  // Initial check (in case extension was already injected)
846
904
  this.checkExtensionAvailability();
@@ -977,6 +1035,11 @@ class ExtensionCommunicator extends EventEmitter {
977
1035
  this.pendingRequests.clear();
978
1036
  this.isInitialized = false;
979
1037
  this.isExtensionAvailableState = false;
1038
+ // Remove message listener to prevent accumulation
1039
+ if (this.messageListener) {
1040
+ window.removeEventListener('message', this.messageListener);
1041
+ this.messageListener = null;
1042
+ }
980
1043
  // Call parent cleanup
981
1044
  this.removeAllListeners();
982
1045
  this.logger.log('Communication cleanup complete');
@@ -1050,6 +1113,12 @@ function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
1050
1113
  function getAllNetworks() {
1051
1114
  return Object.values(NETWORKS);
1052
1115
  }
1116
+ /**
1117
+ * Check if network ID is valid
1118
+ */
1119
+ function isValidNetworkId(networkId) {
1120
+ return networkId in NETWORKS;
1121
+ }
1053
1122
 
1054
1123
  /**
1055
1124
  * SDK Configuration
@@ -1069,7 +1138,7 @@ function createDefaultBalance(total = 0) {
1069
1138
  * SDK Configuration constants
1070
1139
  */
1071
1140
  const SDK_CONFIG = {
1072
- version: '2.3.0',
1141
+ version: '2.4.1',
1073
1142
  defaultNetworkId: DEFAULT_NETWORK_ID,
1074
1143
  communicationTimeout: 30000, // 30 seconds
1075
1144
  retryAttempts: 3,
@@ -1770,7 +1839,7 @@ var wallet = /*#__PURE__*/Object.freeze({
1770
1839
  */
1771
1840
  // Main exports
1772
1841
  // Version information
1773
- const SDK_VERSION = '2.3.0';
1842
+ const SDK_VERSION = '2.4.1';
1774
1843
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
1775
1844
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
1776
1845
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -1825,7 +1894,8 @@ if (typeof window !== 'undefined') {
1825
1894
  window.__OCTRA_SDK_VERSION__ = SDK_VERSION;
1826
1895
  window.__ZEROXIO_SDK_VERSION__ = SDK_VERSION;
1827
1896
  // Development mode detection
1828
- const isDevelopment = window.location.hostname === 'localhost' ||
1897
+ const isDevelopment = (typeof globalThis !== 'undefined' && globalThis.process?.env?.NODE_ENV === 'development') ||
1898
+ window.location.hostname === 'localhost' ||
1829
1899
  window.location.hostname === '127.0.0.1';
1830
1900
  if (isDevelopment) {
1831
1901
  // Set debug flag but don't automatically log
@@ -1850,13 +1920,7 @@ if (typeof window !== 'undefined') {
1850
1920
  debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
1851
1921
  environment: isDevelopment ? 'development' : 'production'
1852
1922
  }),
1853
- simulateExtensionEvent: (eventType, data) => {
1854
- window.postMessage({
1855
- source: '0xio-sdk-bridge',
1856
- event: { type: eventType, data }
1857
- }, window.location.origin);
1858
- console.log('[0xio SDK] Simulated extension event:', eventType, data);
1859
- },
1923
+ // simulateExtensionEvent removed for security — could be exploited on staging builds
1860
1924
  showWelcome: () => {
1861
1925
  console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
1862
1926
  console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');