@0xio/sdk 2.7.1 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -92,6 +92,16 @@ var ErrorCode;
92
92
  ErrorCode["DUPLICATE_TRANSACTION"] = "DUPLICATE_TRANSACTION";
93
93
  ErrorCode["NONCE_TOO_FAR"] = "NONCE_TOO_FAR";
94
94
  ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
95
+ // Codes the 0xio wallet returns over the bridge
96
+ ErrorCode["NOT_CONNECTED"] = "NOT_CONNECTED";
97
+ ErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS";
98
+ ErrorCode["METHOD_NOT_ALLOWED"] = "METHOD_NOT_ALLOWED";
99
+ ErrorCode["NOT_AVAILABLE"] = "NOT_AVAILABLE";
100
+ ErrorCode["PRIVATE_PROOF_FAILED"] = "PRIVATE_PROOF_FAILED";
101
+ ErrorCode["PRIVATE_TRANSFER_FAILED"] = "PRIVATE_TRANSFER_FAILED";
102
+ ErrorCode["CONTRACT_CALL_FAILED"] = "CONTRACT_CALL_FAILED";
103
+ ErrorCode["SIGN_FAILED"] = "SIGN_FAILED";
104
+ ErrorCode["RECIPIENT_NOT_REGISTERED"] = "RECIPIENT_NOT_REGISTERED";
95
105
  })(ErrorCode || (ErrorCode = {}));
96
106
  class ZeroXIOWalletError extends Error {
97
107
  constructor(code, message, details) {
@@ -114,7 +124,7 @@ class ZeroXIOWalletError extends Error {
114
124
  * window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
115
125
  * window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
116
126
  *
117
- * H-2: Session nonce validation injected.ts broadcasts the nonce received from the
127
+ * Session nonce validation: injected.ts broadcasts the nonce received from the
118
128
  * isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
119
129
  * mismatched nonce is rejected, preventing response injection by malicious page scripts.
120
130
  */
@@ -147,7 +157,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
147
157
  window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
148
158
  }
149
159
  catch {
150
- // Do not fall back to '*' silent failure is safer
160
+ // Do not fall back to '*': silent failure is safer
151
161
  }
152
162
  },
153
163
  listen(handler, options) {
@@ -162,7 +172,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
162
172
  ...(options?.trustedParentOrigins ?? []),
163
173
  ]);
164
174
  let _sessionNonce = null;
