@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.umd.js CHANGED
@@ -98,6 +98,16 @@
98
98
  ErrorCode["DUPLICATE_TRANSACTION"] = "DUPLICATE_TRANSACTION";
99
99
  ErrorCode["NONCE_TOO_FAR"] = "NONCE_TOO_FAR";
100
100
  ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
101
+ // Codes the 0xio wallet returns over the bridge
102
+ ErrorCode["NOT_CONNECTED"] = "NOT_CONNECTED";
103
+ ErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS";
104
+ ErrorCode["METHOD_NOT_ALLOWED"] = "METHOD_NOT_ALLOWED";
105
+ ErrorCode["NOT_AVAILABLE"] = "NOT_AVAILABLE";
106
+ ErrorCode["PRIVATE_PROOF_FAILED"] = "PRIVATE_PROOF_FAILED";
107
+ ErrorCode["PRIVATE_TRANSFER_FAILED"] = "PRIVATE_TRANSFER_FAILED";
108
+ ErrorCode["CONTRACT_CALL_FAILED"] = "CONTRACT_CALL_FAILED";
109
+ ErrorCode["SIGN_FAILED"] = "SIGN_FAILED";
110
+ ErrorCode["RECIPIENT_NOT_REGISTERED"] = "RECIPIENT_NOT_REGISTERED";
101
111
  })(exports.ErrorCode || (exports.ErrorCode = {}));
102
112
  class ZeroXIOWalletError extends Error {
103
113
  constructor(code, message, details) {
@@ -120,7 +130,7 @@
120
130
  * window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
121
131
  * window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
122
132
  *
123
- * H-2: Session nonce validation injected.ts broadcasts the nonce received from the
133
+ * Session nonce validation: injected.ts broadcasts the nonce received from the
124
134
  * isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
125
135
  * mismatched nonce is rejected, preventing response injection by malicious page scripts.
126
136
  */
@@ -153,7 +163,7 @@
153
163
  window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
154
164
  }
155
165
  catch {
156
- // Do not fall back to '*' silent failure is safer
166
+ // Do not fall back to '*': silent failure is safer
157
167
  }
158
168
  },
159
169
  listen(handler, options) {
@@ -168,7 +178,7 @@
168
178
  ...(options?.trustedParentOrigins ?? []),
169
179
  ]);
170
180
  let _sessionNonce = null;
171
- // H-2: receive session nonce from injected.ts (MAIN world content script)
181
+ // receive the session nonce from injected.ts (MAIN world content script)
172
182
  const nonceListener = (e) => {
173
183
  if (e.origin !== allowedOrigin)
174
184
  return;
@@ -191,7 +201,7 @@
191
201
  return;
192
202
  if (!e.data || e.data.source !== '0xio-sdk-bridge')
193
203
  return;
194
- // H-2: session nonce validation.
204
+ // session nonce validation.
195
205
  // Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
196
206
  // Fallback path: if the init broadcast was missed (race between document_start
197
207
  // content script and page script load), capture nonce from the first same-origin
@@ -261,7 +271,7 @@
261
271
  amount <= Number.MAX_SAFE_INTEGER;
262
272
  }
263
273
  function isValidMessage(message) {
264
- // Type check first falsy non-strings (0, false, null) are NOT valid messages
274
+ // Type check first: falsy non-strings (0, false, null) are not valid messages
265
275
  if (typeof message !== 'string') {
266
276
  return message === undefined || message === null ? true : false;
267
277
  }
@@ -269,7 +279,7 @@
269
279
  if (message.length === 0) {
270
280
  return true;
271
281
  }
272
- // 100KB limit contract call params can be large (serialized JSON)
282
+ // 100KB limit: contract call params can be large (serialized JSON)
273
283
  return message.length <= 100000;
274
284
  }
275
285
  function isValidFeeLevel(feeLevel) {
@@ -302,7 +312,7 @@
302
312
  }
303
313
  /**
304
314
  * Derive the canonical Octra address from a base64-encoded Ed25519 public key.
305
- * Algorithm: SHA-256(pubkey_bytes) base58 prepend "oct"
315
+ * Algorithm: SHA-256 of the pubkey bytes, base58-encoded, with "oct" prepended
306
316
  * Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
307
317
  */
308
318
  async function deriveOctraAddress(publicKeyBase64) {
@@ -361,6 +371,22 @@
361
371
  const microOCT = Math.round(amount * 1000000);
362
372
  return microOCT.toString();
363
373
  }
