@gibs/common 1.12.0 → 1.13.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/LICENSE +15 -0
- package/dist/assert.d.ts +18 -0
- package/{src/assert.ts → dist/assert.js} +9 -10
- package/dist/cache.d.ts +14 -0
- package/dist/cache.js +44 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +164 -0
- package/dist/erc20.d.ts +20 -0
- package/dist/erc20.js +38 -0
- package/dist/index.js +7 -0
- package/dist/multicall.d.ts +56 -0
- package/dist/multicall.js +213 -0
- package/dist/rpcPacer.d.ts +88 -0
- package/dist/rpcPacer.js +153 -0
- package/dist/serialize.d.ts +19 -0
- package/dist/serialize.js +42 -0
- package/{src/types.ts → dist/types.d.ts} +5 -7
- package/dist/types.js +1 -0
- package/package.json +82 -17
- package/src/assert.test.ts +0 -141
- package/src/cache.test.ts +0 -176
- package/src/cache.ts +0 -48
- package/src/client.test.ts +0 -205
- package/src/client.ts +0 -76
- package/src/erc20.test.ts +0 -140
- package/src/erc20.ts +0 -43
- package/src/multicall.test.ts +0 -263
- package/src/multicall.ts +0 -68
- package/src/serialize.test.ts +0 -128
- package/src/serialize.ts +0 -30
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -13
- /package/{src/index.ts → dist/index.d.ts} +0 -0
package/src/cache.test.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { jsonAnyStringify } from './serialize.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Memoize a function result and return a single value
|
|
5
|
-
* @param fn the function to memoize
|
|
6
|
-
* @param max the maximum number of values to cache
|
|
7
|
-
* @returns the memoized function
|
|
8
|
-
*/
|
|
9
|
-
export const maxMemoize = <A extends unknown[], B>(fn: (...a: A) => B, max = 1024) => {
|
|
10
|
-
const cache = new Map<string, B>();
|
|
11
|
-
return (...a: A) => {
|
|
12
|
-
const key = JSON.stringify(a, jsonAnyStringify);
|
|
13
|
-
const existing = cache.get(key);
|
|
14
|
-
if (cache.has(key)) {
|
|
15
|
-
return existing;
|
|
16
|
-
}
|
|
17
|
-
for (const k of cache.keys()) {
|
|
18
|
-
if (cache.size < max) break;
|
|
19
|
-
cache.delete(k);
|
|
20
|
-
}
|
|
21
|
-
const result = fn(...a);
|
|
22
|
-
cache.set(key, result);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const hour1 = 1000 * 60 * 60;
|
|
28
|
-
/**
|
|
29
|
-
* Memoize a function result and return a single value
|
|
30
|
-
* @param fn the function to memoize
|
|
31
|
-
* @param ttl the time to live for the cache
|
|
32
|
-
* @returns the memoized function
|
|
33
|
-
*/
|
|
34
|
-
export const ttlMemoizeSingle = <A extends unknown[], B>(fn: (...a: A) => B, ttl = hour1) => {
|
|
35
|
-
let cache: {
|
|
36
|
-
timestamp: number;
|
|
37
|
-
result: B;
|
|
38
|
-
} | null = null;
|
|
39
|
-
return (...a: A) => {
|
|
40
|
-
const now = Date.now();
|
|
41
|
-
if (cache && cache.timestamp + ttl > now) {
|
|
42
|
-
return cache.result;
|
|
43
|
-
}
|
|
44
|
-
const result = fn(...a);
|
|
45
|
-
cache = { timestamp: now, result };
|
|
46
|
-
return result;
|
|
47
|
-
};
|
|
48
|
-
};
|
package/src/client.test.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
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
|
-
});
|
package/src/client.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { type Chain, createPublicClient, fallback, http, type PublicClient, webSocket } from 'viem';
|
|
2
|
-
|
|
3
|
-
/** The default batch config for the client */
|
|
4
|
-
export const defaultBatchConfig = {
|
|
5
|
-
batch: {
|
|
6
|
-
wait: 10,
|
|
7
|
-
batchSize: 32,
|
|
8
|
-
},
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Returns the chain key for the given chain id and urls
|
|
13
|
-
* @param chainId - the chain id
|
|
14
|
-
* @param urls - the urls
|
|
15
|
-
* @returns the chain key
|
|
16
|
-
*/
|
|
17
|
-
export const chainKey = (chainId: number, urls: string[]) => {
|
|
18
|
-
return `${chainId},${urls.join(',')}`.toLowerCase();
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/** The client cache for the given chain id */
|
|
22
|
-
export const clientCache = new Map<
|
|
23
|
-
number,
|
|
24
|
-
{
|
|
25
|
-
key: string;
|
|
26
|
-
client: PublicClient;
|
|
27
|
-
}
|
|
28
|
-
>();
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Returns the client for the given chain and urls
|
|
32
|
-
* @param chain - the chain
|
|
33
|
-
* @param urls - the urls
|
|
34
|
-
* @returns the client
|
|
35
|
-
*/
|
|
36
|
-
export const clientFromChain = ({
|
|
37
|
-
chain,
|
|
38
|
-
urls,
|
|
39
|
-
}: {
|
|
40
|
-
chain: Chain;
|
|
41
|
-
urls: string[];
|
|
42
|
-
}): PublicClient => {
|
|
43
|
-
const key = chainKey(chain.id, urls);
|
|
44
|
-
const existing = clientCache.get(chain.id);
|
|
45
|
-
if (existing && existing.key === key) {
|
|
46
|
-
return existing.client;
|
|
47
|
-
}
|
|
48
|
-
const transport = !urls?.length
|
|
49
|
-
? http()
|
|
50
|
-
: fallback(
|
|
51
|
-
urls.map((rpc) =>
|
|
52
|
-
rpc.startsWith('http')
|
|
53
|
-
? http(rpc, {
|
|
54
|
-
...defaultBatchConfig,
|
|
55
|
-
})
|
|
56
|
-
: webSocket(rpc, {
|
|
57
|
-
keepAlive: true,
|
|
58
|
-
reconnect: true,
|
|
59
|
-
retryDelay: 250,
|
|
60
|
-
retryCount: 10,
|
|
61
|
-
timeout: 4_000,
|
|
62
|
-
...defaultBatchConfig,
|
|
63
|
-
}),
|
|
64
|
-
),
|
|
65
|
-
{ rank: true },
|
|
66
|
-
);
|
|
67
|
-
const client = createPublicClient({
|
|
68
|
-
chain,
|
|
69
|
-
transport,
|
|
70
|
-
});
|
|
71
|
-
clientCache.set(chain.id, {
|
|
72
|
-
key,
|
|
73
|
-
client,
|
|
74
|
-
});
|
|
75
|
-
return client;
|
|
76
|
-
};
|
package/src/erc20.test.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { Chain, Hex, PublicClient } from 'viem';
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { erc20MetadataCalls, multicallErc20 } from './erc20';
|
|
4
|
-
|
|
5
|
-
// Mock the multicall module
|
|
6
|
-
vi.mock('./multicall', () => ({
|
|
7
|
-
multicallRead: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
describe('erc20', () => {
|
|
11
|
-
describe('erc20MetadataCalls', () => {
|
|
12
|
-
it('should have 3 metadata calls', () => {
|
|
13
|
-
expect(erc20MetadataCalls).toHaveLength(3);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should include name call', () => {
|
|
17
|
-
const nameCall = erc20MetadataCalls.find((c) => c.functionName === 'name');
|
|
18
|
-
expect(nameCall).toBeDefined();
|
|
19
|
-
expect(nameCall?.functionName).toBe('name');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should include symbol call', () => {
|
|
23
|
-
const symbolCall = erc20MetadataCalls.find((c) => c.functionName === 'symbol');
|
|
24
|
-
expect(symbolCall).toBeDefined();
|
|
25
|
-
expect(symbolCall?.functionName).toBe('symbol');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should include decimals call', () => {
|
|
29
|
-
const decimalsCall = erc20MetadataCalls.find((c) => c.functionName === 'decimals');
|
|
30
|
-
expect(decimalsCall).toBeDefined();
|
|
31
|
-
expect(decimalsCall?.functionName).toBe('decimals');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should have calls in correct order (name, symbol, decimals)', () => {
|
|
35
|
-
expect(erc20MetadataCalls[0].functionName).toBe('name');
|
|
36
|
-
expect(erc20MetadataCalls[1].functionName).toBe('symbol');
|
|
37
|
-
expect(erc20MetadataCalls[2].functionName).toBe('decimals');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should have only functionName property in each call', () => {
|
|
41
|
-
erc20MetadataCalls.forEach((call) => {
|
|
42
|
-
const keys = Object.keys(call);
|
|
43
|
-
expect(keys).toHaveLength(1);
|
|
44
|
-
expect(keys[0]).toBe('functionName');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('multicallErc20', () => {
|
|
50
|
-
let mockClient: PublicClient;
|
|
51
|
-
let mockChain: Chain;
|
|
52
|
-
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
vi.clearAllMocks();
|
|
55
|
-
// Clear memoization cache by deleting and recreating it
|
|
56
|
-
(multicallErc20 as any).cache = new Map();
|
|
57
|
-
|
|
58
|
-
mockChain = {
|
|
59
|
-
id: 1,
|
|
60
|
-
name: 'Ethereum',
|
|
61
|
-
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
62
|
-
rpcUrls: {
|
|
63
|
-
default: { http: ['https://eth.llamarpc.com'] },
|
|
64
|
-
},
|
|
65
|
-
} as Chain;
|
|
66
|
-
|
|
67
|
-
mockClient = {
|
|
68
|
-
chain: mockChain,
|
|
69
|
-
} as PublicClient;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should successfully fetch erc20 metadata', async () => {
|
|
73
|
-
const { multicallRead } = await import('./multicall');
|
|
74
|
-
(multicallRead as any).mockResolvedValue(['Test Token', 'TST', 18]);
|
|
75
|
-
|
|
76
|
-
const result = await multicallErc20({
|
|
77
|
-
client: mockClient,
|
|
78
|
-
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
79
|
-
chain: mockChain,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(result).toEqual(['Test Token', 'TST', 18]);
|
|
83
|
-
expect(multicallRead).toHaveBeenCalledWith({
|
|
84
|
-
chain: mockChain,
|
|
85
|
-
client: mockClient,
|
|
86
|
-
abi: expect.anything(),
|
|
87
|
-
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
88
|
-
calls: erc20MetadataCalls,
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should fallback to bytes32 ABI on failure', async () => {
|
|
93
|
-
const { multicallRead } = await import('./multicall');
|
|
94
|
-
const mockError = new Error('Failed with standard ABI');
|
|
95
|
-
(multicallRead as any)
|
|
96
|
-
.mockRejectedValueOnce(mockError)
|
|
97
|
-
.mockResolvedValueOnce(['Test Token', 'TST', 18]);
|
|
98
|
-
|
|
99
|
-
const result = await multicallErc20({
|
|
100
|
-
client: mockClient,
|
|
101
|
-
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
102
|
-
chain: mockChain,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
expect(result).toEqual(['Test Token', 'TST', 18]);
|
|
106
|
-
expect(multicallRead).toHaveBeenCalledTimes(2);
|
|
107
|
-
// First call with standard ABI
|
|
108
|
-
expect((multicallRead as any).mock.calls[0][0].abi).toBeDefined();
|
|
109
|
-
// Second call with bytes32 ABI (fallback)
|
|
110
|
-
expect((multicallRead as any).mock.calls[1][0].abi).toBeDefined();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should use memoization with target address', async () => {
|
|
114
|
-
const { multicallRead } = await import('./multicall');
|
|
115
|
-
(multicallRead as any).mockResolvedValue(['Test Token', 'TST', 18]);
|
|
116
|
-
|
|
117
|
-
const target = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex;
|
|
118
|
-
|
|
119
|
-
// First call
|
|
120
|
-
const result1 = await multicallErc20({ client: mockClient, target, chain: mockChain });
|
|
121
|
-
// Second call with same params - should use memoized result
|
|
122
|
-
const result2 = await multicallErc20({ client: mockClient, target, chain: mockChain });
|
|
123
|
-
|
|
124
|
-
expect(result1).toEqual(result2);
|
|
125
|
-
// Should only call multicallRead once due to memoization
|
|
126
|
-
expect(multicallRead).toHaveBeenCalledTimes(1);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should use chain id as memoization key when target is falsy', async () => {
|
|
130
|
-
const { multicallRead } = await import('./multicall');
|
|
131
|
-
(multicallRead as any).mockResolvedValue(['Test Token', 'TST', 18]);
|
|
132
|
-
|
|
133
|
-
// Call with null target (uses chain.id as key)
|
|
134
|
-
await multicallErc20({ client: mockClient, target: null as any, chain: mockChain });
|
|
135
|
-
|
|
136
|
-
// The memoization resolver should handle null target gracefully
|
|
137
|
-
expect(multicallRead).toHaveBeenCalled();
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
});
|
package/src/erc20.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
|
-
import {
|
|
3
|
-
type Abi,
|
|
4
|
-
type Chain,
|
|
5
|
-
erc20Abi,
|
|
6
|
-
erc20Abi_bytes32,
|
|
7
|
-
getAddress,
|
|
8
|
-
type Hex,
|
|
9
|
-
type PublicClient,
|
|
10
|
-
} from 'viem';
|
|
11
|
-
import { multicallRead } from './multicall.js';
|
|
12
|
-
import type { Erc20Metadata } from './types.js';
|
|
13
|
-
|
|
14
|
-
/** The erc20 metadata calls */
|
|
15
|
-
export const erc20MetadataCalls = [
|
|
16
|
-
{ functionName: 'name' },
|
|
17
|
-
{ functionName: 'symbol' },
|
|
18
|
-
{ functionName: 'decimals' },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
/** The multicall for the erc20 metadata */
|
|
22
|
-
export const multicallErc20 = _.memoize(
|
|
23
|
-
async ({ client, target, chain }: { client: PublicClient; target: Hex; chain: Chain }) => {
|
|
24
|
-
const options = {
|
|
25
|
-
chain: chain,
|
|
26
|
-
client: client,
|
|
27
|
-
abi: erc20Abi,
|
|
28
|
-
target,
|
|
29
|
-
calls: erc20MetadataCalls,
|
|
30
|
-
};
|
|
31
|
-
try {
|
|
32
|
-
return await multicallRead<Erc20Metadata>(options);
|
|
33
|
-
} catch {
|
|
34
|
-
return await multicallRead<Erc20Metadata>({
|
|
35
|
-
...options,
|
|
36
|
-
abi: erc20Abi_bytes32,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
({ target, chain }) => {
|
|
41
|
-
return !target ? chain.id : getAddress(target, chain.id);
|
|
42
|
-
},
|
|
43
|
-
);
|