voltaire-effect 0.3.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/{X25519Test-D5Q-5fL9.d.ts → X25519Test-avt1DUgp.d.ts} +231 -6
  2. package/dist/crypto/index.d.ts +2 -2
  3. package/dist/crypto/index.js +72 -2
  4. package/dist/{index-3UKSP3cd.d.ts → index-DxwZo3xo.d.ts} +7781 -5513
  5. package/dist/index.d.ts +990 -3096
  6. package/dist/index.js +2374 -1652
  7. package/dist/native/index.d.ts +6 -6
  8. package/dist/native/index.js +2399 -1677
  9. package/dist/primitives/index.d.ts +7 -6
  10. package/dist/primitives/index.js +2966 -2361
  11. package/dist/services/index.d.ts +1631 -1255
  12. package/dist/services/index.js +4493 -3977
  13. package/package.json +7 -3
  14. package/src/crypto/Signers/SignersService.ts +1 -1
  15. package/src/crypto/Signers/errors.ts +29 -0
  16. package/src/crypto/Signers/index.ts +1 -0
  17. package/src/crypto/Signers/operations.ts +26 -8
  18. package/src/crypto/index.ts +10 -11
  19. package/src/index.ts +1 -2
  20. package/src/jsonrpc/Anvil.ts +13 -8
  21. package/src/jsonrpc/Eth.ts +13 -8
  22. package/src/jsonrpc/Hardhat.ts +13 -8
  23. package/src/jsonrpc/IdCounter.ts +21 -5
  24. package/src/jsonrpc/JsonRpc.test.ts +126 -61
  25. package/src/jsonrpc/Net.ts +13 -8
  26. package/src/jsonrpc/Request.ts +16 -8
  27. package/src/jsonrpc/Txpool.ts +13 -8
  28. package/src/jsonrpc/Wallet.ts +13 -8
  29. package/src/jsonrpc/Web3.ts +13 -8
  30. package/src/jsonrpc/index.ts +1 -1
  31. package/src/primitives/Abi/AbiSchema.ts +3 -4
  32. package/src/primitives/Abi/fromBytecode.test.ts +47 -0
  33. package/src/primitives/Abi/fromBytecode.ts +81 -0
  34. package/src/primitives/Abi/index.ts +3 -0
  35. package/src/primitives/AccessList/from.ts +12 -9
  36. package/src/primitives/Address/Checksummed.ts +21 -27
  37. package/src/primitives/Address/from.ts +12 -15
  38. package/src/primitives/Address/toHex.ts +2 -1
  39. package/src/primitives/Base64/from.ts +21 -4
  40. package/src/primitives/Blob/from.ts +12 -4
  41. package/src/primitives/BlockHash/index.ts +2 -2
  42. package/src/primitives/BlockNumber/index.ts +3 -3
  43. package/src/primitives/Bytecode/from.ts +11 -2
  44. package/src/primitives/ContractSignature/verifySignature.ts +3 -5
  45. package/src/primitives/Ens/from.ts +12 -11
  46. package/src/primitives/Hex/from.ts +12 -4
  47. package/src/primitives/Signature/from.ts +11 -2
  48. package/src/primitives/Transaction/EIP2930/index.ts +12 -12
  49. package/src/primitives/Transaction/EIP4844/index.ts +14 -14
  50. package/src/primitives/Transaction/EIP7702/index.ts +13 -13
  51. package/src/primitives/Transaction/Legacy/index.ts +13 -13
  52. package/src/primitives/TransactionHash/index.ts +3 -2
  53. package/src/primitives/TransactionIndex/index.ts +2 -2
  54. package/src/primitives/Trie/Trie.test.ts +70 -0
  55. package/src/primitives/Trie/TrieSchema.ts +26 -0
  56. package/src/primitives/Trie/clear.ts +16 -0
  57. package/src/primitives/Trie/del.ts +18 -0
  58. package/src/primitives/Trie/get.ts +18 -0
  59. package/src/primitives/Trie/index.ts +30 -0
  60. package/src/primitives/Trie/init.ts +13 -0
  61. package/src/primitives/Trie/prove.ts +19 -0
  62. package/src/primitives/Trie/put.ts +20 -0
  63. package/src/primitives/Trie/rootHash.ts +14 -0
  64. package/src/primitives/Trie/verify.ts +18 -0
  65. package/src/primitives/Uint/from.ts +11 -2
  66. package/src/primitives/Uint16/index.ts +5 -4
  67. package/src/primitives/Uint64/index.ts +5 -4
  68. package/src/primitives/Uint8/index.ts +5 -4
  69. package/src/primitives/index.ts +3 -2
  70. package/src/services/BlockExplorerApi/BlockExplorerApi.test.ts +789 -0
  71. package/src/services/BlockExplorerApi/BlockExplorerApi.ts +797 -0
  72. package/src/services/BlockExplorerApi/BlockExplorerApiErrors.ts +176 -0
  73. package/src/services/BlockExplorerApi/BlockExplorerApiService.ts +60 -0
  74. package/src/services/BlockExplorerApi/BlockExplorerApiTypes.ts +225 -0
  75. package/src/services/BlockExplorerApi/index.ts +42 -0
  76. package/src/services/Contract/Contract.test.ts +2 -6
  77. package/src/services/Contract/ContractTypes.ts +26 -8
  78. package/src/services/Contract/estimateGas.test.ts +4 -7
  79. package/src/services/Provider/actions/multicall.ts +28 -9
  80. package/src/services/Provider/actions/readContract.test.ts +8 -11
  81. package/src/services/Provider/actions/readContract.ts +28 -9
  82. package/src/services/Provider/functions/getBlock.ts +2 -1
  83. package/src/services/Provider/functions/getBlockReceipts.ts +2 -1
  84. package/src/services/Provider/functions/getBlockTransactionCount.ts +2 -1
  85. package/src/services/Provider/functions/getUncle.ts +2 -1
  86. package/src/services/Provider/functions/getUncleCount.ts +2 -1
  87. package/src/services/Signer/actions/deployContract.ts +1 -1
  88. package/src/services/index.ts +25 -0
