@crewhaus/chain-adapter-evm 0.1.1 → 0.1.2

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 (2) hide show
  1. package/package.json +8 -13
  2. package/src/index.test.ts +73 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/chain-adapter-evm",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "EVM chain adapter: JSON-RPC over fetch with single/quorum/fallback dispatch, finality enforcement, and §41 boundary classification of every response (Section 47, Slice 0).",
6
6
  "main": "src/index.ts",
@@ -12,15 +12,15 @@
12
12
  "test": "bun test src"
13
13
  },
14
14
  "dependencies": {
15
- "@crewhaus/boundary-classifier": "0.1.1",
16
- "@crewhaus/chain-adapter-base": "0.1.1",
17
- "@crewhaus/errors": "0.1.1"
15
+ "@crewhaus/boundary-classifier": "0.1.2",
16
+ "@crewhaus/chain-adapter-base": "0.1.2",
17
+ "@crewhaus/errors": "0.1.2"
18
18
  },
19
19
  "license": "Apache-2.0",
20
20
  "author": {
21
21
  "name": "Max Meier",
22
- "email": "max@studiomax.io",
23
- "url": "https://studiomax.io"
22
+ "email": "max@crewhaus.ai",
23
+ "url": "https://crewhaus.ai"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
@@ -32,12 +32,7 @@
32
32
  "url": "https://github.com/crewhaus/factory/issues"
33
33
  },
34
34
  "publishConfig": {
35
- "access": "restricted"
35
+ "access": "public"
36
36
  },
37
- "files": [
38
- "src",
39
- "README.md",
40
- "LICENSE",
41
- "NOTICE"
42
- ]
37
+ "files": ["src", "README.md", "LICENSE", "NOTICE"]
43
38
  }
package/src/index.test.ts CHANGED
@@ -63,6 +63,26 @@ describe("createEvmAdapter — rpcRead", () => {
63
63
  );
64
64
  });
65
65
 
66
+ test("rejects a 200 response whose body is not valid JSON", async () => {
67
+ // The classifier passes the (benign) text, but JSON.parse fails — the
68
+ // adapter must surface a 'not valid JSON' error rather than crash.
69
+ const fetchImpl = mockFetch(() => new Response("not json at all", { status: 200 }));
70
+ const adapter = createEvmAdapter(BASE_CONFIG, fetchImpl);
71
+ let caught: unknown;
72
+ try {
73
+ await adapter.rpcRead("eth_blockNumber", [], { bypassCache: true });
74
+ } catch (err) {
75
+ caught = err;
76
+ }
77
+ expect(caught).toBeInstanceOf(ChainAdapterError);
78
+ // Single-URL dispatch routes through fallback semantics; the parse
79
+ // failure is preserved on the cause chain.
80
+ expect((caught as ChainAdapterError).message).toContain("all 1 RPC URL(s) failed");
81
+ expect(((caught as ChainAdapterError).cause as Error).message).toContain(
82
+ "response was not valid JSON",
83
+ );
84
+ });
85
+
66
86
  test("surfaces JSON-RPC error envelopes as adapter errors", async () => {
67
87
  const fetchImpl = mockFetch(
68
88
  () =>
@@ -161,4 +181,57 @@ describe("createEvmAdapter — quorum policy", () => {
161
181
  );
162
182
  await expect(adapter.rpcRead("eth_blockNumber", [])).rejects.toThrow(/quorum failed/);
163
183
  });
184
+
185
+ test("throws 'every RPC URL rejected' when all quorum dispatches fail", async () => {
186
+ // Every URL returns a non-2xx status, so each dispatchOne rejects and
187
+ // Promise.allSettled yields zero fulfilled results.
188
+ let calls = 0;
189
+ const fetchImpl = mockFetch(() => {
190
+ calls += 1;
191
+ return new Response("upstream down", { status: 503 });
192
+ });
193
+ const adapter = createEvmAdapter(
194
+ {
195
+ ...BASE_CONFIG,
196
+ rpcUrls: ["https://a.test", "https://b.test", "https://c.test"],
197
+ rpcPolicy: "quorum",
198
+ },
199
+ fetchImpl,
200
+ );
201
+ let caught: unknown;
202
+ try {
203
+ await adapter.rpcRead("eth_blockNumber", []);
204
+ } catch (err) {
205
+ caught = err;
206
+ }
207
+ expect(caught).toBeInstanceOf(ChainAdapterError);
208
+ expect((caught as ChainAdapterError).message).toContain(
209
+ "quorum failed: every RPC URL rejected",
210
+ );
211
+ // All three URLs were attempted concurrently.
212
+ expect(calls).toBe(3);
213
+ });
214
+ });
215
+
216
+ describe("createEvmAdapter — network errors", () => {
217
+ test("wraps a fetch rejection (transport-level failure) as a ChainAdapterError", async () => {
218
+ // fetchImpl throws before producing a Response — e.g. DNS failure,
219
+ // connection refused, or an aborted socket. This exercises the
220
+ // dispatchOne network-error catch (not the !res.ok HTTP branch).
221
+ const fetchImpl = (() =>
222
+ Promise.reject(new Error("ECONNREFUSED rpc.example.test:443"))) as unknown as typeof fetch;
223
+ const adapter = createEvmAdapter(BASE_CONFIG, fetchImpl);
224
+ let caught: unknown;
225
+ try {
226
+ await adapter.rpcRead("eth_blockNumber", []);
227
+ } catch (err) {
228
+ caught = err;
229
+ }
230
+ expect(caught).toBeInstanceOf(ChainAdapterError);
231
+ // fallbackDispatch wraps the per-URL failure; the network message is
232
+ // preserved on the cause chain.
233
+ expect((caught as ChainAdapterError).message).toContain("all 1 RPC URL(s) failed");
234
+ expect(((caught as ChainAdapterError).cause as Error).message).toContain("network error");
235
+ expect(((caught as ChainAdapterError).cause as Error).message).toContain("ECONNREFUSED");
236
+ });
164
237
  });