@nkmc/agent-fs 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/chunk-7LIZT7L3.js +966 -0
- package/dist/index.cjs +1278 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +419 -0
- package/dist/rpc-D1IHpjF_.d.cts +330 -0
- package/dist/rpc-D1IHpjF_.d.ts +330 -0
- package/dist/testing.cjs +842 -0
- package/dist/testing.d.cts +29 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +10 -0
- package/package.json +25 -0
- package/src/agent-fs.ts +151 -0
- package/src/backends/http.ts +835 -0
- package/src/backends/memory.ts +183 -0
- package/src/backends/rpc.ts +456 -0
- package/src/index.ts +36 -0
- package/src/mount.ts +84 -0
- package/src/parser.ts +162 -0
- package/src/server.ts +158 -0
- package/src/testing.ts +3 -0
- package/src/types.ts +52 -0
- package/test/agent-fs.test.ts +325 -0
- package/test/http-204.test.ts +102 -0
- package/test/http-auth-prefix.test.ts +79 -0
- package/test/http-cloudflare.test.ts +533 -0
- package/test/http-form-encoding.test.ts +119 -0
- package/test/http-github.test.ts +580 -0
- package/test/http-listkey.test.ts +128 -0
- package/test/http-oauth2.test.ts +174 -0
- package/test/http-pagination.test.ts +200 -0
- package/test/http-param-styles.test.ts +98 -0
- package/test/http-passthrough.test.ts +282 -0
- package/test/http-retry.test.ts +132 -0
- package/test/http.test.ts +360 -0
- package/test/memory.test.ts +120 -0
- package/test/mount.test.ts +94 -0
- package/test/parser.test.ts +100 -0
- package/test/rpc-crud.test.ts +627 -0
- package/test/rpc-evm.test.ts +390 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { createServer, type Server } from "node:http";
|
|
3
|
+
import {
|
|
4
|
+
RpcBackend,
|
|
5
|
+
JsonRpcTransport,
|
|
6
|
+
RpcError,
|
|
7
|
+
type RpcResource,
|
|
8
|
+
} from "../src/backends/rpc.js";
|
|
9
|
+
|
|
10
|
+
// --- Hex utilities ---
|
|
11
|
+
|
|
12
|
+
function toHex(value: string | number): string {
|
|
13
|
+
const n = typeof value === "string" ? parseInt(value, 10) : value;
|
|
14
|
+
return "0x" + n.toString(16);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fromHex(hex: string): number {
|
|
18
|
+
return parseInt(hex, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- Mock JSON-RPC server ---
|
|
22
|
+
|
|
23
|
+
const mockBlocks: Record<string, object> = {};
|
|
24
|
+
for (let i = 0; i <= 16; i++) {
|
|
25
|
+
mockBlocks[toHex(i)] = {
|
|
26
|
+
number: toHex(i),
|
|
27
|
+
hash: `0x${"ab".repeat(16)}${i.toString(16).padStart(4, "0")}`,
|
|
28
|
+
transactions: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleRpcMethod(
|
|
33
|
+
method: string,
|
|
34
|
+
params: unknown[],
|
|
35
|
+
): { result?: unknown; error?: { code: number; message: string } } {
|
|
36
|
+
switch (method) {
|
|
37
|
+
case "eth_blockNumber":
|
|
38
|
+
return { result: "0x10" }; // 16
|
|
39
|
+
|
|
40
|
+
case "eth_getBlockByNumber": {
|
|
41
|
+
const blockNum = params[0] as string;
|
|
42
|
+
const block = mockBlocks[blockNum];
|
|
43
|
+
return { result: block ?? null };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case "eth_getTransactionReceipt": {
|
|
47
|
+
const txHash = params[0] as string;
|
|
48
|
+
if (txHash === "0x0000") return { result: null };
|
|
49
|
+
return {
|
|
50
|
+
result: {
|
|
51
|
+
transactionHash: txHash,
|
|
52
|
+
blockNumber: "0x10",
|
|
53
|
+
status: "0x1",
|
|
54
|
+
gasUsed: "0x5208",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "eth_getBalance":
|
|
60
|
+
return { result: "0xde0b6b3a7640000" }; // 1 ETH in wei
|
|
61
|
+
|
|
62
|
+
case "eth_getCode": {
|
|
63
|
+
const addr = params[0] as string;
|
|
64
|
+
if (addr === "0xeoa") return { result: "0x" };
|
|
65
|
+
return { result: "0x6060604052" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "eth_chainId":
|
|
69
|
+
return { result: "0x1" };
|
|
70
|
+
|
|
71
|
+
default:
|
|
72
|
+
return { error: { code: -32601, message: "Method not found" } };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let server: Server;
|
|
77
|
+
let baseUrl: string;
|
|
78
|
+
|
|
79
|
+
function createMockRpcServer(): Promise<{ server: Server; baseUrl: string }> {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const srv = createServer((req, res) => {
|
|
82
|
+
res.setHeader("Content-Type", "application/json");
|
|
83
|
+
|
|
84
|
+
const chunks: Buffer[] = [];
|
|
85
|
+
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
86
|
+
req.on("end", () => {
|
|
87
|
+
const raw = Buffer.concat(chunks).toString();
|
|
88
|
+
const body = JSON.parse(raw);
|
|
89
|
+
|
|
90
|
+
// Batch request
|
|
91
|
+
if (Array.isArray(body)) {
|
|
92
|
+
const results = body.map((req: { id: number; method: string; params: unknown[] }) => {
|
|
93
|
+
const { result, error } = handleRpcMethod(req.method, req.params ?? []);
|
|
94
|
+
if (error) {
|
|
95
|
+
return { jsonrpc: "2.0", id: req.id, error };
|
|
96
|
+
}
|
|
97
|
+
return { jsonrpc: "2.0", id: req.id, result };
|
|
98
|
+
});
|
|
99
|
+
res.writeHead(200);
|
|
100
|
+
res.end(JSON.stringify(results));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Single request
|
|
105
|
+
const { result, error } = handleRpcMethod(body.method, body.params ?? []);
|
|
106
|
+
if (error) {
|
|
107
|
+
res.writeHead(200);
|
|
108
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id, error }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
res.writeHead(200);
|
|
112
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id, result }));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
srv.listen(0, () => {
|
|
117
|
+
const addr = srv.address();
|
|
118
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
119
|
+
resolve({ server: srv, baseUrl: `http://localhost:${port}` });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- EVM Resource configuration ---
|
|
125
|
+
|
|
126
|
+
function createEvmResources(): RpcResource[] {
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
name: "blocks",
|
|
130
|
+
idField: "number",
|
|
131
|
+
methods: {
|
|
132
|
+
read: {
|
|
133
|
+
method: "eth_getBlockByNumber",
|
|
134
|
+
params: (ctx) => [toHex(ctx.id!), false],
|
|
135
|
+
},
|
|
136
|
+
list: {
|
|
137
|
+
method: "eth_blockNumber",
|
|
138
|
+
params: () => [],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
transform: {
|
|
142
|
+
list: (result) => {
|
|
143
|
+
const latest = fromHex(result as string);
|
|
144
|
+
return Array.from({ length: 10 }, (_, i) => `${latest - i}.json`);
|
|
145
|
+
},
|
|
146
|
+
read: (data) => ({
|
|
147
|
+
...(data as object),
|
|
148
|
+
_number: fromHex((data as { number: string }).number),
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "txs",
|
|
154
|
+
idField: "hash",
|
|
155
|
+
methods: {
|
|
156
|
+
read: {
|
|
157
|
+
method: "eth_getTransactionReceipt",
|
|
158
|
+
params: (ctx) => [ctx.id!],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "balances",
|
|
164
|
+
methods: {
|
|
165
|
+
read: {
|
|
166
|
+
method: "eth_getBalance",
|
|
167
|
+
params: (ctx) => [ctx.id!, "latest"],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
transform: {
|
|
171
|
+
read: (data) => ({
|
|
172
|
+
balance: data,
|
|
173
|
+
balanceWei: fromHex(data as string),
|
|
174
|
+
balanceEth: fromHex(data as string) / 1e18,
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "code",
|
|
180
|
+
methods: {
|
|
181
|
+
read: {
|
|
182
|
+
method: "eth_getCode",
|
|
183
|
+
params: (ctx) => [ctx.id!, "latest"],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
transform: {
|
|
187
|
+
read: (data) => ({
|
|
188
|
+
code: data,
|
|
189
|
+
isContract: (data as string) !== "0x",
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "chain",
|
|
195
|
+
methods: {
|
|
196
|
+
read: {
|
|
197
|
+
method: "eth_chainId",
|
|
198
|
+
params: () => [],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
transform: {
|
|
202
|
+
read: (data) => ({
|
|
203
|
+
chainId: data,
|
|
204
|
+
chainIdDecimal: fromHex(data as string),
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Tests ---
|
|
212
|
+
|
|
213
|
+
describe("RpcBackend (EVM)", () => {
|
|
214
|
+
let backend: RpcBackend;
|
|
215
|
+
let transport: JsonRpcTransport;
|
|
216
|
+
|
|
217
|
+
beforeAll(async () => {
|
|
218
|
+
const mock = await createMockRpcServer();
|
|
219
|
+
server = mock.server;
|
|
220
|
+
baseUrl = mock.baseUrl;
|
|
221
|
+
|
|
222
|
+
transport = new JsonRpcTransport({ url: baseUrl });
|
|
223
|
+
backend = new RpcBackend({
|
|
224
|
+
transport,
|
|
225
|
+
resources: createEvmResources(),
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
afterAll(
|
|
230
|
+
() =>
|
|
231
|
+
new Promise<void>((resolve) => {
|
|
232
|
+
server.close(() => resolve());
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// 1. Root listing
|
|
237
|
+
it("should list all resources at root", async () => {
|
|
238
|
+
const result = await backend.list("/");
|
|
239
|
+
expect(result).toContain("blocks/");
|
|
240
|
+
expect(result).toContain("txs/");
|
|
241
|
+
expect(result).toContain("balances/");
|
|
242
|
+
expect(result).toContain("code/");
|
|
243
|
+
expect(result).toContain("chain/");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// 2. Blocks list
|
|
247
|
+
it("should list recent blocks", async () => {
|
|
248
|
+
const result = await backend.list("/blocks/");
|
|
249
|
+
expect(result).toHaveLength(10);
|
|
250
|
+
expect(result[0]).toBe("16.json");
|
|
251
|
+
expect(result[9]).toBe("7.json");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// 3. Block read
|
|
255
|
+
it("should read a block by number", async () => {
|
|
256
|
+
const result = (await backend.read("/blocks/16.json")) as {
|
|
257
|
+
number: string;
|
|
258
|
+
_number: number;
|
|
259
|
+
};
|
|
260
|
+
expect(result._number).toBe(16);
|
|
261
|
+
expect(result.number).toBe("0x10");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// 4. Block not found
|
|
265
|
+
it("should throw NotFoundError for non-existent block", async () => {
|
|
266
|
+
await expect(backend.read("/blocks/999.json")).rejects.toThrow("Not found");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// 5. Transaction read
|
|
270
|
+
it("should read a transaction receipt", async () => {
|
|
271
|
+
const result = (await backend.read("/txs/0xabc.json")) as {
|
|
272
|
+
transactionHash: string;
|
|
273
|
+
status: string;
|
|
274
|
+
};
|
|
275
|
+
expect(result.transactionHash).toBe("0xabc");
|
|
276
|
+
expect(result.status).toBe("0x1");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// 6. Transaction not found
|
|
280
|
+
it("should throw NotFoundError for non-existent transaction", async () => {
|
|
281
|
+
await expect(backend.read("/txs/0x0000.json")).rejects.toThrow("Not found");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 7. No list method
|
|
285
|
+
it("should return empty array for resources without list method", async () => {
|
|
286
|
+
const result = await backend.list("/txs/");
|
|
287
|
+
expect(result).toEqual([]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// 8. Balance read
|
|
291
|
+
it("should read balance with ETH conversion", async () => {
|
|
292
|
+
const result = (await backend.read("/balances/0xdead.json")) as {
|
|
293
|
+
balanceEth: number;
|
|
294
|
+
};
|
|
295
|
+
expect(result.balanceEth).toBe(1);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// 9. Contract code
|
|
299
|
+
it("should detect contract code", async () => {
|
|
300
|
+
const result = (await backend.read("/code/0xcontract.json")) as {
|
|
301
|
+
isContract: boolean;
|
|
302
|
+
};
|
|
303
|
+
expect(result.isContract).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// 10. EOA detection
|
|
307
|
+
it("should detect EOA (no code)", async () => {
|
|
308
|
+
const result = (await backend.read("/code/0xeoa.json")) as {
|
|
309
|
+
isContract: boolean;
|
|
310
|
+
};
|
|
311
|
+
expect(result.isContract).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// 11. Chain info
|
|
315
|
+
it("should read chain info", async () => {
|
|
316
|
+
const result = (await backend.read("/chain/info.json")) as {
|
|
317
|
+
chainIdDecimal: number;
|
|
318
|
+
};
|
|
319
|
+
expect(result.chainIdDecimal).toBe(1);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// 12. Unknown resource
|
|
323
|
+
it("should throw NotFoundError for unknown resource", async () => {
|
|
324
|
+
await expect(backend.read("/unknown/1.json")).rejects.toThrow("Not found");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// 13. RPC error
|
|
328
|
+
it("should throw RpcError for unknown RPC method", async () => {
|
|
329
|
+
await expect(
|
|
330
|
+
transport.call("eth_nonExistent", []),
|
|
331
|
+
).rejects.toThrow(RpcError);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// 14. Search fallback
|
|
335
|
+
it("should search blocks via client-side filter", async () => {
|
|
336
|
+
// eth_blockNumber returns "0x10" which contains "0x1"
|
|
337
|
+
const results = await backend.search("/blocks/", "0x1");
|
|
338
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// 15. AgentFs integration
|
|
342
|
+
describe("integration with AgentFs", async () => {
|
|
343
|
+
const { AgentFs } = await import("../src/agent-fs.js");
|
|
344
|
+
|
|
345
|
+
it("should work as a mount in AgentFs", async () => {
|
|
346
|
+
const agentFs = new AgentFs({
|
|
347
|
+
mounts: [{ path: "/eth", backend }],
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const lsRoot = await agentFs.execute("ls /");
|
|
351
|
+
expect(lsRoot.ok).toBe(true);
|
|
352
|
+
if (lsRoot.ok) {
|
|
353
|
+
expect(lsRoot.data).toContain("eth/");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const lsEth = await agentFs.execute("ls /eth/");
|
|
357
|
+
expect(lsEth.ok).toBe(true);
|
|
358
|
+
if (lsEth.ok) {
|
|
359
|
+
expect(lsEth.data).toContain("blocks/");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const catBlock = await agentFs.execute("cat /eth/blocks/16.json");
|
|
363
|
+
expect(catBlock.ok).toBe(true);
|
|
364
|
+
if (catBlock.ok) {
|
|
365
|
+
expect((catBlock.data as { _number: number })._number).toBe(16);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const catBalance = await agentFs.execute(
|
|
369
|
+
"cat /eth/balances/0xdead.json",
|
|
370
|
+
);
|
|
371
|
+
expect(catBalance.ok).toBe(true);
|
|
372
|
+
if (catBalance.ok) {
|
|
373
|
+
expect((catBalance.data as { balanceEth: number }).balanceEth).toBe(1);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Batch transport test
|
|
379
|
+
describe("JsonRpcTransport batch", () => {
|
|
380
|
+
it("should handle batch requests", async () => {
|
|
381
|
+
const results = await transport.batch!([
|
|
382
|
+
{ method: "eth_chainId", params: [] },
|
|
383
|
+
{ method: "eth_blockNumber", params: [] },
|
|
384
|
+
]);
|
|
385
|
+
expect(results).toHaveLength(2);
|
|
386
|
+
expect(results[0]).toBe("0x1");
|
|
387
|
+
expect(results[1]).toBe("0x10");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
package/tsconfig.json
ADDED