@@ -0,0 +1,789 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Exit from "effect/Exit";
5
+ import { BlockExplorerApiService, type BlockExplorerApiShape } from "./BlockExplorerApiService";
6
+ import {
7
+ BlockExplorerConfigError,
8
+ BlockExplorerNotFoundError,
9
+ BlockExplorerRateLimitError,
10
+ } from "./BlockExplorerApiErrors";
11
+ import type { ExplorerContractInstance, AbiItem } from "./BlockExplorerApiTypes";
12
+ import { ChainService, type ChainConfig } from "../Chain/ChainService";
13
+ import { ContractCallError, ContractWriteError } from "../Contract/ContractTypes";
14
+
15
+ // Mock chain config
16
+ const mockMainnetConfig: ChainConfig = {
17
+ id: 1,
18
+ name: "Ethereum",
19
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
20
+ blockTime: 12000,
21
+ };
22
+
23
+ const mockChainLayer = Layer.succeed(ChainService, mockMainnetConfig);
24
+
25
+ // Mock ABI for testing
26
+ const mockAbi: AbiItem[] = [
27
+ {
28
+ type: "function",
29
+ name: "symbol",
30
+ inputs: [],
31
+ outputs: [{ name: "", type: "string" }],
32
+ stateMutability: "view",
33
+ },
34
+ ];
35
+
36
+ // Mock contract response with method maps
37
+ const createMockContract = (address: `0x${string}`, abi: AbiItem[] = mockAbi): ExplorerContractInstance => ({
38
+ address,
39
+ requestedAddress: address,
40
+ abi,
41
+ resolution: { mode: "verified", source: "sourcify" },
42
+ read: {
43
+ symbol: () => Effect.succeed("MOCK"),
44
+ "symbol()": () => Effect.succeed("MOCK"),
45
+ },
46
+ write: {},
47
+ simulate: {},
48
+ call: (_sig, _args) => Effect.succeed("MOCK"),
49
+ });
50
+
51
+ // Create mock service layer
52
+ const createMockService = (overrides?: Partial<BlockExplorerApiShape>): Layer.Layer<BlockExplorerApiService> => {
53
+ const defaultShape: BlockExplorerApiShape = {
54
+ getContract: (address, _options) => Effect.succeed(createMockContract(address)),
55
+ getAbi: (address, _options) => Effect.succeed(mockAbi),
56
+ getSources: (_address, _options) => Effect.succeed([]),
57
+ };
58
+ return Layer.succeed(BlockExplorerApiService, { ...defaultShape, ...overrides });
59
+ };
60
+
61
+ describe("BlockExplorerApi", () => {
62
+ describe("Error typing guarantees", () => {
63
+ it("BlockExplorerNotFoundError has correct tag and fields", () => {
64
+ const error = new BlockExplorerNotFoundError(
65
+ "0x1234567890123456789012345678901234567890",
66
+ ["sourcify", "etherscanV2"],
67
+ );
68
+
69
+ expect(error._tag).toBe("BlockExplorerNotFoundError");
70
+ expect(error.address).toBe("0x1234567890123456789012345678901234567890");
71
+ expect(error.attemptedSources).toEqual(["sourcify", "etherscanV2"]);
72
+ expect(error.message).toContain("No ABI found");
73
+ });
74
+
75
+ it("BlockExplorerRateLimitError has correct tag and fields", () => {
76
+ const error = new BlockExplorerRateLimitError(
77
+ "etherscanV2",
78
+ "0x1234567890123456789012345678901234567890",
79
+ "Rate limit exceeded",
80
+ 60,
81
+ );
82
+
83
+ expect(error._tag).toBe("BlockExplorerRateLimitError");
84
+ expect(error.source).toBe("etherscanV2");
85
+ expect(error.retryAfterSeconds).toBe(60);
86
+ });
87
+
88
+ it("BlockExplorerConfigError has correct tag", () => {
89
+ const error = new BlockExplorerConfigError("Test message");
90
+
91
+ expect(error._tag).toBe("BlockExplorerConfigError");
92
+ expect(error.message).toBe("Test message");
93
+ });
94
+
95
+ it("errors can be caught with Effect.catchTag", async () => {
96
+ const mockService = createMockService({
97
+ getAbi: (_address, _options) =>
98
+ Effect.fail(new BlockExplorerNotFoundError("0x0000000000000000000000000000000000000001", ["sourcify"])),
99
+ });
100
+
101
+ const program = Effect.gen(function* () {
102
+ const explorer = yield* BlockExplorerApiService;
103
+ return yield* explorer.getAbi("0x0000000000000000000000000000000000000001");
104
+ }).pipe(
105
+ Effect.catchTag("BlockExplorerNotFoundError", (e) =>
106
+ Effect.succeed(`Caught: ${e._tag}`),
107
+ ),
108
+ Effect.provide(mockService),
109
+ Effect.provide(mockChainLayer),
110
+ );
111
+
112
+ const result = await Effect.runPromise(program);
113
+ expect(result).toBe("Caught: BlockExplorerNotFoundError");
114
+ });
115
+ });
116
+
117
+ describe("Service interface", () => {
118
+ it("provides getContract method", async () => {
119
+ const program = Effect.gen(function* () {
120
+ const explorer = yield* BlockExplorerApiService;
121
+ return typeof explorer.getContract === "function";
122
+ }).pipe(
123
+ Effect.provide(createMockService()),
124
+ Effect.provide(mockChainLayer),
125
+ );
126
+
127
+ const result = await Effect.runPromise(program);
128
+ expect(result).toBe(true);
129
+ });
130
+
131
+ it("provides getAbi method", async () => {
132
+ const program = Effect.gen(function* () {
133
+ const explorer = yield* BlockExplorerApiService;
134
+ return typeof explorer.getAbi === "function";
135
+ }).pipe(
136
+ Effect.provide(createMockService()),
137
+ Effect.provide(mockChainLayer),
138
+ );
139
+
140
+ const result = await Effect.runPromise(program);
141
+ expect(result).toBe(true);
142
+ });
143
+
144
+ it("provides getSources method", async () => {
145
+ const program = Effect.gen(function* () {
146
+ const explorer = yield* BlockExplorerApiService;
147
+ return typeof explorer.getSources === "function";
148
+ }).pipe(
149
+ Effect.provide(createMockService()),
150
+ Effect.provide(mockChainLayer),
151
+ );
152
+
153
+ const result = await Effect.runPromise(program);
154
+ expect(result).toBe(true);
155
+ });
156
+
157
+ it("getContract returns ResolvedExplorerContract", async () => {
158
+ const program = Effect.gen(function* () {
159
+ const explorer = yield* BlockExplorerApiService;
160
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
161
+ }).pipe(
162
+ Effect.provide(createMockService()),
163
+ Effect.provide(mockChainLayer),
164
+ );
165
+
166
+ const result = await Effect.runPromise(program);
167
+
168
+ expect(result.address).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
169
+ expect(result.abi).toEqual(mockAbi);
170
+ expect(result.resolution.mode).toBe("verified");
171
+ });
172
+
173
+ it("getAbi returns ABI array", async () => {
174
+ const program = Effect.gen(function* () {
175
+ const explorer = yield* BlockExplorerApiService;
176
+ return yield* explorer.getAbi("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
177
+ }).pipe(
178
+ Effect.provide(createMockService()),
179
+ Effect.provide(mockChainLayer),
180
+ );
181
+
182
+ const result = await Effect.runPromise(program);
183
+ expect(Array.isArray(result)).toBe(true);
184
+ expect(result).toEqual(mockAbi);
185
+ });
186
+ });
187
+
188
+ describe("Resolution options", () => {
189
+ it("accepts resolution option in getAbi", async () => {
190
+ const program = Effect.gen(function* () {
191
+ const explorer = yield* BlockExplorerApiService;
192
+ return yield* explorer.getAbi("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", {
193
+ resolution: "verified-only",
194
+ });
195
+ }).pipe(
196
+ Effect.provide(createMockService()),
197
+ Effect.provide(mockChainLayer),
198
+ );
199
+
200
+ const result = await Effect.runPromise(program);
201
+ expect(Array.isArray(result)).toBe(true);
202
+ });
203
+
204
+ it("accepts followProxies option in getContract", async () => {
205
+ const program = Effect.gen(function* () {
206
+ const explorer = yield* BlockExplorerApiService;
207
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", {
208
+ followProxies: true,
209
+ });
210
+ }).pipe(
211
+ Effect.provide(createMockService()),
212
+ Effect.provide(mockChainLayer),
213
+ );
214
+
215
+ const result = await Effect.runPromise(program);
216
+ expect(result).toBeDefined();
217
+ });
218
+
219
+ it("accepts includeSources option", async () => {
220
+ const mockWithSources = createMockService({
221
+ getContract: (address, _options) =>
222
+ Effect.succeed({
223
+ ...createMockContract(address),
224
+ sources: [{ path: "Contract.sol", content: "// SPDX..." }],
225
+ }),
226
+ });
227
+
228
+ const program = Effect.gen(function* () {
229
+ const explorer = yield* BlockExplorerApiService;
230
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", {
231
+ includeSources: true,
232
+ });
233
+ }).pipe(
234
+ Effect.provide(mockWithSources),
235
+ Effect.provide(mockChainLayer),
236
+ );
237
+
238
+ const result = await Effect.runPromise(program);
239
+ expect(result.sources).toBeDefined();
240
+ expect(result.sources?.length).toBeGreaterThan(0);
241
+ });
242
+ });
243
+
244
+ describe("Error handling", () => {
245
+ it("propagates BlockExplorerNotFoundError", async () => {
246
+ const mockService = createMockService({
247
+ getAbi: (_address, _options) =>
248
+ Effect.fail(new BlockExplorerNotFoundError("0x0000000000000000000000000000000000000001", ["sourcify", "etherscanV2"])),
249
+ });
250
+
251
+ const program = Effect.gen(function* () {
252
+ const explorer = yield* BlockExplorerApiService;
253
+ return yield* explorer.getAbi("0x0000000000000000000000000000000000000001");
254
+ }).pipe(
255
+ Effect.provide(mockService),
256
+ Effect.provide(mockChainLayer),
257
+ );
258
+
259
+ const exit = await Effect.runPromiseExit(program);
260
+ expect(Exit.isFailure(exit)).toBe(true);
261
+ });
262
+
263
+ it("propagates BlockExplorerRateLimitError", async () => {
264
+ const mockService = createMockService({
265
+ getAbi: (_address, _options) =>
266
+ Effect.fail(new BlockExplorerRateLimitError("etherscanV2", "0x0000000000000000000000000000000000000001", "Rate limited", 60)),
267
+ });
268
+
269
+ const program = Effect.gen(function* () {
270
+ const explorer = yield* BlockExplorerApiService;
271
+ return yield* explorer.getAbi("0x0000000000000000000000000000000000000001");
272
+ }).pipe(
273
+ Effect.catchTag("BlockExplorerRateLimitError", (e) =>
274
+ Effect.succeed(`Rate limited by ${e.source}, retry in ${e.retryAfterSeconds}s`),
275
+ ),
276
+ Effect.provide(mockService),
277
+ Effect.provide(mockChainLayer),
278
+ );
279
+
280
+ const result = await Effect.runPromise(program);
281
+ expect(result).toBe("Rate limited by etherscanV2, retry in 60s");
282
+ });
283
+
284
+ it("getSources fails when no sources available", async () => {
285
+ const mockService = createMockService({
286
+ getSources: (_address, _options) =>
287
+ Effect.fail(new BlockExplorerNotFoundError("0x0000000000000000000000000000000000000001", ["sources"])),
288
+ });
289
+
290
+ const program = Effect.gen(function* () {
291
+ const explorer = yield* BlockExplorerApiService;
292
+ return yield* explorer.getSources("0x0000000000000000000000000000000000000001");
293
+ }).pipe(
294
+ Effect.catchTag("BlockExplorerNotFoundError", () =>
295
+ Effect.succeed("no sources"),
296
+ ),
297
+ Effect.provide(mockService),
298
+ Effect.provide(mockChainLayer),
299
+ );
300
+
301
+ const result = await Effect.runPromise(program);
302
+ expect(result).toBe("no sources");
303
+ });
304
+ });
305
+
306
+ describe("Resolution modes", () => {
307
+ it("verified-only fails if not verified", async () => {
308
+ const mockService = createMockService({
309
+ getContract: (_address, options) => {
310
+ if (options?.resolution === "verified-only") {
311
+ return Effect.fail(new BlockExplorerNotFoundError(_address, ["sourcify"]));
312
+ }
313
+ return Effect.succeed(createMockContract(_address));
314
+ },
315
+ });
316
+
317
+ const program = Effect.gen(function* () {
318
+ const explorer = yield* BlockExplorerApiService;
319
+ return yield* explorer.getContract("0x0000000000000000000000000000000000000001", {
320
+ resolution: "verified-only",
321
+ });
322
+ }).pipe(
323
+ Effect.catchTag("BlockExplorerNotFoundError", () =>
324
+ Effect.succeed("not verified"),
325
+ ),
326
+ Effect.provide(mockService),
327
+ Effect.provide(mockChainLayer),
328
+ );
329
+
330
+ const result = await Effect.runPromise(program);
331
+ expect(result).toBe("not verified");
332
+ });
333
+
334
+ it("verified-first returns first successful result", async () => {
335
+ const mockService = createMockService({
336
+ getContract: (address, _options) =>
337
+ Effect.succeed({
338
+ ...createMockContract(address),
339
+ resolution: { mode: "verified" as const, source: "etherscanV2" as const },
340
+ }),
341
+ });
342
+
343
+ const program = Effect.gen(function* () {
344
+ const explorer = yield* BlockExplorerApiService;
345
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", {
346
+ resolution: "verified-first",
347
+ });
348
+ }).pipe(
349
+ Effect.provide(mockService),
350
+ Effect.provide(mockChainLayer),
351
+ );
352
+
353
+ const result = await Effect.runPromise(program);
354
+ expect(result.resolution.source).toBe("etherscanV2");
355
+ });
356
+
357
+ it("best-effort can return non-verified ABI", async () => {
358
+ const mockService = createMockService({
359
+ getContract: (address, _options) =>
360
+ Effect.succeed({
361
+ ...createMockContract(address),
362
+ resolution: { mode: "best-effort" as const, source: "whatsabi" as const },
363
+ }),
364
+ });
365
+
366
+ const program = Effect.gen(function* () {
367
+ const explorer = yield* BlockExplorerApiService;
368
+ return yield* explorer.getContract("0x0000000000000000000000000000000000000001", {
369
+ resolution: "best-effort",
370
+ });
371
+ }).pipe(
372
+ Effect.provide(mockService),
373
+ Effect.provide(mockChainLayer),
374
+ );
375
+
376
+ const result = await Effect.runPromise(program);
377
+ expect(result.resolution.mode).toBe("best-effort");
378
+ });
379
+ });
380
+
381
+ describe("Proxy resolution", () => {
382
+ it("followProxies=true resolves implementation address", async () => {
383
+ const mockService = createMockService({
384
+ getContract: (address, options) => {
385
+ if (options?.followProxies) {
386
+ return Effect.succeed({
387
+ address: "0x1111111111111111111111111111111111111111",
388
+ requestedAddress: address,
389
+ abi: mockAbi,
390
+ resolution: { mode: "verified" as const, source: "sourcify" as const },
391
+ proxies: [{ kind: "EIP-1967", address }],
392
+ });
393
+ }
394
+ return Effect.succeed(createMockContract(address));
395
+ },
396
+ });
397
+
398
+ const program = Effect.gen(function* () {
399
+ const explorer = yield* BlockExplorerApiService;
400
+ return yield* explorer.getContract("0x2222222222222222222222222222222222222222", {
401
+ followProxies: true,
402
+ });
403
+ }).pipe(
404
+ Effect.provide(mockService),
405
+ Effect.provide(mockChainLayer),
406
+ );
407
+
408
+ const result = await Effect.runPromise(program);
409
+ expect(result.address).toBe("0x1111111111111111111111111111111111111111");
410
+ expect(result.requestedAddress).toBe("0x2222222222222222222222222222222222222222");
411
+ expect(result.proxies).toBeDefined();
412
+ expect(result.proxies?.length).toBeGreaterThan(0);
413
+ });
414
+
415
+ it("followProxies=false returns original address", async () => {
416
+ const program = Effect.gen(function* () {
417
+ const explorer = yield* BlockExplorerApiService;
418
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", {
419
+ followProxies: false,
420
+ });
421
+ }).pipe(
422
+ Effect.provide(createMockService()),
423
+ Effect.provide(mockChainLayer),
424
+ );
425
+
426
+ const result = await Effect.runPromise(program);
427
+ expect(result.address).toBe(result.requestedAddress);
428
+ });
429
+ });
430
+
431
+ describe("Error type exhaustiveness", () => {
432
+ it("BlockExplorerDecodeError can be caught with catchTag", async () => {
433
+ const { BlockExplorerDecodeError } = await import("./BlockExplorerApiErrors");
434
+ const mockService = createMockService({
435
+ getAbi: (_address, _options) =>
436
+ Effect.fail(new BlockExplorerDecodeError("sourcify", "0x0000000000000000000000000000000000000001", "Invalid JSON", "{ bad json")),
437
+ });
438
+
439
+ const program = Effect.gen(function* () {
440
+ const explorer = yield* BlockExplorerApiService;
441
+ return yield* explorer.getAbi("0x0000000000000000000000000000000000000001");
442
+ }).pipe(
443
+ Effect.catchTag("BlockExplorerDecodeError", (e) =>
444
+ Effect.succeed(`Decode error: ${e.source}`),
445
+ ),
446
+ Effect.provide(mockService),
447
+ Effect.provide(mockChainLayer),
448
+ );
449
+
450
+ const result = await Effect.runPromise(program);
451
+ expect(result).toBe("Decode error: sourcify");
452
+ });
453
+
454
+ it("BlockExplorerResponseError can be caught with catchTag", async () => {
455
+ const { BlockExplorerResponseError } = await import("./BlockExplorerApiErrors");
456
+ const mockService = createMockService({
457
+ getAbi: (_address, _options) =>
458
+ Effect.fail(new BlockExplorerResponseError("etherscanV2", "0x0000000000000000000000000000000000000001", "Server error", { status: 500 })),
459
+ });
460
+
461
+ const program = Effect.gen(function* () {
462
+ const explorer = yield* BlockExplorerApiService;
463
+ return yield* explorer.getAbi("0x0000000000000000000000000000000000000001");
464
+ }).pipe(
465
+ Effect.catchTag("BlockExplorerResponseError", (e) =>
466
+ Effect.succeed(`Response error: ${e.status}`),
467
+ ),
468
+ Effect.provide(mockService),
469
+ Effect.provide(mockChainLayer),
470
+ );
471
+
472
+ const result = await Effect.runPromise(program);
473
+ expect(result).toBe("Response error: 500");
474
+ });
475
+
476
+ it("BlockExplorerProxyResolutionError can be caught with catchTag", async () => {
477
+ const { BlockExplorerProxyResolutionError } = await import("./BlockExplorerApiErrors");
478
+ const mockService = createMockService({
479
+ getContract: (_address, _options) =>
480
+ Effect.fail(new BlockExplorerProxyResolutionError("0x0000000000000000000000000000000000000001", "Cyclic proxy detected")),
481
+ });
482
+
483
+ const program = Effect.gen(function* () {
484
+ const explorer = yield* BlockExplorerApiService;
485
+ return yield* explorer.getContract("0x0000000000000000000000000000000000000001", { followProxies: true });
486
+ }).pipe(
487
+ Effect.catchTag("BlockExplorerProxyResolutionError", (e) =>
488
+ Effect.succeed(`Proxy error: ${e.message}`),
489
+ ),
490
+ Effect.provide(mockService),
491
+ Effect.provide(mockChainLayer),
492
+ );
493
+
494
+ const result = await Effect.runPromise(program);
495
+ expect(result).toBe("Proxy error: Cyclic proxy detected");
496
+ });
497
+
498
+ it("BlockExplorerUnexpectedError can be caught with catchTag", async () => {
499
+ const { BlockExplorerUnexpectedError } = await import("./BlockExplorerApiErrors");
500
+ const mockService = createMockService({
501
+ getAbi: (_address, _options) =>
502
+ Effect.fail(new BlockExplorerUnexpectedError("getAbi", "Unknown error", new Error("Oops"))),
503
+ });
504
+
505
+ const program = Effect.gen(function* () {
506
+ const explorer = yield* BlockExplorerApiService;
507
+ return yield* explorer.getAbi("0x0000000000000000000000000000000000000001");
508
+ }).pipe(
509
+ Effect.catchTag("BlockExplorerUnexpectedError", (e) =>
510
+ Effect.succeed(`Unexpected error in ${e.phase}`),
511
+ ),
512
+ Effect.provide(mockService),
513
+ Effect.provide(mockChainLayer),
514
+ );
515
+
516
+ const result = await Effect.runPromise(program);
517
+ expect(result).toBe("Unexpected error in getAbi");
518
+ });
519
+ });
520
+
521
+ describe("ABI normalization", () => {
522
+ it("normalizes function ABI items", async () => {
523
+ const normalizedAbi: AbiItem[] = [
524
+ {
525
+ type: "function",
526
+ name: "transfer",
527
+ inputs: [
528
+ { name: "to", type: "address" },
529
+ { name: "amount", type: "uint256" },
530
+ ],
531
+ outputs: [{ name: "", type: "bool" }],
532
+ stateMutability: "nonpayable",
533
+ },
534
+ ];
535
+
536
+ const mockService = createMockService({
537
+ getAbi: (_address, _options) => Effect.succeed(normalizedAbi),
538
+ });
539
+
540
+ const program = Effect.gen(function* () {
541
+ const explorer = yield* BlockExplorerApiService;
542
+ return yield* explorer.getAbi("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
543
+ }).pipe(
544
+ Effect.provide(mockService),
545
+ Effect.provide(mockChainLayer),
546
+ );
547
+
548
+ const result = await Effect.runPromise(program);
549
+ expect(result[0].type).toBe("function");
550
+ expect(result[0].name).toBe("transfer");
551
+ expect(result[0].inputs).toHaveLength(2);
552
+ });
553
+
554
+ it("returns empty array for contracts with no ABI", async () => {
555
+ const mockService = createMockService({
556
+ getAbi: (_address, _options) => Effect.succeed([]),
557
+ });
558
+
559
+ const program = Effect.gen(function* () {
560
+ const explorer = yield* BlockExplorerApiService;
561
+ return yield* explorer.getAbi("0x0000000000000000000000000000000000000001");
562
+ }).pipe(
563
+ Effect.provide(mockService),
564
+ Effect.provide(mockChainLayer),
565
+ );
566
+
567
+ const result = await Effect.runPromise(program);
568
+ expect(result).toEqual([]);
569
+ });
570
+ });
571
+
572
+ describe("Resolution metadata", () => {
573
+ it("includes source in resolution for verified ABI", async () => {
574
+ const mockService = createMockService({
575
+ getContract: (address, _options) =>
576
+ Effect.succeed({
577
+ ...createMockContract(address),
578
+ resolution: { mode: "verified" as const, source: "blockscout" as const },
579
+ }),
580
+ });
581
+
582
+ const program = Effect.gen(function* () {
583
+ const explorer = yield* BlockExplorerApiService;
584
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
585
+ }).pipe(
586
+ Effect.provide(mockService),
587
+ Effect.provide(mockChainLayer),
588
+ );
589
+
590
+ const result = await Effect.runPromise(program);
591
+ expect(result.resolution).toEqual({ mode: "verified", source: "blockscout" });
592
+ });
593
+
594
+ it("attemptedSources includes all tried sources", async () => {
595
+ const error = new BlockExplorerNotFoundError(
596
+ "0x0000000000000000000000000000000000000001",
597
+ ["sourcify", "etherscanV2", "blockscout"],
598
+ );
599
+
600
+ expect(error.attemptedSources).toContain("sourcify");
601
+ expect(error.attemptedSources).toContain("etherscanV2");
602
+ expect(error.attemptedSources).toContain("blockscout");
603
+ expect(error.attemptedSources).toHaveLength(3);
604
+ });
605
+ });
606
+
607
+ describe("Contract method access", () => {
608
+ const erc20Abi: AbiItem[] = [
609
+ {
610
+ type: "function",
611
+ name: "name",
612
+ inputs: [],
613
+ outputs: [{ name: "", type: "string" }],
614
+ stateMutability: "view",
615
+ },
616
+ {
617
+ type: "function",
618
+ name: "symbol",
619
+ inputs: [],
620
+ outputs: [{ name: "", type: "string" }],
621
+ stateMutability: "view",
622
+ },
623
+ {
624
+ type: "function",
625
+ name: "balanceOf",
626
+ inputs: [{ name: "account", type: "address" }],
627
+ outputs: [{ name: "", type: "uint256" }],
628
+ stateMutability: "view",
629
+ },
630
+ {
631
+ type: "function",
632
+ name: "transfer",
633
+ inputs: [
634
+ { name: "to", type: "address" },
635
+ { name: "amount", type: "uint256" },
636
+ ],
637
+ outputs: [{ name: "", type: "bool" }],
638
+ stateMutability: "nonpayable",
639
+ },
640
+ ];
641
+
642
+ it("getContract returns contract with read methods", async () => {
643
+ const mockWithMethods: ExplorerContractInstance = {
644
+ ...createMockContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", erc20Abi),
645
+ read: {
646
+ name: () => Effect.succeed("USD Coin"),
647
+ symbol: () => Effect.succeed("USDC"),
648
+ balanceOf: () => Effect.succeed(1000n),
649
+ "name()": () => Effect.succeed("USD Coin"),
650
+ "symbol()": () => Effect.succeed("USDC"),
651
+ "balanceOf(address)": () => Effect.succeed(1000n),
652
+ },
653
+ write: {
654
+ transfer: () => Effect.succeed("0x123" as `0x${string}`),
655
+ "transfer(address,uint256)": () => Effect.succeed("0x123" as `0x${string}`),
656
+ },
657
+ simulate: {
658
+ transfer: () => Effect.succeed(true),
659
+ "transfer(address,uint256)": () => Effect.succeed(true),
660
+ },
661
+ };
662
+
663
+ const mockService = createMockService({
664
+ getContract: (_address, _options) => Effect.succeed(mockWithMethods),
665
+ });
666
+
667
+ const program = Effect.gen(function* () {
668
+ const explorer = yield* BlockExplorerApiService;
669
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
670
+ }).pipe(
671
+ Effect.provide(mockService),
672
+ Effect.provide(mockChainLayer),
673
+ );
674
+
675
+ const result = await Effect.runPromise(program);
676
+
677
+ expect(typeof result.read).toBe("object");
678
+ expect(typeof result.read.name).toBe("function");
679
+ expect(typeof result.read.symbol).toBe("function");
680
+ expect(typeof result.read["balanceOf(address)"]).toBe("function");
681
+ });
682
+
683
+ it("getContract returns contract with write methods", async () => {
684
+ const mockWithMethods: ExplorerContractInstance = {
685
+ ...createMockContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", erc20Abi),
686
+ read: {},
687
+ write: {
688
+ transfer: () => Effect.succeed("0x123" as `0x${string}`),
689
+ "transfer(address,uint256)": () => Effect.succeed("0x123" as `0x${string}`),
690
+ },
691
+ simulate: {},
692
+ };
693
+
694
+ const mockService = createMockService({
695
+ getContract: (_address, _options) => Effect.succeed(mockWithMethods),
696
+ });
697
+
698
+ const program = Effect.gen(function* () {
699
+ const explorer = yield* BlockExplorerApiService;
700
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
701
+ }).pipe(
702
+ Effect.provide(mockService),
703
+ Effect.provide(mockChainLayer),
704
+ );
705
+
706
+ const result = await Effect.runPromise(program);
707
+
708
+ expect(typeof result.write).toBe("object");
709
+ expect(typeof result.write.transfer).toBe("function");
710
+ expect(typeof result.write["transfer(address,uint256)"]).toBe("function");
711
+ });
712
+
713
+ it("getContract returns contract with call method", async () => {
714
+ const program = Effect.gen(function* () {
715
+ const explorer = yield* BlockExplorerApiService;
716
+ return yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
717
+ }).pipe(
718
+ Effect.provide(createMockService()),
719
+ Effect.provide(mockChainLayer),
720
+ );
721
+
722
+ const result = await Effect.runPromise(program);
723
+
724
+ expect(typeof result.call).toBe("function");
725
+ });
726
+
727
+ it("handles overloaded functions with signature keys only", async () => {
728
+ const overloadedAbi: AbiItem[] = [
729
+ {
730
+ type: "function",
731
+ name: "safeTransferFrom",
732
+ inputs: [
733
+ { name: "from", type: "address" },
734
+ { name: "to", type: "address" },
735
+ { name: "tokenId", type: "uint256" },
736
+ ],
737
+ outputs: [],
738
+ stateMutability: "nonpayable",
739
+ },
740
+ {
741
+ type: "function",
742
+ name: "safeTransferFrom",
743
+ inputs: [
744
+ { name: "from", type: "address" },
745
+ { name: "to", type: "address" },
746
+ { name: "tokenId", type: "uint256" },
747
+ { name: "data", type: "bytes" },
748
+ ],
749
+ outputs: [],
750
+ stateMutability: "nonpayable",
751
+ },
752
+ ];
753
+
754
+ const mockWithOverloads: ExplorerContractInstance = {
755
+ ...createMockContract("0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", overloadedAbi),
756
+ read: {},
757
+ write: {
758
+ // No name-only key for overloaded function
759
+ "safeTransferFrom(address,address,uint256)": () => Effect.succeed("0x123" as `0x${string}`),
760
+ "safeTransferFrom(address,address,uint256,bytes)": () => Effect.succeed("0x456" as `0x${string}`),
761
+ },
762
+ simulate: {
763
+ "safeTransferFrom(address,address,uint256)": () => Effect.succeed(undefined),
764
+ "safeTransferFrom(address,address,uint256,bytes)": () => Effect.succeed(undefined),
765
+ },
766
+ };
767
+
768
+ const mockService = createMockService({
769
+ getContract: (_address, _options) => Effect.succeed(mockWithOverloads),
770
+ });
771
+
772
+ const program = Effect.gen(function* () {
773
+ const explorer = yield* BlockExplorerApiService;
774
+ return yield* explorer.getContract("0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D");
775
+ }).pipe(
776
+ Effect.provide(mockService),
777
+ Effect.provide(mockChainLayer),
778
+ );
779
+
780
+ const result = await Effect.runPromise(program);
781
+
782
+ // Overloaded function should not have name-only key
783
+ expect(result.write.safeTransferFrom).toBeUndefined();
784
+ // But should have signature keys
785
+ expect(typeof result.write["safeTransferFrom(address,address,uint256)"]).toBe("function");
786
+ expect(typeof result.write["safeTransferFrom(address,address,uint256,bytes)"]).toBe("function");
787
+ });
788
+ });
789
+ });