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