@0xio/sdk 2.6.0 → 2.7.1
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 +75 -0
- package/README.md +77 -2
- package/dist/index.d.ts +326 -336
- package/dist/index.esm.js +1053 -682
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1059 -684
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +1059 -684
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 0xio Wallet SDK - Event System
|
|
3
|
-
* Type-safe event emitter for wallet events
|
|
4
|
-
*/
|
|
5
1
|
class EventEmitter {
|
|
6
|
-
constructor(
|
|
2
|
+
constructor(_debug = false) {
|
|
7
3
|
this.listeners = new Map();
|
|
8
|
-
this.debug = debug;
|
|
9
4
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Add event listener
|
|
12
|
-
*/
|
|
13
5
|
on(eventType, listener) {
|
|
14
6
|
if (!this.listeners.has(eventType)) {
|
|
15
7
|
this.listeners.set(eventType, new Set());
|
|
16
8
|
}
|
|
17
9
|
this.listeners.get(eventType).add(listener);
|
|
18
|
-
if (this.debug) ;
|
|
19
10
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Remove event listener
|
|
22
|
-
*/
|
|
23
11
|
off(eventType, listener) {
|
|
24
12
|
const eventListeners = this.listeners.get(eventType);
|
|
25
13
|
if (eventListeners) {
|
|
@@ -27,22 +15,16 @@ class EventEmitter {
|
|
|
27
15
|
if (eventListeners.size === 0) {
|
|
28
16
|
this.listeners.delete(eventType);
|
|
29
17
|
}
|
|
30
|
-
if (this.debug) ;
|
|
31
18
|
}
|
|
32
19
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Add one-time event listener
|
|
35
|
-
*/
|
|
36
20
|
once(eventType, listener) {
|
|
37
21
|
const onceListener = (event) => {
|
|
38
|
-
listener
|
|
22
|
+
// Remove BEFORE calling so a throwing listener doesn't stay registered
|
|
39
23
|
this.off(eventType, onceListener);
|
|
24
|
+
listener(event);
|
|
40
25
|
};
|
|
41
26
|
this.on(eventType, onceListener);
|
|
42
27
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Emit event to all listeners
|
|
45
|
-
*/
|
|
46
28
|
emit(eventType, data) {
|
|
47
29
|
const event = {
|
|
48
30
|
type: eventType,
|
|
@@ -51,49 +33,32 @@ class EventEmitter {
|
|
|
51
33
|
};
|
|
52
34
|
const eventListeners = this.listeners.get(eventType);
|
|
53
35
|
if (eventListeners && eventListeners.size > 0) {
|
|
54
|
-
if
|
|
55
|
-
|
|
56
|
-
const listenersArray = Array.from(eventListeners);
|
|
57
|
-
for (const listener of listenersArray) {
|
|
36
|
+
// snapshot to avoid issues if listeners modify the set during iteration
|
|
37
|
+
for (const listener of Array.from(eventListeners)) {
|
|
58
38
|
try {
|
|
59
39
|
listener(event);
|
|
60
40
|
}
|
|
61
|
-
catch
|
|
62
|
-
//
|
|
41
|
+
catch {
|
|
42
|
+
// listener errors are swallowed to keep the event loop running
|
|
63
43
|
}
|
|
64
44
|
}
|
|
65
45
|
}
|
|
66
|
-
else if (this.debug) ;
|
|
67
46
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Remove all listeners for a specific event type
|
|
70
|
-
*/
|
|
71
47
|
removeAllListeners(eventType) {
|
|
72
48
|
if (eventType) {
|
|
73
49
|
this.listeners.delete(eventType);
|
|
74
|
-
if (this.debug) ;
|
|
75
50
|
}
|
|
76
51
|
else {
|
|
77
52
|
this.listeners.clear();
|
|
78
|
-
if (this.debug) ;
|
|
79
53
|
}
|
|
80
54
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Get number of listeners for an event type
|
|
83
|
-
*/
|
|
84
55
|
listenerCount(eventType) {
|
|
85
56
|
const eventListeners = this.listeners.get(eventType);
|
|
86
57
|
return eventListeners ? eventListeners.size : 0;
|
|
87
58
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Get all event types that have listeners
|
|
90
|
-
*/
|
|
91
59
|
eventTypes() {
|
|
92
60
|
return Array.from(this.listeners.keys());
|
|
93
61
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Check if there are any listeners for an event type
|
|
96
|
-
*/
|
|
97
62
|
hasListeners(eventType) {
|
|
98
63
|
return this.listenerCount(eventType) > 0;
|
|
99
64
|
}
|
|
@@ -138,15 +103,134 @@ class ZeroXIOWalletError extends Error {
|
|
|
138
103
|
}
|
|
139
104
|
|
|
140
105
|
/**
|
|
141
|
-
* 0xio Wallet
|
|
142
|
-
*
|
|
106
|
+
* 0xio Wallet transport adapter.
|
|
107
|
+
*
|
|
108
|
+
* Implements the postMessage protocol used by the 0xio browser extension (>= v2.4.0).
|
|
109
|
+
*
|
|
110
|
+
* Outbound wire format:
|
|
111
|
+
* window.postMessage({ source: '0xio-sdk-request', request: { id, method, params, timestamp } }, origin)
|
|
112
|
+
*
|
|
113
|
+
* Inbound wire format:
|
|
114
|
+
* window.postMessage({ source: '0xio-sdk-bridge', response: { id, success, data, error }, sessionNonce })
|
|
115
|
+
* window.postMessage({ source: '0xio-sdk-bridge', event: { type, data } })
|
|
116
|
+
*
|
|
117
|
+
* H-2: Session nonce validation — injected.ts broadcasts the nonce received from the
|
|
118
|
+
* isolated content script. Once set, any '0xio-sdk-bridge' response with a missing or
|
|
119
|
+
* mismatched nonce is rejected, preventing response injection by malicious page scripts.
|
|
143
120
|
*/
|
|
144
|
-
// ===================
|
|
145
|
-
// VALIDATION UTILITIES
|
|
146
|
-
// ===================
|
|
147
121
|
/**
|
|
148
|
-
*
|
|
122
|
+
* Creates a 0xio adapter. The factory accepts optional extra trusted parent origins
|
|
123
|
+
* so the communicator can forward its own trustedOrigins setting to origin validation.
|
|
149
124
|
*/
|
|
125
|
+
function createZeroXIOAdapter(extraTrustedOrigins = []) {
|
|
126
|
+
return {
|
|
127
|
+
name: '0xio',
|
|
128
|
+
displayName: '0xio Wallet',
|
|
129
|
+
detect() {
|
|
130
|
+
if (typeof window === 'undefined')
|
|
131
|
+
return false;
|
|
132
|
+
const win = window;
|
|
133
|
+
return !!(win.wallet0xio ||
|
|
134
|
+
win.ZeroXIOWallet ||
|
|
135
|
+
// 0xio extension also exposes window.octra with isOctra=true; match it here
|
|
136
|
+
// so this adapter (postMessage protocol) takes priority over OctraProviderAdapter
|
|
137
|
+
(win.octra?.isOctra && (win.wallet0xio || win.ZeroXIOWallet)) ||
|
|
138
|
+
win.chrome?.runtime?.id ||
|
|
139
|
+
document.querySelector('meta[name="0xio-dapp"]') ||
|
|
140
|
+
document.querySelector('[data-0xio-sdk-bridge]'));
|
|
141
|
+
},
|
|
142
|
+
postRequest(request) {
|
|
143
|
+
window.postMessage({ source: '0xio-sdk-request', request }, window.location.origin);
|
|
144
|
+
},
|
|
145
|
+
postRequestToParent(request, parentOrigin) {
|
|
146
|
+
try {
|
|
147
|
+
window.parent.postMessage({ source: '0xio-sdk-request', request }, parentOrigin);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Do not fall back to '*' — silent failure is safer
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
listen(handler, options) {
|
|
154
|
+
const allowedOrigin = window.location.origin;
|
|
155
|
+
const trustedParentOrigins = new Set([
|
|
156
|
+
allowedOrigin,
|
|
157
|
+
'tauri://localhost',
|
|
158
|
+
'https://tauri.localhost',
|
|
159
|
+
'http://localhost',
|
|
160
|
+
'https://localhost',
|
|
161
|
+
...(extraTrustedOrigins),
|
|
162
|
+
...(options?.trustedParentOrigins ?? []),
|
|
163
|
+
]);
|
|
164
|
+
let _sessionNonce = null;
|
|
165
|
+
// H-2: receive session nonce from injected.ts (MAIN world content script)
|
|
166
|
+
const nonceListener = (e) => {
|
|
167
|
+
if (e.origin !== allowedOrigin)
|
|
168
|
+
return;
|
|
169
|
+
if (e.data?.source === '0xio-sdk-nonce-init' && typeof e.data.nonce === 'string') {
|
|
170
|
+
_sessionNonce = e.data.nonce;
|
|
171
|
+
window.removeEventListener('message', nonceListener);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
window.addEventListener('message', nonceListener);
|
|
175
|
+
const msgListener = (e) => {
|
|
176
|
+
const isFromSameOrigin = e.origin === allowedOrigin;
|
|
177
|
+
const isLocalhost = e.origin.startsWith('http://localhost:') ||
|
|
178
|
+
e.origin.startsWith('http://127.0.0.1:');
|
|
179
|
+
const isFromTrustedParent = e.source === window.parent &&
|
|
180
|
+
window.parent !== window &&
|
|
181
|
+
(trustedParentOrigins.has(e.origin) || isLocalhost);
|
|
182
|
+
if (!isFromSameOrigin && !isFromTrustedParent)
|
|
183
|
+
return;
|
|
184
|
+
if (e.source !== window && e.source !== window.parent)
|
|
185
|
+
return;
|
|
186
|
+
if (!e.data || e.data.source !== '0xio-sdk-bridge')
|
|
187
|
+
return;
|
|
188
|
+
// H-2: session nonce validation.
|
|
189
|
+
// Preferred path: nonce set via 0xio-sdk-nonce-init from injected.ts.
|
|
190
|
+
// Fallback path: if the init broadcast was missed (race between document_start
|
|
191
|
+
// content script and page script load), capture nonce from the first same-origin
|
|
192
|
+
// response so that all subsequent responses are validated.
|
|
193
|
+
if (!_sessionNonce && isFromSameOrigin && typeof e.data.sessionNonce === 'string') {
|
|
194
|
+
_sessionNonce = e.data.sessionNonce;
|
|
195
|
+
}
|
|
196
|
+
if (_sessionNonce && e.data.sessionNonce !== _sessionNonce)
|
|
197
|
+
return;
|
|
198
|
+
if (e.data.response) {
|
|
199
|
+
const r = e.data.response;
|
|
200
|
+
handler({
|
|
201
|
+
requestId: r.id,
|
|
202
|
+
success: r.success,
|
|
203
|
+
data: r.data,
|
|
204
|
+
error: r.error,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
else if (e.data.event) {
|
|
208
|
+
handler({
|
|
209
|
+
eventType: e.data.event.type,
|
|
210
|
+
eventData: e.data.event.data ?? e.data.event,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
window.addEventListener('message', msgListener);
|
|
215
|
+
return () => {
|
|
216
|
+
window.removeEventListener('message', nonceListener);
|
|
217
|
+
window.removeEventListener('message', msgListener);
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
listenForReady(onReady) {
|
|
221
|
+
const handler = () => onReady();
|
|
222
|
+
window.addEventListener('0xioWalletReady', handler);
|
|
223
|
+
window.addEventListener('wallet0xioReady', handler);
|
|
224
|
+
return () => {
|
|
225
|
+
window.removeEventListener('0xioWalletReady', handler);
|
|
226
|
+
window.removeEventListener('wallet0xioReady', handler);
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Default 0xio adapter instance (no extra trusted origins). */
|
|
232
|
+
const ZeroXIOAdapter = createZeroXIOAdapter();
|
|
233
|
+
|
|
150
234
|
function isValidAddress(address) {
|
|
151
235
|
if (!address || typeof address !== 'string') {
|
|
152
236
|
return false;
|
|
@@ -156,51 +240,89 @@ function isValidAddress(address) {
|
|
|
156
240
|
return addressRegex.test(address);
|
|
157
241
|
}
|
|
158
242
|
/**
|
|
159
|
-
* Validate transaction amount
|
|
243
|
+
* Validate transaction amount.
|
|
244
|
+
* Accepts both number and string representations.
|
|
245
|
+
* String amounts avoid JS number precision loss for very large values.
|
|
160
246
|
*/
|
|
161
247
|
function isValidAmount(amount) {
|
|
248
|
+
if (typeof amount === 'string') {
|
|
249
|
+
const n = parseFloat(amount);
|
|
250
|
+
return !isNaN(n) && n > 0 && Number.isFinite(n);
|
|
251
|
+
}
|
|
162
252
|
return typeof amount === 'number' &&
|
|
163
253
|
amount > 0 &&
|
|
164
254
|
Number.isFinite(amount) &&
|
|
165
255
|
amount <= Number.MAX_SAFE_INTEGER;
|
|
166
256
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Validate transaction message
|
|
169
|
-
*/
|
|
170
257
|
function isValidMessage(message) {
|
|
171
|
-
|
|
172
|
-
return true; // Empty messages are valid
|
|
173
|
-
}
|
|
258
|
+
// Type check first — falsy non-strings (0, false, null) are NOT valid messages
|
|
174
259
|
if (typeof message !== 'string') {
|
|
175
|
-
return false;
|
|
260
|
+
return message === undefined || message === null ? true : false;
|
|
261
|
+
}
|
|
262
|
+
// Empty string is valid (optional field)
|
|
263
|
+
if (message.length === 0) {
|
|
264
|
+
return true;
|
|
176
265
|
}
|
|
177
266
|
// 100KB limit — contract call params can be large (serialized JSON)
|
|
178
267
|
return message.length <= 100000;
|
|
179
268
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Validate fee level
|
|
182
|
-
*/
|
|
183
269
|
function isValidFeeLevel(feeLevel) {
|
|
184
270
|
return feeLevel === 1 || feeLevel === 3;
|
|
185
271
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
272
|
+
const _B58_ALPHA = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
273
|
+
function _base58Encode(buf) {
|
|
274
|
+
let zeros = 0;
|
|
275
|
+
for (let i = 0; i < buf.length && buf[i] === 0; i++)
|
|
276
|
+
zeros++;
|
|
277
|
+
const digits = [];
|
|
278
|
+
for (let i = zeros; i < buf.length; i++) {
|
|
279
|
+
let carry = buf[i];
|
|
280
|
+
for (let j = 0; j < digits.length; j++) {
|
|
281
|
+
carry += digits[j] << 8;
|
|
282
|
+
digits[j] = carry % 58;
|
|
283
|
+
carry = Math.floor(carry / 58);
|
|
284
|
+
}
|
|
285
|
+
while (carry > 0) {
|
|
286
|
+
digits.push(carry % 58);
|
|
287
|
+
carry = Math.floor(carry / 58);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
let out = '';
|
|
291
|
+
for (let i = 0; i < zeros; i++)
|
|
292
|
+
out += _B58_ALPHA[0];
|
|
293
|
+
for (let i = digits.length - 1; i >= 0; i--)
|
|
294
|
+
out += _B58_ALPHA[digits[i]];
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
189
297
|
/**
|
|
190
|
-
*
|
|
298
|
+
* Derive the canonical Octra address from a base64-encoded Ed25519 public key.
|
|
299
|
+
* Algorithm: SHA-256(pubkey_bytes) → base58 → prepend "oct"
|
|
300
|
+
* Source of truth: ocho-push-server/src/index.ts#deriveAddressFromPubkey
|
|
191
301
|
*/
|
|
302
|
+
async function deriveOctraAddress(publicKeyBase64) {
|
|
303
|
+
if (!publicKeyBase64 || typeof publicKeyBase64 !== 'string') {
|
|
304
|
+
throw new Error('publicKeyBase64 must be a non-empty string');
|
|
305
|
+
}
|
|
306
|
+
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
307
|
+
throw new Error('Web Crypto API is not available in this environment');
|
|
308
|
+
}
|
|
309
|
+
const binStr = atob(publicKeyBase64);
|
|
310
|
+
const bytes = new Uint8Array(binStr.length);
|
|
311
|
+
for (let i = 0; i < binStr.length; i++)
|
|
312
|
+
bytes[i] = binStr.charCodeAt(i);
|
|
313
|
+
const hashBuf = await crypto.subtle.digest('SHA-256', bytes);
|
|
314
|
+
return 'oct' + _base58Encode(new Uint8Array(hashBuf));
|
|
315
|
+
}
|
|
192
316
|
function formatOCT(amount, decimals = 6) {
|
|
193
|
-
|
|
317
|
+
const n = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
318
|
+
if (!isValidAmount(n)) {
|
|
194
319
|
return '0';
|
|
195
320
|
}
|
|
196
|
-
return
|
|
321
|
+
return n.toLocaleString(undefined, {
|
|
197
322
|
minimumFractionDigits: 0,
|
|
198
323
|
maximumFractionDigits: decimals
|
|
199
324
|
});
|
|
200
325
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Format address for display (truncated)
|
|
203
|
-
*/
|
|
204
326
|
function formatAddress(address, prefixLength = 6, suffixLength = 4) {
|
|
205
327
|
if (!isValidAddress(address)) {
|
|
206
328
|
return 'Invalid Address';
|
|
@@ -210,16 +332,10 @@ function formatAddress(address, prefixLength = 6, suffixLength = 4) {
|
|
|
210
332
|
}
|
|
211
333
|
return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}`;
|
|
212
334
|
}
|
|
213
|
-
/**
|
|
214
|
-
* Format timestamp for display
|
|
215
|
-
*/
|
|
216
335
|
function formatTimestamp(timestamp) {
|
|
217
336
|
const date = new Date(timestamp);
|
|
218
337
|
return date.toLocaleString();
|
|
219
338
|
}
|
|
220
|
-
/**
|
|
221
|
-
* Format transaction hash for display
|
|
222
|
-
*/
|
|
223
339
|
function formatTxHash(hash, length = 12) {
|
|
224
340
|
if (!hash || typeof hash !== 'string') {
|
|
225
341
|
return 'Invalid Hash';
|
|
@@ -231,12 +347,6 @@ function formatTxHash(hash, length = 12) {
|
|
|
231
347
|
const suffixLength = Math.floor(length / 2);
|
|
232
348
|
return `${hash.slice(0, prefixLength)}...${hash.slice(-suffixLength)}`;
|
|
233
349
|
}
|
|
234
|
-
// ===================
|
|
235
|
-
// CONVERSION UTILITIES
|
|
236
|
-
// ===================
|
|
237
|
-
/**
|
|
238
|
-
* Convert OCT to micro OCT (for network transmission)
|
|
239
|
-
*/
|
|
240
350
|
function toMicroOCT(amount) {
|
|
241
351
|
if (!isValidAmount(amount)) {
|
|
242
352
|
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, 'Invalid amount for conversion');
|
|
@@ -245,9 +355,6 @@ function toMicroOCT(amount) {
|
|
|
245
355
|
const microOCT = Math.round(amount * 1000000);
|
|
246
356
|
return microOCT.toString();
|
|
247
357
|
}
|
|
248
|
-
/**
|
|
249
|
-
* Convert micro OCT to OCT (for display)
|
|
250
|
-
*/
|
|
251
358
|
function fromMicroOCT(microAmount) {
|
|
252
359
|
const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
|
|
253
360
|
if (!Number.isFinite(amount) || amount < 0) {
|
|
@@ -255,12 +362,6 @@ function fromMicroOCT(microAmount) {
|
|
|
255
362
|
}
|
|
256
363
|
return amount / 1000000;
|
|
257
364
|
}
|
|
258
|
-
// ===================
|
|
259
|
-
// ERROR UTILITIES
|
|
260
|
-
// ===================
|
|
261
|
-
/**
|
|
262
|
-
* Create standardized error messages
|
|
263
|
-
*/
|
|
264
365
|
function createErrorMessage(code, context) {
|
|
265
366
|
const baseMessages = {
|
|
266
367
|
[ErrorCode.EXTENSION_NOT_FOUND]: '0xio Wallet extension is not installed or enabled',
|
|
@@ -287,24 +388,12 @@ function createErrorMessage(code, context) {
|
|
|
287
388
|
const baseMessage = baseMessages[code] || 'Unknown error';
|
|
288
389
|
return context ? `${baseMessage}: ${context}` : baseMessage;
|
|
289
390
|
}
|
|
290
|
-
/**
|
|
291
|
-
* Check if error is a specific type
|
|
292
|
-
*/
|
|
293
391
|
function isErrorType(error, code) {
|
|
294
392
|
return error instanceof ZeroXIOWalletError && error.code === code;
|
|
295
393
|
}
|
|
296
|
-
// ===================
|
|
297
|
-
// ASYNC UTILITIES
|
|
298
|
-
// ===================
|
|
299
|
-
/**
|
|
300
|
-
* Create a promise that resolves after a delay
|
|
301
|
-
*/
|
|
302
394
|
function delay(ms) {
|
|
303
395
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
304
396
|
}
|
|
305
|
-
/**
|
|
306
|
-
* Retry an async operation with exponential backoff
|
|
307
|
-
*/
|
|
308
397
|
async function retry(operation, maxRetries = 3, baseDelay = 1000) {
|
|
309
398
|
let lastError;
|
|
310
399
|
// maxRetries = number of retries AFTER the first attempt
|
|
@@ -344,18 +433,9 @@ function withTimeout(promise, timeoutMs, timeoutMessage = 'Operation timed out')
|
|
|
344
433
|
clearTimeout(timer);
|
|
345
434
|
});
|
|
346
435
|
}
|
|
347
|
-
// ===================
|
|
348
|
-
// BROWSER UTILITIES
|
|
349
|
-
// ===================
|
|
350
|
-
/**
|
|
351
|
-
* Check if running in browser environment
|
|
352
|
-
*/
|
|
353
436
|
function isBrowser() {
|
|
354
437
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
355
438
|
}
|
|
356
|
-
/**
|
|
357
|
-
* Check if browser supports required features
|
|
358
|
-
*/
|
|
359
439
|
function checkBrowserSupport() {
|
|
360
440
|
const missingFeatures = [];
|
|
361
441
|
if (!isBrowser()) {
|
|
@@ -377,12 +457,6 @@ function checkBrowserSupport() {
|
|
|
377
457
|
missingFeatures
|
|
378
458
|
};
|
|
379
459
|
}
|
|
380
|
-
// ===================
|
|
381
|
-
// DEVELOPMENT UTILITIES
|
|
382
|
-
// ===================
|
|
383
|
-
/**
|
|
384
|
-
* Generate mock data for development/testing
|
|
385
|
-
*/
|
|
386
460
|
function generateMockData() {
|
|
387
461
|
return {
|
|
388
462
|
address: 'oct' + Math.random().toString(36).substring(2, 22) + Math.random().toString(36).substring(2, 26),
|
|
@@ -395,7 +469,7 @@ function generateMockData() {
|
|
|
395
469
|
networkInfo: {
|
|
396
470
|
id: 'mainnet',
|
|
397
471
|
name: 'Octra Mainnet',
|
|
398
|
-
rpcUrl: '
|
|
472
|
+
rpcUrl: 'https://octra.network',
|
|
399
473
|
explorerUrl: 'https://lite.octrascan.io/tx.html?hash=',
|
|
400
474
|
explorerAddressUrl: 'https://lite.octrascan.io/address.html?addr=',
|
|
401
475
|
indexerUrl: 'https://lite.octrascan.io',
|
|
@@ -405,9 +479,6 @@ function generateMockData() {
|
|
|
405
479
|
}
|
|
406
480
|
};
|
|
407
481
|
}
|
|
408
|
-
/**
|
|
409
|
-
* Create development logger
|
|
410
|
-
*/
|
|
411
482
|
function createLogger(prefix, debug) {
|
|
412
483
|
const isDevelopment = typeof window !== 'undefined' && ((typeof globalThis !== 'undefined' && globalThis.process?.env?.NODE_ENV === 'development') ||
|
|
413
484
|
window.location.hostname === 'localhost' ||
|
|
@@ -460,116 +531,51 @@ function createLogger(prefix, debug) {
|
|
|
460
531
|
};
|
|
461
532
|
}
|
|
462
533
|
|
|
463
|
-
/**
|
|
464
|
-
* 0xio Wallet SDK - Extension Communication Module
|
|
465
|
-
*
|
|
466
|
-
* @fileoverview Manages secure communication between the SDK and browser extension.
|
|
467
|
-
* Implements message passing, request/response handling, rate limiting, and origin validation
|
|
468
|
-
* to ensure secure wallet interactions.
|
|
469
|
-
*
|
|
470
|
-
* @module communication
|
|
471
|
-
* @version 2.6.0
|
|
472
|
-
* @license MIT
|
|
473
|
-
*/
|
|
474
|
-
/**
|
|
475
|
-
* ExtensionCommunicator - Manages communication with the 0xio Wallet browser extension
|
|
476
|
-
*
|
|
477
|
-
* @class
|
|
478
|
-
* @extends EventEmitter
|
|
479
|
-
*
|
|
480
|
-
* @description
|
|
481
|
-
* Handles all communication between the SDK and wallet extension including:
|
|
482
|
-
* - Request/response message passing with origin validation
|
|
483
|
-
* - Rate limiting to prevent DoS attacks
|
|
484
|
-
* - Automatic retry logic with exponential backoff
|
|
485
|
-
* - Extension detection and availability monitoring
|
|
486
|
-
* - Cryptographically secure request ID generation
|
|
487
|
-
*
|
|
488
|
-
* @example
|
|
489
|
-
* ```typescript
|
|
490
|
-
* const communicator = new ExtensionCommunicator(true); // debug mode
|
|
491
|
-
* await communicator.initialize();
|
|
492
|
-
*
|
|
493
|
-
* const response = await communicator.sendRequest('get_balance', {});
|
|
494
|
-
* console.log(response);
|
|
495
|
-
* ```
|
|
496
|
-
*/
|
|
497
534
|
class ExtensionCommunicator extends EventEmitter {
|
|
498
|
-
|
|
499
|
-
* Creates a new ExtensionCommunicator instance
|
|
500
|
-
*
|
|
501
|
-
* @param {boolean} debug - Enable debug logging
|
|
502
|
-
*/
|
|
503
|
-
constructor(debug = false, trustedOrigins = []) {
|
|
535
|
+
constructor(debug = false, trustedOrigins = [], adapter) {
|
|
504
536
|
super(debug);
|
|
505
|
-
/** Legacy request counter (deprecated, kept for fallback) */
|
|
506
|
-
this.requestId = 0;
|
|
507
|
-
/** Map of pending requests awaiting responses */
|
|
508
537
|
this.pendingRequests = new Map();
|
|
509
|
-
/** Initialization state flag */
|
|
510
538
|
this.isInitialized = false;
|
|
511
|
-
/** Interval handle for periodic extension detection */
|
|
512
539
|
this.extensionDetectionInterval = null;
|
|
513
|
-
/** Current extension availability state */
|
|
514
540
|
this.isExtensionAvailableState = false;
|
|
515
|
-
/** Message listener reference for cleanup */
|
|
516
|
-
this.messageListener = null;
|
|
517
|
-
/** Trusted parent origins for iframe communication */
|
|
518
541
|
this.trustedOrigins = [];
|
|
519
|
-
/** Parent origin learned from walletReady signal */
|
|
520
542
|
this._parentOrigin = null;
|
|
521
|
-
|
|
522
|
-
|
|
543
|
+
/** Teardown fn returned by adapter.listen() */
|
|
544
|
+
this._adapterTeardown = null;
|
|
545
|
+
/** Teardown fn returned by adapter.listenForReady() */
|
|
546
|
+
this._adapterReadyTeardown = null;
|
|
547
|
+
/**
|
|
548
|
+
* Set when a trusted walletReady has been received from window.parent.
|
|
549
|
+
* The polling fallback must NOT clear this flag.
|
|
550
|
+
*/
|
|
551
|
+
this._parentTrusted = false;
|
|
552
|
+
/** walletReady postMessage listener stored for cleanup */
|
|
553
|
+
this._walletReadyMessageListener = null;
|
|
554
|
+
/**
|
|
555
|
+
* In-flight interactive request lock.
|
|
556
|
+
* Methods that open approval popups are serialized — only one at a time.
|
|
557
|
+
*/
|
|
558
|
+
this._interactiveInFlight = false;
|
|
523
559
|
this.MAX_CONCURRENT_REQUESTS = 50;
|
|
524
|
-
/** Time window for rate limiting (milliseconds) */
|
|
525
560
|
this.RATE_LIMIT_WINDOW = 1000;
|
|
526
|
-
/** Maximum requests allowed per time window */
|
|
527
561
|
this.MAX_REQUESTS_PER_WINDOW = 20;
|
|
528
|
-
/** Timestamps of recent requests for rate limiting */
|
|
529
562
|
this.requestTimestamps = [];
|
|
530
563
|
this.logger = createLogger('ExtensionCommunicator', debug);
|
|
531
564
|
this.trustedOrigins = trustedOrigins;
|
|
565
|
+
this.adapter = adapter ?? createZeroXIOAdapter(trustedOrigins);
|
|
532
566
|
this.setupMessageListener();
|
|
533
567
|
this.startExtensionDetection();
|
|
534
568
|
}
|
|
535
|
-
/**
|
|
536
|
-
* Add trusted origins for iframe/bridge communication
|
|
537
|
-
* Call this before connecting if your dApp runs inside a trusted frame
|
|
538
|
-
*/
|
|
539
569
|
setTrustedOrigins(origins) {
|
|
540
570
|
this.trustedOrigins = origins;
|
|
541
571
|
}
|
|
542
|
-
/**
|
|
543
|
-
* Initialize communication with the wallet extension
|
|
544
|
-
*
|
|
545
|
-
* @description
|
|
546
|
-
* Performs initial setup and verification:
|
|
547
|
-
* 1. Waits for extension to become available
|
|
548
|
-
* 2. Sends ping to verify communication
|
|
549
|
-
* 3. Establishes message handlers
|
|
550
|
-
*
|
|
551
|
-
* Must be called before any other methods.
|
|
552
|
-
*
|
|
553
|
-
* @returns {Promise<boolean>} True if initialization succeeded, false otherwise
|
|
554
|
-
* @throws {ZeroXIOWalletError} If extension is not available after timeout
|
|
555
|
-
*
|
|
556
|
-
* @example
|
|
557
|
-
* ```typescript
|
|
558
|
-
* const success = await communicator.initialize();
|
|
559
|
-
* if (!success) {
|
|
560
|
-
* console.error('Failed to initialize wallet connection');
|
|
561
|
-
* }
|
|
562
|
-
* ```
|
|
563
|
-
*/
|
|
564
572
|
async initialize() {
|
|
565
573
|
if (this.isInitialized) {
|
|
566
574
|
return true;
|
|
567
575
|
}
|
|
568
576
|
try {
|
|
569
|
-
// Wait for extension detection with timeout
|
|
570
577
|
const available = await this.waitForExtensionAvailability(10000);
|
|
571
578
|
if (available) {
|
|
572
|
-
// Verify with ping
|
|
573
579
|
await withTimeout(this.sendRequestWithRetry('ping', {}, 3, 2000), 8000, 'Extension ping timeout during initialization');
|
|
574
580
|
this.isInitialized = true;
|
|
575
581
|
this.logger.log('Extension communication initialized successfully');
|
|
@@ -584,149 +590,106 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
584
590
|
return false;
|
|
585
591
|
}
|
|
586
592
|
}
|
|
587
|
-
/**
|
|
588
|
-
* Check if extension is available
|
|
589
|
-
*/
|
|
590
593
|
isExtensionAvailable() {
|
|
591
594
|
return this.isExtensionAvailableState && this.hasExtensionContext();
|
|
592
595
|
}
|
|
593
|
-
/**
|
|
594
|
-
* Send request to extension
|
|
595
|
-
*/
|
|
596
596
|
async sendRequest(method, params = {}, timeout = 30000) {
|
|
597
597
|
const isInteractive = ExtensionCommunicator.NO_RETRY_METHODS.has(method);
|
|
598
598
|
const maxRetries = isInteractive ? 0 : 1;
|
|
599
|
-
// Give users 3 minutes for interactive approvals (review + sign + FHE proof generation)
|
|
600
599
|
const effectiveTimeout = isInteractive ? Math.max(timeout, 180000) : timeout;
|
|
601
600
|
return this.sendRequestWithRetry(method, params, maxRetries, effectiveTimeout);
|
|
602
601
|
}
|
|
603
|
-
/**
|
|
604
|
-
* Send request to extension with automatic retry logic
|
|
605
|
-
*/
|
|
606
602
|
async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
|
|
607
603
|
if (!this.hasExtensionContext()) {
|
|
608
|
-
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, '0xio Wallet extension is not installed or available', {
|
|
609
|
-
method,
|
|
610
|
-
params,
|
|
611
|
-
browserContext: this.getBrowserDiagnostics()
|
|
612
|
-
});
|
|
604
|
+
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, '0xio Wallet extension is not installed or available', { method, browserContext: this.getBrowserDiagnostics() });
|
|
613
605
|
}
|
|
614
606
|
if (!this.isExtensionAvailableState) {
|
|
615
|
-
// Wait a bit for extension to become available
|
|
616
607
|
await this.waitForExtensionAvailability(5000);
|
|
617
608
|
if (!this.isExtensionAvailableState) {
|
|
618
|
-
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Extension not available for communication', {
|
|
619
|
-
method,
|
|
620
|
-
params,
|
|
621
|
-
extensionState: this.getExtensionDiagnostics()
|
|
622
|
-
});
|
|
609
|
+
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Extension not available for communication', { method, extensionState: this.getExtensionDiagnostics() });
|
|
623
610
|
}
|
|
624
611
|
}
|
|
625
|
-
//
|
|
612
|
+
// Enforce one-at-a-time for interactive popup methods
|
|
613
|
+
const isInteractive = ExtensionCommunicator.INTERACTIVE_METHODS.has(method);
|
|
614
|
+
if (isInteractive) {
|
|
615
|
+
if (this._interactiveInFlight) {
|
|
616
|
+
throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, 'Another approval popup is already open. Please wait for it to complete.');
|
|
617
|
+
}
|
|
618
|
+
this._interactiveInFlight = true;
|
|
619
|
+
}
|
|
626
620
|
this.checkRateLimit();
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
621
|
+
try {
|
|
622
|
+
return await retry(async () => {
|
|
623
|
+
const requestId = this.generateRequestId();
|
|
624
|
+
const request = {
|
|
625
|
+
id: requestId,
|
|
626
|
+
method,
|
|
627
|
+
params,
|
|
628
|
+
timestamp: Date.now()
|
|
629
|
+
};
|
|
630
|
+
this.logger.log(`Sending request (${method}):`, { id: requestId });
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
const timeoutHandle = setTimeout(() => {
|
|
633
|
+
const pending = this.pendingRequests.get(requestId);
|
|
634
|
+
if (pending) {
|
|
635
|
+
this.pendingRequests.delete(requestId);
|
|
636
|
+
reject(new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, `Request timeout after ${timeout}ms`, { method, requestId, retryCount: pending.retryCount }));
|
|
637
|
+
}
|
|
638
|
+
}, timeout);
|
|
639
|
+
this.pendingRequests.set(requestId, {
|
|
640
|
+
resolve,
|
|
641
|
+
reject,
|
|
642
|
+
timeout: timeoutHandle,
|
|
643
|
+
retryCount: 0
|
|
644
|
+
});
|
|
645
|
+
// Wrap postMessage so a DataCloneError cleans up the pending entry
|
|
646
|
+
try {
|
|
647
|
+
this.postMessageToExtension(request);
|
|
648
|
+
}
|
|
649
|
+
catch (cloneErr) {
|
|
650
|
+
clearTimeout(timeoutHandle);
|
|
641
651
|
this.pendingRequests.delete(requestId);
|
|
642
|
-
reject(new ZeroXIOWalletError(ErrorCode.
|
|
643
|
-
method,
|
|
644
|
-
params,
|
|
645
|
-
requestId,
|
|
646
|
-
retryCount: pending.retryCount,
|
|
647
|
-
extensionState: this.getExtensionDiagnostics()
|
|
648
|
-
}));
|
|
652
|
+
reject(new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Request params are not serializable', { method, requestId }));
|
|
649
653
|
}
|
|
650
|
-
}, timeout);
|
|
651
|
-
// Store request handlers
|
|
652
|
-
this.pendingRequests.set(requestId, {
|
|
653
|
-
resolve,
|
|
654
|
-
reject,
|
|
655
|
-
timeout: timeoutHandle,
|
|
656
|
-
retryCount: 0
|
|
657
654
|
});
|
|
658
|
-
|
|
659
|
-
this.postMessageToExtension(request);
|
|
660
|
-
});
|
|
661
|
-
}, maxRetries, 1000);
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Setup message listener for responses from extension
|
|
665
|
-
*/
|
|
666
|
-
setupMessageListener() {
|
|
667
|
-
if (typeof window === 'undefined') {
|
|
668
|
-
return; // Not in browser environment
|
|
669
|
-
}
|
|
670
|
-
const allowedOrigin = window.location.origin;
|
|
671
|
-
// Store allowed origins for parent frame communication
|
|
672
|
-
// Desktop (Tauri): tauri://localhost or https://tauri.localhost
|
|
673
|
-
// Mobile (Expo): about:blank or custom scheme
|
|
674
|
-
const trustedParentOrigins = new Set([
|
|
675
|
-
allowedOrigin,
|
|
676
|
-
'tauri://localhost',
|
|
677
|
-
'https://tauri.localhost',
|
|
678
|
-
'http://localhost',
|
|
679
|
-
'https://localhost',
|
|
680
|
-
]);
|
|
681
|
-
// Allow dApps to register additional trusted origins
|
|
682
|
-
if (this.trustedOrigins) {
|
|
683
|
-
for (const origin of this.trustedOrigins) {
|
|
684
|
-
trustedParentOrigins.add(origin);
|
|
685
|
-
}
|
|
655
|
+
}, maxRetries, 1000);
|
|
686
656
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const isLocalhost = event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:');
|
|
691
|
-
const isFromTrustedParent = event.source === window.parent
|
|
692
|
-
&& window.parent !== window
|
|
693
|
-
&& (trustedParentOrigins.has(event.origin) || isLocalhost);
|
|
694
|
-
if (!isFromSameOrigin && !isFromTrustedParent) {
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
// Only accept from same window (extension content script) or trusted parent frame
|
|
698
|
-
if (event.source !== window && event.source !== window.parent) {
|
|
699
|
-
return;
|
|
657
|
+
finally {
|
|
658
|
+
if (isInteractive) {
|
|
659
|
+
this._interactiveInFlight = false;
|
|
700
660
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
setupMessageListener() {
|
|
664
|
+
if (typeof window === 'undefined')
|
|
665
|
+
return;
|
|
666
|
+
this._adapterTeardown = this.adapter.listen((msg) => {
|
|
667
|
+
if (msg.requestId !== undefined) {
|
|
668
|
+
// response — map AdapterIncomingMessage → ExtensionResponse shape
|
|
669
|
+
if (this.pendingRequests.has(msg.requestId)) {
|
|
670
|
+
this.handleExtensionResponse({
|
|
671
|
+
id: msg.requestId,
|
|
672
|
+
success: msg.success ?? false,
|
|
673
|
+
data: msg.data,
|
|
674
|
+
error: msg.error,
|
|
675
|
+
timestamp: Date.now(),
|
|
676
|
+
});
|
|
710
677
|
}
|
|
711
678
|
}
|
|
712
|
-
else if (
|
|
713
|
-
this.handleExtensionEvent(
|
|
679
|
+
else if (msg.eventType) {
|
|
680
|
+
this.handleExtensionEvent({ type: msg.eventType, data: msg.eventData });
|
|
714
681
|
}
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
this.logger.log('Message listener setup complete');
|
|
682
|
+
}, { trustedParentOrigins: this.trustedOrigins });
|
|
683
|
+
this.logger.log(`Message listener setup complete (adapter: ${this.adapter.name})`);
|
|
718
684
|
}
|
|
719
|
-
/**
|
|
720
|
-
* Handle extension event
|
|
721
|
-
*/
|
|
722
685
|
handleExtensionEvent(event) {
|
|
686
|
+
if (!event?.type || !ExtensionCommunicator.VALID_EVENT_TYPES.has(event.type)) {
|
|
687
|
+
this.logger.warn(`Received unknown event type from bridge: ${event?.type}`);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
723
690
|
this.logger.log('Received extension event:', event.type);
|
|
724
|
-
// Forward the event to listeners
|
|
725
691
|
this.emit(event.type, event.data);
|
|
726
692
|
}
|
|
727
|
-
/**
|
|
728
|
-
* Handle response from extension
|
|
729
|
-
*/
|
|
730
693
|
handleExtensionResponse(response) {
|
|
731
694
|
this.logger.log(`Received response:`, { id: response.id, success: response.success });
|
|
732
695
|
const pending = this.pendingRequests.get(response.id);
|
|
@@ -734,192 +697,134 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
734
697
|
this.logger.warn(`Received response for unknown request ID: ${response.id}`);
|
|
735
698
|
return;
|
|
736
699
|
}
|
|
737
|
-
// Clear timeout and remove from pending
|
|
738
700
|
clearTimeout(pending.timeout);
|
|
739
701
|
this.pendingRequests.delete(response.id);
|
|
740
|
-
//
|
|
741
|
-
if (response.success) {
|
|
702
|
+
// Require strict boolean true — "false" string or other truthy values are failures
|
|
703
|
+
if (response.success === true) {
|
|
742
704
|
pending.resolve(response.data);
|
|
743
705
|
}
|
|
744
706
|
else {
|
|
745
707
|
const error = response.error;
|
|
746
708
|
if (error) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
709
|
+
// Handle both object {code, message} and legacy plain-string error formats
|
|
710
|
+
const isObj = error !== null && typeof error === 'object';
|
|
711
|
+
const code = isObj && error.code ? error.code : ErrorCode.UNKNOWN_ERROR;
|
|
712
|
+
const message = isObj
|
|
713
|
+
? (error.message ?? 'Unknown error')
|
|
714
|
+
: (typeof error === 'string' ? error : 'Unknown error');
|
|
715
|
+
const enhancedError = new ZeroXIOWalletError(code, message,
|
|
716
|
+
// Redact bridge-supplied error details; only keep non-sensitive metadata
|
|
717
|
+
{ requestId: response.id, retryCount: pending.retryCount, timestamp: Date.now() });
|
|
754
718
|
pending.reject(enhancedError);
|
|
755
719
|
}
|
|
756
720
|
else {
|
|
757
|
-
pending.reject(new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Unknown error occurred', {
|
|
758
|
-
requestId: response.id,
|
|
759
|
-
retryCount: pending.retryCount,
|
|
760
|
-
extensionState: this.getExtensionDiagnostics()
|
|
761
|
-
}));
|
|
721
|
+
pending.reject(new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Unknown error occurred', { requestId: response.id, retryCount: pending.retryCount }));
|
|
762
722
|
}
|
|
763
723
|
}
|
|
764
724
|
}
|
|
765
|
-
/**
|
|
766
|
-
* Post message to extension via content script
|
|
767
|
-
*/
|
|
768
725
|
postMessageToExtension(request) {
|
|
769
|
-
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
// Response validation is done by request ID matching, not origin
|
|
776
|
-
const parentOrigin = this._parentOrigin || '*';
|
|
777
|
-
try {
|
|
778
|
-
window.parent.postMessage(msg, parentOrigin);
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
// Fallback to wildcard if specific origin fails
|
|
782
|
-
try {
|
|
783
|
-
window.parent.postMessage(msg, '*');
|
|
784
|
-
}
|
|
785
|
-
catch { }
|
|
726
|
+
this.adapter.postRequest(request);
|
|
727
|
+
// Parent bridge (iframe/desktop mode) — only when a trusted origin is established.
|
|
728
|
+
// Sending with '*' would leak method + params to any intercepting frame.
|
|
729
|
+
if (window.parent !== window && this._parentOrigin) {
|
|
730
|
+
if (this.adapter.postRequestToParent) {
|
|
731
|
+
this.adapter.postRequestToParent(request, this._parentOrigin);
|
|
786
732
|
}
|
|
787
733
|
}
|
|
788
734
|
}
|
|
789
|
-
/**
|
|
790
|
-
* Check if we're in a context that can communicate with extension
|
|
791
|
-
*/
|
|
792
735
|
hasExtensionContext() {
|
|
793
736
|
return typeof window !== 'undefined' &&
|
|
794
737
|
typeof window.postMessage === 'function';
|
|
795
738
|
}
|
|
796
|
-
/**
|
|
797
|
-
* Check and enforce rate limits to prevent denial-of-service attacks
|
|
798
|
-
*
|
|
799
|
-
* @private
|
|
800
|
-
* @throws {ZeroXIOWalletError} RATE_LIMIT_EXCEEDED if limits are exceeded
|
|
801
|
-
*
|
|
802
|
-
* @description
|
|
803
|
-
* Implements two-tier rate limiting:
|
|
804
|
-
* 1. Concurrent requests: Maximum 50 pending requests at once
|
|
805
|
-
* 2. Request frequency: Maximum 20 requests per second
|
|
806
|
-
*
|
|
807
|
-
* Rate limiting protects both the SDK and extension from:
|
|
808
|
-
* - Accidental infinite loops in dApp code
|
|
809
|
-
* - Malicious DoS attacks
|
|
810
|
-
* - Resource exhaustion
|
|
811
|
-
*
|
|
812
|
-
* @security Critical security function - enforces resource limits
|
|
813
|
-
*/
|
|
814
739
|
checkRateLimit() {
|
|
815
740
|
const now = Date.now();
|
|
816
|
-
// ✅ SECURITY: Check concurrent request limit
|
|
817
741
|
if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
|
|
818
742
|
throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
|
|
819
743
|
}
|
|
820
|
-
//
|
|
744
|
+
// Trim expired timestamps — cap array size to prevent unbounded growth in idle tabs
|
|
821
745
|
this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
|
|
746
|
+
if (this.requestTimestamps.length > this.MAX_REQUESTS_PER_WINDOW) {
|
|
747
|
+
this.requestTimestamps = this.requestTimestamps.slice(-this.MAX_REQUESTS_PER_WINDOW);
|
|
748
|
+
}
|
|
822
749
|
if (this.requestTimestamps.length >= this.MAX_REQUESTS_PER_WINDOW) {
|
|
823
750
|
throw new ZeroXIOWalletError(ErrorCode.RATE_LIMIT_EXCEEDED, `Too many requests per second (max: ${this.MAX_REQUESTS_PER_WINDOW} per ${this.RATE_LIMIT_WINDOW}ms)`);
|
|
824
751
|
}
|
|
825
752
|
this.requestTimestamps.push(now);
|
|
826
753
|
}
|
|
827
|
-
/**
|
|
828
|
-
* Generate cryptographically secure unique request ID
|
|
829
|
-
*
|
|
830
|
-
* @private
|
|
831
|
-
* @returns {string} A unique, unpredictable request identifier
|
|
832
|
-
*
|
|
833
|
-
* @description
|
|
834
|
-
* Uses Web Crypto API for secure random ID generation:
|
|
835
|
-
* 1. Primary: crypto.randomUUID() - UUID v4 format
|
|
836
|
-
* 2. Fallback: crypto.getRandomValues() - 128-bit random hex
|
|
837
|
-
* 3. Last resort: timestamp + counter (logs warning)
|
|
838
|
-
*
|
|
839
|
-
* Security importance:
|
|
840
|
-
* - Prevents request ID prediction attacks
|
|
841
|
-
* - Mitigates replay attacks
|
|
842
|
-
* - Makes session hijacking more difficult
|
|
843
|
-
*
|
|
844
|
-
* @security Critical - IDs must be cryptographically unpredictable
|
|
845
|
-
*/
|
|
846
754
|
generateRequestId() {
|
|
847
|
-
// ✅ SECURITY: Use crypto.randomUUID() for secure, unpredictable IDs
|
|
848
755
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
849
756
|
return `0xio-sdk-${crypto.randomUUID()}`;
|
|
850
757
|
}
|
|
851
|
-
// Fallback to crypto.getRandomValues for older browsers
|
|
852
758
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
853
759
|
const array = new Uint8Array(16);
|
|
854
760
|
crypto.getRandomValues(array);
|
|
855
761
|
const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
856
762
|
return `0xio-sdk-${hex}`;
|
|
857
763
|
}
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
764
|
+
// Crypto API unavailable — throw rather than produce a guessable ID that
|
|
765
|
+
// could allow response spoofing via a known requestId.
|
|
766
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Cryptographic random number generation is not available in this environment');
|
|
861
767
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Start continuous extension detection
|
|
864
|
-
*/
|
|
865
768
|
startExtensionDetection() {
|
|
866
769
|
if (typeof window === 'undefined')
|
|
867
770
|
return;
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
this.
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
771
|
+
// Adapter-provided wallet-ready events (e.g. '0xioWalletReady', 'exampleWalletReady')
|
|
772
|
+
if (this.adapter.listenForReady) {
|
|
773
|
+
this._adapterReadyTeardown = this.adapter.listenForReady(() => {
|
|
774
|
+
this.logger.log(`Received wallet-ready event (adapter: ${this.adapter.name})`);
|
|
775
|
+
this.isExtensionAvailableState = true;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
// walletReady via postMessage (desktop/mobile iframe bridge) — store ref for cleanup
|
|
779
|
+
this._walletReadyMessageListener = (event) => {
|
|
780
|
+
if (event.data?.source !== '0xio-sdk-bridge' || event.data?.event?.type !== 'walletReady') {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const isSameOrigin = event.origin === window.location.origin;
|
|
784
|
+
const isTauri = event.origin === 'tauri://localhost' || event.origin === 'https://tauri.localhost';
|
|
785
|
+
const hasExplicitList = this.trustedOrigins.length > 0;
|
|
786
|
+
// When trustedParentOrigins is explicitly set, implicit localhost trust is disabled
|
|
787
|
+
const isLocalhostAllowed = !hasExplicitList &&
|
|
788
|
+
(event.origin.startsWith('http://localhost:') || event.origin.startsWith('http://127.0.0.1:'));
|
|
789
|
+
const isTrustedOrigin = this.trustedOrigins.includes(event.origin) || isTauri || isLocalhostAllowed;
|
|
790
|
+
// In iframe mode, only trust the actual parent window
|
|
791
|
+
const inIframe = window.parent !== window;
|
|
792
|
+
if (inIframe && event.source !== window.parent) {
|
|
793
|
+
this.logger.warn(`Ignored walletReady from non-parent source in iframe mode`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (isSameOrigin || isTrustedOrigin) {
|
|
797
|
+
this.logger.log('Received walletReady via postMessage from trusted origin');
|
|
798
|
+
this.isExtensionAvailableState = true;
|
|
799
|
+
this._parentTrusted = true; // Mark parent-bridge readiness separately
|
|
800
|
+
if (event.data.parentOrigin) {
|
|
801
|
+
this._parentOrigin = event.data.parentOrigin;
|
|
899
802
|
}
|
|
900
|
-
else {
|
|
901
|
-
this.
|
|
803
|
+
else if (event.origin && event.origin !== 'null') {
|
|
804
|
+
this._parentOrigin = event.origin;
|
|
902
805
|
}
|
|
903
806
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
807
|
+
else {
|
|
808
|
+
this.logger.warn(`Ignored walletReady from untrusted origin: ${event.origin}`);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
window.addEventListener('message', this._walletReadyMessageListener);
|
|
907
812
|
if (window.parent !== window) {
|
|
908
813
|
this.logger.log('Running inside a frame — waiting for trusted walletReady signal');
|
|
909
814
|
}
|
|
910
|
-
// Initial check (in case extension was already injected)
|
|
911
815
|
this.checkExtensionAvailability();
|
|
912
|
-
// Set up periodic checks as fallback
|
|
913
816
|
this.extensionDetectionInterval = setInterval(() => {
|
|
914
817
|
this.checkExtensionAvailability();
|
|
915
818
|
}, 2000);
|
|
916
819
|
}
|
|
917
|
-
/**
|
|
918
|
-
* Check if extension is currently available
|
|
919
|
-
*/
|
|
920
820
|
checkExtensionAvailability() {
|
|
821
|
+
// If parent-bridge readiness was established via a trusted walletReady handshake,
|
|
822
|
+
// preserve that state — the polling fallback (detectExtensionSignals) does not
|
|
823
|
+
// consider the iframe parent signal and would incorrectly flip state back
|
|
824
|
+
if (this._parentTrusted) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
921
827
|
const wasAvailable = this.isExtensionAvailableState;
|
|
922
|
-
// Basic checks for extension context
|
|
923
828
|
this.isExtensionAvailableState = this.hasExtensionContext() && this.detectExtensionSignals();
|
|
924
829
|
if (!wasAvailable && this.isExtensionAvailableState) {
|
|
925
830
|
this.logger.log('Extension became available');
|
|
@@ -928,25 +833,9 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
928
833
|
this.logger.warn('Extension became unavailable');
|
|
929
834
|
}
|
|
930
835
|
}
|
|
931
|
-
/**
|
|
932
|
-
* Detect extension signals/indicators
|
|
933
|
-
*/
|
|
934
836
|
detectExtensionSignals() {
|
|
935
|
-
|
|
936
|
-
return false;
|
|
937
|
-
// Check for extension-injected indicators
|
|
938
|
-
const win = window;
|
|
939
|
-
// Look for extension-injected globals (matches injected.ts)
|
|
940
|
-
return !!(win.wallet0xio ||
|
|
941
|
-
win.ZeroXIOWallet ||
|
|
942
|
-
win.octraWallet ||
|
|
943
|
-
(win.chrome?.runtime?.id) ||
|
|
944
|
-
document.querySelector('meta[name="0xio-dapp"]') ||
|
|
945
|
-
document.querySelector('[data-0xio-sdk-bridge]'));
|
|
837
|
+
return this.adapter.detect();
|
|
946
838
|
}
|
|
947
|
-
/**
|
|
948
|
-
* Wait for extension to become available
|
|
949
|
-
*/
|
|
950
839
|
async waitForExtensionAvailability(timeoutMs) {
|
|
951
840
|
if (this.isExtensionAvailableState) {
|
|
952
841
|
return true;
|
|
@@ -954,14 +843,11 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
954
843
|
return new Promise((resolve) => {
|
|
955
844
|
let resolved = false;
|
|
956
845
|
const startTime = Date.now();
|
|
957
|
-
|
|
846
|
+
let adapterReadyTeardown = null;
|
|
958
847
|
const cleanup = () => {
|
|
959
848
|
clearInterval(checkInterval);
|
|
960
|
-
|
|
961
|
-
window.removeEventListener('wallet0xioReady', onReady);
|
|
962
|
-
window.removeEventListener('octraWalletReady', onReady);
|
|
849
|
+
adapterReadyTeardown?.();
|
|
963
850
|
};
|
|
964
|
-
// Event handler for instant resolution
|
|
965
851
|
const onReady = () => {
|
|
966
852
|
if (resolved)
|
|
967
853
|
return;
|
|
@@ -970,11 +856,10 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
970
856
|
cleanup();
|
|
971
857
|
resolve(true);
|
|
972
858
|
};
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
// Polling fallback with shorter interval
|
|
859
|
+
// Use adapter's listenForReady for fast resolution; polling is the fallback
|
|
860
|
+
if (this.adapter.listenForReady) {
|
|
861
|
+
adapterReadyTeardown = this.adapter.listenForReady(onReady);
|
|
862
|
+
}
|
|
978
863
|
const checkInterval = setInterval(() => {
|
|
979
864
|
if (resolved)
|
|
980
865
|
return;
|
|
@@ -992,9 +877,6 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
992
877
|
}, 100);
|
|
993
878
|
});
|
|
994
879
|
}
|
|
995
|
-
/**
|
|
996
|
-
* Get browser diagnostics for error reporting
|
|
997
|
-
*/
|
|
998
880
|
getBrowserDiagnostics() {
|
|
999
881
|
if (typeof window === 'undefined') {
|
|
1000
882
|
return { environment: 'non-browser' };
|
|
@@ -1006,29 +888,21 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1006
888
|
hasChromeRuntime: !!(win.chrome?.runtime),
|
|
1007
889
|
hasPostMessage: typeof window.postMessage === 'function',
|
|
1008
890
|
origin: window.location?.origin,
|
|
1009
|
-
extensionDetection: {
|
|
1010
|
-
hasWallet0xio: !!win.wallet0xio,
|
|
1011
|
-
hasZeroXIOWallet: !!win.ZeroXIOWallet,
|
|
1012
|
-
hasOctraWallet: !!win.octraWallet,
|
|
1013
|
-
hasChromeRuntimeId: !!(win.chrome?.runtime?.id),
|
|
1014
|
-
hasSdkBridge: !!document.querySelector('[data-0xio-sdk-bridge]')
|
|
1015
|
-
}
|
|
1016
891
|
};
|
|
1017
892
|
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Get extension state diagnostics
|
|
1020
|
-
*/
|
|
1021
893
|
getExtensionDiagnostics() {
|
|
1022
894
|
return {
|
|
1023
895
|
initialized: this.isInitialized,
|
|
1024
896
|
available: this.isExtensionAvailableState,
|
|
897
|
+
parentTrusted: this._parentTrusted,
|
|
1025
898
|
pendingRequests: this.pendingRequests.size,
|
|
1026
899
|
hasExtensionContext: this.hasExtensionContext(),
|
|
1027
|
-
browserDiagnostics: this.getBrowserDiagnostics()
|
|
1028
900
|
};
|
|
1029
901
|
}
|
|
1030
902
|
/**
|
|
1031
|
-
*
|
|
903
|
+
* Clean up SDK resources.
|
|
904
|
+
* After cleanup() the instance is terminal — do not call initialize() again.
|
|
905
|
+
* Construct a new instance instead.
|
|
1032
906
|
*/
|
|
1033
907
|
cleanup() {
|
|
1034
908
|
if (this.extensionDetectionInterval) {
|
|
@@ -1042,18 +916,22 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1042
916
|
this.pendingRequests.clear();
|
|
1043
917
|
this.isInitialized = false;
|
|
1044
918
|
this.isExtensionAvailableState = false;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
919
|
+
this._parentTrusted = false;
|
|
920
|
+
this._parentOrigin = null;
|
|
921
|
+
this._interactiveInFlight = false;
|
|
922
|
+
// Tear down adapter listeners (response/event + wallet-ready)
|
|
923
|
+
this._adapterTeardown?.();
|
|
924
|
+
this._adapterTeardown = null;
|
|
925
|
+
this._adapterReadyTeardown?.();
|
|
926
|
+
this._adapterReadyTeardown = null;
|
|
927
|
+
// Remove walletReady postMessage listener (parent iframe bridge)
|
|
928
|
+
if (typeof window !== 'undefined' && this._walletReadyMessageListener) {
|
|
929
|
+
window.removeEventListener('message', this._walletReadyMessageListener);
|
|
930
|
+
this._walletReadyMessageListener = null;
|
|
1049
931
|
}
|
|
1050
|
-
// Call parent cleanup
|
|
1051
932
|
this.removeAllListeners();
|
|
1052
933
|
this.logger.log('Communication cleanup complete');
|
|
1053
934
|
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Get debug information
|
|
1056
|
-
*/
|
|
1057
935
|
getDebugInfo() {
|
|
1058
936
|
return {
|
|
1059
937
|
initialized: this.isInitialized,
|
|
@@ -1064,23 +942,31 @@ class ExtensionCommunicator extends EventEmitter {
|
|
|
1064
942
|
};
|
|
1065
943
|
}
|
|
1066
944
|
}
|
|
1067
|
-
// Methods that trigger user-facing popups — NEVER retry these
|
|
945
|
+
// Methods that trigger user-facing popups — NEVER retry these.
|
|
1068
946
|
// Retrying sends a second request while the first popup is still open,
|
|
1069
|
-
// causing double popups where the second tx fails (stale nonce/state)
|
|
947
|
+
// causing double popups where the second tx fails (stale nonce/state).
|
|
1070
948
|
ExtensionCommunicator.NO_RETRY_METHODS = new Set([
|
|
1071
949
|
'connect', 'send_transaction', 'call_contract', 'signMessage',
|
|
950
|
+
'sign_transaction', 'broadcast_only',
|
|
1072
951
|
'send_private_transfer', 'claim_private_transfer',
|
|
1073
952
|
'encrypt_balance', 'decrypt_balance',
|
|
1074
953
|
]);
|
|
954
|
+
ExtensionCommunicator.INTERACTIVE_METHODS = ExtensionCommunicator.NO_RETRY_METHODS;
|
|
955
|
+
// only forward known event types
|
|
956
|
+
ExtensionCommunicator.VALID_EVENT_TYPES = new Set([
|
|
957
|
+
'connect', 'disconnect', 'accountChanged', 'balanceChanged',
|
|
958
|
+
'networkChanged', 'transactionConfirmed', 'permissionsChanged', 'message',
|
|
959
|
+
'error', 'extensionLocked', 'extensionUnlocked'
|
|
960
|
+
]);
|
|
1075
961
|
|
|
1076
962
|
/**
|
|
1077
963
|
* Network configuration for 0xio SDK
|
|
1078
964
|
*/
|
|
1079
|
-
const
|
|
965
|
+
const _NETWORKS = {
|
|
1080
966
|
'mainnet': {
|
|
1081
967
|
id: 'mainnet',
|
|
1082
968
|
name: 'Octra Mainnet',
|
|
1083
|
-
rpcUrl: '
|
|
969
|
+
rpcUrl: 'https://octra.network',
|
|
1084
970
|
explorerUrl: 'https://lite.octrascan.io/tx.html?hash=',
|
|
1085
971
|
explorerAddressUrl: 'https://lite.octrascan.io/address.html?addr=',
|
|
1086
972
|
indexerUrl: 'https://lite.octrascan.io',
|
|
@@ -1111,70 +997,125 @@ const NETWORKS = {
|
|
|
1111
997
|
isTestnet: false
|
|
1112
998
|
}
|
|
1113
999
|
};
|
|
1000
|
+
/**
|
|
1001
|
+
* Immutable public copy of the built-in network table.
|
|
1002
|
+
* Modifications to returned objects do not affect SDK-internal state.
|
|
1003
|
+
*/
|
|
1004
|
+
const NETWORKS = Object.freeze(Object.fromEntries(Object.entries(_NETWORKS).map(([k, v]) => [k, Object.freeze({ ...v })])));
|
|
1114
1005
|
const DEFAULT_NETWORK_ID = 'mainnet';
|
|
1115
1006
|
/**
|
|
1116
|
-
* Get network configuration by ID
|
|
1007
|
+
* Get network configuration by ID.
|
|
1008
|
+
* Returns a frozen copy — callers cannot mutate SDK-internal state.
|
|
1117
1009
|
*/
|
|
1118
1010
|
function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
1119
|
-
|
|
1120
|
-
if (!network) {
|
|
1011
|
+
if (!Object.prototype.hasOwnProperty.call(_NETWORKS, networkId)) {
|
|
1121
1012
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, `Unknown network ID: ${networkId}`);
|
|
1122
1013
|
}
|
|
1123
|
-
return
|
|
1014
|
+
return Object.freeze({ ..._NETWORKS[networkId] });
|
|
1124
1015
|
}
|
|
1125
1016
|
/**
|
|
1126
|
-
* Get all available networks
|
|
1017
|
+
* Get all available networks.
|
|
1018
|
+
* Returns frozen copies — callers cannot mutate SDK-internal state.
|
|
1127
1019
|
*/
|
|
1128
1020
|
function getAllNetworks() {
|
|
1129
|
-
return Object.values(
|
|
1021
|
+
return Object.values(_NETWORKS).map(n => Object.freeze({ ...n }));
|
|
1130
1022
|
}
|
|
1131
1023
|
/**
|
|
1132
|
-
* Check if network ID is valid
|
|
1024
|
+
* Check if network ID is valid (own property check, prevents prototype pollution).
|
|
1133
1025
|
*/
|
|
1134
1026
|
function isValidNetworkId(networkId) {
|
|
1135
|
-
return networkId
|
|
1027
|
+
return typeof networkId === 'string' && Object.prototype.hasOwnProperty.call(_NETWORKS, networkId);
|
|
1136
1028
|
}
|
|
1137
|
-
|
|
1138
1029
|
/**
|
|
1139
|
-
*
|
|
1030
|
+
* Validate a NetworkInfo shape from an untrusted source (bridge response).
|
|
1031
|
+
* Returns a frozen copy if valid, null otherwise.
|
|
1140
1032
|
*/
|
|
1033
|
+
function validateNetworkInfo(raw) {
|
|
1034
|
+
if (!raw || typeof raw !== 'object')
|
|
1035
|
+
return null;
|
|
1036
|
+
if (typeof raw.id !== 'string' || !raw.id)
|
|
1037
|
+
return null;
|
|
1038
|
+
if (typeof raw.name !== 'string')
|
|
1039
|
+
return null;
|
|
1040
|
+
if (typeof raw.rpcUrl !== 'string' || (!raw.rpcUrl && raw.id !== 'custom'))
|
|
1041
|
+
return null;
|
|
1042
|
+
if (raw.rpcUrl) {
|
|
1043
|
+
const isHttps = raw.rpcUrl.startsWith('https://');
|
|
1044
|
+
const isLocal = raw.rpcUrl.startsWith('http://localhost') || raw.rpcUrl.startsWith('http://127.0.0.1');
|
|
1045
|
+
const isTestnet = raw.isTestnet === true;
|
|
1046
|
+
// Reject plain-http RPC URLs from untrusted sources unless testnet-flagged or local
|
|
1047
|
+
if (!isHttps && !isLocal && !isTestnet)
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof raw.supportsPrivacy !== 'boolean')
|
|
1051
|
+
return null;
|
|
1052
|
+
return Object.freeze({
|
|
1053
|
+
id: raw.id,
|
|
1054
|
+
name: raw.name,
|
|
1055
|
+
rpcUrl: raw.rpcUrl,
|
|
1056
|
+
explorerUrl: typeof raw.explorerUrl === 'string' ? raw.explorerUrl : undefined,
|
|
1057
|
+
explorerAddressUrl: typeof raw.explorerAddressUrl === 'string' ? raw.explorerAddressUrl : undefined,
|
|
1058
|
+
indexerUrl: typeof raw.indexerUrl === 'string' ? raw.indexerUrl : undefined,
|
|
1059
|
+
supportsPrivacy: raw.supportsPrivacy,
|
|
1060
|
+
color: typeof raw.color === 'string' ? raw.color : '#64748b',
|
|
1061
|
+
isTestnet: typeof raw.isTestnet === 'boolean' ? raw.isTestnet : false,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1141
1065
|
/**
|
|
1142
|
-
* Default balance structure
|
|
1066
|
+
* Default balance structure.
|
|
1067
|
+
* Accepts a numeric total or undefined — never pass a Balance object here.
|
|
1143
1068
|
*/
|
|
1144
1069
|
function createDefaultBalance(total = 0) {
|
|
1070
|
+
const safeTotal = typeof total === 'number' && Number.isFinite(total) && total >= 0 ? total : 0;
|
|
1145
1071
|
return {
|
|
1146
|
-
total,
|
|
1147
|
-
public:
|
|
1072
|
+
total: safeTotal,
|
|
1073
|
+
public: safeTotal,
|
|
1148
1074
|
private: 0,
|
|
1149
1075
|
currency: 'OCT'
|
|
1150
1076
|
};
|
|
1151
1077
|
}
|
|
1152
1078
|
/**
|
|
1153
|
-
*
|
|
1079
|
+
* Validate and normalise a Balance from an untrusted source (bridge response).
|
|
1080
|
+
* Returns null if the payload cannot be coerced into a valid Balance.
|
|
1154
1081
|
*/
|
|
1082
|
+
function validateBalance(raw) {
|
|
1083
|
+
if (raw === null || raw === undefined)
|
|
1084
|
+
return null;
|
|
1085
|
+
// If it's already a Balance-shaped object, extract numeric fields
|
|
1086
|
+
// Use Number() not parseFloat() — parseFloat('10abc') silently returns 10
|
|
1087
|
+
const pub = typeof raw === 'object' ? Number(raw.public ?? raw.total ?? 0) : Number(raw);
|
|
1088
|
+
const priv = typeof raw === 'object' ? Number(raw.private ?? 0) : 0;
|
|
1089
|
+
if (!Number.isFinite(pub) || pub < 0)
|
|
1090
|
+
return null;
|
|
1091
|
+
if (!Number.isFinite(priv) || priv < 0)
|
|
1092
|
+
return null;
|
|
1093
|
+
return {
|
|
1094
|
+
public: pub,
|
|
1095
|
+
private: priv,
|
|
1096
|
+
total: pub + priv,
|
|
1097
|
+
currency: 'OCT'
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1155
1100
|
const SDK_CONFIG = {
|
|
1156
|
-
version: '2.
|
|
1101
|
+
version: '2.7.1',
|
|
1157
1102
|
defaultNetworkId: DEFAULT_NETWORK_ID,
|
|
1158
1103
|
communicationTimeout: 30000, // 30 seconds
|
|
1159
1104
|
retryAttempts: 3,
|
|
1160
1105
|
retryDelay: 1000, // 1 second
|
|
1161
1106
|
};
|
|
1162
|
-
/**
|
|
1163
|
-
* Get default network configuration
|
|
1164
|
-
*/
|
|
1165
1107
|
function getDefaultNetwork() {
|
|
1166
1108
|
return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
|
|
1167
1109
|
}
|
|
1168
1110
|
|
|
1169
|
-
/**
|
|
1170
|
-
* 0xio Wallet SDK - Main Wallet Class
|
|
1171
|
-
* Primary interface for DApp developers to interact with 0xio Wallet
|
|
1172
|
-
*/
|
|
1173
1111
|
class ZeroXIOWallet extends EventEmitter {
|
|
1174
1112
|
constructor(config) {
|
|
1175
1113
|
super(config.debug);
|
|
1176
1114
|
this.connectionInfo = { isConnected: false };
|
|
1177
1115
|
this.isInitialized = false;
|
|
1116
|
+
this._initPromise = null;
|
|
1117
|
+
// session version — stale write detection
|
|
1118
|
+
this._sessionVersion = 0;
|
|
1178
1119
|
this.config = {
|
|
1179
1120
|
...config,
|
|
1180
1121
|
appVersion: config.appVersion || '1.0.0',
|
|
@@ -1182,91 +1123,107 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1182
1123
|
debug: config.debug || false
|
|
1183
1124
|
};
|
|
1184
1125
|
this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
|
|
1185
|
-
this.communicator = new ExtensionCommunicator(this.config.debug);
|
|
1126
|
+
this.communicator = new ExtensionCommunicator(this.config.debug, this.config.trustedParentOrigins ?? [], this.config.adapter);
|
|
1186
1127
|
this.logger.log('Wallet instance created with config:', this.config);
|
|
1187
1128
|
}
|
|
1188
|
-
// ===================
|
|
1189
|
-
// INITIALIZATION
|
|
1190
|
-
// ===================
|
|
1191
|
-
/**
|
|
1192
|
-
* Initialize the SDK
|
|
1193
|
-
* Must be called before using any other methods
|
|
1194
|
-
*/
|
|
1195
1129
|
async initialize() {
|
|
1196
1130
|
if (this.isInitialized) {
|
|
1197
1131
|
return true;
|
|
1198
1132
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
const communicationReady = await this.communicator.initialize();
|
|
1202
|
-
if (!communicationReady) {
|
|
1203
|
-
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
|
|
1204
|
-
}
|
|
1205
|
-
// Register this DApp with the extension
|
|
1206
|
-
await this.communicator.sendRequest('register_dapp', {
|
|
1207
|
-
appName: this.config.appName,
|
|
1208
|
-
appDescription: this.config.appDescription,
|
|
1209
|
-
appVersion: this.config.appVersion,
|
|
1210
|
-
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1211
|
-
appIcon: this.config.appIcon,
|
|
1212
|
-
requiredPermissions: this.config.requiredPermissions,
|
|
1213
|
-
networkId: this.config.networkId
|
|
1214
|
-
});
|
|
1215
|
-
// Setup event forwarding from extension
|
|
1216
|
-
this.setupExtensionEventListeners();
|
|
1217
|
-
this.isInitialized = true;
|
|
1218
|
-
this.logger.log('SDK initialized successfully');
|
|
1219
|
-
return true;
|
|
1133
|
+
if (this._initPromise) {
|
|
1134
|
+
return this._initPromise;
|
|
1220
1135
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1136
|
+
this._initPromise = (async () => {
|
|
1137
|
+
try {
|
|
1138
|
+
const communicationReady = await this.communicator.initialize();
|
|
1139
|
+
if (!communicationReady) {
|
|
1140
|
+
throw new ZeroXIOWalletError(ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
|
|
1141
|
+
}
|
|
1142
|
+
await this.communicator.sendRequest('register_dapp', {
|
|
1143
|
+
appName: this.config.appName,
|
|
1144
|
+
appDescription: this.config.appDescription,
|
|
1145
|
+
appVersion: this.config.appVersion,
|
|
1146
|
+
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1147
|
+
appIcon: this.config.appIcon,
|
|
1148
|
+
requiredPermissions: this.config.requiredPermissions,
|
|
1149
|
+
networkId: this.config.networkId
|
|
1150
|
+
});
|
|
1151
|
+
this.setupExtensionEventListeners();
|
|
1152
|
+
this.isInitialized = true;
|
|
1153
|
+
this.logger.log('SDK initialized successfully');
|
|
1154
|
+
return true;
|
|
1225
1155
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
this.logger.error('Failed to initialize:', error);
|
|
1158
|
+
if (error instanceof ZeroXIOWalletError) {
|
|
1159
|
+
throw error;
|
|
1160
|
+
}
|
|
1161
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Failed to initialize SDK', error);
|
|
1162
|
+
}
|
|
1163
|
+
finally {
|
|
1164
|
+
this._initPromise = null;
|
|
1165
|
+
}
|
|
1166
|
+
})();
|
|
1167
|
+
return this._initPromise;
|
|
1228
1168
|
}
|
|
1229
|
-
/**
|
|
1230
|
-
* Check if SDK is initialized
|
|
1231
|
-
*/
|
|
1232
1169
|
isReady() {
|
|
1233
1170
|
return this.isInitialized && this.communicator.isExtensionAvailable();
|
|
1234
1171
|
}
|
|
1235
|
-
// ===================
|
|
1236
|
-
// CONNECTION MANAGEMENT
|
|
1237
|
-
// ===================
|
|
1238
|
-
/**
|
|
1239
|
-
* Connect to wallet
|
|
1240
|
-
*/
|
|
1241
1172
|
async connect(options = {}) {
|
|
1242
1173
|
this.ensureInitialized();
|
|
1243
1174
|
try {
|
|
1244
1175
|
this.logger.log('Attempting to connect with options:', options);
|
|
1176
|
+
// filter to declared perms only — accept both RFC 'permissions' and legacy 'requestPermissions'
|
|
1177
|
+
const declaredPermissions = this.config.requiredPermissions || [];
|
|
1178
|
+
const requestedPerms = options.permissions ?? options.requestPermissions;
|
|
1179
|
+
const requestedPermissions = requestedPerms
|
|
1180
|
+
? requestedPerms.filter(p => declaredPermissions.includes(p))
|
|
1181
|
+
: declaredPermissions;
|
|
1245
1182
|
const result = await this.communicator.sendRequest('connect', {
|
|
1246
|
-
|
|
1183
|
+
permissions: requestedPermissions,
|
|
1247
1184
|
networkId: options.networkId || this.config.networkId
|
|
1248
1185
|
});
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1186
|
+
// verify pubkey→addr binding
|
|
1187
|
+
if (result.publicKey && result.address) {
|
|
1188
|
+
try {
|
|
1189
|
+
const derived = await deriveOctraAddress(result.publicKey);
|
|
1190
|
+
if (derived !== result.address) {
|
|
1191
|
+
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'Address-key binding verification failed — the reported public key does not derive to the reported address');
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
catch (e) {
|
|
1195
|
+
if (e instanceof ZeroXIOWalletError)
|
|
1196
|
+
throw e;
|
|
1197
|
+
this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// Use networkInfo from extension response — validate before caching.
|
|
1201
|
+
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1202
|
+
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1203
|
+
if (!networkInfo) {
|
|
1204
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Wallet did not return valid network metadata.');
|
|
1205
|
+
}
|
|
1206
|
+
const permissions = result.permissions || [];
|
|
1207
|
+
// Update connection info — including permissions
|
|
1252
1208
|
this.connectionInfo = {
|
|
1253
1209
|
isConnected: true,
|
|
1254
1210
|
address: result.address,
|
|
1255
1211
|
publicKey: result.publicKey,
|
|
1256
1212
|
balance: result.balance,
|
|
1257
1213
|
networkInfo,
|
|
1258
|
-
connectedAt: Date.now()
|
|
1214
|
+
connectedAt: Date.now(),
|
|
1215
|
+
permissions
|
|
1259
1216
|
};
|
|
1260
1217
|
const connectEvent = {
|
|
1261
1218
|
address: result.address,
|
|
1262
1219
|
publicKey: result.publicKey,
|
|
1263
1220
|
balance: result.balance,
|
|
1264
1221
|
networkInfo,
|
|
1265
|
-
permissions
|
|
1222
|
+
permissions
|
|
1266
1223
|
};
|
|
1267
1224
|
// Emit connect event
|
|
1268
1225
|
this.emit('connect', connectEvent);
|
|
1269
|
-
this.logger.log('Connected successfully:', connectEvent);
|
|
1226
|
+
this.logger.log('Connected successfully:', { address: connectEvent.address, network: networkInfo.id });
|
|
1270
1227
|
return connectEvent;
|
|
1271
1228
|
}
|
|
1272
1229
|
catch (error) {
|
|
@@ -1284,6 +1241,7 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1284
1241
|
this.ensureInitialized();
|
|
1285
1242
|
try {
|
|
1286
1243
|
await this.communicator.sendRequest('disconnect');
|
|
1244
|
+
++this._sessionVersion;
|
|
1287
1245
|
this.connectionInfo = { isConnected: false };
|
|
1288
1246
|
const disconnectEvent = {
|
|
1289
1247
|
reason: 'user_action'
|
|
@@ -1314,30 +1272,59 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1314
1272
|
async getConnectionStatus() {
|
|
1315
1273
|
this.ensureInitialized();
|
|
1316
1274
|
try {
|
|
1275
|
+
const sv = this._sessionVersion;
|
|
1317
1276
|
const result = await this.communicator.sendRequest('getConnectionStatus');
|
|
1277
|
+
// skip if session changed mid-flight
|
|
1278
|
+
if (this._sessionVersion !== sv)
|
|
1279
|
+
return { ...this.connectionInfo };
|
|
1318
1280
|
if (result.isConnected && result.address) {
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1281
|
+
// verify pubkey→addr binding
|
|
1282
|
+
if (result.publicKey) {
|
|
1283
|
+
try {
|
|
1284
|
+
const derived = await deriveOctraAddress(result.publicKey);
|
|
1285
|
+
if (derived !== result.address) {
|
|
1286
|
+
this.logger.warn('Address-key binding mismatch on session restore — ignoring stale session');
|
|
1287
|
+
this.connectionInfo = { isConnected: false };
|
|
1288
|
+
return { ...this.connectionInfo };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
catch (e) {
|
|
1292
|
+
this.logger.warn('Address derivation check skipped (crypto unavailable):', e);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// validate untrusted balance/networkInfo before caching
|
|
1296
|
+
const balanceInfo = validateBalance(result.balance) ?? createDefaultBalance();
|
|
1297
|
+
const networkInfo = validateNetworkInfo(result.networkInfo)
|
|
1298
|
+
?? (result.networkId ? getNetworkConfig(result.networkId) : null);
|
|
1299
|
+
if (!networkInfo) {
|
|
1300
|
+
this.logger.warn('getConnectionStatus: wallet returned no network metadata — returning cached state');
|
|
1301
|
+
return this.connectionInfo;
|
|
1302
|
+
}
|
|
1303
|
+
const wasConnected = this.connectionInfo.isConnected;
|
|
1304
|
+
const permissions = result.permissions || [];
|
|
1305
|
+
// preserve existing connectedAt
|
|
1306
|
+
const connectedAt = this.connectionInfo.connectedAt || result.connectedAt || Date.now();
|
|
1324
1307
|
this.connectionInfo = {
|
|
1325
1308
|
isConnected: true,
|
|
1326
1309
|
address: result.address,
|
|
1327
1310
|
publicKey: result.publicKey,
|
|
1328
1311
|
balance: balanceInfo,
|
|
1329
1312
|
networkInfo,
|
|
1330
|
-
connectedAt
|
|
1331
|
-
|
|
1332
|
-
this.logger.log('Discovered existing connection:', this.connectionInfo);
|
|
1333
|
-
// Emit connect event to notify the wrapper
|
|
1334
|
-
const connectEvent = {
|
|
1335
|
-
address: result.address,
|
|
1336
|
-
balance: balanceInfo,
|
|
1337
|
-
networkInfo,
|
|
1338
|
-
permissions: result.permissions || []
|
|
1313
|
+
connectedAt,
|
|
1314
|
+
permissions
|
|
1339
1315
|
};
|
|
1340
|
-
this.
|
|
1316
|
+
this.logger.log('Discovered existing connection:', { address: result.address, network: networkInfo.id });
|
|
1317
|
+
// only emit on disconnected→connected transition
|
|
1318
|
+
if (!wasConnected) {
|
|
1319
|
+
const connectEvent = {
|
|
1320
|
+
address: result.address,
|
|
1321
|
+
publicKey: result.publicKey,
|
|
1322
|
+
balance: balanceInfo,
|
|
1323
|
+
networkInfo,
|
|
1324
|
+
permissions
|
|
1325
|
+
};
|
|
1326
|
+
this.emit('connect', connectEvent);
|
|
1327
|
+
}
|
|
1341
1328
|
}
|
|
1342
1329
|
else {
|
|
1343
1330
|
// No existing connection
|
|
@@ -1357,10 +1344,14 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1357
1344
|
* The extension broadcasts 'networkChanged' event to all connected dApps.
|
|
1358
1345
|
*/
|
|
1359
1346
|
async switchNetwork(networkId) {
|
|
1360
|
-
this.
|
|
1347
|
+
this.ensureConnected();
|
|
1361
1348
|
try {
|
|
1349
|
+
const sv = this._sessionVersion;
|
|
1362
1350
|
const result = await this.communicator.sendRequest('switch_network', { networkId });
|
|
1363
1351
|
this.logger.log(`Network switch result:`, result);
|
|
1352
|
+
// skip if session changed mid-flight
|
|
1353
|
+
if (this._sessionVersion !== sv)
|
|
1354
|
+
return { network: result.network || networkId, switched: result.switched ?? false };
|
|
1364
1355
|
if (result.switched) {
|
|
1365
1356
|
// Update internal state
|
|
1366
1357
|
const networkInfo = getNetworkConfig(networkId);
|
|
@@ -1381,18 +1372,9 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1381
1372
|
getNetworkId() {
|
|
1382
1373
|
return this.connectionInfo.networkInfo?.id || null;
|
|
1383
1374
|
}
|
|
1384
|
-
// ===================
|
|
1385
|
-
// WALLET INFORMATION
|
|
1386
|
-
// ===================
|
|
1387
|
-
/**
|
|
1388
|
-
* Get current wallet address
|
|
1389
|
-
*/
|
|
1390
1375
|
getAddress() {
|
|
1391
1376
|
return this.connectionInfo.address || null;
|
|
1392
1377
|
}
|
|
1393
|
-
/**
|
|
1394
|
-
* Get current balance
|
|
1395
|
-
*/
|
|
1396
1378
|
async getBalance(forceRefresh = false) {
|
|
1397
1379
|
this.ensureConnected();
|
|
1398
1380
|
try {
|
|
@@ -1403,22 +1385,27 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1403
1385
|
// Fetch balance from extension (bypasses CORS, has access to private balance)
|
|
1404
1386
|
let publicBalance = 0;
|
|
1405
1387
|
let privateBalance = 0;
|
|
1388
|
+
const sv = this._sessionVersion;
|
|
1406
1389
|
const extResult = await this.communicator.sendRequest('getBalance', { forceRefresh });
|
|
1407
1390
|
publicBalance = parseFloat(extResult.balance || '0');
|
|
1408
1391
|
privateBalance = parseFloat(extResult.privateBalance || '0');
|
|
1409
|
-
this.logger.log('Balance fetched from extension:', { public: publicBalance
|
|
1392
|
+
this.logger.log('Balance fetched from extension:', { public: publicBalance });
|
|
1410
1393
|
const result = {
|
|
1411
1394
|
public: publicBalance,
|
|
1412
1395
|
private: privateBalance,
|
|
1413
1396
|
total: publicBalance + privateBalance,
|
|
1414
1397
|
currency: 'OCT'
|
|
1415
1398
|
};
|
|
1416
|
-
//
|
|
1399
|
+
// skip if session changed mid-flight
|
|
1400
|
+
if (this._sessionVersion !== sv)
|
|
1401
|
+
return result;
|
|
1417
1402
|
if (this.connectionInfo.balance) {
|
|
1418
1403
|
const previousBalance = this.connectionInfo.balance;
|
|
1419
1404
|
this.connectionInfo.balance = result;
|
|
1420
|
-
//
|
|
1421
|
-
if (previousBalance.total !== result.total
|
|
1405
|
+
// emit on total or pub/priv split change
|
|
1406
|
+
if (previousBalance.total !== result.total ||
|
|
1407
|
+
previousBalance.public !== result.public ||
|
|
1408
|
+
previousBalance.private !== result.private) {
|
|
1422
1409
|
const balanceChangedEvent = {
|
|
1423
1410
|
address: this.connectionInfo.address,
|
|
1424
1411
|
previousBalance,
|
|
@@ -1433,52 +1420,63 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1433
1420
|
return result;
|
|
1434
1421
|
}
|
|
1435
1422
|
catch (error) {
|
|
1423
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1424
|
+
throw error;
|
|
1436
1425
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
|
|
1437
1426
|
}
|
|
1438
1427
|
}
|
|
1439
|
-
/**
|
|
1440
|
-
* Get network information
|
|
1441
|
-
*/
|
|
1442
1428
|
async getNetworkInfo() {
|
|
1443
1429
|
this.ensureInitialized();
|
|
1444
1430
|
try {
|
|
1431
|
+
const sv = this._sessionVersion;
|
|
1445
1432
|
const result = await this.communicator.sendRequest('get_network_info');
|
|
1446
|
-
|
|
1433
|
+
const networkInfo = validateNetworkInfo(result);
|
|
1434
|
+
if (!networkInfo) {
|
|
1435
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Extension returned invalid network info');
|
|
1436
|
+
}
|
|
1437
|
+
// skip if session changed mid-flight
|
|
1438
|
+
if (this._sessionVersion !== sv)
|
|
1439
|
+
return networkInfo;
|
|
1447
1440
|
if (this.connectionInfo.networkInfo) {
|
|
1448
1441
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1449
|
-
this.connectionInfo.networkInfo =
|
|
1450
|
-
|
|
1451
|
-
if (previousNetwork.id !== result.id) {
|
|
1442
|
+
this.connectionInfo.networkInfo = networkInfo;
|
|
1443
|
+
if (previousNetwork.id !== networkInfo.id) {
|
|
1452
1444
|
const networkChangedEvent = {
|
|
1453
1445
|
previousNetwork,
|
|
1454
|
-
newNetwork:
|
|
1446
|
+
newNetwork: networkInfo
|
|
1455
1447
|
};
|
|
1456
1448
|
this.emit('networkChanged', networkChangedEvent);
|
|
1457
1449
|
}
|
|
1458
1450
|
}
|
|
1459
1451
|
else {
|
|
1460
|
-
this.connectionInfo.networkInfo =
|
|
1452
|
+
this.connectionInfo.networkInfo = networkInfo;
|
|
1461
1453
|
}
|
|
1462
|
-
return
|
|
1454
|
+
return networkInfo;
|
|
1463
1455
|
}
|
|
1464
1456
|
catch (error) {
|
|
1457
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1458
|
+
throw error;
|
|
1465
1459
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
|
|
1466
1460
|
}
|
|
1467
1461
|
}
|
|
1468
|
-
// ===================
|
|
1469
|
-
// TRANSACTIONS
|
|
1470
|
-
// ===================
|
|
1471
|
-
/**
|
|
1472
|
-
* Send transaction
|
|
1473
|
-
*/
|
|
1474
1462
|
async sendTransaction(txData) {
|
|
1475
1463
|
this.ensureConnected();
|
|
1464
|
+
if (!isValidAddress(txData.to)) {
|
|
1465
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1466
|
+
}
|
|
1467
|
+
if (!isValidAmount(txData.amount)) {
|
|
1468
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1469
|
+
}
|
|
1470
|
+
if (txData.message && txData.message.length > 1000) {
|
|
1471
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1472
|
+
}
|
|
1476
1473
|
try {
|
|
1477
|
-
|
|
1474
|
+
// log non-sensitive only
|
|
1475
|
+
this.logger.log('Sending transaction:', { to: txData.to });
|
|
1478
1476
|
const result = await this.communicator.sendRequest('send_transaction', txData);
|
|
1479
1477
|
this.logger.log('Transaction result:', result);
|
|
1480
|
-
// Refresh balance after successful transaction
|
|
1481
|
-
if (result.success) {
|
|
1478
|
+
// Refresh balance after successful transaction (accept RFC 'accepted' or legacy 'success')
|
|
1479
|
+
if (result.accepted ?? result.success) {
|
|
1482
1480
|
setTimeout(() => {
|
|
1483
1481
|
this.getBalance(true).catch(error => {
|
|
1484
1482
|
this.logger.warn('Failed to refresh balance after transaction:', error);
|
|
@@ -1495,14 +1493,83 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1495
1493
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to send transaction', error);
|
|
1496
1494
|
}
|
|
1497
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Sign a transaction without broadcasting it (RFC-O-1 octra_signTransaction).
|
|
1498
|
+
* Returns the signed transaction object for manual submission via submitTransaction().
|
|
1499
|
+
*/
|
|
1500
|
+
async signTransaction(txData) {
|
|
1501
|
+
this.ensureConnected();
|
|
1502
|
+
if (!isValidAddress(txData.to)) {
|
|
1503
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1504
|
+
}
|
|
1505
|
+
if (!isValidAmount(txData.amount)) {
|
|
1506
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transaction amount');
|
|
1507
|
+
}
|
|
1508
|
+
if (txData.message && txData.message.length > 1000) {
|
|
1509
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transaction message too long (max 1,000 characters)');
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
this.logger.log('Requesting transaction signature:', { to: txData.to });
|
|
1513
|
+
const result = await this.communicator.sendRequest('sign_transaction', txData);
|
|
1514
|
+
return result;
|
|
1515
|
+
}
|
|
1516
|
+
catch (error) {
|
|
1517
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1518
|
+
throw error;
|
|
1519
|
+
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Failed to sign transaction', error);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Broadcast a pre-signed transaction (RFC-O-1 octra_submitTransaction).
|
|
1524
|
+
* Use after signTransaction() to submit the signed tx to the network.
|
|
1525
|
+
*/
|
|
1526
|
+
async submitTransaction(signedTx) {
|
|
1527
|
+
this.ensureConnected();
|
|
1528
|
+
if (!signedTx || typeof signedTx !== 'object') {
|
|
1529
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'signedTx must be an object');
|
|
1530
|
+
}
|
|
1531
|
+
try {
|
|
1532
|
+
this.logger.log('Submitting pre-signed transaction');
|
|
1533
|
+
const result = await this.communicator.sendRequest('broadcast_only', { signedTx });
|
|
1534
|
+
return result;
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
if (error instanceof ZeroXIOWalletError)
|
|
1538
|
+
throw error;
|
|
1539
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to submit transaction', error);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1498
1542
|
/**
|
|
1499
1543
|
* Call a smart contract method (state-changing).
|
|
1500
1544
|
* The extension builds, signs, and submits the transaction via octra_submit.
|
|
1501
1545
|
*/
|
|
1502
1546
|
async callContract(callData) {
|
|
1503
1547
|
this.ensureConnected();
|
|
1548
|
+
if (!isValidAddress(callData.contract)) {
|
|
1549
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1550
|
+
}
|
|
1551
|
+
if (!callData.method || typeof callData.method !== 'string') {
|
|
1552
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method is required');
|
|
1553
|
+
}
|
|
1554
|
+
if (callData.method.length > 200) {
|
|
1555
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract method name too long (max 200 characters)');
|
|
1556
|
+
}
|
|
1557
|
+
if (callData.amount != null) {
|
|
1558
|
+
this.assertExactOCTAmount(callData.amount, 'Contract call amount');
|
|
1559
|
+
}
|
|
1560
|
+
try {
|
|
1561
|
+
if (JSON.stringify(callData.params).length > 65536) {
|
|
1562
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract params too large (max 64 KB)');
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
catch (e) {
|
|
1566
|
+
if (e instanceof ZeroXIOWalletError)
|
|
1567
|
+
throw e;
|
|
1568
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Contract params are not serialisable');
|
|
1569
|
+
}
|
|
1504
1570
|
try {
|
|
1505
|
-
|
|
1571
|
+
// log non-sensitive only
|
|
1572
|
+
this.logger.log('Calling contract:', { contract: callData.contract, method: callData.method });
|
|
1506
1573
|
const result = await this.communicator.sendRequest('call_contract', {
|
|
1507
1574
|
contract: callData.contract,
|
|
1508
1575
|
method: callData.method,
|
|
@@ -1527,13 +1594,34 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1527
1594
|
*/
|
|
1528
1595
|
async contractCallView(viewData) {
|
|
1529
1596
|
this.ensureInitialized();
|
|
1597
|
+
if (!isValidAddress(viewData.contract)) {
|
|
1598
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1599
|
+
}
|
|
1600
|
+
if (!viewData.method || typeof viewData.method !== 'string') {
|
|
1601
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract method is required');
|
|
1602
|
+
}
|
|
1603
|
+
if (viewData.method.length > 200) {
|
|
1604
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract method name too long (max 200 characters)');
|
|
1605
|
+
}
|
|
1606
|
+
try {
|
|
1607
|
+
if (JSON.stringify(viewData.params).length > 65536) {
|
|
1608
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract params too large (max 64 KB)');
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
catch (e) {
|
|
1612
|
+
if (e instanceof ZeroXIOWalletError)
|
|
1613
|
+
throw e;
|
|
1614
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Contract params are not serialisable');
|
|
1615
|
+
}
|
|
1530
1616
|
try {
|
|
1531
|
-
|
|
1617
|
+
// log non-sensitive only
|
|
1618
|
+
this.logger.log('Contract view call:', { contract: viewData.contract, method: viewData.method });
|
|
1532
1619
|
const result = await this.communicator.sendRequest('contract_call_view', {
|
|
1533
1620
|
contract: viewData.contract,
|
|
1534
1621
|
method: viewData.method,
|
|
1535
1622
|
params: viewData.params,
|
|
1536
|
-
|
|
1623
|
+
// only include caller if explicit
|
|
1624
|
+
...(viewData.caller != null ? { caller: viewData.caller } : {}),
|
|
1537
1625
|
});
|
|
1538
1626
|
this.logger.log('Contract view result:', result);
|
|
1539
1627
|
return result;
|
|
@@ -1551,6 +1639,15 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1551
1639
|
*/
|
|
1552
1640
|
async getContractStorage(contract, key) {
|
|
1553
1641
|
this.ensureInitialized();
|
|
1642
|
+
if (!isValidAddress(contract)) {
|
|
1643
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid contract address');
|
|
1644
|
+
}
|
|
1645
|
+
if (!key || typeof key !== 'string') {
|
|
1646
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Storage key is required');
|
|
1647
|
+
}
|
|
1648
|
+
if (key.length > 200) {
|
|
1649
|
+
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Storage key too long (max 200 characters)');
|
|
1650
|
+
}
|
|
1554
1651
|
try {
|
|
1555
1652
|
this.logger.log('Getting contract storage:', { contract, key });
|
|
1556
1653
|
const result = await this.communicator.sendRequest('get_contract_storage', {
|
|
@@ -1583,12 +1680,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1583
1680
|
throw new ZeroXIOWalletError(ErrorCode.NETWORK_ERROR, 'Failed to get transaction history', error);
|
|
1584
1681
|
}
|
|
1585
1682
|
}
|
|
1586
|
-
// ===================
|
|
1587
|
-
// PRIVATE FEATURES
|
|
1588
|
-
// ===================
|
|
1589
|
-
/**
|
|
1590
|
-
* Get private balance information
|
|
1591
|
-
*/
|
|
1592
1683
|
async getPrivateBalanceInfo() {
|
|
1593
1684
|
this.ensureConnected();
|
|
1594
1685
|
try {
|
|
@@ -1604,13 +1695,17 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1604
1695
|
*/
|
|
1605
1696
|
async encryptBalance(amount) {
|
|
1606
1697
|
this.ensureConnected();
|
|
1698
|
+
this.assertExactOCTAmount(amount, 'Encrypt amount');
|
|
1699
|
+
if (!isValidAmount(amount)) {
|
|
1700
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
|
|
1701
|
+
}
|
|
1607
1702
|
try {
|
|
1608
1703
|
const result = await this.communicator.sendRequest('encrypt_balance', { amount });
|
|
1609
1704
|
// Refresh balance after encryption
|
|
1610
1705
|
setTimeout(() => {
|
|
1611
1706
|
this.getBalance(true).catch(() => { });
|
|
1612
1707
|
}, 1000);
|
|
1613
|
-
return result
|
|
1708
|
+
return result;
|
|
1614
1709
|
}
|
|
1615
1710
|
catch (error) {
|
|
1616
1711
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to encrypt balance', error);
|
|
@@ -1621,13 +1716,17 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1621
1716
|
*/
|
|
1622
1717
|
async decryptBalance(amount) {
|
|
1623
1718
|
this.ensureConnected();
|
|
1719
|
+
this.assertExactOCTAmount(amount, 'Decrypt amount');
|
|
1720
|
+
if (!isValidAmount(amount)) {
|
|
1721
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid amount');
|
|
1722
|
+
}
|
|
1624
1723
|
try {
|
|
1625
1724
|
const result = await this.communicator.sendRequest('decrypt_balance', { amount });
|
|
1626
1725
|
// Refresh balance after decryption
|
|
1627
1726
|
setTimeout(() => {
|
|
1628
1727
|
this.getBalance(true).catch(() => { });
|
|
1629
1728
|
}, 1000);
|
|
1630
|
-
return result
|
|
1729
|
+
return result;
|
|
1631
1730
|
}
|
|
1632
1731
|
catch (error) {
|
|
1633
1732
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to decrypt balance', error);
|
|
@@ -1643,10 +1742,21 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1643
1742
|
*/
|
|
1644
1743
|
async sendPrivateTransfer(transferData) {
|
|
1645
1744
|
this.ensureConnected();
|
|
1745
|
+
if (!isValidAddress(transferData.to)) {
|
|
1746
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_ADDRESS, 'Invalid recipient address');
|
|
1747
|
+
}
|
|
1748
|
+
if (!isValidAmount(transferData.amount)) {
|
|
1749
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transfer amount');
|
|
1750
|
+
}
|
|
1751
|
+
this.assertExactOCTAmount(transferData.amount, 'Transfer amount');
|
|
1752
|
+
// bound msg size
|
|
1753
|
+
if (transferData.message && transferData.message.length > 1000) {
|
|
1754
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Transfer message too long (max 1,000 characters)');
|
|
1755
|
+
}
|
|
1646
1756
|
try {
|
|
1647
1757
|
const result = await this.communicator.sendRequest('send_private_transfer', transferData);
|
|
1648
|
-
// Refresh balance after transfer
|
|
1649
|
-
if (result.success) {
|
|
1758
|
+
// Refresh balance after transfer (accept RFC 'accepted' or legacy 'success')
|
|
1759
|
+
if (result.accepted ?? result.success) {
|
|
1650
1760
|
setTimeout(() => {
|
|
1651
1761
|
this.getBalance(true).catch(() => { });
|
|
1652
1762
|
}, 1000);
|
|
@@ -1678,12 +1788,16 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1678
1788
|
*/
|
|
1679
1789
|
async claimPrivateTransfer(transferId) {
|
|
1680
1790
|
this.ensureConnected();
|
|
1791
|
+
// validate transfer ID
|
|
1792
|
+
if (!transferId || typeof transferId !== 'string') {
|
|
1793
|
+
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Invalid transfer ID');
|
|
1794
|
+
}
|
|
1681
1795
|
try {
|
|
1682
1796
|
const result = await this.communicator.sendRequest('claim_private_transfer', {
|
|
1683
1797
|
transferId
|
|
1684
1798
|
});
|
|
1685
|
-
// Refresh balance after claiming
|
|
1686
|
-
if (result.success) {
|
|
1799
|
+
// Refresh balance after claiming (accept RFC 'accepted' or legacy 'success')
|
|
1800
|
+
if (result.accepted ?? result.success) {
|
|
1687
1801
|
setTimeout(() => {
|
|
1688
1802
|
this.getBalance(true).catch(() => { });
|
|
1689
1803
|
}, 1000);
|
|
@@ -1694,9 +1808,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1694
1808
|
throw new ZeroXIOWalletError(ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
|
|
1695
1809
|
}
|
|
1696
1810
|
}
|
|
1697
|
-
// ===================
|
|
1698
|
-
// MESSAGE SIGNING
|
|
1699
|
-
// ===================
|
|
1700
1811
|
/**
|
|
1701
1812
|
* Sign an arbitrary message with the wallet's private key
|
|
1702
1813
|
* The user will be prompted to approve the signature request in the extension
|
|
@@ -1734,9 +1845,27 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1734
1845
|
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Failed to sign message', error);
|
|
1735
1846
|
}
|
|
1736
1847
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1848
|
+
/**
|
|
1849
|
+
* Sign a domain-separated authentication message.
|
|
1850
|
+
* Unlike `signMessage()`, this prepends a standard header that binds the signature
|
|
1851
|
+
* to the calling service and a one-time nonce, preventing cross-service replay attacks.
|
|
1852
|
+
*
|
|
1853
|
+
* @param service - Identifies the relying service (e.g. 'MyDApp' or 'api.mydapp.com')
|
|
1854
|
+
* @param nonce - Unique one-time value — use a server-generated UUID or challenge
|
|
1855
|
+
* @returns Promise resolving to the base64-encoded Ed25519 signature
|
|
1856
|
+
*/
|
|
1857
|
+
async signAuthMessage(service, nonce) {
|
|
1858
|
+
this.ensureConnected();
|
|
1859
|
+
if (!service || typeof service !== 'string') {
|
|
1860
|
+
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Service name is required');
|
|
1861
|
+
}
|
|
1862
|
+
if (!nonce || typeof nonce !== 'string') {
|
|
1863
|
+
throw new ZeroXIOWalletError(ErrorCode.SIGNATURE_FAILED, 'Nonce is required');
|
|
1864
|
+
}
|
|
1865
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
|
|
1866
|
+
const domainSeparated = `0xio auth\nService: ${service}\nNonce: ${nonce}\nOrigin: ${origin}`;
|
|
1867
|
+
return this.signMessage(domainSeparated);
|
|
1868
|
+
}
|
|
1740
1869
|
ensureInitialized() {
|
|
1741
1870
|
if (!this.isInitialized) {
|
|
1742
1871
|
throw new ZeroXIOWalletError(ErrorCode.UNKNOWN_ERROR, 'SDK not initialized. Call initialize() first.');
|
|
@@ -1749,7 +1878,6 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1749
1878
|
}
|
|
1750
1879
|
}
|
|
1751
1880
|
setupExtensionEventListeners() {
|
|
1752
|
-
// Listen for extension events through the communicator
|
|
1753
1881
|
this.communicator.on('accountChanged', (event) => {
|
|
1754
1882
|
this.handleAccountChanged(event.data);
|
|
1755
1883
|
});
|
|
@@ -1768,76 +1896,93 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1768
1896
|
this.communicator.on('transactionConfirmed', (event) => {
|
|
1769
1897
|
this.handleTransactionConfirmed(event.data);
|
|
1770
1898
|
});
|
|
1899
|
+
this.communicator.on('permissionsChanged', (event) => {
|
|
1900
|
+
const permissions = event.data ?? event;
|
|
1901
|
+
if (this.connectionInfo.isConnected) {
|
|
1902
|
+
this.connectionInfo.permissions = Array.isArray(permissions) ? permissions : [];
|
|
1903
|
+
}
|
|
1904
|
+
this.emit('permissionsChanged', permissions);
|
|
1905
|
+
});
|
|
1906
|
+
this.communicator.on('message', (event) => {
|
|
1907
|
+
this.emit('message', event.data ?? event);
|
|
1908
|
+
});
|
|
1771
1909
|
this.logger.log('Extension event listeners setup complete');
|
|
1772
1910
|
}
|
|
1773
|
-
/**
|
|
1774
|
-
* Handle account changed event from extension
|
|
1775
|
-
*/
|
|
1776
1911
|
handleAccountChanged(data) {
|
|
1912
|
+
++this._sessionVersion;
|
|
1777
1913
|
const previousAddress = this.connectionInfo.address;
|
|
1778
1914
|
this.connectionInfo.address = data.address;
|
|
1915
|
+
// clear stale pubkey on acct change
|
|
1916
|
+
this.connectionInfo.publicKey = data.publicKey;
|
|
1779
1917
|
if (data.balance) {
|
|
1780
|
-
|
|
1918
|
+
const validated = validateBalance(data.balance);
|
|
1919
|
+
if (validated) {
|
|
1920
|
+
this.connectionInfo.balance = validated;
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
this.connectionInfo.balance = undefined; // clear stale balance
|
|
1924
|
+
}
|
|
1781
1925
|
}
|
|
1782
1926
|
const accountChangedEvent = {
|
|
1783
1927
|
previousAddress,
|
|
1784
1928
|
newAddress: data.address,
|
|
1929
|
+
publicKey: data.publicKey,
|
|
1785
1930
|
balance: data.balance ?? this.connectionInfo.balance
|
|
1786
1931
|
};
|
|
1787
1932
|
this.emit('accountChanged', accountChangedEvent);
|
|
1788
|
-
this.logger.log('Account changed:', accountChangedEvent);
|
|
1933
|
+
this.logger.log('Account changed:', { newAddress: accountChangedEvent.newAddress });
|
|
1789
1934
|
}
|
|
1790
|
-
/**
|
|
1791
|
-
* Handle network changed event from extension
|
|
1792
|
-
*/
|
|
1793
1935
|
handleNetworkChanged(data) {
|
|
1794
1936
|
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1795
|
-
|
|
1937
|
+
// validate networkInfo — drop invalid
|
|
1938
|
+
const networkInfo = validateNetworkInfo(data.networkInfo);
|
|
1939
|
+
if (!networkInfo) {
|
|
1940
|
+
this.logger.warn('Received invalid networkInfo in networkChanged event, ignoring');
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
this.connectionInfo.networkInfo = networkInfo;
|
|
1944
|
+
// invalidate balance on network change
|
|
1945
|
+
this.connectionInfo.balance = undefined;
|
|
1796
1946
|
const networkChangedEvent = {
|
|
1797
1947
|
previousNetwork,
|
|
1798
|
-
newNetwork:
|
|
1948
|
+
newNetwork: networkInfo
|
|
1799
1949
|
};
|
|
1800
1950
|
this.emit('networkChanged', networkChangedEvent);
|
|
1801
1951
|
this.logger.log('Network changed:', networkChangedEvent);
|
|
1802
1952
|
}
|
|
1803
|
-
/**
|
|
1804
|
-
* Handle balance changed event from extension
|
|
1805
|
-
*/
|
|
1806
1953
|
handleBalanceChanged(data) {
|
|
1954
|
+
const balance = validateBalance(data.balance);
|
|
1955
|
+
if (!balance) {
|
|
1956
|
+
this.logger.warn('Received invalid balance in balanceChanged event, ignoring');
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1807
1959
|
const previousBalance = this.connectionInfo.balance;
|
|
1808
|
-
this.connectionInfo.balance =
|
|
1960
|
+
this.connectionInfo.balance = balance;
|
|
1809
1961
|
const balanceChangedEvent = {
|
|
1810
1962
|
address: this.connectionInfo.address,
|
|
1811
1963
|
previousBalance,
|
|
1812
|
-
newBalance:
|
|
1964
|
+
newBalance: balance
|
|
1813
1965
|
};
|
|
1814
1966
|
this.emit('balanceChanged', balanceChangedEvent);
|
|
1815
|
-
this.logger.log('Balance changed:',
|
|
1967
|
+
this.logger.log('Balance changed:', { public: balance.public });
|
|
1816
1968
|
}
|
|
1817
|
-
/**
|
|
1818
|
-
* Handle extension locked event
|
|
1819
|
-
*/
|
|
1820
1969
|
handleExtensionLocked() {
|
|
1970
|
+
++this._sessionVersion;
|
|
1821
1971
|
this.connectionInfo = { isConnected: false };
|
|
1972
|
+
this.emit('extensionLocked', {});
|
|
1822
1973
|
const disconnectEvent = {
|
|
1823
1974
|
reason: 'extension_locked'
|
|
1824
1975
|
};
|
|
1825
1976
|
this.emit('disconnect', disconnectEvent);
|
|
1826
1977
|
this.logger.log('Extension locked - disconnected');
|
|
1827
1978
|
}
|
|
1828
|
-
/**
|
|
1829
|
-
* Handle extension unlocked event
|
|
1830
|
-
*/
|
|
1831
1979
|
handleExtensionUnlocked() {
|
|
1832
|
-
|
|
1980
|
+
this.emit('extensionUnlocked', {});
|
|
1833
1981
|
this.getConnectionStatus().catch(() => {
|
|
1834
1982
|
this.logger.warn('Could not restore connection after unlock');
|
|
1835
1983
|
});
|
|
1836
1984
|
this.logger.log('Extension unlocked');
|
|
1837
1985
|
}
|
|
1838
|
-
/**
|
|
1839
|
-
* Handle transaction confirmed event
|
|
1840
|
-
*/
|
|
1841
1986
|
handleTransactionConfirmed(data) {
|
|
1842
1987
|
this.emit('transactionConfirmed', {
|
|
1843
1988
|
txHash: data.txHash,
|
|
@@ -1850,17 +1995,28 @@ class ZeroXIOWallet extends EventEmitter {
|
|
|
1850
1995
|
}, 2000);
|
|
1851
1996
|
this.logger.log('Transaction confirmed:', data.txHash);
|
|
1852
1997
|
}
|
|
1853
|
-
// ===================
|
|
1854
|
-
// CLEANUP
|
|
1855
|
-
// ===================
|
|
1856
1998
|
/**
|
|
1857
|
-
*
|
|
1999
|
+
* Reject numeric amounts that cannot be represented exactly in micro-OCT.
|
|
2000
|
+
* e.g. 0.1 + 0.2 = 0.30000000000000004 — the extension would sign the wrong value.
|
|
2001
|
+
* String amounts bypass this check (caller is responsible for correctness).
|
|
1858
2002
|
*/
|
|
2003
|
+
assertExactOCTAmount(amount, label) {
|
|
2004
|
+
if (typeof amount === 'number') {
|
|
2005
|
+
const micro = Math.round(amount * 1000000);
|
|
2006
|
+
if (Math.abs(amount - micro / 1000000) > 1e-10) {
|
|
2007
|
+
const suggested = (micro / 1000000).toFixed(6);
|
|
2008
|
+
throw new ZeroXIOWalletError(ErrorCode.INVALID_AMOUNT, `${label} cannot be represented exactly in micro-OCT. ` +
|
|
2009
|
+
`Pass a string instead (e.g. "${suggested}").`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1859
2013
|
cleanup() {
|
|
1860
2014
|
this.communicator.cleanup();
|
|
1861
2015
|
this.removeAllListeners();
|
|
1862
2016
|
this.connectionInfo = { isConnected: false };
|
|
1863
2017
|
this.isInitialized = false;
|
|
2018
|
+
this._initPromise = null;
|
|
2019
|
+
++this._sessionVersion;
|
|
1864
2020
|
this.logger.log('SDK cleanup complete');
|
|
1865
2021
|
}
|
|
1866
2022
|
}
|
|
@@ -1870,6 +2026,209 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
1870
2026
|
ZeroXIOWallet: ZeroXIOWallet
|
|
1871
2027
|
});
|
|
1872
2028
|
|
|
2029
|
+
/**
|
|
2030
|
+
* RFC-O-1 OctraProvider transport adapter.
|
|
2031
|
+
*
|
|
2032
|
+
* Targets any wallet that exposes `window.octra` per the RFC-O-1 specification:
|
|
2033
|
+
* window.octra.isOctra === true
|
|
2034
|
+
* window.octra.request({ method, params }) → Promise<unknown>
|
|
2035
|
+
* window.octra.on(event, listener) / removeListener(event, listener)
|
|
2036
|
+
*
|
|
2037
|
+
* This adapter translates the SDK's internal method names into RFC-O-1 method
|
|
2038
|
+
* names and maps events back to the SDK event vocabulary.
|
|
2039
|
+
*
|
|
2040
|
+
* Non-standard SDK methods (ping, register_dapp, getTransactionHistory, etc.)
|
|
2041
|
+
* are passed through as-is; the wallet's request() handles or rejects them.
|
|
2042
|
+
*/
|
|
2043
|
+
/** SDK method → RFC-O-1 method name */
|
|
2044
|
+
const SDK_TO_RFC = {
|
|
2045
|
+
get_network_info: 'octra_networkInfo',
|
|
2046
|
+
switch_network: 'octra_switchNetwork',
|
|
2047
|
+
signMessage: 'octra_signMessage',
|
|
2048
|
+
send_transaction: 'octra_sendTransaction',
|
|
2049
|
+
sign_transaction: 'octra_signTransaction',
|
|
2050
|
+
broadcast_only: 'octra_submitTransaction',
|
|
2051
|
+
call_contract: 'octra_sendContractTransaction',
|
|
2052
|
+
contract_call_view: 'octra_callContract',
|
|
2053
|
+
get_private_balance_info: 'octra_getEncryptedBalance',
|
|
2054
|
+
encrypt_balance: 'octra_encryptBalance',
|
|
2055
|
+
decrypt_balance: 'octra_decryptBalance',
|
|
2056
|
+
send_private_transfer: 'octra_sendPrivateTransfer',
|
|
2057
|
+
claim_private_transfer: 'octra_claimStealth',
|
|
2058
|
+
};
|
|
2059
|
+
/** RFC-O-1 error code → SDK ErrorCode string */
|
|
2060
|
+
const RFC_TO_SDK_ERROR = {
|
|
2061
|
+
4001: 'USER_REJECTED',
|
|
2062
|
+
4100: 'PERMISSION_DENIED',
|
|
2063
|
+
4200: 'UNKNOWN_ERROR',
|
|
2064
|
+
4900: 'CONNECTION_REFUSED',
|
|
2065
|
+
4901: 'NETWORK_ERROR',
|
|
2066
|
+
};
|
|
2067
|
+
function getProvider() {
|
|
2068
|
+
return typeof window !== 'undefined' ? window.octra : null;
|
|
2069
|
+
}
|
|
2070
|
+
function mapError(err) {
|
|
2071
|
+
const code = RFC_TO_SDK_ERROR[err?.code] ?? 'UNKNOWN_ERROR';
|
|
2072
|
+
return { code, message: err?.message ?? 'Request failed' };
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Build a connect response compatible with what the SDK expects
|
|
2076
|
+
* ({ address, networkInfo, permissions, balance }) by making
|
|
2077
|
+
* three RFC-O-1 calls: octra_requestAccounts, octra_networkInfo, octra_permissions.
|
|
2078
|
+
*/
|
|
2079
|
+
async function rfcConnect(provider, params) {
|
|
2080
|
+
const requestPerms = params?.permissions ?? params?.requestPermissions ?? [];
|
|
2081
|
+
const accounts = (await provider.request({
|
|
2082
|
+
method: 'octra_requestAccounts',
|
|
2083
|
+
params: [{ permissions: requestPerms }],
|
|
2084
|
+
}));
|
|
2085
|
+
const address = accounts?.[0] ?? null;
|
|
2086
|
+
const [networkInfo, permissions] = await Promise.all([
|
|
2087
|
+
provider.request({ method: 'octra_networkInfo' }),
|
|
2088
|
+
provider.request({ method: 'octra_permissions' }),
|
|
2089
|
+
]);
|
|
2090
|
+
return { address, networkInfo, permissions, balance: null };
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Build a getConnectionStatus response by checking octra_accounts.
|
|
2094
|
+
*/
|
|
2095
|
+
async function rfcConnectionStatus(provider) {
|
|
2096
|
+
const accounts = (await provider.request({ method: 'octra_accounts' }));
|
|
2097
|
+
const address = accounts?.[0] ?? null;
|
|
2098
|
+
if (!address)
|
|
2099
|
+
return { isConnected: false };
|
|
2100
|
+
const [networkInfo, permissions] = await Promise.all([
|
|
2101
|
+
provider.request({ method: 'octra_networkInfo' }),
|
|
2102
|
+
provider.request({ method: 'octra_permissions' }),
|
|
2103
|
+
]);
|
|
2104
|
+
return { isConnected: true, address, networkInfo, permissions };
|
|
2105
|
+
}
|
|
2106
|
+
function createOctraProviderAdapter() {
|
|
2107
|
+
let _handler = null;
|
|
2108
|
+
return {
|
|
2109
|
+
name: 'octra-provider',
|
|
2110
|
+
displayName: 'Octra Wallet (RFC-O-1)',
|
|
2111
|
+
detect() {
|
|
2112
|
+
const p = getProvider();
|
|
2113
|
+
return p?.isOctra === true;
|
|
2114
|
+
},
|
|
2115
|
+
postRequest(request) {
|
|
2116
|
+
const { id, method, params } = request;
|
|
2117
|
+
const provider = getProvider();
|
|
2118
|
+
if (!provider) {
|
|
2119
|
+
_handler?.({
|
|
2120
|
+
requestId: id,
|
|
2121
|
+
success: false,
|
|
2122
|
+
error: { code: 'EXTENSION_NOT_FOUND', message: 'No RFC-O-1 provider found on window.octra' },
|
|
2123
|
+
});
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
(async () => {
|
|
2127
|
+
try {
|
|
2128
|
+
let data;
|
|
2129
|
+
if (method === 'ping') {
|
|
2130
|
+
data = { available: true };
|
|
2131
|
+
}
|
|
2132
|
+
else if (method === 'register_dapp') {
|
|
2133
|
+
data = { success: true };
|
|
2134
|
+
}
|
|
2135
|
+
else if (method === 'connect') {
|
|
2136
|
+
data = await rfcConnect(provider, params);
|
|
2137
|
+
}
|
|
2138
|
+
else if (method === 'disconnect') {
|
|
2139
|
+
await provider.request({ method: 'disconnect' }).catch(() => { });
|
|
2140
|
+
data = { success: true };
|
|
2141
|
+
}
|
|
2142
|
+
else if (method === 'getConnectionStatus') {
|
|
2143
|
+
data = await rfcConnectionStatus(provider);
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
const rfcMethod = SDK_TO_RFC[method] ?? method;
|
|
2147
|
+
data = await provider.request({ method: rfcMethod, params });
|
|
2148
|
+
}
|
|
2149
|
+
_handler?.({ requestId: id, success: true, data });
|
|
2150
|
+
}
|
|
2151
|
+
catch (err) {
|
|
2152
|
+
_handler?.({ requestId: id, success: false, error: mapError(err) });
|
|
2153
|
+
}
|
|
2154
|
+
})();
|
|
2155
|
+
},
|
|
2156
|
+
listen(handler) {
|
|
2157
|
+
_handler = handler;
|
|
2158
|
+
const provider = getProvider();
|
|
2159
|
+
if (!provider)
|
|
2160
|
+
return () => { _handler = null; };
|
|
2161
|
+
// RFC-O-1 event → SDK event name + data shape
|
|
2162
|
+
const onConnect = (data) => handler({ eventType: 'connect', eventData: data });
|
|
2163
|
+
const onDisconnect = (data) => handler({ eventType: 'disconnect', eventData: { reason: 'network_error', ...data } });
|
|
2164
|
+
const onAccountsChanged = (accounts) => handler({ eventType: 'accountChanged', eventData: { address: accounts?.[0] ?? null } });
|
|
2165
|
+
const onNetworkChanged = (data) => handler({ eventType: 'networkChanged', eventData: { networkInfo: data } });
|
|
2166
|
+
const onBalanceChanged = (data) => handler({ eventType: 'balanceChanged', eventData: data });
|
|
2167
|
+
const onTransactionChanged = (data) => handler({ eventType: 'transactionConfirmed', eventData: data });
|
|
2168
|
+
const onPermissionsChanged = (data) => handler({ eventType: 'permissionsChanged', eventData: data });
|
|
2169
|
+
provider.on('connect', onConnect);
|
|
2170
|
+
provider.on('disconnect', onDisconnect);
|
|
2171
|
+
provider.on('accountsChanged', onAccountsChanged);
|
|
2172
|
+
provider.on('networkChanged', onNetworkChanged);
|
|
2173
|
+
provider.on('balanceChanged', onBalanceChanged);
|
|
2174
|
+
provider.on('transactionChanged', onTransactionChanged);
|
|
2175
|
+
provider.on('permissionsChanged', onPermissionsChanged);
|
|
2176
|
+
const cleanup = () => {
|
|
2177
|
+
provider.removeListener('connect', onConnect);
|
|
2178
|
+
provider.removeListener('disconnect', onDisconnect);
|
|
2179
|
+
provider.removeListener('accountsChanged', onAccountsChanged);
|
|
2180
|
+
provider.removeListener('networkChanged', onNetworkChanged);
|
|
2181
|
+
provider.removeListener('balanceChanged', onBalanceChanged);
|
|
2182
|
+
provider.removeListener('transactionChanged', onTransactionChanged);
|
|
2183
|
+
provider.removeListener('permissionsChanged', onPermissionsChanged);
|
|
2184
|
+
_handler = null;
|
|
2185
|
+
};
|
|
2186
|
+
return cleanup;
|
|
2187
|
+
},
|
|
2188
|
+
listenForReady(onReady) {
|
|
2189
|
+
const handler = () => onReady();
|
|
2190
|
+
window.addEventListener('octraWalletReady', handler);
|
|
2191
|
+
window.addEventListener('octra#initialized', handler);
|
|
2192
|
+
return () => {
|
|
2193
|
+
window.removeEventListener('octraWalletReady', handler);
|
|
2194
|
+
window.removeEventListener('octra#initialized', handler);
|
|
2195
|
+
};
|
|
2196
|
+
},
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
/** Default RFC-O-1 adapter instance. */
|
|
2200
|
+
const OctraProviderAdapter = createOctraProviderAdapter();
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* 0xio SDK — Wallet Adapter Registry
|
|
2204
|
+
*
|
|
2205
|
+
* Add new wallet adapters here. Detection order determines which wallet takes
|
|
2206
|
+
* priority when multiple wallets are installed at the same time.
|
|
2207
|
+
*/
|
|
2208
|
+
const REGISTERED_ADAPTERS = [
|
|
2209
|
+
ZeroXIOAdapter, // 0xio extension (postMessage protocol) — highest priority
|
|
2210
|
+
OctraProviderAdapter, // any RFC-O-1 compliant wallet (window.octra)
|
|
2211
|
+
// Add new wallet adapters here — detection runs in order, first match wins
|
|
2212
|
+
];
|
|
2213
|
+
/**
|
|
2214
|
+
* Auto-detects the first available wallet in the current page.
|
|
2215
|
+
* Returns null if no supported wallet is found.
|
|
2216
|
+
*
|
|
2217
|
+
* @example
|
|
2218
|
+
* const adapter = detectWalletAdapter();
|
|
2219
|
+
* if (!adapter) throw new Error('No supported wallet found');
|
|
2220
|
+
* const wallet = new ZeroXIOWallet({ appName: 'My DApp', adapter });
|
|
2221
|
+
*/
|
|
2222
|
+
function detectWalletAdapter() {
|
|
2223
|
+
if (typeof window === 'undefined')
|
|
2224
|
+
return null;
|
|
2225
|
+
return REGISTERED_ADAPTERS.find((a) => a.detect()) ?? null;
|
|
2226
|
+
}
|
|
2227
|
+
/** Returns all registered adapter instances. */
|
|
2228
|
+
function getAllAdapters() {
|
|
2229
|
+
return [...REGISTERED_ADAPTERS];
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1873
2232
|
/**
|
|
1874
2233
|
* 0xio Wallet SDK - Main Entry Point
|
|
1875
2234
|
* Official SDK for integrating with 0xio Wallet Extension
|
|
@@ -1896,7 +2255,7 @@ var wallet = /*#__PURE__*/Object.freeze({
|
|
|
1896
2255
|
*/
|
|
1897
2256
|
// Main exports
|
|
1898
2257
|
// Version information
|
|
1899
|
-
const SDK_VERSION = '2.
|
|
2258
|
+
const SDK_VERSION = '2.7.1';
|
|
1900
2259
|
const MIN_EXTENSION_VERSION = '2.0.1'; // Mainnet Alpha
|
|
1901
2260
|
const MIN_EXTENSION_VERSION_DEVNET = '2.2.1'; // Devnet (contract calls, privacy)
|
|
1902
2261
|
const SUPPORTED_EXTENSION_VERSIONS = '^2.0.1'; // Supports all versions >= 2.0.1
|
|
@@ -1906,37 +2265,49 @@ async function createZeroXIOWallet(config) {
|
|
|
1906
2265
|
const wallet$1 = new ZeroXIOWallet({
|
|
1907
2266
|
appName: config.appName,
|
|
1908
2267
|
appDescription: config.appDescription,
|
|
1909
|
-
debug: config.debug || false
|
|
2268
|
+
debug: config.debug || false,
|
|
2269
|
+
...(config.adapter ? { adapter: config.adapter } : {}),
|
|
1910
2270
|
});
|
|
1911
2271
|
await wallet$1.initialize();
|
|
1912
2272
|
if (config.autoConnect) {
|
|
1913
2273
|
try {
|
|
1914
2274
|
await wallet$1.connect();
|
|
1915
2275
|
}
|
|
1916
|
-
catch
|
|
1917
|
-
if (config.debug) ;
|
|
2276
|
+
catch {
|
|
1918
2277
|
// Don't throw - let the app handle connection manually
|
|
1919
2278
|
}
|
|
1920
2279
|
}
|
|
1921
2280
|
return wallet$1;
|
|
1922
2281
|
}
|
|
1923
|
-
// Legacy alias for backward compatibility
|
|
1924
|
-
const createOctraWallet = createZeroXIOWallet;
|
|
1925
2282
|
// Browser detection and compatibility check
|
|
1926
2283
|
function checkSDKCompatibility() {
|
|
1927
2284
|
const issues = [];
|
|
1928
2285
|
const recommendations = [];
|
|
1929
|
-
//
|
|
2286
|
+
// Hard blockers — SDK cannot function without these
|
|
1930
2287
|
if (typeof window === 'undefined') {
|
|
1931
2288
|
issues.push('Window object not available');
|
|
1932
2289
|
recommendations.push('SDK must be used in a browser environment');
|
|
2290
|
+
return { compatible: false, issues, recommendations };
|
|
2291
|
+
}
|
|
2292
|
+
if (typeof window.postMessage !== 'function') {
|
|
2293
|
+
issues.push('postMessage API not available');
|
|
2294
|
+
recommendations.push('Your browser environment must support postMessage');
|
|
2295
|
+
}
|
|
2296
|
+
if (typeof window.addEventListener !== 'function') {
|
|
2297
|
+
issues.push('addEventListener not available');
|
|
2298
|
+
}
|
|
2299
|
+
if (typeof Promise === 'undefined') {
|
|
2300
|
+
issues.push('Promise not available');
|
|
1933
2301
|
}
|
|
1934
|
-
//
|
|
1935
|
-
if (
|
|
2302
|
+
// Informational: note which transport is likely active
|
|
2303
|
+
if (issues.length === 0) {
|
|
1936
2304
|
const win = window;
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2305
|
+
const hasExtension = !!(win.wallet0xio || win.ZeroXIOWallet || win.chrome?.runtime?.id ||
|
|
2306
|
+
document.querySelector('meta[name="0xio-dapp"]') || document.querySelector('[data-0xio-sdk-bridge]'));
|
|
2307
|
+
const hasParentBridge = window.parent !== window;
|
|
2308
|
+
if (!hasExtension && !hasParentBridge) {
|
|
2309
|
+
recommendations.push('No 0xio transport detected yet. Install the 0xio Wallet browser extension, ' +
|
|
2310
|
+
'or run inside the 0xio Desktop/Mobile app iframe bridge.');
|
|
1940
2311
|
}
|
|
1941
2312
|
}
|
|
1942
2313
|
return {
|
|
@@ -1992,5 +2363,5 @@ if (typeof window !== 'undefined') {
|
|
|
1992
2363
|
}
|
|
1993
2364
|
}
|
|
1994
2365
|
|
|
1995
|
-
export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, SDK_CONFIG, SDK_VERSION, SUPPORTED_EXTENSION_VERSIONS, ZeroXIOWallet, ZeroXIOWalletError, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger,
|
|
2366
|
+
export { DEFAULT_NETWORK_ID, ErrorCode, EventEmitter, ExtensionCommunicator, MIN_EXTENSION_VERSION, MIN_EXTENSION_VERSION_DEVNET, NETWORKS, OctraProviderAdapter, SDK_CONFIG, SDK_VERSION, SUPPORTED_EXTENSION_VERSIONS, ZeroXIOAdapter, ZeroXIOWallet, ZeroXIOWalletError, checkBrowserSupport, checkSDKCompatibility, createDefaultBalance, createErrorMessage, createLogger, createOctraProviderAdapter, createZeroXIOAdapter, createZeroXIOWallet, delay, deriveOctraAddress, detectWalletAdapter, formatAddress, formatOCT, formatTimestamp, formatTxHash, formatOCT as formatZeroXIO, fromMicroOCT, fromMicroOCT as fromMicroZeroXIO, generateMockData, getAllAdapters, getAllNetworks, getDefaultNetwork, getNetworkConfig, isBrowser, isErrorType, isValidAddress, isValidAmount, isValidFeeLevel, isValidMessage, isValidNetworkId, toMicroOCT, toMicroOCT as toMicroZeroXIO };
|
|
1996
2367
|
//# sourceMappingURL=index.esm.js.map
|