@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/CHANGELOG.md +25 -0
- package/README.md +33 -2
- package/dist/index.d.ts +40 -157
- package/dist/index.esm.js +134 -265
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +134 -265
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +134 -265
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|
|
56
|
-
|
|
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
|
|
63
|
-
//
|
|
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
|
|
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', '
|
|
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.
|
|
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
|
|
1326
|
-
|
|
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
|
-
|
|
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
|
-
??
|
|
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
|
-
??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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.
|
|
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
|
|
2407
|
-
if (config.debug) ;
|
|
2276
|
+
catch {
|
|
2408
2277
|
// Don't throw - let the app handle connection manually
|
|
2409
2278
|
}
|
|
2410
2279
|
}
|