Skip to content
This repository was archived by the owner on Jul 9, 2024. It is now read-only.

Commit 98aced0

Browse files
authored
Merge pull request #400 from api3dao/filter-empty-sponsors
Filter empty sponsors
2 parents 2466c70 + b415d94 commit 98aced0

File tree

6 files changed

+421
-38
lines changed

6 files changed

+421
-38
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"build": "yarn clean && yarn compile && yarn build:ncc",
2424
"clean": "rimraf -rf ./ncc ./build ./dist *.tgz",
2525
"compile": "tsc --build tsconfig.json",
26-
"build:ncc": "ncc build -m -o ncc -s --target es2020 src/serverless.ts",
26+
"build:ncc": "ncc build -m -o ncc -s --target es2021 src/serverless.ts",
2727
"dev:create-local-config": "ts-node scripts/create-local-config.js",
2828
"dev:setup-local-node": "hardhat run scripts/setup-local-node.js",
2929
"dev:api": "ts-node test/server/server.ts",

src/main.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { loadConfig } from './config';
44
import { initiateFetchingBeaconData } from './fetch-beacon-data';
55
import { initiateDataFeedUpdates } from './update-data-feeds';
66
import { initializeProviders } from './providers';
7-
import { initializeWallets } from './wallets';
7+
import { filterEmptySponsors, initializeWallets } from './wallets';
88
import { expireLimiterJobs, initializeState, updateState } from './state';
99

