@0xio/sdk 2.7.0 → 2.8.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
@@ -1,27 +1,15 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * 0xio Wallet SDK - Event System
5
- * Type-safe event emitter for wallet events
6
- */
7
3
  class EventEmitter {
8
- constructor(debug = false) {
4
+ constructor(_debug = false) {
9
5
  this.listeners = new Map();
10
- this.debug = debug;
11
6
  }
12
- /**
13
- * Add event listener
14
- */
15
7
  on(eventType, listener) {
16
8
  if (!this.listeners.has(eventType)) {
17
9
  this.listeners.set(eventType, new Set());
18
10
  }
19
11
  this.listeners.get(eventType).add(listener);
20
- if (this.debug) ;
21
12
  }
22
- /**
23
- * Remove event listener
24
- */
25
13
  off(eventType, listener) {
26
14
  const eventListeners = this.listeners.get(eventType);
27
15
  if (eventListeners) {
@@ -29,12 +17,8 @@ class EventEmitter {
29
17
  if (eventListeners.size === 0) {
30
18
  this.listeners.delete(eventType);
31
19
  }
32
- if (this.debug) ;
33
20
  }
34
21
  }
35
- /**
36
- * Add one-time event listener
37
- */
38
22
  once(eventType, listener) {
39
23
  const onceListener = (event) => {
40
24
  // Remove BEFORE calling so a throwing listener doesn't stay registered
@@ -43,9 +27,6 @@ class EventEmitter {
43
27
  };
44
28
  this.on(eventType, onceListener);
45
29
  }
46
- /**
47
- * Emit event to all listeners
48
- */
49
30
  emit(eventType, data) {
50
31
  const event = {
51
32
  type: eventType,
@@ -54,49 +35,32 @@ class EventEmitter {
54
35
  };
55
36
  const eventListeners = this.listeners.get(eventType);
56
37
  if (eventListeners && eventListeners.size > 0) {
57
- if (this.debug) ;
58
- // Create a copy to avoid issues if listeners modify the set during iteration
59
- const listenersArray = Array.from(eventListeners);
60
- for (const listener of listenersArray) {
38
+ // snapshot to avoid issues if listeners modify the set during iteration
39
+ for (const listener of Array.from(eventListeners)) {
61
40
  try {
62
41
  listener(event);
63
42
  }
64
- catch (error) {
65
- // console.error(`[0xio SDK] Error in event listener for '${eventType}':`, error);
43
+ catch {
44
+ // listener errors are swallowed to keep the event loop running
66
45
  }
67
46
  }
68
47
  }
69
- else if (this.debug) ;
70
48
  }
71
- /**
72
- * Remove all listeners for a specific event type
73
- */
74
49
  removeAllListeners(eventType) {
75
50
  if (eventType) {
76
51
  this.listeners.delete(eventType);
77
- if (this.debug) ;
78
52
  }
79
53
  else {
80
54
  this.listeners.clear();
81
- if (this.debug) ;
82
55
  }
83
56
  }
84
- /**
85
- * Get number of listeners for an event type
86
- */
87
57
  listenerCount(eventType) {
88
58
  const eventListeners = this.listeners.get(eventType);
89
59
  return eventListeners ? eventListeners.size : 0;
90
60
  }
91
- /**
92
- * Get all event types that have listeners
93
- */
94
61
  eventTypes() {
95
62
  return Array.from(this.listeners.keys());
96
63
  }
97
- /**
98
- * Check if there are any listeners for an event type
99
- */
100
64
  hasListeners(eventType) {
101
65
  return this.listenerCount(eventType) > 0;
102
66
  }
@@ -130,6 +94,16 @@ exports.ErrorCode = void 0;
130
94
  ErrorCode["DUPLICATE_TRANSACTION"] = "DUPLICATE_TRANSACTION";
131
95
  ErrorCode["NONCE_TOO_FAR"] = "NONCE_TOO_FAR";
132
96
  ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
97
+ // Codes the 0xio wallet returns over the bridge
98
+ ErrorCode["NOT_CONNECTED"] = "NOT_CONNECTED";
99
+ ErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS";
100
+ ErrorCode["METHOD_NOT_ALLOWED"] = "METHOD_NOT_ALLOWED";
101
+ ErrorCode["NOT_AVAILABLE"] = "NOT_AVAILABLE";
102
+ ErrorCode["PRIVATE_PROOF_FAILED"] = "PRIVATE_PROOF_FAILED";
103
+ ErrorCode["PRIVATE_TRANSFER_FAILED"] = "PRIVATE_TRANSFER_FAILED";
104
+ ErrorCode["CONTRACT_CALL_FAILED"] = "CONTRACT_CALL_FAILED";
105
+ ErrorCode["SIGN_FAILED"] = "SIGN_FAILED";
106
+ ErrorCode["RECIPIENT_NOT_REGISTERED"] = "RECIPIENT_NOT_REGISTERED";
133
107
  })(exports.ErrorCode || (exports.ErrorCode = {}));
134
108
  class ZeroXIOWalletError extends Error {
135
109
  constructor(code, message, details) {
@@ -152,7 +126,7 @@ class ZeroXIOWalletError extends Error {
152
126
  * window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
153
127
  * window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
154
128
  *
155
- * H-2: Session nonce validation injected.ts broadcasts the nonce received from the
129
+ * Session nonce validation: injected.ts broadcasts the nonce received from the
156
130
  * isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
157
131
  * mismatched nonce is rejected, preventing response injection by malicious page scripts.
158
132
  */
@@ -185,7 +159,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
185
159
  window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
186
160
  }
187
161
  catch {
188
- // Do not fall back to '*' silent failure is safer
162
+ // Do not fall back to '*': silent failure is safer
189
163
  }
190
164
  },
191
165
  listen(handler, options) {
@@ -200,7 +174,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
200
174
  ...(options?.trustedParentOrigins ?? []),
201
175
  ]);
202
176
  let _sessionNonce = null;
203
- // H-2: receive session nonce from injected.ts (MAIN world content script)
177
+ // receive the session nonce from injected.ts (MAIN world content script)
204
178
  const nonceListener = (e) => {
205
179
  if (e.origin !== allowedOrigin)
206
180
  return;
@@ -223,7 +197,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
223
197
  return;
224
198
  if (!e.data || e.data.source !== '0xio-sdk-bridge')
225
199
  return;
226
- // H-2: session nonce validation.
200
+ // session nonce validation.
227
201
  // Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
228
202
  // Fallback path: if the init broadcast was missed (race between document_start
229
203
  // content script and page script load), capture nonce from the first same-origin
@@ -269,16 +243,6 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
269
243
  /** Default 0xio adapter instance (no extra trusted origins). */
270
244
  const ZeroXIOAdapter = createZeroXIOAdapter();
271
245
 
272
- /**
273
- * 0xio Wallet SDK - Utilities
274
- * Helper functions for validation, formatting, and common operations
275
- */
276
- // ===================
277
- // VALIDATION UTILITIES
278
- // ===================
279
- /**
280
- * Validate wallet address for Octra blockchain
281
- */
282
246
  function isValidAddress(address) {
283
247
  if (!address || typeof address !== 'string') {
284
248
  return false;
@@ -302,11 +266,8 @@ function isValidAmount(amount) {
302
266
  Number.isFinite(amount) &&
303
267
  amount <= Number.MAX_SAFE_INTEGER;
304
268
  }
305
- /**
306
- * Validate transaction message
307
- */
308
269
  function isValidMessage(message) {
309
- // Type check first falsy non-strings (0, false, null) are NOT valid messages
270
+ // Type check first: falsy non-strings (0, false, null) are not valid messages
310
271
  if (typeof message !== 'string') {
311
272
  return message === undefined || message === null ? true : false;
312
273
  }
@@ -314,18 +275,12 @@ function isValidMessage(message) {
314
275
  if (message.length === 0) {
315
276
  return true;
316
277
  }
317
- // 100KB limit contract call params can be large (serialized JSON)
278
+ // 100KB limit: contract call params can be large (serialized JSON)
318
279
  return message.length <= 100000;
319
280
  }
320
- /**
321
- * Validate fee level
322
- */
323
281
  function isValidFeeLevel(feeLevel) {
324
282
  return feeLevel === 1 || feeLevel === 3;
325
283
  }
326
- // ===================
327
- // ADDRESS DERIVATION
328
- // ===================
329
284
  const _B58_ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
330
285
  function _base58Encode(buf) {
331
286
  let zeros = 0;
@@ -353,7 +308,7 @@ function _base58Encode(buf) {
353
308
  }
354
309
  /**
355
310
  * Derive the canonical Octra address from a base64-encoded Ed25519 public key.
356
- * Algorithm: SHA-256(pubkey_bytes) base58 prepend "oct"
311
+ * Algorithm: SHA-256 of the pubkey bytes, base58-encoded, with "oct" prepended
357
312
  * Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
358
313
  */
359
314
  async function deriveOctraAddress(publicKeyBase64) {
@@ -370,12 +325,6 @@ async function deriveOctraAddress(publicKeyBase64) {
370
325
  const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
371
326
  return 'oct' + _base58Encode(new Uint8Array(hashBuf));
372
327
  }
373
- // ===================
374
- // FORMATTING UTILITIES
375
- // ===================
376
- /**
377
- * Format OCT amount for display
378
- */
379
328
  function formatOCT(amount, decimals = 6) {
380
329
  const n = typeof amount === 'string' ? parseFloat(amount) : amount;
381
330
  if (!isValidAmount(n)) {
@@ -386,9 +335,6 @@ function formatOCT(amount, decimals = 6) {
386
335
  maximumFractionDigits: decimals
387
336
  });
388
337
  }
389
- /**
390
- * Format address for display (truncated)
391
- */
392
338
  function formatAddress(address, prefixLength = 6, suffixLength = 4) {
393
339
  if (!isValidAddress(address)) {
394
340
  return 'Invalid Address';
@@ -398,16 +344,10 @@ function formatAddress(address, prefixLength = 6, suffixLength = 4) {
398
344
  }
399
345
  return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}`;
400
346
  }
401
- /**
402
- * Format timestamp for display
403
- */
404
347
  function formatTimestamp(timestamp) {
405
348
  const date = new Date(timestamp);
406
349
  return date.toLocaleString();
407
350
  }
408
- /**
409
- * Format transaction hash for display
410
- */
411
351
  function formatTxHash(hash, length = 12) {
412
352
  if (!hash || typeof hash !== 'string') {
413
353
  return 'Invalid Hash';
@@ -419,12 +359,6 @@ function formatTxHash(hash, length = 12) {
419
359
  const suffixLength = Math.floor(length / 2);
420
360
  return `${hash.slice(0, prefixLength)}...${hash.slice(-suffixLength)}`;
421
361
  }
422
- // ===================
423
- // CONVERSION UTILITIES
424
- // ===================
425
- /**
426
- * Convert OCT to micro OCT (for network transmission)
427
- */
428
362
  function toMicroOCT(amount) {
429
363
  if (!isValidAmount(amount)) {
430
364
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Invalid amount for conversion');
@@ -434,8 +368,21 @@ function toMicroOCT(amount) {
434
368
  return microOCT.toString();
435
369
  }
436
370
  /**
437
- * Convert micro OCT to OCT (for display)
371
+ * Exact OCT to raw micro-OCT conversion using string arithmetic (no float rounding).
372
+ * Accepts up to 6 decimals; anything else is rejected.
438
373
  */
374
+ function octToMicro(amount) {
375
+ const text = typeof amount === 'number' ? amount.toFixed(6) : String(amount).trim();
376
+ if (!/^\d+(\.\d{1,6})?$/.test(text)) {
377
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be a positive decimal with at most 6 places');
378
+ }
379
+ const [whole, frac = ''] = text.split('.');
380
+ const micro = BigInt(whole) * BigInt(1000000) + BigInt(frac.padEnd(6, '0'));
381
+ if (micro <= BigInt(0)) {
382
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be greater than zero');
383
+ }
384
+ return micro.toString();
385
+ }
439
386
  function fromMicroOCT(microAmount) {
440
387
  const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
441
388
  if (!Number.isFinite(amount) || amount < 0) {
@@ -443,12 +390,6 @@ function fromMicroOCT(microAmount) {
443
390
  }
444
391
  return amount / 1000000;
445
392
  }
446
- // ===================
447
- // ERROR UTILITIES
448
- // ===================
449
- /**
450
- * Create standardized error messages
451
- */
452
393
  function createErrorMessage(code, context) {
453
394
  const baseMessages = {
454
395
  [exports.ErrorCode.EXTENSION_NOT_FOUND]: '0xio Wallet extension is not installed or enabled',
@@ -470,29 +411,26 @@ function createErrorMessage(code, context) {
470
411
  [exports.ErrorCode.INVALID_SIGNATURE]: 'Invalid transaction signature',
471
412
  [exports.ErrorCode.DUPLICATE_TRANSACTION]: 'Duplicate transaction detected',
472
413
  [exports.ErrorCode.NONCE_TOO_FAR]: 'Transaction nonce is too far ahead',
473
- [exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error'
414
+ [exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error',
415
+ [exports.ErrorCode.NOT_CONNECTED]: 'Connect the wallet before this request',
416
+ [exports.ErrorCode.INVALID_PARAMS]: 'Invalid request parameters',
417
+ [exports.ErrorCode.METHOD_NOT_ALLOWED]: 'This method is not permitted through the wallet',
418
+ [exports.ErrorCode.NOT_AVAILABLE]: 'Not available through the wallet bridge',
419
+ [exports.ErrorCode.PRIVATE_PROOF_FAILED]: 'Private proof generation failed',
420
+ [exports.ErrorCode.PRIVATE_TRANSFER_FAILED]: 'Private transfer failed',
421
+ [exports.ErrorCode.CONTRACT_CALL_FAILED]: 'Contract call failed',
422
+ [exports.ErrorCode.SIGN_FAILED]: 'Signing failed',
423
+ [exports.ErrorCode.RECIPIENT_NOT_REGISTERED]: 'Recipient has no private view key registered'
474
424
  };
475
425
  const baseMessage = baseMessages[code] || 'Unknown error';
476
426
  return context ? `${baseMessage}: ${context}` : baseMessage;
477
427
  }
478
- /**
479
- * Check if error is a specific type
480
- */
481
428
  function isErrorType(error, code) {
482
429
  return error instanceof ZeroXIOWalletError && error.code === code;
483
430
  }
484
- // ===================
485
- // ASYNC UTILITIES
486
- // ===================
487
- /**
488
- * Create a promise that resolves after a delay
489
- */
490
431
  function delay(ms) {
491
432
  return new Promise(resolve => setTimeout(resolve, ms));
492
433
  }
493
- /**
494
- * Retry an async operation with exponential backoff
495
- */
496
434
  async function retry(operation, maxRetries = 3, baseDelay = 1000) {
497
435
  let lastError;
498
436
  // maxRetries = number of retries AFTER the first attempt
@@ -503,7 +441,7 @@ async function retry(operation, maxRetries = 3, baseDelay = 1000) {
503
441
  }
504
442
  catch (error) {
505
443
  lastError = error;
506
- // Never retry user rejections these are intentional
444
+ // Never retry user rejections, they are intentional
507
445
  const msg = lastError.message?.toLowerCase() || '';
508
446
  if (msg.includes('rejected') || msg.includes('denied') || msg.includes('cancelled') || msg.includes('user refused')) {
509
447
  throw lastError;
@@ -532,18 +470,9 @@ function withTimeout(promise, timeoutMs, timeoutMessage = 'Operation timed out')
532
470
  clearTimeout(timer);
533
471
  });
534
472
  }
535
- // ===================
536
- // BROWSER UTILITIES
537
- // ===================
538
- /**
539
- * Check if running in browser environment
540
- */
541
473
  function isBrowser() {
542
474
  return typeof window !== 'undefined' && typeof document !== 'undefined';
543
475
  }
544
- /**
545
- * Check if browser supports required features
546
- */
547
476
  function checkBrowserSupport() {
548
477
  const missingFeatures = [];
549
478
  if (!isBrowser()) {
@@ -565,12 +494,6 @@ function checkBrowserSupport() {
565
494
  missingFeatures
566
495
  };
567
496
  }
568
- // ===================
569
- // DEVELOPMENT UTILITIES
570
- // ===================
571
- /**
572
- * Generate mock data for development/testing
573
- */
574
497
  function generateMockData() {
575
498
  return {
576
499
  address: 'oct' + Math.random().toString(36).substring(2, 22) + Math.random().toString(36).substring(2, 26),
@@ -593,9 +516,6 @@ function generateMockData() {
593
516
  }
594
517
  };
595
518
  }
596
- /**
597
- * Create development logger
598
- */
599
519
  function createLogger(prefix, debug) {
600
520
  const isDevelopment = typeof window !== 'undefined' && ((typeof globalThis !== 'undefined' && globalThis.process?.env?.NODE_ENV === 'development') ||
601
521
  window.location.hostname === 'localhost' ||
@@ -648,17 +568,6 @@ function createLogger(prefix, debug) {
648
568
  };
649
569
  }
650
570
 
651
- /**
652
- * 0xio Wallet SDK - Extension Communication Module
653
- *
654
- * @fileoverview Manages secure communication between the SDK and browser extension.
655
- * Implements message passing, request/response handling, rate limiting, and origin validation
656
- * to ensure secure wallet interactions.
657
- *
658
- * @module communication
659
- * @version 2.7.0
660
- * @license MIT
661
- */
662
571
  class ExtensionCommunicator extends EventEmitter {
663
572
  constructor(debug = false, trustedOrigins = [], adapter) {
664
573
  super(debug);
@@ -674,14 +583,14 @@ class ExtensionCommunicator extends EventEmitter {
674
583
  this._adapterReadyTeardown = null;
675
584
  /**
676
585
  * Set when a trusted walletReady has been received from window.parent.
677
- * The polling fallback must NOT clear this flag.
586
+ * The polling fallback must not clear this flag.
678
587
  */
679
588
  this._parentTrusted = false;
680
589
  /** walletReady postMessage listener stored for cleanup */
681
590
  this._walletReadyMessageListener = null;
682
591
  /**
683
592
  * In-flight interactive request lock.
684
- * Methods that open approval popups are serialized only one at a time.
593
+ * Methods that open approval popups are serialized: only one at a time.
685
594
  */
686
595
  this._interactiveInFlight = false;
687
596
  this.MAX_CONCURRENT_REQUESTS = 50;
@@ -722,9 +631,15 @@ class ExtensionCommunicator extends EventEmitter {
722
631
  return this.isExtensionAvailableState && this.hasExtensionContext();
723
632
  }
724
633
  async sendRequest(method, params = {}, timeout = 30000) {
725
- const isInteractive = ExtensionCommunicator.NO_RETRY_METHODS.has(method);
726
- const maxRetries = isInteractive ? 0 : 1;
727
- const effectiveTimeout = isInteractive ? Math.max(timeout, 180000) : timeout;
634
+ // No-retry + long timeout for both popup/broadcast methods and long compute primitives.
635
+ const longOrNoRetry = ExtensionCommunicator.NO_RETRY_METHODS.has(method) ||
636
+ ExtensionCommunicator.LONG_COMPUTE_METHODS.has(method);
637
+ const maxRetries = longOrNoRetry ? 0 : 1;
638
+ const effectiveTimeout = ExtensionCommunicator.PROOF_METHODS.has(method)
639
+ ? Math.max(timeout, 600000)
640
+ : longOrNoRetry
641
+ ? Math.max(timeout, 180000)
642
+ : timeout;
728
643
  return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
729
644
  }
730
645
  async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
@@ -793,7 +708,7 @@ class ExtensionCommunicator extends EventEmitter {
793
708
  return;
794
709
  this._adapterTeardown = this.adapter.listen((msg) => {
795
710
  if (msg.requestId !== undefined) {
796
- // response map AdapterIncomingMessage ExtensionResponse shape
711
+ // response: map AdapterIncomingMessage to the ExtensionResponse shape
797
712
  if (this.pendingRequests.has(msg.requestId)) {
798
713
  this.handleExtensionResponse({
799
714
  id: msg.requestId,
@@ -827,7 +742,7 @@ class ExtensionCommunicator extends EventEmitter {
827
742
  }
828
743
  clearTimeout(pending.timeout);
829
744
  this.pendingRequests.delete(response.id);
830
- // Require strict boolean true "false" string or other truthy values are failures
745
+ // Require strict boolean true: a "false" string or other truthy values are failures
831
746
  if (response.success === true) {
832
747
  pending.resolve(response.data);
833
748
  }
@@ -852,7 +767,7 @@ class ExtensionCommunicator extends EventEmitter {
852
767
  }
853
768
  postMessageToExtension(request) {
854
769
  this.adapter.postRequest(request);
855
- // Parent bridge (iframe/desktop mode) only when a trusted origin is established.
770
+ // Parent bridge (iframe/desktop mode), only when a trusted origin is established.
856
771
  // Sending with '*' would leak method + params to any intercepting frame.
857
772
  if (window.parent !== window && this._parentOrigin) {
858
773
  if (this.adapter.postRequestToParent) {
@@ -869,7 +784,7 @@ class ExtensionCommunicator extends EventEmitter {
869
784
  if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
870
785
  throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
871
786
  }
872
- // Trim expired timestamps cap array size to prevent unbounded growth in idle tabs
787
+ // Trim expired timestamps and cap the array size to prevent unbounded growth in idle tabs
873
788
  this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
874
789
  if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
875
790
  this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
@@ -889,7 +804,7 @@ class ExtensionCommunicator extends EventEmitter {
889
804
  const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
890
805
  return `0xio-sdk-${hex}`;
891
806
  }
892
- // Crypto API unavailable throw rather than produce a guessable ID that
807
+ // Crypto API unavailable: throw rather than produce a guessable ID that
893
808
  // could allow response spoofing via a known requestId.
894
809
  throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
895
810
  }
@@ -903,15 +818,18 @@ class ExtensionCommunicator extends EventEmitter {
903
818
  this.isExtensionAvailableState = true;
904
819
  });
905
820
  }
906
- // walletReady via postMessage (desktop/mobile iframe bridge) store ref for cleanup
821
+ // walletReady via postMessage (desktop/mobile iframe bridge), store the ref for cleanup
907
822
  this._walletReadyMessageListener = (event) => {
908
823
  if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
909
824
  return;
910
825
  }
911
826
  const isSameOrigin = event.origin === window.location.origin;
912
- const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
913
827
  const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
914
- const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhost;
828
+ const hasExplicitList = this.trustedOrigins.length > 0;
829
+ // When trustedParentOrigins is explicitly set, implicit localhost trust is disabled
830
+ const isLocalhostAllowed = !hasExplicitList &&
831
+ (event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:'));
832
+ const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhostAllowed;
915
833
  // In iframe mode, only trust the actual parent window
916
834
  const inIframe = window.parent !== window;
917
835
  if (inIframe && event.source !== window.parent) {
@@ -935,7 +853,7 @@ class ExtensionCommunicator extends EventEmitter {
935
853
  };
936
854
  window.addEventListener('message', this._walletReadyMessageListener);
937
855
  if (window.parent !== window) {
938
- this.logger.log('Running inside a frame waiting for trusted walletReady signal');
856
+ this.logger.log('Running inside a frame, waiting for the trusted walletReady signal');
939
857
  }
940
858
  this.checkExtensionAvailability();
941
859
  this.extensionDetectionInterval = setInterval(() => {
@@ -944,7 +862,7 @@ class ExtensionCommunicator extends EventEmitter {
944
862
  }
945
863
  checkExtensionAvailability() {
946
864
  // If parent-bridge readiness was established via a trusted walletReady handshake,
947
- // preserve that state the polling fallback (detectExtensionSignals) does not
865
+ // preserve that state: the polling fallback (detectExtensionSignals) does not
948
866
  // consider the iframe parent signal and would incorrectly flip state back
949
867
  if (this._parentTrusted) {
950
868
  return;
@@ -1026,7 +944,7 @@ class ExtensionCommunicator extends EventEmitter {
1026
944
  }
1027
945
  /**
1028
946
  * Clean up SDK resources.
1029
- * After cleanup() the instance is terminal do not call initialize() again.
947
+ * After cleanup() the instance is terminal: do not call initialize() again.
1030
948
  * Construct a new instance instead.
1031
949
  */
1032
950
  cleanup() {
@@ -1067,20 +985,38 @@ class ExtensionCommunicator extends EventEmitter {
1067
985
  };
1068
986
  }
1069
987
  }
1070
- // Methods that trigger user-facing popups NEVER retry these.
988
+ // Methods that trigger user-facing popups: never retry these.
1071
989
  // Retrying sends a second request while the first popup is still open,
1072
990
  // causing double popups where the second tx fails (stale nonce/state).
1073
991
  ExtensionCommunicator.NO_RETRY_METHODS = new Set([
1074
992
  'connect', 'send_transaction', 'call_contract', 'signMessage',
993
+ 'sign_transaction', 'broadcast_only',
1075
994
  'send_private_transfer', 'claim_private_transfer',
1076
995
  'encrypt_balance', 'decrypt_balance',
996
+ // Broadcasts a sequence of contract txs, so it must never be retried (it would double-send).
997
+ 'send_contract_transaction_sequence',
998
+ // Shows an approval popup + broadcasts a registration tx (offscreen cold-init can exceed
999
+ // 30s): interactive, long timeout, no retry.
1000
+ 'register_private_view_key',
1001
+ ]);
1002
+ // Long-running compute primitives (RFP): proof generation / decrypt take 10-120s, so
1003
+ // they need the long (180s) timeout and must not be retried (a retry wastes ~a minute of
1004
+ // compute). They do not show approval popups, so, unlike NO_RETRY_METHODS, they are not
1005
+ // subject to the one-at-a-time interactive lock (a dapp may run several concurrently).
1006
+ ExtensionCommunicator.LONG_COMPUTE_METHODS = new Set([
1007
+ 'make_zero_proof', 'make_range_proof', 'decrypt_value',
1008
+ 'get_private_balance', 'encrypt_value',
1077
1009
  ]);
1010
+ // A private transfer runs proof generation after the approval (minutes on a slow machine),
1011
+ // so its wait is longer than the popup window alone.
1012
+ ExtensionCommunicator.PROOF_METHODS = new Set(['send_private_transfer']);
1013
+ // The interactive lock applies only to methods that open a wallet popup / broadcast.
1078
1014
  ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
1079
1015
  // only forward known event types
1080
1016
  ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
1081
1017
  'connect', 'disconnect', 'accountChanged', 'balanceChanged',
1082
- 'networkChanged', 'transactionConfirmed', 'error',
1083
- 'extensionLocked', 'extensionUnlocked'
1018
+ 'networkChanged', 'transactionConfirmed', 'transactionFailed', 'permissionsChanged', 'message',
1019
+ 'error', 'extensionLocked', 'extensionUnlocked'
1084
1020
  ]);
1085
1021
 
1086
1022
  /**
@@ -1101,7 +1037,7 @@ const _NETWORKS = {
1101
1037
  'devnet': {
1102
1038
  id: 'devnet',
1103
1039
  name: 'Octra Devnet',
1104
- rpcUrl: 'http://165.227.225.79:8080',
1040
+ rpcUrl: 'https://devnet.octrascan.io',
1105
1041
  explorerUrl: 'https://devnet.octrascan.io/tx.html?hash=',
1106
1042
  explorerAddressUrl: 'https://devnet.octrascan.io/address.html?addr=',
1107
1043
  indexerUrl: 'https://devnet.octrascan.io',
@@ -1129,7 +1065,7 @@ const NETWORKS = Object.freeze(Object.fromEntries(Object.entries(_NETWORKS).map(
1129
1065
  const DEFAULT_NETWORK_ID = 'mainnet';
1130
1066
  /**
1131
1067
  * Get network configuration by ID.
1132
- * Returns a frozen copy callers cannot mutate SDK-internal state.
1068
+ * Returns a frozen copy, so callers cannot mutate SDK-internal state.
1133
1069
  */
1134
1070
  function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
1135
1071
  if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
@@ -1139,7 +1075,7 @@ function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
1139
1075
  }
1140
1076
  /**
1141
1077
  * Get all available networks.
1142
- * Returns frozen copies callers cannot mutate SDK-internal state.
1078
+ * Returns frozen copies, so callers cannot mutate SDK-internal state.
1143
1079
  */
1144
1080
  function getAllNetworks() {
1145
1081
  return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
@@ -1163,6 +1099,14 @@ function validateNetworkInfo(raw) {
1163
1099
  return null;
1164
1100
  if (typeof raw.rpcUrl !== 'string' || (!raw.rpcUrl && raw.id !== 'custom'))
1165
1101
  return null;
1102
+ if (raw.rpcUrl) {
1103
+ const isHttps = raw.rpcUrl.startsWith('https://');
1104
+ const isLocal = raw.rpcUrl.startsWith('http://localhost') || raw.rpcUrl.startsWith('http://127.0.0.1');
1105
+ const isTestnet = raw.isTestnet === true;
1106
+ // Reject plain-http RPC URLs from untrusted sources unless testnet-flagged or local
1107
+ if (!isHttps && !isLocal && !isTestnet)
1108
+ return null;
1109
+ }
1166
1110
  if (typeof raw.supportsPrivacy !== 'boolean')
1167
1111
  return null;
1168
1112
  return Object.freeze({
@@ -1178,12 +1122,9 @@ function validateNetworkInfo(raw) {
1178
1122
  });
1179
1123
  }
1180
1124
 
1181
- /**
1182
- * SDK Configuration
1183
- */
1184
1125
  /**
1185
1126
  * Default balance structure.
1186
- * Accepts a numeric total or undefined never pass a Balance object here.
1127
+ * Accepts a numeric total or undefined. Never pass a Balance object here.
1187
1128
  */
1188
1129
  function createDefaultBalance(total = 0) {
1189
1130
  const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
@@ -1202,7 +1143,7 @@ function validateBalance(raw) {
1202
1143
  if (raw === null || raw === undefined)
1203
1144
  return null;
1204
1145
  // If it's already a Balance-shaped object, extract numeric fields
1205
- // Use Number() not parseFloat() parseFloat('10abc') silently returns 10
1146
+ // Use Number() not parseFloat(): parseFloat('10abc') silently returns 10
1206
1147
  const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
1207
1148
  const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
1208
1149
  if (!Number.isFinite(pub) || pub < 0)
@@ -1216,34 +1157,165 @@ function validateBalance(raw) {
1216
1157
  currency: 'OCT'
1217
1158
  };
1218
1159
  }
1219
- /**
1220
- * SDK Configuration constants
1221
- */
1222
1160
  const SDK_CONFIG = {
1223
- version: '2.7.0',
1161
+ version: '2.8.0',
1224
1162
  defaultNetworkId: DEFAULT_NETWORK_ID,
1225
1163
  communicationTimeout: 30000, // 30 seconds
1226
1164
  retryAttempts: 3,
1227
1165
  retryDelay: 1000, // 1 second
1228
1166
  };
1229
- /**
1230
- * Get default network configuration
1231
- */
1232
1167
  function getDefaultNetwork() {
1233
1168
  return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
1234
1169
  }
1235
1170
 
1171
+ /** Scope names the 0xio wallet enforces. Any other name is dropped at connect. */
1172
+ const WALLET_PERMISSIONS = [
1173
+ 'accounts',
1174
+ 'public_transactions',
1175
+ 'contract_calls',
1176
+ 'contract_views',
1177
+ 'private_balance_read',
1178
+ 'private_proofs',
1179
+ 'private_transfers',
1180
+ 'private_claims',
1181
+ ];
1182
+ /** Older SDK permission names and the wallet scope each one means. */
1183
+ const LEGACY_PERMISSION_MAP = {
1184
+ read_address: 'accounts',
1185
+ read_balance: 'accounts',
1186
+ read_public_key: 'accounts',
1187
+ send_transactions: 'public_transactions',
1188
+ sign_messages: 'public_transactions',
1189
+ contract_calls: 'contract_calls',
1190
+ view_private_balance: 'private_balance_read',
1191
+ view_encrypted_balance: 'private_balance_read',
1192
+ stealth_scan: 'private_balance_read',
1193
+ decrypt_balance: 'private_balance_read',
1194
+ encrypt_balance: 'private_proofs',
1195
+ private_transfers: 'private_transfers',
1196
+ stealth_claim: 'private_claims',
1197
+ };
1198
+ /** Translate any mix of old and new names into the wallet's scope names, without duplicates. */
1199
+ function toWalletPermissions(perms) {
1200
+ const out = [];
1201
+ for (const p of perms ?? []) {
1202
+ const canonical = WALLET_PERMISSIONS.includes(p)
1203
+ ? p
1204
+ : LEGACY_PERMISSION_MAP[p];
1205
+ if (canonical && !out.includes(canonical))
1206
+ out.push(canonical);
1207
+ }
1208
+ return out;
1209
+ }
1236
1210
  /**
1237
- * 0xio Wallet SDK - Main Wallet Class
1238
- * Primary interface for DApp developers to interact with 0xio Wallet
1211
+ * The granted wallet scopes plus every requested old name they satisfy, so a dapp that checks
1212
+ * for the name it asked for (for example 'read_balance') keeps seeing it.
1239
1213
  */
1214
+ function withLegacyAliases(granted, requested) {
1215
+ const set = new Set(granted ?? []);
1216
+ for (const p of requested ?? []) {
1217
+ const canonical = LEGACY_PERMISSION_MAP[p];
1218
+ if (canonical && set.has(canonical))
1219
+ set.add(p);
1220
+ }
1221
+ return [...set];
1222
+ }
1223
+
1224
+ /**
1225
+ * 0xio Signed Message standard (v1).
1226
+ *
1227
+ * `wallet.signMessage(message)` never signs the raw message. The wallet frames it first so a signed
1228
+ * "message" can never collide with a transaction pre-image (a transaction is canonical JSON that
1229
+ * begins with '{'). The framing is:
1230
+ *
1231
+ * "Octra Signed Message:\n" + utf8ByteLength(message) + "\n" + message
1232
+ *
1233
+ * signed as an Ed25519 detached signature over the UTF-8 bytes of that string. The leading 'O'
1234
+ * guarantees the signed bytes never begin with '{', so a personal-message signature can never be a
1235
+ * valid transaction. Any verifier MUST reconstruct the same bytes: use `getSignedMessageBytes()`
1236
+ * with any Ed25519 library, or `verifyMessage()` for a batteries-included check.
1237
+ */
1238
+ /** Fixed prefix tag for the 0xio Signed Message scheme. */
1239
+ const SIGNED_MESSAGE_PREFIX = 'Octra Signed Message:';
1240
+ /** Scheme version, bumped if the framing ever changes so verifiers can detect it. */
1241
+ const SIGNED_MESSAGE_VERSION = 1;
1242
+ /** UTF-8 byte length of a string (JS `.length` counts UTF-16 units, not bytes). */
1243
+ function utf8ByteLength(s) {
1244
+ return new TextEncoder().encode(s).length;
1245
+ }
1246
+ function base64ToBytes(b64) {
1247
+ // atob is available in browsers and Node 16+; utils.deriveOctraAddress uses the same path.
1248
+ const bin = atob(b64);
1249
+ const out = new Uint8Array(bin.length);
1250
+ for (let i = 0; i < bin.length; i++)
1251
+ out[i] = bin.charCodeAt(i);
1252
+ return out;
1253
+ }
1254
+ /**
1255
+ * The exact bytes that `wallet.signMessage(message)` produces a signature over. Verify a 0xio
1256
+ * message signature by checking an Ed25519 signature against these bytes with the signer's public
1257
+ * key. Zero-dependency - bring your own Ed25519 verifier, or use `verifyMessage`.
1258
+ */
1259
+ function getSignedMessageBytes(message) {
1260
+ if (typeof message !== 'string') {
1261
+ throw new TypeError('message must be a string');
1262
+ }
1263
+ const framed = `${SIGNED_MESSAGE_PREFIX}\n${utf8ByteLength(message)}\n${message}`;
1264
+ return new TextEncoder().encode(framed);
1265
+ }
1266
+ /**
1267
+ * Reconstruct the auth message that `wallet.signAuthMessage(service, nonce)` signs. A relying
1268
+ * service verifies an auth signature with `verifyMessage(buildAuthMessage(service, nonce, origin),
1269
+ * signature, publicKey)`, where `origin` is the caller's page origin.
1270
+ */
1271
+ function buildAuthMessage(service, nonce, origin) {
1272
+ return `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
1273
+ }
1274
+ /**
1275
+ * Verify a 0xio message signature produced by `wallet.signMessage`.
1276
+ *
1277
+ * @param message The original message passed to `wallet.signMessage`.
1278
+ * @param signature Base64 Ed25519 signature returned by `wallet.signMessage`.
1279
+ * @param publicKey Base64 Ed25519 public key of the signer (from `wallet.getPublicKey()`).
1280
+ * @returns Whether the signature is valid for this message and key.
1281
+ *
1282
+ * Uses the Web Crypto Ed25519 primitive (Node 18+, Chrome 137+, Safari 17+, Firefox 129+). In an
1283
+ * environment without it, verify `getSignedMessageBytes(message)` with your own Ed25519 library.
1284
+ */
1285
+ async function verifyMessage(message, signature, publicKey) {
1286
+ if (typeof crypto === 'undefined' || !crypto.subtle) {
1287
+ throw new Error('Web Crypto API unavailable; verify getSignedMessageBytes(message) with your own Ed25519 library');
1288
+ }
1289
+ let sigBytes;
1290
+ let pubBytes;
1291
+ try {
1292
+ sigBytes = base64ToBytes(signature);
1293
+ pubBytes = base64ToBytes(publicKey);
1294
+ }
1295
+ catch {
1296
+ return false;
1297
+ }
1298
+ try {
1299
+ const data = new Uint8Array(getSignedMessageBytes(message));
1300
+ const key = await crypto.subtle.importKey('raw', pubBytes, { name: 'Ed25519' }, false, [
1301
+ 'verify',
1302
+ ]);
1303
+ return await crypto.subtle.verify({ name: 'Ed25519' }, key, sigBytes, data);
1304
+ }
1305
+ catch {
1306
+ // importKey/verify can throw (rather than return false) on malformed input or an environment
1307
+ // that recognizes Ed25519 only partially; treat any such failure as "not verified".
1308
+ return false;
1309
+ }
1310
+ }
1311
+
1240
1312
  class ZeroXIOWallet extends EventEmitter {
1241
1313
  constructor(config) {
1242
1314
  super(config.debug);
1243
1315
  this.connectionInfo = { isConnected: false };
1244
1316
  this.isInitialized = false;
1245
1317
  this._initPromise = null;
1246
- // session version stale write detection
1318
+ // session version, for stale write detection
1247
1319
  this._sessionVersion = 0;
1248
1320
  this.config = {
1249
1321
  ...config,
@@ -1252,42 +1324,31 @@ class ZeroXIOWallet extends EventEmitter {
1252
1324
  debug: config.debug || false
1253
1325
  };
1254
1326
  this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
1255
- this.communicator = new ExtensionCommunicator(this.config.debug, [], this.config.adapter);
1327
+ this.communicator = new ExtensionCommunicator(this.config.debug, this.config.trustedParentOrigins ?? [], this.config.adapter);
1256
1328
  this.logger.log('Wallet instance created with config:', this.config);
1257
1329
  }
1258
- // ===================
1259
- // INITIALIZATION
1260
- // ===================
1261
- /**
1262
- * Initialize the SDK
1263
- * Must be called before using any other methods
1264
- */
1265
1330
  async initialize() {
1266
1331
  if (this.isInitialized) {
1267
1332
  return true;
1268
1333
  }
1269
- // single-flight init
1270
1334
  if (this._initPromise) {
1271
1335
  return this._initPromise;
1272
1336
  }
1273
1337
  this._initPromise = (async () => {
1274
1338
  try {
1275
- // Initialize extension communication
1276
1339
  const communicationReady = await this.communicator.initialize();
1277
1340
  if (!communicationReady) {
1278
1341
  throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
1279
1342
  }
1280
- // Register this DApp with the extension
1281
1343
  await this.communicator.sendRequest('register_dapp', {
1282
1344
  appName: this.config.appName,
1283
1345
  appDescription: this.config.appDescription,
1284
1346
  appVersion: this.config.appVersion,
1285
1347
  appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
1286
1348
  appIcon: this.config.appIcon,
1287
- requiredPermissions: this.config.requiredPermissions,
1349
+ requiredPermissions: toWalletPermissions(this.config.requiredPermissions),
1288
1350
  networkId: this.config.networkId
1289
1351
  });
1290
- // Setup event forwarding from extension
1291
1352
  this.setupExtensionEventListeners();
1292
1353
  this.isInitialized = true;
1293
1354
  this.logger.log('SDK initialized successfully');
@@ -1306,37 +1367,29 @@ class ZeroXIOWallet extends EventEmitter {
1306
1367
  })();
1307
1368
  return this._initPromise;
1308
1369
  }
1309
- /**
1310
- * Check if SDK is initialized
1311
- */
1312
1370
  isReady() {
1313
1371
  return this.isInitialized && this.communicator.isExtensionAvailable();
1314
1372
  }
1315
- // ===================
1316
- // CONNECTION MANAGEMENT
1317
- // ===================
1318
- /**
1319
- * Connect to wallet
1320
- */
1321
1373
  async connect(options = {}) {
1322
1374
  this.ensureInitialized();
1323
1375
  try {
1324
1376
  this.logger.log('Attempting to connect with options:', options);
1325
- // filter to declared perms only
1377
+ // filter to declared perms only: accept both RFC 'permissions' and legacy 'requestPermissions'
1326
1378
  const declaredPermissions = this.config.requiredPermissions || [];
1327
- const requestPermissions = options.requestPermissions
1328
- ? options.requestPermissions.filter(p => declaredPermissions.includes(p))
1379
+ const requestedPerms = options.permissions ?? options.requestPermissions;
1380
+ const requestedPermissions = requestedPerms
1381
+ ? requestedPerms.filter(p => declaredPermissions.includes(p))
1329
1382
  : declaredPermissions;
1330
1383
  const result = await this.communicator.sendRequest('connect', {
1331
- requestPermissions,
1384
+ permissions: toWalletPermissions(requestedPermissions),
1332
1385
  networkId: options.networkId || this.config.networkId
1333
1386
  });
1334
- // verify pubkey→addr binding
1387
+ // verify the public key to address binding
1335
1388
  if (result.publicKey && result.address) {
1336
1389
  try {
1337
1390
  const derived = await deriveOctraAddress(result.publicKey);
1338
1391
  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');
1392
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed: the reported public key does not derive to the reported address');
1340
1393
  }
1341
1394
  }
1342
1395
  catch (e) {
@@ -1345,11 +1398,16 @@ class ZeroXIOWallet extends EventEmitter {
1345
1398
  this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
1346
1399
  }
1347
1400
  }
1348
- // Use networkInfo from extension response validate before caching
1401
+ // Use networkInfo from the extension response, validated before caching.
1349
1402
  const networkInfo = validateNetworkInfo(result.networkInfo)
1350
- ?? getNetworkConfig(result.networkId || this.config.networkId);
1351
- const permissions = result.permissions || [];
1352
- // Update connection info including permissions
1403
+ ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1404
+ if (!networkInfo) {
1405
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
1406
+ }
1407
+ // The wallet answers with its own scope names; the names the dapp asked for are kept as
1408
+ // aliases so existing permission checks keep working.
1409
+ const permissions = withLegacyAliases(result.permissions, requestedPermissions);
1410
+ // Update connection info, including permissions
1353
1411
  this.connectionInfo = {
1354
1412
  isConnected: true,
1355
1413
  address: result.address,
@@ -1423,12 +1481,12 @@ class ZeroXIOWallet extends EventEmitter {
1423
1481
  if (this._sessionVersion !== sv)
1424
1482
  return { ...this.connectionInfo };
1425
1483
  if (result.isConnected && result.address) {
1426
- // verify pubkey→addr binding
1484
+ // verify the public key to address binding
1427
1485
  if (result.publicKey) {
1428
1486
  try {
1429
1487
  const derived = await deriveOctraAddress(result.publicKey);
1430
1488
  if (derived !== result.address) {
1431
- this.logger.warn('Address-key binding mismatch on session restore ignoring stale session');
1489
+ this.logger.warn('Address-key binding mismatch on session restore, ignoring stale session');
1432
1490
  this.connectionInfo = { isConnected: false };
1433
1491
  return { ...this.connectionInfo };
1434
1492
  }
@@ -1440,9 +1498,13 @@ class ZeroXIOWallet extends EventEmitter {
1440
1498
  // validate untrusted balance/networkInfo before caching
1441
1499
  const balanceInfo = validateBalance(result.balance) ?? createDefaultBalance();
1442
1500
  const networkInfo = validateNetworkInfo(result.networkInfo)
1443
- ?? getNetworkConfig(result.networkId || this.config.networkId);
1501
+ ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1502
+ if (!networkInfo) {
1503
+ this.logger.warn('getConnectionStatus: wallet returned no network metadata, returning cached state');
1504
+ return this.connectionInfo;
1505
+ }
1444
1506
  const wasConnected = this.connectionInfo.isConnected;
1445
- const permissions = result.permissions || [];
1507
+ const permissions = withLegacyAliases(result.permissions, this.config.requiredPermissions);
1446
1508
  // preserve existing connectedAt
1447
1509
  const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
1448
1510
  this.connectionInfo = {
@@ -1455,7 +1517,7 @@ class ZeroXIOWallet extends EventEmitter {
1455
1517
  permissions
1456
1518
  };
1457
1519
  this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
1458
- // only emit on disconnectedconnected transition
1520
+ // only emit on the disconnected to connected transition
1459
1521
  if (!wasConnected) {
1460
1522
  const connectEvent = {
1461
1523
  address: result.address,
@@ -1480,8 +1542,8 @@ class ZeroXIOWallet extends EventEmitter {
1480
1542
  }
1481
1543
  }
1482
1544
  /**
1483
- * Switch the extension's active network (e.g. 'mainnet' 'devnet').
1484
- * Works silently no popup, no user confirmation needed.
1545
+ * Switch the extension's active network (e.g. 'mainnet' to 'devnet').
1546
+ * Works silently: no popup, no user confirmation needed.
1485
1547
  * The extension broadcasts 'networkChanged' event to all connected dApps.
1486
1548
  */
1487
1549
  async switchNetwork(networkId) {
@@ -1513,18 +1575,25 @@ class ZeroXIOWallet extends EventEmitter {
1513
1575
  getNetworkId() {
1514
1576
  return this.connectionInfo.networkInfo?.id || null;
1515
1577
  }
1516
- // ===================
1517
- // WALLET INFORMATION
1518
- // ===================
1519
- /**
1520
- * Get current wallet address
1521
- */
1522
1578
  getAddress() {
1523
1579
  return this.connectionInfo.address || null;
1524
1580
  }
1525
1581
  /**
1526
- * Get current balance
1582
+ * The connected account's Ed25519 public key (base64). Served from the session when the
1583
+ * wallet reported it at connect, otherwise asked from the wallet.
1527
1584
  */
1585
+ async getPublicKey() {
1586
+ this.ensureConnected();
1587
+ if (this.connectionInfo.publicKey)
1588
+ return this.connectionInfo.publicKey;
1589
+ const result = await this.communicator.sendRequest('getPublicKey');
1590
+ const publicKey = result?.publicKey;
1591
+ if (typeof publicKey !== 'string' || !publicKey) {
1592
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
1593
+ }
1594
+ this.connectionInfo.publicKey = publicKey;
1595
+ return publicKey;
1596
+ }
1528
1597
  async getBalance(forceRefresh = false) {
1529
1598
  this.ensureConnected();
1530
1599
  try {
@@ -1549,7 +1618,6 @@ class ZeroXIOWallet extends EventEmitter {
1549
1618
  // skip if session changed mid-flight
1550
1619
  if (this._sessionVersion !== sv)
1551
1620
  return result;
1552
- // Update cached balance
1553
1621
  if (this.connectionInfo.balance) {
1554
1622
  const previousBalance = this.connectionInfo.balance;
1555
1623
  this.connectionInfo.balance = result;
@@ -1576,15 +1644,11 @@ class ZeroXIOWallet extends EventEmitter {
1576
1644
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
1577
1645
  }
1578
1646
  }
1579
- /**
1580
- * Get network information
1581
- */
1582
1647
  async getNetworkInfo() {
1583
1648
  this.ensureInitialized();
1584
1649
  try {
1585
1650
  const sv = this._sessionVersion;
1586
1651
  const result = await this.communicator.sendRequest('get_network_info');
1587
- // validate network info before caching
1588
1652
  const networkInfo = validateNetworkInfo(result);
1589
1653
  if (!networkInfo) {
1590
1654
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Extension returned invalid network info');
@@ -1592,11 +1656,9 @@ class ZeroXIOWallet extends EventEmitter {
1592
1656
  // skip if session changed mid-flight
1593
1657
  if (this._sessionVersion !== sv)
1594
1658
  return networkInfo;
1595
- // Update cached network info
1596
1659
  if (this.connectionInfo.networkInfo) {
1597
1660
  const previousNetwork = this.connectionInfo.networkInfo;
1598
1661
  this.connectionInfo.networkInfo = networkInfo;
1599
- // Emit network changed event if different
1600
1662
  if (previousNetwork.id !== networkInfo.id) {
1601
1663
  const networkChangedEvent = {
1602
1664
  previousNetwork,
@@ -1616,32 +1678,22 @@ class ZeroXIOWallet extends EventEmitter {
1616
1678
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
1617
1679
  }
1618
1680
  }
1619
- // ===================
1620
- // TRANSACTIONS
1621
- // ===================
1622
- /**
1623
- * Send transaction
1624
- */
1625
1681
  async sendTransaction(txData) {
1626
1682
  this.ensureConnected();
1627
- // validate inputs
1628
1683
  if (!isValidAddress(txData.to)) {
1629
1684
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1630
1685
  }
1631
- if (!isValidAmount(txData.amount)) {
1632
- throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1633
- }
1634
- // bound memo
1686
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1635
1687
  if (txData.message && txData.message.length > 1000) {
1636
1688
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1637
1689
  }
1638
1690
  try {
1639
1691
  // log non-sensitive only
1640
1692
  this.logger.log('Sending transaction:', { to: txData.to });
1641
- const result = await this.communicator.sendRequest('send_transaction', txData);
1693
+ const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
1642
1694
  this.logger.log('Transaction result:', result);
1643
- // Refresh balance after successful transaction
1644
- if (result.success) {
1695
+ // Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
1696
+ if (result.accepted ?? result.success) {
1645
1697
  setTimeout(() => {
1646
1698
  this.getBalance(true).catch(error => {
1647
1699
  this.logger.warn('Failed to refresh balance after transaction:', error);
@@ -1658,23 +1710,74 @@ class ZeroXIOWallet extends EventEmitter {
1658
1710
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to send transaction', error);
1659
1711
  }
1660
1712
  }
1713
+ /**
1714
+ * Sign a transaction without broadcasting it (RFC-O-1 octra_signTransaction).
1715
+ * Returns the signed transaction object for manual submission via submitTransaction().
1716
+ */
1717
+ async signTransaction(txData) {
1718
+ this.ensureConnected();
1719
+ if (!isValidAddress(txData.to)) {
1720
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1721
+ }
1722
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1723
+ if (txData.message && txData.message.length > 1000) {
1724
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1725
+ }
1726
+ try {
1727
+ this.logger.log('Requesting transaction signature:', { to: txData.to });
1728
+ const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
1729
+ return result;
1730
+ }
1731
+ catch (error) {
1732
+ if (error instanceof ZeroXIOWalletError)
1733
+ throw error;
1734
+ throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Failed to sign transaction', error);
1735
+ }
1736
+ }
1737
+ /**
1738
+ * Broadcast a pre-signed transaction (RFC-O-1 octra_submitTransaction).
1739
+ * Use after signTransaction() to submit the signed tx to the network.
1740
+ */
1741
+ async submitTransaction(signedTx) {
1742
+ this.ensureConnected();
1743
+ if (!signedTx || typeof signedTx !== 'object') {
1744
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'signedTx must be an object');
1745
+ }
1746
+ try {
1747
+ this.logger.log('Submitting pre-signed transaction');
1748
+ const result = await this.communicator.sendRequest('broadcast_only', { signedTx });
1749
+ return result;
1750
+ }
1751
+ catch (error) {
1752
+ if (error instanceof ZeroXIOWalletError)
1753
+ throw error;
1754
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to submit transaction', error);
1755
+ }
1756
+ }
1661
1757
  /**
1662
1758
  * Call a smart contract method (state-changing).
1663
1759
  * The extension builds, signs, and submits the transaction via octra_submit.
1664
1760
  */
1665
1761
  async callContract(callData) {
1666
1762
  this.ensureConnected();
1667
- // validate inputs
1668
1763
  if (!isValidAddress(callData.contract)) {
1669
1764
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1670
1765
  }
1671
1766
  if (!callData.method || typeof callData.method !== 'string') {
1672
1767
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method is required');
1673
1768
  }
1674
- // bound method + params size
1675
1769
  if (callData.method.length > 200) {
1676
1770
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
1677
1771
  }
1772
+ if (callData.amount != null && callData.amountOct != null) {
1773
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
1774
+ }
1775
+ if (callData.amount != null) {
1776
+ this.assertExactOCTAmount(callData.amount, 'Contract call amount');
1777
+ }
1778
+ if (callData.amountOct != null) {
1779
+ this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
1780
+ }
1678
1781
  try {
1679
1782
  if (JSON.stringify(callData.params).length > 65536) {
1680
1783
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
@@ -1692,7 +1795,11 @@ class ZeroXIOWallet extends EventEmitter {
1692
1795
  contract: callData.contract,
1693
1796
  method: callData.method,
1694
1797
  params: callData.params,
1695
- amount: callData.amount != null ? String(callData.amount) : '0',
1798
+ amount: callData.amountOct != null
1799
+ ? octToMicro(callData.amountOct)
1800
+ : callData.amount != null
1801
+ ? String(callData.amount)
1802
+ : '0',
1696
1803
  ou: callData.ou != null ? String(callData.ou) : '10000',
1697
1804
  });
1698
1805
  this.logger.log('Contract call result:', result);
@@ -1712,14 +1819,12 @@ class ZeroXIOWallet extends EventEmitter {
1712
1819
  */
1713
1820
  async contractCallView(viewData) {
1714
1821
  this.ensureInitialized();
1715
- // validate inputs
1716
1822
  if (!isValidAddress(viewData.contract)) {
1717
1823
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1718
1824
  }
1719
1825
  if (!viewData.method || typeof viewData.method !== 'string') {
1720
1826
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method is required');
1721
1827
  }
1722
- // bound method + params size
1723
1828
  if (viewData.method.length > 200) {
1724
1829
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method name too long (max 200 characters)');
1725
1830
  }
@@ -1759,7 +1864,6 @@ class ZeroXIOWallet extends EventEmitter {
1759
1864
  */
1760
1865
  async getContractStorage(contract, key) {
1761
1866
  this.ensureInitialized();
1762
- // validate inputs
1763
1867
  if (!isValidAddress(contract)) {
1764
1868
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1765
1869
  }
@@ -1801,12 +1905,6 @@ class ZeroXIOWallet extends EventEmitter {
1801
1905
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get transaction history', error);
1802
1906
  }
1803
1907
  }
1804
- // ===================
1805
- // PRIVATE FEATURES
1806
- // ===================
1807
- /**
1808
- * Get private balance information
1809
- */
1810
1908
  async getPrivateBalanceInfo() {
1811
1909
  this.ensureConnected();
1812
1910
  try {
@@ -1818,11 +1916,13 @@ class ZeroXIOWallet extends EventEmitter {
1818
1916
  }
1819
1917
  }
1820
1918
  /**
1821
- * Encrypt public balance to private
1919
+ * Encrypt public balance to private.
1920
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1921
+ * NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
1822
1922
  */
1823
1923
  async encryptBalance(amount) {
1824
1924
  this.ensureConnected();
1825
- // validate amount
1925
+ this.assertExactOCTAmount(amount, 'Encrypt amount');
1826
1926
  if (!isValidAmount(amount)) {
1827
1927
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1828
1928
  }
@@ -1839,11 +1939,13 @@ class ZeroXIOWallet extends EventEmitter {
1839
1939
  }
1840
1940
  }
1841
1941
  /**
1842
- * Decrypt private balance to public
1942
+ * Decrypt private balance to public.
1943
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1944
+ * NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
1843
1945
  */
1844
1946
  async decryptBalance(amount) {
1845
1947
  this.ensureConnected();
1846
- // validate amount
1948
+ this.assertExactOCTAmount(amount, 'Decrypt amount');
1847
1949
  if (!isValidAmount(amount)) {
1848
1950
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1849
1951
  }
@@ -1869,21 +1971,24 @@ class ZeroXIOWallet extends EventEmitter {
1869
1971
  */
1870
1972
  async sendPrivateTransfer(transferData) {
1871
1973
  this.ensureConnected();
1872
- // validate inputs
1873
1974
  if (!isValidAddress(transferData.to)) {
1874
1975
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1875
1976
  }
1876
1977
  if (!isValidAmount(transferData.amount)) {
1877
1978
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transfer amount');
1878
1979
  }
1980
+ this.assertExactOCTAmount(transferData.amount, 'Transfer amount');
1879
1981
  // bound msg size
1880
1982
  if (transferData.message && transferData.message.length > 1000) {
1881
1983
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
1882
1984
  }
1883
1985
  try {
1884
- const result = await this.communicator.sendRequest('send_private_transfer', transferData);
1885
- // Refresh balance after transfer
1886
- if (result.success) {
1986
+ const result = await this.communicator.sendRequest('send_private_transfer', {
1987
+ ...transferData,
1988
+ ...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
1989
+ });
1990
+ // Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
1991
+ if (result.accepted ?? result.success) {
1887
1992
  setTimeout(() => {
1888
1993
  this.getBalance(true).catch(() => { });
1889
1994
  }, 1000);
@@ -1923,8 +2028,8 @@ class ZeroXIOWallet extends EventEmitter {
1923
2028
  const result = await this.communicator.sendRequest('claim_private_transfer', {
1924
2029
  transferId
1925
2030
  });
1926
- // Refresh balance after claiming
1927
- if (result.success) {
2031
+ // Refresh balance after claiming (accept RFC 'accepted' or legacy 'success')
2032
+ if (result.accepted ?? result.success) {
1928
2033
  setTimeout(() => {
1929
2034
  this.getBalance(true).catch(() => { });
1930
2035
  }, 1000);
@@ -1935,19 +2040,89 @@ class ZeroXIOWallet extends EventEmitter {
1935
2040
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
1936
2041
  }
1937
2042
  }
1938
- // ===================
1939
- // MESSAGE SIGNING
1940
- // ===================
2043
+ // Generic provider passthrough and RFP private primitives (since 2.8.0)
2044
+ // These route through the wallet bridge. The wallet keeps all private/FHE
2045
+ // secret material internal and returns only ciphertexts/proofs/tx hashes.
2046
+ /**
2047
+ * Send any wallet method + params through the bridge. Escape hatch for
2048
+ * primitives that don't have a typed helper yet (no SDK upgrade needed).
2049
+ * @since 2.8.0
2050
+ */
2051
+ async request(method, params = {}) {
2052
+ return this.communicator.sendRequest(method, params);
2053
+ }
2054
+ /**
2055
+ * Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
2056
+ * (octra_balance, octra_transaction, contract_call and similar); writes are refused.
2057
+ * @since 2.8.0
2058
+ */
2059
+ async rpcCall(method, params = []) {
2060
+ return this.communicator.sendRequest('rpc_call', { method, params });
2061
+ }
1941
2062
  /**
1942
- * Sign an arbitrary message with the wallet's private key
1943
- * The user will be prompted to approve the signature request in the extension
2063
+ * Feature-detect which private capabilities the connected wallet supports.
2064
+ * Lets a dapp render the correct UI (or fail closed) before any action.
2065
+ * @since 2.8.0
2066
+ */
2067
+ async getPrivateCapabilities() {
2068
+ return this.communicator.sendRequest('get_private_capabilities');
2069
+ }
2070
+ /** Read-only contract view (no approval popup). @since 2.8.0 */
2071
+ async callContractView(params) {
2072
+ this.ensureConnected();
2073
+ return this.communicator.sendRequest('contract_call_view', params);
2074
+ }
2075
+ /** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
2076
+ async encryptValue(params) {
2077
+ this.ensureConnected();
2078
+ return this.communicator.sendRequest('encrypt_value', params);
2079
+ }
2080
+ /** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
2081
+ async decryptValue(params) {
2082
+ this.ensureConnected();
2083
+ return this.communicator.sendRequest('decrypt_value', params);
2084
+ }
2085
+ /** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
2086
+ async makeZeroProof(params) {
2087
+ this.ensureConnected();
2088
+ return this.communicator.sendRequest('make_zero_proof', params);
2089
+ }
2090
+ /** Range proof for a ciphertext. @since 2.8.0 */
2091
+ async makeRangeProof(params) {
2092
+ this.ensureConnected();
2093
+ return this.communicator.sendRequest('make_range_proof', params);
2094
+ }
2095
+ /** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
2096
+ async getPrivateBalance(params = {}) {
2097
+ this.ensureConnected();
2098
+ return this.communicator.sendRequest('get_private_balance', params);
2099
+ }
2100
+ /** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
2101
+ async registerPrivateViewKey(params) {
2102
+ this.ensureConnected();
2103
+ return this.communicator.sendRequest('register_private_view_key', params);
2104
+ }
2105
+ /** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
2106
+ async sendContractTransactionSequence(params) {
2107
+ this.ensureConnected();
2108
+ return this.communicator.sendRequest('send_contract_transaction_sequence', params);
2109
+ }
2110
+ /**
2111
+ * Sign an arbitrary message with the wallet's private key.
2112
+ * The user will be prompted to approve the signature request in the extension.
2113
+ *
2114
+ * The wallet does not sign the raw message: it signs the 0xio Signed Message framing
2115
+ * (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
2116
+ * transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
2117
+ * Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
2118
+ *
1944
2119
  * @param message - The message to sign (non-empty string)
1945
2120
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1946
2121
  * @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
1947
2122
  * @example
1948
2123
  * ```typescript
1949
2124
  * const signature = await wallet.signMessage('Hello, 0xio!');
1950
- * console.log('Signature:', signature);
2125
+ * const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
1951
2126
  * ```
1952
2127
  */
1953
2128
  async signMessage(message) {
@@ -1975,16 +2150,13 @@ class ZeroXIOWallet extends EventEmitter {
1975
2150
  throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Failed to sign message', error);
1976
2151
  }
1977
2152
  }
1978
- // ===================
1979
- // AUTHENTICATION HELPERS
1980
- // ===================
1981
2153
  /**
1982
2154
  * Sign a domain-separated authentication message.
1983
2155
  * Unlike `signMessage()`, this prepends a standard header that binds the signature
1984
2156
  * to the calling service and a one-time nonce, preventing cross-service replay attacks.
1985
2157
  *
1986
2158
  * @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
2159
+ * @param nonce - Unique one-time value. Use a server-generated UUID or challenge
1988
2160
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1989
2161
  */
1990
2162
  async signAuthMessage(service, nonce) {
@@ -1996,12 +2168,8 @@ class ZeroXIOWallet extends EventEmitter {
1996
2168
  throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
1997
2169
  }
1998
2170
  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);
2171
+ return this.signMessage(buildAuthMessage(service, nonce, origin));
2001
2172
  }
2002
- // ===================
2003
- // PRIVATE METHODS
2004
- // ===================
2005
2173
  ensureInitialized() {
2006
2174
  if (!this.isInitialized) {
2007
2175
  throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'SDK not initialized. Call initialize() first.');
@@ -2014,7 +2182,6 @@ class ZeroXIOWallet extends EventEmitter {
2014
2182
  }
2015
2183
  }
2016
2184
  setupExtensionEventListeners() {
2017
- // Listen for extension events through the communicator
2018
2185
  this.communicator.on('accountChanged', (event) => {
2019
2186
  this.handleAccountChanged(event.data);
2020
2187
  });
@@ -2033,11 +2200,21 @@ class ZeroXIOWallet extends EventEmitter {
2033
2200
  this.communicator.on('transactionConfirmed', (event) => {
2034
2201
  this.handleTransactionConfirmed(event.data);
2035
2202
  });
2203
+ this.communicator.on('transactionFailed', (event) => {
2204
+ this.emit('transactionFailed', event.data ?? event);
2205
+ });
2206
+ this.communicator.on('permissionsChanged', (event) => {
2207
+ const permissions = event.data ?? event;
2208
+ if (this.connectionInfo.isConnected) {
2209
+ this.connectionInfo.permissions = Array.isArray(permissions) ? permissions : [];
2210
+ }
2211
+ this.emit('permissionsChanged', permissions);
2212
+ });
2213
+ this.communicator.on('message', (event) => {
2214
+ this.emit('message', event.data ?? event);
2215
+ });
2036
2216
  this.logger.log('Extension event listeners setup complete');
2037
2217
  }
2038
- /**
2039
- * Handle account changed event from extension
2040
- */
2041
2218
  handleAccountChanged(data) {
2042
2219
  ++this._sessionVersion;
2043
2220
  const previousAddress = this.connectionInfo.address;
@@ -2045,7 +2222,6 @@ class ZeroXIOWallet extends EventEmitter {
2045
2222
  // clear stale pubkey on acct change
2046
2223
  this.connectionInfo.publicKey = data.publicKey;
2047
2224
  if (data.balance) {
2048
- // validate balance before caching
2049
2225
  const validated = validateBalance(data.balance);
2050
2226
  if (validated) {
2051
2227
  this.connectionInfo.balance = validated;
@@ -2063,12 +2239,9 @@ class ZeroXIOWallet extends EventEmitter {
2063
2239
  this.emit('accountChanged', accountChangedEvent);
2064
2240
  this.logger.log('Account changed:', { newAddress: accountChangedEvent.newAddress });
2065
2241
  }
2066
- /**
2067
- * Handle network changed event from extension
2068
- */
2069
2242
  handleNetworkChanged(data) {
2070
2243
  const previousNetwork = this.connectionInfo.networkInfo;
2071
- // validate networkInfo drop invalid
2244
+ // validate networkInfo, drop invalid
2072
2245
  const networkInfo = validateNetworkInfo(data.networkInfo);
2073
2246
  if (!networkInfo) {
2074
2247
  this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
@@ -2084,11 +2257,7 @@ class ZeroXIOWallet extends EventEmitter {
2084
2257
  this.emit('networkChanged', networkChangedEvent);
2085
2258
  this.logger.log('Network changed:', networkChangedEvent);
2086
2259
  }
2087
- /**
2088
- * Handle balance changed event from extension
2089
- */
2090
2260
  handleBalanceChanged(data) {
2091
- // validate balance before caching
2092
2261
  const balance = validateBalance(data.balance);
2093
2262
  if (!balance) {
2094
2263
  this.logger.warn('Received invalid balance in balanceChanged event, ignoring');
@@ -2104,13 +2273,9 @@ class ZeroXIOWallet extends EventEmitter {
2104
2273
  this.emit('balanceChanged', balanceChangedEvent);
2105
2274
  this.logger.log('Balance changed:', { public: balance.public });
2106
2275
  }
2107
- /**
2108
- * Handle extension locked event
2109
- */
2110
2276
  handleExtensionLocked() {
2111
2277
  ++this._sessionVersion;
2112
2278
  this.connectionInfo = { isConnected: false };
2113
- // emit extensionLocked then disconnect
2114
2279
  this.emit('extensionLocked', {});
2115
2280
  const disconnectEvent = {
2116
2281
  reason: 'extension_locked'
@@ -2118,21 +2283,13 @@ class ZeroXIOWallet extends EventEmitter {
2118
2283
  this.emit('disconnect', disconnectEvent);
2119
2284
  this.logger.log('Extension locked - disconnected');
2120
2285
  }
2121
- /**
2122
- * Handle extension unlocked event
2123
- */
2124
2286
  handleExtensionUnlocked() {
2125
- // emit extensionUnlocked
2126
2287
  this.emit('extensionUnlocked', {});
2127
- // Attempt to restore connection
2128
2288
  this.getConnectionStatus().catch(() => {
2129
2289
  this.logger.warn('Could not restore connection after unlock');
2130
2290
  });
2131
2291
  this.logger.log('Extension unlocked');
2132
2292
  }
2133
- /**
2134
- * Handle transaction confirmed event
2135
- */
2136
2293
  handleTransactionConfirmed(data) {
2137
2294
  this.emit('transactionConfirmed', {
2138
2295
  txHash: data.txHash,
@@ -2145,12 +2302,38 @@ class ZeroXIOWallet extends EventEmitter {
2145
2302
  }, 2000);
2146
2303
  this.logger.log('Transaction confirmed:', data.txHash);
2147
2304
  }
2148
- // ===================
2149
- // CLEANUP
2150
- // ===================
2151
2305
  /**
2152
- * Clean up SDK resources
2306
+ * The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
2307
+ * dapp sends today); `amountOct` is converted exactly. Never both.
2308
+ */
2309
+ resolveRawAmount(amount, amountOct, label) {
2310
+ if (amount != null && amountOct != null) {
2311
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
2312
+ }
2313
+ if (amountOct != null) {
2314
+ this.assertExactOCTAmount(amountOct, label);
2315
+ return octToMicro(amountOct);
2316
+ }
2317
+ if (amount == null || !isValidAmount(amount)) {
2318
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
2319
+ }
2320
+ return amount;
2321
+ }
2322
+ /**
2323
+ * Reject numeric amounts that cannot be represented exactly in micro-OCT.
2324
+ * e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
2325
+ * String amounts bypass this check (caller is responsible for correctness).
2153
2326
  */
2327
+ assertExactOCTAmount(amount, label) {
2328
+ if (typeof amount === 'number') {
2329
+ const micro = Math.round(amount * 1000000);
2330
+ if (Math.abs(amount - micro / 1000000) > 1e-10) {
2331
+ const suggested = (micro / 1000000).toFixed(6);
2332
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label} cannot be represented exactly in micro-OCT. ` +
2333
+ `Pass a string instead (e.g. "${suggested}").`);
2334
+ }
2335
+ }
2336
+ }
2154
2337
  cleanup() {
2155
2338
  this.communicator.cleanup();
2156
2339
  this.removeAllListeners();
@@ -2172,7 +2355,7 @@ var wallet = /*#__PURE__*/Object.freeze({
2172
2355
  *
2173
2356
  * Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
2174
2357
  * window.octra.isOctra === true
2175
- * window.octra.request({ method, params }) Promise<unknown>
2358
+ * window.octra.request({ method, params }) returns Promise<unknown>
2176
2359
  * window.octra.on(event, listener) / removeListener(event, listener)
2177
2360
  *
2178
2361
  * This adapter translates the SDK's internal method names into RFC-O-1 method
@@ -2181,25 +2364,35 @@ var wallet = /*#__PURE__*/Object.freeze({
2181
2364
  * Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
2182
2365
  * are passed through as-is; the wallet's request() handles or rejects them.
2183
2366
  */
2184
- /** SDK method RFC-O-1 method name */
2367
+ /** SDK method to RFC-O-1 method name */
2185
2368
  const SDK_TO_RFC = {
2186
2369
  get_network_info: 'octra_networkInfo',
2187
2370
  switch_network: 'octra_switchNetwork',
2188
2371
  signMessage: 'octra_signMessage',
2189
2372
  send_transaction: 'octra_sendTransaction',
2190
- call_contract: 'octra_callContract',
2373
+ sign_transaction: 'octra_signTransaction',
2374
+ broadcast_only: 'octra_submitTransaction',
2375
+ call_contract: 'octra_sendContractTransaction',
2191
2376
  contract_call_view: 'octra_callContract',
2192
2377
  get_private_balance_info: 'octra_getEncryptedBalance',
2193
2378
  encrypt_balance: 'octra_encryptBalance',
2194
2379
  decrypt_balance: 'octra_decryptBalance',
2195
2380
  send_private_transfer: 'octra_sendPrivateTransfer',
2196
2381
  claim_private_transfer: 'octra_claimStealth',
2382
+ get_private_capabilities: 'octra_getPrivateCapabilities',
2383
+ encrypt_value: 'octra_encryptValue',
2384
+ decrypt_value: 'octra_decryptValue',
2385
+ make_zero_proof: 'octra_makeZeroProof',
2386
+ make_range_proof: 'octra_makeRangeProof',
2387
+ get_private_balance: 'octra_getPrivateBalance',
2388
+ register_private_view_key: 'octra_registerPrivateViewKey',
2389
+ send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
2197
2390
  };
2198
- /** RFC-O-1 error code SDK ErrorCode string */
2391
+ /** RFC-O-1 error code to SDK ErrorCode string */
2199
2392
  const RFC_TO_SDK_ERROR = {
2200
2393
  4001: 'USER_REJECTED',
2201
2394
  4100: 'PERMISSION_DENIED',
2202
- 4200: 'NETWORK_ERROR',
2395
+ 4200: 'UNKNOWN_ERROR',
2203
2396
  4900: 'CONNECTION_REFUSED',
2204
2397
  4901: 'NETWORK_ERROR',
2205
2398
  };
@@ -2216,7 +2409,7 @@ function mapError(err) {
2216
2409
  * three RFC-O-1 calls: octra_requestAccounts, octra_networkInfo, octra_permissions.
2217
2410
  */
2218
2411
  async function rfcConnect(provider, params) {
2219
- const requestPerms = params?.requestPermissions ?? [];
2412
+ const requestPerms = params?.permissions ?? params?.requestPermissions ?? [];
2220
2413
  const accounts = (await provider.request({
2221
2414
  method: 'octra_requestAccounts',
2222
2415
  params: [{ permissions: requestPerms }],
@@ -2297,19 +2490,21 @@ function createOctraProviderAdapter() {
2297
2490
  const provider = getProvider();
2298
2491
  if (!provider)
2299
2492
  return () => { _handler = null; };
2300
- // RFC-O-1 event SDK event name + data shape
2493
+ // RFC-O-1 event to SDK event name and data shape
2301
2494
  const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
2302
2495
  const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
2303
2496
  const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
2304
2497
  const onNetworkChanged = (data) => handler({ eventType: 'networkChanged', eventData: { networkInfo: data } });
2305
2498
  const onBalanceChanged = (data) => handler({ eventType: 'balanceChanged', eventData: data });
2306
2499
  const onTransactionChanged = (data) => handler({ eventType: 'transactionConfirmed', eventData: data });
2500
+ const onPermissionsChanged = (data) => handler({ eventType: 'permissionsChanged', eventData: data });
2307
2501
  provider.on('connect', onConnect);
2308
2502
  provider.on('disconnect', onDisconnect);
2309
2503
  provider.on('accountsChanged', onAccountsChanged);
2310
2504
  provider.on('networkChanged', onNetworkChanged);
2311
2505
  provider.on('balanceChanged', onBalanceChanged);
2312
2506
  provider.on('transactionChanged', onTransactionChanged);
2507
+ provider.on('permissionsChanged', onPermissionsChanged);
2313
2508
  const cleanup = () => {
2314
2509
  provider.removeListener('connect', onConnect);
2315
2510
  provider.removeListener('disconnect', onDisconnect);
@@ -2317,6 +2512,7 @@ function createOctraProviderAdapter() {
2317
2512
  provider.removeListener('networkChanged', onNetworkChanged);
2318
2513
  provider.removeListener('balanceChanged', onBalanceChanged);
2319
2514
  provider.removeListener('transactionChanged', onTransactionChanged);
2515
+ provider.removeListener('permissionsChanged', onPermissionsChanged);
2320
2516
  _handler = null;
2321
2517
  };
2322
2518
  return cleanup;
@@ -2324,7 +2520,11 @@ function createOctraProviderAdapter() {
2324
2520
  listenForReady(onReady) {
2325
2521
  const handler = () => onReady();
2326
2522
  window.addEventListener('octraWalletReady', handler);
2327
- return () => window.removeEventListener('octraWalletReady', handler);
2523
+ window.addEventListener('octra#initialized', handler);
2524
+ return () => {
2525
+ window.removeEventListener('octraWalletReady', handler);
2526
+ window.removeEventListener('octra#initialized', handler);
2527
+ };
2328
2528
  },
2329
2529
  };
2330
2530
  }
@@ -2332,15 +2532,15 @@ function createOctraProviderAdapter() {
2332
2532
  const OctraProviderAdapter = createOctraProviderAdapter();
2333
2533
 
2334
2534
  /**
2335
- * 0xio SDK Wallet Adapter Registry
2535
+ * 0xio SDK: Wallet Adapter Registry
2336
2536
  *
2337
2537
  * Add new wallet adapters here. Detection order determines which wallet takes
2338
2538
  * priority when multiple wallets are installed at the same time.
2339
2539
  */
2340
2540
  const REGISTERED_ADAPTERS = [
2341
- ZeroXIOAdapter, // 0xio extension (postMessage protocol) highest priority
2541
+ ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
2342
2542
  OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
2343
- // Add new wallet adapters here detection runs in order, first match wins
2543
+ // Add new wallet adapters here: detection runs in order, first match wins
2344
2544
  ];
2345
2545
  /**
2346
2546
  * Auto-detects the first available wallet in the current page.
@@ -2387,7 +2587,7 @@ function getAllAdapters() {
2387
2587
  */
2388
2588
  // Main exports
2389
2589
  // Version information
2390
- const SDK_VERSION = '2.7.0';
2590
+ const SDK_VERSION = '2.8.0';
2391
2591
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
2392
2592
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
2393
2593
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -2405,8 +2605,7 @@ async function createZeroXIOWallet(config) {
2405
2605
  try {
2406
2606
  await wallet$1.connect();
2407
2607
  }
2408
- catch (error) {
2409
- if (config.debug) ;
2608
+ catch {
2410
2609
  // Don't throw - let the app handle connection manually
2411
2610
  }
2412
2611
  }
@@ -2416,7 +2615,7 @@ async function createZeroXIOWallet(config) {
2416
2615
  function checkSDKCompatibility() {
2417
2616
  const issues = [];
2418
2617
  const recommendations = [];
2419
- // Hard blockers SDK cannot function without these
2618
+ // Hard blockers: the SDK cannot function without these
2420
2619
  if (typeof window === 'undefined') {
2421
2620
  issues.push('Window object not available');
2422
2621
  recommendations.push('SDK must be used in a browser environment');
@@ -2481,7 +2680,7 @@ if (typeof window !== 'undefined') {
2481
2680
  debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
2482
2681
  environment: isDevelopment ? 'development' : 'production'
2483
2682
  }),
2484
- // simulateExtensionEvent removed for security could be exploited on staging builds
2683
+ // simulateExtensionEvent removed for security: it could be exploited on staging builds
2485
2684
  showWelcome: () => {
2486
2685
  console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
2487
2686
  console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
@@ -2499,16 +2698,21 @@ if (typeof window !== 'undefined') {
2499
2698
  exports.DEFAULT_NETWORK_ID = DEFAULT_NETWORK_ID;
2500
2699
  exports.EventEmitter = EventEmitter;
2501
2700
  exports.ExtensionCommunicator = ExtensionCommunicator;
2701
+ exports.LEGACY_PERMISSION_MAP = LEGACY_PERMISSION_MAP;
2502
2702
  exports.MIN_EXTENSION_VERSION = MIN_EXTENSION_VERSION;
2503
2703
  exports.MIN_EXTENSION_VERSION_DEVNET = MIN_EXTENSION_VERSION_DEVNET;
2504
2704
  exports.NETWORKS = NETWORKS;
2505
2705
  exports.OctraProviderAdapter = OctraProviderAdapter;
2506
2706
  exports.SDK_CONFIG = SDK_CONFIG;
2507
2707
  exports.SDK_VERSION = SDK_VERSION;
2708
+ exports.SIGNED_MESSAGE_PREFIX = SIGNED_MESSAGE_PREFIX;
2709
+ exports.SIGNED_MESSAGE_VERSION = SIGNED_MESSAGE_VERSION;
2508
2710
  exports.SUPPORTED_EXTENSION_VERSIONS = SUPPORTED_EXTENSION_VERSIONS;
2711
+ exports.WALLET_PERMISSIONS = WALLET_PERMISSIONS;
2509
2712
  exports.ZeroXIOAdapter = ZeroXIOAdapter;
2510
2713
  exports.ZeroXIOWallet = ZeroXIOWallet;
2511
2714
  exports.ZeroXIOWalletError = ZeroXIOWalletError;
2715
+ exports.buildAuthMessage = buildAuthMessage;
2512
2716
  exports.checkBrowserSupport = checkBrowserSupport;
2513
2717
  exports.checkSDKCompatibility = checkSDKCompatibility;
2514
2718
  exports.createDefaultBalance = createDefaultBalance;
@@ -2532,6 +2736,7 @@ exports.getAllAdapters = getAllAdapters;
2532
2736
  exports.getAllNetworks = getAllNetworks;
2533
2737
  exports.getDefaultNetwork = getDefaultNetwork;
2534
2738
  exports.getNetworkConfig = getNetworkConfig;
2739
+ exports.getSignedMessageBytes = getSignedMessageBytes;
2535
2740
  exports.isBrowser = isBrowser;
2536
2741
  exports.isErrorType = isErrorType;
2537
2742
  exports.isValidAddress = isValidAddress;
@@ -2539,6 +2744,10 @@ exports.isValidAmount = isValidAmount;
2539
2744
  exports.isValidFeeLevel = isValidFeeLevel;
2540
2745
  exports.isValidMessage = isValidMessage;
2541
2746
  exports.isValidNetworkId = isValidNetworkId;
2747
+ exports.octToMicro = octToMicro;
2542
2748
  exports.toMicroOCT = toMicroOCT;
2543
2749
  exports.toMicroZeroXIO = toMicroOCT;
2750
+ exports.toWalletPermissions = toWalletPermissions;
2751
+ exports.verifyMessage = verifyMessage;
2752
+ exports.withLegacyAliases = withLegacyAliases;
2544
2753
  //# sourceMappingURL=index.js.map