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