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