@0xio/sdk 2.7.0 → 2.7.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.js CHANGED
@@ -1,27 +1,15 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * 0xio Wallet SDK - Event System
5
- * Type-safe event emitter for wallet events
6
- */
7
3
  class EventEmitter {
8
- constructor(debug = false) {
4
+ constructor(_debug = false) {
9
5
  this.listeners = new Map();
10
- this.debug = debug;
11
6
  }
12
- /**
13
- * Add event listener
14
- */
15
7
  on(eventType, listener) {
16
8
  if (!this.listeners.has(eventType)) {
17
9
  this.listeners.set(eventType, new Set());
18
10
  }
19
11
  this.listeners.get(eventType).add(listener);
20
- if (this.debug) ;
21
12
  }
22
- /**
23
- * Remove event listener
24
- */
25
13
  off(eventType, listener) {
26
14
  const eventListeners = this.listeners.get(eventType);
27
15
  if (eventListeners) {
@@ -29,12 +17,8 @@ class EventEmitter {
29
17
  if (eventListeners.size === 0) {
30
18
  this.listeners.delete(eventType);
31
19
  }
32
- if (this.debug) ;
33
20
  }
34
21
  }
35
- /**
36
- * Add one-time event listener
37
- */
38
22
  once(eventType, listener) {
39
23
  const onceListener = (event) => {
40
24
  // Remove BEFORE calling so a throwing listener doesn't stay registered
@@ -43,9 +27,6 @@ class EventEmitter {
43
27
  };
44
28
  this.on(eventType, onceListener);
45
29
  }
46
- /**
47
- * Emit event to all listeners
48
- */
49
30
  emit(eventType, data) {
50
31
  const event = {
51
32
  type: eventType,
@@ -54,49 +35,32 @@ class EventEmitter {
54
35
  };
55
36
  const eventListeners = this.listeners.get(eventType);
56
37
  if (eventListeners && eventListeners.size > 0) {
57
- if (this.debug) ;
58
- // Create a copy to avoid issues if listeners modify the set during iteration
59
- const listenersArray = Array.from(eventListeners);
60
- for (const listener of listenersArray) {
38
+ // snapshot to avoid issues if listeners modify the set during iteration
39
+ for (const listener of Array.from(eventListeners)) {
61
40
  try {
62
41
  listener(event);
63
42
  }
64
- catch (error) {
65
- // console.error(`[0xio SDK] Error in event listener for '${eventType}':`, error);
43
+ catch {
44
+ // listener errors are swallowed to keep the event loop running
66
45
  }
67
46
  }
68
47
  }
69
- else if (this.debug) ;
70
48
  }
71
- /**
72
- * Remove all listeners for a specific event type
73
- */
74
49
  removeAllListeners(eventType) {
75
50
  if (eventType) {
76
51
  this.listeners.delete(eventType);
77
- if (this.debug) ;
78
52
  }
79
53
  else {
80
54
  this.listeners.clear();
81
- if (this.debug) ;
82
55
  }
83
56
  }
84
- /**
85
- * Get number of listeners for an event type
86
- */
87
57
  listenerCount(eventType) {
88
58
  const eventListeners = this.listeners.get(eventType);
89
59
  return eventListeners ? eventListeners.size : 0;
90
60
  }
91
- /**
92
- * Get all event types that have listeners
93
- */
94
61
  eventTypes() {
95
62
  return Array.from(this.listeners.keys());
96
63
  }
97
- /**
98
- * Check if there are any listeners for an event type
99
- */
100
64
  hasListeners(eventType) {
101
65
  return this.listenerCount(eventType) > 0;
102
66
  }
@@ -269,16 +233,6 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
269
233
  /** Default 0xio adapter instance (no extra trusted origins). */
270
234
  const ZeroXIOAdapter = createZeroXIOAdapter();
271
235
 
272
- /**
273
- * 0xio Wallet SDK - Utilities
274
- * Helper functions for validation, formatting, and common operations
275
- */
276
- // ===================
277
- // VALIDATION UTILITIES
278
- // ===================
279
- /**
280
- * Validate wallet address for Octra blockchain
281
- */
282
236
  function isValidAddress(address) {
283
237
  if (!address || typeof address !== 'string') {
284
238
  return false;
@@ -302,9 +256,6 @@ function isValidAmount(amount) {
302
256
  Number.isFinite(amount) &&
303
257
  amount <= Number.MAX_SAFE_INTEGER;
304
258
  }
305
- /**
306
- * Validate transaction message
307
- */
308
259
  function isValidMessage(message) {
309
260
  // Type check first — falsy non-strings (0, false, null) are NOT valid messages
310
261
  if (typeof message !== 'string') {
@@ -317,15 +268,9 @@ function isValidMessage(message) {
317
268
  // 100KB limit — contract call params can be large (serialized JSON)
318
269
  return message.length <= 100000;
319
270
  }
320
- /**
321
- * Validate fee level
322
- */
323
271
  function isValidFeeLevel(feeLevel) {
324
272
  return feeLevel === 1 || feeLevel === 3;
325
273
  }
326
- // ===================
327
- // ADDRESS DERIVATION
328
- // ===================
329
274
  const _B58_ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
330
275
  function _base58Encode(buf) {
331
276
  let zeros = 0;
@@ -370,12 +315,6 @@ async function deriveOctraAddress(publicKeyBase64) {
370
315
  const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
371
316
  return 'oct' + _base58Encode(new Uint8Array(hashBuf));
372
317
  }
373
- // ===================
374
- // FORMATTING UTILITIES
375
- // ===================
376
- /**
377
- * Format OCT amount for display
378
- */
379
318
  function formatOCT(amount, decimals = 6) {
380
319
  const n = typeof amount === 'string' ? parseFloat(amount) : amount;
381
320
  if (!isValidAmount(n)) {
@@ -386,9 +325,6 @@ function formatOCT(amount, decimals = 6) {
386
325
  maximumFractionDigits: decimals
387
326
  });
388
327
  }
389
- /**
390
- * Format address for display (truncated)
391
- */
392
328
  function formatAddress(address, prefixLength = 6, suffixLength = 4) {
393
329
  if (!isValidAddress(address)) {
394
330
  return 'Invalid Address';
@@ -398,16 +334,10 @@ function formatAddress(address, prefixLength = 6, suffixLength = 4) {
398
334
  }
399
335
  return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}`;
400
336
  }
401
- /**
402
- * Format timestamp for display
403
- */
404
337
  function formatTimestamp(timestamp) {
405
338
  const date = new Date(timestamp);
406
339
  return date.toLocaleString();
407
340
  }
408
- /**
409
- * Format transaction hash for display
410
- */
411
341
  function formatTxHash(hash, length = 12) {
412
342
  if (!hash || typeof hash !== 'string') {
413
343
  return 'Invalid Hash';
@@ -419,12 +349,6 @@ function formatTxHash(hash, length = 12) {
419
349
  const suffixLength = Math.floor(length / 2);
420
350
  return `${hash.slice(0, prefixLength)}...${hash.slice(-suffixLength)}`;
421
351
  }
422
- // ===================
423
- // CONVERSION UTILITIES
424
- // ===================
425
- /**
426
- * Convert OCT to micro OCT (for network transmission)
427
- */
428
352
  function toMicroOCT(amount) {
429
353
  if (!isValidAmount(amount)) {
430
354
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Invalid amount for conversion');
@@ -433,9 +357,6 @@ function toMicroOCT(amount) {
433
357
  const microOCT = Math.round(amount * 1000000);
434
358
  return microOCT.toString();
435
359
  }
436
- /**
437
- * Convert micro OCT to OCT (for display)
438
- */
439
360
  function fromMicroOCT(microAmount) {
440
361
  const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
441
362
  if (!Number.isFinite(amount) || amount < 0) {
@@ -443,12 +364,6 @@ function fromMicroOCT(microAmount) {
443
364
  }
444
365
  return amount / 1000000;
445
366
  }
446
- // ===================
447
- // ERROR UTILITIES
448
- // ===================
449
- /**
450
- * Create standardized error messages
451
- */
452
367
  function createErrorMessage(code, context) {
453
368
  const baseMessages = {
454
369
  [exports.ErrorCode.EXTENSION_NOT_FOUND]: '0xio Wallet extension is not installed or enabled',
@@ -475,24 +390,12 @@ function createErrorMessage(code, context) {
475
390
  const baseMessage = baseMessages[code] || 'Unknown error';
476
391
  return context ? `${baseMessage}: ${context}` : baseMessage;
477
392
  }
478
- /**
479
- * Check if error is a specific type
480
- */
481
393
  function isErrorType(error, code) {
482
394
  return error instanceof ZeroXIOWalletError && error.code === code;
483
395
  }
484
- // ===================
485
- // ASYNC UTILITIES
486
- // ===================
487
- /**
488
- * Create a promise that resolves after a delay
489
- */
490
396
  function delay(ms) {
491
397
  return new Promise(resolve => setTimeout(resolve, ms));
492
398
  }
493
- /**
494
- * Retry an async operation with exponential backoff
495
- */
496
399
  async function retry(operation, maxRetries = 3, baseDelay = 1000) {
497
400
  let lastError;
498
401
  // maxRetries = number of retries AFTER the first attempt
@@ -532,18 +435,9 @@ function withTimeout(promise, timeoutMs, timeoutMessage = 'Operation timed out')
532
435
  clearTimeout(timer);
533
436
  });
534
437
  }
535
- // ===================
536
- // BROWSER UTILITIES
537
- // ===================
538
- /**
539
- * Check if running in browser environment
540
- */
541
438
  function isBrowser() {
542
439
  return typeof window !== 'undefined' && typeof document !== 'undefined';
543
440
  }
544
- /**
545
- * Check if browser supports required features
546
- */
547
441
  function checkBrowserSupport() {
548
442
  const missingFeatures = [];
549
443
  if (!isBrowser()) {
@@ -565,12 +459,6 @@ function checkBrowserSupport() {
565
459
  missingFeatures
566
460
  };
567
461
  }
568
- // ===================
569
- // DEVELOPMENT UTILITIES
570
- // ===================
571
- /**
572
- * Generate mock data for development/testing
573
- */
574
462
  function generateMockData() {
575
463
  return {
576
464
  address: 'oct' + Math.random().toString(36).substring(2, 22) + Math.random().toString(36).substring(2, 26),
@@ -593,9 +481,6 @@ function generateMockData() {
593
481
  }
594
482
  };
595
483
  }
596
- /**
597
- * Create development logger
598
- */
599
484
  function createLogger(prefix, debug) {
600
485
  const isDevelopment = typeof window !== 'undefined' && ((typeof globalThis !== 'undefined' && globalThis.process?.env?.NODE_ENV === 'development') ||
601
486
  window.location.hostname === 'localhost' ||
@@ -648,17 +533,6 @@ function createLogger(prefix, debug) {
648
533
  };
649
534
  }
650
535
 
651
- /**
652
- * 0xio Wallet SDK - Extension Communication Module
653
- *
654
- * @fileoverview Manages secure communication between the SDK and browser extension.
655
- * Implements message passing, request/response handling, rate limiting, and origin validation
656
- * to ensure secure wallet interactions.
657
- *
658
- * @module communication
659
- * @version 2.7.0
660
- * @license MIT
661
- */
662
536
  class ExtensionCommunicator extends EventEmitter {
663
537
  constructor(debug = false, trustedOrigins = [], adapter) {
664
538
  super(debug);
@@ -909,9 +783,12 @@ class ExtensionCommunicator extends EventEmitter {
909
783
  return;
910
784
  }
911
785
  const isSameOrigin = event.origin === window.location.origin;
912
- const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
913
786
  const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
914
- const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhost;
787
+ const hasExplicitList = this.trustedOrigins.length > 0;
788
+ // When trustedParentOrigins is explicitly set, implicit localhost trust is disabled
789
+ const isLocalhostAllowed = !hasExplicitList &&
790
+ (event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:'));
791
+ const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhostAllowed;
915
792
  // In iframe mode, only trust the actual parent window
916
793
  const inIframe = window.parent !== window;
917
794
  if (inIframe && event.source !== window.parent) {
@@ -1072,6 +949,7 @@ class ExtensionCommunicator extends EventEmitter {
1072
949
  // causing double popups where the second tx fails (stale nonce/state).
1073
950
  ExtensionCommunicator.NO_RETRY_METHODS = new Set([
1074
951
  'connect', 'send_transaction', 'call_contract', 'signMessage',
952
+ 'sign_transaction', 'broadcast_only',
1075
953
  'send_private_transfer', 'claim_private_transfer',
1076
954
  'encrypt_balance', 'decrypt_balance',
1077
955
  ]);
@@ -1079,8 +957,8 @@ ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHO
1079
957
  // only forward known event types
1080
958
  ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
1081
959
  'connect', 'disconnect', 'accountChanged', 'balanceChanged',
1082
- 'networkChanged', 'transactionConfirmed', 'error',
1083
- 'extensionLocked', 'extensionUnlocked'
960
+ 'networkChanged', 'transactionConfirmed', 'permissionsChanged', 'message',
961
+ 'error', 'extensionLocked', 'extensionUnlocked'
1084
962
  ]);
1085
963
 
1086
964
  /**
@@ -1163,6 +1041,14 @@ function validateNetworkInfo(raw) {
1163
1041
  return null;
1164
1042
  if (typeof raw.rpcUrl !== 'string' || (!raw.rpcUrl && raw.id !== 'custom'))
1165
1043
  return null;
1044
+ if (raw.rpcUrl) {
1045
+ const isHttps = raw.rpcUrl.startsWith('https://');
1046
+ const isLocal = raw.rpcUrl.startsWith('http://localhost') || raw.rpcUrl.startsWith('http://127.0.0.1');
1047
+ const isTestnet = raw.isTestnet === true;
1048
+ // Reject plain-http RPC URLs from untrusted sources unless testnet-flagged or local
1049
+ if (!isHttps && !isLocal && !isTestnet)
1050
+ return null;
1051
+ }
1166
1052
  if (typeof raw.supportsPrivacy !== 'boolean')
1167
1053
  return null;
1168
1054
  return Object.freeze({
@@ -1178,9 +1064,6 @@ function validateNetworkInfo(raw) {
1178
1064
  });
1179
1065
  }
1180
1066
 
1181
- /**
1182
- * SDK Configuration
1183
- */
1184
1067
  /**
1185
1068
  * Default balance structure.
1186
1069
  * Accepts a numeric total or undefined — never pass a Balance object here.
@@ -1216,27 +1099,17 @@ function validateBalance(raw) {
1216
1099
  currency: 'OCT'
1217
1100
  };
1218
1101
  }
1219
- /**
1220
- * SDK Configuration constants
1221
- */
1222
1102
  const SDK_CONFIG = {
1223
- version: '2.7.0',
1103
+ version: '2.7.1',
1224
1104
  defaultNetworkId: DEFAULT_NETWORK_ID,
1225
1105
  communicationTimeout: 30000, // 30 seconds
1226
1106
  retryAttempts: 3,
1227
1107
  retryDelay: 1000, // 1 second
1228
1108
  };
1229
- /**
1230
- * Get default network configuration
1231
- */
1232
1109
  function getDefaultNetwork() {
1233
1110
  return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
1234
1111
  }
1235
1112
 
1236
- /**
1237
- * 0xio Wallet SDK - Main Wallet Class
1238
- * Primary interface for DApp developers to interact with 0xio Wallet
1239
- */
1240
1113
  class ZeroXIOWallet extends EventEmitter {
1241
1114
  constructor(config) {
1242
1115
  super(config.debug);
@@ -1252,32 +1125,22 @@ class ZeroXIOWallet extends EventEmitter {
1252
1125
  debug: config.debug || false
1253
1126
  };
1254
1127
  this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
1255
- this.communicator = new ExtensionCommunicator(this.config.debug, [], this.config.adapter);
1128
+ this.communicator = new ExtensionCommunicator(this.config.debug, this.config.trustedParentOrigins ?? [], this.config.adapter);
1256
1129
  this.logger.log('Wallet instance created with config:', this.config);
1257
1130
  }
1258
- // ===================
1259
- // INITIALIZATION
1260
- // ===================
1261
- /**
1262
- * Initialize the SDK
1263
- * Must be called before using any other methods
1264
- */
1265
1131
  async initialize() {
1266
1132
  if (this.isInitialized) {
1267
1133
  return true;
1268
1134
  }
1269
- // single-flight init
1270
1135
  if (this._initPromise) {
1271
1136
  return this._initPromise;
1272
1137
  }
1273
1138
  this._initPromise = (async () => {
1274
1139
  try {
1275
- // Initialize extension communication
1276
1140
  const communicationReady = await this.communicator.initialize();
1277
1141
  if (!communicationReady) {
1278
1142
  throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
1279
1143
  }
1280
- // Register this DApp with the extension
1281
1144
  await this.communicator.sendRequest('register_dapp', {
1282
1145
  appName: this.config.appName,
1283
1146
  appDescription: this.config.appDescription,
@@ -1287,7 +1150,6 @@ class ZeroXIOWallet extends EventEmitter {
1287
1150
  requiredPermissions: this.config.requiredPermissions,
1288
1151
  networkId: this.config.networkId
1289
1152
  });
1290
- // Setup event forwarding from extension
1291
1153
  this.setupExtensionEventListeners();
1292
1154
  this.isInitialized = true;
1293
1155
  this.logger.log('SDK initialized successfully');
@@ -1306,29 +1168,21 @@ class ZeroXIOWallet extends EventEmitter {
1306
1168
  })();
1307
1169
  return this._initPromise;
1308
1170
  }
1309
- /**
1310
- * Check if SDK is initialized
1311
- */
1312
1171
  isReady() {
1313
1172
  return this.isInitialized && this.communicator.isExtensionAvailable();
1314
1173
  }
1315
- // ===================
1316
- // CONNECTION MANAGEMENT
1317
- // ===================
1318
- /**
1319
- * Connect to wallet
1320
- */
1321
1174
  async connect(options = {}) {
1322
1175
  this.ensureInitialized();
1323
1176
  try {
1324
1177
  this.logger.log('Attempting to connect with options:', options);
1325
- // filter to declared perms only
1178
+ // filter to declared perms only — accept both RFC 'permissions' and legacy 'requestPermissions'
1326
1179
  const declaredPermissions = this.config.requiredPermissions || [];
1327
- const requestPermissions = options.requestPermissions
1328
- ? options.requestPermissions.filter(p => declaredPermissions.includes(p))
1180
+ const requestedPerms = options.permissions ?? options.requestPermissions;
1181
+ const requestedPermissions = requestedPerms
1182
+ ? requestedPerms.filter(p => declaredPermissions.includes(p))
1329
1183
  : declaredPermissions;
1330
1184
  const result = await this.communicator.sendRequest('connect', {
1331
- requestPermissions,
1185
+ permissions: requestedPermissions,
1332
1186
  networkId: options.networkId || this.config.networkId
1333
1187
  });
1334
1188
  // verify pubkey→addr binding
@@ -1345,9 +1199,12 @@ class ZeroXIOWallet extends EventEmitter {
1345
1199
  this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
1346
1200
  }
1347
1201
  }
1348
- // Use networkInfo from extension response — validate before caching
1202
+ // Use networkInfo from extension response — validate before caching.
1349
1203
  const networkInfo = validateNetworkInfo(result.networkInfo)
1350
- ?? getNetworkConfig(result.networkId || this.config.networkId);
1204
+ ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1205
+ if (!networkInfo) {
1206
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
1207
+ }
1351
1208
  const permissions = result.permissions || [];
1352
1209
  // Update connection info — including permissions
1353
1210
  this.connectionInfo = {
@@ -1440,7 +1297,11 @@ class ZeroXIOWallet extends EventEmitter {
1440
1297
  // validate untrusted balance/networkInfo before caching
1441
1298
  const balanceInfo = validateBalance(result.balance) ?? createDefaultBalance();
1442
1299
  const networkInfo = validateNetworkInfo(result.networkInfo)
1443
- ?? getNetworkConfig(result.networkId || this.config.networkId);
1300
+ ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1301
+ if (!networkInfo) {
1302
+ this.logger.warn('getConnectionStatus: wallet returned no network metadata — returning cached state');
1303
+ return this.connectionInfo;
1304
+ }
1444
1305
  const wasConnected = this.connectionInfo.isConnected;
1445
1306
  const permissions = result.permissions || [];
1446
1307
  // preserve existing connectedAt
@@ -1513,18 +1374,9 @@ class ZeroXIOWallet extends EventEmitter {
1513
1374
  getNetworkId() {
1514
1375
  return this.connectionInfo.networkInfo?.id || null;
1515
1376
  }
1516
- // ===================
1517
- // WALLET INFORMATION
1518
- // ===================
1519
- /**
1520
- * Get current wallet address
1521
- */
1522
1377
  getAddress() {
1523
1378
  return this.connectionInfo.address || null;
1524
1379
  }
1525
- /**
1526
- * Get current balance
1527
- */
1528
1380
  async getBalance(forceRefresh = false) {
1529
1381
  this.ensureConnected();
1530
1382
  try {
@@ -1549,7 +1401,6 @@ class ZeroXIOWallet extends EventEmitter {
1549
1401
  // skip if session changed mid-flight
1550
1402
  if (this._sessionVersion !== sv)
1551
1403
  return result;
1552
- // Update cached balance
1553
1404
  if (this.connectionInfo.balance) {
1554
1405
  const previousBalance = this.connectionInfo.balance;
1555
1406
  this.connectionInfo.balance = result;
@@ -1576,15 +1427,11 @@ class ZeroXIOWallet extends EventEmitter {
1576
1427
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
1577
1428
  }
1578
1429
  }
1579
- /**
1580
- * Get network information
1581
- */
1582
1430
  async getNetworkInfo() {
1583
1431
  this.ensureInitialized();
1584
1432
  try {
1585
1433
  const sv = this._sessionVersion;
1586
1434
  const result = await this.communicator.sendRequest('get_network_info');
1587
- // validate network info before caching
1588
1435
  const networkInfo = validateNetworkInfo(result);
1589
1436
  if (!networkInfo) {
1590
1437
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Extension returned invalid network info');
@@ -1592,11 +1439,9 @@ class ZeroXIOWallet extends EventEmitter {
1592
1439
  // skip if session changed mid-flight
1593
1440
  if (this._sessionVersion !== sv)
1594
1441
  return networkInfo;
1595
- // Update cached network info
1596
1442
  if (this.connectionInfo.networkInfo) {
1597
1443
  const previousNetwork = this.connectionInfo.networkInfo;
1598
1444
  this.connectionInfo.networkInfo = networkInfo;
1599
- // Emit network changed event if different
1600
1445
  if (previousNetwork.id !== networkInfo.id) {
1601
1446
  const networkChangedEvent = {
1602
1447
  previousNetwork,
@@ -1616,22 +1461,14 @@ class ZeroXIOWallet extends EventEmitter {
1616
1461
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
1617
1462
  }
1618
1463
  }
1619
- // ===================
1620
- // TRANSACTIONS
1621
- // ===================
1622
- /**
1623
- * Send transaction
1624
- */
1625
1464
  async sendTransaction(txData) {
1626
1465
  this.ensureConnected();
1627
- // validate inputs
1628
1466
  if (!isValidAddress(txData.to)) {
1629
1467
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1630
1468
  }
1631
1469
  if (!isValidAmount(txData.amount)) {
1632
1470
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1633
1471
  }
1634
- // bound memo
1635
1472
  if (txData.message && txData.message.length > 1000) {
1636
1473
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1637
1474
  }
@@ -1640,8 +1477,8 @@ class ZeroXIOWallet extends EventEmitter {
1640
1477
  this.logger.log('Sending transaction:', { to: txData.to });
1641
1478
  const result = await this.communicator.sendRequest('send_transaction', txData);
1642
1479
  this.logger.log('Transaction result:', result);
1643
- // Refresh balance after successful transaction
1644
- if (result.success) {
1480
+ // Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
1481
+ if (result.accepted ?? result.success) {
1645
1482
  setTimeout(() => {
1646
1483
  this.getBalance(true).catch(error => {
1647
1484
  this.logger.warn('Failed to refresh balance after transaction:', error);
@@ -1658,23 +1495,70 @@ class ZeroXIOWallet extends EventEmitter {
1658
1495
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to send transaction', error);
1659
1496
  }
1660
1497
  }
1498
+ /**
1499
+ * Sign a transaction without broadcasting it (RFC-O-1 octra_signTransaction).
1500
+ * Returns the signed transaction object for manual submission via submitTransaction().
1501
+ */
1502
+ async signTransaction(txData) {
1503
+ this.ensureConnected();
1504
+ if (!isValidAddress(txData.to)) {
1505
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1506
+ }
1507
+ if (!isValidAmount(txData.amount)) {
1508
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1509
+ }
1510
+ if (txData.message && txData.message.length > 1000) {
1511
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1512
+ }
1513
+ try {
1514
+ this.logger.log('Requesting transaction signature:', { to: txData.to });
1515
+ const result = await this.communicator.sendRequest('sign_transaction', txData);
1516
+ return result;
1517
+ }
1518
+ catch (error) {
1519
+ if (error instanceof ZeroXIOWalletError)
1520
+ throw error;
1521
+ throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Failed to sign transaction', error);
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Broadcast a pre-signed transaction (RFC-O-1 octra_submitTransaction).
1526
+ * Use after signTransaction() to submit the signed tx to the network.
1527
+ */
1528
+ async submitTransaction(signedTx) {
1529
+ this.ensureConnected();
1530
+ if (!signedTx || typeof signedTx !== 'object') {
1531
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'signedTx must be an object');
1532
+ }
1533
+ try {
1534
+ this.logger.log('Submitting pre-signed transaction');
1535
+ const result = await this.communicator.sendRequest('broadcast_only', { signedTx });
1536
+ return result;
1537
+ }
1538
+ catch (error) {
1539
+ if (error instanceof ZeroXIOWalletError)
1540
+ throw error;
1541
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to submit transaction', error);
1542
+ }
1543
+ }
1661
1544
  /**
1662
1545
  * Call a smart contract method (state-changing).
1663
1546
  * The extension builds, signs, and submits the transaction via octra_submit.
1664
1547
  */
1665
1548
  async callContract(callData) {
1666
1549
  this.ensureConnected();
1667
- // validate inputs
1668
1550
  if (!isValidAddress(callData.contract)) {
1669
1551
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1670
1552
  }
1671
1553
  if (!callData.method || typeof callData.method !== 'string') {
1672
1554
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method is required');
1673
1555
  }
1674
- // bound method + params size
1675
1556
  if (callData.method.length > 200) {
1676
1557
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
1677
1558
  }
1559
+ if (callData.amount != null) {
1560
+ this.assertExactOCTAmount(callData.amount, 'Contract call amount');
1561
+ }
1678
1562
  try {
1679
1563
  if (JSON.stringify(callData.params).length > 65536) {
1680
1564
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
@@ -1712,14 +1596,12 @@ class ZeroXIOWallet extends EventEmitter {
1712
1596
  */
1713
1597
  async contractCallView(viewData) {
1714
1598
  this.ensureInitialized();
1715
- // validate inputs
1716
1599
  if (!isValidAddress(viewData.contract)) {
1717
1600
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1718
1601
  }
1719
1602
  if (!viewData.method || typeof viewData.method !== 'string') {
1720
1603
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method is required');
1721
1604
  }
1722
- // bound method + params size
1723
1605
  if (viewData.method.length > 200) {
1724
1606
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method name too long (max 200 characters)');
1725
1607
  }
@@ -1759,7 +1641,6 @@ class ZeroXIOWallet extends EventEmitter {
1759
1641
  */
1760
1642
  async getContractStorage(contract, key) {
1761
1643
  this.ensureInitialized();
1762
- // validate inputs
1763
1644
  if (!isValidAddress(contract)) {
1764
1645
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1765
1646
  }
@@ -1801,12 +1682,6 @@ class ZeroXIOWallet extends EventEmitter {
1801
1682
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get transaction history', error);
1802
1683
  }
1803
1684
  }
1804
- // ===================
1805
- // PRIVATE FEATURES
1806
- // ===================
1807
- /**
1808
- * Get private balance information
1809
- */
1810
1685
  async getPrivateBalanceInfo() {
1811
1686
  this.ensureConnected();
1812
1687
  try {
@@ -1822,7 +1697,7 @@ class ZeroXIOWallet extends EventEmitter {
1822
1697
  */
1823
1698
  async encryptBalance(amount) {
1824
1699
  this.ensureConnected();
1825
- // validate amount
1700
+ this.assertExactOCTAmount(amount, 'Encrypt amount');
1826
1701
  if (!isValidAmount(amount)) {
1827
1702
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1828
1703
  }
@@ -1843,7 +1718,7 @@ class ZeroXIOWallet extends EventEmitter {
1843
1718
  */
1844
1719
  async decryptBalance(amount) {
1845
1720
  this.ensureConnected();
1846
- // validate amount
1721
+ this.assertExactOCTAmount(amount, 'Decrypt amount');
1847
1722
  if (!isValidAmount(amount)) {
1848
1723
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1849
1724
  }
@@ -1869,21 +1744,21 @@ class ZeroXIOWallet extends EventEmitter {
1869
1744
  */
1870
1745
  async sendPrivateTransfer(transferData) {
1871
1746
  this.ensureConnected();
1872
- // validate inputs
1873
1747
  if (!isValidAddress(transferData.to)) {
1874
1748
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1875
1749
  }
1876
1750
  if (!isValidAmount(transferData.amount)) {
1877
1751
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transfer amount');
1878
1752
  }
1753
+ this.assertExactOCTAmount(transferData.amount, 'Transfer amount');
1879
1754
  // bound msg size
1880
1755
  if (transferData.message && transferData.message.length > 1000) {
1881
1756
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
1882
1757
  }
1883
1758
  try {
1884
1759
  const result = await this.communicator.sendRequest('send_private_transfer', transferData);
1885
- // Refresh balance after transfer
1886
- if (result.success) {
1760
+ // Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
1761
+ if (result.accepted ?? result.success) {
1887
1762
  setTimeout(() => {
1888
1763
  this.getBalance(true).catch(() => { });
1889
1764
  }, 1000);
@@ -1923,8 +1798,8 @@ class ZeroXIOWallet extends EventEmitter {
1923
1798
  const result = await this.communicator.sendRequest('claim_private_transfer', {
1924
1799
  transferId
1925
1800
  });
1926
- // Refresh balance after claiming
1927
- if (result.success) {
1801
+ // Refresh balance after claiming (accept RFC 'accepted' or legacy 'success')
1802
+ if (result.accepted ?? result.success) {
1928
1803
  setTimeout(() => {
1929
1804
  this.getBalance(true).catch(() => { });
1930
1805
  }, 1000);
@@ -1935,9 +1810,6 @@ class ZeroXIOWallet extends EventEmitter {
1935
1810
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
1936
1811
  }
1937
1812
  }
1938
- // ===================
1939
- // MESSAGE SIGNING
1940
- // ===================
1941
1813
  /**
1942
1814
  * Sign an arbitrary message with the wallet's private key
1943
1815
  * The user will be prompted to approve the signature request in the extension
@@ -1975,9 +1847,6 @@ class ZeroXIOWallet extends EventEmitter {
1975
1847
  throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Failed to sign message', error);
1976
1848
  }
1977
1849
  }
1978
- // ===================
1979
- // AUTHENTICATION HELPERS
1980
- // ===================
1981
1850
  /**
1982
1851
  * Sign a domain-separated authentication message.
1983
1852
  * Unlike `signMessage()`, this prepends a standard header that binds the signature
@@ -1999,9 +1868,6 @@ class ZeroXIOWallet extends EventEmitter {
1999
1868
  const domainSeparated = `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
2000
1869
  return this.signMessage(domainSeparated);
2001
1870
  }
2002
- // ===================
2003
- // PRIVATE METHODS
2004
- // ===================
2005
1871
  ensureInitialized() {
2006
1872
  if (!this.isInitialized) {
2007
1873
  throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'SDK not initialized. Call initialize() first.');
@@ -2014,7 +1880,6 @@ class ZeroXIOWallet extends EventEmitter {
2014
1880
  }
2015
1881
  }
2016
1882
  setupExtensionEventListeners() {
2017
- // Listen for extension events through the communicator
2018
1883
  this.communicator.on('accountChanged', (event) => {
2019
1884
  this.handleAccountChanged(event.data);
2020
1885
  });
@@ -2033,11 +1898,18 @@ class ZeroXIOWallet extends EventEmitter {
2033
1898
  this.communicator.on('transactionConfirmed', (event) => {
2034
1899
  this.handleTransactionConfirmed(event.data);
2035
1900
  });
1901
+ this.communicator.on('permissionsChanged', (event) => {
1902
+ const permissions = event.data ?? event;
1903
+ if (this.connectionInfo.isConnected) {
1904
+ this.connectionInfo.permissions = Array.isArray(permissions) ? permissions : [];
1905
+ }
1906
+ this.emit('permissionsChanged', permissions);
1907
+ });
1908
+ this.communicator.on('message', (event) => {
1909
+ this.emit('message', event.data ?? event);
1910
+ });
2036
1911
  this.logger.log('Extension event listeners setup complete');
2037
1912
  }
2038
- /**
2039
- * Handle account changed event from extension
2040
- */
2041
1913
  handleAccountChanged(data) {
2042
1914
  ++this._sessionVersion;
2043
1915
  const previousAddress = this.connectionInfo.address;
@@ -2045,7 +1917,6 @@ class ZeroXIOWallet extends EventEmitter {
2045
1917
  // clear stale pubkey on acct change
2046
1918
  this.connectionInfo.publicKey = data.publicKey;
2047
1919
  if (data.balance) {
2048
- // validate balance before caching
2049
1920
  const validated = validateBalance(data.balance);
2050
1921
  if (validated) {
2051
1922
  this.connectionInfo.balance = validated;
@@ -2063,9 +1934,6 @@ class ZeroXIOWallet extends EventEmitter {
2063
1934
  this.emit('accountChanged', accountChangedEvent);
2064
1935
  this.logger.log('Account changed:', { newAddress: accountChangedEvent.newAddress });
2065
1936
  }
2066
- /**
2067
- * Handle network changed event from extension
2068
- */
2069
1937
  handleNetworkChanged(data) {
2070
1938
  const previousNetwork = this.connectionInfo.networkInfo;
2071
1939
  // validate networkInfo — drop invalid
@@ -2084,11 +1952,7 @@ class ZeroXIOWallet extends EventEmitter {
2084
1952
  this.emit('networkChanged', networkChangedEvent);
2085
1953
  this.logger.log('Network changed:', networkChangedEvent);
2086
1954
  }
2087
- /**
2088
- * Handle balance changed event from extension
2089
- */
2090
1955
  handleBalanceChanged(data) {
2091
- // validate balance before caching
2092
1956
  const balance = validateBalance(data.balance);
2093
1957
  if (!balance) {
2094
1958
  this.logger.warn('Received invalid balance in balanceChanged event, ignoring');
@@ -2104,13 +1968,9 @@ class ZeroXIOWallet extends EventEmitter {
2104
1968
  this.emit('balanceChanged', balanceChangedEvent);
2105
1969
  this.logger.log('Balance changed:', { public: balance.public });
2106
1970
  }
2107
- /**
2108
- * Handle extension locked event
2109
- */
2110
1971
  handleExtensionLocked() {
2111
1972
  ++this._sessionVersion;
2112
1973
  this.connectionInfo = { isConnected: false };
2113
- // emit extensionLocked then disconnect
2114
1974
  this.emit('extensionLocked', {});
2115
1975
  const disconnectEvent = {
2116
1976
  reason: 'extension_locked'
@@ -2118,21 +1978,13 @@ class ZeroXIOWallet extends EventEmitter {
2118
1978
  this.emit('disconnect', disconnectEvent);
2119
1979
  this.logger.log('Extension locked - disconnected');
2120
1980
  }
2121
- /**
2122
- * Handle extension unlocked event
2123
- */
2124
1981
  handleExtensionUnlocked() {
2125
- // emit extensionUnlocked
2126
1982
  this.emit('extensionUnlocked', {});
2127
- // Attempt to restore connection
2128
1983
  this.getConnectionStatus().catch(() => {
2129
1984
  this.logger.warn('Could not restore connection after unlock');
2130
1985
  });
2131
1986
  this.logger.log('Extension unlocked');
2132
1987
  }
2133
- /**
2134
- * Handle transaction confirmed event
2135
- */
2136
1988
  handleTransactionConfirmed(data) {
2137
1989
  this.emit('transactionConfirmed', {
2138
1990
  txHash: data.txHash,
@@ -2145,12 +1997,21 @@ class ZeroXIOWallet extends EventEmitter {
2145
1997
  }, 2000);
2146
1998
  this.logger.log('Transaction confirmed:', data.txHash);
2147
1999
  }
2148
- // ===================
2149
- // CLEANUP
2150
- // ===================
2151
2000
  /**
2152
- * Clean up SDK resources
2001
+ * Reject numeric amounts that cannot be represented exactly in micro-OCT.
2002
+ * e.g. 0.1 + 0.2 = 0.30000000000000004 — the extension would sign the wrong value.
2003
+ * String amounts bypass this check (caller is responsible for correctness).
2153
2004
  */
2005
+ assertExactOCTAmount(amount, label) {
2006
+ if (typeof amount === 'number') {
2007
+ const micro = Math.round(amount * 1000000);
2008
+ if (Math.abs(amount - micro / 1000000) > 1e-10) {
2009
+ const suggested = (micro / 1000000).toFixed(6);
2010
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label} cannot be represented exactly in micro-OCT. ` +
2011
+ `Pass a string instead (e.g. "${suggested}").`);
2012
+ }
2013
+ }
2014
+ }
2154
2015
  cleanup() {
2155
2016
  this.communicator.cleanup();
2156
2017
  this.removeAllListeners();
@@ -2187,7 +2048,9 @@ const SDK_TO_RFC = {
2187
2048
  switch_network: 'octra_switchNetwork',
2188
2049
  signMessage: 'octra_signMessage',
2189
2050
  send_transaction: 'octra_sendTransaction',
2190
- call_contract: 'octra_callContract',
2051
+ sign_transaction: 'octra_signTransaction',
2052
+ broadcast_only: 'octra_submitTransaction',
2053
+ call_contract: 'octra_sendContractTransaction',
2191
2054
  contract_call_view: 'octra_callContract',
2192
2055
  get_private_balance_info: 'octra_getEncryptedBalance',
2193
2056
  encrypt_balance: 'octra_encryptBalance',
@@ -2199,7 +2062,7 @@ const SDK_TO_RFC = {
2199
2062
  const RFC_TO_SDK_ERROR = {
2200
2063
  4001: 'USER_REJECTED',
2201
2064
  4100: 'PERMISSION_DENIED',
2202
- 4200: 'NETWORK_ERROR',
2065
+ 4200: 'UNKNOWN_ERROR',
2203
2066
  4900: 'CONNECTION_REFUSED',
2204
2067
  4901: 'NETWORK_ERROR',
2205
2068
  };
@@ -2216,7 +2079,7 @@ function mapError(err) {
2216
2079
  * three RFC-O-1 calls: octra_requestAccounts, octra_networkInfo, octra_permissions.
2217
2080
  */
2218
2081
  async function rfcConnect(provider, params) {
2219
- const requestPerms = params?.requestPermissions ?? [];
2082
+ const requestPerms = params?.permissions ?? params?.requestPermissions ?? [];
2220
2083
  const accounts = (await provider.request({
2221
2084
  method: 'octra_requestAccounts',
2222
2085
  params: [{ permissions: requestPerms }],
@@ -2304,12 +2167,14 @@ function createOctraProviderAdapter() {
2304
2167
  const onNetworkChanged = (data) => handler({ eventType: 'networkChanged', eventData: { networkInfo: data } });
2305
2168
  const onBalanceChanged = (data) => handler({ eventType: 'balanceChanged', eventData: data });
2306
2169
  const onTransactionChanged = (data) => handler({ eventType: 'transactionConfirmed', eventData: data });
2170
+ const onPermissionsChanged = (data) => handler({ eventType: 'permissionsChanged', eventData: data });
2307
2171
  provider.on('connect', onConnect);
2308
2172
  provider.on('disconnect', onDisconnect);
2309
2173
  provider.on('accountsChanged', onAccountsChanged);
2310
2174
  provider.on('networkChanged', onNetworkChanged);
2311
2175
  provider.on('balanceChanged', onBalanceChanged);
2312
2176
  provider.on('transactionChanged', onTransactionChanged);
2177
+ provider.on('permissionsChanged', onPermissionsChanged);
2313
2178
  const cleanup = () => {
2314
2179
  provider.removeListener('connect', onConnect);
2315
2180
  provider.removeListener('disconnect', onDisconnect);
@@ -2317,6 +2182,7 @@ function createOctraProviderAdapter() {
2317
2182
  provider.removeListener('networkChanged', onNetworkChanged);
2318
2183
  provider.removeListener('balanceChanged', onBalanceChanged);
2319
2184
  provider.removeListener('transactionChanged', onTransactionChanged);
2185
+ provider.removeListener('permissionsChanged', onPermissionsChanged);
2320
2186
  _handler = null;
2321
2187
  };
2322
2188
  return cleanup;
@@ -2324,7 +2190,11 @@ function createOctraProviderAdapter() {
2324
2190
  listenForReady(onReady) {
2325
2191
  const handler = () => onReady();
2326
2192
  window.addEventListener('octraWalletReady', handler);
2327
- return () => window.removeEventListener('octraWalletReady', handler);
2193
+ window.addEventListener('octra#initialized', handler);
2194
+ return () => {
2195
+ window.removeEventListener('octraWalletReady', handler);
2196
+ window.removeEventListener('octra#initialized', handler);
2197
+ };
2328
2198
  },
2329
2199
  };
2330
2200
  }
@@ -2387,7 +2257,7 @@ function getAllAdapters() {
2387
2257
  */
2388
2258
  // Main exports
2389
2259
  // Version information
2390
- const SDK_VERSION = '2.7.0';
2260
+ const SDK_VERSION = '2.7.1';
2391
2261
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
2392
2262
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
2393
2263
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -2405,8 +2275,7 @@ async function createZeroXIOWallet(config) {
2405
2275
  try {
2406
2276
  await wallet$1.connect();
2407
2277
  }
2408
- catch (error) {
2409
- if (config.debug) ;
2278
+ catch {
2410
2279
  // Don't throw - let the app handle connection manually
2411
2280
  }
2412
2281
  }