165
- // H-2: receive session nonce from injected.ts (MAIN world content script)
175
+ // receive the session nonce from injected.ts (MAIN world content script)
166
176
  const nonceListener = (e) => {
167
177
  if (e.origin !== allowedOrigin)
168
178
  return;
@@ -185,7 +195,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
185
195
  return;
186
196
  if (!e.data || e.data.source !== '0xio-sdk-bridge')
187
197
  return;
188
- // H-2: session nonce validation.
198
+ // session nonce validation.
189
199
  // Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
190
200
  // Fallback path: if the init broadcast was missed (race between document_start
191
201
  // content script and page script load), capture nonce from the first same-origin
@@ -255,7 +265,7 @@ function isValidAmount(amount) {
255
265
  amount <= Number.MAX_SAFE_INTEGER;
256
266
  }
257
267
  function isValidMessage(message) {
258
- // Type check first falsy non-strings (0, false, null) are NOT valid messages
268
+ // Type check first: falsy non-strings (0, false, null) are not valid messages
259
269
  if (typeof message !== 'string') {
260
270
  return message === undefined || message === null ? true : false;
261
271
  }
@@ -263,7 +273,7 @@ function isValidMessage(message) {
263
273
  if (message.length === 0) {
264
274
  return true;
265
275
  }
266
- // 100KB limit contract call params can be large (serialized JSON)
276
+ // 100KB limit: contract call params can be large (serialized JSON)
267
277
  return message.length <= 100000;
268
278
  }
269
279
  function isValidFeeLevel(feeLevel) {
@@ -296,7 +306,7 @@ function _base58Encode(buf) {
296
306
  }
297
307
  /**
298
308
  * Derive the canonical Octra address from a base64-encoded Ed25519 public key.
299
- * Algorithm: SHA-256(pubkey_bytes) base58 prepend "oct"
309
+ * Algorithm: SHA-256 of the pubkey bytes, base58-encoded, with "oct" prepended
300
310
  * Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
301
311
  */
302
312
  async function deriveOctraAddress(publicKeyBase64) {
@@ -355,6 +365,22 @@ function toMicroOCT(amount) {
355
365
  const microOCT = Math.round(amount * 1000000);
356
366
  return microOCT.toString();
357
367
  }
368
+ /**
369
+ * Exact OCT to raw micro-OCT conversion using string arithmetic (no float rounding).
370
+ * Accepts up to 6 decimals; anything else is rejected.
371
+ */
372
+ function octToMicro(amount) {
373
+ const text = typeof amount === 'number' ? amount.toFixed(6) : String(amount).trim();
374
+ if (!/^\d+(\.\d{1,6})?$/.test(text)) {
375
+ throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Amount must be a positive decimal with at most 6 places');
376
+ }
377
+ const [whole, frac = ''] = text.split('.');
378
+ const micro = BigInt(whole) * BigInt(1000000) + BigInt(frac.padEnd(6, '0'));
379
+ if (micro <= BigInt(0)) {
380
+ throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Amount must be greater than zero');
381
+ }
382
+ return micro.toString();
383
+ }
358
384
  function fromMicroOCT(microAmount) {
359
385
  const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
360
386
  if (!Number.isFinite(amount) || amount < 0) {
@@ -383,7 +409,16 @@ function createErrorMessage(code, context) {
383
409
  [ErrorCode.INVALID_SIGNATURE]: 'Invalid transaction signature',
384
410
  [ErrorCode.DUPLICATE_TRANSACTION]: 'Duplicate transaction detected',
385
411
  [ErrorCode.NONCE_TOO_FAR]: 'Transaction nonce is too far ahead',
386
- [ErrorCode.INTERNAL_ERROR]: 'Internal server error'
412
+ [ErrorCode.INTERNAL_ERROR]: 'Internal server error',
413
+ [ErrorCode.NOT_CONNECTED]: 'Connect the wallet before this request',
414
+ [ErrorCode.INVALID_PARAMS]: 'Invalid request parameters',
415
+ [ErrorCode.METHOD_NOT_ALLOWED]: 'This method is not permitted through the wallet',
416
+ [ErrorCode.NOT_AVAILABLE]: 'Not available through the wallet bridge',
417
+ [ErrorCode.PRIVATE_PROOF_FAILED]: 'Private proof generation failed',
418
+ [ErrorCode.PRIVATE_TRANSFER_FAILED]: 'Private transfer failed',
419
+ [ErrorCode.CONTRACT_CALL_FAILED]: 'Contract call failed',
420
+ [ErrorCode.SIGN_FAILED]: 'Signing failed',
421
+ [ErrorCode.RECIPIENT_NOT_REGISTERED]: 'Recipient has no private view key registered'
387
422
  };
388
423
  const baseMessage = baseMessages[code] || 'Unknown error';
389
424
  return context ? `${baseMessage}: ${context}` : baseMessage;
@@ -404,7 +439,7 @@ async function retry(operation, maxRetries = 3, baseDelay = 1000) {
404
439
  }
405
440
  catch (error) {
406
441
  lastError = error;
407
- // Never retry user rejections these are intentional
442
+ // Never retry user rejections, they are intentional
408
443
  const msg = lastError.message?.toLowerCase() || '';
409
444
  if (msg.includes('rejected') || msg.includes('denied') || msg.includes('cancelled') || msg.includes('user refused')) {
410
445
  throw lastError;
@@ -546,14 +581,14 @@ class ExtensionCommunicator extends EventEmitter {
546
581
  this._adapterReadyTeardown = null;
547
582
  /**
548
583
  * Set when a trusted walletReady has been received from window.parent.
549
- * The polling fallback must NOT clear this flag.
584
+ * The polling fallback must not clear this flag.
550
585
  */
551
586
  this._parentTrusted = false;
552
587
  /** walletReady postMessage listener stored for cleanup */
553
588
  this._walletReadyMessageListener = null;
554
589
  /**
555
590
  * In-flight interactive request lock.
556
- * Methods that open approval popups are serialized only one at a time.
591
+ * Methods that open approval popups are serialized: only one at a time.
557
592
  */
558
593
  this._interactiveInFlight = false;
559
594
  this.MAX_CONCURRENT_REQUESTS = 50;
@@ -594,9 +629,15 @@ class ExtensionCommunicator extends EventEmitter {
594
629
  return this.isExtensionAvailableState && this.hasExtensionContext();
595
630
  }
596
631
  async sendRequest(method, params = {}, timeout = 30000) {
597
- const isInteractive = ExtensionCommunicator.NO_RETRY_METHODS.has(method);
598
- const maxRetries = isInteractive ? 0 : 1;
599
- const effectiveTimeout = isInteractive ? Math.max(timeout, 180000) : timeout;
632
+ // No-retry + long timeout for both popup/broadcast methods and long compute primitives.
633
+ const longOrNoRetry = ExtensionCommunicator.NO_RETRY_METHODS.has(method) ||
634
+ ExtensionCommunicator.LONG_COMPUTE_METHODS.has(method);
635
+ const maxRetries = longOrNoRetry ? 0 : 1;
636
+ const effectiveTimeout = ExtensionCommunicator.PROOF_METHODS.has(method)
637
+ ? Math.max(timeout, 600000)
638
+ : longOrNoRetry
639
+ ? Math.max(timeout, 180000)
640
+ : timeout;
600
641
  return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
601
642
  }
602
643
  async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
@@ -665,7 +706,7 @@ class ExtensionCommunicator extends EventEmitter {
665
706
  return;
666
707
  this._adapterTeardown = this.adapter.listen((msg) => {
667
708
  if (msg.requestId !== undefined) {
668
- // response map AdapterIncomingMessage ExtensionResponse shape
709
+ // response: map AdapterIncomingMessage to the ExtensionResponse shape
669
710
  if (this.pendingRequests.has(msg.requestId)) {
670
711
  this.handleExtensionResponse({
671
712
  id: msg.requestId,
@@ -699,7 +740,7 @@ class ExtensionCommunicator extends EventEmitter {
699
740
  }
700
741
  clearTimeout(pending.timeout);
701
742
  this.pendingRequests.delete(response.id);
702
- // Require strict boolean true "false" string or other truthy values are failures
743
+ // Require strict boolean true: a "false" string or other truthy values are failures
703
744
  if (response.success === true) {
704
745
  pending.resolve(response.data);
705
746
  }
@@ -724,7 +765,7 @@ class ExtensionCommunicator extends EventEmitter {
724
765
  }
725
766
  postMessageToExtension(request) {
726
767
  this.adapter.postRequest(request);
727
- // Parent bridge (iframe/desktop mode) only when a trusted origin is established.
768
+ // Parent bridge (iframe/desktop mode), only when a trusted origin is established.
728
769
  // Sending with '*' would leak method + params to any intercepting frame.
729
770
  if (window.parent !== window && this._parentOrigin) {
730
771
  if (this.adapter.postRequestToParent) {
@@ -741,7 +782,7 @@ class ExtensionCommunicator extends EventEmitter {
741
782
  if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
742
783
  throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
743
784
  }
744
- // Trim expired timestamps cap array size to prevent unbounded growth in idle tabs
785
+ // Trim expired timestamps and cap the array size to prevent unbounded growth in idle tabs
745
786
  this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
746
787
  if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
747
788
  this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
@@ -761,7 +802,7 @@ class ExtensionCommunicator extends EventEmitter {
761
802
  const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
762
803
  return `0xio-sdk-${hex}`;
763
804
  }
764
- // Crypto API unavailable throw rather than produce a guessable ID that
805
+ // Crypto API unavailable: throw rather than produce a guessable ID that
765
806
  // could allow response spoofing via a known requestId.
766
807
  throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
767
808
  }
@@ -775,7 +816,7 @@ class ExtensionCommunicator extends EventEmitter {
775
816
  this.isExtensionAvailableState = true;
776
817
  });
777
818
  }
778
- // walletReady via postMessage (desktop/mobile iframe bridge) store ref for cleanup
819
+ // walletReady via postMessage (desktop/mobile iframe bridge), store the ref for cleanup
779
820
  this._walletReadyMessageListener = (event) => {
780
821
  if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
781
822
  return;
@@ -810,7 +851,7 @@ class ExtensionCommunicator extends EventEmitter {
810
851
  };
811
852
  window.addEventListener('message', this._walletReadyMessageListener);
812
853
  if (window.parent !== window) {
813
- this.logger.log('Running inside a frame waiting for trusted walletReady signal');
854
+ this.logger.log('Running inside a frame, waiting for the trusted walletReady signal');
814
855
  }
815
856
  this.checkExtensionAvailability();
816
857
  this.extensionDetectionInterval = setInterval(() => {
@@ -819,7 +860,7 @@ class ExtensionCommunicator extends EventEmitter {
819
860
  }
820
861
  checkExtensionAvailability() {
821
862
  // If parent-bridge readiness was established via a trusted walletReady handshake,
822
- // preserve that state the polling fallback (detectExtensionSignals) does not
863
+ // preserve that state: the polling fallback (detectExtensionSignals) does not
823
864
  // consider the iframe parent signal and would incorrectly flip state back
824
865
  if (this._parentTrusted) {
825
866
  return;
@@ -901,7 +942,7 @@ class ExtensionCommunicator extends EventEmitter {
901
942
  }
902
943
  /**
903
944
  * Clean up SDK resources.
904
- * After cleanup() the instance is terminal do not call initialize() again.
945
+ * After cleanup() the instance is terminal: do not call initialize() again.
905
946
  * Construct a new instance instead.
906
947
  */
907
948
  cleanup() {
@@ -942,7 +983,7 @@ class ExtensionCommunicator extends EventEmitter {
942
983
  };
943
984
  }
944
985
  }
945
- // Methods that trigger user-facing popups NEVER retry these.
986
+ // Methods that trigger user-facing popups: never retry these.
946
987
  // Retrying sends a second request while the first popup is still open,
947
988
  // causing double popups where the second tx fails (stale nonce/state).
948
989
  ExtensionCommunicator.NO_RETRY_METHODS = new Set([
@@ -950,12 +991,29 @@ ExtensionCommunicator.NO_RETRY_METHODS = new Set([
950
991
  'sign_transaction', 'broadcast_only',
951
992
  'send_private_transfer', 'claim_private_transfer',
952
993
  'encrypt_balance', 'decrypt_balance',
994
+ // Broadcasts a sequence of contract txs, so it must never be retried (it would double-send).
995
+ 'send_contract_transaction_sequence',
996
+ // Shows an approval popup + broadcasts a registration tx (offscreen cold-init can exceed
997
+ // 30s): interactive, long timeout, no retry.
998
+ 'register_private_view_key',
999
+ ]);
1000
+ // Long-running compute primitives (RFP): proof generation / decrypt take 10-120s, so
1001
+ // they need the long (180s) timeout and must not be retried (a retry wastes ~a minute of
1002
+ // compute). They do not show approval popups, so, unlike NO_RETRY_METHODS, they are not
1003
+ // subject to the one-at-a-time interactive lock (a dapp may run several concurrently).
1004
+ ExtensionCommunicator.LONG_COMPUTE_METHODS = new Set([
1005
+ 'make_zero_proof', 'make_range_proof', 'decrypt_value',
1006
+ 'get_private_balance', 'encrypt_value',
953
1007
  ]);
1008
+ // A private transfer runs proof generation after the approval (minutes on a slow machine),
1009
+ // so its wait is longer than the popup window alone.
1010
+ ExtensionCommunicator.PROOF_METHODS = new Set(['send_private_transfer']);
1011
+ // The interactive lock applies only to methods that open a wallet popup / broadcast.
954
1012
  ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
955
1013
  // only forward known event types
956
1014
  ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
957
1015
  'connect', 'disconnect', 'accountChanged', 'balanceChanged',
958
- 'networkChanged', 'transactionConfirmed', 'permissionsChanged', 'message',
1016
+ 'networkChanged', 'transactionConfirmed', 'transactionFailed', 'permissionsChanged', 'message',
959
1017
  'error', 'extensionLocked', 'extensionUnlocked'
960
1018
  ]);
961
1019
 
@@ -977,7 +1035,7 @@ const _NETWORKS = {
977
1035
  'devnet': {
978
1036
  id: 'devnet',
979
1037
  name: 'Octra Devnet',
980
- rpcUrl: 'http://165.227.225.79:8080',
1038
+ rpcUrl: 'https://devnet.octrascan.io',
981
1039
  explorerUrl: 'https://devnet.octrascan.io/tx.html?hash=',
982
1040
  explorerAddressUrl: 'https://devnet.octrascan.io/address.html?addr=',
983
1041
  indexerUrl: 'https://devnet.octrascan.io',
@@ -1005,7 +1063,7 @@ const NETWORKS = Object.freeze(Object.fromEntries(Object.entries(_NETWORKS).map(
1005
1063
  const DEFAULT_NETWORK_ID = 'mainnet';
1006
1064
  /**
1007
1065
  * Get network configuration by ID.
1008
- * Returns a frozen copy callers cannot mutate SDK-internal state.
1066
+ * Returns a frozen copy, so callers cannot mutate SDK-internal state.
1009
1067
  */
1010
1068
  function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
1011
1069
  if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
@@ -1015,7 +1073,7 @@ function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
1015
1073
  }
1016
1074
  /**
1017
1075
  * Get all available networks.
1018
- * Returns frozen copies callers cannot mutate SDK-internal state.
1076
+ * Returns frozen copies, so callers cannot mutate SDK-internal state.
1019
1077
  */
1020
1078
  function getAllNetworks() {
1021
1079
  return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
@@ -1064,7 +1122,7 @@ function validateNetworkInfo(raw) {
1064
1122
 
1065
1123
  /**
1066
1124
  * Default balance structure.
1067
- * Accepts a numeric total or undefined never pass a Balance object here.
1125
+ * Accepts a numeric total or undefined. Never pass a Balance object here.
1068
1126
  */
1069
1127
  function createDefaultBalance(total = 0) {
1070
1128
  const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
@@ -1083,7 +1141,7 @@ function validateBalance(raw) {
1083
1141
  if (raw === null || raw === undefined)
1084
1142
  return null;
1085
1143
  // If it's already a Balance-shaped object, extract numeric fields
1086
- // Use Number() not parseFloat() parseFloat('10abc') silently returns 10
1144
+ // Use Number() not parseFloat(): parseFloat('10abc') silently returns 10
1087
1145
  const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
1088
1146
  const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
1089
1147
  if (!Number.isFinite(pub) || pub < 0)
@@ -1098,7 +1156,7 @@ function validateBalance(raw) {
1098
1156
  };
1099
1157
  }
1100
1158
  const SDK_CONFIG = {
1101
- version: '2.7.1',
1159
+ version: '2.8.0',
1102
1160
  defaultNetworkId: DEFAULT_NETWORK_ID,
1103
1161
  communicationTimeout: 30000, // 30 seconds
1104
1162
  retryAttempts: 3,
@@ -1108,13 +1166,154 @@ function getDefaultNetwork() {
1108
1166
  return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
1109
1167
  }
1110
1168
 
1169
+ /** Scope names the 0xio wallet enforces. Any other name is dropped at connect. */
1170
+ const WALLET_PERMISSIONS = [
1171
+ 'accounts',
1172
+ 'public_transactions',
1173
+ 'contract_calls',
1174
+ 'contract_views',
1175
+ 'private_balance_read',
1176
+ 'private_proofs',
1177
+ 'private_transfers',
1178
+ 'private_claims',
1179
+ ];
1180
+ /** Older SDK permission names and the wallet scope each one means. */
1181
+ const LEGACY_PERMISSION_MAP = {
1182
+ read_address: 'accounts',
1183
+ read_balance: 'accounts',
1184
+ read_public_key: 'accounts',
1185
+ send_transactions: 'public_transactions',
1186
+ sign_messages: 'public_transactions',
1187
+ contract_calls: 'contract_calls',
1188
+ view_private_balance: 'private_balance_read',
1189
+ view_encrypted_balance: 'private_balance_read',
1190
+ stealth_scan: 'private_balance_read',
1191
+ decrypt_balance: 'private_balance_read',
1192
+ encrypt_balance: 'private_proofs',
1193
+ private_transfers: 'private_transfers',
1194
+ stealth_claim: 'private_claims',
1195
+ };
1196
+ /** Translate any mix of old and new names into the wallet's scope names, without duplicates. */
1197
+ function toWalletPermissions(perms) {
1198
+ const out = [];
1199
+ for (const p of perms ?? []) {
1200
+ const canonical = WALLET_PERMISSIONS.includes(p)
1201
+ ? p
1202
+ : LEGACY_PERMISSION_MAP[p];
1203
+ if (canonical && !out.includes(canonical))
1204
+ out.push(canonical);
1205
+ }
1206
+ return out;
1207
+ }
1208
+ /**
1209
+ * The granted wallet scopes plus every requested old name they satisfy, so a dapp that checks
1210
+ * for the name it asked for (for example 'read_balance') keeps seeing it.
1211
+ */
1212
+ function withLegacyAliases(granted, requested) {
1213
+ const set = new Set(granted ?? []);
1214
+ for (const p of requested ?? []) {
1215
+ const canonical = LEGACY_PERMISSION_MAP[p];
1216
+ if (canonical && set.has(canonical))
1217
+ set.add(p);
1218
+ }
1219
+ return [...set];
1220
+ }
1221
+
1222
+ /**
1223
+ * 0xio Signed Message standard (v1).
1224
+ *
1225
+ * `wallet.signMessage(message)` never signs the raw message. The wallet frames it first so a signed
1226
+ * "message" can never collide with a transaction pre-image (a transaction is canonical JSON that
1227
+ * begins with '{'). The framing is:
1228
+ *
1229
+ * "Octra Signed Message:\n" + utf8ByteLength(message) + "\n" + message
1230
+ *
1231
+ * signed as an Ed25519 detached signature over the UTF-8 bytes of that string. The leading 'O'
1232
+ * guarantees the signed bytes never begin with '{', so a personal-message signature can never be a
1233
+ * valid transaction. Any verifier MUST reconstruct the same bytes: use `getSignedMessageBytes()`
1234
+ * with any Ed25519 library, or `verifyMessage()` for a batteries-included check.
1235
+ */
1236
+ /** Fixed prefix tag for the 0xio Signed Message scheme. */
1237
+ const SIGNED_MESSAGE_PREFIX = 'Octra Signed Message:';
1238
+ /** Scheme version, bumped if the framing ever changes so verifiers can detect it. */
1239
+ const SIGNED_MESSAGE_VERSION = 1;
1240
+ /** UTF-8 byte length of a string (JS `.length` counts UTF-16 units, not bytes). */
1241
+ function utf8ByteLength(s) {
1242
+ return new TextEncoder().encode(s).length;
1243
+ }
1244
+ function base64ToBytes(b64) {
1245
+ // atob is available in browsers and Node 16+; utils.deriveOctraAddress uses the same path.
1246
+ const bin = atob(b64);
1247
+ const out = new Uint8Array(bin.length);
1248
+ for (let i = 0; i < bin.length; i++)
1249
+ out[i] = bin.charCodeAt(i);
1250
+ return out;
1251
+ }
1252
+ /**
1253
+ * The exact bytes that `wallet.signMessage(message)` produces a signature over. Verify a 0xio
1254
+ * message signature by checking an Ed25519 signature against these bytes with the signer's public
1255
+ * key. Zero-dependency - bring your own Ed25519 verifier, or use `verifyMessage`.
1256
+ */
1257
+ function getSignedMessageBytes(message) {
1258
+ if (typeof message !== 'string') {
1259
+ throw new TypeError('message must be a string');
1260
+ }
1261
+ const framed = `${SIGNED_MESSAGE_PREFIX}\n${utf8ByteLength(message)}\n${message}`;
1262
+ return new TextEncoder().encode(framed);
1263
+ }
1264
+ /**
1265
+ * Reconstruct the auth message that `wallet.signAuthMessage(service, nonce)` signs. A relying
1266
+ * service verifies an auth signature with `verifyMessage(buildAuthMessage(service, nonce, origin),
1267
+ * signature, publicKey)`, where `origin` is the caller's page origin.
1268
+ */
1269
+ function buildAuthMessage(service, nonce, origin) {
1270
+ return `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
1271
+ }
1272
+ /**
1273
+ * Verify a 0xio message signature produced by `wallet.signMessage`.
1274
+ *
1275
+ * @param message The original message passed to `wallet.signMessage`.
1276
+ * @param signature Base64 Ed25519 signature returned by `wallet.signMessage`.
1277
+ * @param publicKey Base64 Ed25519 public key of the signer (from `wallet.getPublicKey()`).
1278
+ * @returns Whether the signature is valid for this message and key.
1279
+ *
1280
+ * Uses the Web Crypto Ed25519 primitive (Node 18+, Chrome 137+, Safari 17+, Firefox 129+). In an
1281
+ * environment without it, verify `getSignedMessageBytes(message)` with your own Ed25519 library.
1282
+ */
1283
+ async function verifyMessage(message, signature, publicKey) {
1284
+ if (typeof crypto === 'undefined' || !crypto.subtle) {
1285
+ throw new Error('Web Crypto API unavailable; verify getSignedMessageBytes(message) with your own Ed25519 library');
1286
+ }
1287
+ let sigBytes;
1288
+ let pubBytes;
1289
+ try {
1290
+ sigBytes = base64ToBytes(signature);
1291
+ pubBytes = base64ToBytes(publicKey);
1292
+ }
1293
+ catch {
1294
+ return false;
1295
+ }
1296
+ try {
1297
+ const data = new Uint8Array(getSignedMessageBytes(message));
1298
+ const key = await crypto.subtle.importKey('raw', pubBytes, { name: 'Ed25519' }, false, [
1299
+ 'verify',
1300
+ ]);
1301
+ return await crypto.subtle.verify({ name: 'Ed25519' }, key, sigBytes, data);
1302
+ }
1303
+ catch {
1304
+ // importKey/verify can throw (rather than return false) on malformed input or an environment
1305
+ // that recognizes Ed25519 only partially; treat any such failure as "not verified".
1306
+ return false;
1307
+ }
1308
+ }
1309
+
1111
1310
  class ZeroXIOWallet extends EventEmitter {
1112
1311
  constructor(config) {
1113
1312
  super(config.debug);
1114
1313
  this.connectionInfo = { isConnected: false };
1115
1314
  this.isInitialized = false;
1116
1315
  this._initPromise = null;
1117
- // session version stale write detection
1316
+ // session version, for stale write detection
1118
1317
  this._sessionVersion = 0;
1119
1318
  this.config = {
1120
1319
  ...config,
@@ -1145,7 +1344,7 @@ class ZeroXIOWallet extends EventEmitter {
1145
1344
  appVersion: this.config.appVersion,
1146
1345
  appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
1147
1346
  appIcon: this.config.appIcon,
1148
- requiredPermissions: this.config.requiredPermissions,
1347
+ requiredPermissions: toWalletPermissions(this.config.requiredPermissions),
1149
1348
  networkId: this.config.networkId
1150
1349
  });
1151
1350
  this.setupExtensionEventListeners();
@@ -1173,22 +1372,22 @@ class ZeroXIOWallet extends EventEmitter {
1173
1372
  this.ensureInitialized();
1174
1373
  try {
1175
1374
  this.logger.log('Attempting to connect with options:', options);
1176
- // filter to declared perms only accept both RFC 'permissions' and legacy 'requestPermissions'
1375
+ // filter to declared perms only: accept both RFC 'permissions' and legacy 'requestPermissions'
1177
1376
  const declaredPermissions = this.config.requiredPermissions || [];
1178
1377
  const requestedPerms = options.permissions ?? options.requestPermissions;
1179
1378
  const requestedPermissions = requestedPerms
1180
1379
  ? requestedPerms.filter(p => declaredPermissions.includes(p))
1181
1380
  : declaredPermissions;
1182
1381
  const result = await this.communicator.sendRequest('connect', {
1183
- permissions: requestedPermissions,
1382
+ permissions: toWalletPermissions(requestedPermissions),
1184
1383
  networkId: options.networkId || this.config.networkId
1185
1384
  });
1186
- // verify pubkey→addr binding
1385
+ // verify the public key to address binding
1187
1386
  if (result.publicKey && result.address) {
1188
1387
  try {
1189
1388
  const derived = await deriveOctraAddress(result.publicKey);
1190
1389
  if (derived !== result.address) {
1191
- throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed the reported public key does not derive to the reported address');
1390
+ throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed: the reported public key does not derive to the reported address');
1192
1391
  }
1193
1392
  }
1194
1393
  catch (e) {
@@ -1197,14 +1396,16 @@ class ZeroXIOWallet extends EventEmitter {
1197
1396
  this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
1198
1397
  }
1199
1398
  }
1200
- // Use networkInfo from extension response validate before caching.
1399
+ // Use networkInfo from the extension response, validated before caching.
1201
1400
  const networkInfo = validateNetworkInfo(result.networkInfo)
1202
1401
  ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1203
1402
  if (!networkInfo) {
1204
1403
  throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
1205
1404
  }
1206
- const permissions = result.permissions || [];
1207
- // Update connection info including permissions
1405
+ // The wallet answers with its own scope names; the names the dapp asked for are kept as
1406
+ // aliases so existing permission checks keep working.
1407
+ const permissions = withLegacyAliases(result.permissions, requestedPermissions);
1408
+ // Update connection info, including permissions
1208
1409
  this.connectionInfo = {
1209
1410
  isConnected: true,
1210
1411
  address: result.address,
@@ -1278,12 +1479,12 @@ class ZeroXIOWallet extends EventEmitter {
1278
1479
  if (this._sessionVersion !== sv)
1279
1480
  return { ...this.connectionInfo };
1280
1481
  if (result.isConnected && result.address) {
1281
- // verify pubkey→addr binding
1482
+ // verify the public key to address binding
1282
1483
  if (result.publicKey) {
1283
1484
  try {
1284
1485
  const derived = await deriveOctraAddress(result.publicKey);
1285
1486
  if (derived !== result.address) {
1286
- this.logger.warn('Address-key binding mismatch on session restore ignoring stale session');
1487
+ this.logger.warn('Address-key binding mismatch on session restore, ignoring stale session');
1287
1488
  this.connectionInfo = { isConnected: false };
1288
1489
  return { ...this.connectionInfo };
1289
1490
  }
@@ -1297,11 +1498,11 @@ class ZeroXIOWallet extends EventEmitter {
1297
1498
  const networkInfo = validateNetworkInfo(result.networkInfo)
1298
1499
  ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1299
1500
  if (!networkInfo) {
1300
- this.logger.warn('getConnectionStatus: wallet returned no network metadata returning cached state');
1501
+ this.logger.warn('getConnectionStatus: wallet returned no network metadata, returning cached state');
1301
1502
  return this.connectionInfo;
1302
1503
  }
1303
1504
  const wasConnected = this.connectionInfo.isConnected;
1304
- const permissions = result.permissions || [];
1505
+ const permissions = withLegacyAliases(result.permissions, this.config.requiredPermissions);
1305
1506
  // preserve existing connectedAt
1306
1507
  const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
1307
1508
  this.connectionInfo = {
@@ -1314,7 +1515,7 @@ class ZeroXIOWallet extends EventEmitter {
1314
1515
  permissions
1315
1516
  };
1316
1517
  this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
1317
- // only emit on disconnectedconnected transition
1518
+ // only emit on the disconnected to connected transition
1318
1519
  if (!wasConnected) {
1319
1520
  const connectEvent = {
1320
1521
  address: result.address,
@@ -1339,9 +1540,10 @@ class ZeroXIOWallet extends EventEmitter {
1339
1540
  }
1340
1541
  }
1341
1542
  /**
1342
- * Switch the extension's active network (e.g. 'mainnet' 'devnet').
1343
- * Works silently no popup, no user confirmation needed.
1344
- * The extension broadcasts 'networkChanged' event to all connected dApps.
1543
+ * Ask the wallet to switch its active network (e.g. 'mainnet' to 'devnet').
1544
+ * The wallet asks the user to confirm first; declining rejects with USER_REJECTED. The call is
1545
+ * also refused while another request from this page is waiting for approval.
1546
+ * On a switch the wallet emits 'networkChanged' to connected dApps.
1345
1547
  */
1346
1548
  async switchNetwork(networkId) {
1347
1549
  this.ensureConnected();
@@ -1375,6 +1577,22 @@ class ZeroXIOWallet extends EventEmitter {
1375
1577
  getAddress() {
1376
1578
  return this.connectionInfo.address || null;
1377
1579
  }
1580
+ /**
1581
+ * The connected account's Ed25519 public key (base64). Served from the session when the
1582
+ * wallet reported it at connect, otherwise asked from the wallet.
1583
+ */
1584
+ async getPublicKey() {
1585
+ this.ensureConnected();
1586
+ if (this.connectionInfo.publicKey)
1587
+ return this.connectionInfo.publicKey;
1588
+ const result = await this.communicator.sendRequest('getPublicKey');
1589
+ const publicKey = result?.publicKey;
1590
+ if (typeof publicKey !== 'string' || !publicKey) {
1591
+ throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
1592
+ }
1593
+ this.connectionInfo.publicKey = publicKey;
1594
+ return publicKey;
1595
+ }
1378
1596
  async getBalance(forceRefresh = false) {
1379
1597
  this.ensureConnected();
1380
1598
  try {
@@ -1464,16 +1682,14 @@ class ZeroXIOWallet extends EventEmitter {
1464
1682
  if (!isValidAddress(txData.to)) {
1465
1683
  throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1466
1684
  }
1467
- if (!isValidAmount(txData.amount)) {
1468
- throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1469
- }
1685
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1470
1686
  if (txData.message && txData.message.length > 1000) {
1471
1687
  throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1472
1688
  }
1473
1689
  try {
1474
1690
  // log non-sensitive only
1475
1691
  this.logger.log('Sending transaction:', { to: txData.to });
1476
- const result = await this.communicator.sendRequest('send_transaction', txData);
1692
+ const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
1477
1693
  this.logger.log('Transaction result:', result);
1478
1694
  // Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
1479
1695
  if (result.accepted ?? result.success) {
@@ -1502,15 +1718,13 @@ class ZeroXIOWallet extends EventEmitter {
1502
1718
  if (!isValidAddress(txData.to)) {
1503
1719
  throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1504
1720
  }
1505
- if (!isValidAmount(txData.amount)) {
1506
- throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1507
- }
1721
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1508
1722
  if (txData.message && txData.message.length > 1000) {
1509
1723
  throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1510
1724
  }
1511
1725
  try {
1512
1726
  this.logger.log('Requesting transaction signature:', { to: txData.to });
1513
- const result = await this.communicator.sendRequest('sign_transaction', txData);
1727
+ const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
1514
1728
  return result;
1515
1729
  }
1516
1730
  catch (error) {
@@ -1554,9 +1768,15 @@ class ZeroXIOWallet extends EventEmitter {
1554
1768
  if (callData.method.length > 200) {
1555
1769
  throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
1556
1770
  }
1771
+ if (callData.amount != null && callData.amountOct != null) {
1772
+ throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
1773
+ }
1557
1774
  if (callData.amount != null) {
1558
1775
  this.assertExactOCTAmount(callData.amount, 'Contract call amount');
1559
1776
  }
1777
+ if (callData.amountOct != null) {
1778
+ this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
1779
+ }
1560
1780
  try {
1561
1781
  if (JSON.stringify(callData.params).length > 65536) {
1562
1782
  throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
@@ -1574,7 +1794,11 @@ class ZeroXIOWallet extends EventEmitter {
1574
1794
  contract: callData.contract,
1575
1795
  method: callData.method,
1576
1796
  params: callData.params,
1577
- amount: callData.amount != null ? String(callData.amount) : '0',
1797
+ amount: callData.amountOct != null
1798
+ ? octToMicro(callData.amountOct)
1799
+ : callData.amount != null
1800
+ ? String(callData.amount)
1801
+ : '0',
1578
1802
  ou: callData.ou != null ? String(callData.ou) : '10000',
1579
1803
  });
1580
1804
  this.logger.log('Contract call result:', result);
@@ -1691,7 +1915,9 @@ class ZeroXIOWallet extends EventEmitter {
1691
1915
  }
1692
1916
  }
1693
1917
  /**
1694
- * Encrypt public balance to private
1918
+ * Encrypt public balance to private.
1919
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1920
+ * NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
1695
1921
  */
1696
1922
  async encryptBalance(amount) {
1697
1923
  this.ensureConnected();
@@ -1712,7 +1938,9 @@ class ZeroXIOWallet extends EventEmitter {
1712
1938
  }
1713
1939
  }
1714
1940
  /**
1715
- * Decrypt private balance to public
1941
+ * Decrypt private balance to public.
1942
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1943
+ * NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
1716
1944
  */
1717
1945
  async decryptBalance(amount) {
1718
1946
  this.ensureConnected();
@@ -1754,7 +1982,10 @@ class ZeroXIOWallet extends EventEmitter {
1754
1982
  throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
1755
1983
  }
1756
1984
  try {
1757
- const result = await this.communicator.sendRequest('send_private_transfer', transferData);
1985
+ const result = await this.communicator.sendRequest('send_private_transfer', {
1986
+ ...transferData,
1987
+ ...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
1988
+ });
1758
1989
  // Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
1759
1990
  if (result.accepted ?? result.success) {
1760
1991
  setTimeout(() => {
@@ -1808,16 +2039,89 @@ class ZeroXIOWallet extends EventEmitter {
1808
2039
  throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
1809
2040
  }
1810
2041
  }
2042
+ // Generic provider passthrough and RFP private primitives (since 2.8.0)
2043
+ // These route through the wallet bridge. The wallet keeps all private/FHE
2044
+ // secret material internal and returns only ciphertexts/proofs/tx hashes.
2045
+ /**
2046
+ * Send any wallet method + params through the bridge. Escape hatch for
2047
+ * primitives that don't have a typed helper yet (no SDK upgrade needed).
2048
+ * @since 2.8.0
2049
+ */
2050
+ async request(method, params = {}) {
2051
+ return this.communicator.sendRequest(method, params);
2052
+ }
2053
+ /**
2054
+ * Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
2055
+ * (octra_balance, octra_transaction, contract_call and similar); writes are refused.
2056
+ * @since 2.8.0
2057
+ */
2058
+ async rpcCall(method, params = []) {
2059
+ return this.communicator.sendRequest('rpc_call', { method, params });
2060
+ }
2061
+ /**
2062
+ * Feature-detect which private capabilities the connected wallet supports.
2063
+ * Lets a dapp render the correct UI (or fail closed) before any action.
2064
+ * @since 2.8.0
2065
+ */
2066
+ async getPrivateCapabilities() {
2067
+ return this.communicator.sendRequest('get_private_capabilities');
2068
+ }
2069
+ /** Read-only contract view (no approval popup). @since 2.8.0 */
2070
+ async callContractView(params) {
2071
+ this.ensureConnected();
2072
+ return this.communicator.sendRequest('contract_call_view', params);
2073
+ }
2074
+ /** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
2075
+ async encryptValue(params) {
2076
+ this.ensureConnected();
2077
+ return this.communicator.sendRequest('encrypt_value', params);
2078
+ }
2079
+ /** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
2080
+ async decryptValue(params) {
2081
+ this.ensureConnected();
2082
+ return this.communicator.sendRequest('decrypt_value', params);
2083
+ }
2084
+ /** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
2085
+ async makeZeroProof(params) {
2086
+ this.ensureConnected();
2087
+ return this.communicator.sendRequest('make_zero_proof', params);
2088
+ }
2089
+ /** Range proof for a ciphertext. @since 2.8.0 */
2090
+ async makeRangeProof(params) {
2091
+ this.ensureConnected();
2092
+ return this.communicator.sendRequest('make_range_proof', params);
2093
+ }
2094
+ /** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
2095
+ async getPrivateBalance(params = {}) {
2096
+ this.ensureConnected();
2097
+ return this.communicator.sendRequest('get_private_balance', params);
2098
+ }
2099
+ /** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
2100
+ async registerPrivateViewKey(params) {
2101
+ this.ensureConnected();
2102
+ return this.communicator.sendRequest('register_private_view_key', params);
2103
+ }
2104
+ /** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
2105
+ async sendContractTransactionSequence(params) {
2106
+ this.ensureConnected();
2107
+ return this.communicator.sendRequest('send_contract_transaction_sequence', params);
2108
+ }
1811
2109
  /**
1812
- * Sign an arbitrary message with the wallet's private key
1813
- * The user will be prompted to approve the signature request in the extension
2110
+ * Sign an arbitrary message with the wallet's private key.
2111
+ * The user will be prompted to approve the signature request in the extension.
2112
+ *
2113
+ * The wallet does not sign the raw message: it signs the 0xio Signed Message framing
2114
+ * (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
2115
+ * transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
2116
+ * Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
2117
+ *
1814
2118
  * @param message - The message to sign (non-empty string)
1815
2119
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1816
2120
  * @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
1817
2121
  * @example
1818
2122
  * ```typescript
1819
2123
  * const signature = await wallet.signMessage('Hello, 0xio!');
1820
- * console.log('Signature:', signature);
2124
+ * const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
1821
2125
  * ```
1822
2126
  */
1823
2127
  async signMessage(message) {
@@ -1851,7 +2155,7 @@ class ZeroXIOWallet extends EventEmitter {
1851
2155
  * to the calling service and a one-time nonce, preventing cross-service replay attacks.
1852
2156
  *
1853
2157
  * @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
1854
- * @param nonce - Unique one-time value use a server-generated UUID or challenge
2158
+ * @param nonce - Unique one-time value. Use a server-generated UUID or challenge
1855
2159
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1856
2160
  */
1857
2161
  async signAuthMessage(service, nonce) {
@@ -1863,8 +2167,7 @@ class ZeroXIOWallet extends EventEmitter {
1863
2167
  throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
1864
2168
  }
1865
2169
  const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
1866
- const domainSeparated = `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
1867
- return this.signMessage(domainSeparated);
2170
+ return this.signMessage(buildAuthMessage(service, nonce, origin));
1868
2171
  }
1869
2172
  ensureInitialized() {
1870
2173
  if (!this.isInitialized) {
@@ -1896,6 +2199,9 @@ class ZeroXIOWallet extends EventEmitter {
1896
2199
  this.communicator.on('transactionConfirmed', (event) => {
1897
2200
  this.handleTransactionConfirmed(event.data);
1898
2201
  });
2202
+ this.communicator.on('transactionFailed', (event) => {
2203
+ this.emit('transactionFailed', event.data ?? event);
2204
+ });
1899
2205
  this.communicator.on('permissionsChanged', (event) => {
1900
2206
  const permissions = event.data ?? event;
1901
2207
  if (this.connectionInfo.isConnected) {
@@ -1934,7 +2240,7 @@ class ZeroXIOWallet extends EventEmitter {
1934
2240
  }
1935
2241
  handleNetworkChanged(data) {
1936
2242
  const previousNetwork = this.connectionInfo.networkInfo;
1937
- // validate networkInfo drop invalid
2243
+ // validate networkInfo, drop invalid
1938
2244
  const networkInfo = validateNetworkInfo(data.networkInfo);
1939
2245
  if (!networkInfo) {
1940
2246
  this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
@@ -1995,9 +2301,26 @@ class ZeroXIOWallet extends EventEmitter {
1995
2301
  }, 2000);
1996
2302
  this.logger.log('Transaction confirmed:', data.txHash);
1997
2303
  }
2304
+ /**
2305
+ * The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
2306
+ * dapp sends today); `amountOct` is converted exactly. Never both.
2307
+ */
2308
+ resolveRawAmount(amount, amountOct, label) {
2309
+ if (amount != null && amountOct != null) {
2310
+ throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
2311
+ }
2312
+ if (amountOct != null) {
2313
+ this.assertExactOCTAmount(amountOct, label);
2314
+ return octToMicro(amountOct);
2315
+ }
2316
+ if (amount == null || !isValidAmount(amount)) {
2317
+ throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
2318
+ }
2319
+ return amount;
2320
+ }
1998
2321
  /**
1999
2322
  * Reject numeric amounts that cannot be represented exactly in micro-OCT.
2000
- * e.g. 0.1 + 0.2 = 0.30000000000000004 the extension would sign the wrong value.
2323
+ * e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
2001
2324
  * String amounts bypass this check (caller is responsible for correctness).
2002
2325
  */
2003
2326
  assertExactOCTAmount(amount, label) {
@@ -2031,7 +2354,7 @@ var wallet = /*#__PURE__*/Object.freeze({
2031
2354
  *
2032
2355
  * Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
2033
2356
  * window.octra.isOctra === true
2034
- * window.octra.request({ method, params }) Promise<unknown>
2357
+ * window.octra.request({ method, params }) returns Promise<unknown>
2035
2358
  * window.octra.on(event, listener) / removeListener(event, listener)
2036
2359
  *
2037
2360
  * This adapter translates the SDK's internal method names into RFC-O-1 method
@@ -2040,7 +2363,7 @@ var wallet = /*#__PURE__*/Object.freeze({
2040
2363
  * Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
2041
2364
  * are passed through as-is; the wallet's request() handles or rejects them.
2042
2365
  */
2043
- /** SDK method RFC-O-1 method name */
2366
+ /** SDK method to RFC-O-1 method name */
2044
2367
  const SDK_TO_RFC = {
2045
2368
  get_network_info: 'octra_networkInfo',
2046
2369
  switch_network: 'octra_switchNetwork',
@@ -2055,8 +2378,16 @@ const SDK_TO_RFC = {
2055
2378
  decrypt_balance: 'octra_decryptBalance',
2056
2379
  send_private_transfer: 'octra_sendPrivateTransfer',
2057
2380
  claim_private_transfer: 'octra_claimStealth',
2381
+ get_private_capabilities: 'octra_getPrivateCapabilities',
2382
+ encrypt_value: 'octra_encryptValue',
2383
+ decrypt_value: 'octra_decryptValue',
2384
+ make_zero_proof: 'octra_makeZeroProof',
2385
+ make_range_proof: 'octra_makeRangeProof',
2386
+ get_private_balance: 'octra_getPrivateBalance',
2387
+ register_private_view_key: 'octra_registerPrivateViewKey',
2388
+ send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
2058
2389
  };
2059
- /** RFC-O-1 error code SDK ErrorCode string */
2390
+ /** RFC-O-1 error code to SDK ErrorCode string */
2060
2391
  const RFC_TO_SDK_ERROR = {
2061
2392
  4001: 'USER_REJECTED',
2062
2393
  4100: 'PERMISSION_DENIED',
@@ -2158,7 +2489,7 @@ function createOctraProviderAdapter() {
2158
2489
  const provider = getProvider();
2159
2490
  if (!provider)
2160
2491
  return () => { _handler = null; };
2161
- // RFC-O-1 event SDK event name + data shape
2492
+ // RFC-O-1 event to SDK event name and data shape
2162
2493
  const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
2163
2494
  const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
2164
2495
  const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
@@ -2200,15 +2531,15 @@ function createOctraProviderAdapter() {
2200
2531
  const OctraProviderAdapter = createOctraProviderAdapter();
2201
2532
 
2202
2533
  /**
2203
- * 0xio SDK Wallet Adapter Registry
2534
+ * 0xio SDK: Wallet Adapter Registry
2204
2535
  *
2205
2536
  * Add new wallet adapters here. Detection order determines which wallet takes
2206
2537
  * priority when multiple wallets are installed at the same time.
2207
2538
  */
2208
2539
  const REGISTERED_ADAPTERS = [
2209
- ZeroXIOAdapter, // 0xio extension (postMessage protocol) highest priority
2540
+ ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
2210
2541
  OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
2211
- // Add new wallet adapters here detection runs in order, first match wins
2542
+ // Add new wallet adapters here: detection runs in order, first match wins
2212
2543
  ];
2213
2544
  /**
2214
2545
  * Auto-detects the first available wallet in the current page.
@@ -2255,7 +2586,7 @@ function getAllAdapters() {
2255
2586
  */
2256
2587
  // Main exports
2257
2588
  // Version information
2258
- const SDK_VERSION = '2.7.1';
2589
+ const SDK_VERSION = '2.8.0';
2259
2590
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
2260
2591
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
2261
2592
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -2283,7 +2614,7 @@ async function createZeroXIOWallet(config) {
2283
2614
  function checkSDKCompatibility() {
2284
2615
  const issues = [];
2285
2616
  const recommendations = [];
2286
- // Hard blockers SDK cannot function without these
2617
+ // Hard blockers: the SDK cannot function without these
2287
2618
  if (typeof window === 'undefined') {
2288
2619
  issues.push('Window object not available');
2289
2620
  recommendations.push('SDK must be used in a browser environment');
@@ -2348,7 +2679,7 @@ if (typeof window !== 'undefined') {
2348
2679
  debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
2349
2680
  environment: isDevelopment ? 'development' : 'production'
2350
2681
  }),
2351
- // simulateExtensionEvent removed for security could be exploited on staging builds
2682
+ // simulateExtensionEvent removed for security: it could be exploited on staging builds
2352
2683
  showWelcome: () => {
2353
2684
  console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
2354
2685
  console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
@@ -2363,5 +2694,5 @@ if (typeof window !== 'undefined') {
2363
2694
  }
2364
2695
  }
2365
2696
 
2366
- export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, OctraProviderAdapter, SDK_CONFIG, SDK_VERSION, SUPPORTED_EXTENSION_VERSIONS, ZeroXIOAdapter, ZeroXIOWallet, ZeroXIOWalletError, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger, createOctraProviderAdapter, createZeroXIOAdapter, createZeroXIOWallet, delay, deriveOctraAddress, detectWalletAdapter, formatAddress, formatOCT, formatTimestamp, formatTxHash, formatOCT as formatZeroXIO, fromMicroOCT, fromMicroOCT as fromMicroZeroXIO, generateMockData, getAllAdapters, getAllNetworks, getDefaultNetwork, getNetworkConfig, isBrowser, isErrorType, isValidAddress, isValidAmount, isValidFeeLevel, isValidMessage, isValidNetworkId, toMicroOCT, toMicroOCT as toMicroZeroXIO };
2697
+ export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, LEGACY_PERMISSION_MAP, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, OctraProviderAdapter, SDK_CONFIG, SDK_VERSION, SIGNED_MESSAGE_PREFIX, SIGNED_MESSAGE_VERSION, SUPPORTED_EXTENSION_VERSIONS, WALLET_PERMISSIONS, ZeroXIOAdapter, ZeroXIOWallet, ZeroXIOWalletError, buildAuthMessage, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger, createOctraProviderAdapter, createZeroXIOAdapter, createZeroXIOWallet, delay, deriveOctraAddress, detectWalletAdapter, formatAddress, formatOCT, formatTimestamp, formatTxHash, formatOCT as formatZeroXIO, fromMicroOCT, fromMicroOCT as fromMicroZeroXIO, generateMockData, getAllAdapters, getAllNetworks, getDefaultNetwork, getNetworkConfig, getSignedMessageBytes, isBrowser, isErrorType, isValidAddress, isValidAmount, isValidFeeLevel, isValidMessage, isValidNetworkId, octToMicro, toMicroOCT, toMicroOCT as toMicroZeroXIO, toWalletPermissions, verifyMessage, withLegacyAliases };
2367
2698
  //# sourceMappingURL=index.esm.js.map