@chain-lens/sdk 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,609 @@
1
+ // src/budget.ts
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ var DEFAULTS = {
5
+ perCallMaxUsdc: 1,
6
+ dailyMaxUsdc: 50,
7
+ monthlyMaxUsdc: 500
8
+ };
9
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
10
+ var MS_PER_MONTH = 30 * MS_PER_DAY;
11
+ var BudgetController = class {
12
+ cfg;
13
+ db = null;
14
+ walletAddress;
15
+ initPromise = null;
16
+ constructor(walletAddress, cfg = {}) {
17
+ this.walletAddress = walletAddress;
18
+ this.cfg = { ...DEFAULTS, ...cfg };
19
+ }
20
+ async getDb() {
21
+ if (this.db) return this.db;
22
+ if (!this.initPromise) {
23
+ this.initPromise = this.openDb();
24
+ }
25
+ await this.initPromise;
26
+ return this.db;
27
+ }
28
+ async openDb() {
29
+ try {
30
+ const { Level } = await import("level");
31
+ const dbPath = join(
32
+ homedir(),
33
+ ".chainlens",
34
+ "budget",
35
+ sanitizeAddress(this.walletAddress)
36
+ );
37
+ const level = new Level(dbPath, { valueEncoding: "json" });
38
+ await level.open();
39
+ this.db = new LevelDB(level);
40
+ } catch {
41
+ process.stderr.write(
42
+ "chain-lens SDK: LevelDB unavailable, using in-memory budget storage.\n"
43
+ );
44
+ this.db = new InMemoryDB();
45
+ }
46
+ }
47
+ async canSpend(amount) {
48
+ if (amount > this.cfg.perCallMaxUsdc) {
49
+ return { ok: false, reason: `per-call cap: $${amount} > $${this.cfg.perCallMaxUsdc}` };
50
+ }
51
+ const db = await this.getDb();
52
+ const now = Date.now();
53
+ const records = await db.readAll();
54
+ const alive = records.filter((r) => now - r.ts < MS_PER_MONTH);
55
+ const dailySpend = alive.filter((r) => now - r.ts < MS_PER_DAY).reduce((s, r) => s + r.amount, 0);
56
+ const monthlySpend = alive.reduce((s, r) => s + r.amount, 0);
57
+ if (dailySpend + amount > this.cfg.dailyMaxUsdc) {
58
+ return {
59
+ ok: false,
60
+ reason: `daily cap: $${(dailySpend + amount).toFixed(4)} > $${this.cfg.dailyMaxUsdc}`
61
+ };
62
+ }
63
+ if (monthlySpend + amount > this.cfg.monthlyMaxUsdc) {
64
+ return {
65
+ ok: false,
66
+ reason: `monthly cap: $${(monthlySpend + amount).toFixed(4)} > $${this.cfg.monthlyMaxUsdc}`
67
+ };
68
+ }
69
+ return { ok: true };
70
+ }
71
+ async debit(amount, idempotencyKey) {
72
+ const db = await this.getDb();
73
+ if (idempotencyKey) {
74
+ const existing = await db.readAll();
75
+ if (existing.some((r) => r.idempotencyKey === idempotencyKey)) return;
76
+ }
77
+ await db.append({ ts: Date.now(), amount, idempotencyKey });
78
+ await db.evictBefore(Date.now() - MS_PER_MONTH);
79
+ }
80
+ async currentSpend() {
81
+ const db = await this.getDb();
82
+ const now = Date.now();
83
+ const records = await db.readAll();
84
+ const dailyUsdc = records.filter((r) => now - r.ts < MS_PER_DAY).reduce((s, r) => s + r.amount, 0);
85
+ const monthlyUsdc = records.filter((r) => now - r.ts < MS_PER_MONTH).reduce((s, r) => s + r.amount, 0);
86
+ return { dailyUsdc, monthlyUsdc };
87
+ }
88
+ };
89
+ var InMemoryDB = class {
90
+ records = [];
91
+ async readAll() {
92
+ return [...this.records];
93
+ }
94
+ async append(record) {
95
+ this.records.push(record);
96
+ }
97
+ async evictBefore(ts) {
98
+ this.records = this.records.filter((r) => r.ts >= ts);
99
+ }
100
+ };
101
+ var LevelDB = class {
102
+ constructor(level) {
103
+ this.level = level;
104
+ }
105
+ level;
106
+ async readAll() {
107
+ const records = [];
108
+ for await (const value of this.level.values()) {
109
+ try {
110
+ records.push(JSON.parse(value));
111
+ } catch {
112
+ }
113
+ }
114
+ return records;
115
+ }
116
+ async append(record) {
117
+ const key = `${record.ts}-${Math.random().toString(36).slice(2)}`;
118
+ await this.level.put(key, JSON.stringify(record));
119
+ }
120
+ async evictBefore(ts) {
121
+ const batch = this.level.batch();
122
+ for await (const [key, value] of this.level.iterator()) {
123
+ try {
124
+ const record = JSON.parse(value);
125
+ if (record.ts < ts) batch.del(key);
126
+ } catch {
127
+ batch.del(key);
128
+ }
129
+ }
130
+ await batch.write();
131
+ }
132
+ };
133
+ function sanitizeAddress(addr) {
134
+ return addr.toLowerCase().replace(/[^a-f0-9x]/g, "").slice(0, 42);
135
+ }
136
+
137
+ // src/telemetry.ts
138
+ import { homedir as homedir2 } from "os";
139
+ import { join as join2 } from "path";
140
+ import { createHash } from "crypto";
141
+ import { appendFile, mkdir } from "fs/promises";
142
+ var TelemetryRecorder = class {
143
+ cfg;
144
+ constructor(cfg) {
145
+ this.cfg = cfg;
146
+ }
147
+ async record(entry) {
148
+ if (!this.cfg.enabled) return;
149
+ const line = JSON.stringify(entry) + "\n";
150
+ const dir = join2(homedir2(), ".chainlens", "telemetry");
151
+ const filePath = join2(dir, `${sanitizeAddress2(this.cfg.walletAddress)}.jsonl`);
152
+ try {
153
+ await mkdir(dir, { recursive: true });
154
+ await appendFile(filePath, line, "utf8");
155
+ } catch {
156
+ }
157
+ if (this.cfg.upload) {
158
+ void this.uploadAsync(entry);
159
+ }
160
+ }
161
+ async uploadAsync(entry) {
162
+ try {
163
+ await fetch(`${this.cfg.gatewayUrl}/v1/telemetry/batch`, {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ events: [entry] })
167
+ });
168
+ } catch {
169
+ }
170
+ }
171
+ };
172
+ function hashParams(params) {
173
+ const json = params != null ? JSON.stringify(params) : "";
174
+ return createHash("sha256").update(json, "utf8").digest("hex").slice(0, 16);
175
+ }
176
+ function sanitizeAddress2(addr) {
177
+ return addr.toLowerCase().replace(/[^a-f0-9x]/g, "").slice(0, 42);
178
+ }
179
+
180
+ // src/eip3009.ts
181
+ import { randomBytes } from "crypto";
182
+ var USDC_ADDRESSES = {
183
+ 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
184
+ // Base Sepolia
185
+ 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
186
+ // Base Mainnet
187
+ };
188
+ var CHAIN_LENS_MARKET_ADDRESSES = {
189
+ 84532: "0x45bB56fDB0E6bb14d178E417b67Ed7B3323ffFf7",
190
+ // Base Sepolia
191
+ 8453: "0x0000000000000000000000000000000000000000"
192
+ // placeholder — not deployed yet
193
+ };
194
+ var RECEIVE_WITH_AUTHORIZATION_TYPES = {
195
+ ReceiveWithAuthorization: [
196
+ { name: "from", type: "address" },
197
+ { name: "to", type: "address" },
198
+ { name: "value", type: "uint256" },
199
+ { name: "validAfter", type: "uint256" },
200
+ { name: "validBefore", type: "uint256" },
201
+ { name: "nonce", type: "bytes32" }
202
+ ]
203
+ };
204
+ async function signReceiveWithAuthorization(opts) {
205
+ const { wallet, chainId, amount, to } = opts;
206
+ const usdcAddress = USDC_ADDRESSES[chainId];
207
+ if (!usdcAddress) throw new Error(`No USDC address for chainId=${chainId}`);
208
+ const from = await wallet.address();
209
+ const now = Math.floor(Date.now() / 1e3);
210
+ const validAfter = BigInt(now - 60);
211
+ const validBefore = BigInt(now + 300);
212
+ const nonce = "0x" + randomBytes(32).toString("hex");
213
+ const typedData = {
214
+ domain: {
215
+ name: "USD Coin",
216
+ version: "2",
217
+ chainId,
218
+ verifyingContract: usdcAddress
219
+ },
220
+ types: RECEIVE_WITH_AUTHORIZATION_TYPES,
221
+ primaryType: "ReceiveWithAuthorization",
222
+ message: {
223
+ from,
224
+ to,
225
+ value: amount,
226
+ validAfter,
227
+ validBefore,
228
+ nonce
229
+ }
230
+ };
231
+ const sig = await wallet.signTypedData(typedData);
232
+ return {
233
+ from,
234
+ to,
235
+ amount: amount.toString(),
236
+ validAfter: validAfter.toString(),
237
+ validBefore: validBefore.toString(),
238
+ nonce,
239
+ v: sig.v,
240
+ r: sig.r,
241
+ s: sig.s
242
+ };
243
+ }
244
+ function usdcToAtomic(usdc) {
245
+ return BigInt(Math.round(usdc * 1e6));
246
+ }
247
+ function atomicToUsdc(atomic) {
248
+ return Number(BigInt(atomic)) / 1e6;
249
+ }
250
+
251
+ // src/errors.ts
252
+ var ChainLensError = class extends Error {
253
+ code;
254
+ cause;
255
+ constructor(message, code, cause) {
256
+ super(message);
257
+ this.name = "ChainLensError";
258
+ this.code = code;
259
+ this.cause = cause;
260
+ }
261
+ };
262
+ var ChainLensResolveError = class extends ChainLensError {
263
+ constructor(message, cause) {
264
+ super(message, "RESOLVE", cause);
265
+ this.name = "ChainLensResolveError";
266
+ }
267
+ };
268
+ var BudgetExceededError = class extends ChainLensError {
269
+ reason;
270
+ constructor(reason) {
271
+ super(`Budget exceeded: ${reason}`, "BUDGET");
272
+ this.name = "BudgetExceededError";
273
+ this.reason = reason;
274
+ }
275
+ };
276
+ var ChainLensSignError = class extends ChainLensError {
277
+ constructor(message, cause) {
278
+ super(message, "SIGN", cause);
279
+ this.name = "ChainLensSignError";
280
+ }
281
+ };
282
+ var ChainLensGatewayError = class extends ChainLensError {
283
+ status;
284
+ constructor(message, status, cause) {
285
+ super(message, "GATEWAY", cause);
286
+ this.name = "ChainLensGatewayError";
287
+ this.status = status;
288
+ }
289
+ };
290
+ var ChainLensCallError = class extends ChainLensError {
291
+ failure;
292
+ constructor(failure) {
293
+ super(`Call failed: ${failure.kind} \u2014 ${failure.hint}`, "CALL");
294
+ this.name = "ChainLensCallError";
295
+ this.failure = failure;
296
+ }
297
+ };
298
+
299
+ // src/call.ts
300
+ async function fetchListingInfo(gatewayUrl, listingId) {
301
+ const res = await fetch(`${gatewayUrl}/v1/listings/${listingId}`);
302
+ if (!res.ok) {
303
+ const body = await res.text().catch(() => "");
304
+ throw new ChainLensResolveError(
305
+ `Failed to fetch listing ${listingId}: ${res.status} ${body}`
306
+ );
307
+ }
308
+ return res.json();
309
+ }
310
+ async function executeCall(cfg, budget, telemetry, listingId, params, options = {}) {
311
+ const t0 = Date.now();
312
+ let listing;
313
+ try {
314
+ listing = await fetchListingInfo(cfg.gatewayUrl, listingId);
315
+ } catch (err) {
316
+ throw err instanceof ChainLensResolveError ? err : new ChainLensResolveError(String(err));
317
+ }
318
+ const priceUsdc = listing.priceAtomic ? atomicToUsdc(listing.priceAtomic) : 0;
319
+ const effectiveMaxUsdc = options.maxUsdc ?? priceUsdc;
320
+ const budgetCheck = await budget.canSpend(effectiveMaxUsdc);
321
+ if (!budgetCheck.ok) {
322
+ throw new BudgetExceededError(budgetCheck.reason);
323
+ }
324
+ const marketAddress = CHAIN_LENS_MARKET_ADDRESSES[cfg.chainId];
325
+ if (!marketAddress) throw new ChainLensResolveError(`No market address for chainId=${cfg.chainId}`);
326
+ const amountAtomic = usdcToAtomic(effectiveMaxUsdc);
327
+ let auth;
328
+ try {
329
+ auth = await signReceiveWithAuthorization({
330
+ wallet: cfg.wallet,
331
+ chainId: cfg.chainId,
332
+ amount: amountAtomic,
333
+ to: marketAddress,
334
+ signal: options.signal
335
+ });
336
+ } catch (err) {
337
+ throw new ChainLensSignError(String(err), err);
338
+ }
339
+ let res;
340
+ try {
341
+ res = await fetch(`${cfg.gatewayUrl}/v1/call`, {
342
+ method: "POST",
343
+ headers: { "Content-Type": "application/json" },
344
+ signal: options.signal,
345
+ body: JSON.stringify({
346
+ listingId,
347
+ params,
348
+ auth: {
349
+ from: auth.from,
350
+ to: auth.to,
351
+ amount: auth.amount,
352
+ validAfter: auth.validAfter,
353
+ validBefore: auth.validBefore,
354
+ nonce: auth.nonce,
355
+ v: auth.v,
356
+ r: auth.r,
357
+ s: auth.s
358
+ }
359
+ })
360
+ });
361
+ } catch (err) {
362
+ const latencyMs2 = Date.now() - t0;
363
+ await telemetry.record({
364
+ ts: t0,
365
+ listingId,
366
+ amountUsdc: effectiveMaxUsdc,
367
+ latencyMs: latencyMs2,
368
+ ok: false,
369
+ failure: { kind: "unknown", hint: String(err) },
370
+ paramsHash: hashParams(params)
371
+ });
372
+ throw new ChainLensGatewayError(`Network error: ${String(err)}`, 0, err);
373
+ }
374
+ const latencyMs = Date.now() - t0;
375
+ const body = await res.json().catch(() => null);
376
+ if (!res.ok || !body?.ok) {
377
+ const failure = body?.failure ?? { kind: "unknown", hint: `HTTP ${res.status}` };
378
+ await telemetry.record({
379
+ ts: t0,
380
+ listingId,
381
+ amountUsdc: effectiveMaxUsdc,
382
+ latencyMs,
383
+ ok: false,
384
+ failure,
385
+ paramsHash: hashParams(params)
386
+ });
387
+ throw new ChainLensCallError(failure);
388
+ }
389
+ const amountUsdc = body.amount ? atomicToUsdc(body.amount) : effectiveMaxUsdc;
390
+ const feeUsdc = body.fee ? atomicToUsdc(body.fee) : 0;
391
+ const netUsdc = body.net ? atomicToUsdc(body.net) : amountUsdc - feeUsdc;
392
+ await budget.debit(amountUsdc, options.idempotencyKey);
393
+ await telemetry.record({
394
+ ts: t0,
395
+ listingId,
396
+ amountUsdc,
397
+ latencyMs,
398
+ ok: true,
399
+ txHash: body.settlement?.txHash,
400
+ paramsHash: hashParams(params)
401
+ });
402
+ return {
403
+ ok: true,
404
+ data: body.response,
405
+ listingId,
406
+ amountUsdc,
407
+ feeUsdc,
408
+ netUsdc,
409
+ settlement: body.settlement,
410
+ latencyMs,
411
+ attemptIndex: 0
412
+ };
413
+ }
414
+
415
+ // src/recommend.ts
416
+ async function fetchRecommendations(gatewayUrl, task, maxResults = 5) {
417
+ const res = await fetch(`${gatewayUrl}/v1/recommend`, {
418
+ method: "POST",
419
+ headers: { "Content-Type": "application/json" },
420
+ body: JSON.stringify({ task, maxResults })
421
+ });
422
+ if (!res.ok) {
423
+ const body = await res.text().catch(() => "");
424
+ throw new ChainLensResolveError(`recommend failed: ${res.status} ${body}`);
425
+ }
426
+ const data = await res.json();
427
+ return data.listings ?? [];
428
+ }
429
+
430
+ // src/provider.ts
431
+ var ProviderClient = class {
432
+ constructor(gatewayUrl, wallet) {
433
+ this.gatewayUrl = gatewayUrl;
434
+ this.wallet = wallet;
435
+ }
436
+ gatewayUrl;
437
+ wallet;
438
+ async claimable() {
439
+ const address = await this.wallet.address();
440
+ const res = await fetch(
441
+ `${this.gatewayUrl}/v1/provider/claimable?address=${address}`
442
+ );
443
+ if (!res.ok) {
444
+ throw new ChainLensGatewayError(
445
+ `claimable fetch failed: ${res.status}`,
446
+ res.status
447
+ );
448
+ }
449
+ return res.json();
450
+ }
451
+ async claim() {
452
+ const claimable = await this.claimable();
453
+ if (BigInt(claimable.atomicBalance) === 0n) {
454
+ return { skipped: true };
455
+ }
456
+ const address = await this.wallet.address();
457
+ const res = await fetch(`${this.gatewayUrl}/v1/provider/claim`, {
458
+ method: "POST",
459
+ headers: { "Content-Type": "application/json" },
460
+ body: JSON.stringify({ address })
461
+ });
462
+ if (!res.ok) {
463
+ const body = await res.text().catch(() => "");
464
+ throw new ChainLensGatewayError(`claim failed: ${res.status} ${body}`, res.status);
465
+ }
466
+ const data = await res.json();
467
+ return { txHash: data.txHash };
468
+ }
469
+ async listingDashboard(listingId) {
470
+ const address = await this.wallet.address();
471
+ const res = await fetch(
472
+ `${this.gatewayUrl}/v1/provider/listing/${listingId}?address=${address}`
473
+ );
474
+ if (!res.ok) {
475
+ if (res.status === 403) {
476
+ throw new ChainLensResolveError(`Not authorized to view listing ${listingId}`);
477
+ }
478
+ throw new ChainLensGatewayError(
479
+ `dashboard fetch failed: ${res.status}`,
480
+ res.status
481
+ );
482
+ }
483
+ return res.json();
484
+ }
485
+ };
486
+
487
+ // src/client.ts
488
+ var DEFAULT_GATEWAY = "https://chainlens.pelicanlab.dev";
489
+ var RETRYABLE_KINDS = /* @__PURE__ */ new Set([
490
+ "http_5xx",
491
+ "timeout",
492
+ "schema_mismatch"
493
+ ]);
494
+ var ChainLens = class {
495
+ cfg;
496
+ budget = null;
497
+ telemetry = null;
498
+ walletAddress = null;
499
+ provider;
500
+ constructor(cfg) {
501
+ this.cfg = { gatewayUrl: DEFAULT_GATEWAY, ...cfg };
502
+ this.provider = new ProviderClient(this.cfg.gatewayUrl, cfg.wallet);
503
+ }
504
+ async init() {
505
+ if (!this.walletAddress) {
506
+ this.walletAddress = await this.cfg.wallet.address();
507
+ }
508
+ if (!this.budget) {
509
+ this.budget = new BudgetController(this.walletAddress, this.cfg.budget);
510
+ }
511
+ if (!this.telemetry) {
512
+ this.telemetry = new TelemetryRecorder({
513
+ enabled: this.cfg.telemetry?.enabled ?? true,
514
+ upload: this.cfg.telemetry?.upload ?? false,
515
+ bufferMaxEntries: this.cfg.telemetry?.bufferMaxEntries ?? 1e3,
516
+ gatewayUrl: this.cfg.gatewayUrl,
517
+ walletAddress: this.walletAddress
518
+ });
519
+ }
520
+ return { budget: this.budget, telemetry: this.telemetry };
521
+ }
522
+ async call(listingId, params, options = {}) {
523
+ const { budget, telemetry } = await this.init();
524
+ const maxAttempts = options.fallback !== false && this.cfg.fallback?.enabled ? this.cfg.fallback.maxAttempts ?? 2 : 1;
525
+ let lastErr;
526
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
527
+ try {
528
+ const result = await executeCall(this.cfg, budget, telemetry, listingId, params, options);
529
+ return { ...result, attemptIndex: attempt };
530
+ } catch (err) {
531
+ lastErr = err;
532
+ if (attempt < maxAttempts - 1 && err instanceof ChainLensCallError && RETRYABLE_KINDS.has(err.failure.kind)) {
533
+ continue;
534
+ }
535
+ break;
536
+ }
537
+ }
538
+ throw lastErr;
539
+ }
540
+ async recommend(task, maxResults = 5) {
541
+ return fetchRecommendations(this.cfg.gatewayUrl, task, maxResults);
542
+ }
543
+ async currentSpend() {
544
+ const { budget } = await this.init();
545
+ return budget.currentSpend();
546
+ }
547
+ };
548
+
549
+ // src/wallet/viem.ts
550
+ import { hexToNumber, numberToHex, parseSignature } from "viem";
551
+ var ViemWallet = class {
552
+ constructor(client) {
553
+ this.client = client;
554
+ }
555
+ client;
556
+ async address() {
557
+ const accounts = await this.client.getAddresses();
558
+ const addr = accounts[0];
559
+ if (!addr) throw new Error("ViemWallet: no accounts available");
560
+ return addr;
561
+ }
562
+ async signTypedData(typedData) {
563
+ const { domain, types, primaryType, message } = typedData;
564
+ const sig = await this.client.signTypedData({
565
+ domain: {
566
+ name: domain.name,
567
+ version: domain.version,
568
+ chainId: domain.chainId,
569
+ verifyingContract: domain.verifyingContract
570
+ },
571
+ types,
572
+ primaryType,
573
+ message
574
+ });
575
+ const parsed = parseSignature(sig);
576
+ return {
577
+ v: hexToNumber(numberToHex(parsed.v ?? 27n)),
578
+ r: parsed.r,
579
+ s: parsed.s
580
+ };
581
+ }
582
+ async sendTransaction(tx) {
583
+ const from = await this.address();
584
+ return this.client.sendTransaction({
585
+ account: from,
586
+ to: tx.to,
587
+ data: tx.data,
588
+ value: tx.value,
589
+ chain: this.client.chain ?? null
590
+ });
591
+ }
592
+ };
593
+ export {
594
+ BudgetController,
595
+ BudgetExceededError,
596
+ CHAIN_LENS_MARKET_ADDRESSES,
597
+ ChainLens,
598
+ ChainLensCallError,
599
+ ChainLensError,
600
+ ChainLensGatewayError,
601
+ ChainLensResolveError,
602
+ ChainLensSignError,
603
+ ProviderClient,
604
+ USDC_ADDRESSES,
605
+ ViemWallet,
606
+ atomicToUsdc,
607
+ signReceiveWithAuthorization,
608
+ usdcToAtomic
609
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@chain-lens/sdk",
3
+ "version": "0.1.0",
4
+ "description": "ChainLens SDK — pay-and-call AI data APIs on Base with EIP-3009 instant settlement.",
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
+ "dependencies": {
18
+ "cross-fetch": "^4.0.0",
19
+ "level": "^8.0.1"
20
+ },
21
+ "peerDependencies": {
22
+ "viem": ">=2.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "tsup": "^8.0.0",
27
+ "tsx": "^4.19.0",
28
+ "typescript": "^5.7.0",
29
+ "viem": "^2.21.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "test": "tsx --test 'src/**/*.test.ts'",
34
+ "typecheck": "tsc --noEmit"
35
+ }
36
+ }