@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.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,8 +1546,8 @@
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.
1549
+ * Switch the extension's active network (e.g. 'mainnet' to 'devnet').
1550
+ * Works silently: no popup, no user confirmation needed.
1350
1551
  * The extension broadcasts 'networkChanged' event to all connected dApps.
1351
1552
  */
1352
1553
  async switchNetwork(networkId) {
@@ -1381,6 +1582,22 @@
1381
1582
  getAddress() {
1382
1583
  return this.connectionInfo.address || null;
1383
1584
  }
1585
+ /**
1586
+ * The connected account's Ed25519 public key (base64). Served from the session when the
1587
+ * wallet reported it at connect, otherwise asked from the wallet.
1588
+ */
1589
+ async getPublicKey() {
1590
+ this.ensureConnected();
1591
+ if (this.connectionInfo.publicKey)
1592
+ return this.connectionInfo.publicKey;
1593
+ const result = await this.communicator.sendRequest('getPublicKey');
1594
+ const publicKey = result?.publicKey;
1595
+ if (typeof publicKey !== 'string' || !publicKey) {
1596
+ throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
1597
+ }
1598
+ this.connectionInfo.publicKey = publicKey;
1599
+ return publicKey;
1600
+ }
1384
1601
  async getBalance(forceRefresh = false) {
1385
1602
  this.ensureConnected();
1386
1603
  try {
@@ -1470,16 +1687,14 @@
1470
1687
  if (!isValidAddress(txData.to)) {
1471
1688
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1472
1689
  }
1473
- if (!isValidAmount(txData.amount)) {
1474
- throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1475
- }
1690
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1476
1691
  if (txData.message && txData.message.length > 1000) {
1477
1692
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1478
1693
  }
1479
1694
  try {
1480
1695
  // log non-sensitive only
1481
1696
  this.logger.log('Sending transaction:', { to: txData.to });
1482
- const result = await this.communicator.sendRequest('send_transaction', txData);
1697
+ const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
1483
1698
  this.logger.log('Transaction result:', result);
1484
1699
  // Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
1485
1700
  if (result.accepted ?? result.success) {
@@ -1508,15 +1723,13 @@
1508
1723
  if (!isValidAddress(txData.to)) {
1509
1724
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1510
1725
  }
1511
- if (!isValidAmount(txData.amount)) {
1512
- throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1513
- }
1726
+ const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
1514
1727
  if (txData.message && txData.message.length > 1000) {
1515
1728
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1516
1729
  }
1517
1730
  try {
1518
1731
  this.logger.log('Requesting transaction signature:', { to: txData.to });
1519
- const result = await this.communicator.sendRequest('sign_transaction', txData);
1732
+ const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
1520
1733
  return result;
1521
1734
  }
1522
1735
  catch (error) {
@@ -1560,9 +1773,15 @@
1560
1773
  if (callData.method.length > 200) {
1561
1774
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
1562
1775
  }
1776
+ if (callData.amount != null && callData.amountOct != null) {
1777
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
1778
+ }
1563
1779
  if (callData.amount != null) {
1564
1780
  this.assertExactOCTAmount(callData.amount, 'Contract call amount');
1565
1781
  }
1782
+ if (callData.amountOct != null) {
1783
+ this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
1784
+ }
1566
1785
  try {
1567
1786
  if (JSON.stringify(callData.params).length > 65536) {
1568
1787
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
@@ -1580,7 +1799,11 @@
1580
1799
  contract: callData.contract,
1581
1800
  method: callData.method,
1582
1801
  params: callData.params,
1583
- amount: callData.amount != null ? String(callData.amount) : '0',
1802
+ amount: callData.amountOct != null
1803
+ ? octToMicro(callData.amountOct)
1804
+ : callData.amount != null
1805
+ ? String(callData.amount)
1806
+ : '0',
1584
1807
  ou: callData.ou != null ? String(callData.ou) : '10000',
1585
1808
  });
1586
1809
  this.logger.log('Contract call result:', result);
@@ -1697,7 +1920,9 @@
1697
1920
  }
1698
1921
  }
1699
1922
  /**
1700
- * Encrypt public balance to private
1923
+ * Encrypt public balance to private.
1924
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1925
+ * NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
1701
1926
  */
1702
1927
  async encryptBalance(amount) {
1703
1928
  this.ensureConnected();
@@ -1718,7 +1943,9 @@
1718
1943
  }
1719
1944
  }
1720
1945
  /**
1721
- * Decrypt private balance to public
1946
+ * Decrypt private balance to public.
1947
+ * @deprecated The 0xio extension does not serve this through the bridge (it answers
1948
+ * NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
1722
1949
  */
1723
1950
  async decryptBalance(amount) {
1724
1951
  this.ensureConnected();
@@ -1760,7 +1987,10 @@
1760
1987
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
1761
1988
  }
1762
1989
  try {
1763
- const result = await this.communicator.sendRequest('send_private_transfer', transferData);
1990
+ const result = await this.communicator.sendRequest('send_private_transfer', {
1991
+ ...transferData,
1992
+ ...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
1993
+ });
1764
1994
  // Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
1765
1995
  if (result.accepted ?? result.success) {
1766
1996
  setTimeout(() => {
@@ -1814,16 +2044,89 @@
1814
2044
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
1815
2045
  }
1816
2046
  }
2047
+ // Generic provider passthrough and RFP private primitives (since 2.8.0)
2048
+ // These route through the wallet bridge. The wallet keeps all private/FHE
2049
+ // secret material internal and returns only ciphertexts/proofs/tx hashes.
2050
+ /**
2051
+ * Send any wallet method + params through the bridge. Escape hatch for
2052
+ * primitives that don't have a typed helper yet (no SDK upgrade needed).
2053
+ * @since 2.8.0
2054
+ */
2055
+ async request(method, params = {}) {
2056
+ return this.communicator.sendRequest(method, params);
2057
+ }
2058
+ /**
2059
+ * Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
2060
+ * (octra_balance, octra_transaction, contract_call and similar); writes are refused.
2061
+ * @since 2.8.0
2062
+ */
2063
+ async rpcCall(method, params = []) {
2064
+ return this.communicator.sendRequest('rpc_call', { method, params });
2065
+ }
2066
+ /**
2067
+ * Feature-detect which private capabilities the connected wallet supports.
2068
+ * Lets a dapp render the correct UI (or fail closed) before any action.
2069
+ * @since 2.8.0
2070
+ */
2071
+ async getPrivateCapabilities() {
2072
+ return this.communicator.sendRequest('get_private_capabilities');
2073
+ }
2074
+ /** Read-only contract view (no approval popup). @since 2.8.0 */
2075
+ async callContractView(params) {
2076
+ this.ensureConnected();
2077
+ return this.communicator.sendRequest('contract_call_view', params);
2078
+ }
2079
+ /** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
2080
+ async encryptValue(params) {
2081
+ this.ensureConnected();
2082
+ return this.communicator.sendRequest('encrypt_value', params);
2083
+ }
2084
+ /** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
2085
+ async decryptValue(params) {
2086
+ this.ensureConnected();
2087
+ return this.communicator.sendRequest('decrypt_value', params);
2088
+ }
2089
+ /** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
2090
+ async makeZeroProof(params) {
2091
+ this.ensureConnected();
2092
+ return this.communicator.sendRequest('make_zero_proof', params);
2093
+ }
2094
+ /** Range proof for a ciphertext. @since 2.8.0 */
2095
+ async makeRangeProof(params) {
2096
+ this.ensureConnected();
2097
+ return this.communicator.sendRequest('make_range_proof', params);
2098
+ }
2099
+ /** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
2100
+ async getPrivateBalance(params = {}) {
2101
+ this.ensureConnected();
2102
+ return this.communicator.sendRequest('get_private_balance', params);
2103
+ }
2104
+ /** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
2105
+ async registerPrivateViewKey(params) {
2106
+ this.ensureConnected();
2107
+ return this.communicator.sendRequest('register_private_view_key', params);
2108
+ }
2109
+ /** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
2110
+ async sendContractTransactionSequence(params) {
2111
+ this.ensureConnected();
2112
+ return this.communicator.sendRequest('send_contract_transaction_sequence', params);
2113
+ }
1817
2114
  /**
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
2115
+ * Sign an arbitrary message with the wallet's private key.
2116
+ * The user will be prompted to approve the signature request in the extension.
2117
+ *
2118
+ * The wallet does not sign the raw message: it signs the 0xio Signed Message framing
2119
+ * (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
2120
+ * transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
2121
+ * Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
2122
+ *
1820
2123
  * @param message - The message to sign (non-empty string)
1821
2124
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1822
2125
  * @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
1823
2126
  * @example
1824
2127
  * ```typescript
1825
2128
  * const signature = await wallet.signMessage('Hello, 0xio!');
1826
- * console.log('Signature:', signature);
2129
+ * const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
1827
2130
  * ```
1828
2131
  */
1829
2132
  async signMessage(message) {
@@ -1857,7 +2160,7 @@
1857
2160
  * to the calling service and a one-time nonce, preventing cross-service replay attacks.
1858
2161
  *
1859
2162
  * @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
2163
+ * @param nonce - Unique one-time value. Use a server-generated UUID or challenge
1861
2164
  * @returns Promise resolving to the base64-encoded Ed25519 signature
1862
2165
  */
1863
2166
  async signAuthMessage(service, nonce) {
@@ -1869,8 +2172,7 @@
1869
2172
  throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
1870
2173
  }
1871
2174
  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);
2175
+ return this.signMessage(buildAuthMessage(service, nonce, origin));
1874
2176
  }
1875
2177
  ensureInitialized() {
1876
2178
  if (!this.isInitialized) {
@@ -1902,6 +2204,9 @@
1902
2204
  this.communicator.on('transactionConfirmed', (event) => {
1903
2205
  this.handleTransactionConfirmed(event.data);
1904
2206
  });
2207
+ this.communicator.on('transactionFailed', (event) => {
2208
+ this.emit('transactionFailed', event.data ?? event);
2209
+ });
1905
2210
  this.communicator.on('permissionsChanged', (event) => {
1906
2211
  const permissions = event.data ?? event;
1907
2212
  if (this.connectionInfo.isConnected) {
@@ -1940,7 +2245,7 @@
1940
2245
  }
1941
2246
  handleNetworkChanged(data) {
1942
2247
  const previousNetwork = this.connectionInfo.networkInfo;
1943
- // validate networkInfo drop invalid
2248
+ // validate networkInfo, drop invalid
1944
2249
  const networkInfo = validateNetworkInfo(data.networkInfo);
1945
2250
  if (!networkInfo) {
1946
2251
  this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
@@ -2001,9 +2306,26 @@
2001
2306
  }, 2000);
2002
2307
  this.logger.log('Transaction confirmed:', data.txHash);
2003
2308
  }
2309
+ /**
2310
+ * The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
2311
+ * dapp sends today); `amountOct` is converted exactly. Never both.
2312
+ */
2313
+ resolveRawAmount(amount, amountOct, label) {
2314
+ if (amount != null && amountOct != null) {
2315
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
2316
+ }
2317
+ if (amountOct != null) {
2318
+ this.assertExactOCTAmount(amountOct, label);
2319
+ return octToMicro(amountOct);
2320
+ }
2321
+ if (amount == null || !isValidAmount(amount)) {
2322
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
2323
+ }
2324
+ return amount;
2325
+ }
2004
2326
  /**
2005
2327
  * 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.
2328
+ * e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
2007
2329
  * String amounts bypass this check (caller is responsible for correctness).
2008
2330
  */
2009
2331
  assertExactOCTAmount(amount, label) {
@@ -2037,7 +2359,7 @@
2037
2359
  *
2038
2360
  * Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
2039
2361
  * window.octra.isOctra === true
2040
- * window.octra.request({ method, params }) Promise<unknown>
2362
+ * window.octra.request({ method, params }) returns Promise<unknown>
2041
2363
  * window.octra.on(event, listener) / removeListener(event, listener)
2042
2364
  *
2043
2365
  * This adapter translates the SDK's internal method names into RFC-O-1 method
@@ -2046,7 +2368,7 @@
2046
2368
  * Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
2047
2369
  * are passed through as-is; the wallet's request() handles or rejects them.
2048
2370
  */
2049
- /** SDK method RFC-O-1 method name */
2371
+ /** SDK method to RFC-O-1 method name */
2050
2372
  const SDK_TO_RFC = {
2051
2373
  get_network_info: 'octra_networkInfo',
2052
2374
  switch_network: 'octra_switchNetwork',
@@ -2061,8 +2383,16 @@
2061
2383
  decrypt_balance: 'octra_decryptBalance',
2062
2384
  send_private_transfer: 'octra_sendPrivateTransfer',
2063
2385
  claim_private_transfer: 'octra_claimStealth',
2386
+ get_private_capabilities: 'octra_getPrivateCapabilities',
2387
+ encrypt_value: 'octra_encryptValue',
2388
+ decrypt_value: 'octra_decryptValue',
2389
+ make_zero_proof: 'octra_makeZeroProof',
2390
+ make_range_proof: 'octra_makeRangeProof',
2391
+ get_private_balance: 'octra_getPrivateBalance',
2392
+ register_private_view_key: 'octra_registerPrivateViewKey',
2393
+ send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
2064
2394
  };
2065
- /** RFC-O-1 error code SDK ErrorCode string */
2395
+ /** RFC-O-1 error code to SDK ErrorCode string */
2066
2396
  const RFC_TO_SDK_ERROR = {
2067
2397
  4001: 'USER_REJECTED',
2068
2398
  4100: 'PERMISSION_DENIED',
@@ -2164,7 +2494,7 @@
2164
2494
  const provider = getProvider();
2165
2495
  if (!provider)
2166
2496
  return () => { _handler = null; };
2167
- // RFC-O-1 event SDK event name + data shape
2497
+ // RFC-O-1 event to SDK event name and data shape
2168
2498
  const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
2169
2499
  const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
2170
2500
  const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
@@ -2206,15 +2536,15 @@
2206
2536
  const OctraProviderAdapter = createOctraProviderAdapter();
2207
2537
 
2208
2538
  /**
2209
- * 0xio SDK Wallet Adapter Registry
2539
+ * 0xio SDK: Wallet Adapter Registry
2210
2540
  *
2211
2541
  * Add new wallet adapters here. Detection order determines which wallet takes
2212
2542
  * priority when multiple wallets are installed at the same time.
2213
2543
  */
2214
2544
  const REGISTERED_ADAPTERS = [
2215
- ZeroXIOAdapter, // 0xio extension (postMessage protocol) highest priority
2545
+ ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
2216
2546
  OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
2217
- // Add new wallet adapters here detection runs in order, first match wins
2547
+ // Add new wallet adapters here: detection runs in order, first match wins
2218
2548
  ];
2219
2549
  /**
2220
2550
  * Auto-detects the first available wallet in the current page.
@@ -2261,7 +2591,7 @@
2261
2591
  */
2262
2592
  // Main exports
2263
2593
  // Version information
2264
- const SDK_VERSION = '2.7.1';
2594
+ const SDK_VERSION = '2.8.0';
2265
2595
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
2266
2596
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
2267
2597
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -2289,7 +2619,7 @@
2289
2619
  function checkSDKCompatibility() {
2290
2620
  const issues = [];
2291
2621
  const recommendations = [];
2292
- // Hard blockers SDK cannot function without these
2622
+ // Hard blockers: the SDK cannot function without these
2293
2623
  if (typeof window === 'undefined') {
2294
2624
  issues.push('Window object not available');
2295
2625
  recommendations.push('SDK must be used in a browser environment');
@@ -2354,7 +2684,7 @@
2354
2684
  debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
2355
2685
  environment: isDevelopment ? 'development' : 'production'
2356
2686
  }),
2357
- // simulateExtensionEvent removed for security could be exploited on staging builds
2687
+ // simulateExtensionEvent removed for security: it could be exploited on staging builds
2358
2688
  showWelcome: () => {
2359
2689
  console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
2360
2690
  console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
@@ -2372,16 +2702,21 @@
2372
2702
  exports.DEFAULT_NETWORK_ID = DEFAULT_NETWORK_ID;
2373
2703
  exports.EventEmitter = EventEmitter;
2374
2704
  exports.ExtensionCommunicator = ExtensionCommunicator;
2705
+ exports.LEGACY_PERMISSION_MAP = LEGACY_PERMISSION_MAP;
2375
2706
  exports.MIN_EXTENSION_VERSION = MIN_EXTENSION_VERSION;
2376
2707
  exports.MIN_EXTENSION_VERSION_DEVNET = MIN_EXTENSION_VERSION_DEVNET;
2377
2708
  exports.NETWORKS = NETWORKS;
2378
2709
  exports.OctraProviderAdapter = OctraProviderAdapter;
2379
2710
  exports.SDK_CONFIG = SDK_CONFIG;
2380
2711
  exports.SDK_VERSION = SDK_VERSION;
2712
+ exports.SIGNED_MESSAGE_PREFIX = SIGNED_MESSAGE_PREFIX;
2713
+ exports.SIGNED_MESSAGE_VERSION = SIGNED_MESSAGE_VERSION;
2381
2714
  exports.SUPPORTED_EXTENSION_VERSIONS = SUPPORTED_EXTENSION_VERSIONS;
2715
+ exports.WALLET_PERMISSIONS = WALLET_PERMISSIONS;
2382
2716
  exports.ZeroXIOAdapter = ZeroXIOAdapter;
2383
2717
  exports.ZeroXIOWallet = ZeroXIOWallet;
2384
2718
  exports.ZeroXIOWalletError = ZeroXIOWalletError;
2719
+ exports.buildAuthMessage = buildAuthMessage;
2385
2720
  exports.checkBrowserSupport = checkBrowserSupport;
2386
2721
  exports.checkSDKCompatibility = checkSDKCompatibility;
2387
2722
  exports.createDefaultBalance = createDefaultBalance;
@@ -2405,6 +2740,7 @@
2405
2740
  exports.getAllNetworks = getAllNetworks;
2406
2741
  exports.getDefaultNetwork = getDefaultNetwork;
2407
2742
  exports.getNetworkConfig = getNetworkConfig;
2743
+ exports.getSignedMessageBytes = getSignedMessageBytes;
2408
2744
  exports.isBrowser = isBrowser;
2409
2745
  exports.isErrorType = isErrorType;
2410
2746
  exports.isValidAddress = isValidAddress;
@@ -2412,8 +2748,12 @@
2412
2748
  exports.isValidFeeLevel = isValidFeeLevel;
2413
2749
  exports.isValidMessage = isValidMessage;
2414
2750
  exports.isValidNetworkId = isValidNetworkId;
2751
+ exports.octToMicro = octToMicro;
2415
2752
  exports.toMicroOCT = toMicroOCT;
2416
2753
  exports.toMicroZeroXIO = toMicroOCT;
2754
+ exports.toWalletPermissions = toWalletPermissions;
2755
+ exports.verifyMessage = verifyMessage;
2756
+ exports.withLegacyAliases = withLegacyAliases;
2417
2757
 
2418
2758
  }));
2419
2759
  //# sourceMappingURL=index.umd.js.map