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