@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.umd.js CHANGED
@@ -4,28 +4,16 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ZeroXIOWalletSDK = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- /**
8
- * 0xio Wallet SDK - Event System
9
- * Type-safe event emitter for wallet events
10
- */
11
7
  class EventEmitter {
12
- constructor(debug = false) {
8
+ constructor(_debug = false) {
13
9
  this.listeners = new Map();
14
- this.debug = debug;
15
10
  }
16
- /**
17
- * Add event listener
18
- */
19
11
  on(eventType, listener) {
20
12
  if (!this.listeners.has(eventType)) {
21
13
  this.listeners.set(eventType, new Set());
22
14
  }
23
15
  this.listeners.get(eventType).add(listener);
24
- if (this.debug) ;
25
16
  }
26
- /**
27
- * Remove event listener
28
- */
29
17
  off(eventType, listener) {
30
18
  const eventListeners = this.listeners.get(eventType);
31
19
  if (eventListeners) {
@@ -33,12 +21,8 @@
33
21
  if (eventListeners.size === 0) {
34
22
  this.listeners.delete(eventType);
35
23
  }
36
- if (this.debug) ;
37
24
  }
38
25
  }
39
- /**
40
- * Add one-time event listener
41
- */
42
26
  once(eventType, listener) {
43
27
  const onceListener = (event) => {
44
28
  // Remove BEFORE calling so a throwing listener doesn't stay registered
@@ -47,9 +31,6 @@
47
31
  };
48
32
  this.on(eventType, onceListener);
49
33
  }
50
- /**
51
- * Emit event to all listeners
52
- */
53
34
  emit(eventType, data) {
54
35
  const event = {
55
36
  type: eventType,
@@ -58,49 +39,32 @@
58
39
  };
59
40
  const eventListeners = this.listeners.get(eventType);
60
41
  if (eventListeners && eventListeners.size > 0) {
61
- if (this.debug) ;
62
- // Create a copy to avoid issues if listeners modify the set during iteration
63
- const listenersArray = Array.from(eventListeners);
64
- for (const listener of listenersArray) {
42
+ // snapshot to avoid issues if listeners modify the set during iteration
43
+ for (const listener of Array.from(eventListeners)) {
65
44
  try {
66
45
  listener(event);
67
46
  }
68
- catch (error) {
69
- // console.error(`[0xio SDK] Error in event listener for '${eventType}':`, error);
47
+ catch {
48
+ // listener errors are swallowed to keep the event loop running
70
49
  }
71
50
  }
72
51
  }
73
- else if (this.debug) ;
74
52
  }
75
- /**
76
- * Remove all listeners for a specific event type
77
- */
78
53
  removeAllListeners(eventType) {
79
54
  if (eventType) {
80
55
  this.listeners.delete(eventType);
81
- if (this.debug) ;
82
56
  }
83
57
  else {
84
58
  this.listeners.clear();
85
- if (this.debug) ;
86
59
  }
87
60
  }
88
- /**
89
- * Get number of listeners for an event type
90
- */
91
61
  listenerCount(eventType) {
92
62
  const eventListeners = this.listeners.get(eventType);
93
63
  return eventListeners ? eventListeners.size : 0;
94
64
  }
95
- /**
96
- * Get all event types that have listeners
97
- */
98
65
  eventTypes() {
99
66
  return Array.from(this.listeners.keys());
100
67
  }
101
- /**
102
- * Check if there are any listeners for an event type
103
- */
104
68
  hasListeners(eventType) {
105
69
  return this.listenerCount(eventType) > 0;
106
70
  }
@@ -273,16 +237,6 @@
273
237
  /** Default 0xio adapter instance (no extra trusted origins). */
274
238
  const ZeroXIOAdapter = createZeroXIOAdapter();
275
239
 
276
- /**
277
- * 0xio Wallet SDK - Utilities
278
- * Helper functions for validation, formatting, and common operations
279
- */
280
- // ===================
281
- // VALIDATION UTILITIES
282
- // ===================
283
- /**
284
- * Validate wallet address for Octra blockchain
285
- */
286
240
  function isValidAddress(address) {
287
241
  if (!address || typeof address !== 'string') {
288
242
  return false;
@@ -306,9 +260,6 @@
306
260
  Number.isFinite(amount) &&
307
261
  amount <= Number.MAX_SAFE_INTEGER;
308
262
  }
309
- /**
310
- * Validate transaction message
311
- */
312
263
  function isValidMessage(message) {
313
264
  // Type check first — falsy non-strings (0, false, null) are NOT valid messages
314
265
  if (typeof message !== 'string') {
@@ -321,15 +272,9 @@
321
272
  // 100KB limit — contract call params can be large (serialized JSON)
322
273
  return message.length <= 100000;
323
274
  }
324
- /**
325
- * Validate fee level
326
- */
327
275
  function isValidFeeLevel(feeLevel) {
328
276
  return feeLevel === 1 || feeLevel === 3;
329
277
  }
330
- // ===================
331
- // ADDRESS DERIVATION
332
- // ===================
333
278
  const _B58_ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
334
279
  function _base58Encode(buf) {
335
280
  let zeros = 0;
@@ -374,12 +319,6 @@
374
319
  const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
375
320
  return 'oct' + _base58Encode(new Uint8Array(hashBuf));
376
321
  }
377
- // ===================
378
- // FORMATTING UTILITIES
379
- // ===================
380
- /**
381
- * Format OCT amount for display
382
- */
383
322
  function formatOCT(amount, decimals = 6) {
384
323
  const n = typeof amount === 'string' ? parseFloat(amount) : amount;
385
324
  if (!isValidAmount(n)) {
@@ -390,9 +329,6 @@
390
329
  maximumFractionDigits: decimals
391
330
  });
392
331
  }
393
- /**
394
- * Format address for display (truncated)
395
- */
396
332
  function formatAddress(address, prefixLength = 6, suffixLength = 4) {
397
333
  if (!isValidAddress(address)) {
398
334
  return 'Invalid Address';
@@ -402,16 +338,10 @@
402
338
  }
403
339
  return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}`;
404
340
  }
405
- /**
406
- * Format timestamp for display
407
- */
408
341
  function formatTimestamp(timestamp) {
409
342
  const date = new Date(timestamp);
410
343
  return date.toLocaleString();
411
344
  }
412
- /**
413
- * Format transaction hash for display
414
- */
415
345
  function formatTxHash(hash, length = 12) {
416
346
  if (!hash || typeof hash !== 'string') {
417
347
  return 'Invalid Hash';
@@ -423,12 +353,6 @@
423
353
  const suffixLength = Math.floor(length / 2);
424
354
  return `${hash.slice(0, prefixLength)}...${hash.slice(-suffixLength)}`;
425
355
  }
426
- // ===================
427
- // CONVERSION UTILITIES
428
- // ===================
429
- /**
430
- * Convert OCT to micro OCT (for network transmission)
431
- */
432
356
  function toMicroOCT(amount) {
433
357
  if (!isValidAmount(amount)) {
434
358
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Invalid amount for conversion');
@@ -437,9 +361,6 @@
437
361
  const microOCT = Math.round(amount * 1000000);
438
362
  return microOCT.toString();
439
363
  }
440
- /**
441
- * Convert micro OCT to OCT (for display)
442
- */
443
364
  function fromMicroOCT(microAmount) {
444
365
  const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
445
366
  if (!Number.isFinite(amount) || amount < 0) {
@@ -447,12 +368,6 @@
447
368
  }
448
369
  return amount / 1000000;
449
370
  }
450
- // ===================
451
- // ERROR UTILITIES
452
- // ===================
453
- /**
454
- * Create standardized error messages
455
- */
456
371
  function createErrorMessage(code, context) {
457
372
  const baseMessages = {
458
373
  [exports.ErrorCode.EXTENSION_NOT_FOUND]: '0xio Wallet extension is not installed or enabled',
@@ -479,24 +394,12 @@
479
394
  const baseMessage = baseMessages[code] || 'Unknown error';
480
395
  return context ? `${baseMessage}: ${context}` : baseMessage;
481
396
  }
482
- /**
483
- * Check if error is a specific type
484
- */
485
397
  function isErrorType(error, code) {
486
398
  return error instanceof ZeroXIOWalletError && error.code === code;
487
399
  }
488
- // ===================
489
- // ASYNC UTILITIES
490
- // ===================
491
- /**
492
- * Create a promise that resolves after a delay
493
- */
494
400
  function delay(ms) {
495
401
  return new Promise(resolve => setTimeout(resolve, ms));
496
402
  }
497
- /**
498
- * Retry an async operation with exponential backoff
499
- */
500
403
  async function retry(operation, maxRetries = 3, baseDelay = 1000) {
501
404
  let lastError;
502
405
  // maxRetries = number of retries AFTER the first attempt
@@ -536,18 +439,9 @@
536
439
  clearTimeout(timer);
537
440
  });
538
441
  }
539
- // ===================
540
- // BROWSER UTILITIES
541
- // ===================
542
- /**
543
- * Check if running in browser environment
544
- */
545
442
  function isBrowser() {
546
443
  return typeof window !== 'undefined' && typeof document !== 'undefined';
547
444
  }
548
- /**
549
- * Check if browser supports required features
550
- */
551
445
  function checkBrowserSupport() {
552
446
  const missingFeatures = [];
553
447
  if (!isBrowser()) {
@@ -569,12 +463,6 @@
569
463
  missingFeatures
570
464
  };
571
465
  }
572
- // ===================
573
- // DEVELOPMENT UTILITIES
574
- // ===================
575
- /**
576
- * Generate mock data for development/testing
577
- */
578
466
  function generateMockData() {
579
467
  return {
580
468
  address: 'oct' + Math.random().toString(36).substring(2, 22) + Math.random().toString(36).substring(2, 26),
@@ -597,9 +485,6 @@
597
485
  }
598
486
  };
599
487
  }
600
- /**
601
- * Create development logger
602
- */
603
488
  function createLogger(prefix, debug) {
604
489
  const isDevelopment = typeof window !== 'undefined' && ((typeof globalThis !== 'undefined' && globalThis.process?.env?.NODE_ENV === 'development') ||
605
490
  window.location.hostname === 'localhost' ||
@@ -652,17 +537,6 @@
652
537
  };
653
538
  }
654
539
 
655
- /**
656
- * 0xio Wallet SDK - Extension Communication Module
657
- *
658
- * @fileoverview Manages secure communication between the SDK and browser extension.
659
- * Implements message passing, request/response handling, rate limiting, and origin validation
660
- * to ensure secure wallet interactions.
661
- *
662
- * @module communication
663
- * @version 2.7.0
664
- * @license MIT
665
- */
666
540
  class ExtensionCommunicator extends EventEmitter {
667
541
  constructor(debug = false, trustedOrigins = [], adapter) {
668
542
  super(debug);
@@ -913,9 +787,12 @@
913
787
  return;
914
788
  }
915
789
  const isSameOrigin = event.origin === window.location.origin;
916
- const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
917
790
  const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
918
- const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhost;
791
+ const hasExplicitList = this.trustedOrigins.length > 0;
792
+ // When trustedParentOrigins is explicitly set, implicit localhost trust is disabled
793
+ const isLocalhostAllowed = !hasExplicitList &&
794
+ (event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:'));
795
+ const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhostAllowed;
919
796
  // In iframe mode, only trust the actual parent window
920
797
  const inIframe = window.parent !== window;
921
798
  if (inIframe && event.source !== window.parent) {
@@ -1076,6 +953,7 @@
1076
953
  // causing double popups where the second tx fails (stale nonce/state).
1077
954
  ExtensionCommunicator.NO_RETRY_METHODS = new Set([
1078
955
  'connect', 'send_transaction', 'call_contract', 'signMessage',
956
+ 'sign_transaction', 'broadcast_only',
1079
957
  'send_private_transfer', 'claim_private_transfer',
1080
958
  'encrypt_balance', 'decrypt_balance',
1081
959
  ]);
@@ -1083,8 +961,8 @@
1083
961
  // only forward known event types
1084
962
  ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
1085
963
  'connect', 'disconnect', 'accountChanged', 'balanceChanged',
1086
- 'networkChanged', 'transactionConfirmed', 'error',
1087
- 'extensionLocked', 'extensionUnlocked'
964
+ 'networkChanged', 'transactionConfirmed', 'permissionsChanged', 'message',
965
+ 'error', 'extensionLocked', 'extensionUnlocked'
1088
966
  ]);
1089
967
 
1090
968
  /**
@@ -1167,6 +1045,14 @@
1167
1045
  return null;
1168
1046
  if (typeof raw.rpcUrl !== 'string' || (!raw.rpcUrl && raw.id !== 'custom'))
1169
1047
  return null;
1048
+ if (raw.rpcUrl) {
1049
+ const isHttps = raw.rpcUrl.startsWith('https://');
1050
+ const isLocal = raw.rpcUrl.startsWith('http://localhost') || raw.rpcUrl.startsWith('http://127.0.0.1');
1051
+ const isTestnet = raw.isTestnet === true;
1052
+ // Reject plain-http RPC URLs from untrusted sources unless testnet-flagged or local
1053
+ if (!isHttps && !isLocal && !isTestnet)
1054
+ return null;
1055
+ }
1170
1056
  if (typeof raw.supportsPrivacy !== 'boolean')
1171
1057
  return null;
1172
1058
  return Object.freeze({
@@ -1182,9 +1068,6 @@
1182
1068
  });
1183
1069
  }
1184
1070
 
1185
- /**
1186
- * SDK Configuration
1187
- */
1188
1071
  /**
1189
1072
  * Default balance structure.
1190
1073
  * Accepts a numeric total or undefined — never pass a Balance object here.
@@ -1220,27 +1103,17 @@
1220
1103
  currency: 'OCT'
1221
1104
  };
1222
1105
  }
1223
- /**
1224
- * SDK Configuration constants
1225
- */
1226
1106
  const SDK_CONFIG = {
1227
- version: '2.7.0',
1107
+ version: '2.7.1',
1228
1108
  defaultNetworkId: DEFAULT_NETWORK_ID,
1229
1109
  communicationTimeout: 30000, // 30 seconds
1230
1110
  retryAttempts: 3,
1231
1111
  retryDelay: 1000, // 1 second
1232
1112
  };
1233
- /**
1234
- * Get default network configuration
1235
- */
1236
1113
  function getDefaultNetwork() {
1237
1114
  return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
1238
1115
  }
1239
1116
 
1240
- /**
1241
- * 0xio Wallet SDK - Main Wallet Class
1242
- * Primary interface for DApp developers to interact with 0xio Wallet
1243
- */
1244
1117
  class ZeroXIOWallet extends EventEmitter {
1245
1118
  constructor(config) {
1246
1119
  super(config.debug);
@@ -1256,32 +1129,22 @@
1256
1129
  debug: config.debug || false
1257
1130
  };
1258
1131
  this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
1259
- this.communicator = new ExtensionCommunicator(this.config.debug, [], this.config.adapter);
1132
+ this.communicator = new ExtensionCommunicator(this.config.debug, this.config.trustedParentOrigins ?? [], this.config.adapter);
1260
1133
  this.logger.log('Wallet instance created with config:', this.config);
1261
1134
  }
1262
- // ===================
1263
- // INITIALIZATION
1264
- // ===================
1265
- /**
1266
- * Initialize the SDK
1267
- * Must be called before using any other methods
1268
- */
1269
1135
  async initialize() {
1270
1136
  if (this.isInitialized) {
1271
1137
  return true;
1272
1138
  }
1273
- // single-flight init
1274
1139
  if (this._initPromise) {
1275
1140
  return this._initPromise;
1276
1141
  }
1277
1142
  this._initPromise = (async () => {
1278
1143
  try {
1279
- // Initialize extension communication
1280
1144
  const communicationReady = await this.communicator.initialize();
1281
1145
  if (!communicationReady) {
1282
1146
  throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
1283
1147
  }
1284
- // Register this DApp with the extension
1285
1148
  await this.communicator.sendRequest('register_dapp', {
1286
1149
  appName: this.config.appName,
1287
1150
  appDescription: this.config.appDescription,
@@ -1291,7 +1154,6 @@
1291
1154
  requiredPermissions: this.config.requiredPermissions,
1292
1155
  networkId: this.config.networkId
1293
1156
  });
1294
- // Setup event forwarding from extension
1295
1157
  this.setupExtensionEventListeners();
1296
1158
  this.isInitialized = true;
1297
1159
  this.logger.log('SDK initialized successfully');
@@ -1310,29 +1172,21 @@
1310
1172
  })();
1311
1173
  return this._initPromise;
1312
1174
  }
1313
- /**
1314
- * Check if SDK is initialized
1315
- */
1316
1175
  isReady() {
1317
1176
  return this.isInitialized && this.communicator.isExtensionAvailable();
1318
1177
  }
1319
- // ===================
1320
- // CONNECTION MANAGEMENT
1321
- // ===================
1322
- /**
1323
- * Connect to wallet
1324
- */
1325
1178
  async connect(options = {}) {
1326
1179
  this.ensureInitialized();
1327
1180
  try {
1328
1181
  this.logger.log('Attempting to connect with options:', options);
1329
- // filter to declared perms only
1182
+ // filter to declared perms only — accept both RFC 'permissions' and legacy 'requestPermissions'
1330
1183
  const declaredPermissions = this.config.requiredPermissions || [];
1331
- const requestPermissions = options.requestPermissions
1332
- ? options.requestPermissions.filter(p => declaredPermissions.includes(p))
1184
+ const requestedPerms = options.permissions ?? options.requestPermissions;
1185
+ const requestedPermissions = requestedPerms
1186
+ ? requestedPerms.filter(p => declaredPermissions.includes(p))
1333
1187
  : declaredPermissions;
1334
1188
  const result = await this.communicator.sendRequest('connect', {
1335
- requestPermissions,
1189
+ permissions: requestedPermissions,
1336
1190
  networkId: options.networkId || this.config.networkId
1337
1191
  });
1338
1192
  // verify pubkey→addr binding
@@ -1349,9 +1203,12 @@
1349
1203
  this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
1350
1204
  }
1351
1205
  }
1352
- // Use networkInfo from extension response — validate before caching
1206
+ // Use networkInfo from extension response — validate before caching.
1353
1207
  const networkInfo = validateNetworkInfo(result.networkInfo)
1354
- ?? getNetworkConfig(result.networkId || this.config.networkId);
1208
+ ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1209
+ if (!networkInfo) {
1210
+ throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
1211
+ }
1355
1212
  const permissions = result.permissions || [];
1356
1213
  // Update connection info — including permissions
1357
1214
  this.connectionInfo = {
@@ -1444,7 +1301,11 @@
1444
1301
  // validate untrusted balance/networkInfo before caching
1445
1302
  const balanceInfo = validateBalance(result.balance) ?? createDefaultBalance();
1446
1303
  const networkInfo = validateNetworkInfo(result.networkInfo)
1447
- ?? getNetworkConfig(result.networkId || this.config.networkId);
1304
+ ?? (result.networkId ? getNetworkConfig(result.networkId) : null);
1305
+ if (!networkInfo) {
1306
+ this.logger.warn('getConnectionStatus: wallet returned no network metadata — returning cached state');
1307
+ return this.connectionInfo;
1308
+ }
1448
1309
  const wasConnected = this.connectionInfo.isConnected;
1449
1310
  const permissions = result.permissions || [];
1450
1311
  // preserve existing connectedAt
@@ -1517,18 +1378,9 @@
1517
1378
  getNetworkId() {
1518
1379
  return this.connectionInfo.networkInfo?.id || null;
1519
1380
  }
1520
- // ===================
1521
- // WALLET INFORMATION
1522
- // ===================
1523
- /**
1524
- * Get current wallet address
1525
- */
1526
1381
  getAddress() {
1527
1382
  return this.connectionInfo.address || null;
1528
1383
  }
1529
- /**
1530
- * Get current balance
1531
- */
1532
1384
  async getBalance(forceRefresh = false) {
1533
1385
  this.ensureConnected();
1534
1386
  try {
@@ -1553,7 +1405,6 @@
1553
1405
  // skip if session changed mid-flight
1554
1406
  if (this._sessionVersion !== sv)
1555
1407
  return result;
1556
- // Update cached balance
1557
1408
  if (this.connectionInfo.balance) {
1558
1409
  const previousBalance = this.connectionInfo.balance;
1559
1410
  this.connectionInfo.balance = result;
@@ -1580,15 +1431,11 @@
1580
1431
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
1581
1432
  }
1582
1433
  }
1583
- /**
1584
- * Get network information
1585
- */
1586
1434
  async getNetworkInfo() {
1587
1435
  this.ensureInitialized();
1588
1436
  try {
1589
1437
  const sv = this._sessionVersion;
1590
1438
  const result = await this.communicator.sendRequest('get_network_info');
1591
- // validate network info before caching
1592
1439
  const networkInfo = validateNetworkInfo(result);
1593
1440
  if (!networkInfo) {
1594
1441
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Extension returned invalid network info');
@@ -1596,11 +1443,9 @@
1596
1443
  // skip if session changed mid-flight
1597
1444
  if (this._sessionVersion !== sv)
1598
1445
  return networkInfo;
1599
- // Update cached network info
1600
1446
  if (this.connectionInfo.networkInfo) {
1601
1447
  const previousNetwork = this.connectionInfo.networkInfo;
1602
1448
  this.connectionInfo.networkInfo = networkInfo;
1603
- // Emit network changed event if different
1604
1449
  if (previousNetwork.id !== networkInfo.id) {
1605
1450
  const networkChangedEvent = {
1606
1451
  previousNetwork,
@@ -1620,22 +1465,14 @@
1620
1465
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
1621
1466
  }
1622
1467
  }
1623
- // ===================
1624
- // TRANSACTIONS
1625
- // ===================
1626
- /**
1627
- * Send transaction
1628
- */
1629
1468
  async sendTransaction(txData) {
1630
1469
  this.ensureConnected();
1631
- // validate inputs
1632
1470
  if (!isValidAddress(txData.to)) {
1633
1471
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1634
1472
  }
1635
1473
  if (!isValidAmount(txData.amount)) {
1636
1474
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1637
1475
  }
1638
- // bound memo
1639
1476
  if (txData.message && txData.message.length > 1000) {
1640
1477
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1641
1478
  }
@@ -1644,8 +1481,8 @@
1644
1481
  this.logger.log('Sending transaction:', { to: txData.to });
1645
1482
  const result = await this.communicator.sendRequest('send_transaction', txData);
1646
1483
  this.logger.log('Transaction result:', result);
1647
- // Refresh balance after successful transaction
1648
- if (result.success) {
1484
+ // Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
1485
+ if (result.accepted ?? result.success) {
1649
1486
  setTimeout(() => {
1650
1487
  this.getBalance(true).catch(error => {
1651
1488
  this.logger.warn('Failed to refresh balance after transaction:', error);
@@ -1662,23 +1499,70 @@
1662
1499
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to send transaction', error);
1663
1500
  }
1664
1501
  }
1502
+ /**
1503
+ * Sign a transaction without broadcasting it (RFC-O-1 octra_signTransaction).
1504
+ * Returns the signed transaction object for manual submission via submitTransaction().
1505
+ */
1506
+ async signTransaction(txData) {
1507
+ this.ensureConnected();
1508
+ if (!isValidAddress(txData.to)) {
1509
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1510
+ }
1511
+ if (!isValidAmount(txData.amount)) {
1512
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
1513
+ }
1514
+ if (txData.message && txData.message.length > 1000) {
1515
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
1516
+ }
1517
+ try {
1518
+ this.logger.log('Requesting transaction signature:', { to: txData.to });
1519
+ const result = await this.communicator.sendRequest('sign_transaction', txData);
1520
+ return result;
1521
+ }
1522
+ catch (error) {
1523
+ if (error instanceof ZeroXIOWalletError)
1524
+ throw error;
1525
+ throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Failed to sign transaction', error);
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Broadcast a pre-signed transaction (RFC-O-1 octra_submitTransaction).
1530
+ * Use after signTransaction() to submit the signed tx to the network.
1531
+ */
1532
+ async submitTransaction(signedTx) {
1533
+ this.ensureConnected();
1534
+ if (!signedTx || typeof signedTx !== 'object') {
1535
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'signedTx must be an object');
1536
+ }
1537
+ try {
1538
+ this.logger.log('Submitting pre-signed transaction');
1539
+ const result = await this.communicator.sendRequest('broadcast_only', { signedTx });
1540
+ return result;
1541
+ }
1542
+ catch (error) {
1543
+ if (error instanceof ZeroXIOWalletError)
1544
+ throw error;
1545
+ throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to submit transaction', error);
1546
+ }
1547
+ }
1665
1548
  /**
1666
1549
  * Call a smart contract method (state-changing).
1667
1550
  * The extension builds, signs, and submits the transaction via octra_submit.
1668
1551
  */
1669
1552
  async callContract(callData) {
1670
1553
  this.ensureConnected();
1671
- // validate inputs
1672
1554
  if (!isValidAddress(callData.contract)) {
1673
1555
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1674
1556
  }
1675
1557
  if (!callData.method || typeof callData.method !== 'string') {
1676
1558
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method is required');
1677
1559
  }
1678
- // bound method + params size
1679
1560
  if (callData.method.length > 200) {
1680
1561
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
1681
1562
  }
1563
+ if (callData.amount != null) {
1564
+ this.assertExactOCTAmount(callData.amount, 'Contract call amount');
1565
+ }
1682
1566
  try {
1683
1567
  if (JSON.stringify(callData.params).length > 65536) {
1684
1568
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
@@ -1716,14 +1600,12 @@
1716
1600
  */
1717
1601
  async contractCallView(viewData) {
1718
1602
  this.ensureInitialized();
1719
- // validate inputs
1720
1603
  if (!isValidAddress(viewData.contract)) {
1721
1604
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1722
1605
  }
1723
1606
  if (!viewData.method || typeof viewData.method !== 'string') {
1724
1607
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method is required');
1725
1608
  }
1726
- // bound method + params size
1727
1609
  if (viewData.method.length > 200) {
1728
1610
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Contract method name too long (max 200 characters)');
1729
1611
  }
@@ -1763,7 +1645,6 @@
1763
1645
  */
1764
1646
  async getContractStorage(contract, key) {
1765
1647
  this.ensureInitialized();
1766
- // validate inputs
1767
1648
  if (!isValidAddress(contract)) {
1768
1649
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
1769
1650
  }
@@ -1805,12 +1686,6 @@
1805
1686
  throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get transaction history', error);
1806
1687
  }
1807
1688
  }
1808
- // ===================
1809
- // PRIVATE FEATURES
1810
- // ===================
1811
- /**
1812
- * Get private balance information
1813
- */
1814
1689
  async getPrivateBalanceInfo() {
1815
1690
  this.ensureConnected();
1816
1691
  try {
@@ -1826,7 +1701,7 @@
1826
1701
  */
1827
1702
  async encryptBalance(amount) {
1828
1703
  this.ensureConnected();
1829
- // validate amount
1704
+ this.assertExactOCTAmount(amount, 'Encrypt amount');
1830
1705
  if (!isValidAmount(amount)) {
1831
1706
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1832
1707
  }
@@ -1847,7 +1722,7 @@
1847
1722
  */
1848
1723
  async decryptBalance(amount) {
1849
1724
  this.ensureConnected();
1850
- // validate amount
1725
+ this.assertExactOCTAmount(amount, 'Decrypt amount');
1851
1726
  if (!isValidAmount(amount)) {
1852
1727
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
1853
1728
  }
@@ -1873,21 +1748,21 @@
1873
1748
  */
1874
1749
  async sendPrivateTransfer(transferData) {
1875
1750
  this.ensureConnected();
1876
- // validate inputs
1877
1751
  if (!isValidAddress(transferData.to)) {
1878
1752
  throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
1879
1753
  }
1880
1754
  if (!isValidAmount(transferData.amount)) {
1881
1755
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Invalid transfer amount');
1882
1756
  }
1757
+ this.assertExactOCTAmount(transferData.amount, 'Transfer amount');
1883
1758
  // bound msg size
1884
1759
  if (transferData.message && transferData.message.length > 1000) {
1885
1760
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
1886
1761
  }
1887
1762
  try {
1888
1763
  const result = await this.communicator.sendRequest('send_private_transfer', transferData);
1889
- // Refresh balance after transfer
1890
- if (result.success) {
1764
+ // Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
1765
+ if (result.accepted ?? result.success) {
1891
1766
  setTimeout(() => {
1892
1767
  this.getBalance(true).catch(() => { });
1893
1768
  }, 1000);
@@ -1927,8 +1802,8 @@
1927
1802
  const result = await this.communicator.sendRequest('claim_private_transfer', {
1928
1803
  transferId
1929
1804
  });
1930
- // Refresh balance after claiming
1931
- if (result.success) {
1805
+ // Refresh balance after claiming (accept RFC 'accepted' or legacy 'success')
1806
+ if (result.accepted ?? result.success) {
1932
1807
  setTimeout(() => {
1933
1808
  this.getBalance(true).catch(() => { });
1934
1809
  }, 1000);
@@ -1939,9 +1814,6 @@
1939
1814
  throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
1940
1815
  }
1941
1816
  }
1942
- // ===================
1943
- // MESSAGE SIGNING
1944
- // ===================
1945
1817
  /**
1946
1818
  * Sign an arbitrary message with the wallet's private key
1947
1819
  * The user will be prompted to approve the signature request in the extension
@@ -1979,9 +1851,6 @@
1979
1851
  throw new ZeroXIOWalletError(exports.ErrorCode.SIGNATURE_FAILED, 'Failed to sign message', error);
1980
1852
  }
1981
1853
  }
1982
- // ===================
1983
- // AUTHENTICATION HELPERS
1984
- // ===================
1985
1854
  /**
1986
1855
  * Sign a domain-separated authentication message.
1987
1856
  * Unlike `signMessage()`, this prepends a standard header that binds the signature
@@ -2003,9 +1872,6 @@
2003
1872
  const domainSeparated = `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
2004
1873
  return this.signMessage(domainSeparated);
2005
1874
  }
2006
- // ===================
2007
- // PRIVATE METHODS
2008
- // ===================
2009
1875
  ensureInitialized() {
2010
1876
  if (!this.isInitialized) {
2011
1877
  throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'SDK not initialized. Call initialize() first.');
@@ -2018,7 +1884,6 @@
2018
1884
  }
2019
1885
  }
2020
1886
  setupExtensionEventListeners() {
2021
- // Listen for extension events through the communicator
2022
1887
  this.communicator.on('accountChanged', (event) => {
2023
1888
  this.handleAccountChanged(event.data);
2024
1889
  });
@@ -2037,11 +1902,18 @@
2037
1902
  this.communicator.on('transactionConfirmed', (event) => {
2038
1903
  this.handleTransactionConfirmed(event.data);
2039
1904
  });
1905
+ this.communicator.on('permissionsChanged', (event) => {
1906
+ const permissions = event.data ?? event;
1907
+ if (this.connectionInfo.isConnected) {
1908
+ this.connectionInfo.permissions = Array.isArray(permissions) ? permissions : [];
1909
+ }
1910
+ this.emit('permissionsChanged', permissions);
1911
+ });
1912
+ this.communicator.on('message', (event) => {
1913
+ this.emit('message', event.data ?? event);
1914
+ });
2040
1915
  this.logger.log('Extension event listeners setup complete');
2041
1916
  }
2042
- /**
2043
- * Handle account changed event from extension
2044
- */
2045
1917
  handleAccountChanged(data) {
2046
1918
  ++this._sessionVersion;
2047
1919
  const previousAddress = this.connectionInfo.address;
@@ -2049,7 +1921,6 @@
2049
1921
  // clear stale pubkey on acct change
2050
1922
  this.connectionInfo.publicKey = data.publicKey;
2051
1923
  if (data.balance) {
2052
- // validate balance before caching
2053
1924
  const validated = validateBalance(data.balance);
2054
1925
  if (validated) {
2055
1926
  this.connectionInfo.balance = validated;
@@ -2067,9 +1938,6 @@
2067
1938
  this.emit('accountChanged', accountChangedEvent);
2068
1939
  this.logger.log('Account changed:', { newAddress: accountChangedEvent.newAddress });
2069
1940
  }
2070
- /**
2071
- * Handle network changed event from extension
2072
- */
2073
1941
  handleNetworkChanged(data) {
2074
1942
  const previousNetwork = this.connectionInfo.networkInfo;
2075
1943
  // validate networkInfo — drop invalid
@@ -2088,11 +1956,7 @@
2088
1956
  this.emit('networkChanged', networkChangedEvent);
2089
1957
  this.logger.log('Network changed:', networkChangedEvent);
2090
1958
  }
2091
- /**
2092
- * Handle balance changed event from extension
2093
- */
2094
1959
  handleBalanceChanged(data) {
2095
- // validate balance before caching
2096
1960
  const balance = validateBalance(data.balance);
2097
1961
  if (!balance) {
2098
1962
  this.logger.warn('Received invalid balance in balanceChanged event, ignoring');
@@ -2108,13 +1972,9 @@
2108
1972
  this.emit('balanceChanged', balanceChangedEvent);
2109
1973
  this.logger.log('Balance changed:', { public: balance.public });
2110
1974
  }
2111
- /**
2112
- * Handle extension locked event
2113
- */
2114
1975
  handleExtensionLocked() {
2115
1976
  ++this._sessionVersion;
2116
1977
  this.connectionInfo = { isConnected: false };
2117
- // emit extensionLocked then disconnect
2118
1978
  this.emit('extensionLocked', {});
2119
1979
  const disconnectEvent = {
2120
1980
  reason: 'extension_locked'
@@ -2122,21 +1982,13 @@
2122
1982
  this.emit('disconnect', disconnectEvent);
2123
1983
  this.logger.log('Extension locked - disconnected');
2124
1984
  }
2125
- /**
2126
- * Handle extension unlocked event
2127
- */
2128
1985
  handleExtensionUnlocked() {
2129
- // emit extensionUnlocked
2130
1986
  this.emit('extensionUnlocked', {});
2131
- // Attempt to restore connection
2132
1987
  this.getConnectionStatus().catch(() => {
2133
1988
  this.logger.warn('Could not restore connection after unlock');
2134
1989
  });
2135
1990
  this.logger.log('Extension unlocked');
2136
1991
  }
2137
- /**
2138
- * Handle transaction confirmed event
2139
- */
2140
1992
  handleTransactionConfirmed(data) {
2141
1993
  this.emit('transactionConfirmed', {
2142
1994
  txHash: data.txHash,
@@ -2149,12 +2001,21 @@
2149
2001
  }, 2000);
2150
2002
  this.logger.log('Transaction confirmed:', data.txHash);
2151
2003
  }
2152
- // ===================
2153
- // CLEANUP
2154
- // ===================
2155
2004
  /**
2156
- * Clean up SDK resources
2005
+ * 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.
2007
+ * String amounts bypass this check (caller is responsible for correctness).
2157
2008
  */
2009
+ assertExactOCTAmount(amount, label) {
2010
+ if (typeof amount === 'number') {
2011
+ const micro = Math.round(amount * 1000000);
2012
+ if (Math.abs(amount - micro / 1000000) > 1e-10) {
2013
+ const suggested = (micro / 1000000).toFixed(6);
2014
+ throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, `${label} cannot be represented exactly in micro-OCT. ` +
2015
+ `Pass a string instead (e.g. "${suggested}").`);
2016
+ }
2017
+ }
2018
+ }
2158
2019
  cleanup() {
2159
2020
  this.communicator.cleanup();
2160
2021
  this.removeAllListeners();
@@ -2191,7 +2052,9 @@
2191
2052
  switch_network: 'octra_switchNetwork',
2192
2053
  signMessage: 'octra_signMessage',
2193
2054
  send_transaction: 'octra_sendTransaction',
2194
- call_contract: 'octra_callContract',
2055
+ sign_transaction: 'octra_signTransaction',
2056
+ broadcast_only: 'octra_submitTransaction',
2057
+ call_contract: 'octra_sendContractTransaction',
2195
2058
  contract_call_view: 'octra_callContract',
2196
2059
  get_private_balance_info: 'octra_getEncryptedBalance',
2197
2060
  encrypt_balance: 'octra_encryptBalance',
@@ -2203,7 +2066,7 @@
2203
2066
  const RFC_TO_SDK_ERROR = {
2204
2067
  4001: 'USER_REJECTED',
2205
2068
  4100: 'PERMISSION_DENIED',
2206
- 4200: 'NETWORK_ERROR',
2069
+ 4200: 'UNKNOWN_ERROR',
2207
2070
  4900: 'CONNECTION_REFUSED',
2208
2071
  4901: 'NETWORK_ERROR',
2209
2072
  };
@@ -2220,7 +2083,7 @@
2220
2083
  * three RFC-O-1 calls: octra_requestAccounts, octra_networkInfo, octra_permissions.
2221
2084
  */
2222
2085
  async function rfcConnect(provider, params) {
2223
- const requestPerms = params?.requestPermissions ?? [];
2086
+ const requestPerms = params?.permissions ?? params?.requestPermissions ?? [];
2224
2087
  const accounts = (await provider.request({
2225
2088
  method: 'octra_requestAccounts',
2226
2089
  params: [{ permissions: requestPerms }],
@@ -2308,12 +2171,14 @@
2308
2171
  const onNetworkChanged = (data) => handler({ eventType: 'networkChanged', eventData: { networkInfo: data } });
2309
2172
  const onBalanceChanged = (data) => handler({ eventType: 'balanceChanged', eventData: data });
2310
2173
  const onTransactionChanged = (data) => handler({ eventType: 'transactionConfirmed', eventData: data });
2174
+ const onPermissionsChanged = (data) => handler({ eventType: 'permissionsChanged', eventData: data });
2311
2175
  provider.on('connect', onConnect);
2312
2176
  provider.on('disconnect', onDisconnect);
2313
2177
  provider.on('accountsChanged', onAccountsChanged);
2314
2178
  provider.on('networkChanged', onNetworkChanged);
2315
2179
  provider.on('balanceChanged', onBalanceChanged);
2316
2180
  provider.on('transactionChanged', onTransactionChanged);
2181
+ provider.on('permissionsChanged', onPermissionsChanged);
2317
2182
  const cleanup = () => {
2318
2183
  provider.removeListener('connect', onConnect);
2319
2184
  provider.removeListener('disconnect', onDisconnect);
@@ -2321,6 +2186,7 @@
2321
2186
  provider.removeListener('networkChanged', onNetworkChanged);
2322
2187
  provider.removeListener('balanceChanged', onBalanceChanged);
2323
2188
  provider.removeListener('transactionChanged', onTransactionChanged);
2189
+ provider.removeListener('permissionsChanged', onPermissionsChanged);
2324
2190
  _handler = null;
2325
2191
  };
2326
2192
  return cleanup;
@@ -2328,7 +2194,11 @@
2328
2194
  listenForReady(onReady) {
2329
2195
  const handler = () => onReady();
2330
2196
  window.addEventListener('octraWalletReady', handler);
2331
- return () => window.removeEventListener('octraWalletReady', handler);
2197
+ window.addEventListener('octra#initialized', handler);
2198
+ return () => {
2199
+ window.removeEventListener('octraWalletReady', handler);
2200
+ window.removeEventListener('octra#initialized', handler);
2201
+ };
2332
2202
  },
2333
2203
  };
2334
2204
  }
@@ -2391,7 +2261,7 @@
2391
2261
  */
2392
2262
  // Main exports
2393
2263
  // Version information
2394
- const SDK_VERSION = '2.7.0';
2264
+ const SDK_VERSION = '2.7.1';
2395
2265
  const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
2396
2266
  const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
2397
2267
  const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
@@ -2409,8 +2279,7 @@
2409
2279
  try {
2410
2280
  await wallet$1.connect();
2411
2281
  }
2412
- catch (error) {
2413
- if (config.debug) ;
2282
+ catch {
2414
2283
  // Don't throw - let the app handle connection manually
2415
2284
  }
2416
2285
  }