@gibs/common 1.10.1 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gibs/common",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "main": "index.js",
5
5
  "access": "public",
6
6
  "type": "module",
@@ -14,18 +14,23 @@
14
14
  "./client": "./src/client.ts"
15
15
  },
16
16
  "scripts": {
17
- "build": "npx tsc"
17
+ "build": "npx tsc",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "test:coverage": "vitest run --coverage"
18
21
  },
19
22
  "keywords": [],
20
23
  "author": "",
21
24
  "license": "ISC",
22
25
  "description": "",
23
26
  "devDependencies": {
24
- "@types/node": "^22.12.0",
25
- "typescript": "~5.7.2"
27
+ "@types/node": "^24",
28
+ "@vitest/coverage-v8": "^3.1.1",
29
+ "typescript": "~5.7.2",
30
+ "vitest": "^3.1.1"
26
31
  },
27
32
  "dependencies": {
28
33
  "lodash": "^4.17.21",
29
- "viem": "^2.37.8"
34
+ "viem": "^2.46.3"
30
35
  }
31
36
  }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { assertDefined, ensureResult } from './assert';
3
+
4
+ describe('assert', () => {
5
+ describe('ensureResult', () => {
6
+ it('should return value when truthy', () => {
7
+ expect(ensureResult('hello', 'test')).toBe('hello');
8
+ expect(ensureResult(123, 'test')).toBe(123);
9
+ expect(ensureResult(true, 'test')).toBe(true);
10
+ expect(ensureResult({ foo: 'bar' }, 'test')).toEqual({ foo: 'bar' });
11
+ expect(ensureResult([1, 2, 3], 'test')).toEqual([1, 2, 3]);
12
+ });
13
+
14
+ it('should throw Error when value is null', () => {
15
+ expect(() => ensureResult(null, 'TestContext')).toThrow(
16
+ 'TestContext: Expected result but got null',
17
+ );
18
+ });
19
+
20
+ it('should throw Error when value is undefined', () => {
21
+ expect(() => ensureResult(undefined, 'TestContext')).toThrow(
22
+ 'TestContext: Expected result but got undefined',
23
+ );
24
+ });
25
+
26
+ it('should throw Error when value is false', () => {
27
+ expect(() => ensureResult(false, 'TestContext')).toThrow(
28
+ 'TestContext: Expected result but got false',
29
+ );
30
+ });
31
+
32
+ it('should throw Error when value is 0', () => {
33
+ expect(() => ensureResult(0, 'TestContext')).toThrow(
34
+ 'TestContext: Expected result but got 0',
35
+ );
36
+ });
37
+
38
+ it('should include context in error message', () => {
39
+ expect(() => ensureResult(null, 'Multicall aggregate3')).toThrow(
40
+ 'Multicall aggregate3: Expected result but got null',
41
+ );
42
+
43
+ expect(() => ensureResult(undefined, 'Database query')).toThrow(
44
+ 'Database query: Expected result but got undefined',
45
+ );
46
+ });
47
+
48
+ it('should handle BigInt values', () => {
49
+ expect(ensureResult(123n, 'test')).toBe(123n);
50
+ // 0n is falsy just like 0, so it should throw
51
+ expect(() => ensureResult(0n, 'test')).toThrow('test: Expected result but got 0');
52
+ });
53
+
54
+ it('should handle empty string as falsy', () => {
55
+ expect(() => ensureResult('', 'TestContext')).toThrow(
56
+ 'TestContext: Expected result but got ',
57
+ );
58
+ });
59
+
60
+ it('should handle empty array as truthy', () => {
61
+ expect(ensureResult([], 'test')).toEqual([]);
62
+ });
63
+
64
+ it('should throw Error, not the value itself', () => {
65
+ try {
66
+ ensureResult(null, 'test');
67
+ expect.fail('Should have thrown');
68
+ } catch (err) {
69
+ expect(err).toBeInstanceOf(Error);
70
+ expect(err).not.toBe(null);
71
+ }
72
+ });
73
+ });
74
+
75
+ describe('assertDefined', () => {
76
+ it('should not throw when value is defined', () => {
77
+ expect(() => assertDefined('hello', 'test')).not.toThrow();
78
+ expect(() => assertDefined(123, 'test')).not.toThrow();
79
+ expect(() => assertDefined(true, 'test')).not.toThrow();
80
+ expect(() => assertDefined(false, 'test')).not.toThrow();
81
+ expect(() => assertDefined(0, 'test')).not.toThrow();
82
+ expect(() => assertDefined({}, 'test')).not.toThrow();
83
+ expect(() => assertDefined([], 'test')).not.toThrow();
84
+ });
85
+
86
+ it('should throw when value is null', () => {
87
+ expect(() => assertDefined(null, 'Value cannot be null')).toThrow('Value cannot be null');
88
+ });
89
+
90
+ it('should throw when value is undefined', () => {
91
+ expect(() => assertDefined(undefined, 'Value cannot be undefined')).toThrow(
92
+ 'Value cannot be undefined',
93
+ );
94
+ });
95
+
96
+ it('should not throw for falsy but defined values', () => {
97
+ expect(() => assertDefined(false, 'test')).not.toThrow();
98
+ expect(() => assertDefined(0, 'test')).not.toThrow();
99
+ expect(() => assertDefined('', 'test')).not.toThrow();
100
+ expect(() => assertDefined(0n, 'test')).not.toThrow();
101
+ });
102
+
103
+ it('should include message in error', () => {
104
+ expect(() => assertDefined(null, 'Custom error message')).toThrow('Custom error message');
105
+ });
106
+
107
+ it('should throw Error object', () => {
108
+ try {
109
+ assertDefined(null, 'test');
110
+ expect.fail('Should have thrown');
111
+ } catch (err) {
112
+ expect(err).toBeInstanceOf(Error);
113
+ }
114
+ });
115
+
116
+ it('should work with type narrowing', () => {
117
+ const value: string | null = 'hello';
118
+ assertDefined(value, 'test');
119
+ // TypeScript should now know value is string
120
+ const uppercased: string = value.toUpperCase();
121
+ expect(uppercased).toBe('HELLO');
122
+ });
123
+ });
124
+
125
+ describe('integration', () => {
126
+ it('should allow chaining ensureResult', () => {
127
+ const obj = { nested: { value: 42 } };
128
+ const result = ensureResult(ensureResult(obj, 'obj').nested, 'nested').value;
129
+ expect(result).toBe(42);
130
+ });
131
+
132
+ it('should provide clear error messages in real-world scenarios', async () => {
133
+ const mockApiCall = async (): Promise<any[] | null> => null;
134
+
135
+ await expect(async () => {
136
+ const result = await mockApiCall();
137
+ ensureResult(result, 'API call');
138
+ }).rejects.toThrow('API call: Expected result but got null');
139
+ });
140
+ });
141
+ });
package/src/assert.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Asserts that a value is defined (not null, undefined, or falsy)
3
+ * Throws a descriptive error if the value is falsy
4
+ *
5
+ * @param value - The value to check
6
+ * @param context - Context description for the error message
7
+ * @returns The value if defined
8
+ * @throws Error if value is falsy
9
+ */
10
+ export function ensureResult<T>(value: T | null | undefined | false | 0, context: string): T {
11
+ if (!value) {
12
+ throw new Error(`${context}: Expected result but got ${value}`);
13
+ }
14
+ return value;
15
+ }
16
+
17
+ /**
18
+ * Type guard that asserts a value is defined
19
+ * Similar to ensureResult but uses TypeScript's asserts syntax
20
+ *
21
+ * @param value - The value to check
22
+ * @param message - Error message if assertion fails
23
+ */
24
+ export function assertDefined<T>(value: T | null | undefined, message: string): asserts value is T {
25
+ if (value === null || value === undefined) {
26
+ throw new Error(message);
27
+ }
28
+ }
@@ -0,0 +1,176 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { maxMemoize, ttlMemoizeSingle } from './cache';
3
+
4
+ describe('cache', () => {
5
+ describe('maxMemoize', () => {
6
+ it('should cache function results', () => {
7
+ const fn = vi.fn((a: number, b: number) => a + b);
8
+ const memoized = maxMemoize(fn);
9
+
10
+ const result1 = memoized(1, 2);
11
+ const result2 = memoized(1, 2);
12
+
13
+ expect(result1).toBe(3);
14
+ expect(result2).toBe(3);
15
+ expect(fn).toHaveBeenCalledTimes(1);
16
+ });
17
+
18
+ it('should cache different argument combinations separately', () => {
19
+ const fn = vi.fn((a: number, b: number) => a + b);
20
+ const memoized = maxMemoize(fn);
21
+
22
+ expect(memoized(1, 2)).toBe(3);
23
+ expect(memoized(2, 3)).toBe(5);
24
+ expect(memoized(1, 2)).toBe(3);
25
+
26
+ expect(fn).toHaveBeenCalledTimes(2);
27
+ });
28
+
29
+ it('should handle bigint arguments correctly', () => {
30
+ const fn = vi.fn((a: bigint) => a * 2n);
31
+ const memoized = maxMemoize(fn);
32
+
33
+ const result1 = memoized(100n);
34
+ const result2 = memoized(100n);
35
+
36
+ expect(result1).toBe(200n);
37
+ expect(result2).toBe(200n);
38
+ expect(fn).toHaveBeenCalledTimes(1);
39
+ });
40
+
41
+ it('should respect max cache size', () => {
42
+ const fn = vi.fn((n: number) => n * 2);
43
+ const memoized = maxMemoize(fn, 3);
44
+
45
+ // Fill cache with 3 entries
46
+ memoized(1);
47
+ memoized(2);
48
+ memoized(3);
49
+
50
+ // Add 4th entry - should evict oldest
51
+ memoized(4);
52
+
53
+ // Call first entry again - should recompute
54
+ memoized(1);
55
+
56
+ // Total calls: 5 (1, 2, 3, 4, 1 again)
57
+ expect(fn).toHaveBeenCalledTimes(5);
58
+ });
59
+
60
+ it('should handle complex object arguments', () => {
61
+ const fn = vi.fn((obj: { a: number; b: string }) => `${obj.a}-${obj.b}`);
62
+ const memoized = maxMemoize(fn);
63
+
64
+ const result1 = memoized({ a: 1, b: 'test' });
65
+ const result2 = memoized({ a: 1, b: 'test' });
66
+
67
+ expect(result1).toBe('1-test');
68
+ expect(result2).toBe('1-test');
69
+ expect(fn).toHaveBeenCalledTimes(1);
70
+ });
71
+
72
+ it('should return undefined for cached undefined results', () => {
73
+ const fn = vi.fn(() => undefined);
74
+ const memoized = maxMemoize(fn);
75
+
76
+ const result1 = memoized();
77
+ const result2 = memoized();
78
+
79
+ expect(result1).toBe(undefined);
80
+ expect(result2).toBe(undefined);
81
+ expect(fn).toHaveBeenCalledTimes(1);
82
+ });
83
+ });
84
+
85
+ describe('ttlMemoizeSingle', () => {
86
+ beforeEach(() => {
87
+ vi.useFakeTimers();
88
+ });
89
+
90
+ afterEach(() => {
91
+ vi.useRealTimers();
92
+ });
93
+
94
+ it('should cache result within TTL', () => {
95
+ const fn = vi.fn(() => Math.random());
96
+ const memoized = ttlMemoizeSingle(fn, 1000);
97
+
98
+ const result1 = memoized();
99
+ vi.advanceTimersByTime(500);
100
+ const result2 = memoized();
101
+
102
+ expect(result1).toBe(result2);
103
+ expect(fn).toHaveBeenCalledTimes(1);
104
+ });
105
+
106
+ it('should recompute after TTL expires', () => {
107
+ const fn = vi.fn(() => Math.random());
108
+ const memoized = ttlMemoizeSingle(fn, 1000);
109
+
110
+ const result1 = memoized();
111
+ vi.advanceTimersByTime(1001);
112
+ const result2 = memoized();
113
+
114
+ expect(result1).not.toBe(result2);
115
+ expect(fn).toHaveBeenCalledTimes(2);
116
+ });
117
+
118
+ it('should use default TTL of 1 hour', () => {
119
+ const fn = vi.fn(() => 'test');
120
+ const memoized = ttlMemoizeSingle(fn);
121
+
122
+ memoized();
123
+ vi.advanceTimersByTime(1000 * 60 * 59); // 59 minutes
124
+ memoized();
125
+
126
+ expect(fn).toHaveBeenCalledTimes(1);
127
+
128
+ vi.advanceTimersByTime(1000 * 60 * 2); // 2 more minutes (total 61)
129
+ memoized();
130
+
131
+ expect(fn).toHaveBeenCalledTimes(2);
132
+ });
133
+
134
+ it('should ignore arguments and always return same cached result', () => {
135
+ const fn = vi.fn((n: number) => n * 2);
136
+ const memoized = ttlMemoizeSingle(fn, 1000);
137
+
138
+ const result1 = memoized(5);
139
+ const result2 = memoized(10);
140
+
141
+ // Second call should return cached result from first call
142
+ expect(result1).toBe(10);
143
+ expect(result2).toBe(10);
144
+ expect(fn).toHaveBeenCalledTimes(1);
145
+ expect(fn).toHaveBeenCalledWith(5);
146
+ });
147
+
148
+ it('should handle bigint results', () => {
149
+ const fn = vi.fn(() => 999999999999999999n);
150
+ const memoized = ttlMemoizeSingle(fn, 1000);
151
+
152
+ const result1 = memoized();
153
+ const result2 = memoized();
154
+
155
+ expect(result1).toBe(999999999999999999n);
156
+ expect(result2).toBe(999999999999999999n);
157
+ expect(fn).toHaveBeenCalledTimes(1);
158
+ });
159
+
160
+ it('should cache null and undefined results', () => {
161
+ const fnNull = vi.fn(() => null);
162
+ const memoizedNull = ttlMemoizeSingle(fnNull, 1000);
163
+
164
+ expect(memoizedNull()).toBe(null);
165
+ expect(memoizedNull()).toBe(null);
166
+ expect(fnNull).toHaveBeenCalledTimes(1);
167
+
168
+ const fnUndefined = vi.fn(() => undefined);
169
+ const memoizedUndefined = ttlMemoizeSingle(fnUndefined, 1000);
170
+
171
+ expect(memoizedUndefined()).toBe(undefined);
172
+ expect(memoizedUndefined()).toBe(undefined);
173
+ expect(fnUndefined).toHaveBeenCalledTimes(1);
174
+ });
175
+ });
176
+ });
package/src/cache.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { jsonAnyStringify } from "./serialize.js"
1
+ import { jsonAnyStringify } from './serialize.js';
2
2
 
