@chorus-one/polygon 1.0.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.
Files changed (43) hide show
  1. package/.mocharc.json +6 -0
  2. package/LICENSE +13 -0
  3. package/README.md +233 -0
  4. package/dist/cjs/constants.d.ts +187 -0
  5. package/dist/cjs/constants.js +141 -0
  6. package/dist/cjs/index.d.ts +4 -0
  7. package/dist/cjs/index.js +12 -0
  8. package/dist/cjs/package.json +3 -0
  9. package/dist/cjs/referrer.d.ts +2 -0
  10. package/dist/cjs/referrer.js +15 -0
  11. package/dist/cjs/staker.d.ts +335 -0
  12. package/dist/cjs/staker.js +716 -0
  13. package/dist/cjs/types.d.ts +40 -0
  14. package/dist/cjs/types.js +2 -0
  15. package/dist/mjs/constants.d.ts +187 -0
  16. package/dist/mjs/constants.js +138 -0
  17. package/dist/mjs/index.d.ts +4 -0
  18. package/dist/mjs/index.js +2 -0
  19. package/dist/mjs/package.json +3 -0
  20. package/dist/mjs/referrer.d.ts +2 -0
  21. package/dist/mjs/referrer.js +11 -0
  22. package/dist/mjs/staker.d.ts +335 -0
  23. package/dist/mjs/staker.js +712 -0
  24. package/dist/mjs/types.d.ts +40 -0
  25. package/dist/mjs/types.js +1 -0
  26. package/hardhat.config.ts +27 -0
  27. package/package.json +50 -0
  28. package/src/constants.ts +151 -0
  29. package/src/index.ts +14 -0
  30. package/src/referrer.ts +15 -0
  31. package/src/staker.ts +878 -0
  32. package/src/types.ts +45 -0
  33. package/test/fixtures/expected-data.ts +17 -0
  34. package/test/integration/localSigner.spec.ts +128 -0
  35. package/test/integration/setup.ts +41 -0
  36. package/test/integration/staker.spec.ts +587 -0
  37. package/test/integration/testStaker.ts +130 -0
  38. package/test/integration/utils.ts +263 -0
  39. package/test/lib/networks.json +14 -0
  40. package/test/staker.spec.ts +154 -0
  41. package/tsconfig.cjs.json +9 -0
  42. package/tsconfig.json +13 -0
  43. package/tsconfig.mjs.json +9 -0
