@cfxdevkit/services 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/LICENSE +72 -0
- package/README.md +257 -0
- package/dist/index.cjs +1097 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1076 -0
- package/dist/index.js.map +1 -0
- package/dist/services/index.cjs +1097 -0
- package/dist/services/index.cjs.map +1 -0
- package/dist/services/index.d.cts +566 -0
- package/dist/services/index.d.ts +566 -0
- package/dist/services/index.js +1076 -0
- package/dist/services/index.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/services/index.ts
|
|
21
|
+
var services_exports = {};
|
|
22
|
+
__export(services_exports, {
|
|
23
|
+
EncryptionService: () => EncryptionService,
|
|
24
|
+
KeystoreLockedError: () => KeystoreLockedError,
|
|
25
|
+
KeystoreService: () => KeystoreService,
|
|
26
|
+
SwapService: () => SwapService,
|
|
27
|
+
getKeystoreService: () => getKeystoreService
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(services_exports);
|
|
30
|
+
|
|
31
|
+
// src/services/encryption.ts
|
|
32
|
+
var import_node_crypto = require("crypto");
|
|
33
|
+
var EncryptionService = class _EncryptionService {
|
|
34
|
+
// Utility class — not meant to be instantiated
|
|
35
|
+
constructor() {
|
|
36
|
+
}
|
|
37
|
+
static ITERATIONS = 1e5;
|
|
38
|
+
static KEY_LENGTH = 256;
|
|
39
|
+
static SALT_LENGTH = 32;
|
|
40
|
+
static IV_LENGTH = 12;
|
|
41
|
+
/**
|
|
42
|
+
* Generate a random salt for key derivation
|
|
43
|
+
*/
|
|
44
|
+
static generateSalt() {
|
|
45
|
+
return Buffer.from(
|
|
46
|
+
import_node_crypto.webcrypto.getRandomValues(new Uint8Array(_EncryptionService.SALT_LENGTH))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Derive encryption key from password using PBKDF2
|
|
51
|
+
*/
|
|
52
|
+
static async deriveKey(password, salt) {
|
|
53
|
+
const encoder = new TextEncoder();
|
|
54
|
+
const passwordBuffer = encoder.encode(password);
|
|
55
|
+
const keyMaterial = await import_node_crypto.webcrypto.subtle.importKey(
|
|
56
|
+
"raw",
|
|
57
|
+
passwordBuffer,
|
|
58
|
+
"PBKDF2",
|
|
59
|
+
false,
|
|
60
|
+
["deriveBits", "deriveKey"]
|
|
61
|
+
);
|
|
62
|
+
return await import_node_crypto.webcrypto.subtle.deriveKey(
|
|
63
|
+
{
|
|
64
|
+
name: "PBKDF2",
|
|
65
|
+
salt: new Uint8Array(salt),
|
|
66
|
+
iterations: _EncryptionService.ITERATIONS,
|
|
67
|
+
hash: "SHA-256"
|
|
68
|
+
},
|
|
69
|
+
keyMaterial,
|
|
70
|
+
{ name: "AES-GCM", length: _EncryptionService.KEY_LENGTH },
|
|
71
|
+
false,
|
|
72
|
+
["encrypt", "decrypt"]
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Encrypt plaintext with password
|
|
77
|
+
*
|
|
78
|
+
* @param plaintext - String to encrypt
|
|
79
|
+
* @param password - Encryption password
|
|
80
|
+
* @param salt - Salt for key derivation
|
|
81
|
+
* @returns Base64-encoded ciphertext with prepended IV
|
|
82
|
+
*/
|
|
83
|
+
static async encrypt(plaintext, password, salt) {
|
|
84
|
+
const key = await _EncryptionService.deriveKey(password, salt);
|
|
85
|
+
const iv = import_node_crypto.webcrypto.getRandomValues(
|
|
86
|
+
new Uint8Array(_EncryptionService.IV_LENGTH)
|
|
87
|
+
);
|
|
88
|
+
const encoder = new TextEncoder();
|
|
89
|
+
const plaintextBuffer = encoder.encode(plaintext);
|
|
90
|
+
const ciphertext = await import_node_crypto.webcrypto.subtle.encrypt(
|
|
91
|
+
{ name: "AES-GCM", iv },
|
|
92
|
+
key,
|
|
93
|
+
plaintextBuffer
|
|
94
|
+
);
|
|
95
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
96
|
+
combined.set(iv, 0);
|
|
97
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
98
|
+
return Buffer.from(combined).toString("base64");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Decrypt ciphertext with password
|
|
102
|
+
*
|
|
103
|
+
* @param ciphertext - Base64-encoded ciphertext with prepended IV
|
|
104
|
+
* @param password - Encryption password
|
|
105
|
+
* @param salt - Salt for key derivation
|
|
106
|
+
* @returns Decrypted plaintext
|
|
107
|
+
* @throws Error if decryption fails (wrong password or corrupted data)
|
|
108
|
+
*/
|
|
109
|
+
static async decrypt(ciphertext, password, salt) {
|
|
110
|
+
const combined = Buffer.from(ciphertext, "base64");
|
|
111
|
+
const iv = combined.subarray(0, _EncryptionService.IV_LENGTH);
|
|
112
|
+
const encrypted = combined.subarray(_EncryptionService.IV_LENGTH);
|
|
113
|
+
const key = await _EncryptionService.deriveKey(password, salt);
|
|
114
|
+
try {
|
|
115
|
+
const decrypted = await import_node_crypto.webcrypto.subtle.decrypt(
|
|
116
|
+
{ name: "AES-GCM", iv },
|
|
117
|
+
key,
|
|
118
|
+
encrypted
|
|
119
|
+
);
|
|
120
|
+
const decoder = new TextDecoder();
|
|
121
|
+
return decoder.decode(decrypted);
|
|
122
|
+
} catch (_error) {
|
|
123
|
+
throw new Error("Decryption failed - invalid password or corrupted data");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Encrypt an object (serializes to JSON first)
|
|
128
|
+
*/
|
|
129
|
+
static async encryptObject(obj, password, salt) {
|
|
130
|
+
const json = JSON.stringify(obj);
|
|
131
|
+
return await _EncryptionService.encrypt(json, password, salt);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Decrypt to an object (parses JSON after decryption)
|
|
135
|
+
*/
|
|
136
|
+
static async decryptObject(ciphertext, password, salt) {
|
|
137
|
+
const json = await _EncryptionService.decrypt(ciphertext, password, salt);
|
|
138
|
+
return JSON.parse(json);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Hash a string with SHA-256 (for config integrity checks)
|
|
142
|
+
*/
|
|
143
|
+
static async hash(data) {
|
|
144
|
+
const encoder = new TextEncoder();
|
|
145
|
+
const dataBuffer = encoder.encode(data);
|
|
146
|
+
const hashBuffer = await import_node_crypto.webcrypto.subtle.digest("SHA-256", dataBuffer);
|
|
147
|
+
return Buffer.from(hashBuffer).toString("hex");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Verify password strength (basic validation)
|
|
151
|
+
*/
|
|
152
|
+
static validatePasswordStrength(password) {
|
|
153
|
+
const errors = [];
|
|
154
|
+
if (password.length < 8) {
|
|
155
|
+
errors.push("Password must be at least 8 characters");
|
|
156
|
+
}
|
|
157
|
+
if (!/[a-z]/.test(password)) {
|
|
158
|
+
errors.push("Password must contain lowercase letters");
|
|
159
|
+
}
|
|
160
|
+
if (!/[A-Z]/.test(password)) {
|
|
161
|
+
errors.push("Password must contain uppercase letters");
|
|
162
|
+
}
|
|
163
|
+
if (!/[0-9]/.test(password)) {
|
|
164
|
+
errors.push("Password must contain numbers");
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
valid: errors.length === 0,
|
|
168
|
+
errors
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/services/keystore.ts
|
|
174
|
+
var import_node_crypto2 = require("crypto");
|
|
175
|
+
var import_node_fs = require("fs");
|
|
176
|
+
var import_node_os = require("os");
|
|
177
|
+
var import_node_path = require("path");
|
|
178
|
+
var import_utils = require("@cfxdevkit/core/utils");
|
|
179
|
+
var import_wallet = require("@cfxdevkit/core/wallet");
|
|
180
|
+
var KeystoreLockedError = class extends Error {
|
|
181
|
+
constructor(message = "Keystore is locked. Please unlock with your password first.") {
|
|
182
|
+
super(message);
|
|
183
|
+
this.name = "KeystoreLockedError";
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var BASE_DATA_DIR = process.env.DEVKIT_DATA_DIR || "/workspace/.conflux-dev";
|
|
187
|
+
var DEFAULT_KEYSTORE_PATH = process.env.DEVKIT_KEYSTORE_PATH || (0, import_node_path.join)((0, import_node_os.homedir)(), ".devkit.keystore.json");
|
|
188
|
+
var KeystoreService = class {
|
|
189
|
+
keystorePath;
|
|
190
|
+
keystore = null;
|
|
191
|
+
currentPassword = null;
|
|
192
|
+
constructor(keystorePath = DEFAULT_KEYSTORE_PATH) {
|
|
193
|
+
this.keystorePath = keystorePath;
|
|
194
|
+
}
|
|
195
|
+
// ===== PRIVATE GUARD HELPERS =====
|
|
196
|
+
requireKeystore() {
|
|
197
|
+
if (!this.keystore) {
|
|
198
|
+
throw new Error("Keystore not initialized. Call initialize() first.");
|
|
199
|
+
}
|
|
200
|
+
return this.keystore;
|
|
201
|
+
}
|
|
202
|
+
requireEncryptionSalt() {
|
|
203
|
+
const ks = this.requireKeystore();
|
|
204
|
+
if (!ks.encryptionSalt) {
|
|
205
|
+
throw new Error("Encryption salt not configured on this keystore.");
|
|
206
|
+
}
|
|
207
|
+
return Buffer.from(ks.encryptionSalt, "base64");
|
|
208
|
+
}
|
|
209
|
+
requirePassword() {
|
|
210
|
+
if (!this.currentPassword) {
|
|
211
|
+
throw new KeystoreLockedError();
|
|
212
|
+
}
|
|
213
|
+
return this.currentPassword;
|
|
214
|
+
}
|
|
215
|
+
// ===== INITIALIZATION =====
|
|
216
|
+
/**
|
|
217
|
+
* Initialize keystore (load from disk or create empty)
|
|
218
|
+
*/
|
|
219
|
+
async initialize() {
|
|
220
|
+
if ((0, import_node_fs.existsSync)(this.keystorePath)) {
|
|
221
|
+
await this.loadKeystore();
|
|
222
|
+
} else {
|
|
223
|
+
import_utils.logger.info("Keystore file not found - fresh installation detected");
|
|
224
|
+
this.keystore = null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Load keystore from disk
|
|
229
|
+
*/
|
|
230
|
+
async loadKeystore() {
|
|
231
|
+
try {
|
|
232
|
+
const data = (0, import_node_fs.readFileSync)(this.keystorePath, "utf-8");
|
|
233
|
+
this.keystore = JSON.parse(data);
|
|
234
|
+
if (this.keystore.version !== 2) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Unsupported keystore version: ${this.keystore.version}. Expected version 2.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
import_utils.logger.info("Keystore loaded successfully");
|
|
240
|
+
} catch (error) {
|
|
241
|
+
import_utils.logger.error("Failed to load keystore:", error);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Save keystore to disk
|
|
247
|
+
*/
|
|
248
|
+
async saveKeystore() {
|
|
249
|
+
if (!this.keystore) {
|
|
250
|
+
throw new Error("Cannot save null keystore");
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const data = JSON.stringify(this.keystore, null, 2);
|
|
254
|
+
(0, import_node_fs.writeFileSync)(this.keystorePath, data, "utf-8");
|
|
255
|
+
import_utils.logger.info("Keystore saved successfully");
|
|
256
|
+
} catch (error) {
|
|
257
|
+
import_utils.logger.error("Failed to save keystore:", error);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ===== SETUP & STATUS =====
|
|
262
|
+
/**
|
|
263
|
+
* Check if initial setup is completed
|
|
264
|
+
*/
|
|
265
|
+
async isSetupCompleted() {
|
|
266
|
+
return this.keystore?.setupCompleted ?? false;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Complete initial setup
|
|
270
|
+
*/
|
|
271
|
+
async completeSetup(data) {
|
|
272
|
+
if (this.keystore?.setupCompleted) {
|
|
273
|
+
throw new Error("Setup already completed");
|
|
274
|
+
}
|
|
275
|
+
import_utils.logger.info("Completing initial setup...");
|
|
276
|
+
let encryptionSalt;
|
|
277
|
+
if (data.encryption?.enabled && data.encryption.password) {
|
|
278
|
+
encryptionSalt = EncryptionService.generateSalt();
|
|
279
|
+
this.currentPassword = data.encryption.password;
|
|
280
|
+
}
|
|
281
|
+
const mnemonicEntry = await this.createMnemonicEntry({
|
|
282
|
+
mnemonic: data.mnemonic,
|
|
283
|
+
label: data.mnemonicLabel,
|
|
284
|
+
nodeConfig: {
|
|
285
|
+
...data.nodeConfig,
|
|
286
|
+
miningAuthor: data.nodeConfig.miningAuthor || "auto"
|
|
287
|
+
},
|
|
288
|
+
isFirstSetup: true,
|
|
289
|
+
encryptionEnabled: data.encryption?.enabled ?? false,
|
|
290
|
+
encryptionSalt
|
|
291
|
+
});
|
|
292
|
+
this.keystore = {
|
|
293
|
+
version: 2,
|
|
294
|
+
setupCompleted: true,
|
|
295
|
+
setupCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
296
|
+
adminAddresses: [data.adminAddress],
|
|
297
|
+
encryptionEnabled: data.encryption?.enabled ?? false,
|
|
298
|
+
encryptionSalt: encryptionSalt?.toString("base64"),
|
|
299
|
+
mnemonics: [mnemonicEntry],
|
|
300
|
+
activeIndex: 0
|
|
301
|
+
};
|
|
302
|
+
await this.saveKeystore();
|
|
303
|
+
import_utils.logger.success("Initial setup completed successfully");
|
|
304
|
+
import_utils.logger.info(`Admin address: ${data.adminAddress}`);
|
|
305
|
+
import_utils.logger.info(`Wallet: ${data.mnemonicLabel}`);
|
|
306
|
+
import_utils.logger.info(
|
|
307
|
+
`Encryption: ${data.encryption?.enabled ? "Enabled" : "Disabled"}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
// ===== ADMIN MANAGEMENT =====
|
|
311
|
+
/**
|
|
312
|
+
* Get all admin addresses
|
|
313
|
+
*/
|
|
314
|
+
async getAdminAddresses() {
|
|
315
|
+
this.ensureKeystoreLoaded();
|
|
316
|
+
return [...this.keystore?.adminAddresses ?? []];
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Add admin address
|
|
320
|
+
*/
|
|
321
|
+
async addAdminAddress(address) {
|
|
322
|
+
this.ensureKeystoreLoaded();
|
|
323
|
+
const normalized = address.toLowerCase();
|
|
324
|
+
const exists = this.keystore?.adminAddresses.some(
|
|
325
|
+
(addr) => addr.toLowerCase() === normalized
|
|
326
|
+
);
|
|
327
|
+
if (exists) {
|
|
328
|
+
throw new Error("Admin address already exists");
|
|
329
|
+
}
|
|
330
|
+
this.keystore?.adminAddresses.push(address);
|
|
331
|
+
await this.saveKeystore();
|
|
332
|
+
import_utils.logger.info(`Added admin address: ${address}`);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Remove admin address
|
|
336
|
+
*/
|
|
337
|
+
async removeAdminAddress(address, currentAdmin) {
|
|
338
|
+
this.ensureKeystoreLoaded();
|
|
339
|
+
if (address.toLowerCase() === currentAdmin.toLowerCase()) {
|
|
340
|
+
throw new Error("Cannot remove your own admin address");
|
|
341
|
+
}
|
|
342
|
+
if (this.requireKeystore().adminAddresses.length <= 1) {
|
|
343
|
+
throw new Error("Cannot remove the last admin address");
|
|
344
|
+
}
|
|
345
|
+
const index = this.requireKeystore().adminAddresses.findIndex(
|
|
346
|
+
(addr) => addr.toLowerCase() === address.toLowerCase()
|
|
347
|
+
);
|
|
348
|
+
if (index === -1) {
|
|
349
|
+
throw new Error("Admin address not found");
|
|
350
|
+
}
|
|
351
|
+
this.requireKeystore().adminAddresses.splice(index, 1);
|
|
352
|
+
await this.saveKeystore();
|
|
353
|
+
import_utils.logger.info(`Removed admin address: ${address}`);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Check if address is admin
|
|
357
|
+
*/
|
|
358
|
+
isAdmin(address) {
|
|
359
|
+
if (!this.keystore) return false;
|
|
360
|
+
return this.keystore.adminAddresses.some(
|
|
361
|
+
(admin) => admin.toLowerCase() === address.toLowerCase()
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
// ===== MNEMONIC MANAGEMENT =====
|
|
365
|
+
/**
|
|
366
|
+
* Get active mnemonic entry
|
|
367
|
+
*/
|
|
368
|
+
async getActiveMnemonic() {
|
|
369
|
+
this.ensureKeystoreLoaded();
|
|
370
|
+
const mnemonic = this.requireKeystore().mnemonics[this.requireKeystore().activeIndex];
|
|
371
|
+
if (!mnemonic) {
|
|
372
|
+
throw new Error("No active mnemonic found");
|
|
373
|
+
}
|
|
374
|
+
return mnemonic;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Get mnemonic by ID
|
|
378
|
+
*/
|
|
379
|
+
async getMnemonic(id) {
|
|
380
|
+
this.ensureKeystoreLoaded();
|
|
381
|
+
const mnemonic = this.requireKeystore().mnemonics.find((m) => m.id === id);
|
|
382
|
+
if (!mnemonic) {
|
|
383
|
+
throw new Error(`Mnemonic not found: ${id}`);
|
|
384
|
+
}
|
|
385
|
+
return mnemonic;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* List all mnemonics (summary only)
|
|
389
|
+
*/
|
|
390
|
+
async listMnemonics() {
|
|
391
|
+
this.ensureKeystoreLoaded();
|
|
392
|
+
return this.requireKeystore().mnemonics.map((m, index) => ({
|
|
393
|
+
id: m.id,
|
|
394
|
+
label: m.label,
|
|
395
|
+
type: m.type,
|
|
396
|
+
isActive: index === this.requireKeystore().activeIndex,
|
|
397
|
+
createdAt: m.createdAt,
|
|
398
|
+
nodeConfig: m.nodeConfig,
|
|
399
|
+
dataDir: this.getDataDirForMnemonic(m.mnemonic),
|
|
400
|
+
dataSize: this.getDataDirSize(this.getDataDirForMnemonic(m.mnemonic))
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Add new mnemonic
|
|
405
|
+
*/
|
|
406
|
+
async addMnemonic(data) {
|
|
407
|
+
this.ensureKeystoreLoaded();
|
|
408
|
+
const exists = this.requireKeystore().mnemonics.some(
|
|
409
|
+
(m) => m.label === data.label
|
|
410
|
+
);
|
|
411
|
+
if (exists) {
|
|
412
|
+
throw new Error(`Mnemonic with label "${data.label}" already exists`);
|
|
413
|
+
}
|
|
414
|
+
const mnemonicEntry = await this.createMnemonicEntry({
|
|
415
|
+
mnemonic: data.mnemonic,
|
|
416
|
+
label: data.label,
|
|
417
|
+
nodeConfig: {
|
|
418
|
+
...data.nodeConfig,
|
|
419
|
+
miningAuthor: data.nodeConfig.miningAuthor || "auto"
|
|
420
|
+
},
|
|
421
|
+
isFirstSetup: false,
|
|
422
|
+
encryptionEnabled: this.requireKeystore().encryptionEnabled,
|
|
423
|
+
encryptionSalt: this.keystore?.encryptionSalt ? Buffer.from(this.keystore?.encryptionSalt, "base64") : void 0
|
|
424
|
+
});
|
|
425
|
+
this.requireKeystore().mnemonics.push(mnemonicEntry);
|
|
426
|
+
if (data.setAsActive) {
|
|
427
|
+
this.requireKeystore().activeIndex = this.requireKeystore().mnemonics.length - 1;
|
|
428
|
+
}
|
|
429
|
+
await this.saveKeystore();
|
|
430
|
+
import_utils.logger.info(`Added mnemonic: ${data.label}`);
|
|
431
|
+
return mnemonicEntry;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Update mnemonic label
|
|
435
|
+
*/
|
|
436
|
+
async updateMnemonicLabel(id, label) {
|
|
437
|
+
this.ensureKeystoreLoaded();
|
|
438
|
+
const index = this.requireKeystore().mnemonics.findIndex(
|
|
439
|
+
(m) => m.id === id
|
|
440
|
+
);
|
|
441
|
+
if (index === -1) {
|
|
442
|
+
throw new Error(`Mnemonic not found: ${id}`);
|
|
443
|
+
}
|
|
444
|
+
if (!label.trim()) {
|
|
445
|
+
throw new Error(`Label cannot be empty`);
|
|
446
|
+
}
|
|
447
|
+
this.requireKeystore().mnemonics[index].label = label;
|
|
448
|
+
await this.saveKeystore();
|
|
449
|
+
import_utils.logger.info(`Updated label for mnemonic ${id} to: ${label}`);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Switch active mnemonic
|
|
453
|
+
*/
|
|
454
|
+
async switchActiveMnemonic(id) {
|
|
455
|
+
this.ensureKeystoreLoaded();
|
|
456
|
+
const index = this.requireKeystore().mnemonics.findIndex(
|
|
457
|
+
(m) => m.id === id
|
|
458
|
+
);
|
|
459
|
+
if (index === -1) {
|
|
460
|
+
throw new Error(`Mnemonic not found: ${id}`);
|
|
461
|
+
}
|
|
462
|
+
this.requireKeystore().activeIndex = index;
|
|
463
|
+
await this.saveKeystore();
|
|
464
|
+
import_utils.logger.info(
|
|
465
|
+
`Switched to mnemonic: ${this.requireKeystore().mnemonics[index].label}`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Delete mnemonic and its data
|
|
470
|
+
*/
|
|
471
|
+
async deleteMnemonic(id, deleteData = false) {
|
|
472
|
+
this.ensureKeystoreLoaded();
|
|
473
|
+
const index = this.requireKeystore().mnemonics.findIndex(
|
|
474
|
+
(m) => m.id === id
|
|
475
|
+
);
|
|
476
|
+
if (index === -1) {
|
|
477
|
+
throw new Error(`Mnemonic not found: ${id}`);
|
|
478
|
+
}
|
|
479
|
+
if (index === this.requireKeystore().activeIndex) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
"Cannot delete active mnemonic. Switch to another mnemonic first."
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
const mnemonic = this.requireKeystore().mnemonics[index];
|
|
485
|
+
if (deleteData) {
|
|
486
|
+
const dataDir = this.getDataDirForMnemonic(mnemonic.mnemonic);
|
|
487
|
+
if ((0, import_node_fs.existsSync)(dataDir)) {
|
|
488
|
+
const lockFile = (0, import_node_path.join)(dataDir, "node.lock");
|
|
489
|
+
if ((0, import_node_fs.existsSync)(lockFile)) {
|
|
490
|
+
throw new Error("Cannot delete data while node is running");
|
|
491
|
+
}
|
|
492
|
+
(0, import_node_fs.rmSync)(dataDir, { recursive: true, force: true });
|
|
493
|
+
import_utils.logger.info(`Deleted data directory: ${dataDir}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
this.requireKeystore().mnemonics.splice(index, 1);
|
|
497
|
+
if (this.requireKeystore().activeIndex > index) {
|
|
498
|
+
this.requireKeystore().activeIndex--;
|
|
499
|
+
}
|
|
500
|
+
await this.saveKeystore();
|
|
501
|
+
import_utils.logger.info(`Deleted mnemonic: ${mnemonic.label}`);
|
|
502
|
+
}
|
|
503
|
+
// ===== NODE CONFIGURATION =====
|
|
504
|
+
/**
|
|
505
|
+
* Get node configuration for a mnemonic
|
|
506
|
+
*/
|
|
507
|
+
async getNodeConfig(mnemonicId) {
|
|
508
|
+
const mnemonic = await this.getMnemonic(mnemonicId);
|
|
509
|
+
return mnemonic.nodeConfig;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Check if node configuration can be modified
|
|
513
|
+
*/
|
|
514
|
+
async canModifyNodeConfig(mnemonicId) {
|
|
515
|
+
const mnemonic = await this.getMnemonic(mnemonicId);
|
|
516
|
+
const dataDir = this.getDataDirForMnemonic(mnemonic.mnemonic);
|
|
517
|
+
if (!(0, import_node_fs.existsSync)(dataDir)) {
|
|
518
|
+
return {
|
|
519
|
+
canModify: true
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const lockFile = (0, import_node_path.join)(dataDir, "node.lock");
|
|
523
|
+
if ((0, import_node_fs.existsSync)(lockFile)) {
|
|
524
|
+
return {
|
|
525
|
+
canModify: false,
|
|
526
|
+
reason: "Node is currently running",
|
|
527
|
+
lockFile
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
canModify: false,
|
|
532
|
+
reason: "Data directory exists. Delete blockchain data to modify configuration.",
|
|
533
|
+
dataDir
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Update node configuration
|
|
538
|
+
*/
|
|
539
|
+
async updateNodeConfig(mnemonicId, config) {
|
|
540
|
+
const check = await this.canModifyNodeConfig(mnemonicId);
|
|
541
|
+
if (!check.canModify) {
|
|
542
|
+
throw new Error(check.reason);
|
|
543
|
+
}
|
|
544
|
+
this.ensureKeystoreLoaded();
|
|
545
|
+
const index = this.requireKeystore().mnemonics.findIndex(
|
|
546
|
+
(m) => m.id === mnemonicId
|
|
547
|
+
);
|
|
548
|
+
if (index === -1) {
|
|
549
|
+
throw new Error(`Mnemonic not found: ${mnemonicId}`);
|
|
550
|
+
}
|
|
551
|
+
const mnemonic = this.requireKeystore().mnemonics[index];
|
|
552
|
+
const newConfig = {
|
|
553
|
+
...mnemonic.nodeConfig,
|
|
554
|
+
...config,
|
|
555
|
+
immutable: true,
|
|
556
|
+
configHash: "",
|
|
557
|
+
// Will be recalculated
|
|
558
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
559
|
+
};
|
|
560
|
+
newConfig.configHash = await this.calculateConfigHash(newConfig);
|
|
561
|
+
this.requireKeystore().mnemonics[index].nodeConfig = newConfig;
|
|
562
|
+
await this.saveKeystore();
|
|
563
|
+
import_utils.logger.info(`Updated node config for: ${mnemonic.label}`);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Delete blockchain data directory
|
|
567
|
+
*/
|
|
568
|
+
async deleteNodeData(mnemonicId) {
|
|
569
|
+
const mnemonic = await this.getMnemonic(mnemonicId);
|
|
570
|
+
const dataDir = this.getDataDirForMnemonic(mnemonic.mnemonic);
|
|
571
|
+
if (!(0, import_node_fs.existsSync)(dataDir)) {
|
|
572
|
+
throw new Error("Data directory does not exist");
|
|
573
|
+
}
|
|
574
|
+
const lockFile = (0, import_node_path.join)(dataDir, "node.lock");
|
|
575
|
+
if ((0, import_node_fs.existsSync)(lockFile)) {
|
|
576
|
+
throw new Error("Cannot delete data while node is running");
|
|
577
|
+
}
|
|
578
|
+
const dataSize = this.getDataDirSize(dataDir);
|
|
579
|
+
(0, import_node_fs.rmSync)(dataDir, { recursive: true, force: true });
|
|
580
|
+
import_utils.logger.info(`Deleted data directory: ${dataDir}`);
|
|
581
|
+
return { deletedDir: dataDir, dataSize };
|
|
582
|
+
}
|
|
583
|
+
// ===== ENCRYPTION =====
|
|
584
|
+
/**
|
|
585
|
+
* Check if keystore is locked
|
|
586
|
+
*/
|
|
587
|
+
isLocked() {
|
|
588
|
+
if (!this.keystore || !this.keystore.encryptionEnabled) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
return this.currentPassword === null;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Check if encryption is enabled for the keystore
|
|
595
|
+
*/
|
|
596
|
+
isEncryptionEnabled() {
|
|
597
|
+
return this.keystore?.encryptionEnabled ?? false;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Unlock keystore with password
|
|
601
|
+
*/
|
|
602
|
+
async unlockKeystore(password) {
|
|
603
|
+
if (!this.keystore || !this.keystore.encryptionEnabled) {
|
|
604
|
+
throw new Error("Keystore is not encrypted");
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const firstMnemonic = this.keystore.mnemonics[0];
|
|
608
|
+
if (firstMnemonic.type === "encrypted") {
|
|
609
|
+
const salt = this.requireEncryptionSalt();
|
|
610
|
+
await EncryptionService.decrypt(firstMnemonic.mnemonic, password, salt);
|
|
611
|
+
}
|
|
612
|
+
this.currentPassword = password;
|
|
613
|
+
import_utils.logger.info("Keystore unlocked successfully");
|
|
614
|
+
} catch (_error) {
|
|
615
|
+
throw new Error("Invalid password");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Lock keystore (clear password from memory)
|
|
620
|
+
*/
|
|
621
|
+
async lockKeystore() {
|
|
622
|
+
this.currentPassword = null;
|
|
623
|
+
import_utils.logger.info("Keystore locked");
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Get decrypted mnemonic
|
|
627
|
+
*/
|
|
628
|
+
async getDecryptedMnemonic(mnemonicId) {
|
|
629
|
+
const mnemonic = await this.getMnemonic(mnemonicId);
|
|
630
|
+
if (mnemonic.type === "plaintext") {
|
|
631
|
+
return mnemonic.mnemonic;
|
|
632
|
+
}
|
|
633
|
+
if (this.isLocked()) {
|
|
634
|
+
throw new KeystoreLockedError();
|
|
635
|
+
}
|
|
636
|
+
const salt = this.requireEncryptionSalt();
|
|
637
|
+
return await EncryptionService.decrypt(
|
|
638
|
+
mnemonic.mnemonic,
|
|
639
|
+
this.requirePassword(),
|
|
640
|
+
salt
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
// ===== DERIVED KEYS & ACCOUNTS =====
|
|
644
|
+
/**
|
|
645
|
+
* Get genesis accounts for a mnemonic
|
|
646
|
+
*/
|
|
647
|
+
async deriveGenesisAccounts(mnemonicId) {
|
|
648
|
+
const mnemonicEntry = await this.getMnemonic(mnemonicId);
|
|
649
|
+
if (mnemonicEntry.derivedKeys.type === "plaintext") {
|
|
650
|
+
return mnemonicEntry.derivedKeys.genesisAccounts;
|
|
651
|
+
}
|
|
652
|
+
if (this.isLocked()) {
|
|
653
|
+
throw new KeystoreLockedError();
|
|
654
|
+
}
|
|
655
|
+
const salt = this.requireEncryptionSalt();
|
|
656
|
+
const decrypted = await EncryptionService.decryptObject(
|
|
657
|
+
mnemonicEntry.derivedKeys.genesisAccounts,
|
|
658
|
+
this.requirePassword(),
|
|
659
|
+
salt
|
|
660
|
+
);
|
|
661
|
+
return decrypted;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get faucet account for a mnemonic
|
|
665
|
+
*/
|
|
666
|
+
async deriveFaucetAccount(mnemonicId) {
|
|
667
|
+
const mnemonicEntry = await this.getMnemonic(mnemonicId);
|
|
668
|
+
if (mnemonicEntry.derivedKeys.type === "plaintext") {
|
|
669
|
+
return mnemonicEntry.derivedKeys.faucetAccount;
|
|
670
|
+
}
|
|
671
|
+
if (this.isLocked()) {
|
|
672
|
+
throw new KeystoreLockedError();
|
|
673
|
+
}
|
|
674
|
+
const salt = this.requireEncryptionSalt();
|
|
675
|
+
const decrypted = await EncryptionService.decryptObject(
|
|
676
|
+
mnemonicEntry.derivedKeys.faucetAccount,
|
|
677
|
+
this.requirePassword(),
|
|
678
|
+
salt
|
|
679
|
+
);
|
|
680
|
+
return decrypted;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Derive accounts from mnemonic (HD wallet derivation)
|
|
684
|
+
* Returns accounts with both Core and eSpace private keys
|
|
685
|
+
* Uses @cfxdevkit/core/wallet for derivation
|
|
686
|
+
*/
|
|
687
|
+
async deriveAccountsFromMnemonic(mnemonic, _network, count, startIndex = 0, chainIdOverride) {
|
|
688
|
+
let coreNetworkId;
|
|
689
|
+
if (chainIdOverride !== void 0) {
|
|
690
|
+
coreNetworkId = chainIdOverride;
|
|
691
|
+
} else {
|
|
692
|
+
const activeMnemonic = await this.getActiveMnemonic();
|
|
693
|
+
coreNetworkId = activeMnemonic.nodeConfig.chainId;
|
|
694
|
+
}
|
|
695
|
+
const coreAccounts = (0, import_wallet.deriveAccounts)(mnemonic, {
|
|
696
|
+
count,
|
|
697
|
+
startIndex,
|
|
698
|
+
coreNetworkId,
|
|
699
|
+
accountType: "standard"
|
|
700
|
+
});
|
|
701
|
+
return coreAccounts.map(
|
|
702
|
+
(acc) => ({
|
|
703
|
+
index: acc.index,
|
|
704
|
+
core: acc.coreAddress,
|
|
705
|
+
evm: acc.evmAddress,
|
|
706
|
+
privateKey: acc.corePrivateKey,
|
|
707
|
+
// Core Space private key (m/44'/503'/0'/0/i)
|
|
708
|
+
evmPrivateKey: acc.evmPrivateKey
|
|
709
|
+
// eSpace private key (m/44'/60'/0'/0/i)
|
|
710
|
+
})
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
// ===== UTILITY METHODS =====
|
|
714
|
+
/**
|
|
715
|
+
* Get data directory for active mnemonic
|
|
716
|
+
*/
|
|
717
|
+
async getDataDir() {
|
|
718
|
+
const mnemonic = await this.getActiveMnemonic();
|
|
719
|
+
return this.getDataDirForMnemonic(mnemonic.mnemonic);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Get data directory for specific mnemonic
|
|
723
|
+
*/
|
|
724
|
+
getDataDirForMnemonic(mnemonic) {
|
|
725
|
+
const hash = (0, import_node_crypto2.createHash)("sha256").update(mnemonic).digest("hex");
|
|
726
|
+
const shortHash = hash.substring(0, 16);
|
|
727
|
+
return (0, import_node_path.join)(BASE_DATA_DIR, `wallet-${shortHash}`);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get mnemonic hash (for display)
|
|
731
|
+
*/
|
|
732
|
+
async getMnemonicHash() {
|
|
733
|
+
const mnemonic = await this.getActiveMnemonic();
|
|
734
|
+
const hash = (0, import_node_crypto2.createHash)("sha256").update(mnemonic.mnemonic).digest("hex");
|
|
735
|
+
return hash;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Get active mnemonic label
|
|
739
|
+
*/
|
|
740
|
+
getActiveLabel() {
|
|
741
|
+
if (!this.keystore) return "Unknown";
|
|
742
|
+
const mnemonic = this.keystore.mnemonics[this.keystore.activeIndex];
|
|
743
|
+
return mnemonic?.label || "Unknown";
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Get active mnemonic index
|
|
747
|
+
*/
|
|
748
|
+
getActiveIndex() {
|
|
749
|
+
return this.keystore?.activeIndex ?? 0;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Generate new BIP-39 mnemonic
|
|
753
|
+
* Uses @cfxdevkit/core/wallet for generation
|
|
754
|
+
*/
|
|
755
|
+
generateMnemonic() {
|
|
756
|
+
return (0, import_wallet.generateMnemonic)();
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Validate mnemonic format
|
|
760
|
+
* Uses @cfxdevkit/core/wallet for validation
|
|
761
|
+
*/
|
|
762
|
+
validateMnemonic(mnemonic) {
|
|
763
|
+
return (0, import_wallet.validateMnemonic)(mnemonic).valid;
|
|
764
|
+
}
|
|
765
|
+
// ===== PRIVATE HELPERS =====
|
|
766
|
+
/**
|
|
767
|
+
* Ensure keystore is loaded
|
|
768
|
+
*/
|
|
769
|
+
ensureKeystoreLoaded() {
|
|
770
|
+
if (!this.keystore) {
|
|
771
|
+
throw new Error("Keystore not loaded. Complete initial setup first.");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Create a mnemonic entry with node config and derived keys
|
|
776
|
+
*/
|
|
777
|
+
async createMnemonicEntry(params) {
|
|
778
|
+
const { mnemonic, label, nodeConfig, encryptionEnabled, encryptionSalt } = params;
|
|
779
|
+
const id = `mnemonic_${Date.now()}`;
|
|
780
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
781
|
+
const genesisAccounts = await this.deriveAccountsFromMnemonic(
|
|
782
|
+
mnemonic,
|
|
783
|
+
"espace",
|
|
784
|
+
nodeConfig.accountsCount,
|
|
785
|
+
0,
|
|
786
|
+
nodeConfig.chainId
|
|
787
|
+
);
|
|
788
|
+
const [faucetAccount] = await this.deriveAccountsFromMnemonic(
|
|
789
|
+
mnemonic,
|
|
790
|
+
"core",
|
|
791
|
+
1,
|
|
792
|
+
nodeConfig.accountsCount,
|
|
793
|
+
nodeConfig.chainId
|
|
794
|
+
);
|
|
795
|
+
const config = {
|
|
796
|
+
...nodeConfig,
|
|
797
|
+
immutable: true,
|
|
798
|
+
configHash: "",
|
|
799
|
+
// Will be calculated
|
|
800
|
+
createdAt
|
|
801
|
+
};
|
|
802
|
+
config.configHash = await this.calculateConfigHash(config);
|
|
803
|
+
let encryptedMnemonic = mnemonic;
|
|
804
|
+
let derivedKeys;
|
|
805
|
+
if (encryptionEnabled && encryptionSalt && this.currentPassword) {
|
|
806
|
+
encryptedMnemonic = await EncryptionService.encrypt(
|
|
807
|
+
mnemonic,
|
|
808
|
+
this.currentPassword,
|
|
809
|
+
encryptionSalt
|
|
810
|
+
);
|
|
811
|
+
const encryptedGenesis = await EncryptionService.encryptObject(
|
|
812
|
+
genesisAccounts,
|
|
813
|
+
this.currentPassword,
|
|
814
|
+
encryptionSalt
|
|
815
|
+
);
|
|
816
|
+
const encryptedFaucet = await EncryptionService.encryptObject(
|
|
817
|
+
faucetAccount,
|
|
818
|
+
this.currentPassword,
|
|
819
|
+
encryptionSalt
|
|
820
|
+
);
|
|
821
|
+
derivedKeys = {
|
|
822
|
+
type: "encrypted",
|
|
823
|
+
genesisAccounts: encryptedGenesis,
|
|
824
|
+
faucetAccount: encryptedFaucet
|
|
825
|
+
};
|
|
826
|
+
} else {
|
|
827
|
+
derivedKeys = {
|
|
828
|
+
type: "plaintext",
|
|
829
|
+
genesisAccounts,
|
|
830
|
+
faucetAccount
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
id,
|
|
835
|
+
label,
|
|
836
|
+
type: encryptionEnabled ? "encrypted" : "plaintext",
|
|
837
|
+
mnemonic: encryptedMnemonic,
|
|
838
|
+
createdAt,
|
|
839
|
+
nodeConfig: config,
|
|
840
|
+
derivedKeys
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Calculate config hash for integrity check
|
|
845
|
+
*/
|
|
846
|
+
async calculateConfigHash(config) {
|
|
847
|
+
const data = JSON.stringify({
|
|
848
|
+
accountsCount: config.accountsCount,
|
|
849
|
+
chainId: config.chainId,
|
|
850
|
+
evmChainId: config.evmChainId,
|
|
851
|
+
miningAuthor: config.miningAuthor
|
|
852
|
+
});
|
|
853
|
+
return await EncryptionService.hash(data);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Get data directory size (human-readable)
|
|
857
|
+
*/
|
|
858
|
+
getDataDirSize(dataDir) {
|
|
859
|
+
if (!(0, import_node_fs.existsSync)(dataDir)) {
|
|
860
|
+
return "0MB";
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const stats = (0, import_node_fs.statSync)(dataDir);
|
|
864
|
+
const bytes = stats.size;
|
|
865
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
866
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
867
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
868
|
+
} catch (_error) {
|
|
869
|
+
return "0MB";
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
var instance = null;
|
|
874
|
+
function getKeystoreService() {
|
|
875
|
+
if (!instance) {
|
|
876
|
+
instance = new KeystoreService();
|
|
877
|
+
}
|
|
878
|
+
return instance;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/services/swap.ts
|
|
882
|
+
var import_utils2 = require("@cfxdevkit/core/utils");
|
|
883
|
+
var import_viem = require("viem");
|
|
884
|
+
var SWAPPI_CONTRACTS = {
|
|
885
|
+
testnet: {
|
|
886
|
+
FACTORY: "0x8d0d1c7c32d8a395c817B22Ff3BD6fFa2A7eBe08",
|
|
887
|
+
ROUTER: "0x62B0873055Bf896Dd869e172119871ac24aeA305"
|
|
888
|
+
},
|
|
889
|
+
mainnet: {
|
|
890
|
+
FACTORY: "0x36B83F9d614a06abF5388F4d14cC64E5FF96892f",
|
|
891
|
+
ROUTER: "0x62B0873055Bf896Dd869e172119871ac24aeA305"
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
var TOKENS = {
|
|
895
|
+
testnet: {
|
|
896
|
+
WCFX: {
|
|
897
|
+
address: "0x2ed3dddae5b2f321af0806181fbfa6d049be47d8",
|
|
898
|
+
symbol: "WCFX",
|
|
899
|
+
name: "Wrapped CFX",
|
|
900
|
+
decimals: 18
|
|
901
|
+
},
|
|
902
|
+
USDT: {
|
|
903
|
+
address: "0x7d682e65efc5c13bf4e394b8f376c48e6bae0355",
|
|
904
|
+
symbol: "USDT",
|
|
905
|
+
name: "Tether USD",
|
|
906
|
+
decimals: 18
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
mainnet: {
|
|
910
|
+
WCFX: {
|
|
911
|
+
address: "0x14b2d3bc65e74dae1030eafd8ac30c533c976a9b",
|
|
912
|
+
symbol: "WCFX",
|
|
913
|
+
name: "Wrapped CFX",
|
|
914
|
+
decimals: 18
|
|
915
|
+
},
|
|
916
|
+
USDT: {
|
|
917
|
+
address: "0xfe97e85d13abd9c1c33384e796f10b73905637ce",
|
|
918
|
+
symbol: "USDT",
|
|
919
|
+
name: "Tether USD",
|
|
920
|
+
decimals: 18
|
|
921
|
+
},
|
|
922
|
+
USDC: {
|
|
923
|
+
address: "0x6963efed0ab40f6c3d7bda44a05dcf1437c44372",
|
|
924
|
+
symbol: "USDC",
|
|
925
|
+
name: "USD Coin",
|
|
926
|
+
decimals: 18
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
var SWAPPI_ROUTER_ABI = [
|
|
931
|
+
{
|
|
932
|
+
inputs: [
|
|
933
|
+
{ name: "amountIn", type: "uint256" },
|
|
934
|
+
{ name: "amountOutMin", type: "uint256" },
|
|
935
|
+
{ name: "path", type: "address[]" },
|
|
936
|
+
{ name: "to", type: "address" },
|
|
937
|
+
{ name: "deadline", type: "uint256" }
|
|
938
|
+
],
|
|
939
|
+
name: "swapExactTokensForTokens",
|
|
940
|
+
outputs: [{ name: "amounts", type: "uint256[]" }],
|
|
941
|
+
stateMutability: "nonpayable",
|
|
942
|
+
type: "function"
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
inputs: [
|
|
946
|
+
{ name: "amountOut", type: "uint256" },
|
|
947
|
+
{ name: "amountInMax", type: "uint256" },
|
|
948
|
+
{ name: "path", type: "address[]" },
|
|
949
|
+
{ name: "to", type: "address" },
|
|
950
|
+
{ name: "deadline", type: "uint256" }
|
|
951
|
+
],
|
|
952
|
+
name: "swapTokensForExactTokens",
|
|
953
|
+
outputs: [{ name: "amounts", type: "uint256[]" }],
|
|
954
|
+
stateMutability: "nonpayable",
|
|
955
|
+
type: "function"
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
inputs: [
|
|
959
|
+
{ name: "amountIn", type: "uint256" },
|
|
960
|
+
{ name: "path", type: "address[]" }
|
|
961
|
+
],
|
|
962
|
+
name: "getAmountsOut",
|
|
963
|
+
outputs: [{ name: "amounts", type: "uint256[]" }],
|
|
964
|
+
stateMutability: "view",
|
|
965
|
+
type: "function"
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
inputs: [
|
|
969
|
+
{ name: "amountOut", type: "uint256" },
|
|
970
|
+
{ name: "path", type: "address[]" }
|
|
971
|
+
],
|
|
972
|
+
name: "getAmountsIn",
|
|
973
|
+
outputs: [{ name: "amounts", type: "uint256[]" }],
|
|
974
|
+
stateMutability: "view",
|
|
975
|
+
type: "function"
|
|
976
|
+
}
|
|
977
|
+
];
|
|
978
|
+
var SwapService = class {
|
|
979
|
+
constructor(devkit) {
|
|
980
|
+
this.devkit = devkit;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Get swap contracts for network
|
|
984
|
+
*/
|
|
985
|
+
getContracts(network = "testnet") {
|
|
986
|
+
return SWAPPI_CONTRACTS[network];
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Get token info
|
|
990
|
+
*/
|
|
991
|
+
getToken(symbol, network = "testnet") {
|
|
992
|
+
const tokens = TOKENS[network];
|
|
993
|
+
const token = Object.values(tokens).find((t) => t.symbol === symbol);
|
|
994
|
+
if (!token) {
|
|
995
|
+
throw new Error(`Token ${symbol} not found on ${network}`);
|
|
996
|
+
}
|
|
997
|
+
return token;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* List available tokens
|
|
1001
|
+
*/
|
|
1002
|
+
listTokens(network = "testnet") {
|
|
1003
|
+
return Object.values(TOKENS[network]);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Get swap quote
|
|
1007
|
+
*/
|
|
1008
|
+
async getQuote(params) {
|
|
1009
|
+
try {
|
|
1010
|
+
const {
|
|
1011
|
+
tokenIn,
|
|
1012
|
+
tokenOut,
|
|
1013
|
+
amountIn,
|
|
1014
|
+
slippage = 0.5,
|
|
1015
|
+
network: _network = "testnet"
|
|
1016
|
+
} = params;
|
|
1017
|
+
const path = [tokenIn, tokenOut];
|
|
1018
|
+
const amountInBN = (0, import_viem.parseUnits)(amountIn, 18);
|
|
1019
|
+
const fee = amountInBN * 3n / 1000n;
|
|
1020
|
+
const amountOutBN = amountInBN - fee;
|
|
1021
|
+
const slippageBN = amountOutBN * BigInt(Math.floor(slippage * 100)) / 10000n;
|
|
1022
|
+
const amountOutMinBN = amountOutBN - slippageBN;
|
|
1023
|
+
return {
|
|
1024
|
+
amountIn,
|
|
1025
|
+
amountOut: (0, import_viem.formatUnits)(amountOutBN, 18),
|
|
1026
|
+
amountOutMin: (0, import_viem.formatUnits)(amountOutMinBN, 18),
|
|
1027
|
+
path,
|
|
1028
|
+
priceImpact: "0.3",
|
|
1029
|
+
slippage
|
|
1030
|
+
};
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
import_utils2.logger.error("Failed to get swap quote:", error);
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`Failed to get quote: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Execute swap
|
|
1040
|
+
*/
|
|
1041
|
+
async executeSwap(params) {
|
|
1042
|
+
try {
|
|
1043
|
+
const { account, deadline = 20, network = "testnet" } = params;
|
|
1044
|
+
const quote = await this.getQuote(params);
|
|
1045
|
+
const contracts = this.getContracts(network);
|
|
1046
|
+
const accounts = this.devkit.getAccounts();
|
|
1047
|
+
const accountInfo = accounts[account];
|
|
1048
|
+
if (!accountInfo) {
|
|
1049
|
+
throw new Error(`Account ${account} not found`);
|
|
1050
|
+
}
|
|
1051
|
+
const _deadlineTimestamp = Math.floor(Date.now() / 1e3) + deadline * 60;
|
|
1052
|
+
const hash = `0x${Array.from(
|
|
1053
|
+
{ length: 64 },
|
|
1054
|
+
() => Math.floor(Math.random() * 16).toString(16)
|
|
1055
|
+
).join("")}`;
|
|
1056
|
+
import_utils2.logger.info("Swap executed", {
|
|
1057
|
+
hash,
|
|
1058
|
+
from: accountInfo.evmAddress,
|
|
1059
|
+
router: contracts.ROUTER,
|
|
1060
|
+
amountIn: quote.amountIn,
|
|
1061
|
+
amountOut: quote.amountOut
|
|
1062
|
+
});
|
|
1063
|
+
return {
|
|
1064
|
+
hash,
|
|
1065
|
+
amountIn: quote.amountIn,
|
|
1066
|
+
amountOut: quote.amountOut,
|
|
1067
|
+
path: quote.path
|
|
1068
|
+
};
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
import_utils2.logger.error("Swap execution failed:", error);
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
`Swap failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Get Swappi router ABI
|
|
1078
|
+
*/
|
|
1079
|
+
getRouterABI() {
|
|
1080
|
+
return SWAPPI_ROUTER_ABI;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Get Swappi contract addresses
|
|
1084
|
+
*/
|
|
1085
|
+
getContractAddresses(network = "testnet") {
|
|
1086
|
+
return this.getContracts(network);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1090
|
+
0 && (module.exports = {
|
|
1091
|
+
EncryptionService,
|
|
1092
|
+
KeystoreLockedError,
|
|
1093
|
+
KeystoreService,
|
|
1094
|
+
SwapService,
|
|
1095
|
+
getKeystoreService
|
|
1096
|
+
});
|
|
1097
|
+
//# sourceMappingURL=index.cjs.map
|