@agentlayer.tech/wallet 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.
Files changed (96) hide show
  1. package/.openclaw/AGENTS.md +98 -0
  2. package/.openclaw/extensions/agent-wallet/README.md +127 -0
  3. package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
  5. package/.openclaw/extensions/agent-wallet/package.json +11 -0
  6. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
  7. package/CHANGELOG.md +42 -0
  8. package/LICENSE +104 -0
  9. package/README.md +332 -0
  10. package/RELEASING.md +204 -0
  11. package/agent-wallet/.env.example +62 -0
  12. package/agent-wallet/AGENTS.md +129 -0
  13. package/agent-wallet/README.md +527 -0
  14. package/agent-wallet/agent_wallet/__init__.py +11 -0
  15. package/agent-wallet/agent_wallet/approval.py +161 -0
  16. package/agent-wallet/agent_wallet/bootstrap.py +178 -0
  17. package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
  18. package/agent-wallet/agent_wallet/config.py +382 -0
  19. package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
  20. package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
  21. package/agent-wallet/agent_wallet/exceptions.py +9 -0
  22. package/agent-wallet/agent_wallet/file_ops.py +34 -0
  23. package/agent-wallet/agent_wallet/http_client.py +25 -0
  24. package/agent-wallet/agent_wallet/models.py +66 -0
  25. package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
  26. package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
  27. package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
  28. package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
  29. package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
  30. package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
  31. package/agent-wallet/agent_wallet/providers/bags.py +259 -0
  32. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
  33. package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
  34. package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
  35. package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
  36. package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
  37. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
  38. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
  39. package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
  40. package/agent-wallet/agent_wallet/solana_stake.py +103 -0
  41. package/agent-wallet/agent_wallet/solana_tx.py +93 -0
  42. package/agent-wallet/agent_wallet/spending_limits.py +101 -0
  43. package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
  44. package/agent-wallet/agent_wallet/user_wallets.py +355 -0
  45. package/agent-wallet/agent_wallet/validation.py +31 -0
  46. package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
  47. package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
  48. package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
  49. package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
  50. package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
  51. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
  52. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
  53. package/agent-wallet/examples/bootstrap_wallet.py +21 -0
  54. package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
  55. package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
  56. package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
  57. package/agent-wallet/openclaw.plugin.json +138 -0
  58. package/agent-wallet/pyproject.toml +31 -0
  59. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
  60. package/agent-wallet/scripts/build_release_bundle.py +188 -0
  61. package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
  62. package/agent-wallet/scripts/install_agent_wallet.py +505 -0
  63. package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
  64. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
  65. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
  66. package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
  67. package/agent-wallet/scripts/security_utils.py +37 -0
  68. package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
  69. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
  70. package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
  71. package/bin/openclaw-agent-wallet.mjs +487 -0
  72. package/install-from-github.sh +134 -0
  73. package/package.json +61 -0
  74. package/setup.sh +40 -0
  75. package/wdk-btc-wallet/README.md +325 -0
  76. package/wdk-btc-wallet/bootstrap.sh +22 -0
  77. package/wdk-btc-wallet/package-lock.json +1839 -0
  78. package/wdk-btc-wallet/package.json +18 -0
  79. package/wdk-btc-wallet/run-local.sh +21 -0
  80. package/wdk-btc-wallet/src/config.js +160 -0
  81. package/wdk-btc-wallet/src/json.js +35 -0
  82. package/wdk-btc-wallet/src/local_vault.js +432 -0
  83. package/wdk-btc-wallet/src/network_state.js +84 -0
  84. package/wdk-btc-wallet/src/server.js +257 -0
  85. package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
  86. package/wdk-evm-wallet/README.md +183 -0
  87. package/wdk-evm-wallet/bootstrap.sh +8 -0
  88. package/wdk-evm-wallet/package-lock.json +2340 -0
  89. package/wdk-evm-wallet/package.json +23 -0
  90. package/wdk-evm-wallet/run-local.sh +12 -0
  91. package/wdk-evm-wallet/src/config.js +274 -0
  92. package/wdk-evm-wallet/src/json.js +35 -0
  93. package/wdk-evm-wallet/src/local_vault.js +430 -0
  94. package/wdk-evm-wallet/src/network_state.js +92 -0
  95. package/wdk-evm-wallet/src/server.js +575 -0
  96. package/wdk-evm-wallet/src/wdk_evm_wallet.js +4981 -0
