@0xio/sdk 2.1.3
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 +238 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/index.d.ts +698 -0
- package/dist/index.esm.js +1705 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1745 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +1751 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,1751 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.OctraWalletSDK = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 0xio Wallet SDK - Event System
|
|
9
|
+
* Type-safe event emitter for wallet events
|
|
10
|
+
*/
|
|
11
|
+
class EventEmitter {
|
|
12
|
+
constructor(debug = false) {
|
|
13
|
+
this.listeners = new Map();
|
|
14
|
+
this.debug = debug;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Add event listener
|
|
18
|
+
*/
|
|
19
|
+
on(eventType, listener) {
|
|
20
|
+
if (!this.listeners.has(eventType)) {
|
|
21
|
+
this.listeners.set(eventType, new Set());
|
|
22
|
+
}
|
|
23
|
+
this.listeners.get(eventType).add(listener);
|
|
24
|
+
if (this.debug) ;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Remove event listener
|
|
28
|
+
*/
|
|
29
|
+
off(eventType, listener) {
|
|
30
|
+
const eventListeners = this.listeners.get(eventType);
|
|
31
|
+
if (eventListeners) {
|
|
32
|
+
eventListeners.delete(listener);
|
|
33
|
+
if (eventListeners.size === 0) {
|
|
34
|
+
this.listeners.delete(eventType);
|
|
35
|
+
}
|
|
36
|
+
if (this.debug) ;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Add one-time event listener
|
|
41
|
+
*/
|
|
42
|
+
once(eventType, listener) {
|
|
43
|
+
const onceListener = (event) => {
|
|
44
|
+
listener(event);
|
|
45
|
+
this.off(eventType, onceListener);
|
|
46
|
+
};
|
|
47
|
+
this.on(eventType, onceListener);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Emit event to all listeners
|
|
51
|
+
*/
|
|
52
|
+
emit(eventType, data) {
|
|
53
|
+
const event = {
|
|
54
|
+
type: eventType,
|
|
55
|
+
data,
|
|
56
|
+
timestamp: Date.now()
|
|
57
|
+
};
|
|
58
|
+
const eventListeners = this.listeners.get(eventType);
|
|
59
|
+
if (eventListeners && eventListeners.size > 0) {
|
|
60
|
+
if (this.debug) ;
|
|
61
|
+
// Create a copy to avoid issues if listeners modify the set during iteration
|
|
62
|
+
const listenersArray = Array.from(eventListeners);
|
|
63
|
+
for (const listener of listenersArray) {
|
|
64
|
+
try {
|
|
65
|
+
listener(event);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// console.error(`[0xio SDK] Error in event listener for '${eventType}':`, error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (this.debug) ;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Remove all listeners for a specific event type
|
|
76
|
+
*/
|
|
77
|
+
removeAllListeners(eventType) {
|
|
78
|
+
if (eventType) {
|
|
79
|
+
this.listeners.delete(eventType);
|
|
80
|
+
if (this.debug) ;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.listeners.clear();
|
|
84
|
+
if (this.debug) ;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get number of listeners for an event type
|
|
89
|
+
*/
|
|
90
|
+
listenerCount(eventType) {
|
|
91
|
+
const eventListeners = this.listeners.get(eventType);
|
|
92
|
+
return eventListeners ? eventListeners.size : 0;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get all event types that have listeners
|
|
96
|
+
*/
|
|
97
|
+
eventTypes() {
|
|
98
|
+
return Array.from(this.listeners.keys());
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if there are any listeners for an event type
|
|
102
|
+
*/
|
|
103
|
+
hasListeners(eventType) {
|
|
104
|
+
return this.listenerCount(eventType) > 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 0xio Wallet SDK - Type Definitions
|
|
110
|
+
* Comprehensive type system for 0xio wallet integration with Octra Network
|
|
111
|
+
*/
|
|
112
|
+
// Error types
|
|
113
|
+
exports.ErrorCode = void 0;
|
|
114
|
+
(function (ErrorCode) {
|
|
115
|
+
ErrorCode["EXTENSION_NOT_FOUND"] = "EXTENSION_NOT_FOUND";
|
|
116
|
+
ErrorCode["CONNECTION_REFUSED"] = "CONNECTION_REFUSED";
|
|
117
|
+
ErrorCode["USER_REJECTED"] = "USER_REJECTED";
|
|
118
|
+
ErrorCode["INSUFFICIENT_BALANCE"] = "INSUFFICIENT_BALANCE";
|
|
119
|
+
ErrorCode["INVALID_ADDRESS"] = "INVALID_ADDRESS";
|
|
120
|
+
ErrorCode["INVALID_AMOUNT"] = "INVALID_AMOUNT";
|
|
121
|
+
ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
122
|
+
ErrorCode["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
|
|
123
|
+
ErrorCode["PERMISSION_DENIED"] = "PERMISSION_DENIED";
|
|
124
|
+
ErrorCode["WALLET_LOCKED"] = "WALLET_LOCKED";
|
|
125
|
+
ErrorCode["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
|
|
126
|
+
ErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
127
|
+
})(exports.ErrorCode || (exports.ErrorCode = {}));
|
|
128
|
+
class ZeroXIOWalletError extends Error {
|
|
129
|
+
constructor(code, message, details) {
|
|
130
|
+
super(message);
|
|
131
|
+
this.code = code;
|
|
132
|
+
this.details = details;
|
|
133
|
+
this.name = 'ZeroXIOWalletError';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 0xio Wallet SDK - Utilities
|
|
139
|
+
* Helper functions for validation, formatting, and common operations
|
|
140
|
+
*/
|
|
141
|
+
// ===================
|
|
142
|
+
// VALIDATION UTILITIES
|
|
143
|
+
// ===================
|
|
144
|
+
/**
|
|
145
|
+
* Validate wallet address for Octra blockchain
|
|
146
|
+
*/
|
|
147
|
+
function isValidAddress(address) {
|
|
148
|
+
if (!address || typeof address !== 'string') {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
// Octra addresses should match the expected format
|
|
152
|
+
// This is a basic validation - adjust based on actual Octra address format
|
|
153
|
+
const addressRegex = /^[A-Za-z0-9]{20,64}$/;
|
|
154
|
+
return addressRegex.test(address);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Validate transaction amount
|
|
158
|
+
*/
|
|
159
|
+
function isValidAmount(amount) {
|
|
160
|
+
return typeof amount === 'number' &&
|
|
161
|
+
amount > 0 &&
|
|
162
|
+
Number.isFinite(amount) &&
|
|
163
|
+
amount <= Number.MAX_SAFE_INTEGER;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Validate network ID
|
|
167
|
+
*/
|
|
168
|
+
function isValidNetworkId(networkId) {
|
|
169
|
+
if (!networkId || typeof networkId !== 'string') {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const validNetworks = ['mainnet', 'testnet', 'devnet'];
|
|
173
|
+
return validNetworks.includes(networkId.toLowerCase());
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Validate transaction message
|
|
177
|
+
*/
|
|
178
|
+
function isValidMessage(message) {
|
|
179
|
+
if (!message) {
|
|
180
|
+
return true; // Empty messages are valid
|
|
181
|
+
}
|
|
182
|
+
if (typeof message !== 'string') {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
// Check length (adjust based on network limits)
|
|
186
|
+
return message.length <= 280;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Validate fee level
|
|
190
|
+
*/
|
|
191
|
+
function isValidFeeLevel(feeLevel) {
|
|
192
|
+
return feeLevel === 1 || feeLevel === 3;
|
|
193
|
+
}
|
|
194
|
+
// ===================
|
|
195
|
+
// FORMATTING UTILITIES
|
|
196
|
+
// ===================
|
|
197
|
+
/**
|
|
198
|
+
* Format OCT amount for display
|
|
199
|
+
*/
|
|
200
|
+
function formatOCT(amount, decimals = 6) {
|
|
201
|
+
if (!isValidAmount(amount)) {
|
|
202
|
+
return '0';
|
|
203
|
+
}
|
|
204
|
+
return amount.toLocaleString(undefined, {
|
|
205
|
+
minimumFractionDigits: 0,
|
|
206
|
+
maximumFractionDigits: decimals
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Format address for display (truncated)
|
|
211
|
+
*/
|
|
212
|
+
function formatAddress(address, prefixLength = 6, suffixLength = 4) {
|
|
213
|
+
if (!isValidAddress(address)) {
|
|
214
|
+
return 'Invalid Address';
|
|
215
|
+
}
|
|
216
|
+
if (address.length <= prefixLength + suffixLength) {
|
|
217
|
+
return address;
|
|
218
|
+
}
|
|
219
|
+
return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Format timestamp for display
|
|
223
|
+
*/
|
|
224
|
+
function formatTimestamp(timestamp) {
|
|
225
|
+
const date = new Date(timestamp);
|
|
226
|
+
return date.toLocaleString();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Format transaction hash for display
|
|
230
|
+
*/
|
|
231
|
+
function formatTxHash(hash, length = 12) {
|
|
232
|
+
if (!hash || typeof hash !== 'string') {
|
|
233
|
+
return 'Invalid Hash';
|
|
234
|
+
}
|
|
235
|
+
if (hash.length <= length) {
|
|
236
|
+
return hash;
|
|
237
|
+
}
|
|
238
|
+
const prefixLength = Math.ceil(length / 2);
|
|
239
|
+
const suffixLength = Math.floor(length / 2);
|
|
240
|
+
return `${hash.slice(0, prefixLength)}...${hash.slice(-suffixLength)}`;
|
|
241
|
+
}
|
|
242
|
+
// ===================
|
|
243
|
+
// CONVERSION UTILITIES
|
|
244
|
+
// ===================
|
|
245
|
+
/**
|
|
246
|
+
* Convert OCT to micro OCT (for network transmission)
|
|
247
|
+
*/
|
|
248
|
+
function toMicroOCT(amount) {
|
|
249
|
+
if (!isValidAmount(amount)) {
|
|
250
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Invalid amount for conversion');
|
|
251
|
+
}
|
|
252
|
+
// Assuming OCT has 6 decimal places like many cryptocurrencies
|
|
253
|
+
const microOCT = Math.round(amount * 1000000);
|
|
254
|
+
return microOCT.toString();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Convert micro OCT to OCT (for display)
|
|
258
|
+
*/
|
|
259
|
+
function fromMicroOCT(microAmount) {
|
|
260
|
+
const amount = typeof microAmount === 'string' ? parseInt(microAmount, 10) : microAmount;
|
|
261
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
262
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.INVALID_AMOUNT, 'Invalid micro OCT amount for conversion');
|
|
263
|
+
}
|
|
264
|
+
return amount / 1000000;
|
|
265
|
+
}
|
|
266
|
+
// ===================
|
|
267
|
+
// ERROR UTILITIES
|
|
268
|
+
// ===================
|
|
269
|
+
/**
|
|
270
|
+
* Create standardized error messages
|
|
271
|
+
*/
|
|
272
|
+
function createErrorMessage(code, context) {
|
|
273
|
+
const baseMessages = {
|
|
274
|
+
[exports.ErrorCode.EXTENSION_NOT_FOUND]: '0xio Wallet extension is not installed or enabled',
|
|
275
|
+
[exports.ErrorCode.CONNECTION_REFUSED]: 'Connection to wallet was refused',
|
|
276
|
+
[exports.ErrorCode.USER_REJECTED]: 'User rejected the request',
|
|
277
|
+
[exports.ErrorCode.INSUFFICIENT_BALANCE]: 'Insufficient balance for this transaction',
|
|
278
|
+
[exports.ErrorCode.INVALID_ADDRESS]: 'Invalid wallet address provided',
|
|
279
|
+
[exports.ErrorCode.INVALID_AMOUNT]: 'Invalid transaction amount',
|
|
280
|
+
[exports.ErrorCode.NETWORK_ERROR]: 'Network communication error',
|
|
281
|
+
[exports.ErrorCode.TRANSACTION_FAILED]: 'Transaction failed to process',
|
|
282
|
+
[exports.ErrorCode.PERMISSION_DENIED]: 'Permission denied for this operation',
|
|
283
|
+
[exports.ErrorCode.WALLET_LOCKED]: 'Wallet is locked, please unlock first',
|
|
284
|
+
[exports.ErrorCode.RATE_LIMIT_EXCEEDED]: 'Rate limit exceeded, please try again later',
|
|
285
|
+
[exports.ErrorCode.UNKNOWN_ERROR]: 'An unknown error occurred'
|
|
286
|
+
};
|
|
287
|
+
const baseMessage = baseMessages[code] || 'Unknown error';
|
|
288
|
+
return context ? `${baseMessage}: ${context}` : baseMessage;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Check if error is a specific type
|
|
292
|
+
*/
|
|
293
|
+
function isErrorType(error, code) {
|
|
294
|
+
return error instanceof ZeroXIOWalletError && error.code === code;
|
|
295
|
+
}
|
|
296
|
+
// ===================
|
|
297
|
+
// ASYNC UTILITIES
|
|
298
|
+
// ===================
|
|
299
|
+
/**
|
|
300
|
+
* Create a promise that resolves after a delay
|
|
301
|
+
*/
|
|
302
|
+
function delay(ms) {
|
|
303
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Retry an async operation with exponential backoff
|
|
307
|
+
*/
|
|
308
|
+
async function retry(operation, maxRetries = 3, baseDelay = 1000) {
|
|
309
|
+
let lastError;
|
|
310
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
311
|
+
try {
|
|
312
|
+
return await operation();
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
lastError = error;
|
|
316
|
+
if (attempt === maxRetries) {
|
|
317
|
+
break; // Last attempt failed
|
|
318
|
+
}
|
|
319
|
+
// Exponential backoff: 1s, 2s, 4s, etc.
|
|
320
|
+
const delayMs = baseDelay * Math.pow(2, attempt);
|
|
321
|
+
await delay(delayMs);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
throw lastError;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Timeout wrapper for promises
|
|
328
|
+
*/
|
|
329
|
+
function withTimeout(promise, timeoutMs, timeoutMessage = 'Operation timed out') {
|
|
330
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
reject(new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, timeoutMessage));
|
|
333
|
+
}, timeoutMs);
|
|
334
|
+
});
|
|
335
|
+
return Promise.race([promise, timeoutPromise]);
|
|
336
|
+
}
|
|
337
|
+
// ===================
|
|
338
|
+
// BROWSER UTILITIES
|
|
339
|
+
// ===================
|
|
340
|
+
/**
|
|
341
|
+
* Check if running in browser environment
|
|
342
|
+
*/
|
|
343
|
+
function isBrowser() {
|
|
344
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Check if browser supports required features
|
|
348
|
+
*/
|
|
349
|
+
function checkBrowserSupport() {
|
|
350
|
+
const missingFeatures = [];
|
|
351
|
+
if (!isBrowser()) {
|
|
352
|
+
missingFeatures.push('Browser environment');
|
|
353
|
+
return { supported: false, missingFeatures };
|
|
354
|
+
}
|
|
355
|
+
// Check for required browser APIs
|
|
356
|
+
if (typeof window.postMessage !== 'function') {
|
|
357
|
+
missingFeatures.push('PostMessage API');
|
|
358
|
+
}
|
|
359
|
+
if (typeof window.addEventListener !== 'function') {
|
|
360
|
+
missingFeatures.push('Event Listeners');
|
|
361
|
+
}
|
|
362
|
+
if (typeof Promise === 'undefined') {
|
|
363
|
+
missingFeatures.push('Promise support');
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
supported: missingFeatures.length === 0,
|
|
367
|
+
missingFeatures
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// ===================
|
|
371
|
+
// DEVELOPMENT UTILITIES
|
|
372
|
+
// ===================
|
|
373
|
+
/**
|
|
374
|
+
* Generate mock data for development/testing
|
|
375
|
+
*/
|
|
376
|
+
function generateMockData() {
|
|
377
|
+
return {
|
|
378
|
+
address: 'OCT' + Math.random().toString(36).substr(2, 20).toUpperCase(),
|
|
379
|
+
balance: {
|
|
380
|
+
public: Math.floor(Math.random() * 10000),
|
|
381
|
+
private: Math.floor(Math.random() * 1000),
|
|
382
|
+
total: 0,
|
|
383
|
+
currency: 'OCT'
|
|
384
|
+
},
|
|
385
|
+
networkInfo: {
|
|
386
|
+
id: 'testnet',
|
|
387
|
+
name: 'Octra Testnet',
|
|
388
|
+
rpcUrl: 'https://testnet.octra.network',
|
|
389
|
+
color: '#f59e0b',
|
|
390
|
+
isTestnet: true
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Create development logger
|
|
396
|
+
*/
|
|
397
|
+
function createLogger(prefix, debug) {
|
|
398
|
+
const isDevelopment = typeof window !== 'undefined' && (window.location.hostname === 'localhost' ||
|
|
399
|
+
window.location.hostname === '127.0.0.1' ||
|
|
400
|
+
window.location.hostname.includes('dev') ||
|
|
401
|
+
window.__OCTRA_SDK_DEBUG__);
|
|
402
|
+
// Only enable logging in development mode AND when debug is explicitly enabled
|
|
403
|
+
const shouldLog = debug && isDevelopment;
|
|
404
|
+
return {
|
|
405
|
+
log: (...args) => {
|
|
406
|
+
if (shouldLog) {
|
|
407
|
+
console.log(`[${prefix}]`, ...args);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
warn: (...args) => {
|
|
411
|
+
if (shouldLog) {
|
|
412
|
+
console.warn(`[${prefix}]`, ...args);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
error: (...args) => {
|
|
416
|
+
// Always show errors in development, even without debug flag
|
|
417
|
+
if (isDevelopment) {
|
|
418
|
+
console.error(`[${prefix}]`, ...args);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
debug: (...args) => {
|
|
422
|
+
if (shouldLog) {
|
|
423
|
+
console.debug(`[${prefix}]`, ...args);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
table: (data) => {
|
|
427
|
+
if (shouldLog) {
|
|
428
|
+
console.log(`[${prefix}] Table data:`);
|
|
429
|
+
console.table(data);
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
group: (label) => {
|
|
433
|
+
if (shouldLog) {
|
|
434
|
+
console.group(`[${prefix}] ${label}`);
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
groupEnd: () => {
|
|
438
|
+
if (shouldLog) {
|
|
439
|
+
console.groupEnd();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 0xio Wallet SDK - Extension Communication Module
|
|
447
|
+
*
|
|
448
|
+
* @fileoverview Manages secure communication between the SDK and browser extension.
|
|
449
|
+
* Implements message passing, request/response handling, rate limiting, and origin validation
|
|
450
|
+
* to ensure secure wallet interactions.
|
|
451
|
+
*
|
|
452
|
+
* @module communication
|
|
453
|
+
* @version 1.2.0
|
|
454
|
+
* @license MIT
|
|
455
|
+
*/
|
|
456
|
+
/**
|
|
457
|
+
* ExtensionCommunicator - Manages communication with the 0xio Wallet browser extension
|
|
458
|
+
*
|
|
459
|
+
* @class
|
|
460
|
+
* @extends EventEmitter
|
|
461
|
+
*
|
|
462
|
+
* @description
|
|
463
|
+
* Handles all communication between the SDK and wallet extension including:
|
|
464
|
+
* - Request/response message passing with origin validation
|
|
465
|
+
* - Rate limiting to prevent DoS attacks
|
|
466
|
+
* - Automatic retry logic with exponential backoff
|
|
467
|
+
* - Extension detection and availability monitoring
|
|
468
|
+
* - Cryptographically secure request ID generation
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```typescript
|
|
472
|
+
* const communicator = new ExtensionCommunicator(true); // debug mode
|
|
473
|
+
* await communicator.initialize();
|
|
474
|
+
*
|
|
475
|
+
* const response = await communicator.sendRequest('get_balance', {});
|
|
476
|
+
* console.log(response);
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
class ExtensionCommunicator extends EventEmitter {
|
|
480
|
+
/**
|
|
481
|
+
* Creates a new ExtensionCommunicator instance
|
|
482
|
+
*
|
|
483
|
+
* @param {boolean} debug - Enable debug logging
|
|
484
|
+
*/
|
|
485
|
+
constructor(debug = false) {
|
|
486
|
+
super(debug);
|
|
487
|
+
/** Legacy request counter (deprecated, kept for fallback) */
|
|
488
|
+
this.requestId = 0;
|
|
489
|
+
/** Map of pending requests awaiting responses */
|
|
490
|
+
this.pendingRequests = new Map();
|
|
491
|
+
/** Initialization state flag */
|
|
492
|
+
this.isInitialized = false;
|
|
493
|
+
/** Interval handle for periodic extension detection */
|
|
494
|
+
this.extensionDetectionInterval = null;
|
|
495
|
+
/** Current extension availability state */
|
|
496
|
+
this.isExtensionAvailableState = false;
|
|
497
|
+
// Rate limiting configuration
|
|
498
|
+
/** Maximum number of concurrent pending requests */
|
|
499
|
+
this.MAX_CONCURRENT_REQUESTS = 50;
|
|
500
|
+
/** Time window for rate limiting (milliseconds) */
|
|
501
|
+
this.RATE_LIMIT_WINDOW = 1000;
|
|
502
|
+
/** Maximum requests allowed per time window */
|
|
503
|
+
this.MAX_REQUESTS_PER_WINDOW = 20;
|
|
504
|
+
/** Timestamps of recent requests for rate limiting */
|
|
505
|
+
this.requestTimestamps = [];
|
|
506
|
+
this.logger = createLogger('ExtensionCommunicator', debug);
|
|
507
|
+
this.setupMessageListener();
|
|
508
|
+
this.startExtensionDetection();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Initialize communication with the wallet extension
|
|
512
|
+
*
|
|
513
|
+
* @description
|
|
514
|
+
* Performs initial setup and verification:
|
|
515
|
+
* 1. Waits for extension to become available
|
|
516
|
+
* 2. Sends ping to verify communication
|
|
517
|
+
* 3. Establishes message handlers
|
|
518
|
+
*
|
|
519
|
+
* Must be called before any other methods.
|
|
520
|
+
*
|
|
521
|
+
* @returns {Promise<boolean>} True if initialization succeeded, false otherwise
|
|
522
|
+
* @throws {ZeroXIOWalletError} If extension is not available after timeout
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```typescript
|
|
526
|
+
* const success = await communicator.initialize();
|
|
527
|
+
* if (!success) {
|
|
528
|
+
* console.error('Failed to initialize wallet connection');
|
|
529
|
+
* }
|
|
530
|
+
* ```
|
|
531
|
+
*/
|
|
532
|
+
async initialize() {
|
|
533
|
+
if (this.isInitialized) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
// Wait for extension detection with timeout
|
|
538
|
+
const available = await this.waitForExtensionAvailability(10000);
|
|
539
|
+
if (available) {
|
|
540
|
+
// Verify with ping
|
|
541
|
+
await withTimeout(this.sendRequestWithRetry('ping', {}, 3, 2000), 8000, 'Extension ping timeout during initialization');
|
|
542
|
+
this.isInitialized = true;
|
|
543
|
+
this.logger.log('Extension communication initialized successfully');
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
this.logger.error('Extension not available after waiting');
|
|
547
|
+
}
|
|
548
|
+
return this.isInitialized;
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
this.logger.error('Failed to initialize extension communication:', error);
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Check if extension is available
|
|
557
|
+
*/
|
|
558
|
+
isExtensionAvailable() {
|
|
559
|
+
return this.isExtensionAvailableState && this.hasExtensionContext();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Send request to extension
|
|
563
|
+
*/
|
|
564
|
+
async sendRequest(method, params = {}, timeout = 30000) {
|
|
565
|
+
return this.sendRequestWithRetry(method, params, 1, timeout);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Send request to extension with automatic retry logic
|
|
569
|
+
*/
|
|
570
|
+
async sendRequestWithRetry(method, params = {}, maxRetries = 3, timeout = 30000) {
|
|
571
|
+
if (!this.hasExtensionContext()) {
|
|
572
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, '0xio Wallet extension is not installed or available', {
|
|
573
|
+
method,
|
|
574
|
+
params,
|
|
575
|
+
browserContext: this.getBrowserDiagnostics()
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (!this.isExtensionAvailableState) {
|
|
579
|
+
// Wait a bit for extension to become available
|
|
580
|
+
await this.waitForExtensionAvailability(5000);
|
|
581
|
+
if (!this.isExtensionAvailableState) {
|
|
582
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Extension not available for communication', {
|
|
583
|
+
method,
|
|
584
|
+
params,
|
|
585
|
+
extensionState: this.getExtensionDiagnostics()
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// ✅ SECURITY: Check rate limits before processing
|
|
590
|
+
this.checkRateLimit();
|
|
591
|
+
return retry(async () => {
|
|
592
|
+
const requestId = this.generateRequestId();
|
|
593
|
+
const request = {
|
|
594
|
+
id: requestId,
|
|
595
|
+
method,
|
|
596
|
+
params,
|
|
597
|
+
timestamp: Date.now()
|
|
598
|
+
};
|
|
599
|
+
this.logger.log(`Sending request (${method}):`, { id: requestId, params });
|
|
600
|
+
return new Promise((resolve, reject) => {
|
|
601
|
+
// Set up timeout
|
|
602
|
+
const timeoutHandle = setTimeout(() => {
|
|
603
|
+
const pending = this.pendingRequests.get(requestId);
|
|
604
|
+
if (pending) {
|
|
605
|
+
this.pendingRequests.delete(requestId);
|
|
606
|
+
reject(new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, `Request timeout after ${timeout}ms`, {
|
|
607
|
+
method,
|
|
608
|
+
params,
|
|
609
|
+
requestId,
|
|
610
|
+
retryCount: pending.retryCount,
|
|
611
|
+
extensionState: this.getExtensionDiagnostics()
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
}, timeout);
|
|
615
|
+
// Store request handlers
|
|
616
|
+
this.pendingRequests.set(requestId, {
|
|
617
|
+
resolve,
|
|
618
|
+
reject,
|
|
619
|
+
timeout: timeoutHandle,
|
|
620
|
+
retryCount: 0
|
|
621
|
+
});
|
|
622
|
+
// Send message to extension via content script bridge
|
|
623
|
+
this.postMessageToExtension(request);
|
|
624
|
+
});
|
|
625
|
+
}, maxRetries, 1000);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Setup message listener for responses from extension
|
|
629
|
+
*/
|
|
630
|
+
setupMessageListener() {
|
|
631
|
+
if (typeof window === 'undefined') {
|
|
632
|
+
return; // Not in browser environment
|
|
633
|
+
}
|
|
634
|
+
const allowedOrigin = window.location.origin;
|
|
635
|
+
window.addEventListener('message', (event) => {
|
|
636
|
+
// ✅ SECURITY: Validate origin first - critical security check
|
|
637
|
+
if (event.origin !== allowedOrigin) {
|
|
638
|
+
this.logger.warn('Blocked message from untrusted origin:', event.origin);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
// Only accept messages from same window
|
|
642
|
+
if (event.source !== window) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// Check if it's a 0xio SDK response
|
|
646
|
+
if (!event.data || event.data.source !== '0xio-sdk-bridge') {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// Handle different types of messages
|
|
650
|
+
if (event.data.response) {
|
|
651
|
+
// Regular request/response
|
|
652
|
+
const response = event.data.response;
|
|
653
|
+
if (response && response.id) {
|
|
654
|
+
this.handleExtensionResponse(response);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
else if (event.data.event) {
|
|
658
|
+
// Event notification from extension
|
|
659
|
+
this.handleExtensionEvent(event.data.event);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
this.logger.log('Message listener setup complete');
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Handle extension event
|
|
666
|
+
*/
|
|
667
|
+
handleExtensionEvent(event) {
|
|
668
|
+
this.logger.log('Received extension event:', event.type);
|
|
669
|
+
// Forward the event to listeners
|
|
670
|
+
this.emit(event.type, event.data);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Handle response from extension
|
|
674
|
+
*/
|
|
675
|
+
handleExtensionResponse(response) {
|
|
676
|
+
this.logger.log(`Received response:`, { id: response.id, success: response.success });
|
|
677
|
+
const pending = this.pendingRequests.get(response.id);
|
|
678
|
+
if (!pending) {
|
|
679
|
+
this.logger.warn(`Received response for unknown request ID: ${response.id}`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Clear timeout and remove from pending
|
|
683
|
+
clearTimeout(pending.timeout);
|
|
684
|
+
this.pendingRequests.delete(response.id);
|
|
685
|
+
// Handle response
|
|
686
|
+
if (response.success) {
|
|
687
|
+
pending.resolve(response.data);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
const error = response.error;
|
|
691
|
+
if (error) {
|
|
692
|
+
const enhancedError = new ZeroXIOWalletError(error.code, error.message, {
|
|
693
|
+
...error.details,
|
|
694
|
+
requestId: response.id,
|
|
695
|
+
retryCount: pending.retryCount,
|
|
696
|
+
timestamp: Date.now(),
|
|
697
|
+
extensionState: this.getExtensionDiagnostics()
|
|
698
|
+
});
|
|
699
|
+
pending.reject(enhancedError);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
pending.reject(new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Unknown error occurred', {
|
|
703
|
+
requestId: response.id,
|
|
704
|
+
retryCount: pending.retryCount,
|
|
705
|
+
extensionState: this.getExtensionDiagnostics()
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Post message to extension via content script
|
|
712
|
+
*/
|
|
713
|
+
postMessageToExtension(request) {
|
|
714
|
+
// ✅ SECURITY: Use specific origin instead of wildcard
|
|
715
|
+
const targetOrigin = window.location.origin;
|
|
716
|
+
window.postMessage({
|
|
717
|
+
source: '0xio-sdk-request',
|
|
718
|
+
request
|
|
719
|
+
}, targetOrigin);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Check if we're in a context that can communicate with extension
|
|
723
|
+
*/
|
|
724
|
+
hasExtensionContext() {
|
|
725
|
+
return typeof window !== 'undefined' &&
|
|
726
|
+
typeof window.postMessage === 'function';
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Check and enforce rate limits to prevent denial-of-service attacks
|
|
730
|
+
*
|
|
731
|
+
* @private
|
|
732
|
+
* @throws {ZeroXIOWalletError} RATE_LIMIT_EXCEEDED if limits are exceeded
|
|
733
|
+
*
|
|
734
|
+
* @description
|
|
735
|
+
* Implements two-tier rate limiting:
|
|
736
|
+
* 1. Concurrent requests: Maximum 50 pending requests at once
|
|
737
|
+
* 2. Request frequency: Maximum 20 requests per second
|
|
738
|
+
*
|
|
739
|
+
* Rate limiting protects both the SDK and extension from:
|
|
740
|
+
* - Accidental infinite loops in dApp code
|
|
741
|
+
* - Malicious DoS attacks
|
|
742
|
+
* - Resource exhaustion
|
|
743
|
+
*
|
|
744
|
+
* @security Critical security function - enforces resource limits
|
|
745
|
+
*/
|
|
746
|
+
checkRateLimit() {
|
|
747
|
+
const now = Date.now();
|
|
748
|
+
// ✅ SECURITY: Check concurrent request limit
|
|
749
|
+
if (this.pendingRequests.size >= this.MAX_CONCURRENT_REQUESTS) {
|
|
750
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.RATE_LIMIT_EXCEEDED, `Too many concurrent requests (max: ${this.MAX_CONCURRENT_REQUESTS})`);
|
|
751
|
+
}
|
|
752
|
+
// ✅ SECURITY: Check requests per time window
|
|
753
|
+
this.requestTimestamps = this.requestTimestamps.filter(t => now - t < this.RATE_LIMIT_WINDOW);
|
|
754
|
+
if (this.requestTimestamps.length >= this.MAX_REQUESTS_PER_WINDOW) {
|
|
755
|
+
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)`);
|
|
756
|
+
}
|
|
757
|
+
this.requestTimestamps.push(now);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Generate cryptographically secure unique request ID
|
|
761
|
+
*
|
|
762
|
+
* @private
|
|
763
|
+
* @returns {string} A unique, unpredictable request identifier
|
|
764
|
+
*
|
|
765
|
+
* @description
|
|
766
|
+
* Uses Web Crypto API for secure random ID generation:
|
|
767
|
+
* 1. Primary: crypto.randomUUID() - UUID v4 format
|
|
768
|
+
* 2. Fallback: crypto.getRandomValues() - 128-bit random hex
|
|
769
|
+
* 3. Last resort: timestamp + counter (logs warning)
|
|
770
|
+
*
|
|
771
|
+
* Security importance:
|
|
772
|
+
* - Prevents request ID prediction attacks
|
|
773
|
+
* - Mitigates replay attacks
|
|
774
|
+
* - Makes session hijacking more difficult
|
|
775
|
+
*
|
|
776
|
+
* @security Critical - IDs must be cryptographically unpredictable
|
|
777
|
+
*/
|
|
778
|
+
generateRequestId() {
|
|
779
|
+
// ✅ SECURITY: Use crypto.randomUUID() for secure, unpredictable IDs
|
|
780
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
781
|
+
return `0xio-sdk-${crypto.randomUUID()}`;
|
|
782
|
+
}
|
|
783
|
+
// Fallback to crypto.getRandomValues for older browsers
|
|
784
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
785
|
+
const array = new Uint8Array(16);
|
|
786
|
+
crypto.getRandomValues(array);
|
|
787
|
+
const hex = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
|
788
|
+
return `0xio-sdk-${hex}`;
|
|
789
|
+
}
|
|
790
|
+
// Last resort fallback (not recommended for production)
|
|
791
|
+
this.logger.warn('Crypto API not available, using less secure ID generation');
|
|
792
|
+
return `0xio-sdk-${++this.requestId}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Start continuous extension detection
|
|
796
|
+
*/
|
|
797
|
+
startExtensionDetection() {
|
|
798
|
+
if (typeof window === 'undefined')
|
|
799
|
+
return;
|
|
800
|
+
// Listen for extension ready event (instant detection)
|
|
801
|
+
window.addEventListener('0xioWalletReady', () => {
|
|
802
|
+
this.logger.log('Received 0xioWalletReady event');
|
|
803
|
+
this.isExtensionAvailableState = true;
|
|
804
|
+
});
|
|
805
|
+
// Also listen for legacy event names
|
|
806
|
+
window.addEventListener('wallet0xioReady', () => {
|
|
807
|
+
this.logger.log('Received wallet0xioReady event');
|
|
808
|
+
this.isExtensionAvailableState = true;
|
|
809
|
+
});
|
|
810
|
+
window.addEventListener('octraWalletReady', () => {
|
|
811
|
+
this.logger.log('Received octraWalletReady event');
|
|
812
|
+
this.isExtensionAvailableState = true;
|
|
813
|
+
});
|
|
814
|
+
// Initial check (in case extension was already injected)
|
|
815
|
+
this.checkExtensionAvailability();
|
|
816
|
+
// Set up periodic checks as fallback
|
|
817
|
+
this.extensionDetectionInterval = setInterval(() => {
|
|
818
|
+
this.checkExtensionAvailability();
|
|
819
|
+
}, 2000);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Check if extension is currently available
|
|
823
|
+
*/
|
|
824
|
+
checkExtensionAvailability() {
|
|
825
|
+
const wasAvailable = this.isExtensionAvailableState;
|
|
826
|
+
// Basic checks for extension context
|
|
827
|
+
this.isExtensionAvailableState = this.hasExtensionContext() && this.detectExtensionSignals();
|
|
828
|
+
if (!wasAvailable && this.isExtensionAvailableState) {
|
|
829
|
+
this.logger.log('Extension became available');
|
|
830
|
+
}
|
|
831
|
+
else if (wasAvailable && !this.isExtensionAvailableState) {
|
|
832
|
+
this.logger.warn('Extension became unavailable');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Detect extension signals/indicators
|
|
837
|
+
*/
|
|
838
|
+
detectExtensionSignals() {
|
|
839
|
+
if (typeof window === 'undefined')
|
|
840
|
+
return false;
|
|
841
|
+
// Check for extension-injected indicators
|
|
842
|
+
const win = window;
|
|
843
|
+
// Look for common extension indicators
|
|
844
|
+
return !!(win.wallet0xio ||
|
|
845
|
+
win.ZeroXIOWallet ||
|
|
846
|
+
win.__0XIO_EXTENSION__ ||
|
|
847
|
+
win.__OCTRA_EXTENSION__ ||
|
|
848
|
+
win.octraWallet ||
|
|
849
|
+
(win.chrome?.runtime?.id) ||
|
|
850
|
+
document.querySelector('meta[name="0xio-extension"]') ||
|
|
851
|
+
document.querySelector('meta[name="octra-extension"]') ||
|
|
852
|
+
document.querySelector('[data-0xio-extension]') ||
|
|
853
|
+
document.querySelector('[data-octra-extension]'));
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Wait for extension to become available
|
|
857
|
+
*/
|
|
858
|
+
async waitForExtensionAvailability(timeoutMs) {
|
|
859
|
+
if (this.isExtensionAvailableState) {
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
return new Promise((resolve) => {
|
|
863
|
+
let resolved = false;
|
|
864
|
+
const startTime = Date.now();
|
|
865
|
+
// Cleanup function
|
|
866
|
+
const cleanup = () => {
|
|
867
|
+
clearInterval(checkInterval);
|
|
868
|
+
window.removeEventListener('0xioWalletReady', onReady);
|
|
869
|
+
window.removeEventListener('wallet0xioReady', onReady);
|
|
870
|
+
window.removeEventListener('octraWalletReady', onReady);
|
|
871
|
+
};
|
|
872
|
+
// Event handler for instant resolution
|
|
873
|
+
const onReady = () => {
|
|
874
|
+
if (resolved)
|
|
875
|
+
return;
|
|
876
|
+
resolved = true;
|
|
877
|
+
this.isExtensionAvailableState = true;
|
|
878
|
+
cleanup();
|
|
879
|
+
resolve(true);
|
|
880
|
+
};
|
|
881
|
+
// Listen for extension ready events
|
|
882
|
+
window.addEventListener('0xioWalletReady', onReady);
|
|
883
|
+
window.addEventListener('wallet0xioReady', onReady);
|
|
884
|
+
window.addEventListener('octraWalletReady', onReady);
|
|
885
|
+
// Polling fallback with shorter interval
|
|
886
|
+
const checkInterval = setInterval(() => {
|
|
887
|
+
if (resolved)
|
|
888
|
+
return;
|
|
889
|
+
if (this.isExtensionAvailableState) {
|
|
890
|
+
resolved = true;
|
|
891
|
+
cleanup();
|
|
892
|
+
resolve(true);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
896
|
+
resolved = true;
|
|
897
|
+
cleanup();
|
|
898
|
+
resolve(false);
|
|
899
|
+
}
|
|
900
|
+
}, 100);
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Get browser diagnostics for error reporting
|
|
905
|
+
*/
|
|
906
|
+
getBrowserDiagnostics() {
|
|
907
|
+
if (typeof window === 'undefined') {
|
|
908
|
+
return { environment: 'non-browser' };
|
|
909
|
+
}
|
|
910
|
+
const win = window;
|
|
911
|
+
return {
|
|
912
|
+
userAgent: navigator.userAgent,
|
|
913
|
+
hasChrome: !!win.chrome,
|
|
914
|
+
hasChromeRuntime: !!(win.chrome?.runtime),
|
|
915
|
+
hasPostMessage: typeof window.postMessage === 'function',
|
|
916
|
+
origin: window.location?.origin,
|
|
917
|
+
extensionDetection: {
|
|
918
|
+
hasOctraExtension: !!win.__OCTRA_EXTENSION__,
|
|
919
|
+
hasZeroXIOWallet: !!win.octraWallet,
|
|
920
|
+
hasChromeRuntimeId: !!(win.chrome?.runtime?.id),
|
|
921
|
+
hasMetaTag: !!document.querySelector('meta[name=\"octra-extension\"]'),
|
|
922
|
+
hasDataAttribute: !!document.querySelector('[data-octra-extension]')
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Get extension state diagnostics
|
|
928
|
+
*/
|
|
929
|
+
getExtensionDiagnostics() {
|
|
930
|
+
return {
|
|
931
|
+
initialized: this.isInitialized,
|
|
932
|
+
available: this.isExtensionAvailableState,
|
|
933
|
+
pendingRequests: this.pendingRequests.size,
|
|
934
|
+
hasExtensionContext: this.hasExtensionContext(),
|
|
935
|
+
browserDiagnostics: this.getBrowserDiagnostics()
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Cleanup pending requests
|
|
940
|
+
*/
|
|
941
|
+
cleanup() {
|
|
942
|
+
if (this.extensionDetectionInterval) {
|
|
943
|
+
clearInterval(this.extensionDetectionInterval);
|
|
944
|
+
this.extensionDetectionInterval = null;
|
|
945
|
+
}
|
|
946
|
+
for (const [, pending] of this.pendingRequests) {
|
|
947
|
+
clearTimeout(pending.timeout);
|
|
948
|
+
pending.reject(new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'SDK cleanup called'));
|
|
949
|
+
}
|
|
950
|
+
this.pendingRequests.clear();
|
|
951
|
+
this.isInitialized = false;
|
|
952
|
+
this.isExtensionAvailableState = false;
|
|
953
|
+
// Call parent cleanup
|
|
954
|
+
this.removeAllListeners();
|
|
955
|
+
this.logger.log('Communication cleanup complete');
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Get debug information
|
|
959
|
+
*/
|
|
960
|
+
getDebugInfo() {
|
|
961
|
+
return {
|
|
962
|
+
initialized: this.isInitialized,
|
|
963
|
+
available: this.isExtensionAvailableState,
|
|
964
|
+
pendingRequests: this.pendingRequests.size,
|
|
965
|
+
hasExtensionContext: this.hasExtensionContext(),
|
|
966
|
+
extensionDiagnostics: this.getExtensionDiagnostics()
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Network configuration for 0xio SDK
|
|
973
|
+
*/
|
|
974
|
+
const NETWORKS = {
|
|
975
|
+
'mainnet': {
|
|
976
|
+
id: 'mainnet',
|
|
977
|
+
name: 'Octra Mainnet Alpha',
|
|
978
|
+
rpcUrl: 'https://octra.network',
|
|
979
|
+
explorerUrl: 'https://octrascan.io/',
|
|
980
|
+
color: '#6366f1',
|
|
981
|
+
isTestnet: false
|
|
982
|
+
},
|
|
983
|
+
'custom': {
|
|
984
|
+
id: 'custom',
|
|
985
|
+
name: 'Custom Network',
|
|
986
|
+
rpcUrl: '',
|
|
987
|
+
explorerUrl: '',
|
|
988
|
+
color: '#64748b',
|
|
989
|
+
isTestnet: false // User configurable - can be mainnet or testnet
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const DEFAULT_NETWORK_ID = 'mainnet';
|
|
993
|
+
/**
|
|
994
|
+
* Get network configuration by ID
|
|
995
|
+
*/
|
|
996
|
+
function getNetworkConfig(networkId = DEFAULT_NETWORK_ID) {
|
|
997
|
+
const network = NETWORKS[networkId];
|
|
998
|
+
if (!network) {
|
|
999
|
+
throw new Error(`Unknown network ID: ${networkId}`);
|
|
1000
|
+
}
|
|
1001
|
+
return network;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Get all available networks
|
|
1005
|
+
*/
|
|
1006
|
+
function getAllNetworks() {
|
|
1007
|
+
return Object.values(NETWORKS);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* SDK Configuration
|
|
1012
|
+
*/
|
|
1013
|
+
/**
|
|
1014
|
+
* Default balance structure
|
|
1015
|
+
*/
|
|
1016
|
+
function createDefaultBalance(total = 0) {
|
|
1017
|
+
return {
|
|
1018
|
+
total,
|
|
1019
|
+
public: total,
|
|
1020
|
+
private: 0,
|
|
1021
|
+
currency: 'OCT'
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* SDK Configuration constants
|
|
1026
|
+
*/
|
|
1027
|
+
const SDK_CONFIG = {
|
|
1028
|
+
version: '2.1.2',
|
|
1029
|
+
defaultNetworkId: DEFAULT_NETWORK_ID,
|
|
1030
|
+
communicationTimeout: 30000, // 30 seconds
|
|
1031
|
+
retryAttempts: 3,
|
|
1032
|
+
retryDelay: 1000, // 1 second
|
|
1033
|
+
};
|
|
1034
|
+
/**
|
|
1035
|
+
* Get default network configuration
|
|
1036
|
+
*/
|
|
1037
|
+
function getDefaultNetwork() {
|
|
1038
|
+
return getNetworkConfig(SDK_CONFIG.defaultNetworkId);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* 0xio Wallet SDK - Main Wallet Class
|
|
1043
|
+
* Primary interface for DApp developers to interact with 0xio Wallet
|
|
1044
|
+
*/
|
|
1045
|
+
class ZeroXIOWallet extends EventEmitter {
|
|
1046
|
+
constructor(config) {
|
|
1047
|
+
super(config.debug);
|
|
1048
|
+
this.connectionInfo = { isConnected: false };
|
|
1049
|
+
this.isInitialized = false;
|
|
1050
|
+
this.config = {
|
|
1051
|
+
...config,
|
|
1052
|
+
appVersion: config.appVersion || '1.0.0',
|
|
1053
|
+
requiredPermissions: config.requiredPermissions || ['read_balance'],
|
|
1054
|
+
debug: config.debug || false
|
|
1055
|
+
};
|
|
1056
|
+
this.logger = createLogger('ZeroXIOWallet', this.config.debug || false);
|
|
1057
|
+
this.communicator = new ExtensionCommunicator(this.config.debug);
|
|
1058
|
+
this.logger.log('Wallet instance created with config:', this.config);
|
|
1059
|
+
}
|
|
1060
|
+
// ===================
|
|
1061
|
+
// INITIALIZATION
|
|
1062
|
+
// ===================
|
|
1063
|
+
/**
|
|
1064
|
+
* Initialize the SDK
|
|
1065
|
+
* Must be called before using any other methods
|
|
1066
|
+
*/
|
|
1067
|
+
async initialize() {
|
|
1068
|
+
if (this.isInitialized) {
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
// Initialize extension communication
|
|
1073
|
+
const communicationReady = await this.communicator.initialize();
|
|
1074
|
+
if (!communicationReady) {
|
|
1075
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.EXTENSION_NOT_FOUND, 'Failed to establish communication with 0xio Wallet extension');
|
|
1076
|
+
}
|
|
1077
|
+
// Register this DApp with the extension
|
|
1078
|
+
await this.communicator.sendRequest('register_dapp', {
|
|
1079
|
+
appName: this.config.appName,
|
|
1080
|
+
appDescription: this.config.appDescription,
|
|
1081
|
+
appVersion: this.config.appVersion,
|
|
1082
|
+
appUrl: typeof window !== 'undefined' ? window.location.origin : undefined,
|
|
1083
|
+
appIcon: this.config.appIcon,
|
|
1084
|
+
requiredPermissions: this.config.requiredPermissions,
|
|
1085
|
+
networkId: this.config.networkId
|
|
1086
|
+
});
|
|
1087
|
+
// Setup event forwarding from extension
|
|
1088
|
+
this.setupExtensionEventListeners();
|
|
1089
|
+
this.isInitialized = true;
|
|
1090
|
+
this.logger.log('SDK initialized successfully');
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
this.logger.error('Failed to initialize:', error);
|
|
1095
|
+
if (error instanceof ZeroXIOWalletError) {
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'Failed to initialize SDK', error);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Check if SDK is initialized
|
|
1103
|
+
*/
|
|
1104
|
+
isReady() {
|
|
1105
|
+
return this.isInitialized && this.communicator.isExtensionAvailable();
|
|
1106
|
+
}
|
|
1107
|
+
// ===================
|
|
1108
|
+
// CONNECTION MANAGEMENT
|
|
1109
|
+
// ===================
|
|
1110
|
+
/**
|
|
1111
|
+
* Connect to wallet
|
|
1112
|
+
*/
|
|
1113
|
+
async connect(options = {}) {
|
|
1114
|
+
this.ensureInitialized();
|
|
1115
|
+
try {
|
|
1116
|
+
this.logger.log('Attempting to connect with options:', options);
|
|
1117
|
+
const result = await this.communicator.sendRequest('connect', {
|
|
1118
|
+
requestPermissions: options.requestPermissions || this.config.requiredPermissions,
|
|
1119
|
+
networkId: options.networkId || this.config.networkId
|
|
1120
|
+
});
|
|
1121
|
+
// Update connection info
|
|
1122
|
+
this.connectionInfo = {
|
|
1123
|
+
isConnected: true,
|
|
1124
|
+
address: result.address,
|
|
1125
|
+
balance: result.balance,
|
|
1126
|
+
networkInfo: result.networkInfo,
|
|
1127
|
+
connectedAt: Date.now()
|
|
1128
|
+
};
|
|
1129
|
+
const connectEvent = {
|
|
1130
|
+
address: result.address,
|
|
1131
|
+
balance: result.balance,
|
|
1132
|
+
networkInfo: result.networkInfo,
|
|
1133
|
+
permissions: result.permissions
|
|
1134
|
+
};
|
|
1135
|
+
// Emit connect event
|
|
1136
|
+
this.emit('connect', connectEvent);
|
|
1137
|
+
this.logger.log('Connected successfully:', connectEvent);
|
|
1138
|
+
return connectEvent;
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
this.logger.error('Connection failed:', error);
|
|
1142
|
+
if (error instanceof ZeroXIOWalletError) {
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.CONNECTION_REFUSED, 'Failed to connect to wallet', error);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Disconnect from wallet
|
|
1150
|
+
*/
|
|
1151
|
+
async disconnect() {
|
|
1152
|
+
this.ensureInitialized();
|
|
1153
|
+
try {
|
|
1154
|
+
await this.communicator.sendRequest('disconnect');
|
|
1155
|
+
this.connectionInfo = { isConnected: false };
|
|
1156
|
+
const disconnectEvent = {
|
|
1157
|
+
reason: 'user_action'
|
|
1158
|
+
};
|
|
1159
|
+
this.emit('disconnect', disconnectEvent);
|
|
1160
|
+
this.logger.log('Disconnected from wallet');
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
this.logger.error('Disconnect failed:', error);
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Check if connected to wallet
|
|
1169
|
+
*/
|
|
1170
|
+
isConnected() {
|
|
1171
|
+
return this.connectionInfo.isConnected;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Get current connection info
|
|
1175
|
+
*/
|
|
1176
|
+
getConnectionInfo() {
|
|
1177
|
+
return { ...this.connectionInfo };
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Check connection status with extension
|
|
1181
|
+
*/
|
|
1182
|
+
async getConnectionStatus() {
|
|
1183
|
+
this.ensureInitialized();
|
|
1184
|
+
try {
|
|
1185
|
+
const result = await this.communicator.sendRequest('getConnectionStatus');
|
|
1186
|
+
if (result.isConnected && result.address) {
|
|
1187
|
+
// Update internal state if we discover an existing connection
|
|
1188
|
+
const balanceInfo = createDefaultBalance(result.balance);
|
|
1189
|
+
const networkInfo = getNetworkConfig(this.config.networkId);
|
|
1190
|
+
this.connectionInfo = {
|
|
1191
|
+
isConnected: true,
|
|
1192
|
+
address: result.address,
|
|
1193
|
+
balance: balanceInfo,
|
|
1194
|
+
networkInfo,
|
|
1195
|
+
connectedAt: result.connectedAt || Date.now()
|
|
1196
|
+
};
|
|
1197
|
+
this.logger.log('Discovered existing connection:', this.connectionInfo);
|
|
1198
|
+
// Emit connect event to notify the wrapper
|
|
1199
|
+
const connectEvent = {
|
|
1200
|
+
address: result.address,
|
|
1201
|
+
balance: balanceInfo,
|
|
1202
|
+
networkInfo,
|
|
1203
|
+
permissions: result.permissions || []
|
|
1204
|
+
};
|
|
1205
|
+
this.emit('connect', connectEvent);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
// No existing connection
|
|
1209
|
+
this.connectionInfo = { isConnected: false };
|
|
1210
|
+
}
|
|
1211
|
+
return { ...this.connectionInfo };
|
|
1212
|
+
}
|
|
1213
|
+
catch (error) {
|
|
1214
|
+
this.logger.error('Failed to get connection status:', error);
|
|
1215
|
+
this.connectionInfo = { isConnected: false };
|
|
1216
|
+
return { ...this.connectionInfo };
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
// ===================
|
|
1220
|
+
// WALLET INFORMATION
|
|
1221
|
+
// ===================
|
|
1222
|
+
/**
|
|
1223
|
+
* Get current wallet address
|
|
1224
|
+
*/
|
|
1225
|
+
getAddress() {
|
|
1226
|
+
return this.connectionInfo.address || null;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Get current balance
|
|
1230
|
+
*/
|
|
1231
|
+
async getBalance(forceRefresh = false) {
|
|
1232
|
+
this.ensureConnected();
|
|
1233
|
+
try {
|
|
1234
|
+
const address = this.getAddress();
|
|
1235
|
+
if (!address) {
|
|
1236
|
+
throw new Error('No address found');
|
|
1237
|
+
}
|
|
1238
|
+
// Fetch balance from extension (bypasses CORS, has access to private balance)
|
|
1239
|
+
let publicBalance = 0;
|
|
1240
|
+
let privateBalance = 0;
|
|
1241
|
+
const extResult = await this.communicator.sendRequest('getBalance', { forceRefresh });
|
|
1242
|
+
publicBalance = parseFloat(extResult.balance || '0');
|
|
1243
|
+
privateBalance = parseFloat(extResult.privateBalance || '0');
|
|
1244
|
+
this.logger.log('Balance fetched from extension:', { public: publicBalance, private: privateBalance });
|
|
1245
|
+
const result = {
|
|
1246
|
+
public: publicBalance,
|
|
1247
|
+
private: privateBalance,
|
|
1248
|
+
total: publicBalance + privateBalance,
|
|
1249
|
+
currency: 'OCT'
|
|
1250
|
+
};
|
|
1251
|
+
// Update cached balance
|
|
1252
|
+
if (this.connectionInfo.balance) {
|
|
1253
|
+
const previousBalance = this.connectionInfo.balance;
|
|
1254
|
+
this.connectionInfo.balance = result;
|
|
1255
|
+
// Emit balance changed event if different
|
|
1256
|
+
if (previousBalance.total !== result.total) {
|
|
1257
|
+
const balanceChangedEvent = {
|
|
1258
|
+
address: this.connectionInfo.address,
|
|
1259
|
+
previousBalance,
|
|
1260
|
+
newBalance: result
|
|
1261
|
+
};
|
|
1262
|
+
this.emit('balanceChanged', balanceChangedEvent);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
this.connectionInfo.balance = result;
|
|
1267
|
+
}
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
catch (error) {
|
|
1271
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get balance', error);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Get network information
|
|
1276
|
+
*/
|
|
1277
|
+
async getNetworkInfo() {
|
|
1278
|
+
this.ensureInitialized();
|
|
1279
|
+
try {
|
|
1280
|
+
const result = await this.communicator.sendRequest('get_network_info');
|
|
1281
|
+
// Update cached network info
|
|
1282
|
+
if (this.connectionInfo.networkInfo) {
|
|
1283
|
+
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1284
|
+
this.connectionInfo.networkInfo = result;
|
|
1285
|
+
// Emit network changed event if different
|
|
1286
|
+
if (previousNetwork.id !== result.id) {
|
|
1287
|
+
const networkChangedEvent = {
|
|
1288
|
+
previousNetwork,
|
|
1289
|
+
newNetwork: result
|
|
1290
|
+
};
|
|
1291
|
+
this.emit('networkChanged', networkChangedEvent);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
this.connectionInfo.networkInfo = result;
|
|
1296
|
+
}
|
|
1297
|
+
return result;
|
|
1298
|
+
}
|
|
1299
|
+
catch (error) {
|
|
1300
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get network info', error);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
// ===================
|
|
1304
|
+
// TRANSACTIONS
|
|
1305
|
+
// ===================
|
|
1306
|
+
/**
|
|
1307
|
+
* Send transaction
|
|
1308
|
+
*/
|
|
1309
|
+
async sendTransaction(txData) {
|
|
1310
|
+
this.ensureConnected();
|
|
1311
|
+
try {
|
|
1312
|
+
this.logger.log('Sending transaction:', txData);
|
|
1313
|
+
const result = await this.communicator.sendRequest('send_transaction', txData);
|
|
1314
|
+
this.logger.log('Transaction result:', result);
|
|
1315
|
+
// Refresh balance after successful transaction
|
|
1316
|
+
if (result.success) {
|
|
1317
|
+
setTimeout(() => {
|
|
1318
|
+
this.getBalance(true).catch(error => {
|
|
1319
|
+
this.logger.warn('Failed to refresh balance after transaction:', error);
|
|
1320
|
+
});
|
|
1321
|
+
}, 1000);
|
|
1322
|
+
}
|
|
1323
|
+
return result;
|
|
1324
|
+
}
|
|
1325
|
+
catch (error) {
|
|
1326
|
+
this.logger.error('Transaction failed:', error);
|
|
1327
|
+
if (error instanceof ZeroXIOWalletError) {
|
|
1328
|
+
throw error;
|
|
1329
|
+
}
|
|
1330
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to send transaction', error);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Get transaction history
|
|
1335
|
+
*/
|
|
1336
|
+
async getTransactionHistory(page = 1, limit = 20) {
|
|
1337
|
+
this.ensureConnected();
|
|
1338
|
+
try {
|
|
1339
|
+
const result = await this.communicator.sendRequest('get_transaction_history', {
|
|
1340
|
+
page,
|
|
1341
|
+
limit
|
|
1342
|
+
});
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
catch (error) {
|
|
1346
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get transaction history', error);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
// ===================
|
|
1350
|
+
// PRIVATE FEATURES
|
|
1351
|
+
// ===================
|
|
1352
|
+
/**
|
|
1353
|
+
* Get private balance information
|
|
1354
|
+
*/
|
|
1355
|
+
async getPrivateBalanceInfo() {
|
|
1356
|
+
this.ensureConnected();
|
|
1357
|
+
try {
|
|
1358
|
+
const result = await this.communicator.sendRequest('get_private_balance_info');
|
|
1359
|
+
return result;
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.PERMISSION_DENIED, 'Failed to get private balance info', error);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Encrypt public balance to private
|
|
1367
|
+
*/
|
|
1368
|
+
async encryptBalance(amount) {
|
|
1369
|
+
this.ensureConnected();
|
|
1370
|
+
try {
|
|
1371
|
+
const result = await this.communicator.sendRequest('encrypt_balance', { amount });
|
|
1372
|
+
// Refresh balance after encryption
|
|
1373
|
+
setTimeout(() => {
|
|
1374
|
+
this.getBalance(true).catch(() => { });
|
|
1375
|
+
}, 1000);
|
|
1376
|
+
return result.success;
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to encrypt balance', error);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Decrypt private balance to public
|
|
1384
|
+
*/
|
|
1385
|
+
async decryptBalance(amount) {
|
|
1386
|
+
this.ensureConnected();
|
|
1387
|
+
try {
|
|
1388
|
+
const result = await this.communicator.sendRequest('decrypt_balance', { amount });
|
|
1389
|
+
// Refresh balance after decryption
|
|
1390
|
+
setTimeout(() => {
|
|
1391
|
+
this.getBalance(true).catch(() => { });
|
|
1392
|
+
}, 1000);
|
|
1393
|
+
return result.success;
|
|
1394
|
+
}
|
|
1395
|
+
catch (error) {
|
|
1396
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to decrypt balance', error);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Send private transfer
|
|
1401
|
+
*/
|
|
1402
|
+
async sendPrivateTransfer(transferData) {
|
|
1403
|
+
this.ensureConnected();
|
|
1404
|
+
try {
|
|
1405
|
+
const result = await this.communicator.sendRequest('send_private_transfer', transferData);
|
|
1406
|
+
// Refresh balance after transfer
|
|
1407
|
+
if (result.success) {
|
|
1408
|
+
setTimeout(() => {
|
|
1409
|
+
this.getBalance(true).catch(() => { });
|
|
1410
|
+
}, 1000);
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to send private transfer', error);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Get pending private transfers
|
|
1420
|
+
*/
|
|
1421
|
+
async getPendingPrivateTransfers() {
|
|
1422
|
+
this.ensureConnected();
|
|
1423
|
+
try {
|
|
1424
|
+
const result = await this.communicator.sendRequest('get_pending_private_transfers');
|
|
1425
|
+
return result;
|
|
1426
|
+
}
|
|
1427
|
+
catch (error) {
|
|
1428
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.NETWORK_ERROR, 'Failed to get pending private transfers', error);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Claim private transfer
|
|
1433
|
+
*/
|
|
1434
|
+
async claimPrivateTransfer(transferId) {
|
|
1435
|
+
this.ensureConnected();
|
|
1436
|
+
try {
|
|
1437
|
+
const result = await this.communicator.sendRequest('claim_private_transfer', {
|
|
1438
|
+
transferId
|
|
1439
|
+
});
|
|
1440
|
+
// Refresh balance after claiming
|
|
1441
|
+
if (result.success) {
|
|
1442
|
+
setTimeout(() => {
|
|
1443
|
+
this.getBalance(true).catch(() => { });
|
|
1444
|
+
}, 1000);
|
|
1445
|
+
}
|
|
1446
|
+
return result;
|
|
1447
|
+
}
|
|
1448
|
+
catch (error) {
|
|
1449
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.TRANSACTION_FAILED, 'Failed to claim private transfer', error);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
// ===================
|
|
1453
|
+
// PRIVATE METHODS
|
|
1454
|
+
// ===================
|
|
1455
|
+
ensureInitialized() {
|
|
1456
|
+
if (!this.isInitialized) {
|
|
1457
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.UNKNOWN_ERROR, 'SDK not initialized. Call initialize() first.');
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
ensureConnected() {
|
|
1461
|
+
this.ensureInitialized();
|
|
1462
|
+
if (!this.connectionInfo.isConnected) {
|
|
1463
|
+
throw new ZeroXIOWalletError(exports.ErrorCode.CONNECTION_REFUSED, 'Wallet not connected. Call connect() first.');
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
setupExtensionEventListeners() {
|
|
1467
|
+
// Listen for extension events through the communicator
|
|
1468
|
+
this.communicator.on('accountChanged', (event) => {
|
|
1469
|
+
this.handleAccountChanged(event.data);
|
|
1470
|
+
});
|
|
1471
|
+
this.communicator.on('networkChanged', (event) => {
|
|
1472
|
+
this.handleNetworkChanged(event.data);
|
|
1473
|
+
});
|
|
1474
|
+
this.communicator.on('balanceChanged', (event) => {
|
|
1475
|
+
this.handleBalanceChanged(event.data);
|
|
1476
|
+
});
|
|
1477
|
+
this.communicator.on('extensionLocked', () => {
|
|
1478
|
+
this.handleExtensionLocked();
|
|
1479
|
+
});
|
|
1480
|
+
this.communicator.on('extensionUnlocked', () => {
|
|
1481
|
+
this.handleExtensionUnlocked();
|
|
1482
|
+
});
|
|
1483
|
+
this.communicator.on('transactionConfirmed', (event) => {
|
|
1484
|
+
this.handleTransactionConfirmed(event.data);
|
|
1485
|
+
});
|
|
1486
|
+
this.logger.log('Extension event listeners setup complete');
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Handle account changed event from extension
|
|
1490
|
+
*/
|
|
1491
|
+
handleAccountChanged(data) {
|
|
1492
|
+
const previousAddress = this.connectionInfo.address;
|
|
1493
|
+
this.connectionInfo.address = data.address;
|
|
1494
|
+
if (data.balance) {
|
|
1495
|
+
this.connectionInfo.balance = data.balance;
|
|
1496
|
+
}
|
|
1497
|
+
const accountChangedEvent = {
|
|
1498
|
+
previousAddress,
|
|
1499
|
+
newAddress: data.address,
|
|
1500
|
+
balance: data.balance
|
|
1501
|
+
};
|
|
1502
|
+
this.emit('accountChanged', accountChangedEvent);
|
|
1503
|
+
this.logger.log('Account changed:', accountChangedEvent);
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Handle network changed event from extension
|
|
1507
|
+
*/
|
|
1508
|
+
handleNetworkChanged(data) {
|
|
1509
|
+
const previousNetwork = this.connectionInfo.networkInfo;
|
|
1510
|
+
this.connectionInfo.networkInfo = data.networkInfo;
|
|
1511
|
+
const networkChangedEvent = {
|
|
1512
|
+
previousNetwork,
|
|
1513
|
+
newNetwork: data.networkInfo
|
|
1514
|
+
};
|
|
1515
|
+
this.emit('networkChanged', networkChangedEvent);
|
|
1516
|
+
this.logger.log('Network changed:', networkChangedEvent);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Handle balance changed event from extension
|
|
1520
|
+
*/
|
|
1521
|
+
handleBalanceChanged(data) {
|
|
1522
|
+
const previousBalance = this.connectionInfo.balance;
|
|
1523
|
+
this.connectionInfo.balance = data.balance;
|
|
1524
|
+
const balanceChangedEvent = {
|
|
1525
|
+
address: this.connectionInfo.address,
|
|
1526
|
+
previousBalance,
|
|
1527
|
+
newBalance: data.balance
|
|
1528
|
+
};
|
|
1529
|
+
this.emit('balanceChanged', balanceChangedEvent);
|
|
1530
|
+
this.logger.log('Balance changed:', balanceChangedEvent);
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Handle extension locked event
|
|
1534
|
+
*/
|
|
1535
|
+
handleExtensionLocked() {
|
|
1536
|
+
this.connectionInfo = { isConnected: false };
|
|
1537
|
+
const disconnectEvent = {
|
|
1538
|
+
reason: 'extension_locked'
|
|
1539
|
+
};
|
|
1540
|
+
this.emit('disconnect', disconnectEvent);
|
|
1541
|
+
this.logger.log('Extension locked - disconnected');
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Handle extension unlocked event
|
|
1545
|
+
*/
|
|
1546
|
+
handleExtensionUnlocked() {
|
|
1547
|
+
// Attempt to restore connection
|
|
1548
|
+
this.getConnectionStatus().catch(() => {
|
|
1549
|
+
this.logger.warn('Could not restore connection after unlock');
|
|
1550
|
+
});
|
|
1551
|
+
this.logger.log('Extension unlocked');
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Handle transaction confirmed event
|
|
1555
|
+
*/
|
|
1556
|
+
handleTransactionConfirmed(data) {
|
|
1557
|
+
this.emit('transactionConfirmed', {
|
|
1558
|
+
txHash: data.txHash,
|
|
1559
|
+
transaction: data.transaction,
|
|
1560
|
+
confirmations: data.confirmations
|
|
1561
|
+
});
|
|
1562
|
+
// Refresh balance after transaction confirmation
|
|
1563
|
+
setTimeout(() => {
|
|
1564
|
+
this.getBalance(true).catch(() => { });
|
|
1565
|
+
}, 2000);
|
|
1566
|
+
this.logger.log('Transaction confirmed:', data.txHash);
|
|
1567
|
+
}
|
|
1568
|
+
// ===================
|
|
1569
|
+
// CLEANUP
|
|
1570
|
+
// ===================
|
|
1571
|
+
/**
|
|
1572
|
+
* Clean up SDK resources
|
|
1573
|
+
*/
|
|
1574
|
+
cleanup() {
|
|
1575
|
+
this.communicator.cleanup();
|
|
1576
|
+
this.removeAllListeners();
|
|
1577
|
+
this.connectionInfo = { isConnected: false };
|
|
1578
|
+
this.isInitialized = false;
|
|
1579
|
+
this.logger.log('SDK cleanup complete');
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
var wallet = /*#__PURE__*/Object.freeze({
|
|
1584
|
+
__proto__: null,
|
|
1585
|
+
ZeroXIOWallet: ZeroXIOWallet
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* 0xio Wallet SDK - Main Entry Point
|
|
1590
|
+
* Official SDK for integrating with 0xio Wallet Extension
|
|
1591
|
+
*
|
|
1592
|
+
* @example
|
|
1593
|
+
* ```typescript
|
|
1594
|
+
* import { ZeroXIOWallet } from '@0xgery/wallet-sdk';
|
|
1595
|
+
*
|
|
1596
|
+
* const wallet = new ZeroXIOWallet({
|
|
1597
|
+
* appName: 'My DApp',
|
|
1598
|
+
* appDescription: 'An awesome decentralized application',
|
|
1599
|
+
* requiredPermissions: ['read_balance', 'send_transactions']
|
|
1600
|
+
* });
|
|
1601
|
+
*
|
|
1602
|
+
* await wallet.initialize();
|
|
1603
|
+
* await wallet.connect();
|
|
1604
|
+
*
|
|
1605
|
+
* const balance = await wallet.getBalance();
|
|
1606
|
+
* console.log('Balance:', balance.total, 'OCT');
|
|
1607
|
+
* ```
|
|
1608
|
+
*/
|
|
1609
|
+
// Main exports
|
|
1610
|
+
// Version information
|
|
1611
|
+
const SDK_VERSION = '2.1.1';
|
|
1612
|
+
const SUPPORTED_EXTENSION_VERSIONS = ['2.0.1', '2.0.3', '2.0.4'];
|
|
1613
|
+
// Quick setup function for simple use cases
|
|
1614
|
+
async function createZeroXIOWallet(config) {
|
|
1615
|
+
const { ZeroXIOWallet } = await Promise.resolve().then(function () { return wallet; });
|
|
1616
|
+
const wallet$1 = new ZeroXIOWallet({
|
|
1617
|
+
appName: config.appName,
|
|
1618
|
+
appDescription: config.appDescription,
|
|
1619
|
+
debug: config.debug || false
|
|
1620
|
+
});
|
|
1621
|
+
await wallet$1.initialize();
|
|
1622
|
+
if (config.autoConnect) {
|
|
1623
|
+
try {
|
|
1624
|
+
await wallet$1.connect();
|
|
1625
|
+
}
|
|
1626
|
+
catch (error) {
|
|
1627
|
+
if (config.debug) ;
|
|
1628
|
+
// Don't throw - let the app handle connection manually
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return wallet$1;
|
|
1632
|
+
}
|
|
1633
|
+
// Legacy alias for backward compatibility
|
|
1634
|
+
const createOctraWallet = createZeroXIOWallet;
|
|
1635
|
+
// Browser detection and compatibility check
|
|
1636
|
+
function checkSDKCompatibility() {
|
|
1637
|
+
const issues = [];
|
|
1638
|
+
const recommendations = [];
|
|
1639
|
+
// Basic browser support check
|
|
1640
|
+
if (typeof window === 'undefined') {
|
|
1641
|
+
issues.push('Window object not available');
|
|
1642
|
+
recommendations.push('SDK must be used in a browser environment');
|
|
1643
|
+
}
|
|
1644
|
+
// Check for extension APIs
|
|
1645
|
+
if (typeof window !== 'undefined') {
|
|
1646
|
+
const win = window;
|
|
1647
|
+
if (!win.chrome || !win.chrome.runtime) {
|
|
1648
|
+
issues.push('Chrome extension APIs not available');
|
|
1649
|
+
recommendations.push('This SDK requires a Chromium-based browser (Chrome, Edge, Brave, etc.)');
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return {
|
|
1653
|
+
compatible: issues.length === 0,
|
|
1654
|
+
issues,
|
|
1655
|
+
recommendations
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
// Development helpers
|
|
1659
|
+
if (typeof window !== 'undefined') {
|
|
1660
|
+
// Expose SDK version for debugging (support both old and new naming)
|
|
1661
|
+
window.__OCTRA_SDK_VERSION__ = SDK_VERSION;
|
|
1662
|
+
window.__ZEROXIO_SDK_VERSION__ = SDK_VERSION;
|
|
1663
|
+
// Development mode detection
|
|
1664
|
+
const isDevelopment = window.location.hostname === 'localhost' ||
|
|
1665
|
+
window.location.hostname === '127.0.0.1' ||
|
|
1666
|
+
window.location.hostname.includes('dev');
|
|
1667
|
+
if (isDevelopment) {
|
|
1668
|
+
// Set debug flag but don't automatically log
|
|
1669
|
+
window.__OCTRA_SDK_DEBUG__ = true;
|
|
1670
|
+
window.__ZEROXIO_SDK_DEBUG__ = true;
|
|
1671
|
+
// Expose debugging utilities (new branding)
|
|
1672
|
+
window.__ZEROXIO_SDK_UTILS__ = {
|
|
1673
|
+
enableDebugMode: () => {
|
|
1674
|
+
window.__ZEROXIO_SDK_DEBUG__ = true;
|
|
1675
|
+
window.__OCTRA_SDK_DEBUG__ = true;
|
|
1676
|
+
console.log('[0xio SDK] Debug mode enabled');
|
|
1677
|
+
},
|
|
1678
|
+
disableDebugMode: () => {
|
|
1679
|
+
window.__ZEROXIO_SDK_DEBUG__ = false;
|
|
1680
|
+
window.__OCTRA_SDK_DEBUG__ = false;
|
|
1681
|
+
console.log('[0xio SDK] Debug mode disabled');
|
|
1682
|
+
},
|
|
1683
|
+
getSDKInfo: () => ({
|
|
1684
|
+
version: SDK_VERSION,
|
|
1685
|
+
supportedExtensions: SUPPORTED_EXTENSION_VERSIONS,
|
|
1686
|
+
debugMode: !!window.__ZEROXIO_SDK_DEBUG__,
|
|
1687
|
+
environment: isDevelopment ? 'development' : 'production'
|
|
1688
|
+
}),
|
|
1689
|
+
simulateExtensionEvent: (eventType, data) => {
|
|
1690
|
+
window.postMessage({
|
|
1691
|
+
source: '0xio-sdk-bridge',
|
|
1692
|
+
event: { type: eventType, data }
|
|
1693
|
+
}, '*');
|
|
1694
|
+
console.log('[0xio SDK] Simulated extension event:', eventType, data);
|
|
1695
|
+
},
|
|
1696
|
+
showWelcome: () => {
|
|
1697
|
+
console.log(`[0xio SDK] Development mode - SDK v${SDK_VERSION}`);
|
|
1698
|
+
console.log('[0xio SDK] Debug utilities available at window.__ZEROXIO_SDK_UTILS__');
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
// Legacy alias for backward compatibility
|
|
1702
|
+
window.__OCTRA_SDK_UTILS__ = window.__ZEROXIO_SDK_UTILS__;
|
|
1703
|
+
// Only show welcome message if specifically requested or on localhost
|
|
1704
|
+
// if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
1705
|
+
// (window as any).__ZEROXIO_SDK_UTILS__.showWelcome();
|
|
1706
|
+
// }
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
exports.DEFAULT_NETWORK_ID = DEFAULT_NETWORK_ID;
|
|
1711
|
+
exports.EventEmitter = EventEmitter;
|
|
1712
|
+
exports.ExtensionCommunicator = ExtensionCommunicator;
|
|
1713
|
+
exports.NETWORKS = NETWORKS;
|
|
1714
|
+
exports.SDK_CONFIG = SDK_CONFIG;
|
|
1715
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
1716
|
+
exports.SUPPORTED_EXTENSION_VERSIONS = SUPPORTED_EXTENSION_VERSIONS;
|
|
1717
|
+
exports.ZeroXIOWallet = ZeroXIOWallet;
|
|
1718
|
+
exports.ZeroXIOWalletError = ZeroXIOWalletError;
|
|
1719
|
+
exports.checkBrowserSupport = checkBrowserSupport;
|
|
1720
|
+
exports.checkSDKCompatibility = checkSDKCompatibility;
|
|
1721
|
+
exports.createDefaultBalance = createDefaultBalance;
|
|
1722
|
+
exports.createErrorMessage = createErrorMessage;
|
|
1723
|
+
exports.createLogger = createLogger;
|
|
1724
|
+
exports.createOctraWallet = createOctraWallet;
|
|
1725
|
+
exports.createZeroXIOWallet = createZeroXIOWallet;
|
|
1726
|
+
exports.delay = delay;
|
|
1727
|
+
exports.formatAddress = formatAddress;
|
|
1728
|
+
exports.formatOCT = formatOCT;
|
|
1729
|
+
exports.formatTimestamp = formatTimestamp;
|
|
1730
|
+
exports.formatTxHash = formatTxHash;
|
|
1731
|
+
exports.formatZeroXIO = formatOCT;
|
|
1732
|
+
exports.fromMicroOCT = fromMicroOCT;
|
|
1733
|
+
exports.fromMicroZeroXIO = fromMicroOCT;
|
|
1734
|
+
exports.generateMockData = generateMockData;
|
|
1735
|
+
exports.getAllNetworks = getAllNetworks;
|
|
1736
|
+
exports.getDefaultNetwork = getDefaultNetwork;
|
|
1737
|
+
exports.getNetworkConfig = getNetworkConfig;
|
|
1738
|
+
exports.isBrowser = isBrowser;
|
|
1739
|
+
exports.isErrorType = isErrorType;
|
|
1740
|
+
exports.isValidAddress = isValidAddress;
|
|
1741
|
+
exports.isValidAmount = isValidAmount;
|
|
1742
|
+
exports.isValidFeeLevel = isValidFeeLevel;
|
|
1743
|
+
exports.isValidMessage = isValidMessage;
|
|
1744
|
+
exports.isValidNetworkId = isValidNetworkId;
|
|
1745
|
+
exports.retry = retry;
|
|
1746
|
+
exports.toMicroOCT = toMicroOCT;
|
|
1747
|
+
exports.toMicroZeroXIO = toMicroOCT;
|
|
1748
|
+
exports.withTimeout = withTimeout;
|
|
1749
|
+
|
|
1750
|
+
}));
|
|
1751
|
+
//# sourceMappingURL=index.umd.js.map
|