1
+ import { ethers } from 'ethers' ;
1
2
import * as state from './state' ;
3
+ import * as wallets from './wallets' ;
2
4
import { Config } from './validation' ;
3
- import { initializeWallets } from './wallets' ;
5
+ import { RateLimitedProvider } from './providers' ;
6
+ import { logger } from './logging' ;
7
+ import { shortenAddress } from './utils' ;
4
8
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 ,
20
22
} ,
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 ,
32
34
} ,
33
35
} ,
34
36
} ,
35
- } as unknown as Config ;
37
+ } ,
38
+ } as unknown as Config ;
39
+
40
+ beforeEach ( ( ) => {
36
41
state . initializeState ( config ) ;
42
+ wallets . initializeWallets ( ) ;
43
+ } ) ;
37
44
38
- it ( 'initialize wallets' , ( ) => {
39
- initializeWallets ( ) ;
45
+ afterEach ( ( ) => {
46
+ jest . clearAllMocks ( ) ;
47
+ } ) ;
40
48
49
+ describe ( 'initializeWallets' , ( ) => {
50
+ // This test ensures the initialization of the wallets and their private keys.
51
+ it ( 'initialize wallets' , ( ) => {
41
52
const { airseekerWalletPrivateKey, sponsorWalletsPrivateKey } = state . getState ( ) ;
42
53
43
54
expect ( typeof airseekerWalletPrivateKey ) . toBe ( 'string' ) ;
@@ -55,3 +66,262 @@ describe('initializeWallets', () => {
55
66
) ;
56
67
} ) ;
57
68
} ) ;
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