@@ -0,0 +1,587 @@
1
+ import { PolygonStaker, EXCHANGE_RATE_HIGH_PRECISION, VALIDATOR_SHARE_ABI } from '@chorus-one/polygon'
2
+ import {
3
+ type PublicClient,
4
+ type WalletClient,
5
+ type Address,
6
+ parseEther,
7
+ formatEther,
8
+ createWalletClient,
9
+ http,
10
+ maxUint256
11
+ } from 'viem'
12
+ import { hardhat } from 'viem/chains'
13
+ import { use, expect, assert } from 'chai'
14
+ import chaiAsPromised from 'chai-as-promised'
15
+ import {
16
+ prepareTests,
17
+ fundWithStakingToken,
18
+ approve,
19
+ approveAndStake,
20
+ unstake,
21
+ sendTx,
22
+ advanceEpoch,
23
+ impersonate,
24
+ getStakingTokenBalance,
25
+ getWithdrawalDelay
26
+ } from './utils'
27
+ import { restoreToInitialState } from './setup'
28
+
29
+ use(chaiAsPromised)
30
+
31
+ const AMOUNT = '100'
32
+
33
+ // Existing Chorus One validator delegator with accrued rewards at block 24382010
34
+ const WHALE_DELEGATOR = '0xf382c7202ff9fa88f5ee4054b124fbb9cc196c6e' as Address
35
+
36
+ describe('PolygonStaker', () => {
37
+ let delegatorAddress: Address
38
+ let validatorShareAddress: Address
39
+ let walletClient: WalletClient
40
+ let publicClient: PublicClient
41
+ let staker: PolygonStaker
42
+
43
+ afterEach(async () => {
44
+ await restoreToInitialState()
45
+ })
46
+
47
+ describe('query methods', () => {
48
+ beforeEach(async () => {
49
+ const setup = await prepareTests()
50
+ validatorShareAddress = setup.validatorShareAddress
51
+ publicClient = setup.publicClient
52
+ staker = setup.staker
53
+ })
54
+
55
+ it('reads current epoch', async () => {
56
+ const epoch = await staker.getEpoch()
57
+ assert.equal(epoch, 96822n)
58
+ })
59
+
60
+ it('reads stake info', async () => {
61
+ const stakeInfo = await staker.getStake({ delegatorAddress: WHALE_DELEGATOR, validatorShareAddress })
62
+ assert.equal(stakeInfo.balance, formatEther(135000000000000000000000n))
63
+ })
64
+
65
+ it('reads allowance', async () => {
66
+ const allowance = await staker.getAllowance(WHALE_DELEGATOR)
67
+ assert.equal(allowance, '0')
68
+ })
69
+
70
+ it('reads unbond nonce', async () => {
71
+ const nonce = await staker.getUnbondNonce({ delegatorAddress: WHALE_DELEGATOR, validatorShareAddress })
72
+ assert.equal(nonce, 0n)
73
+ })
74
+
75
+ it('reads liquid rewards', async () => {
76
+ const rewards = await staker.getLiquidRewards({ delegatorAddress: WHALE_DELEGATOR, validatorShareAddress })
77
+ assert.equal(rewards, '45.307877957471003709')
78
+ })
79
+
80
+ it('reads withdrawal delay', async () => {
81
+ const delay = await staker.getWithdrawalDelay()
82
+ assert.equal(delay, 80n)
83
+ })
84
+
85
+ it('caches withdrawal delay on subsequent calls', async () => {
86
+ const delay1 = await staker.getWithdrawalDelay()
87
+ const delay2 = await staker.getWithdrawalDelay()
88
+ assert.equal(delay1, delay2)
89
+ assert.equal(delay1, 80n)
90
+ })
91
+
92
+ it('reads exchange rate precision for non-foundation validator', async () => {
93
+ const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
94
+ assert.equal(precision, EXCHANGE_RATE_HIGH_PRECISION)
95
+ })
96
+
97
+ })
98
+
99
+ describe('staking lifecycle', () => {
100
+ beforeEach(async () => {
101
+ const setup = await prepareTests()
102
+ delegatorAddress = setup.delegatorAddress
103
+ validatorShareAddress = setup.validatorShareAddress
104
+ walletClient = setup.walletClient
105
+ publicClient = setup.publicClient
106
+ staker = setup.staker
107
+
108
+ await fundWithStakingToken({
109
+ publicClient,
110
+ recipientAddress: delegatorAddress,
111
+ amount: parseEther('10000')
112
+ })
113
+ })
114
+
115
+ it('approves staking token and verifies allowance', async () => {
116
+ await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
117
+
118
+ const allowance = await staker.getAllowance(delegatorAddress)
119
+ assert.equal(allowance, AMOUNT)
120
+ })
121
+
122
+ it('approves max (unlimited) allowance', async () => {
123
+ await approve({ delegatorAddress, amount: 'max', staker, walletClient, publicClient })
124
+
125
+ const allowance = await staker.getAllowance(delegatorAddress)
126
+ assert.equal(parseEther(allowance), maxUint256)
127
+ })
128
+
129
+ it('stakes and verifies on-chain state', async () => {
130
+ await approveAndStake({
131
+ delegatorAddress,
132
+ validatorShareAddress,
133
+ amount: AMOUNT,
134
+ staker,
135
+ walletClient,
136
+ publicClient
137
+ })
138
+
139
+ const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
140
+ assert.equal(stakeInfo.balance, AMOUNT)
141
+ })
142
+
143
+ it('stakes with referrer tracking in tx data', async () => {
144
+ await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
145
+
146
+ const { tx } = await staker.buildStakeTx({
147
+ delegatorAddress,
148
+ validatorShareAddress,
149
+ amount: AMOUNT,
150
+ minSharesToMint: 0n
151
+ })
152
+
153
+ assert.isTrue(tx.data.includes('c1c1'), 'tx data should contain referrer marker')
154
+
155
+ const request = await walletClient.prepareTransactionRequest({ ...tx, chain: undefined })
156
+ const hash = await walletClient.sendTransaction({ ...request, account: delegatorAddress })
157
+
158
+ const onChainTx = await publicClient.getTransaction({ hash })
159
+ assert.isTrue(onChainTx.input.includes('c1c1'), 'on-chain tx input should contain referrer marker')
160
+
161
+ const receipt = await publicClient.getTransactionReceipt({ hash })
162
+ assert.equal(receipt.status, 'success')
163
+
164
+ const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
165
+ assert.equal(stakeInfo.balance, AMOUNT)
166
+ })
167
+
168
+ it('unstakes and creates unbond request with amount and isWithdrawable fields', async () => {
169
+ await approveAndStake({
170
+ delegatorAddress,
171
+ validatorShareAddress,
172
+ amount: AMOUNT,
173
+ staker,
174
+ walletClient,
175
+ publicClient
176
+ })
177
+
178
+ const nonceBefore = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
179
+ const currentEpoch = await staker.getEpoch()
180
+
181
+ const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
182
+ await unstake({
183
+ delegatorAddress,
184
+ validatorShareAddress,
185
+ amount: AMOUNT,
186
+ maximumSharesToBurn: stakeBefore.shares,
187
+ staker,
188
+ walletClient,
189
+ publicClient
190
+ })
191
+
192
+ const nonceAfter = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
193
+ assert.equal(nonceAfter, nonceBefore + 1n)
194
+
195
+ const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonceAfter })
196
+ assert.isTrue(unbond.withdrawEpoch === currentEpoch)
197
+ assert.equal(unbond.amount, AMOUNT)
198
+ assert.equal(unbond.isWithdrawable, false)
199
+
200
+ const stakeAfter = await staker.getStake({ delegatorAddress, validatorShareAddress })
201
+ assert.equal(stakeAfter.balance, '0')
202
+ })
203
+
204
+ it('stakes with slippageBps and verifies minSharesToMint calculation matches contract formula', async () => {
205
+ await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
206
+
207
+ const amountWei = parseEther(AMOUNT)
208
+ const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
209
+
210
+ // Get exchange rate from contract (same way the SDK does)
211
+ const exchangeRate = await publicClient
212
+ .readContract({
213
+ address: validatorShareAddress,
214
+ abi: VALIDATOR_SHARE_ABI,
215
+ functionName: 'getTotalStake',
216
+ args: [validatorShareAddress]
217
+ })
218
+ .then(([, rate]) => rate)
219
+
220
+ // Contract formula: shares = amount * precision / exchangeRate
221
+ const expectedShares = (amountWei * precision) / exchangeRate
222
+
223
+ // SDK slippage formula: minSharesToMint = expectedShares - (expectedShares * slippageBps / 10000)
224
+ const slippageBps = 100n // 1%
225
+ const expectedMinShares = expectedShares - (expectedShares * slippageBps) / 10000n
226
+
227
+ const { tx } = await staker.buildStakeTx({
228
+ delegatorAddress,
229
+ validatorShareAddress,
230
+ amount: AMOUNT,
231
+ slippageBps: Number(slippageBps)
232
+ })
233
+
234
+ // Decode the calldata to verify minSharesToMint
235
+ const decodedMinShares = BigInt('0x' + tx.data.slice(74, 138))
236
+ assert.equal(decodedMinShares, expectedMinShares, 'minSharesToMint should match calculated value')
237
+
238
+ // Verify transaction succeeds
239
+ await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
240
+
241
+ const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
242
+ assert.equal(stakeInfo.balance, AMOUNT)
243
+
244
+ // Verify actual shares received are >= minSharesToMint
245
+ assert.isTrue(stakeInfo.shares >= expectedMinShares, 'Actual shares should be >= minSharesToMint')
246
+ })
247
+
248
+ it('stakes with 0 slippageBps sets minSharesToMint to exact expected shares', async () => {
249
+ await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
250
+
251
+ const amountWei = parseEther(AMOUNT)
252
+ const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
253
+
254
+ const exchangeRate = await publicClient
255
+ .readContract({
256
+ address: validatorShareAddress,
257
+ abi: VALIDATOR_SHARE_ABI,
258
+ functionName: 'getTotalStake',
259
+ args: [validatorShareAddress]
260
+ })
261
+ .then(([, rate]) => rate)
262
+
263
+ const expectedShares = (amountWei * precision) / exchangeRate
264
+
265
+ const { tx } = await staker.buildStakeTx({
266
+ delegatorAddress,
267
+ validatorShareAddress,
268
+ amount: AMOUNT,
269
+ slippageBps: 0
270
+ })
271
+
272
+ const decodedMinShares = BigInt('0x' + tx.data.slice(74, 138))
273
+ assert.equal(decodedMinShares, expectedShares, 'With 0 slippage, minSharesToMint should equal expected shares')
274
+
275
+ await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
276
+ const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
277
+ assert.equal(stakeInfo.shares, expectedShares, 'Actual shares should equal expected shares')
278
+ })
279
+
280
+ it('unstakes with slippageBps and verifies maximumSharesToBurn calculation matches contract formula', async () => {
281
+ await approveAndStake({
282
+ delegatorAddress,
283
+ validatorShareAddress,
284
+ amount: AMOUNT,
285
+ staker,
286
+ walletClient,
287
+ publicClient
288
+ })
289
+
290
+ const stake = await staker.getStake({ delegatorAddress, validatorShareAddress })
291
+ const amountWei = parseEther(AMOUNT)
292
+ const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
293
+
294
+ // Contract formula: shares = claimAmount * precision / exchangeRate
295
+ const expectedShares = (amountWei * precision) / stake.exchangeRate
296
+
297
+ // SDK slippage formula: maximumSharesToBurn = expectedShares + (expectedShares * slippageBps / 10000)
298
+ const slippageBps = 100n // 1%
299
+ const expectedMaxShares = expectedShares + (expectedShares * slippageBps) / 10000n
300
+
301
+ const { tx } = await staker.buildUnstakeTx({
302
+ delegatorAddress,
303
+ validatorShareAddress,
304
+ amount: AMOUNT,
305
+ slippageBps: Number(slippageBps)
306
+ })
307
+
308
+ // Decode the calldata to verify maximumSharesToBurn (second arg after 4 byte selector + 32 byte amount)
309
+ const decodedMaxShares = BigInt('0x' + tx.data.slice(74, 138))
310
+ assert.equal(decodedMaxShares, expectedMaxShares, 'maximumSharesToBurn should match calculated value')
311
+
312
+ await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
313
+
314
+ const stakeAfter = await staker.getStake({ delegatorAddress, validatorShareAddress })
315
+ assert.equal(stakeAfter.balance, '0')
316
+
317
+ const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
318
+ const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
319
+ assert.isTrue(unbond.shares <= expectedMaxShares, 'Actual shares burned should be at most maximumSharesToBurn')
320
+ })
321
+
322
+ it('unstakes with 0 slippageBps sets maximumSharesToBurn to exact expected shares', async () => {
323
+ await approveAndStake({
324
+ delegatorAddress,
325
+ validatorShareAddress,
326
+ amount: AMOUNT,
327
+ staker,
328
+ walletClient,
329
+ publicClient
330
+ })
331
+
332
+ const stake = await staker.getStake({ delegatorAddress, validatorShareAddress })
333
+ const amountWei = parseEther(AMOUNT)
334
+ const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
335
+
336
+ const expectedShares = (amountWei * precision) / stake.exchangeRate
337
+
338
+ const { tx } = await staker.buildUnstakeTx({
339
+ delegatorAddress,
340
+ validatorShareAddress,
341
+ amount: AMOUNT,
342
+ slippageBps: 0
343
+ })
344
+
345
+ const decodedMaxShares = BigInt('0x' + tx.data.slice(74, 138))
346
+ assert.equal(
347
+ decodedMaxShares,
348
+ expectedShares,
349
+ 'With 0 slippage, maximumSharesToBurn should equal expected shares'
350
+ )
351
+
352
+ await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
353
+
354
+ const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
355
+ const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
356
+ assert.equal(unbond.shares, expectedShares, 'With 0 slippage, actual shares should equal expected shares')
357
+ })
358
+
359
+ it('fetches multiple unbonds with getUnbonds batch method', async () => {
360
+ await approveAndStake({
361
+ delegatorAddress,
362
+ validatorShareAddress,
363
+ amount: AMOUNT,
364
+ staker,
365
+ walletClient,
366
+ publicClient
367
+ })
368
+
369
+ const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
370
+
371
+ await unstake({
372
+ delegatorAddress,
373
+ validatorShareAddress,
374
+ amount: '30',
375
+ maximumSharesToBurn: (stakeBefore.shares * 30n) / 100n,
376
+ staker,
377
+ walletClient,
378
+ publicClient
379
+ })
380
+
381
+ const stakeAfterFirstUnstake = await staker.getStake({ delegatorAddress, validatorShareAddress })
382
+ await unstake({
383
+ delegatorAddress,
384
+ validatorShareAddress,
385
+ amount: '70',
386
+ maximumSharesToBurn: stakeAfterFirstUnstake.shares,
387
+ staker,
388
+ walletClient,
389
+ publicClient
390
+ })
391
+
392
+ const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
393
+ assert.equal(nonce, 2n)
394
+
395
+ const unbonds = await staker.getUnbonds({
396
+ delegatorAddress,
397
+ validatorShareAddress,
398
+ unbondNonces: [1n, 2n]
399
+ })
400
+
401
+ assert.lengthOf(unbonds, 2)
402
+ assert.equal(unbonds[0].amount, '30')
403
+ assert.equal(unbonds[1].amount, '70')
404
+ assert.equal(unbonds[0].isWithdrawable, false)
405
+ assert.equal(unbonds[1].isWithdrawable, false)
406
+ assert.isTrue(unbonds[0].shares > 0n)
407
+ assert.isTrue(unbonds[1].shares > 0n)
408
+ })
409
+
410
+ it('withdraws after unbonding period and verifies isWithdrawable becomes true', async () => {
411
+ await approveAndStake({
412
+ delegatorAddress,
413
+ validatorShareAddress,
414
+ amount: AMOUNT,
415
+ staker,
416
+ walletClient,
417
+ publicClient
418
+ })
419
+ const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
420
+ await unstake({
421
+ delegatorAddress,
422
+ validatorShareAddress,
423
+ amount: AMOUNT,
424
+ maximumSharesToBurn: stakeBefore.shares,
425
+ staker,
426
+ walletClient,
427
+ publicClient
428
+ })
429
+
430
+ const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
431
+ const unbondBefore = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
432
+ assert.equal(unbondBefore.isWithdrawable, false)
433
+
434
+ const withdrawalDelay = await getWithdrawalDelay({ publicClient })
435
+ await advanceEpoch({ publicClient, staker, targetEpoch: unbondBefore.withdrawEpoch + withdrawalDelay })
436
+
437
+ const unbondAfter = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
438
+ assert.equal(unbondAfter.isWithdrawable, true)
439
+
440
+ const balanceBefore = await getStakingTokenBalance({ publicClient, address: delegatorAddress })
441
+
442
+ const { tx } = await staker.buildWithdrawTx({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
443
+ await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
444
+
445
+ const balanceAfter = await getStakingTokenBalance({ publicClient, address: delegatorAddress })
446
+ assert.equal(balanceAfter - balanceBefore, parseEther(AMOUNT))
447
+ })
448
+
449
+ it('rejects withdraw for non-existent unbond', async () => {
450
+ await expect(
451
+ staker.buildWithdrawTx({ delegatorAddress, validatorShareAddress, unbondNonce: 999n })
452
+ ).to.be.rejectedWith('No unbond request found for nonce 999')
453
+ })
454
+
455
+ it('rejects withdraw before unbonding period completes (withdrawEpoch + withdrawalDelay)', async () => {
456
+ await approveAndStake({
457
+ delegatorAddress,
458
+ validatorShareAddress,
459
+ amount: AMOUNT,
460
+ staker,
461
+ walletClient,
462
+ publicClient
463
+ })
464
+
465
+ const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
466
+ await unstake({
467
+ delegatorAddress,
468
+ validatorShareAddress,
469
+ amount: AMOUNT,
470
+ maximumSharesToBurn: stakeBefore.shares,
471
+ staker,
472
+ walletClient,
473
+ publicClient
474
+ })
475
+
476
+ const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
477
+ const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
478
+ const withdrawalDelay = await getWithdrawalDelay({ publicClient })
479
+
480
+ await expect(
481
+ staker.buildWithdrawTx({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
482
+ ).to.be.rejectedWith(
483
+ `Unbonding not complete. Current epoch: ${unbond.withdrawEpoch}, Required epoch: ${unbond.withdrawEpoch + withdrawalDelay}`
484
+ )
485
+ })
486
+
487
+ it('rejects claim rewards when none available', async () => {
488
+ await approveAndStake({
489
+ delegatorAddress,
490
+ validatorShareAddress,
491
+ amount: AMOUNT,
492
+ staker,
493
+ walletClient,
494
+ publicClient
495
+ })
496
+
497
+ await expect(staker.buildClaimRewardsTx({ delegatorAddress, validatorShareAddress })).to.be.rejectedWith(
498
+ 'No rewards available to claim'
499
+ )
500
+ })
501
+
502
+ it('rejects compound when no rewards available', async () => {
503
+ await approveAndStake({
504
+ delegatorAddress,
505
+ validatorShareAddress,
506
+ amount: AMOUNT,
507
+ staker,
508
+ walletClient,
509
+ publicClient
510
+ })
511
+
512
+ await expect(staker.buildCompoundTx({ delegatorAddress, validatorShareAddress })).to.be.rejectedWith(
513
+ 'No rewards available to compound'
514
+ )
515
+ })
516
+ })
517
+
518
+ describe('whale delegator operations', () => {
519
+ let whaleWallet: WalletClient
520
+
521
+ beforeEach(async () => {
522
+ const setup = await prepareTests()
523
+ validatorShareAddress = setup.validatorShareAddress
524
+ publicClient = setup.publicClient
525
+ staker = setup.staker
526
+
527
+ await impersonate({ publicClient, address: WHALE_DELEGATOR })
528
+ whaleWallet = createWalletClient({ account: WHALE_DELEGATOR, chain: hardhat, transport: http() })
529
+ })
530
+
531
+ it('claims rewards and verifies POL balance increase', async () => {
532
+ const rewardsBefore = await staker.getLiquidRewards({
533
+ delegatorAddress: WHALE_DELEGATOR,
534
+ validatorShareAddress
535
+ })
536
+ assert.isTrue(parseEther(rewardsBefore) > 0n, 'Whale should have accrued rewards')
537
+
538
+ const balanceBefore = await getStakingTokenBalance({ publicClient, address: WHALE_DELEGATOR })
539
+
540
+ const { tx } = await staker.buildClaimRewardsTx({
541
+ delegatorAddress: WHALE_DELEGATOR,
542
+ validatorShareAddress
543
+ })
544
+ await sendTx({ tx, walletClient: whaleWallet, publicClient, senderAddress: WHALE_DELEGATOR })
545
+
546
+ const rewardsAfter = await staker.getLiquidRewards({
547
+ delegatorAddress: WHALE_DELEGATOR,
548
+ validatorShareAddress
549
+ })
550
+ assert.equal(rewardsAfter, '0')
551
+
552
+ const balanceAfter = await getStakingTokenBalance({ publicClient, address: WHALE_DELEGATOR })
553
+ assert.equal(balanceAfter - balanceBefore, parseEther(rewardsBefore))
554
+ })
555
+
556
+ it('compounds rewards and verifies stake increase', async () => {
557
+ const rewardsBefore = await staker.getLiquidRewards({
558
+ delegatorAddress: WHALE_DELEGATOR,
559
+ validatorShareAddress
560
+ })
561
+ assert.isTrue(parseEther(rewardsBefore) > 0n, 'Whale should have accrued rewards')
562
+
563
+ const stakeBefore = await staker.getStake({
564
+ delegatorAddress: WHALE_DELEGATOR,
565
+ validatorShareAddress
566
+ })
567
+
568
+ const { tx } = await staker.buildCompoundTx({
569
+ delegatorAddress: WHALE_DELEGATOR,
570
+ validatorShareAddress
571
+ })
572
+ await sendTx({ tx, walletClient: whaleWallet, publicClient, senderAddress: WHALE_DELEGATOR })
573
+
574
+ const rewardsAfter = await staker.getLiquidRewards({
575
+ delegatorAddress: WHALE_DELEGATOR,
576
+ validatorShareAddress
577
+ })
578
+ assert.equal(rewardsAfter, '0')
579
+
580
+ const stakeAfter = await staker.getStake({
581
+ delegatorAddress: WHALE_DELEGATOR,
582
+ validatorShareAddress
583
+ })
584
+ assert.equal(parseEther(stakeAfter.balance) - parseEther(stakeBefore.balance), parseEther(rewardsBefore))
585
+ })
586
+ })
587
+ })