@0xio/sdk 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -33
- package/README.md +105 -250
- package/dist/index.d.ts +279 -188
- package/dist/index.esm.js +524 -325
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +533 -324
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +533 -324
- 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
|
}
|
|
@@ -128,6 +92,16 @@ var ErrorCode;
|
|
|
128
92
|
ErrorCode["DUPLICATE_TRANSACTION"] = "DUPLICATE_TRANSACTION";
|
|
129
93
|
ErrorCode["NONCE_TOO_FAR"] = "NONCE_TOO_FAR";
|
|
130
94
|
ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
95
|
+
// Codes the 0xio wallet returns over the bridge
|
|
96
|
+
ErrorCode["NOT_CONNECTED"] = "NOT_CONNECTED";
|
|
97
|
+
ErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS";
|
|
98
|
+
ErrorCode["METHOD_NOT_ALLOWED"] = "METHOD_NOT_ALLOWED";
|
|
99
|
+
ErrorCode["NOT_AVAILABLE"] = "NOT_AVAILABLE";
|
|
100
|
+
ErrorCode["PRIVATE_PROOF_FAILED"] = "PRIVATE_PROOF_FAILED";
|
|
101
|
+
ErrorCode["PRIVATE_TRANSFER_FAILED"] = "PRIVATE_TRANSFER_FAILED";
|
|
102
|
+
ErrorCode["CONTRACT_CALL_FAILED"] = "CONTRACT_CALL_FAILED";
|
|
103
|
+
ErrorCode["SIGN_FAILED"] = "SIGN_FAILED";
|
|
104
|
+
ErrorCode["RECIPIENT_NOT_REGISTERED"] = "RECIPIENT_NOT_REGISTERED";
|
|
131
105
|
})(ErrorCode || (ErrorCode = {}));
|
|
132
106
|
class ZeroXIOWalletError extends Error {
|
|
133
107
|
constructor(code, message, details) {
|
|
@@ -150,7 +124,7 @@ class ZeroXIOWalletError extends Error {
|
|
|
150
124
|
* window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
|
|
151
125
|
* window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
|
|
152
126
|
*
|
|
153
|
-
*
|
|
127
|
+
* Session nonce validation: injected.ts broadcasts the nonce received from the
|
|
154
128
|
* isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
|
|
155
129
|
* mismatched nonce is rejected, preventing response injection by malicious page scripts.
|
|
156
130
|
*/
|
|
@@ -183,7 +157,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
|
183
157
|
window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
|
|
184
158
|
}
|
|
185
159
|
catch {
|
|
186
|
-
// Do not fall back to '*'
|
|
160
|
+
// Do not fall back to '*': silent failure is safer
|
|
187
161
|
}
|
|
188
162
|
},
|
|
189
163
|
listen(handler, options) {
|
|
@@ -198,7 +172,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
|
198
172
|
...(options?.trustedParentOrigins ?? []),
|
|
199
173
|
]);
|
|
200
174
|
let _sessionNonce = null;
|
|
201
|
-
//
|
|
175
|
+
// receive the session nonce from injected.ts (MAIN world content script)
|
|
202
176
|
const nonceListener = (e) => {
|
|
203
177
|
if (e.origin !== allowedOrigin)
|
|
204
178
|
return;
|
|
@@ -221,7 +195,7 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
|
221
195
|
return;
|
|
222
196
|
if (!e.data || e.data.source !== '0xio-sdk-bridge')
|
|
223
197
|
return;
|
|
224
|
-
//
|
|
198
|
+
// session nonce validation.
|
|
225
199
|
// Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
|
|
226
200
|
// Fallback path: if the init broadcast was missed (race between document_start
|
|
227
201
|
// content script and page script load), capture nonce from the first same-origin
|
|
@@ -267,16 +241,6 @@ function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
|
267
241
|
/** Default 0xio adapter instance (no extra trusted origins). */
|
|
268
242
|
const ZeroXIOAdapter = createZeroXIOAdapter();
|
|
269
243
|
|
|
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
244
|
function isValidAddress(address) {
|
|
281
245
|
if (!address || typeof address !== 'string') {
|
|
282
246
|
return false;
|
|
@@ -300,11 +264,8 @@ function isValidAmount(amount) {
|
|
|
300
264
|
Number.isFinite(amount) &&
|
|
301
265
|
amount <= Number.MAX_SAFE_INTEGER;
|
|
302
266
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Validate transaction message
|
|
305
|
-
*/
|
|
306
267
|
function isValidMessage(message) {
|
|
307
|
-
// Type check first
|
|
268
|
+
// Type check first: falsy non-strings (0, false, null) are not valid messages
|
|
308
269
|
if (typeof message !== 'string') {
|
|
309
270
|
return message === undefined || message === null ? true : false;
|
|
310
271
|
}
|
|
@@ -312,18 +273,12 @@ function isValidMessage(message) {
|
|
|
312
273
|
if (message.length === 0) {
|
|
313
274
|
return true;
|
|
314
275
|
}
|
|
315
|
-
// 100KB limit
|
|
276
|
+
// 100KB limit: contract call params can be large (serialized JSON)
|
|
316
277
|
return message.length <= 100000;
|
|
317
278
|
}
|
|
318
|
-
/**
|
|
319
|
-
* Validate fee level
|
|
320
|
-
*/
|
|
321
279
|
function isValidFeeLevel(feeLevel) {
|
|
322
280
|
return feeLevel === 1 || feeLevel === 3;
|
|
323
281
|
}
|
|
324
|
-
// ===================
|
|
325
|
-
// ADDRESS DERIVATION
|
|
326
|
-
// ===================
|
|
327
282
|
const _B58_ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
328
283
|
function _base58Encode(buf) {
|
|
329
284
|
let zeros = 0;
|
|
@@ -351,7 +306,7 @@ function _base58Encode(buf) {
|
|
|
351
306
|
}
|
|
352
307
|
/**
|
|
353
308
|
* Derive the canonical Octra address from a base64-encoded Ed25519 public key.
|
|
354
|
-
* Algorithm: SHA-256
|
|
309
|
+
* Algorithm: SHA-256 of the pubkey bytes, base58-encoded, with "oct" prepended
|
|
355
310
|
* Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
|
|
356
311
|
*/
|
|
357
312
|
async function deriveOctraAddress(publicKeyBase64) {
|
|
@@ -368,12 +323,6 @@ async function deriveOctraAddress(publicKeyBase64) {
|
|
|
368
323
|
const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
|
|
369
324
|
return 'oct' + _base58Encode(new Uint8Array(hashBuf));
|
|
370
325
|
}
|
|
371
|
-
// ===================
|
|
372
|
-
// FORMATTING UTILITIES
|
|
373
|
-
// ===================
|
|
374
|
-
/**
|
|
375
|
-
* Format OCT amount for display
|
|
376
|
-
*/
|
|
377
326
|
function formatOCT(amount, decimals = 6) {
|
|
378
327
|
const n = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
379
328
|
if (!isValidAmount(n)) {
|
|
@@ -384,9 +333,6 @@ function formatOCT(amount, decimals = 6) {
|
|
|
384
333
|
maximumFractionDigits: decimals
|
|
385
334
|
});
|
|
386
335
|
}
|
|
387
|
-
/**
|
|
388
|
-
* Format address for display (truncated)
|
|
389
|
-
*/
|
|
390
336
|
function formatAddress(address, prefixLength = 6, suffixLength = 4) {
|
|
391
337
|
if (!isValidAddress(address)) {
|
|
392
338
|
return 'Invalid Address';
|
|
@@ -396,16 +342,10 @@ function formatAddress(address, prefixLength = 6, suffixLength = 4) {
|
|
|
396
342
|
}
|
|
397
343
|
return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}`;
|
|
398
344
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Format timestamp for display
|
|
401
|
-
*/
|
|
402
345
|
function formatTimestamp(timestamp) {
|
|
403
346
|
const date = new Date(timestamp);
|
|
404
347
|
return date.toLocaleString();
|
|
405
348
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Format transaction hash for display
|
|
408
|
-
*/
|
|
409
349
|
function formatTxHash(hash, length = 12) {
|
|
410
350
|
if (!hash || typeof hash !== 'string') {
|
|
411
351
|
return 'Invalid Hash';
|
|
@@ -417,12 +357,6 @@ function formatTxHash(hash, length = 12) {
|
|
|
417
357
|
const suffixLength = Math.floor(length / 2);
|
|
418
358
|
return `${hash.slice(0, prefixLength)}...${hash.slice(-suffixLength)}`;
|
|
419
359
|
}
|
|
420
|
-
// ===================
|
|
421
|
-
// CONVERSION UTILITIES
|
|
422
|
-
// ===================
|
|
423
|
-
/**
|
|
424
|
-
* Convert OCT to micro OCT (for network transmission)
|
|
425
|
-
*/
|
|
426
360
|
function toMicroOCT(amount) {
|
|
427
361
|
if (!isValidAmount(amount)) {
|
|
428
362
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Invalid amount for conversion');
|
|
@@ -432,8 +366,21 @@ function toMicroOCT(amount) {
|
|
|
432
366
|
return microOCT.toString();
|
|
433
367
|
}
|
|
434
368
|
/**
|
|
435
|
-
*
|
|
369
|
+
* Exact OCT to raw micro-OCT conversion using string arithmetic (no float rounding).
|
|
370
|
+
* Accepts up to 6 decimals; anything else is rejected.
|
|
436
371
|
*/
|
|
372
|
+
function octToMicro(amount) {
|
|
373
|
+
const text = typeof amount === 'number' ? amount.toFixed(6) : String(amount).trim();
|
|
374
|
+
if (!/^\d+(\.\d{1,6})?$/.test(text)) {
|
|
375
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Amount must be a positive decimal with at most 6 places');
|
|
376
|
+
}
|
|
377
|
+
const [whole, frac = ''] = text.split('.');
|
|
378
|
+
const micro = BigInt(whole) * BigInt(1000000) + BigInt(frac.padEnd(6, '0'));
|
|
379
|
+
if (micro <= BigInt(0)) {
|
|
380
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Amount must be greater than zero');
|
|
381
|
+
}
|
|
382
|
+
return micro.toString();
|
|
383
|
+
}
|
|
437
384
|
function fromMicroOCT(microAmount) {
|
|
438
385
|
const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
|
|
439
386
|
if (!Number.isFinite(amount) || amount < 0) {
|
|
@@ -441,12 +388,6 @@ function fromMicroOCT(microAmount) {
|
|
|
441
388
|
}
|
|
442
389
|
return amount / 1000000;
|
|
443
390
|
}
|
|
444
|
-
// ===================
|
|
445
|
-
// ERROR UTILITIES
|
|
446
|
-
// ===================
|
|
447
|
-
/**
|
|
448
|
-
* Create standardized error messages
|
|
449
|
-
*/
|
|
450
391
|
function createErrorMessage(code, context) {
|
|
451
392
|
const baseMessages = {
|
|
452
393
|
[ErrorCode.EXTENSION_NOT_FOUND]: '0xio Wallet extension is not installed or enabled',
|
|
@@ -468,29 +409,26 @@ function createErrorMessage(code, context) {
|
|
|
468
409
|
[ErrorCode.INVALID_SIGNATURE]: 'Invalid transaction signature',
|
|
469
410
|
[ErrorCode.DUPLICATE_TRANSACTION]: 'Duplicate transaction detected',
|
|
470
411
|
[ErrorCode.NONCE_TOO_FAR]: 'Transaction nonce is too far ahead',
|
|
471
|
-
[ErrorCode.INTERNAL_ERROR]: 'Internal server error'
|
|
412
|
+
[ErrorCode.INTERNAL_ERROR]: 'Internal server error',
|
|
413
|
+
[ErrorCode.NOT_CONNECTED]: 'Connect the wallet before this request',
|
|
414
|
+
[ErrorCode.INVALID_PARAMS]: 'Invalid request parameters',
|
|
415
|
+
[ErrorCode.METHOD_NOT_ALLOWED]: 'This method is not permitted through the wallet',
|
|
416
|
+
[ErrorCode.NOT_AVAILABLE]: 'Not available through the wallet bridge',
|
|
417
|
+
[ErrorCode.PRIVATE_PROOF_FAILED]: 'Private proof generation failed',
|
|
418
|
+
[ErrorCode.PRIVATE_TRANSFER_FAILED]: 'Private transfer failed',
|
|
419
|
+
[ErrorCode.CONTRACT_CALL_FAILED]: 'Contract call failed',
|
|
420
|
+
[ErrorCode.SIGN_FAILED]: 'Signing failed',
|
|
421
|
+
[ErrorCode.RECIPIENT_NOT_REGISTERED]: 'Recipient has no private view key registered'
|
|
472
422
|
};
|
|
473
423
|
const baseMessage = baseMessages[code] || 'Unknown error';
|
|
474
424
|
return context ? `${baseMessage}: ${context}` : baseMessage;
|
|
475
425
|
}
|
|
476
|
-
/**
|
|
477
|
-
* Check if error is a specific type
|
|
478
|
-
*/
|
|
479
426
|
function isErrorType(error, code) {
|
|
480
427
|
return error instanceof ZeroXIOWalletError && error.code === code;
|
|
481
428
|
}
|
|
482
|
-
// ===================
|
|
483
|
-
// ASYNC UTILITIES
|
|
484
|
-
// ===================
|
|
485
|
-
/**
|
|
486
|
-
* Create a promise that resolves after a delay
|
|
487
|
-
*/
|
|
488
429
|
function delay(ms) {
|
|
489
430
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
490
431
|
}
|
|
491
|
-
/**
|
|
492
|
-
* Retry an async operation with exponential backoff
|
|
493
|
-
*/
|
|
494
432
|
async function retry(operation, maxRetries = 3, baseDelay = 1000) {
|
|
495
433
|
let lastError;
|
|
496
434
|
// maxRetries = number of retries AFTER the first attempt
|
|
@@ -501,7 +439,7 @@ async function retry(operation, maxRetries = 3, baseDelay = 1000) {
|
|
|
501
439
|
}
|
|
502
440
|
catch (error) {
|
|
503
441
|
lastError = error;
|
|
504
|
-
// Never retry user rejections
|
|
442
|
+
// Never retry user rejections, they are intentional
|
|
505
443
|
const msg = lastError.message?.toLowerCase() || '';
|
|
506
444
|
if (msg.includes('rejected') || msg.includes('denied') || msg.includes('cancelled') || msg.includes('user refused')) {
|
|
507
445
|
throw lastError;
|
|
@@ -530,18 +468,9 @@ function withTimeout(promise, timeoutMs, timeoutMessage = 'Operation timed out')
|
|
|
530
468
|
clearTimeout(timer);
|
|
531
469
|
});
|
|
532
470
|
}
|
|
533
|
-
// ===================
|
|
534
|
-
// BROWSER UTILITIES
|
|
535
|
-
// ===================
|
|
536
|
-
/**
|
|
537
|
-
* Check if running in browser environment
|
|
538
|
-
*/
|
|
539
471
|
function isBrowser() {
|
|
540
472
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
541
473
|
}
|
|
542
|
-
/**
|
|
543
|
-
* Check if browser supports required features
|
|
544
|
-
*/
|
|
545
474
|
function checkBrowserSupport() {
|
|
546
475
|
const missingFeatures = [];
|
|
547
476
|
if (!isBrowser()) {
|
|
@@ -563,12 +492,6 @@ function checkBrowserSupport() {
|
|
|
563
492
|
missingFeatures
|
|
564
493
|
};
|
|
565
494
|
}
|
|
566
|
-
// ===================
|
|
567
|
-
// DEVELOPMENT UTILITIES
|
|
568
|
-
// ===================
|
|
569
|
-
/**
|
|
570
|
-
* Generate mock data for development/testing
|
|
571
|
-
*/
|
|
572
495
|
function generateMockData() {
|
|
573
496
|
return {
|
|
574
497
|
address: 'oct' + Math.random().toString(36).substring(2, 22) + Math.random().toString(36).substring(2, 26),
|
|
@@ -591,9 +514,6 @@ function generateMockData() {
|
|
|
591
514
|
}
|
|
592
515
|
};
|
|
593
516
|
}
|
|
594
|
-
/**
|
|
595
|
-
* Create development logger
|
|
596
|
-
*/
|
|
597
517
|
function createLogger(prefix, debug) {
|
|
598
518
|
const isDevelopment = typeof window !== 'undefined' && ((typeof globalThis !== 'undefined' && globalThis.process?.env?.NODE_ENV === 'development') ||
|
|
599
519
|
window.location.hostname === 'localhost' ||
|
|
@@ -646,17 +566,6 @@ function createLogger(prefix, debug) {
|
|
|
646
566
|
};
|
|
647
567
|
}
|
|
648
568
|
|
|
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
569
|
class ExtensionCommunicator extends EventEmitter {
|
|
661
570
|
constructor(debug = false, trustedOrigins = [], adapter) {
|
|
662
571
|
super(debug);
|
|
@@ -672,14 +581,14 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
672
581
|
this._adapterReadyTeardown = null;
|
|
673
582
|
/**
|
|
674
583
|
* Set when a trusted walletReady has been received from window.parent.
|
|
675
|
-
* The polling fallback must
|
|
584
|
+
* The polling fallback must not clear this flag.
|
|
676
585
|
*/
|
|
677
586
|
this._parentTrusted = false;
|
|
678
587
|
/** walletReady postMessage listener stored for cleanup */
|
|
679
588
|
this._walletReadyMessageListener = null;
|
|
680
589
|
/**
|
|
681
590
|
* In-flight interactive request lock.
|
|
682
|
-
* Methods that open approval popups are serialized
|
|
591
|
+
* Methods that open approval popups are serialized: only one at a time.
|
|
683
592
|
*/
|
|
684
593
|
this._interactiveInFlight = false;
|
|
685
594
|
this.MAX_CONCURRENT_REQUESTS = 50;
|
|
@@ -720,9 +629,15 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
720
629
|
return this.isExtensionAvailableState && this.hasExtensionContext();
|
|
721
630
|
}
|
|
722
631
|
async sendRequest(method, params = {}, timeout = 30000) {
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
632
|
+
// No-retry + long timeout for both popup/broadcast methods and long compute primitives.
|
|
633
|
+
const longOrNoRetry = ExtensionCommunicator.NO_RETRY_METHODS.has(method) ||
|
|
634
|
+
ExtensionCommunicator.LONG_COMPUTE_METHODS.has(method);
|
|
635
|
+
const maxRetries = longOrNoRetry ? 0 : 1;
|
|
636
|
+
const effectiveTimeout = ExtensionCommunicator.PROOF_METHODS.has(method)
|
|
637
|
+
? Math.max(timeout, 600000)
|
|
638
|
+
: longOrNoRetry
|
|
639
|
+
? Math.max(timeout, 180000)
|
|
640
|
+
: timeout;
|
|
726
641
|
return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
|
|
727
642
|
}
|
|
728
643
|
async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
|
|
@@ -791,7 +706,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
791
706
|
return;
|
|
792
707
|
this._adapterTeardown = this.adapter.listen((msg) => {
|
|
793
708
|
if (msg.requestId !== undefined) {
|
|
794
|
-
// response
|
|
709
|
+
// response: map AdapterIncomingMessage to the ExtensionResponse shape
|
|
795
710
|
if (this.pendingRequests.has(msg.requestId)) {
|
|
796
711
|
this.handleExtensionResponse({
|
|
797
712
|
id: msg.requestId,
|
|
@@ -825,7 +740,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
825
740
|
}
|
|
826
741
|
clearTimeout(pending.timeout);
|
|
827
742
|
this.pendingRequests.delete(response.id);
|
|
828
|
-
// Require strict boolean true
|
|
743
|
+
// Require strict boolean true: a "false" string or other truthy values are failures
|
|
829
744
|
if (response.success === true) {
|
|
830
745
|
pending.resolve(response.data);
|
|
831
746
|
}
|
|
@@ -850,7 +765,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
850
765
|
}
|
|
851
766
|
postMessageToExtension(request) {
|
|
852
767
|
this.adapter.postRequest(request);
|
|
853
|
-
// Parent bridge (iframe/desktop mode)
|
|
768
|
+
// Parent bridge (iframe/desktop mode), only when a trusted origin is established.
|
|
854
769
|
// Sending with '*' would leak method + params to any intercepting frame.
|
|
855
770
|
if (window.parent !== window && this._parentOrigin) {
|
|
856
771
|
if (this.adapter.postRequestToParent) {
|
|
@@ -867,7 +782,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
867
782
|
if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
|
|
868
783
|
throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
|
|
869
784
|
}
|
|
870
|
-
// Trim expired timestamps
|
|
785
|
+
// Trim expired timestamps and cap the array size to prevent unbounded growth in idle tabs
|
|
871
786
|
this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
|
|
872
787
|
if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
|
|
873
788
|
this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
|
|
@@ -887,7 +802,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
887
802
|
const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
888
803
|
return `0xio-sdk-${hex}`;
|
|
889
804
|
}
|
|
890
|
-
// Crypto API unavailable
|
|
805
|
+
// Crypto API unavailable: throw rather than produce a guessable ID that
|
|
891
806
|
// could allow response spoofing via a known requestId.
|
|
892
807
|
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
|
|
893
808
|
}
|
|
@@ -901,15 +816,18 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
901
816
|
this.isExtensionAvailableState = true;
|
|
902
817
|
});
|
|
903
818
|
}
|
|
904
|
-
// walletReady via postMessage (desktop/mobile iframe bridge)
|
|
819
|
+
// walletReady via postMessage (desktop/mobile iframe bridge), store the ref for cleanup
|
|
905
820
|
this._walletReadyMessageListener = (event) => {
|
|
906
821
|
if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
|
|
907
822
|
return;
|
|
908
823
|
}
|
|
909
824
|
const isSameOrigin = event.origin === window.location.origin;
|
|
910
|
-
const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
|
|
911
825
|
const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
|
|
912
|
-
const
|
|
826
|
+
const hasExplicitList = this.trustedOrigins.length > 0;
|
|
827
|
+
// When trustedParentOrigins is explicitly set, implicit localhost trust is disabled
|
|
828
|
+
const isLocalhostAllowed = !hasExplicitList &&
|
|
829
|
+
(event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:'));
|
|
830
|
+
const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhostAllowed;
|
|
913
831
|
// In iframe mode, only trust the actual parent window
|
|
914
832
|
const inIframe = window.parent !== window;
|
|
915
833
|
if (inIframe && event.source !== window.parent) {
|
|
@@ -933,7 +851,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
933
851
|
};
|
|
934
852
|
window.addEventListener('message', this._walletReadyMessageListener);
|
|
935
853
|
if (window.parent !== window) {
|
|
936
|
-
this.logger.log('Running inside a frame
|
|
854
|
+
this.logger.log('Running inside a frame, waiting for the trusted walletReady signal');
|
|
937
855
|
}
|
|
938
856
|
this.checkExtensionAvailability();
|
|
939
857
|
this.extensionDetectionInterval = setInterval(() => {
|
|
@@ -942,7 +860,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
942
860
|
}
|
|
943
861
|
checkExtensionAvailability() {
|
|
944
862
|
// If parent-bridge readiness was established via a trusted walletReady handshake,
|
|
945
|
-
// preserve that state
|
|
863
|
+
// preserve that state: the polling fallback (detectExtensionSignals) does not
|
|
946
864
|
// consider the iframe parent signal and would incorrectly flip state back
|
|
947
865
|
if (this._parentTrusted) {
|
|
948
866
|
return;
|
|
@@ -1024,7 +942,7 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1024
942
|
}
|
|
1025
943
|
/**
|
|
1026
944
|
* Clean up SDK resources.
|
|
1027
|
-
* After cleanup() the instance is terminal
|
|
945
|
+
* After cleanup() the instance is terminal: do not call initialize() again.
|
|
1028
946
|
* Construct a new instance instead.
|
|
1029
947
|
*/
|
|
1030
948
|
cleanup() {
|
|
@@ -1065,20 +983,38 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1065
983
|
};
|
|
1066
984
|
}
|
|
1067
985
|
}
|
|
1068
|
-
// Methods that trigger user-facing popups
|
|
986
|
+
// Methods that trigger user-facing popups: never retry these.
|
|
1069
987
|
// Retrying sends a second request while the first popup is still open,
|
|
1070
988
|
// causing double popups where the second tx fails (stale nonce/state).
|
|
1071
989
|
ExtensionCommunicator.NO_RETRY_METHODS = new Set([
|
|
1072
990
|
'connect', 'send_transaction', 'call_contract', 'signMessage',
|
|
991
|
+
'sign_transaction', 'broadcast_only',
|
|
1073
992
|
'send_private_transfer', 'claim_private_transfer',
|
|
1074
993
|
'encrypt_balance', 'decrypt_balance',
|
|
994
|
+
// Broadcasts a sequence of contract txs, so it must never be retried (it would double-send).
|
|
995
|
+
'send_contract_transaction_sequence',
|
|
996
|
+
// Shows an approval popup + broadcasts a registration tx (offscreen cold-init can exceed
|
|
997
|
+
// 30s): interactive, long timeout, no retry.
|
|
998
|
+
'register_private_view_key',
|
|
999
|
+
]);
|
|
1000
|
+
// Long-running compute primitives (RFP): proof generation / decrypt take 10-120s, so
|
|
1001
|
+
// they need the long (180s) timeout and must not be retried (a retry wastes ~a minute of
|
|
1002
|
+
// compute). They do not show approval popups, so, unlike NO_RETRY_METHODS, they are not
|
|
1003
|
+
// subject to the one-at-a-time interactive lock (a dapp may run several concurrently).
|
|
1004
|
+
ExtensionCommunicator.LONG_COMPUTE_METHODS = new Set([
|
|
1005
|
+
'make_zero_proof', 'make_range_proof', 'decrypt_value',
|
|
1006
|
+
'get_private_balance', 'encrypt_value',
|
|
1075
1007
|
]);
|
|
1008
|
+
// A private transfer runs proof generation after the approval (minutes on a slow machine),
|
|
1009
|
+
// so its wait is longer than the popup window alone.
|
|
1010
|
+
ExtensionCommunicator.PROOF_METHODS = new Set(['send_private_transfer']);
|
|
1011
|
+
// The interactive lock applies only to methods that open a wallet popup / broadcast.
|
|
1076
1012
|
ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
|
|
1077
1013
|
// only forward known event types
|
|
1078
1014
|
ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
|
|
1079
1015
|
'connect', 'disconnect', 'accountChanged', 'balanceChanged',
|
|
1080
|
-
'networkChanged', 'transactionConfirmed', '
|
|
1081
|
-
'extensionLocked', 'extensionUnlocked'
|
|
1016
|
+
'networkChanged', 'transactionConfirmed', 'transactionFailed', 'permissionsChanged', 'message',
|
|
1017
|
+
'error', 'extensionLocked', 'extensionUnlocked'
|
|
1082
1018
|
]);
|
|
1083
1019
|
|
|
1084
1020
|
/**
|
|
@@ -1099,7 +1035,7 @@ const _NETWORKS = {
|
|
|
1099
1035
|
'devnet': {
|
|
1100
1036
|
id: 'devnet',
|
|
1101
1037
|
name: 'Octra Devnet',
|
|
1102
|
-
rpcUrl: '
|
|
1038
|
+
rpcUrl: 'https://devnet.octrascan.io',
|
|
1103
1039
|
explorerUrl: 'https://devnet.octrascan.io/tx.html?hash=',
|
|
1104
1040
|
explorerAddressUrl: 'https://devnet.octrascan.io/address.html?addr=',
|
|
1105
1041
|
indexerUrl: 'https://devnet.octrascan.io',
|
|
@@ -1127,7 +1063,7 @@ const NETWORKS = Object.freeze(Object.fromEntries(Object.entries(_NETWORKS).map(
|
|
|
1127
1063
|
const DEFAULT_NETWORK_ID = 'mainnet';
|
|
1128
1064
|
/**
|
|
1129
1065
|
* Get network configuration by ID.
|
|
1130
|
-
* Returns a frozen copy
|
|
1066
|
+
* Returns a frozen copy, so callers cannot mutate SDK-internal state.
|
|
1131
1067
|
*/
|
|
1132
1068
|
function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
1133
1069
|
if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
|
|
@@ -1137,7 +1073,7 @@ function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
|
1137
1073
|
}
|
|
1138
1074
|
/**
|
|
1139
1075
|
* Get all available networks.
|
|
1140
|
-
* Returns frozen copies
|
|
1076
|
+
* Returns frozen copies, so callers cannot mutate SDK-internal state.
|
|
1141
1077
|
*/
|
|
1142
1078
|
function getAllNetworks() {
|
|
1143
1079
|
return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
|
|
@@ -1161,6 +1097,14 @@ function validateNetworkInfo(raw) {
|
|
|
1161
1097
|
return null;
|
|
1162
1098
|
if (typeof raw.rpcUrl !== 'string' || (!raw.rpcUrl && raw.id !== 'custom'))
|
|
1163
1099
|
return null;
|
|
1100
|
+
if (raw.rpcUrl) {
|
|
1101
|
+
const isHttps = raw.rpcUrl.startsWith('https://');
|
|
1102
|
+
const isLocal = raw.rpcUrl.startsWith('http://localhost') || raw.rpcUrl.startsWith('http://127.0.0.1');
|
|
1103
|
+
const isTestnet = raw.isTestnet === true;
|
|
1104
|
+
// Reject plain-http RPC URLs from untrusted sources unless testnet-flagged or local
|
|
1105
|
+
if (!isHttps && !isLocal && !isTestnet)
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1164
1108
|
if (typeof raw.supportsPrivacy !== 'boolean')
|
|
1165
1109
|
return null;
|
|
1166
1110
|
return Object.freeze({
|
|
@@ -1176,12 +1120,9 @@ function validateNetworkInfo(raw) {
|
|
|
1176
1120
|
});
|
|
1177
1121
|
}
|
|
1178
1122
|
|
|
1179
|
-
/**
|
|
1180
|
-
* SDK Configuration
|
|
1181
|
-
*/
|
|
1182
1123
|
/**
|
|
1183
1124
|
* Default balance structure.
|
|
1184
|
-
* Accepts a numeric total or undefined
|
|
1125
|
+
* Accepts a numeric total or undefined. Never pass a Balance object here.
|
|
1185
1126
|
*/
|
|
1186
1127
|
function createDefaultBalance(total = 0) {
|
|
1187
1128
|
const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
|
|
@@ -1200,7 +1141,7 @@ function validateBalance(raw) {
|
|
|
1200
1141
|
if (raw === null || raw === undefined)
|
|
1201
1142
|
return null;
|
|
1202
1143
|
// If it's already a Balance-shaped object, extract numeric fields
|
|
1203
|
-
// Use Number() not parseFloat()
|
|
1144
|
+
// Use Number() not parseFloat(): parseFloat('10abc') silently returns 10
|
|
1204
1145
|
const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
|
|
1205
1146
|
const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
|
|
1206
1147
|
if (!Number.isFinite(pub) || pub < 0)
|
|
@@ -1214,34 +1155,165 @@ function validateBalance(raw) {
|
|
|
1214
1155
|
currency: 'OCT'
|
|
1215
1156
|
};
|
|
1216
1157
|
}
|
|
1217
|
-
/**
|
|
1218
|
-
* SDK Configuration constants
|
|
1219
|
-
*/
|
|
1220
1158
|
const SDK_CONFIG = {
|
|
1221
|
-
version: '2.
|
|
1159
|
+
version: '2.8.0',
|
|
1222
1160
|
defaultNetworkId: DEFAULT_NETWORK_ID,
|
|
1223
1161
|
communicationTimeout: 30000, // 30 seconds
|
|
1224
1162
|
retryAttempts: 3,
|
|
1225
1163
|
retryDelay: 1000, // 1 second
|
|
1226
1164
|
};
|
|
1227
|
-
/**
|
|
1228
|
-
* Get default network configuration
|
|
1229
|
-
*/
|
|
1230
1165
|
function getDefaultNetwork() {
|
|
1231
1166
|
return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
|
|
1232
1167
|
}
|
|
1233
1168
|
|
|
1169
|
+
/** Scope names the 0xio wallet enforces. Any other name is dropped at connect. */
|
|
1170
|
+
const WALLET_PERMISSIONS = [
|
|
1171
|
+
'accounts',
|
|
1172
|
+
'public_transactions',
|
|
1173
|
+
'contract_calls',
|
|
1174
|
+
'contract_views',
|
|
1175
|
+
'private_balance_read',
|
|
1176
|
+
'private_proofs',
|
|
1177
|
+
'private_transfers',
|
|
1178
|
+
'private_claims',
|
|
1179
|
+
];
|
|
1180
|
+
/** Older SDK permission names and the wallet scope each one means. */
|
|
1181
|
+
const LEGACY_PERMISSION_MAP = {
|
|
1182
|
+
read_address: 'accounts',
|
|
1183
|
+
read_balance: 'accounts',
|
|
1184
|
+
read_public_key: 'accounts',
|
|
1185
|
+
send_transactions: 'public_transactions',
|
|
1186
|
+
sign_messages: 'public_transactions',
|
|
1187
|
+
contract_calls: 'contract_calls',
|
|
1188
|
+
view_private_balance: 'private_balance_read',
|
|
1189
|
+
view_encrypted_balance: 'private_balance_read',
|
|
1190
|
+
stealth_scan: 'private_balance_read',
|
|
1191
|
+
decrypt_balance: 'private_balance_read',
|
|
1192
|
+
encrypt_balance: 'private_proofs',
|
|
1193
|
+
private_transfers: 'private_transfers',
|
|
1194
|
+
stealth_claim: 'private_claims',
|
|
1195
|
+
};
|
|
1196
|
+
/** Translate any mix of old and new names into the wallet's scope names, without duplicates. */
|
|
1197
|
+
function toWalletPermissions(perms) {
|
|
1198
|
+
const out = [];
|
|
1199
|
+
for (const p of perms ?? []) {
|
|
1200
|
+
const canonical = WALLET_PERMISSIONS.includes(p)
|
|
1201
|
+
? p
|
|
1202
|
+
: LEGACY_PERMISSION_MAP[p];
|
|
1203
|
+
if (canonical && !out.includes(canonical))
|
|
1204
|
+
out.push(canonical);
|
|
1205
|
+
}
|
|
1206
|
+
return out;
|
|
1207
|
+
}
|
|
1234
1208
|
/**
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1209
|
+
* The granted wallet scopes plus every requested old name they satisfy, so a dapp that checks
|
|
1210
|
+
* for the name it asked for (for example 'read_balance') keeps seeing it.
|
|
1237
1211
|
*/
|
|
1212
|
+
function withLegacyAliases(granted, requested) {
|
|
1213
|
+
const set = new Set(granted ?? []);
|
|
1214
|
+
for (const p of requested ?? []) {
|
|
1215
|
+
const canonical = LEGACY_PERMISSION_MAP[p];
|
|
1216
|
+
if (canonical && set.has(canonical))
|
|
1217
|
+
set.add(p);
|
|
1218
|
+
}
|
|
1219
|
+
return [...set];
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* 0xio Signed Message standard (v1).
|
|
1224
|
+
*
|
|
1225
|
+
* `wallet.signMessage(message)` never signs the raw message. The wallet frames it first so a signed
|
|
1226
|
+
* "message" can never collide with a transaction pre-image (a transaction is canonical JSON that
|
|
1227
|
+
* begins with '{'). The framing is:
|
|
1228
|
+
*
|
|
1229
|
+
* "Octra Signed Message:\n" + utf8ByteLength(message) + "\n" + message
|
|
1230
|
+
*
|
|
1231
|
+
* signed as an Ed25519 detached signature over the UTF-8 bytes of that string. The leading 'O'
|
|
1232
|
+
* guarantees the signed bytes never begin with '{', so a personal-message signature can never be a
|
|
1233
|
+
* valid transaction. Any verifier MUST reconstruct the same bytes: use `getSignedMessageBytes()`
|
|
1234
|
+
* with any Ed25519 library, or `verifyMessage()` for a batteries-included check.
|
|
1235
|
+
*/
|
|
1236
|
+
/** Fixed prefix tag for the 0xio Signed Message scheme. */
|
|
1237
|
+
const SIGNED_MESSAGE_PREFIX = 'Octra Signed Message:';
|
|
1238
|
+
/** Scheme version, bumped if the framing ever changes so verifiers can detect it. */
|
|
1239
|
+
const SIGNED_MESSAGE_VERSION = 1;
|
|
1240
|
+
/** UTF-8 byte length of a string (JS `.length` counts UTF-16 units, not bytes). */
|
|
1241
|
+
function utf8ByteLength(s) {
|
|
1242
|
+
return new TextEncoder().encode(s).length;
|
|
1243
|
+
}
|
|
1244
|
+
function base64ToBytes(b64) {
|
|
1245
|
+
// atob is available in browsers and Node 16+; utils.deriveOctraAddress uses the same path.
|
|
1246
|
+
const bin = atob(b64);
|
|
1247
|
+
const out = new Uint8Array(bin.length);
|
|
1248
|
+
for (let i = 0; i < bin.length; i++)
|
|
1249
|
+
out[i] = bin.charCodeAt(i);
|
|
1250
|
+
return out;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* The exact bytes that `wallet.signMessage(message)` produces a signature over. Verify a 0xio
|
|
1254
|
+
* message signature by checking an Ed25519 signature against these bytes with the signer's public
|
|
1255
|
+
* key. Zero-dependency - bring your own Ed25519 verifier, or use `verifyMessage`.
|
|
1256
|
+
*/
|
|
1257
|
+
function getSignedMessageBytes(message) {
|
|
1258
|
+
if (typeof message !== 'string') {
|
|
1259
|
+
throw new TypeError('message must be a string');
|
|
1260
|
+
}
|
|
1261
|
+
const framed = `${SIGNED_MESSAGE_PREFIX}\n${utf8ByteLength(message)}\n${message}`;
|
|
1262
|
+
return new TextEncoder().encode(framed);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Reconstruct the auth message that `wallet.signAuthMessage(service, nonce)` signs. A relying
|
|
1266
|
+
* service verifies an auth signature with `verifyMessage(buildAuthMessage(service, nonce, origin),
|
|
1267
|
+
* signature, publicKey)`, where `origin` is the caller's page origin.
|
|
1268
|
+
*/
|
|
1269
|
+
function buildAuthMessage(service, nonce, origin) {
|
|
1270
|
+
return `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Verify a 0xio message signature produced by `wallet.signMessage`.
|
|
1274
|
+
*
|
|
1275
|
+
* @param message The original message passed to `wallet.signMessage`.
|
|
1276
|
+
* @param signature Base64 Ed25519 signature returned by `wallet.signMessage`.
|
|
1277
|
+
* @param publicKey Base64 Ed25519 public key of the signer (from `wallet.getPublicKey()`).
|
|
1278
|
+
* @returns Whether the signature is valid for this message and key.
|
|
1279
|
+
*
|
|
1280
|
+
* Uses the Web Crypto Ed25519 primitive (Node 18+, Chrome 137+, Safari 17+, Firefox 129+). In an
|
|
1281
|
+
* environment without it, verify `getSignedMessageBytes(message)` with your own Ed25519 library.
|
|
1282
|
+
*/
|
|
1283
|
+
async function verifyMessage(message, signature, publicKey) {
|
|
1284
|
+
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
1285
|
+
throw new Error('Web Crypto API unavailable; verify getSignedMessageBytes(message) with your own Ed25519 library');
|
|
1286
|
+
}
|
|
1287
|
+
let sigBytes;
|
|
1288
|
+
let pubBytes;
|
|
1289
|
+
try {
|
|
1290
|
+
sigBytes = base64ToBytes(signature);
|
|
1291
|
+
pubBytes = base64ToBytes(publicKey);
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
const data = new Uint8Array(getSignedMessageBytes(message));
|
|
1298
|
+
const key = await crypto.subtle.importKey('raw', pubBytes, { name: 'Ed25519' }, false, [
|
|
1299
|
+
'verify',
|
|
1300
|
+
]);
|
|
1301
|
+
return await crypto.subtle.verify({ name: 'Ed25519' }, key, sigBytes, data);
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
// importKey/verify can throw (rather than return false) on malformed input or an environment
|
|
1305
|
+
// that recognizes Ed25519 only partially; treat any such failure as "not verified".
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1238
1310
|
class ZeroXIOWallet extends EventEmitter {
|
|
1239
1311
|
constructor(config) {
|
|
1240
1312
|
super(config.debug);
|
|
1241
1313
|
this.connectionInfo = { isConnected: false };
|
|
1242
1314
|
this.isInitialized = false;
|
|
1243
1315
|
this._initPromise = null;
|
|
1244
|
-
// session version
|
|
1316
|
+
// session version, for stale write detection
|
|
1245
1317
|
this._sessionVersion = 0;
|
|
1246
1318
|
this.config = {
|
|
1247
1319
|
...config,
|
|
@@ -1250,42 +1322,31 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1250
1322
|
debug: config.debug || false
|
|
1251
1323
|
};
|
|
1252
1324
|
this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
|
|
1253
|
-
this.communicator = new ExtensionCommunicator(this.config.debug, [], this.config.adapter);
|
|
1325
|
+
this.communicator = new ExtensionCommunicator(this.config.debug, this.config.trustedParentOrigins ?? [], this.config.adapter);
|
|
1254
1326
|
this.logger.log('Wallet instance created with config:', this.config);
|
|
1255
1327
|
}
|
|
1256
|
-
// ===================
|
|
1257
|
-
// INITIALIZATION
|
|
1258
|
-
// ===================
|
|
1259
|
-
/**
|
|
1260
|
-
* Initialize the SDK
|
|
1261
|
-
* Must be called before using any other methods
|
|
1262
|
-
*/
|
|
1263
1328
|
async initialize() {
|
|
1264
1329
|
if (this.isInitialized) {
|
|
1265
1330
|
return true;
|
|
1266
1331
|
}
|
|
1267
|
-
// single-flight init
|
|
1268
1332
|
if (this._initPromise) {
|
|
1269
1333
|
return this._initPromise;
|
|
1270
1334
|
}
|
|
1271
1335
|
this._initPromise = (async () => {
|
|
1272
1336
|
try {
|
|
1273
|
-
// Initialize extension communication
|
|
1274
1337
|
const communicationReady = await this.communicator.initialize();
|
|
1275
1338
|
if (!communicationReady) {
|
|
1276
1339
|
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
|
|
1277
1340
|
}
|
|
1278
|
-
// Register this DApp with the extension
|
|
1279
1341
|
await this.communicator.sendRequest('register_dapp', {
|
|
1280
1342
|
appName: this.config.appName,
|
|
1281
1343
|
appDescription: this.config.appDescription,
|
|
1282
1344
|
appVersion: this.config.appVersion,
|
|
1283
1345
|
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1284
1346
|
appIcon: this.config.appIcon,
|
|
1285
|
-
requiredPermissions: this.config.requiredPermissions,
|
|
1347
|
+
requiredPermissions: toWalletPermissions(this.config.requiredPermissions),
|
|
1286
1348
|
networkId: this.config.networkId
|
|
1287
1349
|
});
|
|
1288
|
-
// Setup event forwarding from extension
|
|
1289
1350
|
this.setupExtensionEventListeners();
|
|
1290
1351
|
this.isInitialized = true;
|
|
1291
1352
|
this.logger.log('SDK initialized successfully');
|
|
@@ -1304,37 +1365,29 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1304
1365
|
})();
|
|
1305
1366
|
return this._initPromise;
|
|
1306
1367
|
}
|
|
1307
|
-
/**
|
|
1308
|
-
* Check if SDK is initialized
|
|
1309
|
-
*/
|
|
1310
1368
|
isReady() {
|
|
1311
1369
|
return this.isInitialized && this.communicator.isExtensionAvailable();
|
|
1312
1370
|
}
|
|
1313
|
-
// ===================
|
|
1314
|
-
// CONNECTION MANAGEMENT
|
|
1315
|
-
// ===================
|
|
1316
|
-
/**
|
|
1317
|
-
* Connect to wallet
|
|
1318
|
-
*/
|
|
1319
1371
|
async connect(options = {}) {
|
|
1320
1372
|
this.ensureInitialized();
|
|
1321
1373
|
try {
|
|
1322
1374
|
this.logger.log('Attempting to connect with options:', options);
|
|
1323
|
-
// filter to declared perms only
|
|
1375
|
+
// filter to declared perms only: accept both RFC 'permissions' and legacy 'requestPermissions'
|
|
1324
1376
|
const declaredPermissions = this.config.requiredPermissions || [];
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1377
|
+
const requestedPerms = options.permissions ?? options.requestPermissions;
|
|
1378
|
+
const requestedPermissions = requestedPerms
|
|
1379
|
+
? requestedPerms.filter(p => declaredPermissions.includes(p))
|
|
1327
1380
|
: declaredPermissions;
|
|
1328
1381
|
const result = await this.communicator.sendRequest('connect', {
|
|
1329
|
-
|
|
1382
|
+
permissions: toWalletPermissions(requestedPermissions),
|
|
1330
1383
|
networkId: options.networkId || this.config.networkId
|
|
1331
1384
|
});
|
|
1332
|
-
// verify
|
|
1385
|
+
// verify the public key to address binding
|
|
1333
1386
|
if (result.publicKey && result.address) {
|
|
1334
1387
|
try {
|
|
1335
1388
|
const derived = await deriveOctraAddress(result.publicKey);
|
|
1336
1389
|
if (derived !== result.address) {
|
|
1337
|
-
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed
|
|
1390
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed: the reported public key does not derive to the reported address');
|
|
1338
1391
|
}
|
|
1339
1392
|
}
|
|
1340
1393
|
catch (e) {
|
|
@@ -1343,11 +1396,16 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1343
1396
|
this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
|
|
1344
1397
|
}
|
|
1345
1398
|
}
|
|
1346
|
-
// Use networkInfo from extension response
|
|
1399
|
+
// Use networkInfo from the extension response, validated before caching.
|
|
1347
1400
|
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1348
|
-
??
|
|
1349
|
-
|
|
1350
|
-
|
|
1401
|
+
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1402
|
+
if (!networkInfo) {
|
|
1403
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
|
|
1404
|
+
}
|
|
1405
|
+
// The wallet answers with its own scope names; the names the dapp asked for are kept as
|
|
1406
|
+
// aliases so existing permission checks keep working.
|
|
1407
|
+
const permissions = withLegacyAliases(result.permissions, requestedPermissions);
|
|
1408
|
+
// Update connection info, including permissions
|
|
1351
1409
|
this.connectionInfo = {
|
|
1352
1410
|
isConnected: true,
|
|
1353
1411
|
address: result.address,
|
|
@@ -1421,12 +1479,12 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1421
1479
|
if (this._sessionVersion !== sv)
|
|
1422
1480
|
return { ...this.connectionInfo };
|
|
1423
1481
|
if (result.isConnected && result.address) {
|
|
1424
|
-
// verify
|
|
1482
|
+
// verify the public key to address binding
|
|
1425
1483
|
if (result.publicKey) {
|
|
1426
1484
|
try {
|
|
1427
1485
|
const derived = await deriveOctraAddress(result.publicKey);
|
|
1428
1486
|
if (derived !== result.address) {
|
|
1429
|
-
this.logger.warn('Address-key binding mismatch on session restore
|
|
1487
|
+
this.logger.warn('Address-key binding mismatch on session restore, ignoring stale session');
|
|
1430
1488
|
this.connectionInfo = { isConnected: false };
|
|
1431
1489
|
return { ...this.connectionInfo };
|
|
1432
1490
|
}
|
|
@@ -1438,9 +1496,13 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1438
1496
|
// validate untrusted balance/networkInfo before caching
|
|
1439
1497
|
const balanceInfo = validateBalance(result.balance) ?? createDefaultBalance();
|
|
1440
1498
|
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1441
|
-
??
|
|
1499
|
+
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1500
|
+
if (!networkInfo) {
|
|
1501
|
+
this.logger.warn('getConnectionStatus: wallet returned no network metadata, returning cached state');
|
|
1502
|
+
return this.connectionInfo;
|
|
1503
|
+
}
|
|
1442
1504
|
const wasConnected = this.connectionInfo.isConnected;
|
|
1443
|
-
const permissions = result.permissions
|
|
1505
|
+
const permissions = withLegacyAliases(result.permissions, this.config.requiredPermissions);
|
|
1444
1506
|
// preserve existing connectedAt
|
|
1445
1507
|
const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
|
|
1446
1508
|
this.connectionInfo = {
|
|
@@ -1453,7 +1515,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1453
1515
|
permissions
|
|
1454
1516
|
};
|
|
1455
1517
|
this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
|
|
1456
|
-
// only emit on disconnected
|
|
1518
|
+
// only emit on the disconnected to connected transition
|
|
1457
1519
|
if (!wasConnected) {
|
|
1458
1520
|
const connectEvent = {
|
|
1459
1521
|
address: result.address,
|
|
@@ -1478,8 +1540,8 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1478
1540
|
}
|
|
1479
1541
|
}
|
|
1480
1542
|
/**
|
|
1481
|
-
* Switch the extension's active network (e.g. 'mainnet'
|
|
1482
|
-
* Works silently
|
|
1543
|
+
* Switch the extension's active network (e.g. 'mainnet' to 'devnet').
|
|
1544
|
+
* Works silently: no popup, no user confirmation needed.
|
|
1483
1545
|
* The extension broadcasts 'networkChanged' event to all connected dApps.
|
|
1484
1546
|
*/
|
|
1485
1547
|
async switchNetwork(networkId) {
|
|
@@ -1511,18 +1573,25 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1511
1573
|
getNetworkId() {
|
|
1512
1574
|
return this.connectionInfo.networkInfo?.id || null;
|
|
1513
1575
|
}
|
|
1514
|
-
// ===================
|
|
1515
|
-
// WALLET INFORMATION
|
|
1516
|
-
// ===================
|
|
1517
|
-
/**
|
|
1518
|
-
* Get current wallet address
|
|
1519
|
-
*/
|
|
1520
1576
|
getAddress() {
|
|
1521
1577
|
return this.connectionInfo.address || null;
|
|
1522
1578
|
}
|
|
1523
1579
|
/**
|
|
1524
|
-
*
|
|
1580
|
+
* The connected account's Ed25519 public key (base64). Served from the session when the
|
|
1581
|
+
* wallet reported it at connect, otherwise asked from the wallet.
|
|
1525
1582
|
*/
|
|
1583
|
+
async getPublicKey() {
|
|
1584
|
+
this.ensureConnected();
|
|
1585
|
+
if (this.connectionInfo.publicKey)
|
|
1586
|
+
return this.connectionInfo.publicKey;
|
|
1587
|
+
const result = await this.communicator.sendRequest('getPublicKey');
|
|
1588
|
+
const publicKey = result?.publicKey;
|
|
1589
|
+
if (typeof publicKey !== 'string' || !publicKey) {
|
|
1590
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Wallet did not return a public key');
|
|
1591
|
+
}
|
|
1592
|
+
this.connectionInfo.publicKey = publicKey;
|
|
1593
|
+
return publicKey;
|
|
1594
|
+
}
|
|
1526
1595
|
async getBalance(forceRefresh = false) {
|
|
1527
1596
|
this.ensureConnected();
|
|
1528
1597
|
try {
|
|
@@ -1547,7 +1616,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1547
1616
|
// skip if session changed mid-flight
|
|
1548
1617
|
if (this._sessionVersion !== sv)
|
|
1549
1618
|
return result;
|
|
1550
|
-
// Update cached balance
|
|
1551
1619
|
if (this.connectionInfo.balance) {
|
|
1552
1620
|
const previousBalance = this.connectionInfo.balance;
|
|
1553
1621
|
this.connectionInfo.balance = result;
|
|
@@ -1574,15 +1642,11 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1574
1642
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
|
|
1575
1643
|
}
|
|
1576
1644
|
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Get network information
|
|
1579
|
-
*/
|
|
1580
1645
|
async getNetworkInfo() {
|
|
1581
1646
|
this.ensureInitialized();
|
|
1582
1647
|
try {
|
|
1583
1648
|
const sv = this._sessionVersion;
|
|
1584
1649
|
const result = await this.communicator.sendRequest('get_network_info');
|
|
1585
|
-
// validate network info before caching
|
|
1586
1650
|
const networkInfo = validateNetworkInfo(result);
|
|
1587
1651
|
if (!networkInfo) {
|
|
1588
1652
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Extension returned invalid network info');
|
|
@@ -1590,11 +1654,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1590
1654
|
// skip if session changed mid-flight
|
|
1591
1655
|
if (this._sessionVersion !== sv)
|
|
1592
1656
|
return networkInfo;
|
|
1593
|
-
// Update cached network info
|
|
1594
1657
|
if (this.connectionInfo.networkInfo) {
|
|
1595
1658
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1596
1659
|
this.connectionInfo.networkInfo = networkInfo;
|
|
1597
|
-
// Emit network changed event if different
|
|
1598
1660
|
if (previousNetwork.id !== networkInfo.id) {
|
|
1599
1661
|
const networkChangedEvent = {
|
|
1600
1662
|
previousNetwork,
|
|
@@ -1614,32 +1676,22 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1614
1676
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
|
|
1615
1677
|
}
|
|
1616
1678
|
}
|
|
1617
|
-
// ===================
|
|
1618
|
-
// TRANSACTIONS
|
|
1619
|
-
// ===================
|
|
1620
|
-
/**
|
|
1621
|
-
* Send transaction
|
|
1622
|
-
*/
|
|
1623
1679
|
async sendTransaction(txData) {
|
|
1624
1680
|
this.ensureConnected();
|
|
1625
|
-
// validate inputs
|
|
1626
1681
|
if (!isValidAddress(txData.to)) {
|
|
1627
1682
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1628
1683
|
}
|
|
1629
|
-
|
|
1630
|
-
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1631
|
-
}
|
|
1632
|
-
// bound memo
|
|
1684
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1633
1685
|
if (txData.message && txData.message.length > 1000) {
|
|
1634
1686
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1635
1687
|
}
|
|
1636
1688
|
try {
|
|
1637
1689
|
// log non-sensitive only
|
|
1638
1690
|
this.logger.log('Sending transaction:', { to: txData.to });
|
|
1639
|
-
const result = await this.communicator.sendRequest('send_transaction', txData);
|
|
1691
|
+
const result = await this.communicator.sendRequest('send_transaction', { ...txData, amount });
|
|
1640
1692
|
this.logger.log('Transaction result:', result);
|
|
1641
|
-
// Refresh balance after successful transaction
|
|
1642
|
-
if (result.success) {
|
|
1693
|
+
// Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
|
|
1694
|
+
if (result.accepted ?? result.success) {
|
|
1643
1695
|
setTimeout(() => {
|
|
1644
1696
|
this.getBalance(true).catch(error => {
|
|
1645
1697
|
this.logger.warn('Failed to refresh balance after transaction:', error);
|
|
@@ -1656,23 +1708,74 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1656
1708
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to send transaction', error);
|
|
1657
1709
|
}
|
|
1658
1710
|
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Sign a transaction without broadcasting it (RFC-O-1 octra_signTransaction).
|
|
1713
|
+
* Returns the signed transaction object for manual submission via submitTransaction().
|
|
1714
|
+
*/
|
|
1715
|
+
async signTransaction(txData) {
|
|
1716
|
+
this.ensureConnected();
|
|
1717
|
+
if (!isValidAddress(txData.to)) {
|
|
1718
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1719
|
+
}
|
|
1720
|
+
const amount = this.resolveRawAmount(txData.amount, txData.amountOct, 'Transaction amount');
|
|
1721
|
+
if (txData.message && txData.message.length > 1000) {
|
|
1722
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1723
|
+
}
|
|
1724
|
+
try {
|
|
1725
|
+
this.logger.log('Requesting transaction signature:', { to: txData.to });
|
|
1726
|
+
const result = await this.communicator.sendRequest('sign_transaction', { ...txData, amount });
|
|
1727
|
+
return result;
|
|
1728
|
+
}
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1731
|
+
throw error;
|
|
1732
|
+
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Failed to sign transaction', error);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Broadcast a pre-signed transaction (RFC-O-1 octra_submitTransaction).
|
|
1737
|
+
* Use after signTransaction() to submit the signed tx to the network.
|
|
1738
|
+
*/
|
|
1739
|
+
async submitTransaction(signedTx) {
|
|
1740
|
+
this.ensureConnected();
|
|
1741
|
+
if (!signedTx || typeof signedTx !== 'object') {
|
|
1742
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'signedTx must be an object');
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
this.logger.log('Submitting pre-signed transaction');
|
|
1746
|
+
const result = await this.communicator.sendRequest('broadcast_only', { signedTx });
|
|
1747
|
+
return result;
|
|
1748
|
+
}
|
|
1749
|
+
catch (error) {
|
|
1750
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1751
|
+
throw error;
|
|
1752
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to submit transaction', error);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1659
1755
|
/**
|
|
1660
1756
|
* Call a smart contract method (state-changing).
|
|
1661
1757
|
* The extension builds, signs, and submits the transaction via octra_submit.
|
|
1662
1758
|
*/
|
|
1663
1759
|
async callContract(callData) {
|
|
1664
1760
|
this.ensureConnected();
|
|
1665
|
-
// validate inputs
|
|
1666
1761
|
if (!isValidAddress(callData.contract)) {
|
|
1667
1762
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1668
1763
|
}
|
|
1669
1764
|
if (!callData.method || typeof callData.method !== 'string') {
|
|
1670
1765
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method is required');
|
|
1671
1766
|
}
|
|
1672
|
-
// bound method + params size
|
|
1673
1767
|
if (callData.method.length > 200) {
|
|
1674
1768
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
|
|
1675
1769
|
}
|
|
1770
|
+
if (callData.amount != null && callData.amountOct != null) {
|
|
1771
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Contract call amount: pass amount or amountOct, not both');
|
|
1772
|
+
}
|
|
1773
|
+
if (callData.amount != null) {
|
|
1774
|
+
this.assertExactOCTAmount(callData.amount, 'Contract call amount');
|
|
1775
|
+
}
|
|
1776
|
+
if (callData.amountOct != null) {
|
|
1777
|
+
this.assertExactOCTAmount(callData.amountOct, 'Contract call amount');
|
|
1778
|
+
}
|
|
1676
1779
|
try {
|
|
1677
1780
|
if (JSON.stringify(callData.params).length > 65536) {
|
|
1678
1781
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
|
|
@@ -1690,7 +1793,11 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1690
1793
|
contract: callData.contract,
|
|
1691
1794
|
method: callData.method,
|
|
1692
1795
|
params: callData.params,
|
|
1693
|
-
amount: callData.
|
|
1796
|
+
amount: callData.amountOct != null
|
|
1797
|
+
? octToMicro(callData.amountOct)
|
|
1798
|
+
: callData.amount != null
|
|
1799
|
+
? String(callData.amount)
|
|
1800
|
+
: '0',
|
|
1694
1801
|
ou: callData.ou != null ? String(callData.ou) : '10000',
|
|
1695
1802
|
});
|
|
1696
1803
|
this.logger.log('Contract call result:', result);
|
|
@@ -1710,14 +1817,12 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1710
1817
|
*/
|
|
1711
1818
|
async contractCallView(viewData) {
|
|
1712
1819
|
this.ensureInitialized();
|
|
1713
|
-
// validate inputs
|
|
1714
1820
|
if (!isValidAddress(viewData.contract)) {
|
|
1715
1821
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1716
1822
|
}
|
|
1717
1823
|
if (!viewData.method || typeof viewData.method !== 'string') {
|
|
1718
1824
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract method is required');
|
|
1719
1825
|
}
|
|
1720
|
-
// bound method + params size
|
|
1721
1826
|
if (viewData.method.length > 200) {
|
|
1722
1827
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract method name too long (max 200 characters)');
|
|
1723
1828
|
}
|
|
@@ -1757,7 +1862,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1757
1862
|
*/
|
|
1758
1863
|
async getContractStorage(contract, key) {
|
|
1759
1864
|
this.ensureInitialized();
|
|
1760
|
-
// validate inputs
|
|
1761
1865
|
if (!isValidAddress(contract)) {
|
|
1762
1866
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1763
1867
|
}
|
|
@@ -1799,12 +1903,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1799
1903
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get transaction history', error);
|
|
1800
1904
|
}
|
|
1801
1905
|
}
|
|
1802
|
-
// ===================
|
|
1803
|
-
// PRIVATE FEATURES
|
|
1804
|
-
// ===================
|
|
1805
|
-
/**
|
|
1806
|
-
* Get private balance information
|
|
1807
|
-
*/
|
|
1808
1906
|
async getPrivateBalanceInfo() {
|
|
1809
1907
|
this.ensureConnected();
|
|
1810
1908
|
try {
|
|
@@ -1816,11 +1914,13 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1816
1914
|
}
|
|
1817
1915
|
}
|
|
1818
1916
|
/**
|
|
1819
|
-
* Encrypt public balance to private
|
|
1917
|
+
* Encrypt public balance to private.
|
|
1918
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1919
|
+
* NOT_AVAILABLE); users encrypt from the extension's Privacy screen.
|
|
1820
1920
|
*/
|
|
1821
1921
|
async encryptBalance(amount) {
|
|
1822
1922
|
this.ensureConnected();
|
|
1823
|
-
|
|
1923
|
+
this.assertExactOCTAmount(amount, 'Encrypt amount');
|
|
1824
1924
|
if (!isValidAmount(amount)) {
|
|
1825
1925
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
|
|
1826
1926
|
}
|
|
@@ -1837,11 +1937,13 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1837
1937
|
}
|
|
1838
1938
|
}
|
|
1839
1939
|
/**
|
|
1840
|
-
* Decrypt private balance to public
|
|
1940
|
+
* Decrypt private balance to public.
|
|
1941
|
+
* @deprecated The 0xio extension does not serve this through the bridge (it answers
|
|
1942
|
+
* NOT_AVAILABLE); users decrypt from the extension's Privacy screen.
|
|
1841
1943
|
*/
|
|
1842
1944
|
async decryptBalance(amount) {
|
|
1843
1945
|
this.ensureConnected();
|
|
1844
|
-
|
|
1946
|
+
this.assertExactOCTAmount(amount, 'Decrypt amount');
|
|
1845
1947
|
if (!isValidAmount(amount)) {
|
|
1846
1948
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
|
|
1847
1949
|
}
|
|
@@ -1867,21 +1969,24 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1867
1969
|
*/
|
|
1868
1970
|
async sendPrivateTransfer(transferData) {
|
|
1869
1971
|
this.ensureConnected();
|
|
1870
|
-
// validate inputs
|
|
1871
1972
|
if (!isValidAddress(transferData.to)) {
|
|
1872
1973
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1873
1974
|
}
|
|
1874
1975
|
if (!isValidAmount(transferData.amount)) {
|
|
1875
1976
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transfer amount');
|
|
1876
1977
|
}
|
|
1978
|
+
this.assertExactOCTAmount(transferData.amount, 'Transfer amount');
|
|
1877
1979
|
// bound msg size
|
|
1878
1980
|
if (transferData.message && transferData.message.length > 1000) {
|
|
1879
1981
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
|
|
1880
1982
|
}
|
|
1881
1983
|
try {
|
|
1882
|
-
const result = await this.communicator.sendRequest('send_private_transfer',
|
|
1883
|
-
|
|
1884
|
-
|
|
1984
|
+
const result = await this.communicator.sendRequest('send_private_transfer', {
|
|
1985
|
+
...transferData,
|
|
1986
|
+
...(transferData.amountRaw ? { amount_raw: transferData.amountRaw } : {}),
|
|
1987
|
+
});
|
|
1988
|
+
// Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
|
|
1989
|
+
if (result.accepted ?? result.success) {
|
|
1885
1990
|
setTimeout(() => {
|
|
1886
1991
|
this.getBalance(true).catch(() => { });
|
|
1887
1992
|
}, 1000);
|
|
@@ -1921,8 +2026,8 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1921
2026
|
const result = await this.communicator.sendRequest('claim_private_transfer', {
|
|
1922
2027
|
transferId
|
|
1923
2028
|
});
|
|
1924
|
-
// Refresh balance after claiming
|
|
1925
|
-
if (result.success) {
|
|
2029
|
+
// Refresh balance after claiming (accept RFC 'accepted' or legacy 'success')
|
|
2030
|
+
if (result.accepted ?? result.success) {
|
|
1926
2031
|
setTimeout(() => {
|
|
1927
2032
|
this.getBalance(true).catch(() => { });
|
|
1928
2033
|
}, 1000);
|
|
@@ -1933,19 +2038,89 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1933
2038
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
|
|
1934
2039
|
}
|
|
1935
2040
|
}
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
2041
|
+
// Generic provider passthrough and RFP private primitives (since 2.8.0)
|
|
2042
|
+
// These route through the wallet bridge. The wallet keeps all private/FHE
|
|
2043
|
+
// secret material internal and returns only ciphertexts/proofs/tx hashes.
|
|
2044
|
+
/**
|
|
2045
|
+
* Send any wallet method + params through the bridge. Escape hatch for
|
|
2046
|
+
* primitives that don't have a typed helper yet (no SDK upgrade needed).
|
|
2047
|
+
* @since 2.8.0
|
|
2048
|
+
*/
|
|
2049
|
+
async request(method, params = {}) {
|
|
2050
|
+
return this.communicator.sendRequest(method, params);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Read-only node RPC through the wallet. Only the wallet's allow-listed public methods work
|
|
2054
|
+
* (octra_balance, octra_transaction, contract_call and similar); writes are refused.
|
|
2055
|
+
* @since 2.8.0
|
|
2056
|
+
*/
|
|
2057
|
+
async rpcCall(method, params = []) {
|
|
2058
|
+
return this.communicator.sendRequest('rpc_call', { method, params });
|
|
2059
|
+
}
|
|
1939
2060
|
/**
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
2061
|
+
* Feature-detect which private capabilities the connected wallet supports.
|
|
2062
|
+
* Lets a dapp render the correct UI (or fail closed) before any action.
|
|
2063
|
+
* @since 2.8.0
|
|
2064
|
+
*/
|
|
2065
|
+
async getPrivateCapabilities() {
|
|
2066
|
+
return this.communicator.sendRequest('get_private_capabilities');
|
|
2067
|
+
}
|
|
2068
|
+
/** Read-only contract view (no approval popup). @since 2.8.0 */
|
|
2069
|
+
async callContractView(params) {
|
|
2070
|
+
this.ensureConnected();
|
|
2071
|
+
return this.communicator.sendRequest('contract_call_view', params);
|
|
2072
|
+
}
|
|
2073
|
+
/** Encrypt a raw integer value to a contract-ready ciphertext (keys stay in wallet). @since 2.8.0 */
|
|
2074
|
+
async encryptValue(params) {
|
|
2075
|
+
this.ensureConnected();
|
|
2076
|
+
return this.communicator.sendRequest('encrypt_value', params);
|
|
2077
|
+
}
|
|
2078
|
+
/** Decrypt a ciphertext to a raw integer value. @since 2.8.0 */
|
|
2079
|
+
async decryptValue(params) {
|
|
2080
|
+
this.ensureConnected();
|
|
2081
|
+
return this.communicator.sendRequest('decrypt_value', params);
|
|
2082
|
+
}
|
|
2083
|
+
/** Zero proof for a ciphertext (proves it encrypts the given value, default 0). @since 2.8.0 */
|
|
2084
|
+
async makeZeroProof(params) {
|
|
2085
|
+
this.ensureConnected();
|
|
2086
|
+
return this.communicator.sendRequest('make_zero_proof', params);
|
|
2087
|
+
}
|
|
2088
|
+
/** Range proof for a ciphertext. @since 2.8.0 */
|
|
2089
|
+
async makeRangeProof(params) {
|
|
2090
|
+
this.ensureConnected();
|
|
2091
|
+
return this.communicator.sendRequest('make_range_proof', params);
|
|
2092
|
+
}
|
|
2093
|
+
/** Private OCT + private token balances (decrypted inside the wallet). @since 2.8.0 */
|
|
2094
|
+
async getPrivateBalance(params = {}) {
|
|
2095
|
+
this.ensureConnected();
|
|
2096
|
+
return this.communicator.sendRequest('get_private_balance', params);
|
|
2097
|
+
}
|
|
2098
|
+
/** Register a receive/view public key so an address can receive private transfers. @since 2.8.0 */
|
|
2099
|
+
async registerPrivateViewKey(params) {
|
|
2100
|
+
this.ensureConnected();
|
|
2101
|
+
return this.communicator.sendRequest('register_private_view_key', params);
|
|
2102
|
+
}
|
|
2103
|
+
/** Submit an ordered multi-step public contract flow under one approval. @since 2.8.0 */
|
|
2104
|
+
async sendContractTransactionSequence(params) {
|
|
2105
|
+
this.ensureConnected();
|
|
2106
|
+
return this.communicator.sendRequest('send_contract_transaction_sequence', params);
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Sign an arbitrary message with the wallet's private key.
|
|
2110
|
+
* The user will be prompted to approve the signature request in the extension.
|
|
2111
|
+
*
|
|
2112
|
+
* The wallet does not sign the raw message: it signs the 0xio Signed Message framing
|
|
2113
|
+
* (`"Octra Signed Message:\n" + byteLength + "\n" + message`) so a signed message can never be a
|
|
2114
|
+
* transaction pre-image. Verify with `verifyMessage(message, signature, publicKey)`, or check an
|
|
2115
|
+
* Ed25519 signature against `getSignedMessageBytes(message)` with your own library.
|
|
2116
|
+
*
|
|
1942
2117
|
* @param message - The message to sign (non-empty string)
|
|
1943
2118
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1944
2119
|
* @throws ZeroXIOWalletError with code SIGNATURE_FAILED if signing fails
|
|
1945
2120
|
* @example
|
|
1946
2121
|
* ```typescript
|
|
1947
2122
|
* const signature = await wallet.signMessage('Hello, 0xio!');
|
|
1948
|
-
*
|
|
2123
|
+
* const ok = await verifyMessage('Hello, 0xio!', signature, await wallet.getPublicKey());
|
|
1949
2124
|
* ```
|
|
1950
2125
|
*/
|
|
1951
2126
|
async signMessage(message) {
|
|
@@ -1973,16 +2148,13 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1973
2148
|
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Failed to sign message', error);
|
|
1974
2149
|
}
|
|
1975
2150
|
}
|
|
1976
|
-
// ===================
|
|
1977
|
-
// AUTHENTICATION HELPERS
|
|
1978
|
-
// ===================
|
|
1979
2151
|
/**
|
|
1980
2152
|
* Sign a domain-separated authentication message.
|
|
1981
2153
|
* Unlike `signMessage()`, this prepends a standard header that binds the signature
|
|
1982
2154
|
* to the calling service and a one-time nonce, preventing cross-service replay attacks.
|
|
1983
2155
|
*
|
|
1984
2156
|
* @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
|
|
1985
|
-
* @param nonce - Unique one-time value
|
|
2157
|
+
* @param nonce - Unique one-time value. Use a server-generated UUID or challenge
|
|
1986
2158
|
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1987
2159
|
*/
|
|
1988
2160
|
async signAuthMessage(service, nonce) {
|
|
@@ -1994,12 +2166,8 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1994
2166
|
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
|
|
1995
2167
|
}
|
|
1996
2168
|
const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
|
|
1997
|
-
|
|
1998
|
-
return this.signMessage(domainSeparated);
|
|
2169
|
+
return this.signMessage(buildAuthMessage(service, nonce, origin));
|
|
1999
2170
|
}
|
|
2000
|
-
// ===================
|
|
2001
|
-
// PRIVATE METHODS
|
|
2002
|
-
// ===================
|
|
2003
2171
|
ensureInitialized() {
|
|
2004
2172
|
if (!this.isInitialized) {
|
|
2005
2173
|
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'SDK not initialized. Call initialize() first.');
|
|
@@ -2012,7 +2180,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2012
2180
|
}
|
|
2013
2181
|
}
|
|
2014
2182
|
setupExtensionEventListeners() {
|
|
2015
|
-
// Listen for extension events through the communicator
|
|
2016
2183
|
this.communicator.on('accountChanged', (event) => {
|
|
2017
2184
|
this.handleAccountChanged(event.data);
|
|
2018
2185
|
});
|
|
@@ -2031,11 +2198,21 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2031
2198
|
this.communicator.on('transactionConfirmed', (event) => {
|
|
2032
2199
|
this.handleTransactionConfirmed(event.data);
|
|
2033
2200
|
});
|
|
2201
|
+
this.communicator.on('transactionFailed', (event) => {
|
|
2202
|
+
this.emit('transactionFailed', event.data ?? event);
|
|
2203
|
+
});
|
|
2204
|
+
this.communicator.on('permissionsChanged', (event) => {
|
|
2205
|
+
const permissions = event.data ?? event;
|
|
2206
|
+
if (this.connectionInfo.isConnected) {
|
|
2207
|
+
this.connectionInfo.permissions = Array.isArray(permissions) ? permissions : [];
|
|
2208
|
+
}
|
|
2209
|
+
this.emit('permissionsChanged', permissions);
|
|
2210
|
+
});
|
|
2211
|
+
this.communicator.on('message', (event) => {
|
|
2212
|
+
this.emit('message', event.data ?? event);
|
|
2213
|
+
});
|
|
2034
2214
|
this.logger.log('Extension event listeners setup complete');
|
|
2035
2215
|
}
|
|
2036
|
-
/**
|
|
2037
|
-
* Handle account changed event from extension
|
|
2038
|
-
*/
|
|
2039
2216
|
handleAccountChanged(data) {
|
|
2040
2217
|
++this._sessionVersion;
|
|
2041
2218
|
const previousAddress = this.connectionInfo.address;
|
|
@@ -2043,7 +2220,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2043
2220
|
// clear stale pubkey on acct change
|
|
2044
2221
|
this.connectionInfo.publicKey = data.publicKey;
|
|
2045
2222
|
if (data.balance) {
|
|
2046
|
-
// validate balance before caching
|
|
2047
2223
|
const validated = validateBalance(data.balance);
|
|
2048
2224
|
if (validated) {
|
|
2049
2225
|
this.connectionInfo.balance = validated;
|
|
@@ -2061,12 +2237,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2061
2237
|
this.emit('accountChanged', accountChangedEvent);
|
|
2062
2238
|
this.logger.log('Account changed:', { newAddress: accountChangedEvent.newAddress });
|
|
2063
2239
|
}
|
|
2064
|
-
/**
|
|
2065
|
-
* Handle network changed event from extension
|
|
2066
|
-
*/
|
|
2067
2240
|
handleNetworkChanged(data) {
|
|
2068
2241
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
2069
|
-
// validate networkInfo
|
|
2242
|
+
// validate networkInfo, drop invalid
|
|
2070
2243
|
const networkInfo = validateNetworkInfo(data.networkInfo);
|
|
2071
2244
|
if (!networkInfo) {
|
|
2072
2245
|
this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
|
|
@@ -2082,11 +2255,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2082
2255
|
this.emit('networkChanged', networkChangedEvent);
|
|
2083
2256
|
this.logger.log('Network changed:', networkChangedEvent);
|
|
2084
2257
|
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Handle balance changed event from extension
|
|
2087
|
-
*/
|
|
2088
2258
|
handleBalanceChanged(data) {
|
|
2089
|
-
// validate balance before caching
|
|
2090
2259
|
const balance = validateBalance(data.balance);
|
|
2091
2260
|
if (!balance) {
|
|
2092
2261
|
this.logger.warn('Received invalid balance in balanceChanged event, ignoring');
|
|
@@ -2102,13 +2271,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2102
2271
|
this.emit('balanceChanged', balanceChangedEvent);
|
|
2103
2272
|
this.logger.log('Balance changed:', { public: balance.public });
|
|
2104
2273
|
}
|
|
2105
|
-
/**
|
|
2106
|
-
* Handle extension locked event
|
|
2107
|
-
*/
|
|
2108
2274
|
handleExtensionLocked() {
|
|
2109
2275
|
++this._sessionVersion;
|
|
2110
2276
|
this.connectionInfo = { isConnected: false };
|
|
2111
|
-
// emit extensionLocked then disconnect
|
|
2112
2277
|
this.emit('extensionLocked', {});
|
|
2113
2278
|
const disconnectEvent = {
|
|
2114
2279
|
reason: 'extension_locked'
|
|
@@ -2116,21 +2281,13 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2116
2281
|
this.emit('disconnect', disconnectEvent);
|
|
2117
2282
|
this.logger.log('Extension locked - disconnected');
|
|
2118
2283
|
}
|
|
2119
|
-
/**
|
|
2120
|
-
* Handle extension unlocked event
|
|
2121
|
-
*/
|
|
2122
2284
|
handleExtensionUnlocked() {
|
|
2123
|
-
// emit extensionUnlocked
|
|
2124
2285
|
this.emit('extensionUnlocked', {});
|
|
2125
|
-
// Attempt to restore connection
|
|
2126
2286
|
this.getConnectionStatus().catch(() => {
|
|
2127
2287
|
this.logger.warn('Could not restore connection after unlock');
|
|
2128
2288
|
});
|
|
2129
2289
|
this.logger.log('Extension unlocked');
|
|
2130
2290
|
}
|
|
2131
|
-
/**
|
|
2132
|
-
* Handle transaction confirmed event
|
|
2133
|
-
*/
|
|
2134
2291
|
handleTransactionConfirmed(data) {
|
|
2135
2292
|
this.emit('transactionConfirmed', {
|
|
2136
2293
|
txHash: data.txHash,
|
|
@@ -2143,12 +2300,38 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
2143
2300
|
}, 2000);
|
|
2144
2301
|
this.logger.log('Transaction confirmed:', data.txHash);
|
|
2145
2302
|
}
|
|
2146
|
-
// ===================
|
|
2147
|
-
// CLEANUP
|
|
2148
|
-
// ===================
|
|
2149
2303
|
/**
|
|
2150
|
-
*
|
|
2304
|
+
* The wallet takes raw micro-OCT. `amount` passes through unchanged (what every existing
|
|
2305
|
+
* dapp sends today); `amountOct` is converted exactly. Never both.
|
|
2306
|
+
*/
|
|
2307
|
+
resolveRawAmount(amount, amountOct, label) {
|
|
2308
|
+
if (amount != null && amountOct != null) {
|
|
2309
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, `${label}: pass amount or amountOct, not both`);
|
|
2310
|
+
}
|
|
2311
|
+
if (amountOct != null) {
|
|
2312
|
+
this.assertExactOCTAmount(amountOct, label);
|
|
2313
|
+
return octToMicro(amountOct);
|
|
2314
|
+
}
|
|
2315
|
+
if (amount == null || !isValidAmount(amount)) {
|
|
2316
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, `Invalid ${label.toLowerCase()}`);
|
|
2317
|
+
}
|
|
2318
|
+
return amount;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Reject numeric amounts that cannot be represented exactly in micro-OCT.
|
|
2322
|
+
* e.g. 0.1 + 0.2 = 0.30000000000000004: the extension would sign the wrong value.
|
|
2323
|
+
* String amounts bypass this check (caller is responsible for correctness).
|
|
2151
2324
|
*/
|
|
2325
|
+
assertExactOCTAmount(amount, label) {
|
|
2326
|
+
if (typeof amount === 'number') {
|
|
2327
|
+
const micro = Math.round(amount * 1000000);
|
|
2328
|
+
if (Math.abs(amount - micro / 1000000) > 1e-10) {
|
|
2329
|
+
const suggested = (micro / 1000000).toFixed(6);
|
|
2330
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, `${label} cannot be represented exactly in micro-OCT. ` +
|
|
2331
|
+
`Pass a string instead (e.g. "${suggested}").`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2152
2335
|
cleanup() {
|
|
2153
2336
|
this.communicator.cleanup();
|
|
2154
2337
|
this.removeAllListeners();
|
|
@@ -2170,7 +2353,7 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
2170
2353
|
*
|
|
2171
2354
|
* Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
|
|
2172
2355
|
* window.octra.isOctra === true
|
|
2173
|
-
* window.octra.request({ method, params })
|
|
2356
|
+
* window.octra.request({ method, params }) returns Promise<unknown>
|
|
2174
2357
|
* window.octra.on(event, listener) / removeListener(event, listener)
|
|
2175
2358
|
*
|
|
2176
2359
|
* This adapter translates the SDK's internal method names into RFC-O-1 method
|
|
@@ -2179,25 +2362,35 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
2179
2362
|
* Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
|
|
2180
2363
|
* are passed through as-is; the wallet's request() handles or rejects them.
|
|
2181
2364
|
*/
|
|
2182
|
-
/** SDK method
|
|
2365
|
+
/** SDK method to RFC-O-1 method name */
|
|
2183
2366
|
const SDK_TO_RFC = {
|
|
2184
2367
|
get_network_info: 'octra_networkInfo',
|
|
2185
2368
|
switch_network: 'octra_switchNetwork',
|
|
2186
2369
|
signMessage: 'octra_signMessage',
|
|
2187
2370
|
send_transaction: 'octra_sendTransaction',
|
|
2188
|
-
|
|
2371
|
+
sign_transaction: 'octra_signTransaction',
|
|
2372
|
+
broadcast_only: 'octra_submitTransaction',
|
|
2373
|
+
call_contract: 'octra_sendContractTransaction',
|
|
2189
2374
|
contract_call_view: 'octra_callContract',
|
|
2190
2375
|
get_private_balance_info: 'octra_getEncryptedBalance',
|
|
2191
2376
|
encrypt_balance: 'octra_encryptBalance',
|
|
2192
2377
|
decrypt_balance: 'octra_decryptBalance',
|
|
2193
2378
|
send_private_transfer: 'octra_sendPrivateTransfer',
|
|
2194
2379
|
claim_private_transfer: 'octra_claimStealth',
|
|
2380
|
+
get_private_capabilities: 'octra_getPrivateCapabilities',
|
|
2381
|
+
encrypt_value: 'octra_encryptValue',
|
|
2382
|
+
decrypt_value: 'octra_decryptValue',
|
|
2383
|
+
make_zero_proof: 'octra_makeZeroProof',
|
|
2384
|
+
make_range_proof: 'octra_makeRangeProof',
|
|
2385
|
+
get_private_balance: 'octra_getPrivateBalance',
|
|
2386
|
+
register_private_view_key: 'octra_registerPrivateViewKey',
|
|
2387
|
+
send_contract_transaction_sequence: 'octra_sendContractTransactionSequence',
|
|
2195
2388
|
};
|
|
2196
|
-
/** RFC-O-1 error code
|
|
2389
|
+
/** RFC-O-1 error code to SDK ErrorCode string */
|
|
2197
2390
|
const RFC_TO_SDK_ERROR = {
|
|
2198
2391
|
4001: 'USER_REJECTED',
|
|
2199
2392
|
4100: 'PERMISSION_DENIED',
|
|
2200
|
-
4200: '
|
|
2393
|
+
4200: 'UNKNOWN_ERROR',
|
|
2201
2394
|
4900: 'CONNECTION_REFUSED',
|
|
2202
2395
|
4901: 'NETWORK_ERROR',
|
|
2203
2396
|
};
|
|
@@ -2214,7 +2407,7 @@ function mapError(err) {
|
|
|
2214
2407
|
* three RFC-O-1 calls: octra_requestAccounts, octra_networkInfo, octra_permissions.
|
|
2215
2408
|
*/
|
|
2216
2409
|
async function rfcConnect(provider, params) {
|
|
2217
|
-
const requestPerms = params?.requestPermissions ?? [];
|
|
2410
|
+
const requestPerms = params?.permissions ?? params?.requestPermissions ?? [];
|
|
2218
2411
|
const accounts = (await provider.request({
|
|
2219
2412
|
method: 'octra_requestAccounts',
|
|
2220
2413
|
params: [{ permissions: requestPerms }],
|
|
@@ -2295,19 +2488,21 @@ function createOctraProviderAdapter() {
|
|
|
2295
2488
|
const provider = getProvider();
|
|
2296
2489
|
if (!provider)
|
|
2297
2490
|
return () => { _handler = null; };
|
|
2298
|
-
// RFC-O-1 event
|
|
2491
|
+
// RFC-O-1 event to SDK event name and data shape
|
|
2299
2492
|
const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
|
|
2300
2493
|
const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
|
|
2301
2494
|
const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
|
|
2302
2495
|
const onNetworkChanged = (data) => handler({ eventType: 'networkChanged', eventData: { networkInfo: data } });
|
|
2303
2496
|
const onBalanceChanged = (data) => handler({ eventType: 'balanceChanged', eventData: data });
|
|
2304
2497
|
const onTransactionChanged = (data) => handler({ eventType: 'transactionConfirmed', eventData: data });
|
|
2498
|
+
const onPermissionsChanged = (data) => handler({ eventType: 'permissionsChanged', eventData: data });
|
|
2305
2499
|
provider.on('connect', onConnect);
|
|
2306
2500
|
provider.on('disconnect', onDisconnect);
|
|
2307
2501
|
provider.on('accountsChanged', onAccountsChanged);
|
|
2308
2502
|
provider.on('networkChanged', onNetworkChanged);
|
|
2309
2503
|
provider.on('balanceChanged', onBalanceChanged);
|
|
2310
2504
|
provider.on('transactionChanged', onTransactionChanged);
|
|
2505
|
+
provider.on('permissionsChanged', onPermissionsChanged);
|
|
2311
2506
|
const cleanup = () => {
|
|
2312
2507
|
provider.removeListener('connect', onConnect);
|
|
2313
2508
|
provider.removeListener('disconnect', onDisconnect);
|
|
@@ -2315,6 +2510,7 @@ function createOctraProviderAdapter() {
|
|
|
2315
2510
|
provider.removeListener('networkChanged', onNetworkChanged);
|
|
2316
2511
|
provider.removeListener('balanceChanged', onBalanceChanged);
|
|
2317
2512
|
provider.removeListener('transactionChanged', onTransactionChanged);
|
|
2513
|
+
provider.removeListener('permissionsChanged', onPermissionsChanged);
|
|
2318
2514
|
_handler = null;
|
|
2319
2515
|
};
|
|
2320
2516
|
return cleanup;
|
|
@@ -2322,7 +2518,11 @@ function createOctraProviderAdapter() {
|
|
|
2322
2518
|
listenForReady(onReady) {
|
|
2323
2519
|
const handler = () => onReady();
|
|
2324
2520
|
window.addEventListener('octraWalletReady', handler);
|
|
2325
|
-
|
|
2521
|
+
window.addEventListener('octra#initialized', handler);
|
|
2522
|
+
return () => {
|
|
2523
|
+
window.removeEventListener('octraWalletReady', handler);
|
|
2524
|
+
window.removeEventListener('octra#initialized', handler);
|
|
2525
|
+
};
|
|
2326
2526
|
},
|
|
2327
2527
|
};
|
|
2328
2528
|
}
|
|
@@ -2330,15 +2530,15 @@ function createOctraProviderAdapter() {
|
|
|
2330
2530
|
const OctraProviderAdapter = createOctraProviderAdapter();
|
|
2331
2531
|
|
|
2332
2532
|
/**
|
|
2333
|
-
* 0xio SDK
|
|
2533
|
+
* 0xio SDK: Wallet Adapter Registry
|
|
2334
2534
|
*
|
|
2335
2535
|
* Add new wallet adapters here. Detection order determines which wallet takes
|
|
2336
2536
|
* priority when multiple wallets are installed at the same time.
|
|
2337
2537
|
*/
|
|
2338
2538
|
const REGISTERED_ADAPTERS = [
|
|
2339
|
-
ZeroXIOAdapter, // 0xio extension (postMessage protocol)
|
|
2539
|
+
ZeroXIOAdapter, // 0xio extension (postMessage protocol), highest priority
|
|
2340
2540
|
OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
|
|
2341
|
-
// Add new wallet adapters here
|
|
2541
|
+
// Add new wallet adapters here: detection runs in order, first match wins
|
|
2342
2542
|
];
|
|
2343
2543
|
/**
|
|
2344
2544
|
* Auto-detects the first available wallet in the current page.
|
|
@@ -2385,7 +2585,7 @@ function getAllAdapters() {
|
|
|
2385
2585
|
*/
|
|
2386
2586
|
// Main exports
|
|
2387
2587
|
// Version information
|
|
2388
|
-
const SDK_VERSION = '2.
|
|
2588
|
+
const SDK_VERSION = '2.8.0';
|
|
2389
2589
|
const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
|
|
2390
2590
|
const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
|
|
2391
2591
|
const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
|
|
@@ -2403,8 +2603,7 @@ async function createZeroXIOWallet(config) {
|
|
|
2403
2603
|
try {
|
|
2404
2604
|
await wallet$1.connect();
|
|
2405
2605
|
}
|
|
2406
|
-
catch
|
|
2407
|
-
if (config.debug) ;
|
|
2606
|
+
catch {
|
|
2408
2607
|
// Don't throw - let the app handle connection manually
|
|
2409
2608
|
}
|
|
2410
2609
|
}
|
|
@@ -2414,7 +2613,7 @@ async function createZeroXIOWallet(config) {
|
|
|
2414
2613
|
function checkSDKCompatibility() {
|
|
2415
2614
|
const issues = [];
|
|
2416
2615
|
const recommendations = [];
|
|
2417
|
-
// Hard blockers
|
|
2616
|
+
// Hard blockers: the SDK cannot function without these
|
|
2418
2617
|
if (typeof window === 'undefined') {
|
|
2419
2618
|
issues.push('Window object not available');
|
|
2420
2619
|
recommendations.push('SDK must be used in a browser environment');
|
|
@@ -2479,7 +2678,7 @@ if (typeof window !== 'undefined') {
|
|
|
2479
2678
|
debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
|
|
2480
2679
|
environment: isDevelopment ? 'development' : 'production'
|
|
2481
2680
|
}),
|
|
2482
|
-
// simulateExtensionEvent removed for security
|
|
2681
|
+
// simulateExtensionEvent removed for security: it could be exploited on staging builds
|
|
2483
2682
|
showWelcome: () => {
|
|
2484
2683
|
console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
|
|
2485
2684
|
console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
|
|
@@ -2494,5 +2693,5 @@ if (typeof window !== 'undefined') {
|
|
|
2494
2693
|
}
|
|
2495
2694
|
}
|
|
2496
2695
|
|
|
2497
|
-
export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, OctraProviderAdapter, SDK_CONFIG, SDK_VERSION, SUPPORTED_EXTENSION_VERSIONS, ZeroXIOAdapter, ZeroXIOWallet, ZeroXIOWalletError, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger, createOctraProviderAdapter, createZeroXIOAdapter, createZeroXIOWallet, delay, deriveOctraAddress, detectWalletAdapter, formatAddress, formatOCT, formatTimestamp, formatTxHash, formatOCT as formatZeroXIO, fromMicroOCT, fromMicroOCT as fromMicroZeroXIO, generateMockData, getAllAdapters, getAllNetworks, getDefaultNetwork, getNetworkConfig, isBrowser, isErrorType, isValidAddress, isValidAmount, isValidFeeLevel, isValidMessage, isValidNetworkId, toMicroOCT, toMicroOCT as toMicroZeroXIO };
|
|
2696
|
+
export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, LEGACY_PERMISSION_MAP, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, OctraProviderAdapter, SDK_CONFIG, SDK_VERSION, SIGNED_MESSAGE_PREFIX, SIGNED_MESSAGE_VERSION, SUPPORTED_EXTENSION_VERSIONS, WALLET_PERMISSIONS, ZeroXIOAdapter, ZeroXIOWallet, ZeroXIOWalletError, buildAuthMessage, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger, createOctraProviderAdapter, createZeroXIOAdapter, createZeroXIOWallet, delay, deriveOctraAddress, detectWalletAdapter, formatAddress, formatOCT, formatTimestamp, formatTxHash, formatOCT as formatZeroXIO, fromMicroOCT, fromMicroOCT as fromMicroZeroXIO, generateMockData, getAllAdapters, getAllNetworks, getDefaultNetwork, getNetworkConfig, getSignedMessageBytes, isBrowser, isErrorType, isValidAddress, isValidAmount, isValidFeeLevel, isValidMessage, isValidNetworkId, octToMicro, toMicroOCT, toMicroOCT as toMicroZeroXIO, toWalletPermissions, verifyMessage, withLegacyAliases };
|
|
2498
2697
|
//# sourceMappingURL=index.esm.js.map
|