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