@selat-ai/router-client 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/dist/index.js ADDED
@@ -0,0 +1,555 @@
1
+ // src/errors/index.ts
2
+ var RouterSdkError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "RouterSdkError";
6
+ }
7
+ };
8
+ var QuoteParseError = class extends RouterSdkError {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "QuoteParseError";
12
+ }
13
+ };
14
+ var RouterClientConfigError = class extends RouterSdkError {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "RouterClientConfigError";
18
+ }
19
+ };
20
+
21
+ // src/protocols/circleNanopaymentPayloadBuilder.ts
22
+ import { BatchEvmScheme, CHAIN_CONFIGS } from "@circle-fin/x402-batching/client";
23
+ function selectGatewayWalletBatchedOption(quote, chain) {
24
+ const expectedNetwork = `eip155:${CHAIN_CONFIGS[chain].chain.id}`;
25
+ const selected = quote.accepts.find((accept) => {
26
+ const extraName = typeof accept.extra?.name === "string" ? accept.extra.name : void 0;
27
+ return accept.scheme === "exact" && accept.network === expectedNetwork && extraName === "GatewayWalletBatched";
28
+ });
29
+ if (!selected) {
30
+ throw new QuoteParseError(
31
+ `no GatewayWalletBatched accept for ${expectedNetwork}; available networks: ${quote.accepts.map((item) => item.network).join(",")}`
32
+ );
33
+ }
34
+ return selected;
35
+ }
36
+ var CircleNanopaymentPayloadBuilder = class {
37
+ constructor(options) {
38
+ this.options = options;
39
+ }
40
+ options;
41
+ async build(challenge) {
42
+ const selected = selectGatewayWalletBatchedOption(challenge, this.options.chain);
43
+ if (typeof this.options.signer.circleAgentWalletProcessor === "function") {
44
+ const verifyingContract = typeof selected.extra?.verifyingContract === "string" ? selected.extra.verifyingContract : void 0;
45
+ const version = typeof selected.extra?.version === "string" ? selected.extra.version : void 0;
46
+ await this.options.signer.circleAgentWalletProcessor({
47
+ chainId: CHAIN_CONFIGS[this.options.chain].chain.id,
48
+ ...verifyingContract ? { verifyingContract } : {},
49
+ ...version ? { version } : {}
50
+ });
51
+ }
52
+ const batchScheme = new BatchEvmScheme({
53
+ address: this.options.signer.address,
54
+ signTypedData: async (params) => this.options.signer.signTypedData(params)
55
+ });
56
+ const paymentPayload = await batchScheme.createPaymentPayload(challenge.x402Version, {
57
+ scheme: selected.scheme,
58
+ network: selected.network,
59
+ asset: selected.asset,
60
+ amount: selected.amount,
61
+ payTo: selected.payTo,
62
+ maxTimeoutSeconds: selected.maxTimeoutSeconds,
63
+ ...selected.extra ? { extra: selected.extra } : {}
64
+ });
65
+ const paymentSignature = Buffer.from(
66
+ JSON.stringify({
67
+ ...paymentPayload,
68
+ ...challenge.resource ? { resource: challenge.resource } : {},
69
+ accepted: selected
70
+ })
71
+ ).toString("base64");
72
+ return { paymentSignature };
73
+ }
74
+ };
75
+
76
+ // src/quote/QuoteParser.ts
77
+ var ROUTER_QUOTE_TTL_SECONDS = 60;
78
+ var QUOTE_TTL_SAFETY_SKEW_SECONDS = 5;
79
+ var LOCAL_QUOTE_TTL_MS = (ROUTER_QUOTE_TTL_SECONDS - QUOTE_TTL_SAFETY_SKEW_SECONDS) * 1e3;
80
+ function parsePaymentRequiredHeader(headerValue) {
81
+ let decoded;
82
+ try {
83
+ decoded = Buffer.from(headerValue, "base64").toString("utf8");
84
+ } catch {
85
+ throw new QuoteParseError("failed to base64 decode payment-required header");
86
+ }
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(decoded);
90
+ } catch {
91
+ throw new QuoteParseError("failed to parse payment-required header as JSON");
92
+ }
93
+ const rawVersion = payload.x402Version;
94
+ const x402Version = typeof rawVersion === "number" && Number.isFinite(rawVersion) ? rawVersion : 2;
95
+ const resource = typeof payload.resource === "object" && payload.resource !== null ? payload.resource : void 0;
96
+ const rawAccepts = Array.isArray(payload.accepts) ? payload.accepts : [];
97
+ const accepts = rawAccepts.map((entry) => {
98
+ if (typeof entry !== "object" || entry === null) {
99
+ return void 0;
100
+ }
101
+ const record = entry;
102
+ const scheme = typeof record.scheme === "string" ? record.scheme : void 0;
103
+ const network = typeof record.network === "string" ? record.network : void 0;
104
+ const asset = typeof record.asset === "string" ? record.asset : void 0;
105
+ const amount = typeof record.amount === "string" ? record.amount : void 0;
106
+ const payTo = typeof record.payTo === "string" ? record.payTo : void 0;
107
+ const maxTimeoutSeconds = typeof record.maxTimeoutSeconds === "number" ? record.maxTimeoutSeconds : void 0;
108
+ const extra = typeof record.extra === "object" && record.extra !== null ? record.extra : void 0;
109
+ if (!scheme || !network || !asset || !amount || !payTo || maxTimeoutSeconds === void 0) {
110
+ return void 0;
111
+ }
112
+ return {
113
+ scheme,
114
+ network,
115
+ asset,
116
+ amount,
117
+ payTo,
118
+ maxTimeoutSeconds,
119
+ ...extra ? { extra } : {}
120
+ };
121
+ }).filter((item) => item !== void 0);
122
+ if (accepts.length === 0) {
123
+ throw new QuoteParseError("payment-required header has no valid accepts");
124
+ }
125
+ return resource ? {
126
+ x402Version,
127
+ resource,
128
+ accepts
129
+ } : {
130
+ x402Version,
131
+ accepts
132
+ };
133
+ }
134
+ var QuoteParser = class {
135
+ async parse(response) {
136
+ if (response.status !== 402) {
137
+ return null;
138
+ }
139
+ const quoteId = response.headers.get("x-selat-quote-id");
140
+ if (!quoteId) {
141
+ throw new QuoteParseError("missing x-selat-quote-id in 402 response");
142
+ }
143
+ const paymentRequiredHeader = response.headers.get("payment-required");
144
+ if (!paymentRequiredHeader) {
145
+ throw new QuoteParseError("missing payment-required header in 402 response");
146
+ }
147
+ const parsed = parsePaymentRequiredHeader(paymentRequiredHeader);
148
+ const expiresAt = Date.now() + LOCAL_QUOTE_TTL_MS;
149
+ return {
150
+ quoteId,
151
+ expiresAt,
152
+ x402Version: parsed.x402Version,
153
+ ...parsed.resource ? { resource: parsed.resource } : {},
154
+ accepts: parsed.accepts
155
+ };
156
+ }
157
+ };
158
+
159
+ // src/signers/createViemSigner.ts
160
+ import { privateKeyToAccount } from "viem/accounts";
161
+ function createViemSigner(privateKey) {
162
+ const account = privateKeyToAccount(privateKey);
163
+ return {
164
+ address: account.address,
165
+ signTypedData: async (params) => account.signTypedData(params)
166
+ };
167
+ }
168
+
169
+ // src/client/RouterClient.ts
170
+ var DEFAULT_ROUTER_URL = "https://router.selat.ai";
171
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
172
+ function toHeaders(base, override) {
173
+ const headers = new Headers(base);
174
+ if (override) {
175
+ new Headers(override).forEach((value, key) => headers.set(key, value));
176
+ }
177
+ return headers;
178
+ }
179
+ function toRouterProxyUrl(routerUrl, targetUrl) {
180
+ const proxyUrl = new URL("/proxy", routerUrl);
181
+ proxyUrl.searchParams.set("target", targetUrl);
182
+ return proxyUrl.toString();
183
+ }
184
+ function withSignal(init, signal) {
185
+ if (!signal) {
186
+ return init;
187
+ }
188
+ return {
189
+ ...init,
190
+ signal
191
+ };
192
+ }
193
+ function createRequestSignal(callerSignal, requestTimeoutMs) {
194
+ const timeoutController = new AbortController();
195
+ const timeoutId = setTimeout(() => timeoutController.abort(), requestTimeoutMs);
196
+ const timeoutSignal = timeoutController.signal;
197
+ if (!callerSignal) {
198
+ return {
199
+ signal: timeoutSignal,
200
+ cleanup: () => clearTimeout(timeoutId)
201
+ };
202
+ }
203
+ const controller = new AbortController();
204
+ const abort = () => controller.abort();
205
+ const cleanup = () => {
206
+ clearTimeout(timeoutId);
207
+ callerSignal.removeEventListener("abort", abort);
208
+ timeoutSignal.removeEventListener("abort", abort);
209
+ };
210
+ if (callerSignal.aborted || timeoutSignal.aborted) {
211
+ cleanup();
212
+ controller.abort();
213
+ return {
214
+ signal: controller.signal,
215
+ cleanup
216
+ };
217
+ }
218
+ callerSignal.addEventListener("abort", abort);
219
+ timeoutSignal.addEventListener("abort", abort);
220
+ return {
221
+ signal: controller.signal,
222
+ cleanup
223
+ };
224
+ }
225
+ var RouterClient = class {
226
+ constructor(options) {
227
+ this.options = options;
228
+ this.routerUrl = options.routerUrl ?? DEFAULT_ROUTER_URL;
229
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
230
+ if (!options.chain) {
231
+ throw new RouterClientConfigError(
232
+ "chain is required"
233
+ );
234
+ }
235
+ const signer = options.signer ?? (() => {
236
+ if (!options.privateKey) {
237
+ return void 0;
238
+ }
239
+ return createViemSigner(options.privateKey);
240
+ })();
241
+ if (!signer) {
242
+ throw new RouterClientConfigError(
243
+ "Provide either signer, or privateKey (with chain) for Circle nanopayments"
244
+ );
245
+ }
246
+ this.paymentPayloadBuilder = new CircleNanopaymentPayloadBuilder({
247
+ chain: options.chain,
248
+ signer
249
+ });
250
+ }
251
+ options;
252
+ quoteParser = new QuoteParser();
253
+ routerUrl;
254
+ requestTimeoutMs;
255
+ paymentPayloadBuilder;
256
+ createFetch(config) {
257
+ return (path, init) => this.fetch(new URL(path, config.baseUrl).toString(), init);
258
+ }
259
+ async fetch(input, init) {
260
+ const { preferProtocol, ...requestInit } = init ?? {};
261
+ const proxyUrl = toRouterProxyUrl(this.routerUrl, input);
262
+ const firstHeaders = toHeaders(this.options.defaultHeaders ?? {}, requestInit.headers);
263
+ firstHeaders.set("x-selat-prefer-protocol", preferProtocol ?? "mpp");
264
+ const firstRequest = createRequestSignal(requestInit.signal ?? void 0, this.requestTimeoutMs);
265
+ let firstResponse;
266
+ try {
267
+ firstResponse = await fetch(proxyUrl, withSignal({
268
+ ...requestInit,
269
+ headers: firstHeaders
270
+ }, firstRequest.signal));
271
+ } finally {
272
+ firstRequest.cleanup();
273
+ }
274
+ const challenge = await this.quoteParser.parse(firstResponse.clone());
275
+ if (!challenge) {
276
+ return firstResponse;
277
+ }
278
+ const payment = await this.paymentPayloadBuilder.build(challenge);
279
+ const paidHeaders = firstHeaders;
280
+ paidHeaders.set("PAYMENT-SIGNATURE", payment.paymentSignature);
281
+ paidHeaders.set("x-selat-quote-id", challenge.quoteId);
282
+ const paidRequest = createRequestSignal(requestInit.signal ?? void 0, this.requestTimeoutMs);
283
+ try {
284
+ return await fetch(proxyUrl, withSignal({
285
+ ...requestInit,
286
+ headers: paidHeaders
287
+ }, paidRequest.signal));
288
+ } finally {
289
+ paidRequest.cleanup();
290
+ }
291
+ }
292
+ };
293
+
294
+ // src/signers/createRemoteSigner.ts
295
+ function createRemoteSigner(address, requester) {
296
+ return {
297
+ address,
298
+ signTypedData: async (params) => requester({ address, typedData: params })
299
+ };
300
+ }
301
+
302
+ // src/signers/createCircleAgentWalletSigner.ts
303
+ import { spawn } from "child_process";
304
+ import { recoverTypedDataAddress } from "viem";
305
+ var DEFAULT_CIRCLE_CLI_COMMAND = "circle";
306
+ var DEFAULT_CIRCLE_CLI_TIMEOUT_MS = 3e4;
307
+ function toCliChain(chain) {
308
+ switch (chain) {
309
+ case "base":
310
+ return "BASE";
311
+ case "ethereum":
312
+ return "ETHEREUM";
313
+ case "avalanche":
314
+ return "AVALANCHE";
315
+ case "optimism":
316
+ return "OPTIMISM";
317
+ case "polygon":
318
+ return "POLYGON";
319
+ default:
320
+ return chain.toUpperCase();
321
+ }
322
+ }
323
+ function stringifySafeJson(value) {
324
+ return JSON.stringify(value, (_key, item) => {
325
+ if (typeof item !== "bigint") {
326
+ return item;
327
+ }
328
+ if (item <= BigInt(Number.MAX_SAFE_INTEGER) && item >= BigInt(Number.MIN_SAFE_INTEGER)) {
329
+ return Number(item);
330
+ }
331
+ return item.toString();
332
+ });
333
+ }
334
+ function ensureEip712DomainTypeForCircleCli(typedData) {
335
+ if (!typedData || typeof typedData !== "object") {
336
+ return typedData;
337
+ }
338
+ const root = typedData;
339
+ const domain = root.domain;
340
+ const types = root.types;
341
+ if (!domain || typeof domain !== "object" || !types || typeof types !== "object") {
342
+ return typedData;
343
+ }
344
+ const typesRecord = types;
345
+ if (Array.isArray(typesRecord.EIP712Domain)) {
346
+ return typedData;
347
+ }
348
+ const domainRecord = domain;
349
+ const eip712Domain = [];
350
+ if (typeof domainRecord.name === "string") {
351
+ eip712Domain.push({ name: "name", type: "string" });
352
+ }
353
+ if (typeof domainRecord.version === "string") {
354
+ eip712Domain.push({ name: "version", type: "string" });
355
+ }
356
+ if (typeof domainRecord.chainId === "number" || typeof domainRecord.chainId === "bigint" || typeof domainRecord.chainId === "string") {
357
+ eip712Domain.push({ name: "chainId", type: "uint256" });
358
+ }
359
+ if (typeof domainRecord.verifyingContract === "string") {
360
+ eip712Domain.push({ name: "verifyingContract", type: "address" });
361
+ }
362
+ if (typeof domainRecord.salt === "string") {
363
+ eip712Domain.push({ name: "salt", type: "bytes32" });
364
+ }
365
+ return {
366
+ ...root,
367
+ types: {
368
+ EIP712Domain: eip712Domain,
369
+ ...typesRecord
370
+ }
371
+ };
372
+ }
373
+ function normalizeAddress(address) {
374
+ return address.toLowerCase();
375
+ }
376
+ function extractHexSignature(text) {
377
+ const matches = text.match(/0x[0-9a-fA-F]{130}/g);
378
+ if (!matches || matches.length === 0) {
379
+ return null;
380
+ }
381
+ return matches[matches.length - 1];
382
+ }
383
+ async function ensureSignatureMatchesExpectedAddress(typedData, signature, fallbackExpectedAddress) {
384
+ try {
385
+ const typedDataRecord = typedData;
386
+ const message = typedDataRecord.message;
387
+ const expectedAddress = typeof message?.from === "string" ? normalizeAddress(message.from) : fallbackExpectedAddress;
388
+ const recoveredAddress = await recoverTypedDataAddress({
389
+ ...typedDataRecord,
390
+ signature
391
+ });
392
+ if (normalizeAddress(recoveredAddress) !== normalizeAddress(expectedAddress)) {
393
+ throw new Error(
394
+ `Circle CLI produced a signature for ${recoveredAddress}, but expected ${expectedAddress}. This commonly happens when an outdated Circle CLI is used or when the selected wallet signs as a different key model. Upgrade @circle-fin/cli and verify the wallet address with \`circle wallet list --chain <CHAIN>\` before running the SDK.`
395
+ );
396
+ }
397
+ } catch (error) {
398
+ if (error instanceof Error && error.message.startsWith("Circle CLI produced a signature")) {
399
+ throw error;
400
+ }
401
+ }
402
+ }
403
+ async function signTypedDataWithCircleCli(command, timeoutMs, address, chain, typedData, options) {
404
+ const normalizedTypedData = ensureEip712DomainTypeForCircleCli(typedData);
405
+ const typedDataJson = stringifySafeJson(normalizedTypedData);
406
+ const args = [
407
+ "wallet",
408
+ "sign",
409
+ "typed-data",
410
+ typedDataJson,
411
+ "--address",
412
+ address,
413
+ "--chain",
414
+ toCliChain(chain)
415
+ ];
416
+ return new Promise((resolve, reject) => {
417
+ const child = spawn(command, args, {
418
+ shell: false,
419
+ windowsHide: true
420
+ });
421
+ let stdout = "";
422
+ let stderr = "";
423
+ const timeout = setTimeout(() => {
424
+ child.kill();
425
+ reject(new Error(`Circle CLI signing timed out after ${timeoutMs}ms`));
426
+ }, timeoutMs);
427
+ child.stdout.on("data", (chunk) => {
428
+ stdout += chunk.toString();
429
+ });
430
+ child.stderr.on("data", (chunk) => {
431
+ stderr += chunk.toString();
432
+ });
433
+ child.on("error", (error) => {
434
+ clearTimeout(timeout);
435
+ const message = error instanceof Error ? error.message : String(error);
436
+ reject(
437
+ new Error(
438
+ `Failed to execute Circle CLI ("${command}"): ${message}. Ensure @circle-fin/cli is installed and authenticated.`
439
+ )
440
+ );
441
+ });
442
+ child.on("close", (code) => {
443
+ clearTimeout(timeout);
444
+ if (code !== 0) {
445
+ const details = stderr.trim() || stdout.trim() || `exit code ${code}`;
446
+ reject(new Error(`Circle CLI signing failed: ${details}`));
447
+ return;
448
+ }
449
+ const signature = extractHexSignature(stdout);
450
+ if (!signature) {
451
+ reject(new Error(`Circle CLI output did not contain a valid signature: ${stdout.trim()}`));
452
+ return;
453
+ }
454
+ if (options?.skipSignatureAddressCheck) {
455
+ resolve(signature);
456
+ return;
457
+ }
458
+ ensureSignatureMatchesExpectedAddress(normalizedTypedData, signature, address).then(() => resolve(signature)).catch((error) => reject(error instanceof Error ? error : new Error(String(error))));
459
+ });
460
+ });
461
+ }
462
+ var GATEWAY_AUTH_TYPES = {
463
+ TransferWithAuthorization: [
464
+ { name: "from", type: "address" },
465
+ { name: "to", type: "address" },
466
+ { name: "value", type: "uint256" },
467
+ { name: "validAfter", type: "uint256" },
468
+ { name: "validBefore", type: "uint256" },
469
+ { name: "nonce", type: "bytes32" }
470
+ ]
471
+ };
472
+ async function resolveGatewaySignerAddress(command, timeoutMs, walletAddress, chain, input) {
473
+ if (!input.verifyingContract) {
474
+ return walletAddress;
475
+ }
476
+ const typedData = {
477
+ domain: {
478
+ name: "GatewayWalletBatched",
479
+ version: input.version ?? "1",
480
+ chainId: input.chainId,
481
+ verifyingContract: input.verifyingContract
482
+ },
483
+ primaryType: "TransferWithAuthorization",
484
+ types: GATEWAY_AUTH_TYPES,
485
+ message: {
486
+ from: walletAddress,
487
+ to: "0x0000000000000000000000000000000000000000",
488
+ value: 0,
489
+ validAfter: 0,
490
+ validBefore: 0,
491
+ nonce: "0x" + "0".repeat(64)
492
+ }
493
+ };
494
+ const probeSignature = await signTypedDataWithCircleCli(
495
+ command,
496
+ timeoutMs,
497
+ walletAddress,
498
+ chain,
499
+ typedData,
500
+ { skipSignatureAddressCheck: true }
501
+ );
502
+ const owner = await recoverTypedDataAddress({
503
+ ...typedData,
504
+ signature: probeSignature
505
+ });
506
+ return normalizeAddress(owner);
507
+ }
508
+ function createCircleAgentWalletSigner(options) {
509
+ const command = options.cliCommand ?? DEFAULT_CIRCLE_CLI_COMMAND;
510
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CIRCLE_CLI_TIMEOUT_MS;
511
+ const walletAddress = normalizeAddress(options.address);
512
+ let effectiveAddress = walletAddress;
513
+ let resolvedOwnerAddress = null;
514
+ let resolveOwnerAddressInFlight = null;
515
+ const resolveOwnerAddress = async (input) => {
516
+ if (!input.verifyingContract) {
517
+ return walletAddress;
518
+ }
519
+ if (resolvedOwnerAddress) {
520
+ return resolvedOwnerAddress;
521
+ }
522
+ if (!resolveOwnerAddressInFlight) {
523
+ resolveOwnerAddressInFlight = resolveGatewaySignerAddress(command, timeoutMs, walletAddress, options.chain, input).then((owner2) => {
524
+ resolvedOwnerAddress = owner2;
525
+ effectiveAddress = owner2;
526
+ return owner2;
527
+ }).finally(() => {
528
+ resolveOwnerAddressInFlight = null;
529
+ });
530
+ }
531
+ const owner = await resolveOwnerAddressInFlight;
532
+ resolvedOwnerAddress = owner;
533
+ effectiveAddress = owner;
534
+ return owner;
535
+ };
536
+ return {
537
+ get address() {
538
+ return effectiveAddress;
539
+ },
540
+ circleAgentWalletProcessor: async (input) => {
541
+ await resolveOwnerAddress(input);
542
+ },
543
+ signTypedData: async (typedData) => signTypedDataWithCircleCli(command, timeoutMs, walletAddress, options.chain, typedData)
544
+ };
545
+ }
546
+ export {
547
+ CircleNanopaymentPayloadBuilder,
548
+ QuoteParseError,
549
+ RouterClient,
550
+ RouterClientConfigError,
551
+ RouterSdkError,
552
+ createCircleAgentWalletSigner,
553
+ createRemoteSigner,
554
+ createViemSigner
555
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@selat-ai/router-client",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript client for interacting with SELATA Router.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "provenance": true
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "clean": "rimraf dist",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "lint": "tsc --noEmit",
33
+ "example": "tsx examples/pay-with-selat.ts",
34
+ "prepublishOnly": "pnpm run clean && pnpm run build"
35
+ },
36
+ "keywords": [
37
+ "selat",
38
+ "router",
39
+ "x402",
40
+ "mpp",
41
+ "payments",
42
+ "circle",
43
+ "nanopayments"
44
+ ],
45
+ "dependencies": {
46
+ "@circle-fin/x402-batching": "^3.1.2",
47
+ "viem": "^2.48.8"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.13.5",
51
+ "dotenv": "^17.2.1",
52
+ "rimraf": "^6.0.1",
53
+ "tsx": "^4.21.0",
54
+ "tsup": "^8.3.6",
55
+ "typescript": "^5.7.3",
56
+ "vitest": "^3.0.5"
57
+ }
58
+ }