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