📄 src/web3/subscription/contract.test.ts
D-OPEN SOVEREIGN
1import { describe, it, expect, vi, beforeEach } from 'vitest';
2
3const mocks = vi.hoisted(() => ({
4 getAddress: vi.fn(),
5 getAbi: vi.fn(),
6}));
7
8vi.mock('../registry', () => ({
9 addressLoader: {
10 getAddress: mocks.getAddress,
11 getAbi: mocks.getAbi,
12 },
13}));
14
15import { multicallGetSubscriptions, multicallGetSubscriptionCounts } from './contract';
16
17const FAKE_ABI = [{ type: 'function', name: 'getSubscriptions' }];
18const SM_ADDRESS = '0xSubMgr00000000000000000000000000000001';
19
20function fakeClient(overrides: any = {}) {
21 return {
22 chainId: 1116,
23 environment: 'production',
24 publicClient: { multicall: vi.fn() },
25 subscriptionManager: {
26 getSubscriptions: vi.fn(),
27 getSubscriptionCount: vi.fn(),
28 },
29 ...overrides,
30 };
31}
32
33beforeEach(() => {
34 mocks.getAddress.mockReturnValue(SM_ADDRESS);
35 mocks.getAbi.mockReturnValue(FAKE_ABI);
36});
37
38describe('multicallGetSubscriptions', () => {
39 it('returns an empty map without calling multicall for an empty userIds list', async () => {
40 const client = fakeClient();
41 const result = await multicallGetSubscriptions(client as any, []);
42 expect(result.size).toBe(0);
43 expect(client.publicClient.multicall).not.toHaveBeenCalled();
44 });
45
46 it('maps successful multicall results to number[] per userId', async () => {
47 const client = fakeClient();
48 (client.publicClient.multicall as any).mockResolvedValue([
49 { status: 'success', result: [2, 3] },
50 { status: 'success', result: [] },
51 ]);
52
53 const result = await multicallGetSubscriptions(client as any, [1, 2]);
54 expect(result.get(1)).toEqual([2, 3]);
55 expect(result.get(2)).toEqual([]);
56 });
57
58 it('defaults a failed individual multicall entry to an empty array rather than dropping the userId', async () => {
59 const client = fakeClient();
60 (client.publicClient.multicall as any).mockResolvedValue([
61 { status: 'failure', error: new Error('revert') },
62 { status: 'success', result: [5] },
63 ]);
64
65 const result = await multicallGetSubscriptions(client as any, [1, 2]);
66 expect(result.get(1)).toEqual([]);
67 expect(result.get(2)).toEqual([5]);
68 });
69
70 it('falls back to sequential per-user reads when the address/ABI binding is unavailable', async () => {
71 mocks.getAddress.mockReturnValue(undefined);
72 const client = fakeClient();
73 client.subscriptionManager.getSubscriptions.mockImplementation(async (id: number) => (id === 1 ? [9] : []));
74
75 const result = await multicallGetSubscriptions(client as any, [1, 2]);
76 expect(client.publicClient.multicall).not.toHaveBeenCalled();
77 expect(result.get(1)).toEqual([9]);
78 expect(result.get(2)).toEqual([]);
79 });
80
81 it('falls back to sequential reads when the multicall RPC call itself throws', async () => {
82 const client = fakeClient();
83 (client.publicClient.multicall as any).mockRejectedValue(new Error('multicall unsupported on this chain'));
84 client.subscriptionManager.getSubscriptions.mockImplementation(async (id: number) => (id === 1 ? [9] : []));
85
86 const result = await multicallGetSubscriptions(client as any, [1, 2]);
87 expect(result.get(1)).toEqual([9]);
88 expect(result.get(2)).toEqual([]);
89 });
90
91 it('sequential fallback defaults a per-user error to an empty array', async () => {
92 mocks.getAddress.mockReturnValue(undefined);
93 const client = fakeClient();
94 client.subscriptionManager.getSubscriptions.mockRejectedValue(new Error('rpc down'));
95
96 const result = await multicallGetSubscriptions(client as any, [1]);
97 expect(result.get(1)).toEqual([]);
98 });
99});
100
101describe('multicallGetSubscriptionCounts', () => {
102 it('returns an empty map without calling multicall for an empty userIds list', async () => {
103 const client = fakeClient();
104 const result = await multicallGetSubscriptionCounts(client as any, []);
105 expect(result.size).toBe(0);
106 expect(client.publicClient.multicall).not.toHaveBeenCalled();
107 });
108
109 it('maps successful multicall results to Number(count) per userId', async () => {
110 const client = fakeClient();
111 (client.publicClient.multicall as any).mockResolvedValue([
112 { status: 'success', result: 3n },
113 { status: 'success', result: 0n },
114 ]);
115
116 const result = await multicallGetSubscriptionCounts(client as any, [1, 2]);
117 expect(result.get(1)).toBe(3);
118 expect(result.get(2)).toBe(0);
119 });
120
121 it('defaults a failed individual multicall entry to 0', async () => {
122 const client = fakeClient();
123 (client.publicClient.multicall as any).mockResolvedValue([
124 { status: 'failure', error: new Error('revert') },
125 ]);
126
127 const result = await multicallGetSubscriptionCounts(client as any, [1]);
128 expect(result.get(1)).toBe(0);
129 });
130
131 it('falls back to sequential reads when the binding is unavailable, converting bigint results to Number', async () => {
132 mocks.getAbi.mockReturnValue(null);
133 const client = fakeClient();
134 client.subscriptionManager.getSubscriptionCount.mockResolvedValue(7n);
135
136 const result = await multicallGetSubscriptionCounts(client as any, [1]);
137 expect(client.publicClient.multicall).not.toHaveBeenCalled();
138 expect(result.get(1)).toBe(7);
139 });
140
141 it('sequential fallback defaults a per-user error to 0', async () => {
142 mocks.getAbi.mockReturnValue(null);
143 const client = fakeClient();
144 client.subscriptionManager.getSubscriptionCount.mockRejectedValue(new Error('rpc down'));
145
146 const result = await multicallGetSubscriptionCounts(client as any, [1]);
147 expect(result.get(1)).toBe(0);
148 });
149});
150