@gibs/common 1.9.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 +10 -5
- package/src/assert.test.ts +141 -0
- package/src/assert.ts +28 -0
- package/src/cache.test.ts +176 -0
- package/src/cache.ts +23 -23
- package/src/client.test.ts +205 -0
- package/src/client.ts +36 -33
- package/src/erc20.test.ts +140 -0
- package/src/erc20.ts +13 -13
- package/src/index.ts +7 -6
- package/src/multicall.test.ts +263 -0
- package/src/multicall.ts +30 -30
- package/src/serialize.test.ts +128 -0
- package/src/serialize.ts +12 -13
- package/src/types.ts +8 -8
- package/tsconfig.json +2 -1
- package/vitest.config.ts +13 -0
- package/dist/src/cache.d.ts +0 -14
- package/dist/src/cache.js +0 -44
- package/dist/src/client.d.ts +0 -30
- package/dist/src/client.js +0 -55
- package/dist/src/erc20.d.ts +0 -13
- package/dist/src/erc20.js +0 -30
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +0 -6
- package/dist/src/multicall.d.ts +0 -10
- package/dist/src/multicall.js +0 -44
- package/dist/src/serialize.d.ts +0 -12
- package/dist/src/serialize.js +0 -22
- package/dist/src/types.d.ts +0 -11
- package/dist/src/types.js +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/src/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Chain,
|
|
1
|
+
import { type Chain, createPublicClient, fallback, http, type PublicClient, webSocket } from 'viem';
|
|
2
2
|
|
|
3
3
|
/** The default batch config for the client */
|
|
4
4
|
export const defaultBatchConfig = {
|
|
@@ -6,7 +6,7 @@ export const defaultBatchConfig = {
|
|
|
6
6
|
wait: 10,
|
|
7
7
|
batchSize: 32,
|
|
8
8
|
},
|
|
9
|
-
}
|
|
9
|
+
};
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Returns the chain key for the given chain id and urls
|
|
@@ -15,14 +15,17 @@ export const defaultBatchConfig = {
|
|
|
15
15
|
* @returns the chain key
|
|
16
16
|
*/
|
|
17
17
|
export const chainKey = (chainId: number, urls: string[]) => {
|
|
18
|
-
return `${chainId},${urls.join(',')}`.toLowerCase()
|
|
19
|
-
}
|
|
18
|
+
return `${chainId},${urls.join(',')}`.toLowerCase();
|
|
19
|
+
};
|
|
20
20
|
|
|
21
21
|
/** The client cache for the given chain id */
|
|
22
|
-
export const clientCache = new Map<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
export const clientCache = new Map<
|
|
23
|
+
number,
|
|
24
|
+
{
|
|
25
|
+
key: string;
|
|
26
|
+
client: PublicClient;
|
|
27
|
+
}
|
|
28
|
+
>();
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* Returns the client for the given chain and urls
|
|
@@ -34,40 +37,40 @@ export const clientFromChain = ({
|
|
|
34
37
|
chain,
|
|
35
38
|
urls,
|
|
36
39
|
}: {
|
|
37
|
-
chain: Chain
|
|
38
|
-
urls: string[]
|
|
40
|
+
chain: Chain;
|
|
41
|
+
urls: string[];
|
|
39
42
|
}): PublicClient => {
|
|
40
|
-
const key = chainKey(chain.id, urls)
|
|
41
|
-
const existing = clientCache.get(chain.id)
|
|
43
|
+
const key = chainKey(chain.id, urls);
|
|
44
|
+
const existing = clientCache.get(chain.id);
|
|
42
45
|
if (existing && existing.key === key) {
|
|
43
|
-
return existing.client
|
|
46
|
+
return existing.client;
|
|
44
47
|
}
|
|
45
48
|
const transport = !urls?.length
|
|
46
49
|
? http()
|
|
47
50
|
: fallback(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
);
|
|
64
67
|
const client = createPublicClient({
|
|
65
68
|
chain,
|
|
66
69
|
transport,
|
|
67
|
-
})
|
|
70
|
+
});
|
|
68
71
|
clientCache.set(chain.id, {
|
|
69
72
|
key,
|
|
70
73
|
client,
|
|
71
|
-
})
|
|
72
|
-
return client
|
|
73
|
-
}
|
|
74
|
+
});
|
|
75
|
+
return client;
|
|
76
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
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
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
1
2
|
import {
|
|
2
|
-
type Chain,
|
|
3
|
-
type PublicClient,
|
|
4
|
-
type Hex,
|
|
5
3
|
type Abi,
|
|
4
|
+
type Chain,
|
|
6
5
|
erc20Abi,
|
|
7
6
|
erc20Abi_bytes32,
|
|
8
7
|
getAddress,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { multicallRead } from './multicall.js'
|
|
8
|
+
type Hex,
|
|
9
|
+
type PublicClient,
|
|
10
|
+
} from 'viem';
|
|
11
|
+
import { multicallRead } from './multicall.js';
|
|
12
|
+
import type { Erc20Metadata } from './types.js';
|
|
13
13
|
|
|
14
14
|
/** The erc20 metadata calls */
|
|
15
15
|
export const erc20MetadataCalls = [
|
|
16
16
|
{ functionName: 'name' },
|
|
17
17
|
{ functionName: 'symbol' },
|
|
18
18
|
{ functionName: 'decimals' },
|
|
19
|
-
]
|
|
19
|
+
];
|
|
20
20
|
|
|
21
21
|
/** The multicall for the erc20 metadata */
|
|
22
22
|
export const multicallErc20 = _.memoize(
|
|
@@ -27,17 +27,17 @@ export const multicallErc20 = _.memoize(
|
|
|
27
27
|
abi: erc20Abi,
|
|
28
28
|
target,
|
|
29
29
|
calls: erc20MetadataCalls,
|
|
30
|
-
}
|
|
30
|
+
};
|
|
31
31
|
try {
|
|
32
|
-
return await multicallRead<Erc20Metadata>(options)
|
|
32
|
+
return await multicallRead<Erc20Metadata>(options);
|
|
33
33
|
} catch {
|
|
34
34
|
return await multicallRead<Erc20Metadata>({
|
|
35
35
|
...options,
|
|
36
36
|
abi: erc20Abi_bytes32,
|
|
37
|
-
})
|
|
37
|
+
});
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
({ target, chain }) => {
|
|
41
|
-
return !target ? chain.id : getAddress(target, chain.id)
|
|
41
|
+
return !target ? chain.id : getAddress(target, chain.id);
|
|
42
42
|
},
|
|
43
|
-
)
|
|
43
|
+
);
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
1
|
+
export * from './assert.js';
|
|
2
|
+
export * from './cache.js';
|
|
3
|
+
export * from './client.js';
|
|
4
|
+
export * from './erc20.js';
|
|
5
|
+
export * from './multicall.js';
|
|
6
|
+
export * from './serialize.js';
|
|
7
|
+
export * from './types.js';
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import type { Chain, Hex, PublicClient } from 'viem';
|
|
2
|
+
import { erc20Abi } from 'viem';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { multicallRead } from './multicall';
|
|
5
|
+
|
|
6
|
+
// Mock viem functions
|
|
7
|
+
vi.mock('viem', async () => {
|
|
8
|
+
const actual = await vi.importActual<typeof import('viem')>('viem');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
getContract: vi.fn(() => ({
|
|
12
|
+
read: {
|
|
13
|
+
aggregate3: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
})),
|
|
16
|
+
encodeFunctionData: vi.fn(() => '0xENCODED_DATA' as Hex),
|
|
17
|
+
decodeFunctionResult: vi.fn((params: any) => {
|
|
18
|
+
// Return different mock data based on function name
|
|
19
|
+
if (params.functionName === 'name') return 'Test Token';
|
|
20
|
+
if (params.functionName === 'symbol') return 'TST';
|
|
21
|
+
if (params.functionName === 'decimals') return 18;
|
|
22
|
+
if (params.functionName === 'balanceOf') return 1000000000000000000n;
|
|
23
|
+
return 'MOCK_RESULT';
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('multicall', () => {
|
|
29
|
+
let mockClient: PublicClient;
|
|
30
|
+
let mockChain: Chain;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockChain = {
|
|
34
|
+
id: 1,
|
|
35
|
+
name: 'Ethereum',
|
|
36
|
+
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
37
|
+
rpcUrls: {
|
|
38
|
+
default: { http: ['https://eth.llamarpc.com'] },
|
|
39
|
+
},
|
|
40
|
+
contracts: {
|
|
41
|
+
multicall3: {
|
|
42
|
+
address: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
} as Chain;
|
|
46
|
+
|
|
47
|
+
mockClient = {
|
|
48
|
+
chain: mockChain,
|
|
49
|
+
} as PublicClient;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('multicallRead', () => {
|
|
53
|
+
it('should successfully read multiple calls', async () => {
|
|
54
|
+
const mockAggregate3 = vi.fn().mockResolvedValue([
|
|
55
|
+
{ success: true, returnData: '0xRESULT1' as Hex },
|
|
56
|
+
{ success: true, returnData: '0xRESULT2' as Hex },
|
|
57
|
+
{ success: true, returnData: '0xRESULT3' as Hex },
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const { getContract } = await import('viem');
|
|
61
|
+
(getContract as any).mockReturnValue({
|
|
62
|
+
read: {
|
|
63
|
+
aggregate3: mockAggregate3,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await multicallRead({
|
|
68
|
+
chain: mockChain,
|
|
69
|
+
client: mockClient,
|
|
70
|
+
abi: erc20Abi,
|
|
71
|
+
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
72
|
+
calls: [{ functionName: 'name' }, { functionName: 'symbol' }, { functionName: 'decimals' }],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result).toBeDefined();
|
|
76
|
+
expect(Array.isArray(result)).toBe(true);
|
|
77
|
+
expect(result).toHaveLength(3);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle calls with arguments', async () => {
|
|
81
|
+
const mockAggregate3 = vi
|
|
82
|
+
.fn()
|
|
83
|
+
.mockResolvedValue([{ success: true, returnData: '0xRESULT1' as Hex }]);
|
|
84
|
+
|
|
85
|
+
const { getContract } = await import('viem');
|
|
86
|
+
(getContract as any).mockReturnValue({
|
|
87
|
+
read: {
|
|
88
|
+
aggregate3: mockAggregate3,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await multicallRead({
|
|
93
|
+
chain: mockChain,
|
|
94
|
+
client: mockClient,
|
|
95
|
+
abi: erc20Abi,
|
|
96
|
+
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
97
|
+
calls: [
|
|
98
|
+
{
|
|
99
|
+
functionName: 'balanceOf',
|
|
100
|
+
args: ['0x1111111111111111111111111111111111111111' as Hex],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result).toBeDefined();
|
|
106
|
+
expect(Array.isArray(result)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle calls with allowFailure and failure', async () => {
|
|
110
|
+
const mockAggregate3 = vi.fn().mockResolvedValue([
|
|
111
|
+
{ success: false, returnData: '0xERROR' as Hex },
|
|
112
|
+
{ success: true, returnData: '0xRESULT2' as Hex },
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const { getContract } = await import('viem');
|
|
116
|
+
(getContract as any).mockReturnValue({
|
|
117
|
+
read: {
|
|
118
|
+
aggregate3: mockAggregate3,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await multicallRead({
|
|
123
|
+
chain: mockChain,
|
|
124
|
+
client: mockClient,
|
|
125
|
+
abi: erc20Abi,
|
|
126
|
+
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
127
|
+
calls: [
|
|
128
|
+
{ functionName: 'name', allowFailure: true },
|
|
129
|
+
{ functionName: 'symbol', allowFailure: false },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result).toBeDefined();
|
|
134
|
+
expect(result[0]).toBe('0xERROR'); // Failed call returns returnData
|
|
135
|
+
expect(result[1]).toBe('TST'); // Successful call returns decoded result (symbol mock)
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle calls with allowFailure and success', async () => {
|
|
139
|
+
const mockAggregate3 = vi.fn().mockResolvedValue([
|
|
140
|
+
{ success: true, returnData: '0xRESULT1' as Hex },
|
|
141
|
+
{ success: true, returnData: '0xRESULT2' as Hex },
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const { getContract } = await import('viem');
|
|
145
|
+
(getContract as any).mockReturnValue({
|
|
146
|
+
read: {
|
|
147
|
+
aggregate3: mockAggregate3,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const result = await multicallRead({
|
|
152
|
+
chain: mockChain,
|
|
153
|
+
client: mockClient,
|
|
154
|
+
abi: erc20Abi,
|
|
155
|
+
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
156
|
+
calls: [
|
|
157
|
+
{ functionName: 'name', allowFailure: true }, // success=true, allowFailure=true -> decode
|
|
158
|
+
{ functionName: 'symbol', allowFailure: false },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result).toBeDefined();
|
|
163
|
+
expect(result[0]).toBe('Test Token'); // Successful call with allowFailure=true returns decoded result
|
|
164
|
+
expect(result[1]).toBe('TST'); // Successful call returns decoded result
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle calls with custom target addresses', async () => {
|
|
168
|
+
const mockAggregate3 = vi.fn().mockResolvedValue([
|
|
169
|
+
{ success: true, returnData: '0xRESULT1' as Hex },
|
|
170
|
+
{ success: true, returnData: '0xRESULT2' as Hex },
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const { getContract } = await import('viem');
|
|
174
|
+
(getContract as any).mockReturnValue({
|
|
175
|
+
read: {
|
|
176
|
+
aggregate3: mockAggregate3,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await multicallRead({
|
|
181
|
+
chain: mockChain,
|
|
182
|
+
client: mockClient,
|
|
183
|
+
abi: erc20Abi,
|
|
184
|
+
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
185
|
+
calls: [
|
|
186
|
+
{
|
|
187
|
+
functionName: 'name',
|
|
188
|
+
target: '0x2222222222222222222222222222222222222222' as Hex,
|
|
189
|
+
},
|
|
190
|
+
{ functionName: 'symbol' }, // Uses default target
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(result).toBeDefined();
|
|
195
|
+
expect(Array.isArray(result)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should handle calls with custom ABIs', async () => {
|
|
199
|
+
const customAbi = [
|
|
200
|
+
{
|
|
201
|
+
name: 'customFunction',
|
|
202
|
+
type: 'function',
|
|
203
|
+
inputs: [],
|
|
204
|
+
outputs: [{ type: 'string' }],
|
|
205
|
+
stateMutability: 'view',
|
|
206
|
+
},
|
|
207
|
+
] as const;
|
|
208
|
+
|
|
209
|
+
const mockAggregate3 = vi
|
|
210
|
+
.fn()
|
|
211
|
+
.mockResolvedValue([{ success: true, returnData: '0xRESULT1' as Hex }]);
|
|
212
|
+
|
|
213
|
+
const { getContract } = await import('viem');
|
|
214
|
+
(getContract as any).mockReturnValue({
|
|
215
|
+
read: {
|
|
216
|
+
aggregate3: mockAggregate3,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const result = await multicallRead({
|
|
221
|
+
chain: mockChain,
|
|
222
|
+
client: mockClient,
|
|
223
|
+
abi: erc20Abi,
|
|
224
|
+
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
225
|
+
calls: [
|
|
226
|
+
{
|
|
227
|
+
functionName: 'customFunction',
|
|
228
|
+
abi: customAbi as any,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(result).toBeDefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should throw error and log when aggregate3 fails', async () => {
|
|
237
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
238
|
+
const mockError = new Error('Multicall failed');
|
|
239
|
+
|
|
240
|
+
const mockAggregate3 = vi.fn().mockRejectedValue(mockError);
|
|
241
|
+
|
|
242
|
+
const { getContract } = await import('viem');
|
|
243
|
+
(getContract as any).mockReturnValue({
|
|
244
|
+
read: {
|
|
245
|
+
aggregate3: mockAggregate3,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await expect(
|
|
250
|
+
multicallRead({
|
|
251
|
+
chain: mockChain,
|
|
252
|
+
client: mockClient,
|
|
253
|
+
abi: erc20Abi,
|
|
254
|
+
target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex,
|
|
255
|
+
calls: [{ functionName: 'name' }],
|
|
256
|
+
}),
|
|
257
|
+
).rejects.toThrow('Multicall failed');
|
|
258
|
+
|
|
259
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
260
|
+
consoleErrorSpy.mockRestore();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
package/src/multicall.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
1
2
|
import {
|
|
2
|
-
type Chain,
|
|
3
|
-
type PublicClient,
|
|
4
|
-
type Hex,
|
|
5
3
|
type Abi,
|
|
6
|
-
|
|
7
|
-
encodeFunctionData,
|
|
4
|
+
type Chain,
|
|
8
5
|
decodeFunctionResult,
|
|
6
|
+
encodeFunctionData,
|
|
7
|
+
getContract,
|
|
8
|
+
type Hex,
|
|
9
9
|
multicall3Abi,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
10
|
+
type PublicClient,
|
|
11
|
+
} from 'viem';
|
|
12
|
+
import { ensureResult } from './assert.js';
|
|
13
|
+
import type { Call } from './types.js';
|
|
13
14
|
|
|
14
15
|
/** The multicall for the given chain, client, abi, calls, and target */
|
|
15
16
|
export const multicallRead = async <T>({
|
|
@@ -19,18 +20,18 @@ export const multicallRead = async <T>({
|
|
|
19
20
|
calls,
|
|
20
21
|
target,
|
|
21
22
|
}: {
|
|
22
|
-
chain: Chain
|
|
23
|
-
client: PublicClient
|
|
24
|
-
abi: Abi
|
|
25
|
-
calls: Call[]
|
|
26
|
-
target?: Hex
|
|
23
|
+
chain: Chain;
|
|
24
|
+
client: PublicClient;
|
|
25
|
+
abi: Abi;
|
|
26
|
+
calls: Call[];
|
|
27
|
+
target?: Hex;
|
|
27
28
|
}) => {
|
|
28
29
|
const multicall = getContract({
|
|
29
30
|
abi: multicall3Abi,
|
|
30
31
|
address: chain.contracts!.multicall3!.address!,
|
|
31
32
|
// this seems like a bug in viem, it should be able to infer the type of the client
|
|
32
33
|
client: client as any,
|
|
33
|
-
}) as any
|
|
34
|
+
}) as any;
|
|
34
35
|
const arg = calls.map((call) => ({
|
|
35
36
|
callData: encodeFunctionData({
|
|
36
37
|
abi: call.abi || abi,
|
|
@@ -40,29 +41,28 @@ export const multicallRead = async <T>({
|
|
|
40
41
|
}),
|
|
41
42
|
allowFailure: call.allowFailure || false,
|
|
42
43
|
target: (call.target || target) as Hex,
|
|
43
|
-
}))
|
|
44
|
-
let reads: null | Awaited<ReturnType<typeof multicall.read.aggregate3>> = null
|
|
44
|
+
}));
|
|
45
|
+
let reads: null | Awaited<ReturnType<typeof multicall.read.aggregate3>> = null;
|
|
45
46
|
try {
|
|
46
|
-
reads = await multicall.read.aggregate3([arg])
|
|
47
|
-
|
|
48
|
-
const r = reads
|
|
47
|
+
reads = await multicall.read.aggregate3([arg]);
|
|
48
|
+
const r = ensureResult(reads, 'Multicall aggregate3');
|
|
49
49
|
return calls.map((call, i) =>
|
|
50
50
|
call.allowFailure
|
|
51
51
|
? r[i].success
|
|
52
52
|
? decodeFunctionResult({
|
|
53
|
+
abi: call.abi || abi,
|
|
54
|
+
functionName: call.functionName,
|
|
55
|
+
data: r[i].returnData,
|
|
56
|
+
})
|
|
57
|
+
: r[i].returnData
|
|
58
|
+
: decodeFunctionResult({
|
|
53
59
|
abi: call.abi || abi,
|
|
54
60
|
functionName: call.functionName,
|
|
55
61
|
data: r[i].returnData,
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
: decodeFunctionResult({
|
|
59
|
-
abi: call.abi || abi,
|
|
60
|
-
functionName: call.functionName,
|
|
61
|
-
data: r[i].returnData,
|
|
62
|
-
}),
|
|
63
|
-
) as T
|
|
62
|
+
}),
|
|
63
|
+
) as T;
|
|
64
64
|
} catch (err) {
|
|
65
|
-
console.error(client.chain?.id, target, calls, reads)
|
|
66
|
-
throw err
|
|
65
|
+
console.error(client.chain?.id, target, calls, reads);
|
|
66
|
+
throw err;
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
};
|