3
3
  /**
4
4
  * Memoize a function result and return a single value
@@ -7,24 +7,24 @@ import { jsonAnyStringify } from "./serialize.js"
7
7
  * @returns the memoized function
8
8
  */
9
9
  export const maxMemoize = <A extends unknown[], B>(fn: (...a: A) => B, max = 1024) => {
10
- const cache = new Map<string, B>()
10
+ const cache = new Map<string, B>();
11
11
  return (...a: A) => {
12
- const key = JSON.stringify(a, jsonAnyStringify)
13
- const existing = cache.get(key)
12
+ const key = JSON.stringify(a, jsonAnyStringify);
13
+ const existing = cache.get(key);
14
14
  if (cache.has(key)) {
15
- return existing
15
+ return existing;
16
16
  }
17
17
  for (const k of cache.keys()) {
18
- if (cache.size < max) break
19
- cache.delete(k)
18
+ if (cache.size < max) break;
19
+ cache.delete(k);
20
20
  }
21
- const result = fn(...a)
22
- cache.set(key, result)
23
- return result
24
- }
25
- }
21
+ const result = fn(...a);
22
+ cache.set(key, result);
23
+ return result;
24
+ };
25
+ };
26
26
 
27
- const hour1 = 1000 * 60 * 60
27
+ const hour1 = 1000 * 60 * 60;
28
28
  /**
29
29
  * Memoize a function result and return a single value
30
30
  * @param fn the function to memoize
@@ -33,16 +33,16 @@ const hour1 = 1000 * 60 * 60
33
33
  */
