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