374
+ /**
375
+ * Exact OCT to raw micro-OCT conversion using string arithmetic (no float rounding).
376
+ * Accepts up to 6 decimals; anything else is rejected.
377
+ */
378
+ function octToMicro(amount) {
379
+ const text = typeof amount === 'number' ? amount.toFixed(6) : String(amount).trim();
380
+ if (!/^\d+(\.\d{1,6})?$/.test(text)) {
381
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be a positive decimal with at most 6 places');
382
+ }
383
+ const [whole, frac = ''] = text.split('.');
384
+ const micro = BigInt(whole) * BigInt(1000000) + BigInt(frac.padEnd(6, '0'));
385
+ if (micro <= BigInt(0)) {
386
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Amount must be greater than zero');
387
+ }
388
+ return micro.toString();
389
+ }
364
390
  function fromMicroOCT(microAmount) {
365
391
  const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
366
392
  if (!Number.isFinite(amount) || amount < 0) {
@@ -389,7 +415,16 @@
389
415
  [exports.ErrorCode.INVALID_SIGNATURE]: 'Invalid transaction signature',
390
416
  [exports.ErrorCode.DUPLICATE_TRANSACTION]: 'Duplicate transaction detected',
391
417
  [exports.ErrorCode.NONCE_TOO_FAR]: 'Transaction nonce is too far ahead',
392
- [exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error'
418
+ [exports.ErrorCode.INTERNAL_ERROR]: 'Internal server error',
419
+ [exports.ErrorCode.NOT_CONNECTED]: 'Connect the wallet before this request',
420
+ [exports.ErrorCode.INVALID_PARAMS]: 'Invalid request parameters',
421
+ [exports.ErrorCode.METHOD_NOT_ALLOWED]: 'This method is not permitted through the wallet',
422
+ [exports.ErrorCode.NOT_AVAILABLE]: 'Not available through the wallet bridge',
423
+ [exports.ErrorCode.PRIVATE_PROOF_FAILED]: 'Private proof generation failed',
424
+ [exports.ErrorCode.PRIVATE_TRANSFER_FAILED]: 'Private transfer failed',
425
+ [exports.ErrorCode.CONTRACT_CALL_FAILED]: 'Contract call failed',
426
+ [exports.ErrorCode.SIGN_FAILED]: 'Signing failed',
427
+ [exports.ErrorCode.RECIPIENT_NOT_REGISTERED]: 'Recipient has no private view key registered'
393
428
  };
394
429
  const baseMessage = baseMessages[code] || 'Unknown error';
395
430
  return context ? `${baseMessage}: ${context}` : baseMessage;
@@ -410,7 +445,7 @@
410
445
  }
411
446
  catch (error) {
412
447
  lastError = error;
413
- // Never retry user rejections these are intentional
448
+ // Never retry user rejections, they are intentional
414
449
  const msg = lastError.message?.toLowerCase() || '';
415
450
  if (msg.includes('rejected') || msg.includes('denied') || msg.includes('cancelled') || msg.includes('user refused')) {
416
451
  throw lastError;
@@ -552,14 +587,14 @@
552
587
  this._adapterReadyTeardown = null;
553
588
  /**
554
589
  * Set when a trusted walletReady has been received from window.parent.
555
- * The polling fallback must NOT clear this flag.
590
+ * The polling fallback must not clear this flag.
556
591
  */
557
592
  this._parentTrusted = false;
558
593
  /** walletReady postMessage listener stored for cleanup */
559
594
  this._walletReadyMessageListener = null;
560
595
  /**
561
596
  * In-flight interactive request lock.
562
- * Methods that open approval popups are serialized only one at a time.
597
+ * Methods that open approval popups are serialized: only one at a time.
563
598
  */
564
599
  this._interactiveInFlight = false;
565
600
  this.MAX_CONCURRENT_REQUESTS = 50;
@@ -600,9 +635,15 @@
600
635
  return this.isExtensionAvailableState && this.hasExtensionContext();
601
636
  }
602
637
  async sendRequest(method, params = {}, timeout = 30000) {
603
- const isInteractive = ExtensionCommunicator.NO_RETRY_METHODS.has(method);
604
- const maxRetries = isInteractive ? 0 : 1;
605
- const effectiveTimeout = isInteractive ? Math.max(timeout, 180000) : timeout;
638
+ // No-retry + long timeout for both popup/broadcast methods and long compute primitives.
639
+ const longOrNoRetry = ExtensionCommunicator.NO_RETRY_METHODS.has(method) ||
640
+ ExtensionCommunicator.LONG_COMPUTE_METHODS.has(method);
641
+ const maxRetries = longOrNoRetry ? 0 : 1;
642
+ const effectiveTimeout = ExtensionCommunicator.PROOF_METHODS.has(method)
643
+ ? Math.max(timeout, 600000)
644
+ : longOrNoRetry
645
+ ? Math.max(timeout, 180000)
646
+ : timeout;
606
647
  return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
607
648
  }
608
649
  async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
@@ -671,7 +712,7 @@
671
712
  return;
672
713
  this._adapterTeardown = this.adapter.listen((msg) => {
673
714
  if (msg.requestId !== undefined) {
674
- // response map AdapterIncomingMessage ExtensionResponse shape
715
+ // response: map AdapterIncomingMessage to the ExtensionResponse shape
675
716
  if (this.pendingRequests.has(msg.requestId)) {
676
717
  this.handleExtensionResponse({
677
718
  id: msg.requestId,
@@ -705,7 +746,7 @@
705
746
  }
706
747
  clearTimeout(pending.timeout);
707
748
  this.pendingRequests.delete(response.id);
708
- // Require strict boolean true "false" string or other truthy values are failures
749
+ // Require strict boolean true: a "false" string or other truthy values are failures
709
750
  if (response.success === true) {
710
751
  pending.resolve(response.data);
711
752
  }
@@ -730,7 +771,7 @@
730
771
  }
731
772
  postMessageToExtension(request) {
732
773
  this.adapter.postRequest(request);
733
- // Parent bridge (iframe/desktop mode) only when a trusted origin is established.
774
+ // Parent bridge (iframe/desktop mode), only when a trusted origin is established.
734
775
  // Sending with '*' would leak method + params to any intercepting frame.
735
776
  if (window.parent !== window && this._parentOrigin) {
736
777
  if (this.adapter.postRequestToParent) {
@@ -747,7 +788,7 @@
747
788
  if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
748
789
  throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
749
790
  }
750
- // Trim expired timestamps cap array size to prevent unbounded growth in idle tabs
791
+ // Trim expired timestamps and cap the array size to prevent unbounded growth in idle tabs
751
792
  this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
752
793
  if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
753
794
  this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
@@ -767,7 +808,7 @@
767
808
  const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
768
809
  return `0xio-sdk-${hex}`;
769
810
  }
770
- // Crypto API unavailable throw rather than produce a guessable ID that
811
+ // Crypto API unavailable: throw rather than produce a guessable ID that
771
812
  // could allow response spoofing via a known requestId.
772
813
  throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
773
814
  }
@@ -781,7 +822,7 @@
781
822
  this.isExtensionAvailableState = true;
782
823
  });
783
824
  }
784
- // walletReady via postMessage (desktop/mobile iframe bridge) store ref for cleanup
825
+ // walletReady via postMessage (desktop/mobile iframe bridge), store the ref for cleanup
785
826
  this._walletReadyMessageListener = (event) => {
786
827
  if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
787
828
  return;
@@ -816,7 +857,7 @@
816
857
  };
817
858
  window.addEventListener('message', this._walletReadyMessageListener);
818
859
  if (window.parent !== window) {
819
- this.logger.log('Running inside a frame waiting for trusted walletReady signal');
860
+ this.logger.log('Running inside a frame, waiting for the trusted walletReady signal');
820
861
  }
821
862
  this.checkExtensionAvailability();
822
863
  this.extensionDetectionInterval = setInterval(() => {
@@ -825,7 +866,7 @@
825
866
  }
826
867
  checkExtensionAvailability() {
827
868
  // If parent-bridge readiness was established via a trusted walletReady handshake,
828
- // preserve that state the polling fallback (detectExtensionSignals) does not
869
+ // preserve that state: the polling fallback (detectExtensionSignals) does not
829
870
  // consider the iframe parent signal and would incorrectly flip state back
830
871
  if (this._parentTrusted) {
831
872
  return;
@@ -907,7 +948,7 @@
907
948
  }
908
949
  /**
909
950
  * Clean up SDK resources.
910
- * After cleanup() the instance is terminal do not call initialize() again.
951
+ * After cleanup() the instance is terminal: do not call initialize() again.
911
952
  * Construct a new instance instead.
912
953
  */
913
954
  cleanup() {
@@ -948,7 +989,7 @@
948
989
  };
949
990
  }
950
991
  }
951
- // Methods that trigger user-facing popups NEVER retry these.
992
+ // Methods that trigger user-facing popups: never retry these.
952
993
  // Retrying sends a second request while the first popup is still open,
953
994
  // causing double popups where the second tx fails (stale nonce/state).
954
995
  ExtensionCommunicator.NO_RETRY_METHODS = new Set([
@@ -956,12 +997,29 @@
956
997
  'sign_transaction', 'broadcast_only',
957
998
  'send_private_transfer', 'claim_private_transfer',
958
999
  'encrypt_balance', 'decrypt_balance',
1000
+ // Broadcasts a sequence of contract txs, so it must never be retried (it would double-send).
1001
+ 'send_contract_transaction_sequence',
1002
+ // Shows an approval popup + broadcasts a registration tx (offscreen cold-init can exceed
1003
+ // 30s): interactive, long timeout, no retry.
1004
+ 'register_private_view_key',
1005
+ ]);
1006
+ // Long-running compute primitives (RFP): proof generation / decrypt take 10-120s, so
1007
+ // they need the long (180s) timeout and must not be retried (a retry wastes ~a minute of
1008
+ // compute). They do not show approval popups, so, unlike NO_RETRY_METHODS, they are not
1009
+ // subject to the one-at-a-time interactive lock (a dapp may run several concurrently).
1010
+ ExtensionCommunicator.LONG_COMPUTE_METHODS = new Set([
1011
+ 'make_zero_proof', 'make_range_proof', 'decrypt_value',
1012
+ 'get_private_balance', 'encrypt_value',
959
1013
  ]);
1014
+ // A private transfer runs proof generation after the approval (minutes on a slow machine),
1015
+ // so its wait is longer than the popup window alone.
1016
+ ExtensionCommunicator.PROOF_METHODS = new Set(['send_private_transfer']);
1017
+ // The interactive lock applies only to methods that open a wallet popup / broadcast.
960
1018
  ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
961
1019
  // only forward known event types
962
1020
  ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
963
1021
  'connect', 'disconnect', 'accountChanged', 'balanceChanged',
964
- 'networkChanged', 'transactionConfirmed', 'permissionsChanged', 'message',
1022
+ 'networkChanged', 'transactionConfirmed', 'transactionFailed', 'permissionsChanged', 'message',
965
1023
  'error', 'extensionLocked', 'extensionUnlocked'
966
1024
  ]);
967
1025
 
@@ -983,7 +1041,7 @@
983
1041
  'devnet': {
984
1042
  id: 'devnet',
985
1043
  name: 'Octra Devnet',
986
- rpcUrl: 'http://165.227.225.79:8080',
1044
+ rpcUrl: 'https://devnet.octrascan.io',
987
1045
  explorerUrl: 'https://devnet.octrascan.io/tx.html?hash=',
988
1046
  explorerAddressUrl: 'https://devnet.octrascan.io/address.html?addr=',
989
1047
  indexerUrl: 'https://devnet.octrascan.io',
@@ -1011,7 +1069,7 @@
1011
1069
  const DEFAULT_NETWORK_ID = 'mainnet';
1012
1070
  /**
1013
1071
  * Get network configuration by ID.
1014
- * Returns a frozen copy callers cannot mutate SDK-internal state.
1072
+ * Returns a frozen copy, so callers cannot mutate SDK-internal state.
1015
1073
  */
1016
1074
  function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
1017
1075
  if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
@@ -1021,7 +1079,7 @@
1021
1079
  }
1022
1080
  /**
1023
1081
  * Get all available networks.
1024
- * Returns frozen copies callers cannot mutate SDK-internal state.
1082
+ * Returns frozen copies, so callers cannot mutate SDK-internal state.
1025
1083
  */
1026
1084
  function getAllNetworks() {
1027
1085
  return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
@@ -1070,7 +1128,7 @@
1070
1128
 
1071
1129
  /**
1072
1130
  * Default balance structure.
1073
- * Accepts a numeric total or undefined never pass a Balance object here.
1131
+ * Accepts a numeric total or undefined. Never pass a Balance object here.
1074
1132
  */
1075
1133
  function createDefaultBalance(total = 0) {
1076
1134
  const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
@@ -1089,7 +1147,7 @@
1089
1147
  if (raw === null || raw === undefined)
1090
1148
  return null;
1091
1149
  // If it's already a Balance-shaped object, extract numeric fields
1092
- // Use Number() not parseFloat() parseFloat('10abc') silently returns 10
1150
+ // Use Number() not parseFloat(): parseFloat('10abc') silently returns 10
1093
1151
  const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
1094
1152
  const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
1095
1153
  if (!Number.isFinite(pub) || pub < 0)
@@ -1104,7 +1162,7 @@
1104
1162
  };
1105
1163
  }
1106
1164
  const SDK_CONFIG = {
1107
- version: '2.7.1',
1165
+ version: '2.8.0',
1108
1166
  defaultNetworkId: DEFAULT_NETWORK_ID,
1109
1167
  communicationTimeout: 30000, // 30 seconds
1110
1168
  retryAttempts: 3,
@@ -1114,13 +1172,154 @@
1114
1172
  return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
1115
1173
  }
1116
1174
 
1175
+ /** Scope names the 0xio wallet enforces. Any other name is dropped at connect. */
1176
+ const WALLET_PERMISSIONS = [
1177
+ 'accounts',
1178
+ 'public_transactions',
1179
+ 'contract_calls',
1180
+ 'contract_views',
1181
+ 'private_balance_read',
1182
+ 'private_proofs',
1183
+ 'private_transfers',
1184
+ 'private_claims',
1185
+ ];
1186
+ /** Older SDK permission names and the wallet scope each one means. */
1187
+ const LEGACY_PERMISSION_MAP = {
1188
+ read_address: 'accounts',
1189
+ read_balance: 'accounts',
1190
+ read_public_key: 'accounts',
1191
+ send_transactions: 'public_transactions',
1192
+ sign_messages: 'public_transactions',
1193
+ contract_calls: 'contract_calls',
1194
+ view_private_balance: 'private_balance_read',
1195
+ view_encrypted_balance: 'private_balance_read',
1196
+ stealth_scan: 'private_balance_read',
1197
+ decrypt_balance: 'private_balance_read',
1198
+ encrypt_balance: 'private_proofs',
1199
+ private_transfers: 'private_transfers',
1200
+ stealth_claim: 'private_claims',
1201
+ };
1202
+ /** Translate any mix of old and new names into the wallet's scope names, without duplicates. */
1203
+ function toWalletPermissions(perms) {
1204
+ const out = [];
1205
+ for (const p of perms ?? []) {
1206
+ const canonical = WALLET_PERMISSIONS.includes(p)
1207
+ ? p
1208
+ : LEGACY_PERMISSION_MAP[p];
1209
+ if (canonical && !out.includes(canonical))
1210
+ out.push(canonical);
1211
+ }
1212
+ return out;
1213
+ }
1214
+ /**
1215
+ * The granted wallet scopes plus every requested old name they satisfy, so a dapp that checks
1216
+ * for the name it asked for (for example 'read_balance') keeps seeing it.
1217
+ */
1218
+ function withLegacyAliases(granted, requested) {
1219
+ const set = new Set(granted ?? []);
1220
+ for (const p of requested ?? []) {
1221
+ const canonical = LEGACY_PERMISSION_MAP[p];
1222
+ if (canonical && set.has(canonical))
1223
+ set.add(p);
1224
+ }
1225
+ return [...set];
1226
+ }
1227
+
1228
+ /**
1229
+ * 0xio Signed Message standard (v1).
1230
+ *
1231
+ * `wallet.signMessage(message)` never signs the raw message. The wallet frames it first so a signed
1232
+ * "message" can never collide with a transaction pre-image (a transaction is canonical JSON that
1233
+ * begins with '{'). The framing is:
1234
+ *
1235
+ * "Octra Signed Message:\n" + utf8ByteLength(message) + "\n" + message
1236
+ *
1237
+ * signed as an Ed25519 detached signature over the UTF-8 bytes of that string. The leading 'O'
1238
+ * guarantees the signed bytes never begin with '{', so a personal-message signature can never be a
1239
+ * valid transaction. Any verifier MUST reconstruct the same bytes: use `getSignedMessageBytes()`
1240
+ * with any Ed25519 library, or `verifyMessage()` for a batteries-included check.
1241
+ */
1242
+ /** Fixed prefix tag for the 0xio Signed Message scheme. */
1243
+ const SIGNED_MESSAGE_PREFIX = 'Octra Signed Message:';
1244
+ /** Scheme version, bumped if the framing ever changes so verifiers can detect it. */
1245
+ const SIGNED_MESSAGE_VERSION = 1;
1246
+ /** UTF-8 byte length of a string (JS `.length` counts UTF-16 units, not bytes). */
1247
+ function utf8ByteLength(s) {
1248
+ return new TextEncoder().encode(s).length;
1249
+ }
1250
+ function base64ToBytes(b64) {
1251
+ // atob is available in browsers and Node 16+; utils.deriveOctraAddress uses the same path.
1252
+ const bin = atob(b64);
1253
+ const out = new Uint8Array(bin.length);
1254
+ for (let i = 0; i < bin.length; i++)
1255
+ out[i] = bin.charCodeAt(i);
1256
+ return out;
1257
+ }
1258
+ /**
1259
+ * The exact bytes that `wallet.signMessage(message)` produces a signature over. Verify a 0xio
1260
+ * message signature by checking an Ed25519 signature against these bytes with the signer's public
1261
+ * key. Zero-dependency - bring your own Ed25519 verifier, or use `verifyMessage`.
1262
+ */
1263
+ function getSignedMessageBytes(message) {
1264
+ if (typeof message !== 'string') {
1265
+ throw new TypeError('message must be a string');
1266
+ }
1267
+ const framed = `${SIGNED_MESSAGE_PREFIX}\n${utf8ByteLength(message)}\n${message}`;
1268
+ return new TextEncoder().encode(framed);
1269
+ }
1270
+ /**
1271
+ * Reconstruct the auth message that `wallet.signAuthMessage(service, nonce)` signs. A relying
1272
+ * service verifies an auth signature with `verifyMessage(buildAuthMessage(service, nonce, origin),
1273
+ * signature, publicKey)`, where `origin` is the caller's page origin.
1274
+ */
1275
+ function buildAuthMessage(service, nonce, origin) {
1276
+ return `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
1277
+ }
1278
+ /**
1279
+ * Verify a 0xio message signature produced by `wallet.signMessage`.
1280
+ *
1281
+ * @param message The original message passed to `wallet.signMessage`.
1282
+ * @param signature Base64 Ed25519 signature returned by `wallet.signMessage`.
1283
+ * @param publicKey Base64 Ed25519 public key of the signer (from `wallet.getPublicKey()`).
1284
+ * @returns Whether the signature is valid for this message and key.
1285
+ *
1286
+ * Uses the Web Crypto Ed25519 primitive (Node 18+, Chrome 137+, Safari 17+, Firefox 129+). In an
1287
+ * environment without it, verify `getSignedMessageBytes(message)` with your own Ed25519 library.
1288
+ */
1289
+ async function verifyMessage(message, signature, publicKey) {
1290
+ if (typeof crypto === 'undefined' || !crypto.subtle) {
1291
+ throw new Error('Web Crypto API unavailable; verify getSignedMessageBytes(message) with your own Ed25519 library');
1292
+ }
1293
+ let sigBytes;
1294
+ let pubBytes;
1295
+ try {
1296
+ sigBytes = base64ToBytes(signature);
1297
+ pubBytes = base64ToBytes(publicKey);
1298
+ }
1299
+ catch {
1300
+ return false;
1301
+ }
1302
+ try {
1303
+ const data = new Uint8Array(getSignedMessageBytes(message));
1304
+ const key = await crypto.subtle.importKey('raw', pubBytes, { name: 'Ed25519' }, false, [
1305
+ 'verify',
1306
+ ]);
1307
+ return await crypto.subtle.verify({ name: 'Ed25519' }, key, sigBytes, data);
1308
+ }
1309
+ catch {
1310
+ // importKey/verify can throw (rather than return false) on malformed input or an environment
1311
+ // that recognizes Ed25519 only partially; treat any such failure as "not verified".
1312
+ return false;
1313
+ }
1314
+ }
1315
+
1117
1316
  class ZeroXIOWallet extends EventEmitter {
1118
1317
  constructor(config) {
1119
1318
  super(config.debug);
1120
1319
  this.connectionInfo = { isConnected: false };
1121
1320
  this.isInitialized = false;
1122
1321
  this._initPromise = null;
1123
- // session version stale write detection
1322
+ // session version, for stale write detection
1124
1323
  this._sessionVersion = 0;
1125
1324
  this.config = {
1126
1325
  ...config,
@@ -1151,7 +1350,7 @@
1151
1350
  appVersion: this.config.appVersion,
1152
1351
  appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
1153
1352
  appIcon: this.config.appIcon,
1154
- requiredPermissions: this.config.requiredPermissions,
1353
+ requiredPermissions: toWalletPermissions(this.config.requiredPermissions),
1155
1354
  networkId: this.config.networkId
1156
1355
  });
1157
1356
  this.setupExtensionEventListeners();
@@ -1179,22 +1378,22 @@
1179
1378
  this.ensureInitialized();
1180
1379
  try {
1181
1380
  this.logger.log('Attempting to connect with options:', options);
1182
- // filter to declared perms only accept both RFC 'permissions' and legacy 'requestPermissions'
1381
+ // filter to declared perms only: accept both RFC 'permissions' and legacy 'requestPermissions'
1183
1382
  const declaredPermissions = this.config.requiredPermissions || [];
1184
1383
  const requestedPerms = options.permissions ?? options.requestPermissions;
1185
1384
  const requestedPermissions = requestedPerms
1186
1385
  ? requestedPerms.filter(p => declaredPermissions.includes(p))
1187
1386
  : declaredPermissions;
1188
1387
  const result = await this.communicator.sendRequest('connect', {
1189
- permissions: requestedPermissions,
1388
+ permissions: toWalletPermissions(requestedPermissions),
1190
1389
  networkId: options.networkId || this.config.networkId
1191
1390
  });
1192
- // verify pubkey→addr binding
1391
+ // verify the public key to address binding
1193
1392
  if (result.publicKey && result.address) {
1194
1393
  try {
1195
1394
  const derived = await deriveOctraAddress(result.publicKey);
1196
1395
  if (derived !== result.address) {
1197
- throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed the reported public key does not derive to the reported address');
1396
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed: the reported public key does not derive to the reported address');
1198
1397
  }
1199
1398
  }
1200
1399
  catch (e) {
@@ -1203,14 +1402,16 @@
1203
1402
  this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
1204
1403
  }
1205
1404
  }
1206
- // Use networkInfo from extension response validate before caching.
1405
+ // Use networkInfo from the extension response, validated before caching.
1207
1406
  const networkInfo = validateNetworkInfo(result.networkInfo)
1208
1407
  ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1209
1408
  if (!networkInfo) {
1210
1409
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
1211
1410
  }
1212
- const permissions = result.permissions || [];
1213
- // Update connection info including permissions
1411
+ // The wallet answers with its own scope names; the names the dapp asked for are kept as
1412
+ // aliases so existing permission checks keep working.
1413
+ const permissions = withLegacyAliases(result.permissions, requestedPermissions);
1414
+ // Update connection info, including permissions
1214
1415
  this.connectionInfo = {
1215
1416
  isConnected: true,
1216
1417
  address: result.address,
@@ -1284,12 +1485,12 @@
1284
1485
  if (this._sessionVersion !== sv)
1285
1486
  return { ...this.connectionInfo };
1286
1487
  if (result.isConnected && result.address) {
1287
- // verify pubkey→addr binding
1488
+ // verify the public key to address binding
1288
1489
  if (result.publicKey) {
1289
1490
  try {
1290
1491
  const derived = await deriveOctraAddress(result.publicKey);
1291
1492
  if (derived !== result.address) {
1292
- this.logger.warn('Address-key binding mismatch on session restore ignoring stale session');
1493
+ this.logger.warn('Address-key binding mismatch on session restore, ignoring stale session');
1293
1494
  this.connectionInfo = { isConnected: false };
1294
1495
  return { ...this.connectionInfo };
1295
1496
  }
@@ -1303,11 +1504,11 @@
1303
1504
  const networkInfo = validateNetworkInfo(result.networkInfo)
1304
1505
  ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1305
1506
  if (!networkInfo) {
1306
- this.logger.warn('getConnectionStatus: wallet returned no network metadata returning cached state');
1507
+ this.logger.warn('getConnectionStatus: wallet returned no network metadata, returning cached state');
1307
1508
  return this.connectionInfo;
1308
1509
  }
1309
1510
  const wasConnected = this.connectionInfo.isConnected;
1310
- const permissions = result.permissions || [];
1511
+ const permissions = withLegacyAliases(result.permissions, this.config.requiredPermissions);
1311
1512
  // preserve existing connectedAt
1312
1513
  const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
1313
1514
  this.connectionInfo = {
@@ -1320,7 +1521,7 @@
1320
1521
  permissions
1321
1522
  };
1322
1523
  this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
1323
- // only emit on disconnectedconnected transition
1524
+ // only emit on the disconnected to connected transition
1324
1525
  if (!wasConnected) {
1325
1526
  const connectEvent = {
1326
1527
  address: result.address,
@@ -1345,9 +1546,10 @@
1345
1546
  }
1346
1547
  }
1347
1548
  /**
1348
- * Switch the extension's active network (e.g. 'mainnet' 'devnet').
1349
- * Works silently no popup, no user confirmation needed.
1350
- * The extension broadcasts 'networkChanged' event to all connected dApps.
1549
+ * Ask the wallet to switch its active network (e.g. 'mainnet' to 'devnet').
1550
+ * The wallet asks the user to confirm first; declining rejects with USER_REJECTED. The call is
1551
+ * also refused while another request from this page is waiting for approval.
1552
+ * On a switch the wallet emits 'networkChanged' to connected dApps.
1351
1553
  */
1352
1554
  async switchNetwork(networkId) {
1353
1555
  this.ensureConnected();
@@ -1381,6 +1583,22 @@
1381
1583
  getAddress() {
1382
1584
  return this.connectionInfo.address || null;
1383
1585
  }
1586
+ /**
1587
+ * The connected account's Ed25519 public key (base64). Served from the session when the
1588
+ * wallet reported it at connect, otherwise asked from the wallet.
1589
+ */
1590
+ async getPublicKey() {
1591
+ this.ensureConnected();
1592
+ if (this.connectionInfo.publicKey)
1593
+ return this.connectionInfo.publicKey;
1594
+ const result = await this.communicator.sendRequest('getPublicKey');
1595
+ const publicKey = result?.publicKey;
1596
+ if (typeof publicKey !== 'string' || !publicKey) {
1597
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
1598
+ }
1599
+ this.connectionInfo.publicKey = publicKey;
1600
+ return publicKey;
1601
+ }
1384
1602
  async getBalance(forceRefresh = false) {
1385
1603
  this.ensureConnected();
1386
1604
  try {
@@ -1470,16 +1688,14 @@
1470
1688
  if (!isValidAddress(txData.to)) {
1471
1689
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1472
1690
  }
1473
- if (!isValidAmount(txData.amount)) {
1474
- throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1475
- }
1691
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1476
1692
  if (txData.message && txData.message.length > 1000) {
1477
1693
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1478
1694
  }
1479
1695
  try {
1480
1696
  // log non-sensitive only
1481
1697
  this.logger.log('Sending transaction:', { to: txData.to });
1482
- const result = await this.communicator.sendRequest('send_transaction', txData);
1698
+ const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
1483
1699
  this.logger.log('Transaction result:', result);
1484
1700
  // Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
1485
1701
  if (result.accepted ?? result.success) {
@@ -1508,15 +1724,13 @@
1508
1724
  if (!isValidAddress(txData.to)) {
1509
1725
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1510
1726
  }
1511
- if (!isValidAmount(txData.amount)) {
1512
- throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1513
- }
1727
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1514
1728
  if (txData.message && txData.message.length > 1000) {
1515
1729
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1516
1730
  }
1517
1731
  try {
1518
1732
  this.logger.log('Requesting transaction signature:', { to: txData.to });
1519
- const result = await this.communicator.sendRequest('sign_transaction', txData);
1733
+ const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
1520
1734
  return result;
1521
1735
  }
1522
1736
  catch (error) {
@@ -1560,9 +1774,15 @@
1560
1774
  if (callData.method.length > 200) {
1561
1775
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
1562
1776
  }
1777
+ if (callData.amount != null && callData.amountOct != null) {
1778
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
1779
+ }
1563
1780
  if (callData.amount != null) {
1564
1781
  this.assertExactOCTAmount(callData.amount, 'Contract call amount');
1565
1782
  }
1783
+ if (callData.amountOct != null) {
1784
+ this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
1785
+ }
1566
1786
  try {
1567
1787
  if (JSON.stringify(callData.params).length > 65536) {
1568
1788
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
@@ -1580,7 +1800,11 @@
1580
1800
  contract: callData.contract,
1581
1801
  method: callData.method,
1582
1802
  params: callData.params,
1583
- amount: callData.amount != null ? String(callData.amount) : '0',
1803
+ amount: callData.amountOct != null
1804
+ ? octToMicro(callData.amountOct)
1805
+ : callData.amount != null
1806
+ ? String(callData.amount)
1807
+ : '0',
1584
1808
  ou: callData.ou != null ? String(callData.ou) : '10000',
1585
1809
  });
1586
1810
  this.logger.log('Contract call result:', result);
@@ -1697,7 +1921,9 @@
1697
1921
  }
1698
1922
  }
1699
1923
  /**
1700
- * Encrypt public balance to private
1924
+ * Encrypt public balance to private.
1925
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1926
+ * NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
1701
1927
  */
1702
1928
  async encryptBalance(amount) {
1703
1929
  this.ensureConnected();
@@ -1718,7 +1944,9 @@
1718
1944
  }
1719
1945
  }
1720
1946
  /**
1721
- * Decrypt private balance to public
1947
+ * Decrypt private balance to public.
1948
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1949
+ * NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
1722
1950
  */
1723
1951
  async decryptBalance(amount) {
1724
1952
  this.ensureConnected();
@@ -1760,7 +1988,10 @@
1760
1988
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
1761
1989
  }
1762
1990
  try {
1763
- const result = await this.communicator.sendRequest('send_private_transfer', transferData);
1991
+ const result = await this.communicator.sendRequest('send_private_transfer', {
1992
+ ...transferData,
1993
+ ...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
1994
+ });
1764
1995
  // Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
1765
1996
  if (result.accepted ?? result.success) {
1766
1997
  setTimeout(() => {
@@ -1814,16 +2045,89 @@
1814
2045
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
1815
2046
  }
1816
2047
  }
2048
+ // Generic provider passthrough and RFP private primitives (since 2.8.0)
2049
+ // These route through the wallet bridge. The wallet keeps all private/FHE
2050
+ // secret material internal and returns only ciphertexts/proofs/tx hashes.
2051
+ /**
2052
+ * Send any wallet method + params through the bridge. Escape hatch for
2053
+ * primitives that don't have a typed helper yet (no SDK upgrade needed).
2054
+ * @since 2.8.0
2055
+ */
2056
+ async request(method, params = {}) {
2057
+ return this.communicator.sendRequest(method, params);
2058
+ }
2059
+ /**
2060
+ * Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
2061
+ * (octra_balance, octra_transaction, contract_call and similar); writes are refused.
2062
+ * @since 2.8.0
2063
+ */
2064
+ async rpcCall(method, params = []) {
2065
+ return this.communicator.sendRequest('rpc_call', { method, params });
2066
+ }
2067
+ /**
2068
+ * Feature-detect which private capabilities the connected wallet supports.
2069
+ * Lets a dapp render the correct UI (or fail closed) before any action.
2070
+ * @since 2.8.0
2071
+ */
2072
+ async getPrivateCapabilities() {
2073
+ return this.communicator.sendRequest('get_private_capabilities');
2074
+ }
2075
+ /** Read-only contract view (no approval popup). @since 2.8.0 */
2076
+ async callContractView(params) {
2077
+ this.ensureConnected();
2078
+ return this.communicator.sendRequest('contract_call_view', params);
2079
+ }
2080
+ /** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
2081
+ async encryptValue(params) {
2082
+ this.ensureConnected();
2083
+ return this.communicator.sendRequest('encrypt_value', params);
2084
+ }
2085
+ /** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
2086
+ async decryptValue(params) {
2087
+ this.ensureConnected();
2088
+ return this.communicator.sendRequest('decrypt_value', params);
2089
+ }
2090
+ /** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
2091
+ async makeZeroProof(params) {
2092
+ this.ensureConnected();
2093
+ return this.communicator.sendRequest('make_zero_proof', params);
2094
+ }
2095
+ /** Range proof for a ciphertext. @since 2.8.0 */
2096
+ async makeRangeProof(params) {
2097
+ this.ensureConnected();
2098
+ return this.communicator.sendRequest('make_range_proof', params);
2099
+ }
2100
+ /** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
2101
+ async getPrivateBalance(params = {}) {
2102
+ this.ensureConnected();
2103
+ return this.communicator.sendRequest('get_private_balance', params);
2104
+ }
2105
+ /** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
2106
+ async registerPrivateViewKey(params) {
2107
+ this.ensureConnected();
2108
+ return this.communicator.sendRequest('register_private_view_key', params);
2109
+ }
2110
+ /** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
2111
+ async sendContractTransactionSequence(params) {
2112
+ this.ensureConnected();
2113
+ return this.communicator.sendRequest('send_contract_transaction_sequence', params);
2114
+ }
1817
2115
  /**
1818
- * Sign an arbitrary message with the wallet's private key
1819
- * The user will be prompted to approve the signature request in the extension
2116
+ * Sign an arbitrary message with the wallet's private key.
2117
+ * The user will be prompted to approve the signature request in the extension.
2118
+ *
2119
+ * The wallet does not sign the raw message: it signs the 0xio Signed Message framing
2120
+ * (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
2121
+ * transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
2122
+ * Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
2123
+ *
1820
2124
  * @param message - The message to sign (non-empty string)
1821
2125
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1822
2126
  * @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
1823
2127
  * @example
1824
2128
  * ```typescript
1825
2129
  * const signature = await wallet.signMessage('Hello, 0xio!');
1826
- * console.log('Signature:', signature);
2130
+ * const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
1827
2131
  * ```
1828
2132
  */
1829
2133
  async signMessage(message) {
@@ -1857,7 +2161,7 @@
1857
2161
  * to the calling service and a one-time nonce, preventing cross-service replay attacks.
1858
2162
  *
1859
2163
  * @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
1860
- * @param nonce - Unique one-time value use a server-generated UUID or challenge
2164
+ * @param nonce - Unique one-time value. Use a server-generated UUID or challenge
1861
2165
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1862
2166
  */
1863
2167
  async signAuthMessage(service, nonce) {
@@ -1869,8 +2173,7 @@
1869
2173
  throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
1870
2174
  }
1871
2175
  const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
1872
- const domainSeparated = `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
1873
- return this.signMessage(domainSeparated);
2176
+ return this.signMessage(buildAuthMessage(service, nonce, origin));
1874
2177
  }
1875
2178
  ensureInitialized() {
1876
2179
  if (!this.isInitialized) {
@@ -1902,6 +2205,9 @@
1902
2205
  this.communicator.on('transactionConfirmed', (event) => {
1903
2206
  this.handleTransactionConfirmed(event.data);
1904
2207
  });
2208
+ this.communicator.on('transactionFailed', (event) => {
2209
+ this.emit('transactionFailed', event.data ?? event);
2210
+ });
1905
2211
  this.communicator.on('permissionsChanged', (event) => {
1906
2212
  const permissions = event.data ?? event;
1907
2213
  if (this.connectionInfo.isConnected) {
@@ -1940,7 +2246,7 @@
1940
2246
  }
1941
2247
  handleNetworkChanged(data) {
1942
2248
  const previousNetwork = this.connectionInfo.networkInfo;
1943
- // validate networkInfo drop invalid
2249
+ // validate networkInfo, drop invalid
1944
2250
  const networkInfo = validateNetworkInfo(data.networkInfo);
1945
2251
  if (!networkInfo) {
1946
2252
  this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
@@ -2001,9 +2307,26 @@
2001
2307
  }, 2000);
2002
2308
  this.logger.log('Transaction confirmed:', data.txHash);
2003
2309
  }
2310
+ /**
2311
+ * The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
2312
+ * dapp sends today); `amountOct` is converted exactly. Never both.
2313
+ */
2314
+ resolveRawAmount(amount, amountOct, label) {
2315
+ if (amount != null && amountOct != null) {
2316
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
2317
+ }
2318
+ if (amountOct != null) {
2319
+ this.assertExactOCTAmount(amountOct, label);
2320
+ return octToMicro(amountOct);
2321
+ }
2322
+ if (amount == null || !isValidAmount(amount)) {
2323
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
2324
+ }
2325
+ return amount;
2326
+ }
2004
2327
  /**
2005
2328
  * Reject numeric amounts that cannot be represented exactly in micro-OCT.
2006
- * e.g. 0.1 + 0.2 = 0.30000000000000004 the extension would sign the wrong value.
2329
+ * e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
2007
2330
  * String amounts bypass this check (caller is responsible for correctness).
2008
2331
  */
2009
2332
  assertExactOCTAmount(amount, label) {
@@ -2037,7 +2360,7 @@
2037
2360
  *
2038
2361
  * Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
2039
2362
  * window.octra.isOctra === true
2040
- * window.octra.request({ method, params }) Promise<unknown>
2363
+ * window.octra.request({ method, params }) returns Promise<unknown>
2041
2364
  * window.octra.on(event, listener) / removeListener(event, listener)
2042
2365
  *
2043
2366
  * This adapter translates the SDK's internal method names into RFC-O-1 method
@@ -2046,7 +2369,7 @@
2046
2369
  * Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
2047
2370
  * are passed through as-is; the wallet's request() handles or rejects them.
2048
2371
  */
2049
- /** SDK method RFC-O-1 method name */
2372
+ /** SDK method to RFC-O-1 method name */
2050
2373
  const SDK_TO_RFC = {
2051
2374
  get_network_info: 'octra_networkInfo',
2052
2375
  switch_network: 'octra_switchNetwork',
@@ -2061,8 +2384,16 @@
2061
2384
  decrypt_balance: 'octra_decryptBalance',
2062
2385
  send_private_transfer: 'octra_sendPrivateTransfer',
2063
2386
  claim_private_transfer: 'octra_claimStealth',
2387
+ get_private_capabilities: 'octra_getPrivateCapabilities',
2388
+ encrypt_value: 'octra_encryptValue',
2389
+ decrypt_value: 'octra_decryptValue',
2390
+ make_zero_proof: 'octra_makeZeroProof',
2391
+ make_range_proof: 'octra_makeRangeProof',
2392
+ get_private_balance: 'octra_getPrivateBalance',
2393
+ register_private_view_key: 'octra_registerPrivateViewKey',
2394
+ send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
2064
2395
  };
2065
- /** RFC-O-1 error code SDK ErrorCode string */
2396
+ /** RFC-O-1 error code to SDK ErrorCode string */
2066
2397
  const RFC_TO_SDK_ERROR = {
2067
2398
  4001: 'USER_REJECTED',
2068
2399
  4100: 'PERMISSION_DENIED',
@@ -2164,7 +2495,7 @@
2164
2495
  const provider = getProvider();
2165
2496
  if (!provider)
2166
2497
  return () => { _handler = null; };
2167
- // RFC-O-1 event SDK event name + data shape
2498
+ // RFC-O-1 event to SDK event name and data shape
2168
2499
  const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
2169
2500
  const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
2170
2501
  const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
@@ -2206,15 +2537,15 @@
2206
2537
  const OctraProviderAdapter = createOctraProviderAdapter();
2207
2538
 
2208
2539
  /**
2209
- * 0xio SDK Wallet Adapter Registry
2540
+ * 0xio SDK: Wallet Adapter Registry
2210
2541
  *
2211
2542
  * Add new wallet adapters here. Detection order determines which wallet takes
2212
2543
  * priority when multiple wallets are installed at the same time.
2213
2544
  */
2214
2545
  const REGISTERED_ADAPTERS = [
2215
- ZeroXIOAdapter, // 0xio extension (postMessage protocol) highest priority
2546
+ ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
2216
2547
  OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
2217
- // Add new wallet adapters here detection runs in order, first match wins
2548
+ // Add new wallet adapters here: detection runs in order, first match wins
2218
2549
  ];
2219
2550
  /**
2220
2551
  * Auto-detects the first available wallet in the current page.
@@ -2261,7 +2592,7 @@
2261
2592
  */
2262
2593
  // Main exports
2263
2594
  // Version information
2264
- const SDK_VERSION = '2.7.1';
2595
+ const SDK_VERSION = '2.8.0';
2265
2596
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
2266
2597
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
2267
2598
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -2289,7 +2620,7 @@
2289
2620
  function checkSDKCompatibility() {
2290
2621
  const issues = [];
2291
2622
  const recommendations = [];
2292
- // Hard blockers SDK cannot function without these
2623
+ // Hard blockers: the SDK cannot function without these
2293
2624
  if (typeof window === 'undefined') {
2294
2625
  issues.push('Window object not available');
2295
2626
  recommendations.push('SDK must be used in a browser environment');
@@ -2354,7 +2685,7 @@
2354
2685
  debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
2355
2686
  environment: isDevelopment ? 'development' : 'production'
2356
2687
  }),
2357
- // simulateExtensionEvent removed for security could be exploited on staging builds
2688
+ // simulateExtensionEvent removed for security: it could be exploited on staging builds
2358
2689
  showWelcome: () => {
2359
2690
  console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
2360
2691
  console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
@@ -2372,16 +2703,21 @@
2372
2703
  exports.DEFAULT_NETWORK_ID = DEFAULT_NETWORK_ID;
2373
2704
  exports.EventEmitter = EventEmitter;
2374
2705
  exports.ExtensionCommunicator = ExtensionCommunicator;
2706
+ exports.LEGACY_PERMISSION_MAP = LEGACY_PERMISSION_MAP;
2375
2707
  exports.MIN_EXTENSION_VERSION = MIN_EXTENSION_VERSION;
2376
2708
  exports.MIN_EXTENSION_VERSION_DEVNET = MIN_EXTENSION_VERSION_DEVNET;
2377
2709
  exports.NETWORKS = NETWORKS;
2378
2710
  exports.OctraProviderAdapter = OctraProviderAdapter;
2379
2711
  exports.SDK_CONFIG = SDK_CONFIG;
2380
2712
  exports.SDK_VERSION = SDK_VERSION;
2713
+ exports.SIGNED_MESSAGE_PREFIX = SIGNED_MESSAGE_PREFIX;
2714
+ exports.SIGNED_MESSAGE_VERSION = SIGNED_MESSAGE_VERSION;
2381
2715
  exports.SUPPORTED_EXTENSION_VERSIONS = SUPPORTED_EXTENSION_VERSIONS;
2716
+ exports.WALLET_PERMISSIONS = WALLET_PERMISSIONS;
2382
2717
  exports.ZeroXIOAdapter = ZeroXIOAdapter;
2383
2718
  exports.ZeroXIOWallet = ZeroXIOWallet;
2384
2719
  exports.ZeroXIOWalletError = ZeroXIOWalletError;
2720
+ exports.buildAuthMessage = buildAuthMessage;
2385
2721
  exports.checkBrowserSupport = checkBrowserSupport;
2386
2722
  exports.checkSDKCompatibility = checkSDKCompatibility;
2387
2723
  exports.createDefaultBalance = createDefaultBalance;
@@ -2405,6 +2741,7 @@
2405
2741
  exports.getAllNetworks = getAllNetworks;
2406
2742
  exports.getDefaultNetwork = getDefaultNetwork;
2407
2743
  exports.getNetworkConfig = getNetworkConfig;
2744
+ exports.getSignedMessageBytes = getSignedMessageBytes;
2408
2745
  exports.isBrowser = isBrowser;
2409
2746
  exports.isErrorType = isErrorType;
2410
2747
  exports.isValidAddress = isValidAddress;
@@ -2412,8 +2749,12 @@
2412
2749
  exports.isValidFeeLevel = isValidFeeLevel;
2413
2750
  exports.isValidMessage = isValidMessage;
2414
2751
  exports.isValidNetworkId = isValidNetworkId;
2752
+ exports.octToMicro = octToMicro;
2415
2753
  exports.toMicroOCT = toMicroOCT;
2416
2754
  exports.toMicroZeroXIO = toMicroOCT;
2755
+ exports.toWalletPermissions = toWalletPermissions;
2756
+ exports.verifyMessage = verifyMessage;
2757
+ exports.withLegacyAliases = withLegacyAliases;
2417
2758
 
2418
2759
  }));
2419
2760
  //# sourceMappingURL=index.umd.js.map