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