@0xio/sdk 2.5.0 → 2.7.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 +66 -0
- package/README.md +54 -2
- package/dist/index.d.ts +302 -187
- package/dist/index.esm.js +983 -473
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +989 -475
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +989 -475
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -35,8 +35,9 @@ class EventEmitter {
|
|
|
35
35
|
*/
|
|
36
36
|
once(eventType, listener) {
|
|
37
37
|
const onceListener = (event) => {
|
|
38
|
-
listener
|
|
38
|
+
// Remove BEFORE calling so a throwing listener doesn't stay registered
|
|
39
39
|
this.off(eventType, onceListener);
|
|
40
|
+
listener(event);
|
|
40
41
|
};
|
|
41
42
|
this.on(eventType, onceListener);
|
|
42
43
|
}
|
|
@@ -137,6 +138,135 @@ class ZeroXIOWalletError extends Error {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* 0xio Wallet transport adapter.
|
|
143
|
+
*
|
|
144
|
+
* Implements the postMessage protocol used by the 0xio browser extension (>= v2.4.0).
|
|
145
|
+
*
|
|
146
|
+
* Outbound wire format:
|
|
147
|
+
* window.postMessage({ source: '0xio-sdk-request', request: { id, method, params, timestamp } }, origin)
|
|
148
|
+
*
|
|
149
|
+
* Inbound wire format:
|
|
150
|
+
* window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
|
|
151
|
+
* window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
|
|
152
|
+
*
|
|
153
|
+
* H-2: Session nonce validation — injected.ts broadcasts the nonce received from the
|
|
154
|
+
* isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
|
|
155
|
+
* mismatched nonce is rejected, preventing response injection by malicious page scripts.
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* Creates a 0xio adapter. The factory accepts optional extra trusted parent origins
|
|
159
|
+
* so the communicator can forward its own trustedOrigins setting to origin validation.
|
|
160
|
+
*/
|
|
161
|
+
function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
162
|
+
return {
|
|
163
|
+
name: '0xio',
|
|
164
|
+
displayName: '0xio Wallet',
|
|
165
|
+
detect() {
|
|
166
|
+
if (typeof window === 'undefined')
|
|
167
|
+
return false;
|
|
168
|
+
const win = window;
|
|
169
|
+
return !!(win.wallet0xio ||
|
|
170
|
+
win.ZeroXIOWallet ||
|
|
171
|
+
// 0xio extension also exposes window.octra with isOctra=true; match it here
|
|
172
|
+
// so this adapter (postMessage protocol) takes priority over OctraProviderAdapter
|
|
173
|
+
(win.octra?.isOctra && (win.wallet0xio || win.ZeroXIOWallet)) ||
|
|
174
|
+
win.chrome?.runtime?.id ||
|
|
175
|
+
document.querySelector('meta[name="0xio-dapp"]') ||
|
|
176
|
+
document.querySelector('[data-0xio-sdk-bridge]'));
|
|
177
|
+
},
|
|
178
|
+
postRequest(request) {
|
|
179
|
+
window.postMessage({ source: '0xio-sdk-request', request }, window.location.origin);
|
|
180
|
+
},
|
|
181
|
+
postRequestToParent(request, parentOrigin) {
|
|
182
|
+
try {
|
|
183
|
+
window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Do not fall back to '*' — silent failure is safer
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
listen(handler, options) {
|
|
190
|
+
const allowedOrigin = window.location.origin;
|
|
191
|
+
const trustedParentOrigins = new Set([
|
|
192
|
+
allowedOrigin,
|
|
193
|
+
'tauri://localhost',
|
|
194
|
+
'https://tauri.localhost',
|
|
195
|
+
'http://localhost',
|
|
196
|
+
'https://localhost',
|
|
197
|
+
...(extraTrustedOrigins),
|
|
198
|
+
...(options?.trustedParentOrigins ?? []),
|
|
199
|
+
]);
|
|
200
|
+
let _sessionNonce = null;
|
|
201
|
+
// H-2: receive session nonce from injected.ts (MAIN world content script)
|
|
202
|
+
const nonceListener = (e) => {
|
|
203
|
+
if (e.origin !== allowedOrigin)
|
|
204
|
+
return;
|
|
205
|
+
if (e.data?.source === '0xio-sdk-nonce-init' && typeof e.data.nonce === 'string') {
|
|
206
|
+
_sessionNonce = e.data.nonce;
|
|
207
|
+
window.removeEventListener('message', nonceListener);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
window.addEventListener('message', nonceListener);
|
|
211
|
+
const msgListener = (e) => {
|
|
212
|
+
const isFromSameOrigin = e.origin === allowedOrigin;
|
|
213
|
+
const isLocalhost = e.origin.startsWith('http://localhost:') ||
|
|
214
|
+
e.origin.startsWith('http://127.0.0.1:');
|
|
215
|
+
const isFromTrustedParent = e.source === window.parent &&
|
|
216
|
+
window.parent !== window &&
|
|
217
|
+
(trustedParentOrigins.has(e.origin) || isLocalhost);
|
|
218
|
+
if (!isFromSameOrigin && !isFromTrustedParent)
|
|
219
|
+
return;
|
|
220
|
+
if (e.source !== window && e.source !== window.parent)
|
|
221
|
+
return;
|
|
222
|
+
if (!e.data || e.data.source !== '0xio-sdk-bridge')
|
|
223
|
+
return;
|
|
224
|
+
// H-2: session nonce validation.
|
|
225
|
+
// Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
|
|
226
|
+
// Fallback path: if the init broadcast was missed (race between document_start
|
|
227
|
+
// content script and page script load), capture nonce from the first same-origin
|
|
228
|
+
// response so that all subsequent responses are validated.
|
|
229
|
+
if (!_sessionNonce && isFromSameOrigin && typeof e.data.sessionNonce === 'string') {
|
|
230
|
+
_sessionNonce = e.data.sessionNonce;
|
|
231
|
+
}
|
|
232
|
+
if (_sessionNonce && e.data.sessionNonce !== _sessionNonce)
|
|
233
|
+
return;
|
|
234
|
+
if (e.data.response) {
|
|
235
|
+
const r = e.data.response;
|
|
236
|
+
handler({
|
|
237
|
+
requestId: r.id,
|
|
238
|
+
success: r.success,
|
|
239
|
+
data: r.data,
|
|
240
|
+
error: r.error,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else if (e.data.event) {
|
|
244
|
+
handler({
|
|
245
|
+
eventType: e.data.event.type,
|
|
246
|
+
eventData: e.data.event.data ?? e.data.event,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
window.addEventListener('message', msgListener);
|
|
251
|
+
return () => {
|
|
252
|
+
window.removeEventListener('message', nonceListener);
|
|
253
|
+
window.removeEventListener('message', msgListener);
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
listenForReady(onReady) {
|
|
257
|
+
const handler = () => onReady();
|
|
258
|
+
window.addEventListener('0xioWalletReady', handler);
|
|
259
|
+
window.addEventListener('wallet0xioReady', handler);
|
|
260
|
+
return () => {
|
|
261
|
+
window.removeEventListener('0xioWalletReady', handler);
|
|
262
|
+
window.removeEventListener('wallet0xioReady', handler);
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/** Default 0xio adapter instance (no extra trusted origins). */
|
|
268
|
+
const ZeroXIOAdapter = createZeroXIOAdapter();
|
|
269
|
+
|
|
140
270
|
/**
|
|
141
271
|
* 0xio Wallet SDK - Utilities
|
|
142
272
|
* Helper functions for validation, formatting, and common operations
|
|
@@ -156,9 +286,15 @@ function isValidAddress(address) {
|
|
|
156
286
|
return addressRegex.test(address);
|
|
157
287
|
}
|
|
158
288
|
/**
|
|
159
|
-
* Validate transaction amount
|
|
289
|
+
* Validate transaction amount.
|
|
290
|
+
* Accepts both number and string representations.
|
|
291
|
+
* String amounts avoid JS number precision loss for very large values.
|
|
160
292
|
*/
|
|
161
293
|
function isValidAmount(amount) {
|
|
294
|
+
if (typeof amount === 'string') {
|
|
295
|
+
const n = parseFloat(amount);
|
|
296
|
+
return !isNaN(n) && n > 0 && Number.isFinite(n);
|
|
297
|
+
}
|
|
162
298
|
return typeof amount === 'number' &&
|
|
163
299
|
amount > 0 &&
|
|
164
300
|
Number.isFinite(amount) &&
|
|
@@ -168,11 +304,13 @@ function isValidAmount(amount) {
|
|
|
168
304
|
* Validate transaction message
|
|
169
305
|
*/
|
|
170
306
|
function isValidMessage(message) {
|
|
171
|
-
|
|
172
|
-
return true; // Empty messages are valid
|
|
173
|
-
}
|
|
307
|
+
// Type check first — falsy non-strings (0, false, null) are NOT valid messages
|
|
174
308
|
if (typeof message !== 'string') {
|
|
175
|
-
return false;
|
|
309
|
+
return message === undefined || message === null ? true : false;
|
|
310
|
+
}
|
|
311
|
+
// Empty string is valid (optional field)
|
|
312
|
+
if (message.length === 0) {
|
|
313
|
+
return true;
|
|
176
314
|
}
|
|
177
315
|
// 100KB limit — contract call params can be large (serialized JSON)
|
|
178
316
|
return message.length <= 100000;
|
|
@@ -184,16 +322,64 @@ function isValidFeeLevel(feeLevel) {
|
|
|
184
322
|
return feeLevel === 1 || feeLevel === 3;
|
|
185
323
|
}
|
|
186
324
|
// ===================
|
|
325
|
+
// ADDRESS DERIVATION
|
|
326
|
+
// ===================
|
|
327
|
+
const _B58_ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
328
|
+
function _base58Encode(buf) {
|
|
329
|
+
let zeros = 0;
|
|
330
|
+
for (let i = 0; i < buf.length && buf[i] === 0; i++)
|
|
331
|
+
zeros++;
|
|
332
|
+
const digits = [];
|
|
333
|
+
for (let i = zeros; i < buf.length; i++) {
|
|
334
|
+
let carry = buf[i];
|
|
335
|
+
for (let j = 0; j < digits.length; j++) {
|
|
336
|
+
carry += digits[j] << 8;
|
|
337
|
+
digits[j] = carry % 58;
|
|
338
|
+
carry = Math.floor(carry / 58);
|
|
339
|
+
}
|
|
340
|
+
while (carry > 0) {
|
|
341
|
+
digits.push(carry % 58);
|
|
342
|
+
carry = Math.floor(carry / 58);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
let out = '';
|
|
346
|
+
for (let i = 0; i < zeros; i++)
|
|
347
|
+
out += _B58_ALPHA[0];
|
|
348
|
+
for (let i = digits.length - 1; i >= 0; i--)
|
|
349
|
+
out += _B58_ALPHA[digits[i]];
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Derive the canonical Octra address from a base64-encoded Ed25519 public key.
|
|
354
|
+
* Algorithm: SHA-256(pubkey_bytes) → base58 → prepend "oct"
|
|
355
|
+
* Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
|
|
356
|
+
*/
|
|
357
|
+
async function deriveOctraAddress(publicKeyBase64) {
|
|
358
|
+
if (!publicKeyBase64 || typeof publicKeyBase64 !== 'string') {
|
|
359
|
+
throw new Error('publicKeyBase64 must be a non-empty string');
|
|
360
|
+
}
|
|
361
|
+
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
362
|
+
throw new Error('Web Crypto API is not available in this environment');
|
|
363
|
+
}
|
|
364
|
+
const binStr = atob(publicKeyBase64);
|
|
365
|
+
const bytes = new Uint8Array(binStr.length);
|
|
366
|
+
for (let i = 0; i < binStr.length; i++)
|
|
367
|
+
bytes[i] = binStr.charCodeAt(i);
|
|
368
|
+
const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
|
|
369
|
+
return 'oct' + _base58Encode(new Uint8Array(hashBuf));
|
|
370
|
+
}
|
|
371
|
+
// ===================
|
|
187
372
|
// FORMATTING UTILITIES
|
|
188
373
|
// ===================
|
|
189
374
|
/**
|
|
190
375
|
* Format OCT amount for display
|
|
191
376
|
*/
|
|
192
377
|
function formatOCT(amount, decimals = 6) {
|
|
193
|
-
|
|
378
|
+
const n = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
379
|
+
if (!isValidAmount(n)) {
|
|
194
380
|
return '0';
|
|
195
381
|
}
|
|
196
|
-
return
|
|
382
|
+
return n.toLocaleString(undefined, {
|
|
197
383
|
minimumFractionDigits: 0,
|
|
198
384
|
maximumFractionDigits: decimals
|
|
199
385
|
});
|
|
@@ -395,7 +581,7 @@ function generateMockData() {
|
|
|
395
581
|
networkInfo: {
|
|
396
582
|
id: 'mainnet',
|
|
397
583
|
name: 'Octra Mainnet',
|
|
398
|
-
rpcUrl: '
|
|
584
|
+
rpcUrl: 'https://octra.network',
|
|
399
585
|
explorerUrl: 'https://lite.octrascan.io/tx.html?hash=',
|
|
400
586
|
explorerAddressUrl: 'https://lite.octrascan.io/address.html?addr=',
|
|
401
587
|
indexerUrl: 'https://lite.octrascan.io',
|
|
@@ -468,108 +654,54 @@ function createLogger(prefix, debug) {
|
|
|
468
654
|
* to ensure secure wallet interactions.
|
|
469
655
|
*
|
|
470
656
|
* @module communication
|
|
471
|
-
* @version 2.
|
|
657
|
+
* @version 2.7.0
|
|
472
658
|
* @license MIT
|
|
473
659
|
*/
|
|
474
|
-
/**
|
|
475
|
-
* ExtensionCommunicator - Manages communication with the 0xio Wallet browser extension
|
|
476
|
-
*
|
|
477
|
-
* @class
|
|
478
|
-
* @extends EventEmitter
|
|
479
|
-
*
|
|
480
|
-
* @description
|
|
481
|
-
* Handles all communication between the SDK and wallet extension including:
|
|
482
|
-
* - Request/response message passing with origin validation
|
|
483
|
-
* - Rate limiting to prevent DoS attacks
|
|
484
|
-
* - Automatic retry logic with exponential backoff
|
|
485
|
-
* - Extension detection and availability monitoring
|
|
486
|
-
* - Cryptographically secure request ID generation
|
|
487
|
-
*
|
|
488
|
-
* @example
|
|
489
|
-
* ```typescript
|
|
490
|
-
* const communicator = new ExtensionCommunicator(true); // debug mode
|
|
491
|
-
* await communicator.initialize();
|
|
492
|
-
*
|
|
493
|
-
* const response = await communicator.sendRequest('get_balance', {});
|
|
494
|
-
* console.log(response);
|
|
495
|
-
* ```
|
|
496
|
-
*/
|
|
497
660
|
class ExtensionCommunicator extends EventEmitter {
|
|
498
|
-
|
|
499
|
-
* Creates a new ExtensionCommunicator instance
|
|
500
|
-
*
|
|
501
|
-
* @param {boolean} debug - Enable debug logging
|
|
502
|
-
*/
|
|
503
|
-
constructor(debug = false, trustedOrigins = []) {
|
|
661
|
+
constructor(debug = false, trustedOrigins = [], adapter) {
|
|
504
662
|
super(debug);
|
|
505
|
-
/** Legacy request counter (deprecated, kept for fallback) */
|
|
506
|
-
this.requestId = 0;
|
|
507
|
-
/** Map of pending requests awaiting responses */
|
|
508
663
|
this.pendingRequests = new Map();
|
|
509
|
-
/** Initialization state flag */
|
|
510
664
|
this.isInitialized = false;
|
|
511
|
-
/** Interval handle for periodic extension detection */
|
|
512
665
|
this.extensionDetectionInterval = null;
|
|
513
|
-
/** Current extension availability state */
|
|
514
666
|
this.isExtensionAvailableState = false;
|
|
515
|
-
/** Message listener reference for cleanup */
|
|
516
|
-
this.messageListener = null;
|
|
517
|
-
/** Trusted parent origins for iframe communication */
|
|
518
667
|
this.trustedOrigins = [];
|
|
519
|
-
/** Parent origin learned from walletReady signal */
|
|
520
668
|
this._parentOrigin = null;
|
|
521
|
-
|
|
522
|
-
|
|
669
|
+
/** Teardown fn returned by adapter.listen() */
|
|
670
|
+
this._adapterTeardown = null;
|
|
671
|
+
/** Teardown fn returned by adapter.listenForReady() */
|
|
672
|
+
this._adapterReadyTeardown = null;
|
|
673
|
+
/**
|
|
674
|
+
* Set when a trusted walletReady has been received from window.parent.
|
|
675
|
+
* The polling fallback must NOT clear this flag.
|
|
676
|
+
*/
|
|
677
|
+
this._parentTrusted = false;
|
|
678
|
+
/** walletReady postMessage listener stored for cleanup */
|
|
679
|
+
this._walletReadyMessageListener = null;
|
|
680
|
+
/**
|
|
681
|
+
* In-flight interactive request lock.
|
|
682
|
+
* Methods that open approval popups are serialized — only one at a time.
|
|
683
|
+
*/
|
|
684
|
+
this._interactiveInFlight = false;
|
|
523
685
|
this.MAX_CONCURRENT_REQUESTS = 50;
|
|
524
|
-
/** Time window for rate limiting (milliseconds) */
|
|
525
686
|
this.RATE_LIMIT_WINDOW = 1000;
|
|
526
|
-
/** Maximum requests allowed per time window */
|
|
527
687
|
this.MAX_REQUESTS_PER_WINDOW = 20;
|
|
528
|
-
/** Timestamps of recent requests for rate limiting */
|
|
529
688
|
this.requestTimestamps = [];
|
|
530
689
|
this.logger = createLogger('ExtensionCommunicator', debug);
|
|
531
690
|
this.trustedOrigins = trustedOrigins;
|
|
691
|
+
this.adapter = adapter ?? createZeroXIOAdapter(trustedOrigins);
|
|
532
692
|
this.setupMessageListener();
|
|
533
693
|
this.startExtensionDetection();
|
|
534
694
|
}
|
|
535
|
-
/**
|
|
536
|
-
* Add trusted origins for iframe/bridge communication
|
|
537
|
-
* Call this before connecting if your dApp runs inside a trusted frame
|
|
538
|
-
*/
|
|
539
695
|
setTrustedOrigins(origins) {
|
|
540
696
|
this.trustedOrigins = origins;
|
|
541
697
|
}
|
|
542
|
-
/**
|
|
543
|
-
* Initialize communication with the wallet extension
|
|
544
|
-
*
|
|
545
|
-
* @description
|
|
546
|
-
* Performs initial setup and verification:
|
|
547
|
-
* 1. Waits for extension to become available
|
|
548
|
-
* 2. Sends ping to verify communication
|
|
549
|
-
* 3. Establishes message handlers
|
|
550
|
-
*
|
|
551
|
-
* Must be called before any other methods.
|
|
552
|
-
*
|
|
553
|
-
* @returns {Promise<boolean>} True if initialization succeeded, false otherwise
|
|
554
|
-
* @throws {ZeroXIOWalletError} If extension is not available after timeout
|
|
555
|
-
*
|
|
556
|
-
* @example
|
|
557
|
-
* ```typescript
|
|
558
|
-
* const success = await communicator.initialize();
|
|
559
|
-
* if (!success) {
|
|
560
|
-
* console.error('Failed to initialize wallet connection');
|
|
561
|
-
* }
|
|
562
|
-
* ```
|
|
563
|
-
*/
|
|
564
698
|
async initialize() {
|
|
565
699
|
if (this.isInitialized) {
|
|
566
700
|
return true;
|
|
567
701
|
}
|
|
568
702
|
try {
|
|
569
|
-
// Wait for extension detection with timeout
|
|
570
703
|
const available = await this.waitForExtensionAvailability(10000);
|
|
571
704
|
if (available) {
|
|
572
|
-
// Verify with ping
|
|
573
705
|
await withTimeout(this.sendRequestWithRetry('ping', {}, 3, 2000), 8000, 'Extension ping timeout during initialization');
|
|
574
706
|
this.isInitialized = true;
|
|
575
707
|
this.logger.log('Extension communication initialized successfully');
|
|
@@ -584,149 +716,106 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
584
716
|
return false;
|
|
585
717
|
}
|
|
586
718
|
}
|
|
587
|
-
/**
|
|
588
|
-
* Check if extension is available
|
|
589
|
-
*/
|
|
590
719
|
isExtensionAvailable() {
|
|
591
720
|
return this.isExtensionAvailableState && this.hasExtensionContext();
|
|
592
721
|
}
|
|
593
|
-
/**
|
|
594
|
-
* Send request to extension
|
|
595
|
-
*/
|
|
596
722
|
async sendRequest(method, params = {}, timeout = 30000) {
|
|
597
723
|
const isInteractive = ExtensionCommunicator.NO_RETRY_METHODS.has(method);
|
|
598
724
|
const maxRetries = isInteractive ? 0 : 1;
|
|
599
|
-
// Give users 3 minutes for interactive approvals (review + sign + FHE proof generation)
|
|
600
725
|
const effectiveTimeout = isInteractive ? Math.max(timeout, 180000) : timeout;
|
|
601
726
|
return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
|
|
602
727
|
}
|
|
603
|
-
/**
|
|
604
|
-
* Send request to extension with automatic retry logic
|
|
605
|
-
*/
|
|
606
728
|
async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
|
|
607
729
|
if (!this.hasExtensionContext()) {
|
|
608
|
-
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, '0xio Wallet extension is not installed or available', {
|
|
609
|
-
method,
|
|
610
|
-
params,
|
|
611
|
-
browserContext: this.getBrowserDiagnostics()
|
|
612
|
-
});
|
|
730
|
+
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, '0xio Wallet extension is not installed or available', { method, browserContext: this.getBrowserDiagnostics() });
|
|
613
731
|
}
|
|
614
732
|
if (!this.isExtensionAvailableState) {
|
|
615
|
-
// Wait a bit for extension to become available
|
|
616
733
|
await this.waitForExtensionAvailability(5000);
|
|
617
734
|
if (!this.isExtensionAvailableState) {
|
|
618
|
-
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Extension not available for communication', {
|
|
619
|
-
method,
|
|
620
|
-
params,
|
|
621
|
-
extensionState: this.getExtensionDiagnostics()
|
|
622
|
-
});
|
|
735
|
+
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Extension not available for communication', { method, extensionState: this.getExtensionDiagnostics() });
|
|
623
736
|
}
|
|
624
737
|
}
|
|
625
|
-
//
|
|
738
|
+
// Enforce one-at-a-time for interactive popup methods
|
|
739
|
+
const isInteractive = ExtensionCommunicator.INTERACTIVE_METHODS.has(method);
|
|
740
|
+
if (isInteractive) {
|
|
741
|
+
if (this._interactiveInFlight) {
|
|
742
|
+
throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, 'Another approval popup is already open. Please wait for it to complete.');
|
|
743
|
+
}
|
|
744
|
+
this._interactiveInFlight = true;
|
|
745
|
+
}
|
|
626
746
|
this.checkRateLimit();
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
747
|
+
try {
|
|
748
|
+
return await retry(async () => {
|
|
749
|
+
const requestId = this.generateRequestId();
|
|
750
|
+
const request = {
|
|
751
|
+
id: requestId,
|
|
752
|
+
method,
|
|
753
|
+
params,
|
|
754
|
+
timestamp: Date.now()
|
|
755
|
+
};
|
|
756
|
+
this.logger.log(`Sending request (${method}):`, { id: requestId });
|
|
757
|
+
return new Promise((resolve, reject) => {
|
|
758
|
+
const timeoutHandle = setTimeout(() => {
|
|
759
|
+
const pending = this.pendingRequests.get(requestId);
|
|
760
|
+
if (pending) {
|
|
761
|
+
this.pendingRequests.delete(requestId);
|
|
762
|
+
reject(new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, `Request timeout after ${timeout}ms`, { method, requestId, retryCount: pending.retryCount }));
|
|
763
|
+
}
|
|
764
|
+
}, timeout);
|
|
765
|
+
this.pendingRequests.set(requestId, {
|
|
766
|
+
resolve,
|
|
767
|
+
reject,
|
|
768
|
+
timeout: timeoutHandle,
|
|
769
|
+
retryCount: 0
|
|
770
|
+
});
|
|
771
|
+
// Wrap postMessage so a DataCloneError cleans up the pending entry
|
|
772
|
+
try {
|
|
773
|
+
this.postMessageToExtension(request);
|
|
774
|
+
}
|
|
775
|
+
catch (cloneErr) {
|
|
776
|
+
clearTimeout(timeoutHandle);
|
|
641
777
|
this.pendingRequests.delete(requestId);
|
|
642
|
-
reject(new ZeroXIOWalletError(ErrorCode.
|
|
643
|
-
method,
|
|
644
|
-
params,
|
|
645
|
-
requestId,
|
|
646
|
-
retryCount: pending.retryCount,
|
|
647
|
-
extensionState: this.getExtensionDiagnostics()
|
|
648
|
-
}));
|
|
778
|
+
reject(new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Request params are not serializable', { method, requestId }));
|
|
649
779
|
}
|
|
650
|
-
}, timeout);
|
|
651
|
-
// Store request handlers
|
|
652
|
-
this.pendingRequests.set(requestId, {
|
|
653
|
-
resolve,
|
|
654
|
-
reject,
|
|
655
|
-
timeout: timeoutHandle,
|
|
656
|
-
retryCount: 0
|
|
657
780
|
});
|
|
658
|
-
|
|
659
|
-
this.postMessageToExtension(request);
|
|
660
|
-
});
|
|
661
|
-
}, maxRetries, 1000);
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Setup message listener for responses from extension
|
|
665
|
-
*/
|
|
666
|
-
setupMessageListener() {
|
|
667
|
-
if (typeof window === 'undefined') {
|
|
668
|
-
return; // Not in browser environment
|
|
669
|
-
}
|
|
670
|
-
const allowedOrigin = window.location.origin;
|
|
671
|
-
// Store allowed origins for parent frame communication
|
|
672
|
-
// Desktop (Tauri): tauri://localhost or https://tauri.localhost
|
|
673
|
-
// Mobile (Expo): about:blank or custom scheme
|
|
674
|
-
const trustedParentOrigins = new Set([
|
|
675
|
-
allowedOrigin,
|
|
676
|
-
'tauri://localhost',
|
|
677
|
-
'https://tauri.localhost',
|
|
678
|
-
'http://localhost',
|
|
679
|
-
'https://localhost',
|
|
680
|
-
]);
|
|
681
|
-
// Allow dApps to register additional trusted origins
|
|
682
|
-
if (this.trustedOrigins) {
|
|
683
|
-
for (const origin of this.trustedOrigins) {
|
|
684
|
-
trustedParentOrigins.add(origin);
|
|
685
|
-
}
|
|
781
|
+
}, maxRetries, 1000);
|
|
686
782
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
|
|
691
|
-
const isFromTrustedParent = event.source === window.parent
|
|
692
|
-
&& window.parent !== window
|
|
693
|
-
&& (trustedParentOrigins.has(event.origin) || isLocalhost);
|
|
694
|
-
if (!isFromSameOrigin && !isFromTrustedParent) {
|
|
695
|
-
return;
|
|
783
|
+
finally {
|
|
784
|
+
if (isInteractive) {
|
|
785
|
+
this._interactiveInFlight = false;
|
|
696
786
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
setupMessageListener() {
|
|
790
|
+
if (typeof window === 'undefined')
|
|
791
|
+
return;
|
|
792
|
+
this._adapterTeardown = this.adapter.listen((msg) => {
|
|
793
|
+
if (msg.requestId !== undefined) {
|
|
794
|
+
// response — map AdapterIncomingMessage → ExtensionResponse shape
|
|
795
|
+
if (this.pendingRequests.has(msg.requestId)) {
|
|
796
|
+
this.handleExtensionResponse({
|
|
797
|
+
id: msg.requestId,
|
|
798
|
+
success: msg.success ?? false,
|
|
799
|
+
data: msg.data,
|
|
800
|
+
error: msg.error,
|
|
801
|
+
timestamp: Date.now(),
|
|
802
|
+
});
|
|
710
803
|
}
|
|
711
804
|
}
|
|
712
|
-
else if (
|
|
713
|
-
this.handleExtensionEvent(
|
|
805
|
+
else if (msg.eventType) {
|
|
806
|
+
this.handleExtensionEvent({ type: msg.eventType, data: msg.eventData });
|
|
714
807
|
}
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
this.logger.log('Message listener setup complete');
|
|
808
|
+
}, { trustedParentOrigins: this.trustedOrigins });
|
|
809
|
+
this.logger.log(`Message listener setup complete (adapter: ${this.adapter.name})`);
|
|
718
810
|
}
|
|
719
|
-
/**
|
|
720
|
-
* Handle extension event
|
|
721
|
-
*/
|
|
722
811
|
handleExtensionEvent(event) {
|
|
812
|
+
if (!event?.type || !ExtensionCommunicator.VALID_EVENT_TYPES.has(event.type)) {
|
|
813
|
+
this.logger.warn(`Received unknown event type from bridge: ${event?.type}`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
723
816
|
this.logger.log('Received extension event:', event.type);
|
|
724
|
-
// Forward the event to listeners
|
|
725
817
|
this.emit(event.type, event.data);
|
|
726
818
|
}
|
|
727
|
-
/**
|
|
728
|
-
* Handle response from extension
|
|
729
|
-
*/
|
|
730
819
|
handleExtensionResponse(response) {
|
|
731
820
|
this.logger.log(`Received response:`, { id: response.id, success: response.success });
|
|
732
821
|
const pending = this.pendingRequests.get(response.id);
|
|
@@ -734,192 +823,131 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
734
823
|
this.logger.warn(`Received response for unknown request ID: ${response.id}`);
|
|
735
824
|
return;
|
|
736
825
|
}
|
|
737
|
-
// Clear timeout and remove from pending
|
|
738
826
|
clearTimeout(pending.timeout);
|
|
739
827
|
this.pendingRequests.delete(response.id);
|
|
740
|
-
//
|
|
741
|
-
if (response.success) {
|
|
828
|
+
// Require strict boolean true — "false" string or other truthy values are failures
|
|
829
|
+
if (response.success === true) {
|
|
742
830
|
pending.resolve(response.data);
|
|
743
831
|
}
|
|
744
832
|
else {
|
|
745
833
|
const error = response.error;
|
|
746
834
|
if (error) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
835
|
+
// Handle both object {code, message} and legacy plain-string error formats
|
|
836
|
+
const isObj = error !== null && typeof error === 'object';
|
|
837
|
+
const code = isObj && error.code ? error.code : ErrorCode.UNKNOWN_ERROR;
|
|
838
|
+
const message = isObj
|
|
839
|
+
? (error.message ?? 'Unknown error')
|
|
840
|
+
: (typeof error === 'string' ? error : 'Unknown error');
|
|
841
|
+
const enhancedError = new ZeroXIOWalletError(code, message,
|
|
842
|
+
// Redact bridge-supplied error details; only keep non-sensitive metadata
|
|
843
|
+
{ requestId: response.id, retryCount: pending.retryCount, timestamp: Date.now() });
|
|
754
844
|
pending.reject(enhancedError);
|
|
755
845
|
}
|
|
756
846
|
else {
|
|
757
|
-
pending.reject(new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Unknown error occurred', {
|
|
758
|
-
requestId: response.id,
|
|
759
|
-
retryCount: pending.retryCount,
|
|
760
|
-
extensionState: this.getExtensionDiagnostics()
|
|
761
|
-
}));
|
|
847
|
+
pending.reject(new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Unknown error occurred', { requestId: response.id, retryCount: pending.retryCount }));
|
|
762
848
|
}
|
|
763
849
|
}
|
|
764
850
|
}
|
|
765
|
-
/**
|
|
766
|
-
* Post message to extension via content script
|
|
767
|
-
*/
|
|
768
851
|
postMessageToExtension(request) {
|
|
769
|
-
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
// Response validation is done by request ID matching, not origin
|
|
776
|
-
const parentOrigin = this._parentOrigin || '*';
|
|
777
|
-
try {
|
|
778
|
-
window.parent.postMessage(msg, parentOrigin);
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
// Fallback to wildcard if specific origin fails
|
|
782
|
-
try {
|
|
783
|
-
window.parent.postMessage(msg, '*');
|
|
784
|
-
}
|
|
785
|
-
catch { }
|
|
852
|
+
this.adapter.postRequest(request);
|
|
853
|
+
// Parent bridge (iframe/desktop mode) — only when a trusted origin is established.
|
|
854
|
+
// Sending with '*' would leak method + params to any intercepting frame.
|
|
855
|
+
if (window.parent !== window && this._parentOrigin) {
|
|
856
|
+
if (this.adapter.postRequestToParent) {
|
|
857
|
+
this.adapter.postRequestToParent(request, this._parentOrigin);
|
|
786
858
|
}
|
|
787
859
|
}
|
|
788
860
|
}
|
|
789
|
-
/**
|
|
790
|
-
* Check if we're in a context that can communicate with extension
|
|
791
|
-
*/
|
|
792
861
|
hasExtensionContext() {
|
|
793
862
|
return typeof window !== 'undefined' &&
|
|
794
863
|
typeof window.postMessage === 'function';
|
|
795
864
|
}
|
|
796
|
-
/**
|
|
797
|
-
* Check and enforce rate limits to prevent denial-of-service attacks
|
|
798
|
-
*
|
|
799
|
-
* @private
|
|
800
|
-
* @throws {ZeroXIOWalletError} RATE_LIMIT_EXCEEDED if limits are exceeded
|
|
801
|
-
*
|
|
802
|
-
* @description
|
|
803
|
-
* Implements two-tier rate limiting:
|
|
804
|
-
* 1. Concurrent requests: Maximum 50 pending requests at once
|
|
805
|
-
* 2. Request frequency: Maximum 20 requests per second
|
|
806
|
-
*
|
|
807
|
-
* Rate limiting protects both the SDK and extension from:
|
|
808
|
-
* - Accidental infinite loops in dApp code
|
|
809
|
-
* - Malicious DoS attacks
|
|
810
|
-
* - Resource exhaustion
|
|
811
|
-
*
|
|
812
|
-
* @security Critical security function - enforces resource limits
|
|
813
|
-
*/
|
|
814
865
|
checkRateLimit() {
|
|
815
866
|
const now = Date.now();
|
|
816
|
-
// ✅ SECURITY: Check concurrent request limit
|
|
817
867
|
if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
|
|
818
868
|
throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
|
|
819
869
|
}
|
|
820
|
-
//
|
|
870
|
+
// Trim expired timestamps — cap array size to prevent unbounded growth in idle tabs
|
|
821
871
|
this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
|
|
872
|
+
if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
|
|
873
|
+
this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
|
|
874
|
+
}
|
|
822
875
|
if (this.requestTimestamps.length >= this.MAX_REQUESTS_PER_WINDOW) {
|
|
823
876
|
throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, `Too many requests per second (max: ${this.MAX_REQUESTS_PER_WINDOW} per ${this.RATE_LIMIT_WINDOW}ms)`);
|
|
824
877
|
}
|
|
825
878
|
this.requestTimestamps.push(now);
|
|
826
879
|
}
|
|
827
|
-
/**
|
|
828
|
-
* Generate cryptographically secure unique request ID
|
|
829
|
-
*
|
|
830
|
-
* @private
|
|
831
|
-
* @returns {string} A unique, unpredictable request identifier
|
|
832
|
-
*
|
|
833
|
-
* @description
|
|
834
|
-
* Uses Web Crypto API for secure random ID generation:
|
|
835
|
-
* 1. Primary: crypto.randomUUID() - UUID v4 format
|
|
836
|
-
* 2. Fallback: crypto.getRandomValues() - 128-bit random hex
|
|
837
|
-
* 3. Last resort: timestamp + counter (logs warning)
|
|
838
|
-
*
|
|
839
|
-
* Security importance:
|
|
840
|
-
* - Prevents request ID prediction attacks
|
|
841
|
-
* - Mitigates replay attacks
|
|
842
|
-
* - Makes session hijacking more difficult
|
|
843
|
-
*
|
|
844
|
-
* @security Critical - IDs must be cryptographically unpredictable
|
|
845
|
-
*/
|
|
846
880
|
generateRequestId() {
|
|
847
|
-
// ✅ SECURITY: Use crypto.randomUUID() for secure, unpredictable IDs
|
|
848
881
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
849
882
|
return `0xio-sdk-${crypto.randomUUID()}`;
|
|
850
883
|
}
|
|
851
|
-
// Fallback to crypto.getRandomValues for older browsers
|
|
852
884
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
853
885
|
const array = new Uint8Array(16);
|
|
854
886
|
crypto.getRandomValues(array);
|
|
855
887
|
const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
856
888
|
return `0xio-sdk-${hex}`;
|
|
857
889
|
}
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
890
|
+
// Crypto API unavailable — throw rather than produce a guessable ID that
|
|
891
|
+
// could allow response spoofing via a known requestId.
|
|
892
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
|
|
861
893
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Start continuous extension detection
|
|
864
|
-
*/
|
|
865
894
|
startExtensionDetection() {
|
|
866
895
|
if (typeof window === 'undefined')
|
|
867
896
|
return;
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
this.
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
else if (event.origin && event.origin !== 'null') {
|
|
897
|
-
this._parentOrigin = event.origin;
|
|
898
|
-
}
|
|
897
|
+
// Adapter-provided wallet-ready events (e.g. '0xioWalletReady', 'exampleWalletReady')
|
|
898
|
+
if (this.adapter.listenForReady) {
|
|
899
|
+
this._adapterReadyTeardown = this.adapter.listenForReady(() => {
|
|
900
|
+
this.logger.log(`Received wallet-ready event (adapter: ${this.adapter.name})`);
|
|
901
|
+
this.isExtensionAvailableState = true;
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
// walletReady via postMessage (desktop/mobile iframe bridge) — store ref for cleanup
|
|
905
|
+
this._walletReadyMessageListener = (event) => {
|
|
906
|
+
if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const isSameOrigin = event.origin === window.location.origin;
|
|
910
|
+
const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
|
|
911
|
+
const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
|
|
912
|
+
const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhost;
|
|
913
|
+
// In iframe mode, only trust the actual parent window
|
|
914
|
+
const inIframe = window.parent !== window;
|
|
915
|
+
if (inIframe && event.source !== window.parent) {
|
|
916
|
+
this.logger.warn(`Ignored walletReady from non-parent source in iframe mode`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (isSameOrigin || isTrustedOrigin) {
|
|
920
|
+
this.logger.log('Received walletReady via postMessage from trusted origin');
|
|
921
|
+
this.isExtensionAvailableState = true;
|
|
922
|
+
this._parentTrusted = true; // Mark parent-bridge readiness separately
|
|
923
|
+
if (event.data.parentOrigin) {
|
|
924
|
+
this._parentOrigin = event.data.parentOrigin;
|
|
899
925
|
}
|
|
900
|
-
else {
|
|
901
|
-
this.
|
|
926
|
+
else if (event.origin && event.origin !== 'null') {
|
|
927
|
+
this._parentOrigin = event.origin;
|
|
902
928
|
}
|
|
903
929
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
930
|
+
else {
|
|
931
|
+
this.logger.warn(`Ignored walletReady from untrusted origin: ${event.origin}`);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
window.addEventListener('message', this._walletReadyMessageListener);
|
|
907
935
|
if (window.parent !== window) {
|
|
908
936
|
this.logger.log('Running inside a frame — waiting for trusted walletReady signal');
|
|
909
937
|
}
|
|
910
|
-
// Initial check (in case extension was already injected)
|
|
911
938
|
this.checkExtensionAvailability();
|
|
912
|
-
// Set up periodic checks as fallback
|
|
913
939
|
this.extensionDetectionInterval = setInterval(() => {
|
|
914
940
|
this.checkExtensionAvailability();
|
|
915
941
|
}, 2000);
|
|
916
942
|
}
|
|
917
|
-
/**
|
|
918
|
-
* Check if extension is currently available
|
|
919
|
-
*/
|
|
920
943
|
checkExtensionAvailability() {
|
|
944
|
+
// If parent-bridge readiness was established via a trusted walletReady handshake,
|
|
945
|
+
// preserve that state — the polling fallback (detectExtensionSignals) does not
|
|
946
|
+
// consider the iframe parent signal and would incorrectly flip state back
|
|
947
|
+
if (this._parentTrusted) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
921
950
|
const wasAvailable = this.isExtensionAvailableState;
|
|
922
|
-
// Basic checks for extension context
|
|
923
951
|
this.isExtensionAvailableState = this.hasExtensionContext() && this.detectExtensionSignals();
|
|
924
952
|
if (!wasAvailable && this.isExtensionAvailableState) {
|
|
925
953
|
this.logger.log('Extension became available');
|
|
@@ -928,25 +956,9 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
928
956
|
this.logger.warn('Extension became unavailable');
|
|
929
957
|
}
|
|
930
958
|
}
|
|
931
|
-
/**
|
|
932
|
-
* Detect extension signals/indicators
|
|
933
|
-
*/
|
|
934
959
|
detectExtensionSignals() {
|
|
935
|
-
|
|
936
|
-
return false;
|
|
937
|
-
// Check for extension-injected indicators
|
|
938
|
-
const win = window;
|
|
939
|
-
// Look for extension-injected globals (matches injected.ts)
|
|
940
|
-
return !!(win.wallet0xio ||
|
|
941
|
-
win.ZeroXIOWallet ||
|
|
942
|
-
win.octraWallet ||
|
|
943
|
-
(win.chrome?.runtime?.id) ||
|
|
944
|
-
document.querySelector('meta[name="0xio-dapp"]') ||
|
|
945
|
-
document.querySelector('[data-0xio-sdk-bridge]'));
|
|
960
|
+
return this.adapter.detect();
|
|
946
961
|
}
|
|
947
|
-
/**
|
|
948
|
-
* Wait for extension to become available
|
|
949
|
-
*/
|
|
950
962
|
async waitForExtensionAvailability(timeoutMs) {
|
|
951
963
|
if (this.isExtensionAvailableState) {
|
|
952
964
|
return true;
|
|
@@ -954,14 +966,11 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
954
966
|
return new Promise((resolve) => {
|
|
955
967
|
let resolved = false;
|
|
956
968
|
const startTime = Date.now();
|
|
957
|
-
|
|
969
|
+
let adapterReadyTeardown = null;
|
|
958
970
|
const cleanup = () => {
|
|
959
971
|
clearInterval(checkInterval);
|
|
960
|
-
|
|
961
|
-
window.removeEventListener('wallet0xioReady', onReady);
|
|
962
|
-
window.removeEventListener('octraWalletReady', onReady);
|
|
972
|
+
adapterReadyTeardown?.();
|
|
963
973
|
};
|
|
964
|
-
// Event handler for instant resolution
|
|
965
974
|
const onReady = () => {
|
|
966
975
|
if (resolved)
|
|
967
976
|
return;
|
|
@@ -970,11 +979,10 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
970
979
|
cleanup();
|
|
971
980
|
resolve(true);
|
|
972
981
|
};
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
// Polling fallback with shorter interval
|
|
982
|
+
// Use adapter's listenForReady for fast resolution; polling is the fallback
|
|
983
|
+
if (this.adapter.listenForReady) {
|
|
984
|
+
adapterReadyTeardown = this.adapter.listenForReady(onReady);
|
|
985
|
+
}
|
|
978
986
|
const checkInterval = setInterval(() => {
|
|
979
987
|
if (resolved)
|
|
980
988
|
return;
|
|
@@ -992,9 +1000,6 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
992
1000
|
}, 100);
|
|
993
1001
|
});
|
|
994
1002
|
}
|
|
995
|
-
/**
|
|
996
|
-
* Get browser diagnostics for error reporting
|
|
997
|
-
*/
|
|
998
1003
|
getBrowserDiagnostics() {
|
|
999
1004
|
if (typeof window === 'undefined') {
|
|
1000
1005
|
return { environment: 'non-browser' };
|
|
@@ -1006,29 +1011,21 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1006
1011
|
hasChromeRuntime: !!(win.chrome?.runtime),
|
|
1007
1012
|
hasPostMessage: typeof window.postMessage === 'function',
|
|
1008
1013
|
origin: window.location?.origin,
|
|
1009
|
-
extensionDetection: {
|
|
1010
|
-
hasWallet0xio: !!win.wallet0xio,
|
|
1011
|
-
hasZeroXIOWallet: !!win.ZeroXIOWallet,
|
|
1012
|
-
hasOctraWallet: !!win.octraWallet,
|
|
1013
|
-
hasChromeRuntimeId: !!(win.chrome?.runtime?.id),
|
|
1014
|
-
hasSdkBridge: !!document.querySelector('[data-0xio-sdk-bridge]')
|
|
1015
|
-
}
|
|
1016
1014
|
};
|
|
1017
1015
|
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Get extension state diagnostics
|
|
1020
|
-
*/
|
|
1021
1016
|
getExtensionDiagnostics() {
|
|
1022
1017
|
return {
|
|
1023
1018
|
initialized: this.isInitialized,
|
|
1024
1019
|
available: this.isExtensionAvailableState,
|
|
1020
|
+
parentTrusted: this._parentTrusted,
|
|
1025
1021
|
pendingRequests: this.pendingRequests.size,
|
|
1026
1022
|
hasExtensionContext: this.hasExtensionContext(),
|
|
1027
|
-
browserDiagnostics: this.getBrowserDiagnostics()
|
|
1028
1023
|
};
|
|
1029
1024
|
}
|
|
1030
1025
|
/**
|
|
1031
|
-
*
|
|
1026
|
+
* Clean up SDK resources.
|
|
1027
|
+
* After cleanup() the instance is terminal — do not call initialize() again.
|
|
1028
|
+
* Construct a new instance instead.
|
|
1032
1029
|
*/
|
|
1033
1030
|
cleanup() {
|
|
1034
1031
|
if (this.extensionDetectionInterval) {
|
|
@@ -1042,18 +1039,22 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1042
1039
|
this.pendingRequests.clear();
|
|
1043
1040
|
this.isInitialized = false;
|
|
1044
1041
|
this.isExtensionAvailableState = false;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1042
|
+
this._parentTrusted = false;
|
|
1043
|
+
this._parentOrigin = null;
|
|
1044
|
+
this._interactiveInFlight = false;
|
|
1045
|
+
// Tear down adapter listeners (response/event + wallet-ready)
|
|
1046
|
+
this._adapterTeardown?.();
|
|
1047
|
+
this._adapterTeardown = null;
|
|
1048
|
+
this._adapterReadyTeardown?.();
|
|
1049
|
+
this._adapterReadyTeardown = null;
|
|
1050
|
+
// Remove walletReady postMessage listener (parent iframe bridge)
|
|
1051
|
+
if (typeof window !== 'undefined' && this._walletReadyMessageListener) {
|
|
1052
|
+
window.removeEventListener('message', this._walletReadyMessageListener);
|
|
1053
|
+
this._walletReadyMessageListener = null;
|
|
1049
1054
|
}
|
|
1050
|
-
// Call parent cleanup
|
|
1051
1055
|
this.removeAllListeners();
|
|
1052
1056
|
this.logger.log('Communication cleanup complete');
|
|
1053
1057
|
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Get debug information
|
|
1056
|
-
*/
|
|
1057
1058
|
getDebugInfo() {
|
|
1058
1059
|
return {
|
|
1059
1060
|
initialized: this.isInitialized,
|
|
@@ -1064,23 +1065,30 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1064
1065
|
};
|
|
1065
1066
|
}
|
|
1066
1067
|
}
|
|
1067
|
-
// Methods that trigger user-facing popups — NEVER retry these
|
|
1068
|
+
// Methods that trigger user-facing popups — NEVER retry these.
|
|
1068
1069
|
// Retrying sends a second request while the first popup is still open,
|
|
1069
|
-
// causing double popups where the second tx fails (stale nonce/state)
|
|
1070
|
+
// causing double popups where the second tx fails (stale nonce/state).
|
|
1070
1071
|
ExtensionCommunicator.NO_RETRY_METHODS = new Set([
|
|
1071
1072
|
'connect', 'send_transaction', 'call_contract', 'signMessage',
|
|
1072
1073
|
'send_private_transfer', 'claim_private_transfer',
|
|
1073
1074
|
'encrypt_balance', 'decrypt_balance',
|
|
1074
1075
|
]);
|
|
1076
|
+
ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
|
|
1077
|
+
// only forward known event types
|
|
1078
|
+
ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
|
|
1079
|
+
'connect', 'disconnect', 'accountChanged', 'balanceChanged',
|
|
1080
|
+
'networkChanged', 'transactionConfirmed', 'error',
|
|
1081
|
+
'extensionLocked', 'extensionUnlocked'
|
|
1082
|
+
]);
|
|
1075
1083
|
|
|
1076
1084
|
/**
|
|
1077
1085
|
* Network configuration for 0xio SDK
|
|
1078
1086
|
*/
|
|
1079
|
-
const
|
|
1087
|
+
const _NETWORKS = {
|
|
1080
1088
|
'mainnet': {
|
|
1081
1089
|
id: 'mainnet',
|
|
1082
1090
|
name: 'Octra Mainnet',
|
|
1083
|
-
rpcUrl: '
|
|
1091
|
+
rpcUrl: 'https://octra.network',
|
|
1084
1092
|
explorerUrl: 'https://lite.octrascan.io/tx.html?hash=',
|
|
1085
1093
|
explorerAddressUrl: 'https://lite.octrascan.io/address.html?addr=',
|
|
1086
1094
|
indexerUrl: 'https://lite.octrascan.io',
|
|
@@ -1111,49 +1119,106 @@ const NETWORKS = {
|
|
|
1111
1119
|
isTestnet: false
|
|
1112
1120
|
}
|
|
1113
1121
|
};
|
|
1122
|
+
/**
|
|
1123
|
+
* Immutable public copy of the built-in network table.
|
|
1124
|
+
* Modifications to returned objects do not affect SDK-internal state.
|
|
1125
|
+
*/
|
|
1126
|
+
const NETWORKS = Object.freeze(Object.fromEntries(Object.entries(_NETWORKS).map(([k, v]) => [k, Object.freeze({ ...v })])));
|
|
1114
1127
|
const DEFAULT_NETWORK_ID = 'mainnet';
|
|
1115
1128
|
/**
|
|
1116
|
-
* Get network configuration by ID
|
|
1129
|
+
* Get network configuration by ID.
|
|
1130
|
+
* Returns a frozen copy — callers cannot mutate SDK-internal state.
|
|
1117
1131
|
*/
|
|
1118
1132
|
function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
1119
|
-
|
|
1120
|
-
if (!network) {
|
|
1133
|
+
if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
|
|
1121
1134
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, `Unknown network ID: ${networkId}`);
|
|
1122
1135
|
}
|
|
1123
|
-
return
|
|
1136
|
+
return Object.freeze({ ..._NETWORKS[networkId] });
|
|
1124
1137
|
}
|
|
1125
1138
|
/**
|
|
1126
|
-
* Get all available networks
|
|
1139
|
+
* Get all available networks.
|
|
1140
|
+
* Returns frozen copies — callers cannot mutate SDK-internal state.
|
|
1127
1141
|
*/
|
|
1128
1142
|
function getAllNetworks() {
|
|
1129
|
-
return Object.values(
|
|
1143
|
+
return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
|
|
1130
1144
|
}
|
|
1131
1145
|
/**
|
|
1132
|
-
* Check if network ID is valid
|
|
1146
|
+
* Check if network ID is valid (own property check, prevents prototype pollution).
|
|
1133
1147
|
*/
|
|
1134
1148
|
function isValidNetworkId(networkId) {
|
|
1135
|
-
return networkId
|
|
1149
|
+
return typeof networkId === 'string' && Object.prototype.hasOwnProperty.call(_NETWORKS, networkId);
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Validate a NetworkInfo shape from an untrusted source (bridge response).
|
|
1153
|
+
* Returns a frozen copy if valid, null otherwise.
|
|
1154
|
+
*/
|
|
1155
|
+
function validateNetworkInfo(raw) {
|
|
1156
|
+
if (!raw || typeof raw !== 'object')
|
|
1157
|
+
return null;
|
|
1158
|
+
if (typeof raw.id !== 'string' || !raw.id)
|
|
1159
|
+
return null;
|
|
1160
|
+
if (typeof raw.name !== 'string')
|
|
1161
|
+
return null;
|
|
1162
|
+
if (typeof raw.rpcUrl !== 'string' || (!raw.rpcUrl && raw.id !== 'custom'))
|
|
1163
|
+
return null;
|
|
1164
|
+
if (typeof raw.supportsPrivacy !== 'boolean')
|
|
1165
|
+
return null;
|
|
1166
|
+
return Object.freeze({
|
|
1167
|
+
id: raw.id,
|
|
1168
|
+
name: raw.name,
|
|
1169
|
+
rpcUrl: raw.rpcUrl,
|
|
1170
|
+
explorerUrl: typeof raw.explorerUrl === 'string' ? raw.explorerUrl : undefined,
|
|
1171
|
+
explorerAddressUrl: typeof raw.explorerAddressUrl === 'string' ? raw.explorerAddressUrl : undefined,
|
|
1172
|
+
indexerUrl: typeof raw.indexerUrl === 'string' ? raw.indexerUrl : undefined,
|
|
1173
|
+
supportsPrivacy: raw.supportsPrivacy,
|
|
1174
|
+
color: typeof raw.color === 'string' ? raw.color : '#64748b',
|
|
1175
|
+
isTestnet: typeof raw.isTestnet === 'boolean' ? raw.isTestnet : false,
|
|
1176
|
+
});
|
|
1136
1177
|
}
|
|
1137
1178
|
|
|
1138
1179
|
/**
|
|
1139
1180
|
* SDK Configuration
|
|
1140
1181
|
*/
|
|
1141
1182
|
/**
|
|
1142
|
-
* Default balance structure
|
|
1183
|
+
* Default balance structure.
|
|
1184
|
+
* Accepts a numeric total or undefined — never pass a Balance object here.
|
|
1143
1185
|
*/
|
|
1144
1186
|
function createDefaultBalance(total = 0) {
|
|
1187
|
+
const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
|
|
1145
1188
|
return {
|
|
1146
|
-
total,
|
|
1147
|
-
public:
|
|
1189
|
+
total: safeTotal,
|
|
1190
|
+
public: safeTotal,
|
|
1148
1191
|
private: 0,
|
|
1149
1192
|
currency: 'OCT'
|
|
1150
1193
|
};
|
|
1151
1194
|
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Validate and normalise a Balance from an untrusted source (bridge response).
|
|
1197
|
+
* Returns null if the payload cannot be coerced into a valid Balance.
|
|
1198
|
+
*/
|
|
1199
|
+
function validateBalance(raw) {
|
|
1200
|
+
if (raw === null || raw === undefined)
|
|
1201
|
+
return null;
|
|
1202
|
+
// If it's already a Balance-shaped object, extract numeric fields
|
|
1203
|
+
// Use Number() not parseFloat() — parseFloat('10abc') silently returns 10
|
|
1204
|
+
const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
|
|
1205
|
+
const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
|
|
1206
|
+
if (!Number.isFinite(pub) || pub < 0)
|
|
1207
|
+
return null;
|
|
1208
|
+
if (!Number.isFinite(priv) || priv < 0)
|
|
1209
|
+
return null;
|
|
1210
|
+
return {
|
|
1211
|
+
public: pub,
|
|
1212
|
+
private: priv,
|
|
1213
|
+
total: pub + priv,
|
|
1214
|
+
currency: 'OCT'
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1152
1217
|
/**
|
|
1153
1218
|
* SDK Configuration constants
|
|
1154
1219
|
*/
|
|
1155
1220
|
const SDK_CONFIG = {
|
|
1156
|
-
version: '2.
|
|
1221
|
+
version: '2.7.0',
|
|
1157
1222
|
defaultNetworkId: DEFAULT_NETWORK_ID,
|
|
1158
1223
|
communicationTimeout: 30000, // 30 seconds
|
|
1159
1224
|
retryAttempts: 3,
|
|
@@ -1175,6 +1240,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1175
1240
|
super(config.debug);
|
|
1176
1241
|
this.connectionInfo = { isConnected: false };
|
|
1177
1242
|
this.isInitialized = false;
|
|
1243
|
+
this._initPromise = null;
|
|
1244
|
+
// session version — stale write detection
|
|
1245
|
+
this._sessionVersion = 0;
|
|
1178
1246
|
this.config = {
|
|
1179
1247
|
...config,
|
|
1180
1248
|
appVersion: config.appVersion || '1.0.0',
|
|
@@ -1182,7 +1250,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1182
1250
|
debug: config.debug || false
|
|
1183
1251
|
};
|
|
1184
1252
|
this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
|
|
1185
|
-
this.communicator = new ExtensionCommunicator(this.config.debug);
|
|
1253
|
+
this.communicator = new ExtensionCommunicator(this.config.debug, [], this.config.adapter);
|
|
1186
1254
|
this.logger.log('Wallet instance created with config:', this.config);
|
|
1187
1255
|
}
|
|
1188
1256
|
// ===================
|
|
@@ -1196,35 +1264,45 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1196
1264
|
if (this.isInitialized) {
|
|
1197
1265
|
return true;
|
|
1198
1266
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
if (!communicationReady) {
|
|
1203
|
-
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
|
|
1204
|
-
}
|
|
1205
|
-
// Register this DApp with the extension
|
|
1206
|
-
await this.communicator.sendRequest('register_dapp', {
|
|
1207
|
-
appName: this.config.appName,
|
|
1208
|
-
appDescription: this.config.appDescription,
|
|
1209
|
-
appVersion: this.config.appVersion,
|
|
1210
|
-
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1211
|
-
appIcon: this.config.appIcon,
|
|
1212
|
-
requiredPermissions: this.config.requiredPermissions,
|
|
1213
|
-
networkId: this.config.networkId
|
|
1214
|
-
});
|
|
1215
|
-
// Setup event forwarding from extension
|
|
1216
|
-
this.setupExtensionEventListeners();
|
|
1217
|
-
this.isInitialized = true;
|
|
1218
|
-
this.logger.log('SDK initialized successfully');
|
|
1219
|
-
return true;
|
|
1267
|
+
// single-flight init
|
|
1268
|
+
if (this._initPromise) {
|
|
1269
|
+
return this._initPromise;
|
|
1220
1270
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1271
|
+
this._initPromise = (async () => {
|
|
1272
|
+
try {
|
|
1273
|
+
// Initialize extension communication
|
|
1274
|
+
const communicationReady = await this.communicator.initialize();
|
|
1275
|
+
if (!communicationReady) {
|
|
1276
|
+
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
|
|
1277
|
+
}
|
|
1278
|
+
// Register this DApp with the extension
|
|
1279
|
+
await this.communicator.sendRequest('register_dapp', {
|
|
1280
|
+
appName: this.config.appName,
|
|
1281
|
+
appDescription: this.config.appDescription,
|
|
1282
|
+
appVersion: this.config.appVersion,
|
|
1283
|
+
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1284
|
+
appIcon: this.config.appIcon,
|
|
1285
|
+
requiredPermissions: this.config.requiredPermissions,
|
|
1286
|
+
networkId: this.config.networkId
|
|
1287
|
+
});
|
|
1288
|
+
// Setup event forwarding from extension
|
|
1289
|
+
this.setupExtensionEventListeners();
|
|
1290
|
+
this.isInitialized = true;
|
|
1291
|
+
this.logger.log('SDK initialized successfully');
|
|
1292
|
+
return true;
|
|
1225
1293
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
this.logger.error('Failed to initialize:', error);
|
|
1296
|
+
if (error instanceof ZeroXIOWalletError) {
|
|
1297
|
+
throw error;
|
|
1298
|
+
}
|
|
1299
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Failed to initialize SDK', error);
|
|
1300
|
+
}
|
|
1301
|
+
finally {
|
|
1302
|
+
this._initPromise = null;
|
|
1303
|
+
}
|
|
1304
|
+
})();
|
|
1305
|
+
return this._initPromise;
|
|
1228
1306
|
}
|
|
1229
1307
|
/**
|
|
1230
1308
|
* Check if SDK is initialized
|
|
@@ -1242,31 +1320,53 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1242
1320
|
this.ensureInitialized();
|
|
1243
1321
|
try {
|
|
1244
1322
|
this.logger.log('Attempting to connect with options:', options);
|
|
1323
|
+
// filter to declared perms only
|
|
1324
|
+
const declaredPermissions = this.config.requiredPermissions || [];
|
|
1325
|
+
const requestPermissions = options.requestPermissions
|
|
1326
|
+
? options.requestPermissions.filter(p => declaredPermissions.includes(p))
|
|
1327
|
+
: declaredPermissions;
|
|
1245
1328
|
const result = await this.communicator.sendRequest('connect', {
|
|
1246
|
-
requestPermissions
|
|
1329
|
+
requestPermissions,
|
|
1247
1330
|
networkId: options.networkId || this.config.networkId
|
|
1248
1331
|
});
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1332
|
+
// verify pubkey→addr binding
|
|
1333
|
+
if (result.publicKey && result.address) {
|
|
1334
|
+
try {
|
|
1335
|
+
const derived = await deriveOctraAddress(result.publicKey);
|
|
1336
|
+
if (derived !== result.address) {
|
|
1337
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed — the reported public key does not derive to the reported address');
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
catch (e) {
|
|
1341
|
+
if (e instanceof ZeroXIOWalletError)
|
|
1342
|
+
throw e;
|
|
1343
|
+
this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
// Use networkInfo from extension response — validate before caching
|
|
1347
|
+
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1348
|
+
?? getNetworkConfig(result.networkId || this.config.networkId);
|
|
1349
|
+
const permissions = result.permissions || [];
|
|
1350
|
+
// Update connection info — including permissions
|
|
1252
1351
|
this.connectionInfo = {
|
|
1253
1352
|
isConnected: true,
|
|
1254
1353
|
address: result.address,
|
|
1255
1354
|
publicKey: result.publicKey,
|
|
1256
1355
|
balance: result.balance,
|
|
1257
1356
|
networkInfo,
|
|
1258
|
-
connectedAt: Date.now()
|
|
1357
|
+
connectedAt: Date.now(),
|
|
1358
|
+
permissions
|
|
1259
1359
|
};
|
|
1260
1360
|
const connectEvent = {
|
|
1261
1361
|
address: result.address,
|
|
1262
1362
|
publicKey: result.publicKey,
|
|
1263
1363
|
balance: result.balance,
|
|
1264
1364
|
networkInfo,
|
|
1265
|
-
permissions
|
|
1365
|
+
permissions
|
|
1266
1366
|
};
|
|
1267
1367
|
// Emit connect event
|
|
1268
1368
|
this.emit('connect', connectEvent);
|
|
1269
|
-
this.logger.log('Connected successfully:', connectEvent);
|
|
1369
|
+
this.logger.log('Connected successfully:', { address: connectEvent.address, network: networkInfo.id });
|
|
1270
1370
|
return connectEvent;
|
|
1271
1371
|
}
|
|
1272
1372
|
catch (error) {
|
|
@@ -1284,6 +1384,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1284
1384
|
this.ensureInitialized();
|
|
1285
1385
|
try {
|
|
1286
1386
|
await this.communicator.sendRequest('disconnect');
|
|
1387
|
+
++this._sessionVersion;
|
|
1287
1388
|
this.connectionInfo = { isConnected: false };
|
|
1288
1389
|
const disconnectEvent = {
|
|
1289
1390
|
reason: 'user_action'
|
|
@@ -1314,30 +1415,55 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1314
1415
|
async getConnectionStatus() {
|
|
1315
1416
|
this.ensureInitialized();
|
|
1316
1417
|
try {
|
|
1418
|
+
const sv = this._sessionVersion;
|
|
1317
1419
|
const result = await this.communicator.sendRequest('getConnectionStatus');
|
|
1420
|
+
// skip if session changed mid-flight
|
|
1421
|
+
if (this._sessionVersion !== sv)
|
|
1422
|
+
return { ...this.connectionInfo };
|
|
1318
1423
|
if (result.isConnected && result.address) {
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1424
|
+
// verify pubkey→addr binding
|
|
1425
|
+
if (result.publicKey) {
|
|
1426
|
+
try {
|
|
1427
|
+
const derived = await deriveOctraAddress(result.publicKey);
|
|
1428
|
+
if (derived !== result.address) {
|
|
1429
|
+
this.logger.warn('Address-key binding mismatch on session restore — ignoring stale session');
|
|
1430
|
+
this.connectionInfo = { isConnected: false };
|
|
1431
|
+
return { ...this.connectionInfo };
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
catch (e) {
|
|
1435
|
+
this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
// validate untrusted balance/networkInfo before caching
|
|
1439
|
+
const balanceInfo = validateBalance(result.balance) ?? createDefaultBalance();
|
|
1440
|
+
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1441
|
+
?? getNetworkConfig(result.networkId || this.config.networkId);
|
|
1442
|
+
const wasConnected = this.connectionInfo.isConnected;
|
|
1443
|
+
const permissions = result.permissions || [];
|
|
1444
|
+
// preserve existing connectedAt
|
|
1445
|
+
const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
|
|
1324
1446
|
this.connectionInfo = {
|
|
1325
1447
|
isConnected: true,
|
|
1326
1448
|
address: result.address,
|
|
1327
1449
|
publicKey: result.publicKey,
|
|
1328
1450
|
balance: balanceInfo,
|
|
1329
1451
|
networkInfo,
|
|
1330
|
-
connectedAt
|
|
1331
|
-
|
|
1332
|
-
this.logger.log('Discovered existing connection:', this.connectionInfo);
|
|
1333
|
-
// Emit connect event to notify the wrapper
|
|
1334
|
-
const connectEvent = {
|
|
1335
|
-
address: result.address,
|
|
1336
|
-
balance: balanceInfo,
|
|
1337
|
-
networkInfo,
|
|
1338
|
-
permissions: result.permissions || []
|
|
1452
|
+
connectedAt,
|
|
1453
|
+
permissions
|
|
1339
1454
|
};
|
|
1340
|
-
this.
|
|
1455
|
+
this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
|
|
1456
|
+
// only emit on disconnected→connected transition
|
|
1457
|
+
if (!wasConnected) {
|
|
1458
|
+
const connectEvent = {
|
|
1459
|
+
address: result.address,
|
|
1460
|
+
publicKey: result.publicKey,
|
|
1461
|
+
balance: balanceInfo,
|
|
1462
|
+
networkInfo,
|
|
1463
|
+
permissions
|
|
1464
|
+
};
|
|
1465
|
+
this.emit('connect', connectEvent);
|
|
1466
|
+
}
|
|
1341
1467
|
}
|
|
1342
1468
|
else {
|
|
1343
1469
|
// No existing connection
|
|
@@ -1357,10 +1483,14 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1357
1483
|
* The extension broadcasts 'networkChanged' event to all connected dApps.
|
|
1358
1484
|
*/
|
|
1359
1485
|
async switchNetwork(networkId) {
|
|
1360
|
-
this.
|
|
1486
|
+
this.ensureConnected();
|
|
1361
1487
|
try {
|
|
1488
|
+
const sv = this._sessionVersion;
|
|
1362
1489
|
const result = await this.communicator.sendRequest('switch_network', { networkId });
|
|
1363
1490
|
this.logger.log(`Network switch result:`, result);
|
|
1491
|
+
// skip if session changed mid-flight
|
|
1492
|
+
if (this._sessionVersion !== sv)
|
|
1493
|
+
return { network: result.network || networkId, switched: result.switched ?? false };
|
|
1364
1494
|
if (result.switched) {
|
|
1365
1495
|
// Update internal state
|
|
1366
1496
|
const networkInfo = getNetworkConfig(networkId);
|
|
@@ -1403,22 +1533,28 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1403
1533
|
// Fetch balance from extension (bypasses CORS, has access to private balance)
|
|
1404
1534
|
let publicBalance = 0;
|
|
1405
1535
|
let privateBalance = 0;
|
|
1536
|
+
const sv = this._sessionVersion;
|
|
1406
1537
|
const extResult = await this.communicator.sendRequest('getBalance', { forceRefresh });
|
|
1407
1538
|
publicBalance = parseFloat(extResult.balance || '0');
|
|
1408
1539
|
privateBalance = parseFloat(extResult.privateBalance || '0');
|
|
1409
|
-
this.logger.log('Balance fetched from extension:', { public: publicBalance
|
|
1540
|
+
this.logger.log('Balance fetched from extension:', { public: publicBalance });
|
|
1410
1541
|
const result = {
|
|
1411
1542
|
public: publicBalance,
|
|
1412
1543
|
private: privateBalance,
|
|
1413
1544
|
total: publicBalance + privateBalance,
|
|
1414
1545
|
currency: 'OCT'
|
|
1415
1546
|
};
|
|
1547
|
+
// skip if session changed mid-flight
|
|
1548
|
+
if (this._sessionVersion !== sv)
|
|
1549
|
+
return result;
|
|
1416
1550
|
// Update cached balance
|
|
1417
1551
|
if (this.connectionInfo.balance) {
|
|
1418
1552
|
const previousBalance = this.connectionInfo.balance;
|
|
1419
1553
|
this.connectionInfo.balance = result;
|
|
1420
|
-
//
|
|
1421
|
-
if (previousBalance.total !== result.total
|
|
1554
|
+
// emit on total or pub/priv split change
|
|
1555
|
+
if (previousBalance.total !== result.total ||
|
|
1556
|
+
previousBalance.public !== result.public ||
|
|
1557
|
+
previousBalance.private !== result.private) {
|
|
1422
1558
|
const balanceChangedEvent = {
|
|
1423
1559
|
address: this.connectionInfo.address,
|
|
1424
1560
|
previousBalance,
|
|
@@ -1433,6 +1569,8 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1433
1569
|
return result;
|
|
1434
1570
|
}
|
|
1435
1571
|
catch (error) {
|
|
1572
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1573
|
+
throw error;
|
|
1436
1574
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
|
|
1437
1575
|
}
|
|
1438
1576
|
}
|
|
@@ -1442,26 +1580,37 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1442
1580
|
async getNetworkInfo() {
|
|
1443
1581
|
this.ensureInitialized();
|
|
1444
1582
|
try {
|
|
1583
|
+
const sv = this._sessionVersion;
|
|
1445
1584
|
const result = await this.communicator.sendRequest('get_network_info');
|
|
1585
|
+
// validate network info before caching
|
|
1586
|
+
const networkInfo = validateNetworkInfo(result);
|
|
1587
|
+
if (!networkInfo) {
|
|
1588
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Extension returned invalid network info');
|
|
1589
|
+
}
|
|
1590
|
+
// skip if session changed mid-flight
|
|
1591
|
+
if (this._sessionVersion !== sv)
|
|
1592
|
+
return networkInfo;
|
|
1446
1593
|
// Update cached network info
|
|
1447
1594
|
if (this.connectionInfo.networkInfo) {
|
|
1448
1595
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1449
|
-
this.connectionInfo.networkInfo =
|
|
1596
|
+
this.connectionInfo.networkInfo = networkInfo;
|
|
1450
1597
|
// Emit network changed event if different
|
|
1451
|
-
if (previousNetwork.id !==
|
|
1598
|
+
if (previousNetwork.id !== networkInfo.id) {
|
|
1452
1599
|
const networkChangedEvent = {
|
|
1453
1600
|
previousNetwork,
|
|
1454
|
-
newNetwork:
|
|
1601
|
+
newNetwork: networkInfo
|
|
1455
1602
|
};
|
|
1456
1603
|
this.emit('networkChanged', networkChangedEvent);
|
|
1457
1604
|
}
|
|
1458
1605
|
}
|
|
1459
1606
|
else {
|
|
1460
|
-
this.connectionInfo.networkInfo =
|
|
1607
|
+
this.connectionInfo.networkInfo = networkInfo;
|
|
1461
1608
|
}
|
|
1462
|
-
return
|
|
1609
|
+
return networkInfo;
|
|
1463
1610
|
}
|
|
1464
1611
|
catch (error) {
|
|
1612
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1613
|
+
throw error;
|
|
1465
1614
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
|
|
1466
1615
|
}
|
|
1467
1616
|
}
|
|
@@ -1473,8 +1622,20 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1473
1622
|
*/
|
|
1474
1623
|
async sendTransaction(txData) {
|
|
1475
1624
|
this.ensureConnected();
|
|
1625
|
+
// validate inputs
|
|
1626
|
+
if (!isValidAddress(txData.to)) {
|
|
1627
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1628
|
+
}
|
|
1629
|
+
if (!isValidAmount(txData.amount)) {
|
|
1630
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1631
|
+
}
|
|
1632
|
+
// bound memo
|
|
1633
|
+
if (txData.message && txData.message.length > 1000) {
|
|
1634
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1635
|
+
}
|
|
1476
1636
|
try {
|
|
1477
|
-
|
|
1637
|
+
// log non-sensitive only
|
|
1638
|
+
this.logger.log('Sending transaction:', { to: txData.to });
|
|
1478
1639
|
const result = await this.communicator.sendRequest('send_transaction', txData);
|
|
1479
1640
|
this.logger.log('Transaction result:', result);
|
|
1480
1641
|
// Refresh balance after successful transaction
|
|
@@ -1501,8 +1662,30 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1501
1662
|
*/
|
|
1502
1663
|
async callContract(callData) {
|
|
1503
1664
|
this.ensureConnected();
|
|
1665
|
+
// validate inputs
|
|
1666
|
+
if (!isValidAddress(callData.contract)) {
|
|
1667
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1668
|
+
}
|
|
1669
|
+
if (!callData.method || typeof callData.method !== 'string') {
|
|
1670
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method is required');
|
|
1671
|
+
}
|
|
1672
|
+
// bound method + params size
|
|
1673
|
+
if (callData.method.length > 200) {
|
|
1674
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
|
|
1675
|
+
}
|
|
1676
|
+
try {
|
|
1677
|
+
if (JSON.stringify(callData.params).length > 65536) {
|
|
1678
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
catch (e) {
|
|
1682
|
+
if (e instanceof ZeroXIOWalletError)
|
|
1683
|
+
throw e;
|
|
1684
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract params are not serialisable');
|
|
1685
|
+
}
|
|
1504
1686
|
try {
|
|
1505
|
-
|
|
1687
|
+
// log non-sensitive only
|
|
1688
|
+
this.logger.log('Calling contract:', { contract: callData.contract, method: callData.method });
|
|
1506
1689
|
const result = await this.communicator.sendRequest('call_contract', {
|
|
1507
1690
|
contract: callData.contract,
|
|
1508
1691
|
method: callData.method,
|
|
@@ -1527,13 +1710,36 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1527
1710
|
*/
|
|
1528
1711
|
async contractCallView(viewData) {
|
|
1529
1712
|
this.ensureInitialized();
|
|
1713
|
+
// validate inputs
|
|
1714
|
+
if (!isValidAddress(viewData.contract)) {
|
|
1715
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1716
|
+
}
|
|
1717
|
+
if (!viewData.method || typeof viewData.method !== 'string') {
|
|
1718
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract method is required');
|
|
1719
|
+
}
|
|
1720
|
+
// bound method + params size
|
|
1721
|
+
if (viewData.method.length > 200) {
|
|
1722
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract method name too long (max 200 characters)');
|
|
1723
|
+
}
|
|
1724
|
+
try {
|
|
1725
|
+
if (JSON.stringify(viewData.params).length > 65536) {
|
|
1726
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract params too large (max 64 KB)');
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
catch (e) {
|
|
1730
|
+
if (e instanceof ZeroXIOWalletError)
|
|
1731
|
+
throw e;
|
|
1732
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract params are not serialisable');
|
|
1733
|
+
}
|
|
1530
1734
|
try {
|
|
1531
|
-
|
|
1735
|
+
// log non-sensitive only
|
|
1736
|
+
this.logger.log('Contract view call:', { contract: viewData.contract, method: viewData.method });
|
|
1532
1737
|
const result = await this.communicator.sendRequest('contract_call_view', {
|
|
1533
1738
|
contract: viewData.contract,
|
|
1534
1739
|
method: viewData.method,
|
|
1535
1740
|
params: viewData.params,
|
|
1536
|
-
|
|
1741
|
+
// only include caller if explicit
|
|
1742
|
+
...(viewData.caller != null ? { caller: viewData.caller } : {}),
|
|
1537
1743
|
});
|
|
1538
1744
|
this.logger.log('Contract view result:', result);
|
|
1539
1745
|
return result;
|
|
@@ -1551,6 +1757,16 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1551
1757
|
*/
|
|
1552
1758
|
async getContractStorage(contract, key) {
|
|
1553
1759
|
this.ensureInitialized();
|
|
1760
|
+
// validate inputs
|
|
1761
|
+
if (!isValidAddress(contract)) {
|
|
1762
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1763
|
+
}
|
|
1764
|
+
if (!key || typeof key !== 'string') {
|
|
1765
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Storage key is required');
|
|
1766
|
+
}
|
|
1767
|
+
if (key.length > 200) {
|
|
1768
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Storage key too long (max 200 characters)');
|
|
1769
|
+
}
|
|
1554
1770
|
try {
|
|
1555
1771
|
this.logger.log('Getting contract storage:', { contract, key });
|
|
1556
1772
|
const result = await this.communicator.sendRequest('get_contract_storage', {
|
|
@@ -1604,13 +1820,17 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1604
1820
|
*/
|
|
1605
1821
|
async encryptBalance(amount) {
|
|
1606
1822
|
this.ensureConnected();
|
|
1823
|
+
// validate amount
|
|
1824
|
+
if (!isValidAmount(amount)) {
|
|
1825
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
|
|
1826
|
+
}
|
|
1607
1827
|
try {
|
|
1608
1828
|
const result = await this.communicator.sendRequest('encrypt_balance', { amount });
|
|
1609
1829
|
// Refresh balance after encryption
|
|
1610
1830
|
setTimeout(() => {
|
|
1611
1831
|
this.getBalance(true).catch(() => { });
|
|
1612
1832
|
}, 1000);
|
|
1613
|
-
return result
|
|
1833
|
+
return result;
|
|
1614
1834
|
}
|
|
1615
1835
|
catch (error) {
|
|
1616
1836
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to encrypt balance', error);
|
|
@@ -1621,23 +1841,43 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1621
1841
|
*/
|
|
1622
1842
|
async decryptBalance(amount) {
|
|
1623
1843
|
this.ensureConnected();
|
|
1844
|
+
// validate amount
|
|
1845
|
+
if (!isValidAmount(amount)) {
|
|
1846
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
|
|
1847
|
+
}
|
|
1624
1848
|
try {
|
|
1625
1849
|
const result = await this.communicator.sendRequest('decrypt_balance', { amount });
|
|
1626
1850
|
// Refresh balance after decryption
|
|
1627
1851
|
setTimeout(() => {
|
|
1628
1852
|
this.getBalance(true).catch(() => { });
|
|
1629
1853
|
}, 1000);
|
|
1630
|
-
return result
|
|
1854
|
+
return result;
|
|
1631
1855
|
}
|
|
1632
1856
|
catch (error) {
|
|
1633
1857
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to decrypt balance', error);
|
|
1634
1858
|
}
|
|
1635
1859
|
}
|
|
1636
1860
|
/**
|
|
1637
|
-
* Send private transfer
|
|
1861
|
+
* Send a private (encrypted) transfer to another address.
|
|
1862
|
+
* The extension builds the PVAC ciphertext subtraction + range proof + zero proof,
|
|
1863
|
+
* then submits the encrypted transaction to the network. The recipient's encrypted
|
|
1864
|
+
* balance is updated by the node using re-encryption under their public key.
|
|
1865
|
+
* Requires 'private_transfers' permission.
|
|
1866
|
+
* @since 2.6.0
|
|
1638
1867
|
*/
|
|
1639
1868
|
async sendPrivateTransfer(transferData) {
|
|
1640
1869
|
this.ensureConnected();
|
|
1870
|
+
// validate inputs
|
|
1871
|
+
if (!isValidAddress(transferData.to)) {
|
|
1872
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1873
|
+
}
|
|
1874
|
+
if (!isValidAmount(transferData.amount)) {
|
|
1875
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transfer amount');
|
|
1876
|
+
}
|
|
1877
|
+
// bound msg size
|
|
1878
|
+
if (transferData.message && transferData.message.length > 1000) {
|
|
1879
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
|
|
1880
|
+
}
|
|
1641
1881
|
try {
|
|
1642
1882
|
const result = await this.communicator.sendRequest('send_private_transfer', transferData);
|
|
1643
1883
|
// Refresh balance after transfer
|
|
@@ -1653,7 +1893,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1653
1893
|
}
|
|
1654
1894
|
}
|
|
1655
1895
|
/**
|
|
1656
|
-
* Get pending private transfers
|
|
1896
|
+
* Get pending private transfers that can be claimed by this wallet.
|
|
1897
|
+
* Returns transfers where the connected address is the recipient.
|
|
1898
|
+
* @since 2.6.0
|
|
1657
1899
|
*/
|
|
1658
1900
|
async getPendingPrivateTransfers() {
|
|
1659
1901
|
this.ensureConnected();
|
|
@@ -1666,10 +1908,15 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1666
1908
|
}
|
|
1667
1909
|
}
|
|
1668
1910
|
/**
|
|
1669
|
-
* Claim private transfer
|
|
1911
|
+
* Claim a pending private transfer, adding it to the wallet's encrypted balance.
|
|
1912
|
+
* @since 2.6.0
|
|
1670
1913
|
*/
|
|
1671
1914
|
async claimPrivateTransfer(transferId) {
|
|
1672
1915
|
this.ensureConnected();
|
|
1916
|
+
// validate transfer ID
|
|
1917
|
+
if (!transferId || typeof transferId !== 'string') {
|
|
1918
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transfer ID');
|
|
1919
|
+
}
|
|
1673
1920
|
try {
|
|
1674
1921
|
const result = await this.communicator.sendRequest('claim_private_transfer', {
|
|
1675
1922
|
transferId
|
|
@@ -1727,6 +1974,30 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1727
1974
|
}
|
|
1728
1975
|
}
|
|
1729
1976
|
// ===================
|
|
1977
|
+
// AUTHENTICATION HELPERS
|
|
1978
|
+
// ===================
|
|
1979
|
+
/**
|
|
1980
|
+
* Sign a domain-separated authentication message.
|
|
1981
|
+
* Unlike `signMessage()`, this prepends a standard header that binds the signature
|
|
1982
|
+
* to the calling service and a one-time nonce, preventing cross-service replay attacks.
|
|
1983
|
+
*
|
|
1984
|
+
* @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
|
|
1985
|
+
* @param nonce - Unique one-time value — use a server-generated UUID or challenge
|
|
1986
|
+
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1987
|
+
*/
|
|
1988
|
+
async signAuthMessage(service, nonce) {
|
|
1989
|
+
this.ensureConnected();
|
|
1990
|
+
if (!service || typeof service !== 'string') {
|
|
1991
|
+
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Service name is required');
|
|
1992
|
+
}
|
|
1993
|
+
if (!nonce || typeof nonce !== 'string') {
|
|
1994
|
+
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
|
|
1995
|
+
}
|
|
1996
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
|
|
1997
|
+
const domainSeparated = `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
|
|
1998
|
+
return this.signMessage(domainSeparated);
|
|
1999
|
+
}
|
|
2000
|
+
// ===================
|
|
1730
2001
|
// PRIVATE METHODS
|
|
1731
2002
|
// ===================
|
|
1732
2003
|
ensureInitialized() {
|
|
@@ -1766,28 +2037,47 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1766
2037
|
* Handle account changed event from extension
|
|
1767
2038
|
*/
|
|
1768
2039
|
handleAccountChanged(data) {
|
|
2040
|
+
++this._sessionVersion;
|
|
1769
2041
|
const previousAddress = this.connectionInfo.address;
|
|
1770
2042
|
this.connectionInfo.address = data.address;
|
|
2043
|
+
// clear stale pubkey on acct change
|
|
2044
|
+
this.connectionInfo.publicKey = data.publicKey;
|
|
1771
2045
|
if (data.balance) {
|
|
1772
|
-
|
|
2046
|
+
// validate balance before caching
|
|
2047
|
+
const validated = validateBalance(data.balance);
|
|
2048
|
+
if (validated) {
|
|
2049
|
+
this.connectionInfo.balance = validated;
|
|
2050
|
+
}
|
|
2051
|
+
else {
|
|
2052
|
+
this.connectionInfo.balance = undefined; // clear stale balance
|
|
2053
|
+
}
|
|
1773
2054
|
}
|
|
1774
2055
|
const accountChangedEvent = {
|
|
1775
2056
|
previousAddress,
|
|
1776
2057
|
newAddress: data.address,
|
|
2058
|
+
publicKey: data.publicKey,
|
|
1777
2059
|
balance: data.balance ?? this.connectionInfo.balance
|
|
1778
2060
|
};
|
|
1779
2061
|
this.emit('accountChanged', accountChangedEvent);
|
|
1780
|
-
this.logger.log('Account changed:', accountChangedEvent);
|
|
2062
|
+
this.logger.log('Account changed:', { newAddress: accountChangedEvent.newAddress });
|
|
1781
2063
|
}
|
|
1782
2064
|
/**
|
|
1783
2065
|
* Handle network changed event from extension
|
|
1784
2066
|
*/
|
|
1785
2067
|
handleNetworkChanged(data) {
|
|
1786
2068
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1787
|
-
|
|
2069
|
+
// validate networkInfo — drop invalid
|
|
2070
|
+
const networkInfo = validateNetworkInfo(data.networkInfo);
|
|
2071
|
+
if (!networkInfo) {
|
|
2072
|
+
this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
this.connectionInfo.networkInfo = networkInfo;
|
|
2076
|
+
// invalidate balance on network change
|
|
2077
|
+
this.connectionInfo.balance = undefined;
|
|
1788
2078
|
const networkChangedEvent = {
|
|
1789
2079
|
previousNetwork,
|
|
1790
|
-
newNetwork:
|
|
2080
|
+
newNetwork: networkInfo
|
|
1791
2081
|
};
|
|
1792
2082
|
this.emit('networkChanged', networkChangedEvent);
|
|
1793
2083
|
this.logger.log('Network changed:', networkChangedEvent);
|
|
@@ -1796,21 +2086,30 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1796
2086
|
* Handle balance changed event from extension
|
|
1797
2087
|
*/
|
|
1798
2088
|
handleBalanceChanged(data) {
|
|
2089
|
+
// validate balance before caching
|
|
2090
|
+
const balance = validateBalance(data.balance);
|
|
2091
|
+
if (!balance) {
|
|
2092
|
+
this.logger.warn('Received invalid balance in balanceChanged event, ignoring');
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
1799
2095
|
const previousBalance = this.connectionInfo.balance;
|
|
1800
|
-
this.connectionInfo.balance =
|
|
2096
|
+
this.connectionInfo.balance = balance;
|
|
1801
2097
|
const balanceChangedEvent = {
|
|
1802
2098
|
address: this.connectionInfo.address,
|
|
1803
2099
|
previousBalance,
|
|
1804
|
-
newBalance:
|
|
2100
|
+
newBalance: balance
|
|
1805
2101
|
};
|
|
1806
2102
|
this.emit('balanceChanged', balanceChangedEvent);
|
|
1807
|
-
this.logger.log('Balance changed:',
|
|
2103
|
+
this.logger.log('Balance changed:', { public: balance.public });
|
|
1808
2104
|
}
|
|
1809
2105
|
/**
|
|
1810
2106
|
* Handle extension locked event
|
|
1811
2107
|
*/
|
|
1812
2108
|
handleExtensionLocked() {
|
|
2109
|
+
++this._sessionVersion;
|
|
1813
2110
|
this.connectionInfo = { isConnected: false };
|
|
2111
|
+
// emit extensionLocked then disconnect
|
|
2112
|
+
this.emit('extensionLocked', {});
|
|
1814
2113
|
const disconnectEvent = {
|
|
1815
2114
|
reason: 'extension_locked'
|
|
1816
2115
|
};
|
|
@@ -1821,6 +2120,8 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1821
2120
|
* Handle extension unlocked event
|
|
1822
2121
|
*/
|
|
1823
2122
|
handleExtensionUnlocked() {
|
|
2123
|
+
// emit extensionUnlocked
|
|
2124
|
+
this.emit('extensionUnlocked', {});
|
|
1824
2125
|
// Attempt to restore connection
|
|
1825
2126
|
this.getConnectionStatus().catch(() => {
|
|
1826
2127
|
this.logger.warn('Could not restore connection after unlock');
|
|
@@ -1853,6 +2154,8 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1853
2154
|
this.removeAllListeners();
|
|
1854
2155
|
this.connectionInfo = { isConnected: false };
|
|
1855
2156
|
this.isInitialized = false;
|
|
2157
|
+
this._initPromise = null;
|
|
2158
|
+
++this._sessionVersion;
|
|
1856
2159
|
this.logger.log('SDK cleanup complete');
|
|
1857
2160
|
}
|
|
1858
2161
|
}
|
|
@@ -1862,6 +2165,200 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
1862
2165
|
ZeroXIOWallet: ZeroXIOWallet
|
|
1863
2166
|
});
|
|
1864
2167
|
|
|
2168
|
+
/**
|
|
2169
|
+
* RFC-O-1 OctraProvider transport adapter.
|
|
2170
|
+
*
|
|
2171
|
+
* Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
|
|
2172
|
+
* window.octra.isOctra === true
|
|
2173
|
+
* window.octra.request({ method, params }) → Promise<unknown>
|
|
2174
|
+
* window.octra.on(event, listener) / removeListener(event, listener)
|
|
2175
|
+
*
|
|
2176
|
+
* This adapter translates the SDK's internal method names into RFC-O-1 method
|
|
2177
|
+
* names and maps events back to the SDK event vocabulary.
|
|
2178
|
+
*
|
|
2179
|
+
* Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
|
|
2180
|
+
* are passed through as-is; the wallet's request() handles or rejects them.
|
|
2181
|
+
*/
|
|
2182
|
+
/** SDK method → RFC-O-1 method name */
|
|
2183
|
+
const SDK_TO_RFC = {
|
|
2184
|
+
get_network_info: 'octra_networkInfo',
|
|
2185
|
+
switch_network: 'octra_switchNetwork',
|
|
2186
|
+
signMessage: 'octra_signMessage',
|
|
2187
|
+
send_transaction: 'octra_sendTransaction',
|
|
2188
|
+
call_contract: 'octra_callContract',
|
|
2189
|
+
contract_call_view: 'octra_callContract',
|
|
2190
|
+
get_private_balance_info: 'octra_getEncryptedBalance',
|
|
2191
|
+
encrypt_balance: 'octra_encryptBalance',
|
|
2192
|
+
decrypt_balance: 'octra_decryptBalance',
|
|
2193
|
+
send_private_transfer: 'octra_sendPrivateTransfer',
|
|
2194
|
+
claim_private_transfer: 'octra_claimStealth',
|
|
2195
|
+
};
|
|
2196
|
+
/** RFC-O-1 error code → SDK ErrorCode string */
|
|
2197
|
+
const RFC_TO_SDK_ERROR = {
|
|
2198
|
+
4001: 'USER_REJECTED',
|
|
2199
|
+
4100: 'PERMISSION_DENIED',
|
|
2200
|
+
4200: 'NETWORK_ERROR',
|
|
2201
|
+
4900: 'CONNECTION_REFUSED',
|
|
2202
|
+
4901: 'NETWORK_ERROR',
|
|
2203
|
+
};
|
|
2204
|
+
function getProvider() {
|
|
2205
|
+
return typeof window !== 'undefined' ? window.octra : null;
|
|
2206
|
+
}
|
|
2207
|
+
function mapError(err) {
|
|
2208
|
+
const code = RFC_TO_SDK_ERROR[err?.code] ?? 'UNKNOWN_ERROR';
|
|
2209
|
+
return { code, message: err?.message ?? 'Request failed' };
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Build a connect response compatible with what the SDK expects
|
|
2213
|
+
* ({ address, networkInfo, permissions, balance }) by making
|
|
2214
|
+
* three RFC-O-1 calls: octra_requestAccounts, octra_networkInfo, octra_permissions.
|
|
2215
|
+
*/
|
|
2216
|
+
async function rfcConnect(provider, params) {
|
|
2217
|
+
const requestPerms = params?.requestPermissions ?? [];
|
|
2218
|
+
const accounts = (await provider.request({
|
|
2219
|
+
method: 'octra_requestAccounts',
|
|
2220
|
+
params: [{ permissions: requestPerms }],
|
|
2221
|
+
}));
|
|
2222
|
+
const address = accounts?.[0] ?? null;
|
|
2223
|
+
const [networkInfo, permissions] = await Promise.all([
|
|
2224
|
+
provider.request({ method: 'octra_networkInfo' }),
|
|
2225
|
+
provider.request({ method: 'octra_permissions' }),
|
|
2226
|
+
]);
|
|
2227
|
+
return { address, networkInfo, permissions, balance: null };
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Build a getConnectionStatus response by checking octra_accounts.
|
|
2231
|
+
*/
|
|
2232
|
+
async function rfcConnectionStatus(provider) {
|
|
2233
|
+
const accounts = (await provider.request({ method: 'octra_accounts' }));
|
|
2234
|
+
const address = accounts?.[0] ?? null;
|
|
2235
|
+
if (!address)
|
|
2236
|
+
return { isConnected: false };
|
|
2237
|
+
const [networkInfo, permissions] = await Promise.all([
|
|
2238
|
+
provider.request({ method: 'octra_networkInfo' }),
|
|
2239
|
+
provider.request({ method: 'octra_permissions' }),
|
|
2240
|
+
]);
|
|
2241
|
+
return { isConnected: true, address, networkInfo, permissions };
|
|
2242
|
+
}
|
|
2243
|
+
function createOctraProviderAdapter() {
|
|
2244
|
+
let _handler = null;
|
|
2245
|
+
return {
|
|
2246
|
+
name: 'octra-provider',
|
|
2247
|
+
displayName: 'Octra Wallet (RFC-O-1)',
|
|
2248
|
+
detect() {
|
|
2249
|
+
const p = getProvider();
|
|
2250
|
+
return p?.isOctra === true;
|
|
2251
|
+
},
|
|
2252
|
+
postRequest(request) {
|
|
2253
|
+
const { id, method, params } = request;
|
|
2254
|
+
const provider = getProvider();
|
|
2255
|
+
if (!provider) {
|
|
2256
|
+
_handler?.({
|
|
2257
|
+
requestId: id,
|
|
2258
|
+
success: false,
|
|
2259
|
+
error: { code: 'EXTENSION_NOT_FOUND', message: 'No RFC-O-1 provider found on window.octra' },
|
|
2260
|
+
});
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
(async () => {
|
|
2264
|
+
try {
|
|
2265
|
+
let data;
|
|
2266
|
+
if (method === 'ping') {
|
|
2267
|
+
data = { available: true };
|
|
2268
|
+
}
|
|
2269
|
+
else if (method === 'register_dapp') {
|
|
2270
|
+
data = { success: true };
|
|
2271
|
+
}
|
|
2272
|
+
else if (method === 'connect') {
|
|
2273
|
+
data = await rfcConnect(provider, params);
|
|
2274
|
+
}
|
|
2275
|
+
else if (method === 'disconnect') {
|
|
2276
|
+
await provider.request({ method: 'disconnect' }).catch(() => { });
|
|
2277
|
+
data = { success: true };
|
|
2278
|
+
}
|
|
2279
|
+
else if (method === 'getConnectionStatus') {
|
|
2280
|
+
data = await rfcConnectionStatus(provider);
|
|
2281
|
+
}
|
|
2282
|
+
else {
|
|
2283
|
+
const rfcMethod = SDK_TO_RFC[method] ?? method;
|
|
2284
|
+
data = await provider.request({ method: rfcMethod, params });
|
|
2285
|
+
}
|
|
2286
|
+
_handler?.({ requestId: id, success: true, data });
|
|
2287
|
+
}
|
|
2288
|
+
catch (err) {
|
|
2289
|
+
_handler?.({ requestId: id, success: false, error: mapError(err) });
|
|
2290
|
+
}
|
|
2291
|
+
})();
|
|
2292
|
+
},
|
|
2293
|
+
listen(handler) {
|
|
2294
|
+
_handler = handler;
|
|
2295
|
+
const provider = getProvider();
|
|
2296
|
+
if (!provider)
|
|
2297
|
+
return () => { _handler = null; };
|
|
2298
|
+
// RFC-O-1 event → SDK event name + data shape
|
|
2299
|
+
const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
|
|
2300
|
+
const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
|
|
2301
|
+
const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
|
|
2302
|
+
const onNetworkChanged = (data) => handler({ eventType: 'networkChanged', eventData: { networkInfo: data } });
|
|
2303
|
+
const onBalanceChanged = (data) => handler({ eventType: 'balanceChanged', eventData: data });
|
|
2304
|
+
const onTransactionChanged = (data) => handler({ eventType: 'transactionConfirmed', eventData: data });
|
|
2305
|
+
provider.on('connect', onConnect);
|
|
2306
|
+
provider.on('disconnect', onDisconnect);
|
|
2307
|
+
provider.on('accountsChanged', onAccountsChanged);
|
|
2308
|
+
provider.on('networkChanged', onNetworkChanged);
|
|
2309
|
+
provider.on('balanceChanged', onBalanceChanged);
|
|
2310
|
+
provider.on('transactionChanged', onTransactionChanged);
|
|
2311
|
+
const cleanup = () => {
|
|
2312
|
+
provider.removeListener('connect', onConnect);
|
|
2313
|
+
provider.removeListener('disconnect', onDisconnect);
|
|
2314
|
+
provider.removeListener('accountsChanged', onAccountsChanged);
|
|
2315
|
+
provider.removeListener('networkChanged', onNetworkChanged);
|
|
2316
|
+
provider.removeListener('balanceChanged', onBalanceChanged);
|
|
2317
|
+
provider.removeListener('transactionChanged', onTransactionChanged);
|
|
2318
|
+
_handler = null;
|
|
2319
|
+
};
|
|
2320
|
+
return cleanup;
|
|
2321
|
+
},
|
|
2322
|
+
listenForReady(onReady) {
|
|
2323
|
+
const handler = () => onReady();
|
|
2324
|
+
window.addEventListener('octraWalletReady', handler);
|
|
2325
|
+
return () => window.removeEventListener('octraWalletReady', handler);
|
|
2326
|
+
},
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
/** Default RFC-O-1 adapter instance. */
|
|
2330
|
+
const OctraProviderAdapter = createOctraProviderAdapter();
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* 0xio SDK — Wallet Adapter Registry
|
|
2334
|
+
*
|
|
2335
|
+
* Add new wallet adapters here. Detection order determines which wallet takes
|
|
2336
|
+
* priority when multiple wallets are installed at the same time.
|
|
2337
|
+
*/
|
|
2338
|
+
const REGISTERED_ADAPTERS = [
|
|
2339
|
+
ZeroXIOAdapter, // 0xio extension (postMessage protocol) — highest priority
|
|
2340
|
+
OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
|
|
2341
|
+
// Add new wallet adapters here — detection runs in order, first match wins
|
|
2342
|
+
];
|
|
2343
|
+
/**
|
|
2344
|
+
* Auto-detects the first available wallet in the current page.
|
|
2345
|
+
* Returns null if no supported wallet is found.
|
|
2346
|
+
*
|
|
2347
|
+
* @example
|
|
2348
|
+
* const adapter = detectWalletAdapter();
|
|
2349
|
+
* if (!adapter) throw new Error('No supported wallet found');
|
|
2350
|
+
* const wallet = new ZeroXIOWallet({ appName: 'My DApp', adapter });
|
|
2351
|
+
*/
|
|
2352
|
+
function detectWalletAdapter() {
|
|
2353
|
+
if (typeof window === 'undefined')
|
|
2354
|
+
return null;
|
|
2355
|
+
return REGISTERED_ADAPTERS.find((a) => a.detect()) ?? null;
|
|
2356
|
+
}
|
|
2357
|
+
/** Returns all registered adapter instances. */
|
|
2358
|
+
function getAllAdapters() {
|
|
2359
|
+
return [...REGISTERED_ADAPTERS];
|
|
2360
|
+
}
|
|
2361
|
+
|
|
1865
2362
|
/**
|
|
1866
2363
|
* 0xio Wallet SDK - Main Entry Point
|
|
1867
2364
|
* Official SDK for integrating with 0xio Wallet Extension
|
|
@@ -1888,7 +2385,7 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
1888
2385
|
*/
|
|
1889
2386
|
// Main exports
|
|
1890
2387
|
// Version information
|
|
1891
|
-
const SDK_VERSION = '2.
|
|
2388
|
+
const SDK_VERSION = '2.7.0';
|
|
1892
2389
|
const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
|
|
1893
2390
|
const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
|
|
1894
2391
|
const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
|
|
@@ -1898,7 +2395,8 @@ async function createZeroXIOWallet(config) {
|
|
|
1898
2395
|
const wallet$1 = new ZeroXIOWallet({
|
|
1899
2396
|
appName: config.appName,
|
|
1900
2397
|
appDescription: config.appDescription,
|
|
1901
|
-
debug: config.debug || false
|
|
2398
|
+
debug: config.debug || false,
|
|
2399
|
+
...(config.adapter ? { adapter: config.adapter } : {}),
|
|
1902
2400
|
});
|
|
1903
2401
|
await wallet$1.initialize();
|
|
1904
2402
|
if (config.autoConnect) {
|
|
@@ -1912,23 +2410,35 @@ async function createZeroXIOWallet(config) {
|
|
|
1912
2410
|
}
|
|
1913
2411
|
return wallet$1;
|
|
1914
2412
|
}
|
|
1915
|
-
// Legacy alias for backward compatibility
|
|
1916
|
-
const createOctraWallet = createZeroXIOWallet;
|
|
1917
2413
|
// Browser detection and compatibility check
|
|
1918
2414
|
function checkSDKCompatibility() {
|
|
1919
2415
|
const issues = [];
|
|
1920
2416
|
const recommendations = [];
|
|
1921
|
-
//
|
|
2417
|
+
// Hard blockers — SDK cannot function without these
|
|
1922
2418
|
if (typeof window === 'undefined') {
|
|
1923
2419
|
issues.push('Window object not available');
|
|
1924
2420
|
recommendations.push('SDK must be used in a browser environment');
|
|
2421
|
+
return { compatible: false, issues, recommendations };
|
|
2422
|
+
}
|
|
2423
|
+
if (typeof window.postMessage !== 'function') {
|
|
2424
|
+
issues.push('postMessage API not available');
|
|
2425
|
+
recommendations.push('Your browser environment must support postMessage');
|
|
2426
|
+
}
|
|
2427
|
+
if (typeof window.addEventListener !== 'function') {
|
|
2428
|
+
issues.push('addEventListener not available');
|
|
2429
|
+
}
|
|
2430
|
+
if (typeof Promise === 'undefined') {
|
|
2431
|
+
issues.push('Promise not available');
|
|
1925
2432
|
}
|
|
1926
|
-
//
|
|
1927
|
-
if (
|
|
2433
|
+
// Informational: note which transport is likely active
|
|
2434
|
+
if (issues.length === 0) {
|
|
1928
2435
|
const win = window;
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2436
|
+
const hasExtension = !!(win.wallet0xio || win.ZeroXIOWallet || win.chrome?.runtime?.id ||
|
|
2437
|
+
document.querySelector('meta[name="0xio-dapp"]') || document.querySelector('[data-0xio-sdk-bridge]'));
|
|
2438
|
+
const hasParentBridge = window.parent !== window;
|
|
2439
|
+
if (!hasExtension && !hasParentBridge) {
|
|
2440
|
+
recommendations.push('No 0xio transport detected yet. Install the 0xio Wallet browser extension, ' +
|
|
2441
|
+
'or run inside the 0xio Desktop/Mobile app iframe bridge.');
|
|
1932
2442
|
}
|
|
1933
2443
|
}
|
|
1934
2444
|
return {
|
|
@@ -1984,5 +2494,5 @@ if (typeof window !== 'undefined') {
|
|
|
1984
2494
|
}
|
|
1985
2495
|
}
|
|
1986
2496
|
|
|
1987
|
-
export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, SDK_CONFIG, SDK_VERSION, SUPPORTED_EXTENSION_VERSIONS, ZeroXIOWallet, ZeroXIOWalletError, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger,
|
|
2497
|
+
export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, OctraProviderAdapter, SDK_CONFIG, SDK_VERSION, SUPPORTED_EXTENSION_VERSIONS, ZeroXIOAdapter, ZeroXIOWallet, ZeroXIOWalletError, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger, createOctraProviderAdapter, createZeroXIOAdapter, createZeroXIOWallet, delay, deriveOctraAddress, detectWalletAdapter, formatAddress, formatOCT, formatTimestamp, formatTxHash, formatOCT as formatZeroXIO, fromMicroOCT, fromMicroOCT as fromMicroZeroXIO, generateMockData, getAllAdapters, getAllNetworks, getDefaultNetwork, getNetworkConfig, isBrowser, isErrorType, isValidAddress, isValidAmount, isValidFeeLevel, isValidMessage, isValidNetworkId, toMicroOCT, toMicroOCT as toMicroZeroXIO };
|
|
1988
2498
|
//# sourceMappingURL=index.esm.js.map
|