@0xio/sdk 2.7.1 → 2.8.0

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