1010
export const handleStopSignal = (signal: string) => {
@@ -30,5 +30,6 @@ export async function main() {
3030

3131
initializeProviders();
3232
initializeWallets();
33+
await filterEmptySponsors();
3334
await Promise.all([initiateFetchingBeaconData(), initiateDataFeedUpdates()]);
3435
}

src/state.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type Provider = {
2525
};
2626
// chainId => Provider[]
2727
export type Providers = Record<string, Provider[]>;
28-
// sponsorAddress => sponsorWallet
28+
// sponsorAddress => sponsorWalletPrivateKey
2929
export type SponsorWalletsPrivateKey = Record<string, string>;
3030

3131
export interface State {

src/wallets.test.ts

+300-30
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,54 @@
1+
import { ethers } from 'ethers';
12
import * as state from './state';
3+
import * as wallets from './wallets';
24
import { Config } from './validation';
3-
import { initializeWallets } from './wallets';
5+
import { RateLimitedProvider } from './providers';
6+
import { logger } from './logging';
7+
import { shortenAddress } from './utils';
48

5-
describe('initializeWallets', () => {
6-
const config = {
7-
log: {
8-
format: 'plain',
9-
level: 'DEBUG',
10-
},
11-
airseekerWalletMnemonic: 'achieve climb couple wait accident symbol spy blouse reduce foil echo label',
12-
triggers: {
13-
dataFeedUpdates: {
14-
1: {
15-
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC': {
16-
beacons: [],
17-
beaconSets: [],
18-
updateInterval: 30,
19-
},
9+
const config = {
10+
log: {
11+
format: 'plain',
12+
level: 'DEBUG',
13+
},
14+
airseekerWalletMnemonic: 'achieve climb couple wait accident symbol spy blouse reduce foil echo label',
15+
triggers: {
16+
dataFeedUpdates: {
17+
1: {
18+
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC': {
19+
beacons: [],
20+
beaconSets: [],
21+
updateInterval: 30,
2022
},
21-
3: {
22-
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC': {
23-
beacons: [],
24-
beaconSets: [],
25-
updateInterval: 30,
26-
},
27-
'0x150700e52ba22fe103d60981c97bc223ac40dd4e': {
28-
beacons: [],
29-
beaconSets: [],
30-
updateInterval: 30,
31-
},
23+
},
24+
3: {
25+
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC': {
26+
beacons: [],
27+
beaconSets: [],
28+
updateInterval: 30,
29+
},
30+
'0x150700e52ba22fe103d60981c97bc223ac40dd4e': {
31+
beacons: [],
32+
beaconSets: [],
33+
updateInterval: 30,
3234
},
3335
},
3436
},
35-
} as unknown as Config;
37+
},
38+
} as unknown as Config;
39+
40+
beforeEach(() => {
3641
state.initializeState(config);
42+
wallets.initializeWallets();
43+
});
3744

38-
it('initialize wallets', () => {
39-
initializeWallets();
45+
afterEach(() => {
46+
jest.clearAllMocks();
47+
});
4048

49+
describe('initializeWallets', () => {
50+
// This test ensures the initialization of the wallets and their private keys.
51+
it('initialize wallets', () => {
4152
const { airseekerWalletPrivateKey, sponsorWalletsPrivateKey } = state.getState();
4253

4354
expect(typeof airseekerWalletPrivateKey).toBe('string');
@@ -55,3 +66,262 @@ describe('initializeWallets', () => {
5566
);
5667
});
5768
});
69+
70+
describe('retrieveSponsorWalletAddress', () => {
71+
beforeEach(() => {
72+
jest.spyOn(state, 'getState');
73+
});
74+
75+
// This test checks if the function retrieves the correct wallet address for a given sponsor address.
76+
it('should return the wallet address corresponding to the sponsor address', () => {
77+
const sponsorAddress = '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC';
78+
const expectedWalletAddress = '0x1129eEDf4996cF133e0e9555d4c9d305c9918EC5';
79+
80+
const walletAddress = wallets.retrieveSponsorWalletAddress(sponsorAddress);
81+
82+
expect(walletAddress).toBe(expectedWalletAddress);
83+
expect(state.getState).toHaveBeenCalledTimes(1);
84+
});
85+
86+
// This test checks if the function throws an error when the sponsor address does not have an associated private key.
87+
it('should throw if private key of sponsor wallet not found for the sponsor', () => {
88+
const sponsorAddress = '0x0000000000000000000000000000000000000000';
89+
const expectedErrorMessage = `Pre-generated private key not found for sponsor ${sponsorAddress}`;
90+
expect(() => wallets.retrieveSponsorWalletAddress(sponsorAddress)).toThrow(expectedErrorMessage);
91+
});
92+
});
93+
94+
describe('isBalanceZero', () => {
95+
// This test checks if the function correctly identifies a zero balance.
96+
it('should return true if the balance is zero', async () => {
97+
const rpcProvider = {
98+
getBalance: jest.fn().mockResolvedValueOnce(ethers.BigNumber.from('0x0')),
99+
} as unknown as RateLimitedProvider;
100+
101+
const sponsorWalletAddress = 'sponsorWalletAddress';
102+
const expectedBalanceStatus = true;
103+
104+
const balanceStatus = await wallets.isBalanceZero(rpcProvider, sponsorWalletAddress);
105+
106+
expect(balanceStatus).toBe(expectedBalanceStatus);
107+
expect(rpcProvider.getBalance).toHaveBeenCalledTimes(1);
108+
expect(rpcProvider.getBalance).toHaveBeenCalledWith(sponsorWalletAddress);
109+
});
110+
111+
// This test checks if the function correctly identifies a non-zero balance.
112+
it('should return false if the balance is non-zero', async () => {
113+
const rpcProvider = {
114+
getBalance: jest.fn().mockResolvedValueOnce(ethers.BigNumber.from('0x3')),
115+
} as unknown as RateLimitedProvider;
116+
117+
const sponsorWalletAddress = 'sponsorWalletAddress';
118+
const expectedBalanceStatus = false;
119+
120+
const balanceStatus = await wallets.isBalanceZero(rpcProvider, sponsorWalletAddress);
121+
122+
expect(balanceStatus).toBe(expectedBalanceStatus);
123+
expect(rpcProvider.getBalance).toHaveBeenCalledTimes(1);
124+
expect(rpcProvider.getBalance).toHaveBeenCalledWith(sponsorWalletAddress);
125+
});
126+
127+
// This test checks if the function properly throws an error when the balance retrieval fails.
128+
it('should throw an error if the balance retrieval fails', async () => {
129+
const rpcProvider = {
130+
getBalance: jest.fn().mockRejectedValue(new Error('RPC Error while retrieving balance')),
131+
} as unknown as RateLimitedProvider;
132+
133+
const sponsorWalletAddress = 'sponsorWalletAddress';
134+
const expectedErrorMessage = 'RPC Error while retrieving balance';
135+
136+
await expect(wallets.isBalanceZero(rpcProvider, sponsorWalletAddress)).rejects.toThrow(expectedErrorMessage);
137+
138+
expect(rpcProvider.getBalance).toHaveBeenCalledTimes(2);
139+
expect(rpcProvider.getBalance).toHaveBeenCalledWith(sponsorWalletAddress);
140+
});
141+
});
142+
143+
describe('getSponsorBalanceStatus', () => {
144+
// This test checks if the function can correctly retrieve the balance status when at least one provider is successful.
145+
it('should return the SponsorBalanceStatus if one of providers returns successfully', async () => {
146+
const chainSponsorGroup: wallets.ChainSponsorGroup = {
147+
chainId: 'chainId1',
148+
sponsorAddress: 'sponsorAddress1',
149+
providers: [
150+
{
151+
rpcProvider: {
152+
getBalance: jest.fn().mockResolvedValueOnce(ethers.BigNumber.from('0x0')),
153+
} as unknown as RateLimitedProvider,
154+
chainId: 'chainId1',
155+
providerName: 'provider1',
156+
},
157+
{
158+
rpcProvider: {
159+
getBalance: jest.fn().mockRejectedValue(new Error('RPC Error while retrieving balance')),
160+
} as unknown as RateLimitedProvider,
161+
chainId: 'chainId1',
162+
providerName: 'provider2',
163+
},
164+
],
165+
};
166+
167+
jest.spyOn(wallets, 'retrieveSponsorWalletAddress').mockImplementation(() => 'sponsorWalletAddress1');
168+
jest.spyOn(wallets, 'isBalanceZero');
169+
170+
const expectedSponsorBalanceStatus = {
171+
sponsorAddress: 'sponsorAddress1',
172+
chainId: 'chainId1',
173+
isEmpty: true,
174+
};
175+
176+
const sponsorBalanceStatus = await wallets.getSponsorBalanceStatus(chainSponsorGroup);
177+
178+
expect(wallets.retrieveSponsorWalletAddress).toHaveBeenCalledTimes(1);
179+
expect(wallets.retrieveSponsorWalletAddress).toHaveBeenCalledWith('sponsorAddress1');
180+
expect(wallets.isBalanceZero).toHaveBeenCalledTimes(2);
181+
expect(wallets.isBalanceZero).toHaveBeenCalledWith(
182+
chainSponsorGroup.providers[0].rpcProvider,
183+
'sponsorWalletAddress1'
184+
);
185+
expect(wallets.isBalanceZero).toHaveBeenCalledWith(
186+
chainSponsorGroup.providers[1].rpcProvider,
187+
'sponsorWalletAddress1'
188+
);
189+
expect(sponsorBalanceStatus).toEqual(expectedSponsorBalanceStatus);
190+
});
191+
192+
// This test checks if the function returns null when all providers fail to retrieve the balance.
193+
it('should return null if balance retrieval fails for all providers', async () => {
194+
const chainSponsorGroup: wallets.ChainSponsorGroup = {
195+
chainId: 'chainId1',
196+
sponsorAddress: 'sponsorAddress1',
197+
providers: [
198+
{
199+
rpcProvider: {
200+
getBalance: jest.fn().mockRejectedValue(new Error('RPC Error while retrieving balance')),
201+
} as unknown as RateLimitedProvider,
202+
chainId: 'chainId1',
203+
providerName: 'provider1',
204+
},
205+
{
206+
rpcProvider: {
207+
getBalance: jest.fn().mockRejectedValue(new Error('RPC Error while retrieving balance')),
208+
} as unknown as RateLimitedProvider,
209+
chainId: 'chainId1',
210+
providerName: 'provider2',
211+
},
212+
],
213+
};
214+
215+
jest.spyOn(wallets, 'retrieveSponsorWalletAddress').mockImplementation(() => 'sponsorWalletAddress1');
216+
jest.spyOn(logger, 'warn');
217+
218+
const expectedSponsorBalanceStatus = null;
219+
220+
const sponsorBalanceStatus = await wallets.getSponsorBalanceStatus(chainSponsorGroup);
221+
222+
expect(sponsorBalanceStatus).toEqual(expectedSponsorBalanceStatus);
223+
expect(logger.warn).toHaveBeenCalledWith(
224+
'Failed to get balance for sponsorWalletAddress1. No provider was resolved. Error: All promises were rejected',
225+
{ meta: { 'Chain-ID': chainSponsorGroup.chainId, Sponsor: shortenAddress(chainSponsorGroup.sponsorAddress) } }
226+
);
227+
});
228+
229+
// This test checks if the function returns null when the retrieval of the sponsor wallet fails.
230+
it('should return null if sponsor wallet retrieval fails', async () => {
231+
const chainSponsorGroup: wallets.ChainSponsorGroup = {
232+
chainId: 'chainId1',
233+
sponsorAddress: 'sponsorAddress1',
234+
providers: [
235+
{
236+
rpcProvider: {
237+
getBalance: jest.fn().mockResolvedValueOnce(ethers.BigNumber.from('0x0')),
238+
} as unknown as RateLimitedProvider,
239+
chainId: 'chainId1',
240+
providerName: 'provider1',
241+
},
242+
],
243+
};
244+
245+
const innerErrMsg = 'Pre-generated private key not found';
246+
jest.spyOn(wallets, 'retrieveSponsorWalletAddress').mockImplementation(() => {
247+
throw new Error(innerErrMsg);
248+
});
249+
jest.spyOn(logger, 'warn');
250+
251+
const expectedSponsorBalanceStatus = null;
252+
253+
const sponsorBalanceStatus = await wallets.getSponsorBalanceStatus(chainSponsorGroup);
254+
255+
expect(sponsorBalanceStatus).toEqual(expectedSponsorBalanceStatus);
256+
expect(logger.warn).toHaveBeenCalledWith(
257+
`Failed to retrieve wallet address for sponsor ${chainSponsorGroup.sponsorAddress}. Skipping. Error: ${innerErrMsg}`,
258+
{ meta: { 'Chain-ID': chainSponsorGroup.chainId, Sponsor: shortenAddress(chainSponsorGroup.sponsorAddress) } }
259+
);
260+
});
261+
});
262+
263+
describe('filterSponsorWallets', () => {
264+
// This test checks if the function correctly updates the state configuration.
265+
it('should update the state to include only funded sponsors', async () => {
266+
const stateProviders: state.Providers = {
267+
1: [
268+
{
269+
rpcProvider: {
270+
getBalance: jest.fn().mockResolvedValue(ethers.BigNumber.from('0x3')),
271+
} as unknown as RateLimitedProvider,
272+
chainId: '1',
273+
providerName: 'provider1',
274+
},
275+
],
276+
3: [
277+
{
278+
rpcProvider: {
279+
getBalance: jest.fn().mockResolvedValue(ethers.BigNumber.from('0x0')),
280+
} as unknown as RateLimitedProvider,
281+
chainId: '3',
282+
providerName: 'provider2',
283+
},
284+
],
285+
};
286+
state.updateState((state) => ({ ...state, providers: stateProviders }));
287+
288+
const expectedConfig = {
289+
log: {
290+
format: 'plain',
291+
level: 'DEBUG',
292+
},
293+
airseekerWalletMnemonic: 'achieve climb couple wait accident symbol spy blouse reduce foil echo label',
294+
triggers: {
295+
dataFeedUpdates: {
296+
1: {
297+
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC': {
298+
beacons: [],
299+
beaconSets: [],
300+
updateInterval: 30,
301+
},
302+
},
303+
},
304+
},
305+
};
306+
307+
const expectedSponsorWalletsPrivateKey = {
308+
'0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC':
309+
'0xcda66e77ae4eaab188a15717955f23cb7ee2a15f024eb272a7561cede1be427c',
310+
};
311+
312+
jest.spyOn(logger, 'info');
313+
jest.spyOn(state, 'updateState');
314+
jest.spyOn(state, 'getState');
315+
316+
await wallets.filterEmptySponsors();
317+
const { config: resultedConfig, sponsorWalletsPrivateKey: resultedSponsorWalletsPrivateKey } = state.getState();
318+
319+
expect(state.updateState).toHaveBeenCalledTimes(1);
320+
expect(logger.info).toHaveBeenCalledTimes(1);
321+
expect(logger.info).toHaveBeenCalledWith(
322+
'Fetched balances for 3/3 sponsor wallets. Continuing with 1 funded sponsors.'
323+
);
324+
expect(resultedConfig).toStrictEqual(expectedConfig);
325+
expect(resultedSponsorWalletsPrivateKey).toStrictEqual(expectedSponsorWalletsPrivateKey);
326+
});
327+
});

0 commit comments

Comments
 (0)