@agentlayer.tech/wallet 0.1.103 → 0.1.106

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.
@@ -1,7 +1,9 @@
1
1
  import "dotenv/config";
2
2
 
3
3
  import crypto from "node:crypto";
4
+ import fs from "node:fs";
4
5
  import { createServer } from "node:http";
6
+ import net from "node:net";
5
7
 
6
8
  import { loadConfig } from "./config.js";
7
9
  import { readJsonBody, sendJson } from "./json.js";
@@ -10,732 +12,806 @@ import { EvmNetworkState } from "./network_state.js";
10
12
  import { createShutdownCoordinator, withTrackedRequest } from "./shutdown.js";
11
13
  import { WdkEvmWalletService } from "./wdk_evm_wallet.js";
12
14
 
13
- const config = loadConfig();
14
- const service = new WdkEvmWalletService(config);
15
- const vault = new LocalEvmVault(config);
16
- const networkState = new EvmNetworkState(config);
17
-
18
- function normalizeErrorCode(errorCode, pathname, message) {
19
- const code = String(errorCode || "").trim().toLowerCase();
20
- const lower = String(message || "").toLowerCase();
21
- const isTokenPath = pathname.includes("/token");
22
- const isTokenReadPath =
23
- pathname.includes("/token-balance/") || pathname.includes("/token-metadata/");
24
-
25
- if (code === "insufficient_funds") {
26
- return "insufficient_funds";
27
- }
28
- if (code === "network_unavailable") {
29
- return "network_unavailable";
30
- }
31
- if (
32
- code === "swap_quote_changed" ||
33
- code === "swap_simulation_failed" ||
34
- code === "swap_approval_required" ||
35
- code === "swap_approval_failed" ||
36
- code === "swap_approval_timeout" ||
37
- code === "swap_cleanup_failed" ||
38
- code === "aave_quote_changed" ||
39
- code === "aave_approval_required" ||
40
- code === "aave_fee_unavailable" ||
41
- code === "aave_operation_reverted" ||
42
- code === "aave_operation_confirmation_timeout" ||
43
- code === "aave_cleanup_failed" ||
44
- code === "morpho_api_failed" ||
45
- code === "morpho_quote_changed" ||
46
- code === "morpho_requirements_unresolved" ||
47
- code === "morpho_fee_unavailable" ||
48
- code === "morpho_cleanup_failed" ||
49
- code === "defi_gas_estimate_unavailable" ||
50
- code === "lido_operation_reverted" ||
51
- code === "lido_operation_confirmation_timeout" ||
52
- code === "lido_withdrawal_reverted" ||
53
- code === "lido_withdrawal_confirmation_timeout" ||
54
- code === "swap_reverted" ||
55
- code === "swap_confirmation_timeout" ||
56
- code === "token_transfer_failed" ||
57
- code === "fee_limit_exceeded" ||
58
- code === "token_read_failed" ||
59
- code === "uniswap_api_key_missing" ||
60
- code === "uniswap_unsupported_route" ||
61
- code === "uniswap_unexpected_router"
62
- ) {
63
- return code;
64
- }
65
- if (
66
- code === "call_exception" ||
67
- code === "bad_data" ||
68
- code === "execution_reverted" ||
69
- code === "contract_not_found"
70
- ) {
71
- if (isTokenReadPath) {
15
+ export async function startServer(config = loadConfig()) {
16
+ const service = new WdkEvmWalletService(config);
17
+ const vault = new LocalEvmVault(config);
18
+ const networkState = new EvmNetworkState(config);
19
+
20
+ function normalizeErrorCode(errorCode, pathname, message) {
21
+ const code = String(errorCode || "").trim().toLowerCase();
22
+ const lower = String(message || "").toLowerCase();
23
+ const isTokenPath = pathname.includes("/token");
24
+ const isTokenReadPath =
25
+ pathname.includes("/token-balance/") || pathname.includes("/token-metadata/");
26
+
27
+ if (code === "insufficient_funds") {
28
+ return "insufficient_funds";
29
+ }
30
+ if (code === "network_unavailable") {
31
+ return "network_unavailable";
32
+ }
33
+ if (
34
+ code === "swap_quote_changed" ||
35
+ code === "swap_simulation_failed" ||
36
+ code === "swap_approval_required" ||
37
+ code === "swap_approval_failed" ||
38
+ code === "swap_approval_timeout" ||
39
+ code === "swap_cleanup_failed" ||
40
+ code === "aave_quote_changed" ||
41
+ code === "aave_approval_required" ||
42
+ code === "aave_fee_unavailable" ||
43
+ code === "aave_operation_reverted" ||
44
+ code === "aave_operation_confirmation_timeout" ||
45
+ code === "aave_cleanup_failed" ||
46
+ code === "morpho_api_failed" ||
47
+ code === "morpho_quote_changed" ||
48
+ code === "morpho_requirements_unresolved" ||
49
+ code === "morpho_fee_unavailable" ||
50
+ code === "morpho_cleanup_failed" ||
51
+ code === "defi_gas_estimate_unavailable" ||
52
+ code === "lido_operation_reverted" ||
53
+ code === "lido_operation_confirmation_timeout" ||
54
+ code === "lido_withdrawal_reverted" ||
55
+ code === "lido_withdrawal_confirmation_timeout" ||
56
+ code === "swap_reverted" ||
57
+ code === "swap_confirmation_timeout" ||
58
+ code === "token_transfer_failed" ||
59
+ code === "fee_limit_exceeded" ||
60
+ code === "token_read_failed" ||
61
+ code === "uniswap_api_key_missing" ||
62
+ code === "uniswap_unsupported_route" ||
63
+ code === "uniswap_unexpected_router"
64
+ ) {
65
+ return code;
66
+ }
67
+ if (
68
+ code === "call_exception" ||
69
+ code === "bad_data" ||
70
+ code === "execution_reverted" ||
71
+ code === "contract_not_found"
72
+ ) {
73
+ if (isTokenReadPath) {
74
+ return "token_not_found";
75
+ }
76
+ }
77
+ if (
78
+ code === "econnrefused" ||
79
+ code === "enotfound" ||
80
+ code === "etimedout" ||
81
+ code === "fetch_failed"
82
+ ) {
83
+ return "network_unavailable";
84
+ }
85
+
86
+ if (lower.includes("wallet is locked")) {
87
+ return "wallet_locked";
88
+ }
89
+ if (lower.includes("insufficient funds")) {
90
+ return "insufficient_funds";
91
+ }
92
+ if (
93
+ lower.includes("recipient must be a valid") ||
94
+ lower.includes("recipient must not be the zero address") ||
95
+ lower.includes("to must be a valid") ||
96
+ lower.includes("to must not be the zero address") ||
97
+ lower.includes("invalid address") ||
98
+ lower.includes("bad address checksum")
99
+ ) {
100
+ return "recipient_invalid";
101
+ }
102
+ if (
103
+ isTokenReadPath &&
104
+ (lower.includes("missing revert data") ||
105
+ lower.includes("call exception") ||
106
+ lower.includes("could not decode result data") ||
107
+ lower.includes("no contract code") ||
108
+ lower.includes("execution reverted"))
109
+ ) {
72
110
  return "token_not_found";
73
111
  }
74
- }
75
- if (
76
- code === "econnrefused" ||
77
- code === "enotfound" ||
78
- code === "etimedout" ||
79
- code === "fetch_failed"
80
- ) {
81
- return "network_unavailable";
82
- }
112
+ if (
113
+ lower.includes("rpc network unavailable") ||
114
+ lower.includes("rpc request failed") ||
115
+ lower.includes("rpc returned invalid json") ||
116
+ lower.includes("fetch failed") ||
117
+ lower.includes("network unavailable") ||
118
+ lower.includes("timeout")
119
+ ) {
120
+ return "network_unavailable";
121
+ }
122
+ if (lower.includes("unknown walletid")) {
123
+ return "wallet_not_found";
124
+ }
125
+ if (lower.includes("invalid password")) {
126
+ return "invalid_password";
127
+ }
83
128
 
84
- if (lower.includes("wallet is locked")) {
85
- return "wallet_locked";
86
- }
87
- if (lower.includes("insufficient funds")) {
88
- return "insufficient_funds";
89
- }
90
- if (
91
- lower.includes("recipient must be a valid") ||
92
- lower.includes("recipient must not be the zero address") ||
93
- lower.includes("to must be a valid") ||
94
- lower.includes("to must not be the zero address") ||
95
- lower.includes("invalid address") ||
96
- lower.includes("bad address checksum")
97
- ) {
98
- return "recipient_invalid";
129
+ return null;
99
130
  }
100
- if (
101
- isTokenReadPath &&
102
- (lower.includes("missing revert data") ||
103
- lower.includes("call exception") ||
104
- lower.includes("could not decode result data") ||
105
- lower.includes("no contract code") ||
106
- lower.includes("execution reverted"))
107
- ) {
108
- return "token_not_found";
109
- }
110
- if (
111
- lower.includes("rpc network unavailable") ||
112
- lower.includes("rpc request failed") ||
113
- lower.includes("rpc returned invalid json") ||
114
- lower.includes("fetch failed") ||
115
- lower.includes("network unavailable") ||
116
- lower.includes("timeout")
117
- ) {
118
- return "network_unavailable";
119
- }
120
- if (lower.includes("unknown walletid")) {
121
- return "wallet_not_found";
122
- }
123
- if (lower.includes("invalid password")) {
124
- return "invalid_password";
125
- }
126
-
127
- return null;
128
- }
129
131
 
130
- function errorStatusCode(errorCode, fallback = 400) {
131
- if (errorCode === "wallet_locked" || errorCode === "insufficient_funds") {
132
- return 409;
133
- }
134
- if (errorCode === "swap_quote_changed") {
135
- return 409;
136
- }
137
- if (errorCode === "aave_quote_changed") {
138
- return 409;
139
- }
140
- if (errorCode === "morpho_quote_changed") {
141
- return 409;
142
- }
143
- if (
144
- errorCode === "swap_simulation_failed" ||
145
- errorCode === "swap_approval_required" ||
146
- errorCode === "swap_approval_failed" ||
147
- errorCode === "swap_approval_timeout" ||
148
- errorCode === "swap_cleanup_failed" ||
149
- errorCode === "aave_approval_required" ||
150
- errorCode === "aave_fee_unavailable" ||
151
- errorCode === "aave_operation_reverted" ||
152
- errorCode === "aave_operation_confirmation_timeout" ||
153
- errorCode === "aave_cleanup_failed" ||
154
- errorCode === "morpho_api_failed" ||
155
- errorCode === "morpho_requirements_unresolved" ||
156
- errorCode === "morpho_fee_unavailable" ||
157
- errorCode === "morpho_cleanup_failed" ||
158
- errorCode === "defi_gas_estimate_unavailable" ||
159
- errorCode === "lido_operation_reverted" ||
160
- errorCode === "lido_operation_confirmation_timeout" ||
161
- errorCode === "lido_withdrawal_reverted" ||
162
- errorCode === "lido_withdrawal_confirmation_timeout" ||
163
- errorCode === "swap_reverted" ||
164
- errorCode === "swap_confirmation_timeout" ||
165
- errorCode === "token_transfer_failed" ||
166
- errorCode === "fee_limit_exceeded" ||
167
- errorCode === "uniswap_api_key_missing"
168
- ) {
169
- return 400;
170
- }
171
- if (errorCode === "uniswap_unsupported_route") {
172
- return 422;
173
- }
174
- if (errorCode === "uniswap_unexpected_router") {
175
- return 502;
132
+ function errorStatusCode(errorCode, fallback = 400) {
133
+ if (errorCode === "wallet_locked" || errorCode === "insufficient_funds") {
134
+ return 409;
135
+ }
136
+ if (errorCode === "swap_quote_changed") {
137
+ return 409;
138
+ }
139
+ if (errorCode === "aave_quote_changed") {
140
+ return 409;
141
+ }
142
+ if (errorCode === "morpho_quote_changed") {
143
+ return 409;
144
+ }
145
+ if (
146
+ errorCode === "swap_simulation_failed" ||
147
+ errorCode === "swap_approval_required" ||
148
+ errorCode === "swap_approval_failed" ||
149
+ errorCode === "swap_approval_timeout" ||
150
+ errorCode === "swap_cleanup_failed" ||
151
+ errorCode === "aave_approval_required" ||
152
+ errorCode === "aave_fee_unavailable" ||
153
+ errorCode === "aave_operation_reverted" ||
154
+ errorCode === "aave_operation_confirmation_timeout" ||
155
+ errorCode === "aave_cleanup_failed" ||
156
+ errorCode === "morpho_api_failed" ||
157
+ errorCode === "morpho_requirements_unresolved" ||
158
+ errorCode === "morpho_fee_unavailable" ||
159
+ errorCode === "morpho_cleanup_failed" ||
160
+ errorCode === "defi_gas_estimate_unavailable" ||
161
+ errorCode === "lido_operation_reverted" ||
162
+ errorCode === "lido_operation_confirmation_timeout" ||
163
+ errorCode === "lido_withdrawal_reverted" ||
164
+ errorCode === "lido_withdrawal_confirmation_timeout" ||
165
+ errorCode === "swap_reverted" ||
166
+ errorCode === "swap_confirmation_timeout" ||
167
+ errorCode === "token_transfer_failed" ||
168
+ errorCode === "fee_limit_exceeded" ||
169
+ errorCode === "uniswap_api_key_missing"
170
+ ) {
171
+ return 400;
172
+ }
173
+ if (errorCode === "uniswap_unsupported_route") {
174
+ return 422;
175
+ }
176
+ if (errorCode === "uniswap_unexpected_router") {
177
+ return 502;
178
+ }
179
+ if (errorCode === "token_read_failed") {
180
+ return 502;
181
+ }
182
+ if (errorCode === "network_unavailable") {
183
+ return 503;
184
+ }
185
+ if (errorCode === "wallet_not_found" || errorCode === "token_not_found") {
186
+ return 404;
187
+ }
188
+ return fallback;
176
189
  }
177
- if (errorCode === "token_read_failed") {
178
- return 502;
179
- }
180
- if (errorCode === "network_unavailable") {
181
- return 503;
182
- }
183
- if (errorCode === "wallet_not_found" || errorCode === "token_not_found") {
184
- return 404;
185
- }
186
- return fallback;
187
- }
188
190
 
189
- function sanitizeProviderUrl(value) {
190
- const raw = String(value || "").trim();
191
- if (!raw) {
192
- return raw;
193
- }
194
- try {
195
- const url = new URL(raw);
196
- if (url.searchParams.has("token")) {
197
- url.searchParams.set("token", "***");
191
+ function sanitizeProviderUrl(value) {
192
+ const raw = String(value || "").trim();
193
+ if (!raw) {
194
+ return raw;
195
+ }
196
+ try {
197
+ const url = new URL(raw);
198
+ if (url.searchParams.has("token")) {
199
+ url.searchParams.set("token", "***");
200
+ }
201
+ return url.toString();
202
+ } catch {
203
+ return raw;
198
204
  }
199
- return url.toString();
200
- } catch {
201
- return raw;
202
205
  }
203
- }
204
-
205
- function sanitizeProfiles(profiles = {}) {
206
- return Object.fromEntries(
207
- Object.entries(profiles).map(([network, profile]) => [
208
- network,
209
- {
210
- ...profile,
211
- providerUrl: sanitizeProviderUrl(profile?.providerUrl),
212
- },
213
- ])
214
- );
215
- }
216
-
217
- function toErrorResponse(error, pathname, fallbackStatus = 400) {
218
- const message = error instanceof Error ? error.message : String(error);
219
- const explicitCode =
220
- (typeof error?.errorCode === "string" && error.errorCode.trim()) ||
221
- (typeof error?.code === "string" && error.code.trim()) ||
222
- "";
223
- const errorCode = normalizeErrorCode(explicitCode, pathname, message);
224
- const details =
225
- error && typeof error === "object" && error.errorDetails && typeof error.errorDetails === "object"
226
- ? { ...error.errorDetails }
227
- : {};
228
- details.source = "wdk-evm-wallet";
229
- details.path = pathname;
230
- return {
231
- statusCode: errorStatusCode(errorCode, fallbackStatus),
232
- payload: {
233
- ok: false,
234
- error: message,
235
- ...(errorCode ? { error_code: errorCode } : {}),
236
- error_details: details,
237
- },
238
- };
239
- }
240
-
241
- function notFound(response) {
242
- sendJson(response, 404, { ok: false, error: "Not Found" });
243
- }
244
-
245
- function unauthorized(response) {
246
- response.setHeader("WWW-Authenticate", 'Bearer realm="wdk-evm-wallet"');
247
- sendJson(response, 401, { ok: false, error: "Unauthorized." });
248
- }
249
206
 
250
- function pathRequiresAuth(pathname) {
251
- return pathname !== "/health";
252
- }
253
-
254
- function isAuthorized(request) {
255
- const header = String(request.headers.authorization || "").trim();
256
- if (!header.startsWith("Bearer ")) {
257
- return false;
258
- }
259
- const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
260
- const expected = Buffer.from(String(config.authToken || ""), "utf8");
261
- if (provided.length === 0 || provided.length !== expected.length) {
262
- return false;
207
+ function sanitizeProfiles(profiles = {}) {
208
+ return Object.fromEntries(
209
+ Object.entries(profiles).map(([network, profile]) => [
210
+ network,
211
+ {
212
+ ...profile,
213
+ providerUrl: sanitizeProviderUrl(profile?.providerUrl),
214
+ },
215
+ ])
216
+ );
263
217
  }
264
- return crypto.timingSafeEqual(provided, expected);
265
- }
266
218
 
267
- async function withResolvedSeed(body = {}) {
268
- const resolved = await vault.resolveSeedPhrase({
269
- walletId: body.walletId,
270
- seedPhrase: body.seedPhrase,
271
- password: body.password,
272
- });
273
- return {
274
- ...body,
275
- seedPhrase: resolved.seedPhrase,
276
- walletId: resolved.walletId ?? body.walletId ?? null,
277
- credentialSource: resolved.source,
278
- unlockExpiresAt: resolved.unlockExpiresAt ?? null,
279
- };
280
- }
281
-
282
- async function withResolvedSeedOrAddress(body = {}) {
283
- const address = typeof body.address === "string" ? body.address.trim() : "";
284
- if (address) {
219
+ function toErrorResponse(error, pathname, fallbackStatus = 400) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ const explicitCode =
222
+ (typeof error?.errorCode === "string" && error.errorCode.trim()) ||
223
+ (typeof error?.code === "string" && error.code.trim()) ||
224
+ "";
225
+ const errorCode = normalizeErrorCode(explicitCode, pathname, message);
226
+ const details =
227
+ error && typeof error === "object" && error.errorDetails && typeof error.errorDetails === "object"
228
+ ? { ...error.errorDetails }
229
+ : {};
230
+ details.source = "wdk-evm-wallet";
231
+ details.path = pathname;
285
232
  return {
286
- ...body,
287
- address,
233
+ statusCode: errorStatusCode(errorCode, fallbackStatus),
234
+ payload: {
235
+ ok: false,
236
+ error: message,
237
+ ...(errorCode ? { error_code: errorCode } : {}),
238
+ error_details: details,
239
+ },
288
240
  };
289
241
  }
290
- return withResolvedSeed(body);
291
- }
292
242
 
293
- async function withResolvedNetwork(body = {}) {
294
- const runtimeConfig = await networkState.resolveRuntimeConfig(body.network);
295
- return {
296
- ...body,
297
- network: runtimeConfig.network,
298
- };
299
- }
300
-
301
- async function handleRequest(request, response) {
302
- try {
303
- const url = new URL(request.url || "/", "http://localhost");
304
- const { method = "GET" } = request;
305
-
306
- if (pathRequiresAuth(url.pathname) && !isAuthorized(request)) {
307
- return unauthorized(response);
308
- }
309
-
310
- if (method === "GET" && url.pathname === "/health") {
311
- const runtimeConfig = await networkState.resolveRuntimeConfig();
312
- const networkInfo = await networkState.getNetworkInfo();
313
- return sendJson(response, 200, {
314
- ok: true,
315
- service: "wdk-evm-wallet",
316
- version: config.version,
317
- wallet: "evm",
318
- network: runtimeConfig.network,
319
- chainId: runtimeConfig.chainId,
320
- host: config.host,
321
- dataDir: config.dataDir,
322
- instanceId: config.instanceId,
323
- pid: process.pid,
324
- authRequired: config.authRequired,
325
- unlockTimeoutSeconds: config.unlockTimeoutSeconds,
326
- availableNetworks: Object.keys(config.networkProfiles),
327
- provider: sanitizeProviderUrl(runtimeConfig.providerUrl),
328
- networkProfiles: sanitizeProfiles(networkInfo.profiles),
329
- source: "wdk",
330
- });
331
- }
243
+ function notFound(response) {
244
+ sendJson(response, 404, { ok: false, error: "Not Found" });
245
+ }
332
246
 
333
- if (method === "POST" && url.pathname === "/v1/evm/seed-phrase/generate") {
334
- const body = await readJsonBody(request);
335
- return sendJson(response, 200, {
336
- ok: true,
337
- data: service.generateSeedPhrase(body.words ?? 12),
338
- });
339
- }
247
+ function unauthorized(response) {
248
+ response.setHeader("WWW-Authenticate", 'Bearer realm="wdk-evm-wallet"');
249
+ sendJson(response, 401, { ok: false, error: "Unauthorized." });
250
+ }
340
251
 
341
- if (method === "GET" && url.pathname === "/v1/evm/wallets") {
342
- const activeNetwork = await networkState.getActiveNetwork();
343
- return sendJson(response, 200, {
344
- ok: true,
345
- data: (await vault.listWallets()).map((wallet) => ({
346
- ...wallet,
347
- activeNetwork,
348
- })),
349
- });
350
- }
252
+ function pathRequiresAuth(pathname) {
253
+ return pathname !== "/health";
254
+ }
351
255
 
352
- if (method === "GET" && url.pathname === "/v1/evm/network") {
353
- return sendJson(response, 200, {
354
- ok: true,
355
- data: await networkState.getNetworkInfo(),
356
- });
256
+ function isAuthorized(request) {
257
+ const header = String(request.headers.authorization || "").trim();
258
+ if (!header.startsWith("Bearer ")) {
259
+ return false;
357
260
  }
358
-
359
- if (method === "POST" && url.pathname === "/v1/evm/network/set") {
360
- const body = await readJsonBody(request);
361
- return sendJson(response, 200, {
362
- ok: true,
363
- data: await networkState.setActiveNetwork(body),
364
- });
261
+ const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
262
+ const expected = Buffer.from(String(config.authToken || ""), "utf8");
263
+ if (provided.length === 0 || provided.length !== expected.length) {
264
+ return false;
365
265
  }
266
+ return crypto.timingSafeEqual(provided, expected);
267
+ }
366
268
 
367
- if (method === "POST" && url.pathname === "/v1/evm/wallets/get") {
368
- const body = await readJsonBody(request);
369
- const activeNetwork = await networkState.getActiveNetwork();
370
- return sendJson(response, 200, {
371
- ok: true,
372
- data: {
373
- ...(await vault.getWallet(body)),
374
- activeNetwork,
375
- },
376
- });
377
- }
269
+ async function withResolvedSeed(body = {}) {
270
+ const resolved = await vault.resolveSeedPhrase({
271
+ walletId: body.walletId,
272
+ seedPhrase: body.seedPhrase,
273
+ password: body.password,
274
+ });
275
+ return {
276
+ ...body,
277
+ seedPhrase: resolved.seedPhrase,
278
+ walletId: resolved.walletId ?? body.walletId ?? null,
279
+ credentialSource: resolved.source,
280
+ unlockExpiresAt: resolved.unlockExpiresAt ?? null,
281
+ };
282
+ }
378
283
 
379
- if (method === "POST" && url.pathname === "/v1/evm/wallets/create") {
380
- const body = await withResolvedNetwork(await readJsonBody(request));
381
- return sendJson(response, 200, {
382
- ok: true,
383
- data: await vault.createWallet(body),
384
- });
284
+ async function withResolvedSeedOrAddress(body = {}) {
285
+ const address = typeof body.address === "string" ? body.address.trim() : "";
286
+ if (address) {
287
+ return {
288
+ ...body,
289
+ address,
290
+ };
385
291
  }
292
+ return withResolvedSeed(body);
293
+ }
386
294
 
387
- if (method === "POST" && url.pathname === "/v1/evm/wallets/import") {
388
- const body = await withResolvedNetwork(await readJsonBody(request));
389
- return sendJson(response, 200, {
390
- ok: true,
391
- data: await vault.importWallet(body),
392
- });
393
- }
295
+ async function withResolvedNetwork(body = {}) {
296
+ const runtimeConfig = await networkState.resolveRuntimeConfig(body.network);
297
+ return {
298
+ ...body,
299
+ network: runtimeConfig.network,
300
+ };
301
+ }
394
302
 
395
- if (method === "POST" && url.pathname === "/v1/evm/wallets/unlock") {
396
- const body = await readJsonBody(request);
397
- return sendJson(response, 200, {
398
- ok: true,
399
- data: await vault.unlockWallet(body),
400
- });
303
+ async function handleRequest(request, response) {
304
+ try {
305
+ const url = new URL(request.url || "/", "http://localhost");
306
+ const { method = "GET" } = request;
307
+
308
+ if (pathRequiresAuth(url.pathname) && !isAuthorized(request)) {
309
+ return unauthorized(response);
310
+ }
311
+
312
+ if (method === "GET" && url.pathname === "/health") {
313
+ const runtimeConfig = await networkState.resolveRuntimeConfig();
314
+ const networkInfo = await networkState.getNetworkInfo();
315
+ return sendJson(response, 200, {
316
+ ok: true,
317
+ service: "wdk-evm-wallet",
318
+ version: config.version,
319
+ wallet: "evm",
320
+ network: runtimeConfig.network,
321
+ chainId: runtimeConfig.chainId,
322
+ host: config.host,
323
+ dataDir: config.dataDir,
324
+ instanceId: config.instanceId,
325
+ pid: process.pid,
326
+ authRequired: config.authRequired,
327
+ unlockTimeoutSeconds: config.unlockTimeoutSeconds,
328
+ availableNetworks: Object.keys(config.networkProfiles),
329
+ provider: sanitizeProviderUrl(runtimeConfig.providerUrl),
330
+ networkProfiles: sanitizeProfiles(networkInfo.profiles),
331
+ source: "wdk",
332
+ });
333
+ }
334
+
335
+ if (method === "POST" && url.pathname === "/v1/evm/seed-phrase/generate") {
336
+ const body = await readJsonBody(request);
337
+ return sendJson(response, 200, {
338
+ ok: true,
339
+ data: service.generateSeedPhrase(body.words ?? 12),
340
+ });
341
+ }
342
+
343
+ if (method === "GET" && url.pathname === "/v1/evm/wallets") {
344
+ const activeNetwork = await networkState.getActiveNetwork();
345
+ return sendJson(response, 200, {
346
+ ok: true,
347
+ data: (await vault.listWallets()).map((wallet) => ({
348
+ ...wallet,
349
+ activeNetwork,
350
+ })),
351
+ });
352
+ }
353
+
354
+ if (method === "GET" && url.pathname === "/v1/evm/network") {
355
+ return sendJson(response, 200, {
356
+ ok: true,
357
+ data: await networkState.getNetworkInfo(),
358
+ });
359
+ }
360
+
361
+ if (method === "POST" && url.pathname === "/v1/evm/network/set") {
362
+ const body = await readJsonBody(request);
363
+ return sendJson(response, 200, {
364
+ ok: true,
365
+ data: await networkState.setActiveNetwork(body),
366
+ });
367
+ }
368
+
369
+ if (method === "POST" && url.pathname === "/v1/evm/wallets/get") {
370
+ const body = await readJsonBody(request);
371
+ const activeNetwork = await networkState.getActiveNetwork();
372
+ return sendJson(response, 200, {
373
+ ok: true,
374
+ data: {
375
+ ...(await vault.getWallet(body)),
376
+ activeNetwork,
377
+ },
378
+ });
379
+ }
380
+
381
+ if (method === "POST" && url.pathname === "/v1/evm/wallets/create") {
382
+ const body = await withResolvedNetwork(await readJsonBody(request));
383
+ return sendJson(response, 200, {
384
+ ok: true,
385
+ data: await vault.createWallet(body),
386
+ });
387
+ }
388
+
389
+ if (method === "POST" && url.pathname === "/v1/evm/wallets/import") {
390
+ const body = await withResolvedNetwork(await readJsonBody(request));
391
+ return sendJson(response, 200, {
392
+ ok: true,
393
+ data: await vault.importWallet(body),
394
+ });
395
+ }
396
+
397
+ if (method === "POST" && url.pathname === "/v1/evm/wallets/unlock") {
398
+ const body = await readJsonBody(request);
399
+ return sendJson(response, 200, {
400
+ ok: true,
401
+ data: await vault.unlockWallet(body),
402
+ });
403
+ }
404
+
405
+ if (method === "POST" && url.pathname === "/v1/evm/wallets/lock") {
406
+ const body = await readJsonBody(request);
407
+ return sendJson(response, 200, {
408
+ ok: true,
409
+ data: await vault.lockWallet(body),
410
+ });
411
+ }
412
+
413
+ if (method === "POST" && url.pathname === "/v1/evm/wallets/reveal-seed") {
414
+ const body = await readJsonBody(request);
415
+ return sendJson(response, 200, {
416
+ ok: true,
417
+ data: await vault.revealSeedPhrase(body),
418
+ });
419
+ }
420
+
421
+ if (method === "POST" && url.pathname === "/v1/evm/wallets/change-password") {
422
+ const body = await readJsonBody(request);
423
+ return sendJson(response, 200, {
424
+ ok: true,
425
+ data: await vault.changePassword(body),
426
+ });
427
+ }
428
+
429
+ if (method === "POST" && url.pathname === "/v1/evm/address/resolve") {
430
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
431
+ const data = await service.resolveAddress(body);
432
+ return sendJson(response, 200, { ok: true, data });
433
+ }
434
+
435
+ if (method === "POST" && url.pathname === "/v1/evm/balance/get") {
436
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
437
+ const data = await service.getBalance(body);
438
+ return sendJson(response, 200, { ok: true, data });
439
+ }
440
+
441
+ if (method === "POST" && url.pathname === "/v1/evm/token-balance/get") {
442
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
443
+ const data = await service.getTokenBalance(body);
444
+ return sendJson(response, 200, { ok: true, data });
445
+ }
446
+
447
+ if (method === "POST" && url.pathname === "/v1/evm/token-metadata/get") {
448
+ const body = await withResolvedNetwork(await readJsonBody(request));
449
+ const data = await service.getTokenMetadata(body);
450
+ return sendJson(response, 200, { ok: true, data });
451
+ }
452
+
453
+ if (method === "POST" && url.pathname === "/v1/evm/fee-rates/get") {
454
+ const body = await withResolvedNetwork(await readJsonBody(request));
455
+ const data = await service.getFeeRates(body);
456
+ return sendJson(response, 200, { ok: true, data });
457
+ }
458
+
459
+ if (method === "POST" && url.pathname === "/v1/evm/transaction/receipt/get") {
460
+ const body = await withResolvedNetwork(await readJsonBody(request));
461
+ const data = await service.getTransactionReceipt(body);
462
+ return sendJson(response, 200, { ok: true, data });
463
+ }
464
+
465
+ if (method === "POST" && url.pathname === "/v1/evm/aave/account/get") {
466
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
467
+ const data = await service.getAaveAccountData(body);
468
+ return sendJson(response, 200, { ok: true, data });
469
+ }
470
+
471
+ if (method === "POST" && url.pathname === "/v1/evm/aave/reserves/get") {
472
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
473
+ const data = await service.getAaveReserves(body);
474
+ return sendJson(response, 200, { ok: true, data });
475
+ }
476
+
477
+ if (method === "POST" && url.pathname === "/v1/evm/aave/positions/get") {
478
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
479
+ const data = await service.getAavePositions(body);
480
+ return sendJson(response, 200, { ok: true, data });
481
+ }
482
+
483
+ if (method === "POST" && url.pathname === "/v1/evm/morpho/vaults/get") {
484
+ const body = await withResolvedNetwork(await readJsonBody(request));
485
+ const data = await service.getMorphoVaults(body);
486
+ return sendJson(response, 200, { ok: true, data });
487
+ }
488
+
489
+ if (method === "POST" && url.pathname === "/v1/evm/morpho/markets/get") {
490
+ const body = await withResolvedNetwork(await readJsonBody(request));
491
+ const data = await service.getMorphoMarkets(body);
492
+ return sendJson(response, 200, { ok: true, data });
493
+ }
494
+
495
+ if (method === "POST" && url.pathname === "/v1/evm/morpho/positions/get") {
496
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
497
+ const data = await service.getMorphoPositions(body);
498
+ return sendJson(response, 200, { ok: true, data });
499
+ }
500
+
501
+ const morphoVaultOperationMatch = url.pathname.match(
502
+ /^\/v1\/evm\/morpho\/vault\/(supply|withdraw)\/(quote|send)$/
503
+ );
504
+ if (method === "POST" && morphoVaultOperationMatch) {
505
+ const operation = morphoVaultOperationMatch[1];
506
+ const action = morphoVaultOperationMatch[2];
507
+ const rawBody = await readJsonBody(request);
508
+ const body =
509
+ action === "quote"
510
+ ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
511
+ : await withResolvedNetwork(await withResolvedSeed(rawBody));
512
+ const data =
513
+ action === "quote"
514
+ ? await service.quoteMorphoVaultOperation({ ...body, operation })
515
+ : await service.sendMorphoVaultOperation({ ...body, operation });
516
+ return sendJson(response, 200, { ok: true, data });
517
+ }
518
+
519
+ const morphoMarketOperationMatch = url.pathname.match(
520
+ /^\/v1\/evm\/morpho\/market\/(supply_collateral|borrow|repay|withdraw_collateral)\/(quote|send)$/
521
+ );
522
+ if (method === "POST" && morphoMarketOperationMatch) {
523
+ const operation = morphoMarketOperationMatch[1];
524
+ const action = morphoMarketOperationMatch[2];
525
+ const rawBody = await readJsonBody(request);
526
+ const body =
527
+ action === "quote"
528
+ ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
529
+ : await withResolvedNetwork(await withResolvedSeed(rawBody));
530
+ const data =
531
+ action === "quote"
532
+ ? await service.quoteMorphoMarketOperation({ ...body, operation })
533
+ : await service.sendMorphoMarketOperation({ ...body, operation });
534
+ return sendJson(response, 200, { ok: true, data });
535
+ }
536
+
537
+ if (method === "POST" && url.pathname === "/v1/evm/lido/overview/get") {
538
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
539
+ const data = await service.getLidoOverview(body);
540
+ return sendJson(response, 200, { ok: true, data });
541
+ }
542
+
543
+ if (method === "POST" && url.pathname === "/v1/evm/lido/positions/get") {
544
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
545
+ const data = await service.getLidoPositions(body);
546
+ return sendJson(response, 200, { ok: true, data });
547
+ }
548
+
549
+ if (method === "POST" && url.pathname === "/v1/evm/lido/withdrawals/get") {
550
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
551
+ const data = await service.getLidoWithdrawalRequests(body);
552
+ return sendJson(response, 200, { ok: true, data });
553
+ }
554
+
555
+ const aaveOperationMatch = url.pathname.match(
556
+ /^\/v1\/evm\/aave\/(supply|withdraw|borrow|repay)\/(quote|send)$/
557
+ );
558
+ if (method === "POST" && aaveOperationMatch) {
559
+ const operation = aaveOperationMatch[1];
560
+ const action = aaveOperationMatch[2];
561
+ const rawBody = await readJsonBody(request);
562
+ const body =
563
+ action === "quote"
564
+ ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
565
+ : await withResolvedNetwork(await withResolvedSeed(rawBody));
566
+ const data =
567
+ action === "quote"
568
+ ? await service.quoteAaveOperation({ ...body, operation })
569
+ : await service.sendAaveOperation({ ...body, operation });
570
+ return sendJson(response, 200, { ok: true, data });
571
+ }
572
+
573
+ const lidoOperationMatch = url.pathname.match(
574
+ /^\/v1\/evm\/lido\/(stake_eth_for_wsteth|wrap_steth|unwrap_wsteth)\/(quote|send)$/
575
+ );
576
+ if (method === "POST" && lidoOperationMatch) {
577
+ const operation = lidoOperationMatch[1];
578
+ const action = lidoOperationMatch[2];
579
+ const rawBody = await readJsonBody(request);
580
+ const body =
581
+ action === "quote"
582
+ ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
583
+ : await withResolvedNetwork(await withResolvedSeed(rawBody));
584
+ const data =
585
+ action === "quote"
586
+ ? await service.quoteLidoOperation({ ...body, operation })
587
+ : await service.sendLidoOperation({ ...body, operation });
588
+ return sendJson(response, 200, { ok: true, data });
589
+ }
590
+
591
+ const lidoWithdrawalOperationMatch = url.pathname.match(
592
+ /^\/v1\/evm\/lido\/(request_withdrawal_steth|request_withdrawal_wsteth|claim_withdrawal)\/(quote|send)$/
593
+ );
594
+ if (method === "POST" && lidoWithdrawalOperationMatch) {
595
+ const operation = lidoWithdrawalOperationMatch[1];
596
+ const action = lidoWithdrawalOperationMatch[2];
597
+ const rawBody = await readJsonBody(request);
598
+ const body =
599
+ action === "quote"
600
+ ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
601
+ : await withResolvedNetwork(await withResolvedSeed(rawBody));
602
+ const data =
603
+ action === "quote"
604
+ ? await service.quoteLidoWithdrawalOperation({ ...body, operation })
605
+ : await service.sendLidoWithdrawalOperation({ ...body, operation });
606
+ return sendJson(response, 200, { ok: true, data });
607
+ }
608
+
609
+ if (method === "POST" && url.pathname === "/v1/evm/swap/quote") {
610
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
611
+ const data = await service.quoteSwap(body);
612
+ return sendJson(response, 200, { ok: true, data });
613
+ }
614
+
615
+ if (method === "POST" && url.pathname === "/v1/evm/swap/send") {
616
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
617
+ const data = await service.swap(body);
618
+ return sendJson(response, 200, { ok: true, data });
619
+ }
620
+
621
+ if (method === "POST" && url.pathname === "/v1/evm/lifi/quote") {
622
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
623
+ const data = await service.quoteLifiSwap(body);
624
+ return sendJson(response, 200, { ok: true, data });
625
+ }
626
+
627
+ if (method === "POST" && url.pathname === "/v1/evm/lifi/send") {
628
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
629
+ const data = await service.sendLifiSwap(body);
630
+ return sendJson(response, 200, { ok: true, data });
631
+ }
632
+
633
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/swap/quote") {
634
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
635
+ const data = await service.quoteUniswapSwap(body);
636
+ return sendJson(response, 200, { ok: true, data });
637
+ }
638
+
639
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/swap/send") {
640
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
641
+ const data = await service.sendUniswapSwap(body);
642
+ return sendJson(response, 200, { ok: true, data });
643
+ }
644
+
645
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/quote") {
646
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
647
+ const data = await service.quoteUniswapLiquidity(body);
648
+ return sendJson(response, 200, { ok: true, data });
649
+ }
650
+
651
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/pools") {
652
+ const body = await withResolvedNetwork(await readJsonBody(request));
653
+ const data = await service.getUniswapLiquidityPools(body);
654
+ return sendJson(response, 200, { ok: true, data });
655
+ }
656
+
657
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/positions") {
658
+ const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
659
+ const data = await service.getUniswapLiquidityPositions(body);
660
+ return sendJson(response, 200, { ok: true, data });
661
+ }
662
+
663
+ if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/send") {
664
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
665
+ const data = await service.sendUniswapLiquidity(body);
666
+ return sendJson(response, 200, { ok: true, data });
667
+ }
668
+
669
+ if (method === "POST" && url.pathname === "/v1/evm/transfer/quote") {
670
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
671
+ const data = await service.quoteNativeTransfer(body);
672
+ return sendJson(response, 200, { ok: true, data });
673
+ }
674
+
675
+ if (method === "POST" && url.pathname === "/v1/evm/transfer/send") {
676
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
677
+ const data = await service.sendNativeTransfer(body);
678
+ return sendJson(response, 200, { ok: true, data });
679
+ }
680
+
681
+ if (method === "POST" && url.pathname === "/v1/evm/token-transfer/quote") {
682
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
683
+ const data = await service.quoteTokenTransfer(body);
684
+ return sendJson(response, 200, { ok: true, data });
685
+ }
686
+
687
+ if (method === "POST" && url.pathname === "/v1/evm/token-transfer/send") {
688
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
689
+ const data = await service.sendTokenTransfer(body);
690
+ return sendJson(response, 200, { ok: true, data });
691
+ }
692
+
693
+ if (method === "POST" && url.pathname === "/v1/evm/x402/exact/sign") {
694
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
695
+ const data = await service.signX402ExactTypedData(body);
696
+ return sendJson(response, 200, { ok: true, data });
697
+ }
698
+
699
+ if (method === "POST" && url.pathname === "/v1/evm/message/sign") {
700
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
701
+ const data = await service.signPersonalMessage(body);
702
+ return sendJson(response, 200, { ok: true, data });
703
+ }
704
+
705
+ return notFound(response);
706
+ } catch (error) {
707
+ const shaped = toErrorResponse(error, new URL(request.url || "/", "http://localhost").pathname, 400);
708
+ return sendJson(response, shaped.statusCode, shaped.payload);
401
709
  }
710
+ }
402
711
 
403
- if (method === "POST" && url.pathname === "/v1/evm/wallets/lock") {
404
- const body = await readJsonBody(request);
405
- return sendJson(response, 200, {
406
- ok: true,
407
- data: await vault.lockWallet(body),
408
- });
409
- }
712
+ const server = createServer((request, response) => {
713
+ void withTrackedRequest(shutdown, async () => {
714
+ try {
715
+ await handleRequest(request, response);
716
+ } catch (error) {
717
+ const shaped = toErrorResponse(
718
+ error,
719
+ new URL(request.url || "/", "http://localhost").pathname,
720
+ 500,
721
+ );
722
+ sendJson(response, shaped.statusCode, shaped.payload);
723
+ }
724
+ });
725
+ });
410
726
 
411
- if (method === "POST" && url.pathname === "/v1/evm/wallets/reveal-seed") {
412
- const body = await readJsonBody(request);
413
- return sendJson(response, 200, {
414
- ok: true,
415
- data: await vault.revealSeedPhrase(body),
416
- });
417
- }
727
+ // 8s stays strictly inside the 10s SIGTERM->SIGKILL window the Python client
728
+ // allows (agent_wallet/evm_user_wallets.py), so the orderly path always wins.
729
+ const shutdown = createShutdownCoordinator({
730
+ closeServer: () => {
731
+ server.close();
732
+ if (config.transport === "socket") {
733
+ try {
734
+ fs.unlinkSync(config.socketPath);
735
+ } catch {
736
+ // already gone — nothing to clean up
737
+ }
738
+ }
739
+ },
740
+ exit: (code) => process.exit(code),
741
+ graceMs: 8000,
742
+ log: (message) => console.log(message),
743
+ });
418
744
 
419
- if (method === "POST" && url.pathname === "/v1/evm/wallets/change-password") {
420
- const body = await readJsonBody(request);
421
- return sendJson(response, 200, {
422
- ok: true,
423
- data: await vault.changePassword(body),
745
+ // Named so a test's close() can remove exactly these listeners — startServer()
746
+ // runs multiple times in one process across tests, and anonymous listeners
747
+ // would accumulate on `process` for the life of the test run.
748
+ const onSigterm = () => shutdown.begin("SIGTERM");
749
+ const onSigint = () => shutdown.begin("SIGINT");
750
+ process.on("SIGTERM", onSigterm);
751
+ process.on("SIGINT", onSigint);
752
+
753
+ if (config.transport === "socket" && fs.existsSync(config.socketPath)) {
754
+ // A leftover socket file from an unclean shutdown makes bind() fail even
755
+ // though nothing is listening. Probe it first: a connection error means
756
+ // it's stale and safe to remove; a live response means something else is
757
+ // genuinely already serving this exact dataDir, which the caller's own
758
+ // health check should have caught before ever spawning us.
759
+ const alive = await new Promise((resolve) => {
760
+ const probe = net.connect(config.socketPath);
761
+ probe.once("connect", () => {
762
+ probe.destroy();
763
+ resolve(true);
424
764
  });
765
+ probe.once("error", () => resolve(false));
766
+ });
767
+ if (!alive) {
768
+ fs.unlinkSync(config.socketPath);
425
769
  }
426
-
427
- if (method === "POST" && url.pathname === "/v1/evm/address/resolve") {
428
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
429
- const data = await service.resolveAddress(body);
430
- return sendJson(response, 200, { ok: true, data });
431
- }
432
-
433
- if (method === "POST" && url.pathname === "/v1/evm/balance/get") {
434
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
435
- const data = await service.getBalance(body);
436
- return sendJson(response, 200, { ok: true, data });
437
- }
438
-
439
- if (method === "POST" && url.pathname === "/v1/evm/token-balance/get") {
440
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
441
- const data = await service.getTokenBalance(body);
442
- return sendJson(response, 200, { ok: true, data });
443
- }
444
-
445
- if (method === "POST" && url.pathname === "/v1/evm/token-metadata/get") {
446
- const body = await withResolvedNetwork(await readJsonBody(request));
447
- const data = await service.getTokenMetadata(body);
448
- return sendJson(response, 200, { ok: true, data });
449
- }
450
-
451
- if (method === "POST" && url.pathname === "/v1/evm/fee-rates/get") {
452
- const body = await withResolvedNetwork(await readJsonBody(request));
453
- const data = await service.getFeeRates(body);
454
- return sendJson(response, 200, { ok: true, data });
455
- }
456
-
457
- if (method === "POST" && url.pathname === "/v1/evm/transaction/receipt/get") {
458
- const body = await withResolvedNetwork(await readJsonBody(request));
459
- const data = await service.getTransactionReceipt(body);
460
- return sendJson(response, 200, { ok: true, data });
461
- }
462
-
463
- if (method === "POST" && url.pathname === "/v1/evm/aave/account/get") {
464
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
465
- const data = await service.getAaveAccountData(body);
466
- return sendJson(response, 200, { ok: true, data });
467
- }
468
-
469
- if (method === "POST" && url.pathname === "/v1/evm/aave/reserves/get") {
470
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
471
- const data = await service.getAaveReserves(body);
472
- return sendJson(response, 200, { ok: true, data });
473
- }
474
-
475
- if (method === "POST" && url.pathname === "/v1/evm/aave/positions/get") {
476
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
477
- const data = await service.getAavePositions(body);
478
- return sendJson(response, 200, { ok: true, data });
479
- }
480
-
481
- if (method === "POST" && url.pathname === "/v1/evm/morpho/vaults/get") {
482
- const body = await withResolvedNetwork(await readJsonBody(request));
483
- const data = await service.getMorphoVaults(body);
484
- return sendJson(response, 200, { ok: true, data });
485
- }
486
-
487
- if (method === "POST" && url.pathname === "/v1/evm/morpho/markets/get") {
488
- const body = await withResolvedNetwork(await readJsonBody(request));
489
- const data = await service.getMorphoMarkets(body);
490
- return sendJson(response, 200, { ok: true, data });
491
- }
492
-
493
- if (method === "POST" && url.pathname === "/v1/evm/morpho/positions/get") {
494
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
495
- const data = await service.getMorphoPositions(body);
496
- return sendJson(response, 200, { ok: true, data });
497
- }
498
-
499
- const morphoVaultOperationMatch = url.pathname.match(
500
- /^\/v1\/evm\/morpho\/vault\/(supply|withdraw)\/(quote|send)$/
501
- );
502
- if (method === "POST" && morphoVaultOperationMatch) {
503
- const operation = morphoVaultOperationMatch[1];
504
- const action = morphoVaultOperationMatch[2];
505
- const rawBody = await readJsonBody(request);
506
- const body =
507
- action === "quote"
508
- ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
509
- : await withResolvedNetwork(await withResolvedSeed(rawBody));
510
- const data =
511
- action === "quote"
512
- ? await service.quoteMorphoVaultOperation({ ...body, operation })
513
- : await service.sendMorphoVaultOperation({ ...body, operation });
514
- return sendJson(response, 200, { ok: true, data });
515
- }
516
-
517
- const morphoMarketOperationMatch = url.pathname.match(
518
- /^\/v1\/evm\/morpho\/market\/(supply_collateral|borrow|repay|withdraw_collateral)\/(quote|send)$/
519
- );
520
- if (method === "POST" && morphoMarketOperationMatch) {
521
- const operation = morphoMarketOperationMatch[1];
522
- const action = morphoMarketOperationMatch[2];
523
- const rawBody = await readJsonBody(request);
524
- const body =
525
- action === "quote"
526
- ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
527
- : await withResolvedNetwork(await withResolvedSeed(rawBody));
528
- const data =
529
- action === "quote"
530
- ? await service.quoteMorphoMarketOperation({ ...body, operation })
531
- : await service.sendMorphoMarketOperation({ ...body, operation });
532
- return sendJson(response, 200, { ok: true, data });
533
- }
534
-
535
- if (method === "POST" && url.pathname === "/v1/evm/lido/overview/get") {
536
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
537
- const data = await service.getLidoOverview(body);
538
- return sendJson(response, 200, { ok: true, data });
539
- }
540
-
541
- if (method === "POST" && url.pathname === "/v1/evm/lido/positions/get") {
542
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
543
- const data = await service.getLidoPositions(body);
544
- return sendJson(response, 200, { ok: true, data });
545
- }
546
-
547
- if (method === "POST" && url.pathname === "/v1/evm/lido/withdrawals/get") {
548
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
549
- const data = await service.getLidoWithdrawalRequests(body);
550
- return sendJson(response, 200, { ok: true, data });
551
- }
552
-
553
- const aaveOperationMatch = url.pathname.match(
554
- /^\/v1\/evm\/aave\/(supply|withdraw|borrow|repay)\/(quote|send)$/
555
- );
556
- if (method === "POST" && aaveOperationMatch) {
557
- const operation = aaveOperationMatch[1];
558
- const action = aaveOperationMatch[2];
559
- const rawBody = await readJsonBody(request);
560
- const body =
561
- action === "quote"
562
- ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
563
- : await withResolvedNetwork(await withResolvedSeed(rawBody));
564
- const data =
565
- action === "quote"
566
- ? await service.quoteAaveOperation({ ...body, operation })
567
- : await service.sendAaveOperation({ ...body, operation });
568
- return sendJson(response, 200, { ok: true, data });
569
- }
570
-
571
- const lidoOperationMatch = url.pathname.match(
572
- /^\/v1\/evm\/lido\/(stake_eth_for_wsteth|wrap_steth|unwrap_wsteth)\/(quote|send)$/
573
- );
574
- if (method === "POST" && lidoOperationMatch) {
575
- const operation = lidoOperationMatch[1];
576
- const action = lidoOperationMatch[2];
577
- const rawBody = await readJsonBody(request);
578
- const body =
579
- action === "quote"
580
- ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
581
- : await withResolvedNetwork(await withResolvedSeed(rawBody));
582
- const data =
583
- action === "quote"
584
- ? await service.quoteLidoOperation({ ...body, operation })
585
- : await service.sendLidoOperation({ ...body, operation });
586
- return sendJson(response, 200, { ok: true, data });
587
- }
588
-
589
- const lidoWithdrawalOperationMatch = url.pathname.match(
590
- /^\/v1\/evm\/lido\/(request_withdrawal_steth|request_withdrawal_wsteth|claim_withdrawal)\/(quote|send)$/
591
- );
592
- if (method === "POST" && lidoWithdrawalOperationMatch) {
593
- const operation = lidoWithdrawalOperationMatch[1];
594
- const action = lidoWithdrawalOperationMatch[2];
595
- const rawBody = await readJsonBody(request);
596
- const body =
597
- action === "quote"
598
- ? await withResolvedNetwork(await withResolvedSeedOrAddress(rawBody))
599
- : await withResolvedNetwork(await withResolvedSeed(rawBody));
600
- const data =
601
- action === "quote"
602
- ? await service.quoteLidoWithdrawalOperation({ ...body, operation })
603
- : await service.sendLidoWithdrawalOperation({ ...body, operation });
604
- return sendJson(response, 200, { ok: true, data });
605
- }
606
-
607
- if (method === "POST" && url.pathname === "/v1/evm/swap/quote") {
608
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
609
- const data = await service.quoteSwap(body);
610
- return sendJson(response, 200, { ok: true, data });
611
- }
612
-
613
- if (method === "POST" && url.pathname === "/v1/evm/swap/send") {
614
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
615
- const data = await service.swap(body);
616
- return sendJson(response, 200, { ok: true, data });
617
- }
618
-
619
- if (method === "POST" && url.pathname === "/v1/evm/lifi/quote") {
620
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
621
- const data = await service.quoteLifiSwap(body);
622
- return sendJson(response, 200, { ok: true, data });
623
- }
624
-
625
- if (method === "POST" && url.pathname === "/v1/evm/lifi/send") {
626
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
627
- const data = await service.sendLifiSwap(body);
628
- return sendJson(response, 200, { ok: true, data });
629
- }
630
-
631
- if (method === "POST" && url.pathname === "/v1/evm/uniswap/swap/quote") {
632
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
633
- const data = await service.quoteUniswapSwap(body);
634
- return sendJson(response, 200, { ok: true, data });
635
- }
636
-
637
- if (method === "POST" && url.pathname === "/v1/evm/uniswap/swap/send") {
638
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
639
- const data = await service.sendUniswapSwap(body);
640
- return sendJson(response, 200, { ok: true, data });
641
- }
642
-
643
- if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/quote") {
644
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
645
- const data = await service.quoteUniswapLiquidity(body);
646
- return sendJson(response, 200, { ok: true, data });
647
- }
648
-
649
- if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/pools") {
650
- const body = await withResolvedNetwork(await readJsonBody(request));
651
- const data = await service.getUniswapLiquidityPools(body);
652
- return sendJson(response, 200, { ok: true, data });
653
- }
654
-
655
- if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/positions") {
656
- const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
657
- const data = await service.getUniswapLiquidityPositions(body);
658
- return sendJson(response, 200, { ok: true, data });
659
- }
660
-
661
- if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/send") {
662
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
663
- const data = await service.sendUniswapLiquidity(body);
664
- return sendJson(response, 200, { ok: true, data });
665
- }
666
-
667
- if (method === "POST" && url.pathname === "/v1/evm/transfer/quote") {
668
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
669
- const data = await service.quoteNativeTransfer(body);
670
- return sendJson(response, 200, { ok: true, data });
671
- }
672
-
673
- if (method === "POST" && url.pathname === "/v1/evm/transfer/send") {
674
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
675
- const data = await service.sendNativeTransfer(body);
676
- return sendJson(response, 200, { ok: true, data });
677
- }
678
-
679
- if (method === "POST" && url.pathname === "/v1/evm/token-transfer/quote") {
680
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
681
- const data = await service.quoteTokenTransfer(body);
682
- return sendJson(response, 200, { ok: true, data });
683
- }
684
-
685
- if (method === "POST" && url.pathname === "/v1/evm/token-transfer/send") {
686
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
687
- const data = await service.sendTokenTransfer(body);
688
- return sendJson(response, 200, { ok: true, data });
689
- }
690
-
691
- if (method === "POST" && url.pathname === "/v1/evm/x402/exact/sign") {
692
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
693
- const data = await service.signX402ExactTypedData(body);
694
- return sendJson(response, 200, { ok: true, data });
695
- }
696
-
697
- if (method === "POST" && url.pathname === "/v1/evm/message/sign") {
698
- const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
699
- const data = await service.signPersonalMessage(body);
700
- return sendJson(response, 200, { ok: true, data });
701
- }
702
-
703
- return notFound(response);
704
- } catch (error) {
705
- const shaped = toErrorResponse(error, new URL(request.url || "/", "http://localhost").pathname, 400);
706
- return sendJson(response, shaped.statusCode, shaped.payload);
707
770
  }
708
- }
709
771
 
710
- const server = createServer((request, response) => {
711
- void withTrackedRequest(shutdown, async () => {
712
- try {
713
- await handleRequest(request, response);
714
- } catch (error) {
715
- const shaped = toErrorResponse(
716
- error,
717
- new URL(request.url || "/", "http://localhost").pathname,
718
- 500,
772
+ return new Promise((resolve) => {
773
+ const onListening = () => {
774
+ if (config.transport === "socket") {
775
+ fs.chmodSync(config.socketPath, 0o600);
776
+ }
777
+ console.log(
778
+ config.transport === "socket"
779
+ ? `wdk-evm-wallet listening on ${config.socketPath} (${config.network})`
780
+ : `wdk-evm-wallet listening on ${config.host}:${config.port} (${config.network})`,
719
781
  );
720
- sendJson(response, shaped.statusCode, shaped.payload);
782
+ resolve({
783
+ server,
784
+ // Deliberately does NOT go through `shutdown.begin` — that path ends
785
+ // in `exit(code)` (`process.exit`), which is correct for the real
786
+ // SIGTERM/SIGINT handlers below but would kill the whole test
787
+ // process the first time a test calls `close()`. This is a plain
788
+ // close: stop the server, clean up the socket file, done.
789
+ close: () =>
790
+ new Promise((closeResolve) => {
791
+ process.off("SIGTERM", onSigterm);
792
+ process.off("SIGINT", onSigint);
793
+ server.close(() => {
794
+ if (config.transport === "socket") {
795
+ try {
796
+ fs.unlinkSync(config.socketPath);
797
+ } catch {
798
+ // already gone — nothing to clean up
799
+ }
800
+ }
801
+ closeResolve();
802
+ });
803
+ }),
804
+ });
805
+ };
806
+ server.once("listening", onListening);
807
+ if (config.transport === "tcp") {
808
+ server.listen(config.port, config.host);
809
+ } else {
810
+ server.listen(config.socketPath);
721
811
  }
722
812
  });
723
- });
724
-
725
- // 8s stays strictly inside the 10s SIGTERM->SIGKILL window the Python client
726
- // allows (agent_wallet/evm_user_wallets.py), so the orderly path always wins.
727
- const shutdown = createShutdownCoordinator({
728
- closeServer: () => server.close(),
729
- exit: (code) => process.exit(code),
730
- graceMs: 8000,
731
- log: (message) => console.log(message),
732
- });
733
-
734
- process.on("SIGTERM", () => shutdown.begin("SIGTERM"));
735
- process.on("SIGINT", () => shutdown.begin("SIGINT"));
736
-
737
- server.listen(config.port, config.host, () => {
738
- console.log(
739
- `wdk-evm-wallet listening on ${config.host}:${config.port} (${config.network})`
740
- );
741
- });
813
+ }
814
+
815
+ if (import.meta.url === `file://${process.argv[1]}`) {
816
+ startServer();
817
+ }