34
34
  export const ttlMemoizeSingle = <A extends unknown[], B>(fn: (...a: A) => B, ttl = hour1) => {
35
35
  let cache: {
36
- timestamp: number
37
- result: B
38
- } | null = null
36
+ timestamp: number;
37
+ result: B;
38
+ } | null = null;
39
39
  return (...a: A) => {
40
- const now = Date.now()
40
+ const now = Date.now();
41
41
  if (cache && cache.timestamp + ttl > now) {
42
- return cache.result
42
+ return cache.result;
43
43
  }
44
- const result = fn(...a)
45
- cache = { timestamp: now, result }
46
- return result
47
- }
48
- }
44
+ const result = fn(...a);
45
+ cache = { timestamp: now, result };
46
+ return result;
47
+ };
48
+ };
@@ -0,0 +1,205 @@
1
+ import type { Chain } from 'viem';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { chainKey, clientFromChain, defaultBatchConfig } from './client';
4
+
5
+ // Mock viem functions
6
+ vi.mock('viem', async () => {
7
+ const actual = await vi.importActual<typeof import('viem')>('viem');
8
+ return {
9
+ ...actual,
10
+ createPublicClient: vi.fn((config: any) => ({
11
+ ...config,
12
+ mockClient: true,
13
+ })),
14
+ http: vi.fn((url?: string, config?: any) => ({
15
+ type: 'http',
16
+ url,
17
+ config,
18
+ })),
19
+ webSocket: vi.fn((url: string, config?: any) => ({
20
+ type: 'webSocket',
21
+ url,
22
+ config,
23
+ })),
24
+ fallback: vi.fn((transports: any[], config?: any) => ({
25
+ type: 'fallback',
26
+ transports,
27
+ config,
28
+ })),
29
+ };
30
+ });
31
+
32
+ describe('client', () => {
33
+ describe('defaultBatchConfig', () => {
34
+ it('should have correct batch wait time', () => {
35
+ expect(defaultBatchConfig.batch.wait).toBe(10);
36
+ });
37
+
38
+ it('should have correct batch size', () => {
39
+ expect(defaultBatchConfig.batch.batchSize).toBe(32);
40
+ });
41
+
42
+ it('should have batch property', () => {
43
+ expect(defaultBatchConfig).toHaveProperty('batch');
44
+ expect(defaultBatchConfig.batch).toHaveProperty('wait');
45
+ expect(defaultBatchConfig.batch).toHaveProperty('batchSize');
46
+ });
47
+ });
48
+
49
+ describe('chainKey', () => {
50
+ it('should create key from chainId and single URL', () => {
51
+ const result = chainKey(1, ['https://eth.llamarpc.com']);
52
+ expect(result).toBe('1,https://eth.llamarpc.com');
53
+ });
54
+
55
+ it('should create key from chainId and multiple URLs', () => {
56
+ const result = chainKey(369, [
57
+ 'https://rpc.pulsechain.com',
58
+ 'https://rpc-pulsechain.g4mm4.io',
59
+ ]);
60
+ expect(result).toBe('369,https://rpc.pulsechain.com,https://rpc-pulsechain.g4mm4.io');
61
+ });
62
+
63
+ it('should lowercase the result', () => {
64
+ const result = chainKey(1, ['HTTPS://ETH.LLAMARPC.COM']);
65
+ expect(result).toBe('1,https://eth.llamarpc.com');
66
+ });
67
+
68
+ it('should handle empty URL array', () => {
69
+ const result = chainKey(1, []);
70
+ expect(result).toBe('1,');
71
+ });
72
+
73
+ it('should handle WebSocket URLs', () => {
74
+ const result = chainKey(1, ['wss://eth.llamarpc.com']);
75
+ expect(result).toBe('1,wss://eth.llamarpc.com');
76
+ });
77
+
78
+ it('should create unique keys for different chainIds', () => {
79
+ const key1 = chainKey(1, ['https://rpc.example.com']);
80
+ const key2 = chainKey(369, ['https://rpc.example.com']);
81
+ expect(key1).not.toBe(key2);
82
+ });
83
+
84
+ it('should create unique keys for different URL arrays', () => {
85
+ const key1 = chainKey(1, ['https://rpc1.example.com']);
86
+ const key2 = chainKey(1, ['https://rpc2.example.com']);
87
+ expect(key1).not.toBe(key2);
88
+ });
89
+
90
+ it('should create same key for same inputs', () => {
91
+ const key1 = chainKey(1, ['https://rpc.example.com', 'https://backup.example.com']);
92
+ const key2 = chainKey(1, ['https://rpc.example.com', 'https://backup.example.com']);
93
+ expect(key1).toBe(key2);
94
+ });
95
+
96
+ it('should handle URLs with query parameters', () => {
97
+ const result = chainKey(1, ['https://rpc.example.com?apiKey=123']);
98
+ expect(result).toBe('1,https://rpc.example.com?apikey=123');
99
+ });
100
+
101
+ it('should handle URLs with ports', () => {
102
+ const result = chainKey(1, ['https://localhost:8545']);
103
+ expect(result).toBe('1,https://localhost:8545');
104
+ });
105
+
106
+ it('should handle testnet chain IDs', () => {
107
+ const sepolia = chainKey(11155111, ['https://sepolia.infura.io']);
108
+ expect(sepolia).toBe('11155111,https://sepolia.infura.io');
109
+ });
110
+
111
+ it('should create different keys when URL order differs', () => {
112
+ const key1 = chainKey(1, ['https://rpc1.example.com', 'https://rpc2.example.com']);
113
+ const key2 = chainKey(1, ['https://rpc2.example.com', 'https://rpc1.example.com']);
114
+ expect(key1).not.toBe(key2);
115
+ });
116
+ });
117
+
118
+ describe('clientFromChain', () => {
119
+ let mockChain: Chain;
120
+
121
+ beforeEach(() => {
122
+ vi.clearAllMocks();
123
+ mockChain = {
124
+ id: 1,
125
+ name: 'Ethereum',
126
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
127
+ rpcUrls: {
128
+ default: { http: ['https://eth.llamarpc.com'] },
129
+ },
130
+ } as Chain;
131
+ });
132
+
133
+ it('should create client with http transport when urls provided', () => {
134
+ const urls = ['https://eth.llamarpc.com'];
135
+ const client = clientFromChain({ chain: mockChain, urls });
136
+
137
+ expect(client).toBeDefined();
138
+ expect((client as any).mockClient).toBe(true);
139
+ });
140
+
141
+ it('should create client with multiple http urls using fallback', () => {
142
+ const urls = ['https://eth.llamarpc.com', 'https://rpc.ankr.com/eth'];
143
+ const client = clientFromChain({ chain: mockChain, urls });
144
+
145
+ expect(client).toBeDefined();
146
+ });
147
+
148
+ it('should create client with websocket transport', () => {
149
+ const urls = ['wss://eth.llamarpc.com'];
150
+ const client = clientFromChain({ chain: mockChain, urls });
151
+
152
+ expect(client).toBeDefined();
153
+ });
154
+
155
+ it('should create client with mixed http and websocket transports', () => {
156
+ const urls = ['https://eth.llamarpc.com', 'wss://eth.llamarpc.com'];
157
+ const client = clientFromChain({ chain: mockChain, urls });
158
+
159
+ expect(client).toBeDefined();
160
+ });
161
+
162
+ it('should create client with empty urls array', () => {
163
+ const urls: string[] = [];
164
+ const client = clientFromChain({ chain: mockChain, urls });
165
+
166
+ expect(client).toBeDefined();
167
+ });
168
+
169
+ it('should use cache for same chain and urls', () => {
170
+ const urls = ['https://eth.llamarpc.com'];
171
+
172
+ const client1 = clientFromChain({ chain: mockChain, urls });
173
+ const client2 = clientFromChain({ chain: mockChain, urls });
174
+
175
+ // Should return cached client
176
+ expect(client1).toBe(client2);
177
+ });
178
+
179
+ it('should create new client for different urls', () => {
180
+ const urls1 = ['https://eth.llamarpc.com'];
181
+ const urls2 = ['https://rpc.ankr.com/eth'];
182
+
183
+ const client1 = clientFromChain({ chain: mockChain, urls: urls1 });
184
+ const client2 = clientFromChain({ chain: mockChain, urls: urls2 });
185
+
186
+ // Should be different clients
187
+ expect(client1).not.toBe(client2);
188
+ });
189
+
190
+ it('should create new client for different chain', () => {
191
+ const urls = ['https://eth.llamarpc.com'];
192
+ const chain2: Chain = {
193
+ ...mockChain,
194
+ id: 369,
195
+ name: 'PulseChain',
196
+ };
197
+
198
+ const client1 = clientFromChain({ chain: mockChain, urls });
199
+ const client2 = clientFromChain({ chain: chain2, urls });
200
+
201
+ // Should be different clients
202
+ expect(client1).not.toBe(client2);
203
+ });
204
+ });
205
+ });