@@ -0,0 +1,430 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ import WDK from "@tetherto/wdk";
7
+
8
+ const scryptAsync = promisify(crypto.scrypt);
9
+
10
+ const REGISTRY_FILE = "registry.json";
11
+ const WALLETS_DIR = "wallets";
12
+ const VAULT_VERSION = 1;
13
+
14
+ function assertNonEmptyString(value, fieldName) {
15
+ if (typeof value !== "string" || !value.trim()) {
16
+ throw new Error(`${fieldName} is required.`);
17
+ }
18
+ return value.trim();
19
+ }
20
+
21
+ function assertPositiveInteger(value, fieldName) {
22
+ const parsed = Number(value);
23
+ if (!Number.isInteger(parsed) || parsed <= 0) {
24
+ throw new Error(`${fieldName} must be a positive integer.`);
25
+ }
26
+ return parsed;
27
+ }
28
+
29
+ function assertNonNegativeInteger(value, fieldName) {
30
+ const parsed = Number(value);
31
+ if (!Number.isInteger(parsed) || parsed < 0) {
32
+ throw new Error(`${fieldName} must be a non-negative integer.`);
33
+ }
34
+ return parsed;
35
+ }
36
+
37
+ function sanitizeLabel(label) {
38
+ const normalized = String(label ?? "").trim();
39
+ return normalized || "EVM Wallet";
40
+ }
41
+
42
+ function assertValidNetwork(network, fieldName = "network") {
43
+ const normalized = assertNonEmptyString(network, fieldName).toLowerCase();
44
+ if (!["ethereum", "sepolia", "base", "base-sepolia"].includes(normalized)) {
45
+ throw new Error(`${fieldName} must be one of: ethereum, sepolia, base, base-sepolia.`);
46
+ }
47
+ return normalized;
48
+ }
49
+
50
+ async function deriveKey(password, salt) {
51
+ return scryptAsync(password, salt, 32, {
52
+ N: 1 << 15,
53
+ r: 8,
54
+ p: 1,
55
+ maxmem: 64 * 1024 * 1024,
56
+ });
57
+ }
58
+
59
+ async function encryptSeedPhrase({ seedPhrase, password, walletId }) {
60
+ const salt = crypto.randomBytes(16);
61
+ const iv = crypto.randomBytes(12);
62
+ const key = await deriveKey(password, salt);
63
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
64
+ cipher.setAAD(Buffer.from(`wdk-evm-wallet:${walletId}:v${VAULT_VERSION}`, "utf8"));
65
+ const ciphertext = Buffer.concat([
66
+ cipher.update(Buffer.from(seedPhrase, "utf8")),
67
+ cipher.final(),
68
+ ]);
69
+ const tag = cipher.getAuthTag();
70
+ return {
71
+ version: VAULT_VERSION,
72
+ kdf: {
73
+ name: "scrypt",
74
+ salt: salt.toString("base64"),
75
+ N: 1 << 15,
76
+ r: 8,
77
+ p: 1,
78
+ },
79
+ cipher: {
80
+ name: "aes-256-gcm",
81
+ iv: iv.toString("base64"),
82
+ tag: tag.toString("base64"),
83
+ },
84
+ ciphertext: ciphertext.toString("base64"),
85
+ };
86
+ }
87
+
88
+ async function decryptSeedPhrase({ encrypted, password, walletId }) {
89
+ const salt = Buffer.from(encrypted.kdf.salt, "base64");
90
+ const iv = Buffer.from(encrypted.cipher.iv, "base64");
91
+ const tag = Buffer.from(encrypted.cipher.tag, "base64");
92
+ const ciphertext = Buffer.from(encrypted.ciphertext, "base64");
93
+ const key = await deriveKey(password, salt);
94
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
95
+ decipher.setAAD(Buffer.from(`wdk-evm-wallet:${walletId}:v${VAULT_VERSION}`, "utf8"));
96
+ decipher.setAuthTag(tag);
97
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
98
+ return plaintext.toString("utf8");
99
+ }
100
+
101
+ async function decryptSeedPhraseWithPasswordCheck(args) {
102
+ try {
103
+ return await decryptSeedPhrase(args);
104
+ } catch (error) {
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ if (
107
+ message.includes("authenticate data") ||
108
+ message.includes("unable to authenticate") ||
109
+ message.includes("Unsupported state")
110
+ ) {
111
+ throw new Error("Invalid password.");
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ export class LocalEvmVault {
118
+ constructor(config) {
119
+ this.config = config;
120
+ this._unlocked = new Map();
121
+ }
122
+
123
+ async createWallet({
124
+ label = "",
125
+ password,
126
+ words = 12,
127
+ revealSeedPhrase = false,
128
+ network,
129
+ }) {
130
+ const count = assertPositiveInteger(words, "words");
131
+ if (count !== 12) {
132
+ throw new Error("Only 12-word wallet creation is currently supported.");
133
+ }
134
+ const seedPhrase = WDK.getRandomSeedPhrase();
135
+ const wallet = await this.#storeWallet({
136
+ label,
137
+ password,
138
+ seedPhrase,
139
+ source: "created",
140
+ network,
141
+ });
142
+ await this.unlockWallet({ walletId: wallet.walletId, password, timeoutSeconds: 0 });
143
+ return {
144
+ ...wallet,
145
+ unlocked: true,
146
+ unlockExpiresAt: null,
147
+ ...(revealSeedPhrase ? { seedPhrase } : {}),
148
+ };
149
+ }
150
+
151
+ async importWallet({ label = "", password, seedPhrase, network }) {
152
+ const mnemonic = assertNonEmptyString(seedPhrase, "seedPhrase");
153
+ if (!WDK.isValidSeed(mnemonic)) {
154
+ throw new Error("seedPhrase must be a valid BIP-39 seed phrase.");
155
+ }
156
+ const wallet = await this.#storeWallet({
157
+ label,
158
+ password,
159
+ seedPhrase: mnemonic,
160
+ source: "imported",
161
+ network,
162
+ });
163
+ await this.unlockWallet({ walletId: wallet.walletId, password, timeoutSeconds: 0 });
164
+ return {
165
+ ...wallet,
166
+ unlocked: true,
167
+ unlockExpiresAt: null,
168
+ };
169
+ }
170
+
171
+ async listWallets() {
172
+ this.#sweepExpiredUnlocked();
173
+ const registry = await this.#loadRegistry();
174
+ return registry.wallets.map((wallet) => {
175
+ const unlocked = this._unlocked.get(wallet.walletId);
176
+ return {
177
+ ...wallet,
178
+ unlocked: Boolean(unlocked),
179
+ unlockExpiresAt: unlocked ? unlocked.expiresAt : null,
180
+ };
181
+ });
182
+ }
183
+
184
+ async getWallet({ walletId }) {
185
+ this.#sweepExpiredUnlocked();
186
+ const wallet = await this.#getWalletMetadata(assertNonEmptyString(walletId, "walletId"));
187
+ const unlocked = this._unlocked.get(wallet.walletId);
188
+ return {
189
+ ...wallet,
190
+ unlocked: Boolean(unlocked),
191
+ unlockExpiresAt: unlocked ? unlocked.expiresAt : null,
192
+ };
193
+ }
194
+
195
+ async unlockWallet({ walletId, password, timeoutSeconds }) {
196
+ const metadata = await this.#getWalletMetadata(assertNonEmptyString(walletId, "walletId"));
197
+ const encrypted = await this.#loadEncryptedWallet(walletId);
198
+ const secret = await decryptSeedPhraseWithPasswordCheck({
199
+ encrypted,
200
+ password: assertNonEmptyString(password, "password"),
201
+ walletId,
202
+ });
203
+ if (!WDK.isValidSeed(secret)) {
204
+ throw new Error("Decrypted wallet seed phrase is invalid.");
205
+ }
206
+ const ttl =
207
+ timeoutSeconds === undefined || timeoutSeconds === null
208
+ ? this.config.unlockTimeoutSeconds
209
+ : assertNonNegativeInteger(timeoutSeconds, "timeoutSeconds");
210
+ const expiresAt = ttl === 0 ? null : new Date(Date.now() + ttl * 1000).toISOString();
211
+ this._unlocked.set(walletId, {
212
+ seedPhrase: secret,
213
+ expiresAt,
214
+ });
215
+ return {
216
+ walletId,
217
+ label: metadata.label,
218
+ unlocked: true,
219
+ unlockExpiresAt: expiresAt,
220
+ };
221
+ }
222
+
223
+ async lockWallet({ walletId }) {
224
+ this._unlocked.delete(assertNonEmptyString(walletId, "walletId"));
225
+ return {
226
+ walletId,
227
+ unlocked: false,
228
+ };
229
+ }
230
+
231
+ async revealSeedPhrase({ walletId, password }) {
232
+ const id = assertNonEmptyString(walletId, "walletId");
233
+ const metadata = await this.#getWalletMetadata(id);
234
+ const encrypted = await this.#loadEncryptedWallet(id);
235
+ const seedPhrase = await decryptSeedPhraseWithPasswordCheck({
236
+ encrypted,
237
+ password: assertNonEmptyString(password, "password"),
238
+ walletId: id,
239
+ });
240
+ if (!WDK.isValidSeed(seedPhrase)) {
241
+ throw new Error("Decrypted wallet seed phrase is invalid.");
242
+ }
243
+ return {
244
+ walletId: id,
245
+ label: metadata.label,
246
+ seedPhrase,
247
+ };
248
+ }
249
+
250
+ async changePassword({ walletId, currentPassword, newPassword }) {
251
+ const id = assertNonEmptyString(walletId, "walletId");
252
+ const safeCurrentPassword = assertNonEmptyString(currentPassword, "currentPassword");
253
+ const safeNewPassword = assertNonEmptyString(newPassword, "newPassword");
254
+ const metadata = await this.#getWalletMetadata(id);
255
+ const encrypted = await this.#loadEncryptedWallet(id);
256
+ const seedPhrase = await decryptSeedPhraseWithPasswordCheck({
257
+ encrypted,
258
+ password: safeCurrentPassword,
259
+ walletId: id,
260
+ });
261
+ if (!WDK.isValidSeed(seedPhrase)) {
262
+ throw new Error("Decrypted wallet seed phrase is invalid.");
263
+ }
264
+ const reencrypted = await encryptSeedPhrase({
265
+ seedPhrase,
266
+ password: safeNewPassword,
267
+ walletId: id,
268
+ });
269
+ await fs.writeFile(this.#walletFilePath(id), JSON.stringify(reencrypted, null, 2), {
270
+ encoding: "utf8",
271
+ mode: 0o600,
272
+ });
273
+
274
+ const registry = await this.#loadRegistry();
275
+ const index = registry.wallets.findIndex((wallet) => wallet.walletId === id);
276
+ if (index === -1) {
277
+ throw new Error(`Unknown walletId: ${id}`);
278
+ }
279
+ const updatedAt = new Date().toISOString();
280
+ registry.wallets[index] = {
281
+ ...registry.wallets[index],
282
+ updatedAt,
283
+ };
284
+ await this.#saveRegistry(registry);
285
+
286
+ const unlocked = this._unlocked.get(id);
287
+ if (unlocked) {
288
+ this._unlocked.set(id, {
289
+ seedPhrase,
290
+ expiresAt: unlocked.expiresAt,
291
+ });
292
+ }
293
+
294
+ return {
295
+ walletId: id,
296
+ label: metadata.label,
297
+ passwordChanged: true,
298
+ updatedAt,
299
+ unlocked: Boolean(this._unlocked.get(id)),
300
+ unlockExpiresAt: this._unlocked.get(id)?.expiresAt ?? null,
301
+ };
302
+ }
303
+
304
+ async resolveSeedPhrase({ walletId, seedPhrase }) {
305
+ if (typeof seedPhrase === "string" && seedPhrase.trim()) {
306
+ if (!WDK.isValidSeed(seedPhrase.trim())) {
307
+ throw new Error("seedPhrase must be a valid BIP-39 seed phrase.");
308
+ }
309
+ return {
310
+ seedPhrase: seedPhrase.trim(),
311
+ source: "request",
312
+ walletId: null,
313
+ };
314
+ }
315
+ const id = assertNonEmptyString(walletId, "walletId");
316
+ this.#sweepExpiredUnlocked();
317
+ const unlocked = this._unlocked.get(id);
318
+ if (!unlocked) {
319
+ throw new Error("Wallet is locked. Unlock it first or provide seedPhrase explicitly.");
320
+ }
321
+ return {
322
+ seedPhrase: unlocked.seedPhrase,
323
+ source: "local-vault",
324
+ walletId: id,
325
+ unlockExpiresAt: unlocked.expiresAt,
326
+ };
327
+ }
328
+
329
+ async #storeWallet({ label, password, seedPhrase, source, network }) {
330
+ const safePassword = assertNonEmptyString(password, "password");
331
+ await this.#ensureLayout();
332
+
333
+ const walletId = crypto.randomUUID();
334
+ const now = new Date().toISOString();
335
+ const encrypted = await encryptSeedPhrase({
336
+ seedPhrase,
337
+ password: safePassword,
338
+ walletId,
339
+ });
340
+ const entry = {
341
+ walletId,
342
+ label: sanitizeLabel(label),
343
+ createdAt: now,
344
+ updatedAt: now,
345
+ network: assertValidNetwork(network ?? this.config.network, "network"),
346
+ source,
347
+ };
348
+
349
+ await fs.writeFile(this.#walletFilePath(walletId), JSON.stringify(encrypted, null, 2), {
350
+ encoding: "utf8",
351
+ mode: 0o600,
352
+ });
353
+ const registry = await this.#loadRegistry();
354
+ registry.wallets.push(entry);
355
+ await this.#saveRegistry(registry);
356
+
357
+ return {
358
+ walletId,
359
+ label: entry.label,
360
+ createdAt: entry.createdAt,
361
+ updatedAt: entry.updatedAt,
362
+ network: entry.network,
363
+ source: entry.source,
364
+ };
365
+ }
366
+
367
+ async #getWalletMetadata(walletId) {
368
+ const registry = await this.#loadRegistry();
369
+ const wallet = registry.wallets.find((item) => item.walletId === walletId);
370
+ if (!wallet) {
371
+ throw new Error(`Unknown walletId: ${walletId}`);
372
+ }
373
+ return wallet;
374
+ }
375
+
376
+ async #loadEncryptedWallet(walletId) {
377
+ const raw = await fs.readFile(this.#walletFilePath(walletId), "utf8");
378
+ return JSON.parse(raw);
379
+ }
380
+
381
+ async #ensureLayout() {
382
+ await fs.mkdir(this.config.dataDir, { recursive: true, mode: 0o700 });
383
+ await fs.mkdir(path.join(this.config.dataDir, WALLETS_DIR), {
384
+ recursive: true,
385
+ mode: 0o700,
386
+ });
387
+ try {
388
+ await fs.access(this.#registryPath());
389
+ } catch {
390
+ await this.#saveRegistry({
391
+ version: VAULT_VERSION,
392
+ wallets: [],
393
+ });
394
+ }
395
+ }
396
+
397
+ async #loadRegistry() {
398
+ await this.#ensureLayout();
399
+ const raw = await fs.readFile(this.#registryPath(), "utf8");
400
+ const parsed = JSON.parse(raw);
401
+ if (!Array.isArray(parsed.wallets)) {
402
+ throw new Error("Vault registry is invalid.");
403
+ }
404
+ return parsed;
405
+ }
406
+
407
+ async #saveRegistry(registry) {
408
+ await fs.writeFile(this.#registryPath(), JSON.stringify(registry, null, 2), {
409
+ encoding: "utf8",
410
+ mode: 0o600,
411
+ });
412
+ }
413
+
414
+ #walletFilePath(walletId) {
415
+ return path.join(this.config.dataDir, WALLETS_DIR, `${walletId}.json`);
416
+ }
417
+
418
+ #registryPath() {
419
+ return path.join(this.config.dataDir, REGISTRY_FILE);
420
+ }
421
+
422
+ #sweepExpiredUnlocked() {
423
+ const now = Date.now();
424
+ for (const [walletId, state] of this._unlocked.entries()) {
425
+ if (state.expiresAt && Date.parse(state.expiresAt) <= now) {
426
+ this._unlocked.delete(walletId);
427
+ }
428
+ }
429
+ }
430
+ }
@@ -0,0 +1,92 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const NETWORK_FILE = "network.json";
5
+
6
+ function assertValidNetwork(network, fieldName = "network") {
7
+ const normalized = String(network ?? "").trim().toLowerCase();
8
+ const aliases = {
9
+ mainnet: "ethereum",
10
+ eth: "ethereum",
11
+ "eth-mainnet": "ethereum",
12
+ "base-mainnet": "base",
13
+ base_sepolia: "base-sepolia",
14
+ };
15
+ const effective = aliases[normalized] || normalized;
16
+ if (!["ethereum", "sepolia", "base", "base-sepolia"].includes(effective)) {
17
+ throw new Error(`${fieldName} must be one of: ethereum, sepolia, base, base-sepolia.`);
18
+ }
19
+ return effective;
20
+ }
21
+
22
+ export class EvmNetworkState {
23
+ constructor(config) {
24
+ this.config = config;
25
+ }
26
+
27
+ async getActiveNetwork() {
28
+ const state = await this.#loadState();
29
+ return state.network;
30
+ }
31
+
32
+ async getNetworkInfo(networkOverride = undefined) {
33
+ const activeNetwork = networkOverride
34
+ ? assertValidNetwork(networkOverride)
35
+ : await this.getActiveNetwork();
36
+ return {
37
+ activeNetwork,
38
+ profiles: this.config.networkProfiles,
39
+ selectedProfile: this.config.networkProfiles[activeNetwork],
40
+ };
41
+ }
42
+
43
+ async setActiveNetwork({ network }) {
44
+ const nextNetwork = assertValidNetwork(network);
45
+ await this.#ensureLayout();
46
+ await fs.writeFile(this.#statePath(), JSON.stringify({ network: nextNetwork }, null, 2), {
47
+ encoding: "utf8",
48
+ mode: 0o600,
49
+ });
50
+ return this.getNetworkInfo(nextNetwork);
51
+ }
52
+
53
+ async resolveRuntimeConfig(networkOverride = undefined) {
54
+ const activeNetwork = networkOverride
55
+ ? assertValidNetwork(networkOverride, "network")
56
+ : await this.getActiveNetwork();
57
+ const profile = this.config.networkProfiles[activeNetwork];
58
+ return {
59
+ ...this.config,
60
+ network: activeNetwork,
61
+ chainId: profile.chainId,
62
+ providerUrl: profile.providerUrl,
63
+ nativeSymbol: profile.nativeSymbol,
64
+ };
65
+ }
66
+
67
+ async #ensureLayout() {
68
+ await fs.mkdir(this.config.dataDir, { recursive: true, mode: 0o700 });
69
+ try {
70
+ await fs.access(this.#statePath());
71
+ } catch {
72
+ await fs.writeFile(
73
+ this.#statePath(),
74
+ JSON.stringify({ network: this.config.network }, null, 2),
75
+ { encoding: "utf8", mode: 0o600 }
76
+ );
77
+ }
78
+ }
79
+
80
+ async #loadState() {
81
+ await this.#ensureLayout();
82
+ const raw = await fs.readFile(this.#statePath(), "utf8");
83
+ const parsed = JSON.parse(raw);
84
+ return {
85
+ network: assertValidNetwork(parsed.network),
86
+ };
87
+ }
88
+
89
+ #statePath() {
90
+ return path.join(this.config.dataDir, NETWORK_FILE);
91
+ }
92
+ }