@factordao/governance 1.0.9 → 1.1.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.
@@ -0,0 +1,682 @@
1
+ import { createPublicClient, http, fallback, keccak256, encodeAbiParameters, parseAbiParameters, toHex, } from 'viem';
2
+ import { ChainId } from '@factordao/tokenlist';
3
+ import { ChainIdToViemChain } from './viem';
4
+ // Storage slot constants for Scale rewards calculation
5
+ const REWARD_MANAGER_SLOT = keccak256(toHex('factor.base.RewardManager.storage'));
6
+ const FACTOR_GAUGE_SLOT = keccak256(toHex('factor.base.gauge.storage'));
7
+ // Minimal ABI for Scale/Boost reward functions
8
+ const REWARDS_ABI = [
9
+ // Scale functions
10
+ {
11
+ name: 'getRewardTokens',
12
+ type: 'function',
13
+ inputs: [],
14
+ outputs: [{ type: 'address[]' }],
15
+ stateMutability: 'view',
16
+ },
17
+ {
18
+ name: 'totalActiveSupply',
19
+ type: 'function',
20
+ inputs: [],
21
+ outputs: [{ type: 'uint256' }],
22
+ stateMutability: 'view',
23
+ },
24
+ {
25
+ name: 'activeBalance',
26
+ type: 'function',
27
+ inputs: [{ name: 'user', type: 'address' }],
28
+ outputs: [{ type: 'uint256' }],
29
+ stateMutability: 'view',
30
+ },
31
+ // Boost functions
32
+ {
33
+ name: 'getAllRewardTokens',
34
+ type: 'function',
35
+ inputs: [],
36
+ outputs: [{ type: 'address[]' }],
37
+ stateMutability: 'view',
38
+ },
39
+ {
40
+ name: 'rewardData',
41
+ type: 'function',
42
+ inputs: [{ name: 'token', type: 'address' }],
43
+ outputs: [
44
+ { name: 'periodFinish', type: 'uint256' },
45
+ { name: 'rewardRate', type: 'uint256' },
46
+ { name: 'lastUpdateTime', type: 'uint256' },
47
+ { name: 'rewardPerTokenStored', type: 'uint256' },
48
+ ],
49
+ stateMutability: 'view',
50
+ },
51
+ {
52
+ name: 'earned',
53
+ type: 'function',
54
+ inputs: [
55
+ { name: 'user', type: 'address' },
56
+ { name: 'token', type: 'address' },
57
+ ],
58
+ outputs: [{ type: 'uint256' }],
59
+ stateMutability: 'view',
60
+ },
61
+ {
62
+ name: 'getRewardForDuration',
63
+ type: 'function',
64
+ inputs: [{ name: 'token', type: 'address' }],
65
+ outputs: [{ type: 'uint256' }],
66
+ stateMutability: 'view',
67
+ },
68
+ ];
69
+ // ERC20 ABI for token metadata
70
+ const ERC20_ABI = [
71
+ {
72
+ name: 'symbol',
73
+ type: 'function',
74
+ inputs: [],
75
+ outputs: [{ type: 'string' }],
76
+ stateMutability: 'view',
77
+ },
78
+ {
79
+ name: 'decimals',
80
+ type: 'function',
81
+ inputs: [],
82
+ outputs: [{ type: 'uint8' }],
83
+ stateMutability: 'view',
84
+ },
85
+ ];
86
+ // Gauge Controller ABI for Scale rewards
87
+ const GAUGE_CONTROLLER_ABI = [
88
+ {
89
+ name: 'rewardData',
90
+ type: 'function',
91
+ inputs: [{ name: 'vault', type: 'address' }],
92
+ outputs: [
93
+ { name: 'fctrPerSec', type: 'uint128' },
94
+ { name: 'accumulatedFctr', type: 'uint128' },
95
+ { name: 'lastUpdated', type: 'uint128' },
96
+ { name: 'incentiveEndsAt', type: 'uint128' },
97
+ ],
98
+ stateMutability: 'view',
99
+ },
100
+ ];
101
+ // Multicall3 ABI for batching getStorageAt calls
102
+ const MULTICALL3_ABI = [
103
+ {
104
+ name: 'aggregate3',
105
+ type: 'function',
106
+ inputs: [
107
+ {
108
+ name: 'calls',
109
+ type: 'tuple[]',
110
+ components: [
111
+ { name: 'target', type: 'address' },
112
+ { name: 'allowFailure', type: 'bool' },
113
+ { name: 'callData', type: 'bytes' },
114
+ ],
115
+ },
116
+ ],
117
+ outputs: [
118
+ {
119
+ name: 'returnData',
120
+ type: 'tuple[]',
121
+ components: [
122
+ { name: 'success', type: 'bool' },
123
+ { name: 'returnData', type: 'bytes' },
124
+ ],
125
+ },
126
+ ],
127
+ stateMutability: 'view',
128
+ },
129
+ ];
130
+ // Multicall3 is deployed at the same address on all chains
131
+ const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
132
+ // Multicall3 getEthBalance can be used to batch reads, but for storage we need a different approach
133
+ // We'll use the standard eth_getStorageAt but batch with Promise.all since viem batches RPC calls
134
+ // Helper to calculate storage slot for mapping(address => value)
135
+ function getMappingSlot(key, baseSlot) {
136
+ return keccak256(encodeAbiParameters(parseAbiParameters('address, bytes32'), [
137
+ key,
138
+ baseSlot,
139
+ ]));
140
+ }
141
+ // Helper to calculate storage slot for nested mapping(address => mapping(address => value))
142
+ function getNestedMappingSlot(outerKey, innerKey, baseSlot) {
143
+ const outerSlot = getMappingSlot(outerKey, baseSlot);
144
+ return getMappingSlot(innerKey, outerSlot);
145
+ }
146
+ // Public RPCs with fallback support
147
+ const PUBLIC_RPCS = {
148
+ [ChainId.ARBITRUM_ONE]: [
149
+ 'https://arb1.arbitrum.io/rpc',
150
+ 'https://arbitrum.llamarpc.com',
151
+ 'https://arbitrum-one-rpc.publicnode.com',
152
+ ],
153
+ [ChainId.BASE]: [
154
+ 'https://base.llamarpc.com',
155
+ 'https://base-rpc.publicnode.com',
156
+ 'https://base.gateway.tenderly.co',
157
+ ],
158
+ [ChainId.OPTIMISM]: [
159
+ 'https://optimism.llamarpc.com',
160
+ 'https://optimism-rpc.publicnode.com',
161
+ ],
162
+ [ChainId.SONIC]: [
163
+ 'https://rpc.soniclabs.com',
164
+ 'https://sonic.drpc.org',
165
+ 'https://sonic-rpc.publicnode.com',
166
+ ],
167
+ };
168
+ // Alchemy RPC URLs
169
+ const ALCHEMY_RPCS = {
170
+ [ChainId.ARBITRUM_ONE]: 'https://arb-mainnet.g.alchemy.com/v2',
171
+ [ChainId.BASE]: 'https://base-mainnet.g.alchemy.com/v2',
172
+ [ChainId.OPTIMISM]: 'https://opt-mainnet.g.alchemy.com/v2',
173
+ [ChainId.SONIC]: 'https://sonic-mainnet.g.alchemy.com/v2',
174
+ };
175
+ export function createClientForChain(chainId, alchemyApiKey) {
176
+ const chain = ChainIdToViemChain(chainId);
177
+ const publicRpcs = PUBLIC_RPCS[chainId] || [];
178
+ // Enable JSON-RPC batching for better performance
179
+ const httpOptions = { batch: true };
180
+ let transports = publicRpcs.map((url) => http(url, httpOptions));
181
+ // If Alchemy key provided, use it as primary
182
+ if (alchemyApiKey && ALCHEMY_RPCS[chainId]) {
183
+ const alchemyUrl = `${ALCHEMY_RPCS[chainId]}/${alchemyApiKey}`;
184
+ transports = [http(alchemyUrl, httpOptions), ...transports];
185
+ }
186
+ return createPublicClient({
187
+ chain,
188
+ transport: transports.length > 1 ? fallback(transports) : transports[0],
189
+ batch: {
190
+ multicall: true,
191
+ },
192
+ });
193
+ }
194
+ // Format bigint with decimals
195
+ function formatWithDecimals(value, decimals) {
196
+ if (value === 0n)
197
+ return '0';
198
+ const divisor = 10n ** BigInt(decimals);
199
+ const whole = value / divisor;
200
+ const remainder = value % divisor;
201
+ if (remainder === 0n) {
202
+ return whole.toString();
203
+ }
204
+ const remainderStr = remainder.toString().padStart(decimals, '0');
205
+ const trimmed = remainderStr.replace(/0+$/, '');
206
+ return `${whole}.${trimmed}`;
207
+ }
208
+ export async function fetchVaultRewardsForChain(client, vaults, userAddress) {
209
+ if (vaults.length === 0) {
210
+ return [];
211
+ }
212
+ // Stage 1: Get reward tokens and total active supply for all vaults
213
+ const stage1Contracts = vaults.flatMap((vault) => [
214
+ {
215
+ address: vault.id,
216
+ abi: REWARDS_ABI,
217
+ functionName: 'getRewardTokens',
218
+ },
219
+ {
220
+ address: vault.id,
221
+ abi: REWARDS_ABI,
222
+ functionName: 'getAllRewardTokens',
223
+ },
224
+ {
225
+ address: vault.id,
226
+ abi: REWARDS_ABI,
227
+ functionName: 'totalActiveSupply',
228
+ },
229
+ ]);
230
+ const stage1Results = await client.multicall({
231
+ contracts: stage1Contracts,
232
+ allowFailure: true,
233
+ });
234
+ // Parse Stage 1 results
235
+ const stage1Data = [];
236
+ for (let i = 0; i < vaults.length; i++) {
237
+ const baseIndex = i * 3;
238
+ const scaleTokensResult = stage1Results[baseIndex];
239
+ const boostTokensResult = stage1Results[baseIndex + 1];
240
+ const totalActiveSupplyResult = stage1Results[baseIndex + 2];
241
+ stage1Data.push({
242
+ vaultAddress: vaults[i].id,
243
+ scaleTokens: scaleTokensResult.status === 'success'
244
+ ? scaleTokensResult.result
245
+ : [],
246
+ boostTokens: boostTokensResult.status === 'success'
247
+ ? boostTokensResult.result
248
+ : [],
249
+ totalActiveSupply: totalActiveSupplyResult.status === 'success'
250
+ ? totalActiveSupplyResult.result
251
+ : 0n,
252
+ });
253
+ }
254
+ // Collect all unique tokens for metadata fetching
255
+ const allTokens = new Set();
256
+ for (const data of stage1Data) {
257
+ data.scaleTokens.forEach((t) => allTokens.add(t));
258
+ data.boostTokens.forEach((t) => allTokens.add(t));
259
+ }
260
+ const uniqueTokens = Array.from(allTokens);
261
+ // Stage 1.5: Fetch token metadata (symbol, decimals)
262
+ const tokenMetadataContracts = uniqueTokens.flatMap((token) => [
263
+ { address: token, abi: ERC20_ABI, functionName: 'symbol' },
264
+ { address: token, abi: ERC20_ABI, functionName: 'decimals' },
265
+ ]);
266
+ let tokenMetadataResults = [];
267
+ if (tokenMetadataContracts.length > 0) {
268
+ tokenMetadataResults = await client.multicall({
269
+ contracts: tokenMetadataContracts,
270
+ allowFailure: true,
271
+ });
272
+ }
273
+ // Build token metadata map
274
+ const tokenMetadata = {};
275
+ for (let i = 0; i < uniqueTokens.length; i++) {
276
+ const tokenKey = uniqueTokens[i].toLowerCase();
277
+ const symbolResult = tokenMetadataResults[i * 2];
278
+ const decimalsResult = tokenMetadataResults[i * 2 + 1];
279
+ tokenMetadata[tokenKey] = {
280
+ symbol: symbolResult.status === 'success'
281
+ ? (symbolResult.result ?? null)
282
+ : null,
283
+ decimals: decimalsResult.status === 'success'
284
+ ? (decimalsResult.result ?? 18)
285
+ : 18,
286
+ };
287
+ }
288
+ // Stage 2: Get reward data and getRewardForDuration for all tokens
289
+ const stage2Contracts = [];
290
+ // Track which tokens belong to which vault and type
291
+ const tokenMapping = [];
292
+ for (let i = 0; i < stage1Data.length; i++) {
293
+ const data = stage1Data[i];
294
+ // Add scale tokens - rewardData + getRewardForDuration
295
+ for (const token of data.scaleTokens) {
296
+ stage2Contracts.push({
297
+ address: data.vaultAddress,
298
+ abi: REWARDS_ABI,
299
+ functionName: 'rewardData',
300
+ args: [token],
301
+ });
302
+ tokenMapping.push({
303
+ vaultIndex: i,
304
+ token,
305
+ type: 'scale',
306
+ callType: 'rewardData',
307
+ });
308
+ stage2Contracts.push({
309
+ address: data.vaultAddress,
310
+ abi: REWARDS_ABI,
311
+ functionName: 'getRewardForDuration',
312
+ args: [token],
313
+ });
314
+ tokenMapping.push({
315
+ vaultIndex: i,
316
+ token,
317
+ type: 'scale',
318
+ callType: 'rewardForDuration',
319
+ });
320
+ }
321
+ // Add boost tokens - rewardData + getRewardForDuration
322
+ for (const token of data.boostTokens) {
323
+ stage2Contracts.push({
324
+ address: data.vaultAddress,
325
+ abi: REWARDS_ABI,
326
+ functionName: 'rewardData',
327
+ args: [token],
328
+ });
329
+ tokenMapping.push({
330
+ vaultIndex: i,
331
+ token,
332
+ type: 'boost',
333
+ callType: 'rewardData',
334
+ });
335
+ stage2Contracts.push({
336
+ address: data.vaultAddress,
337
+ abi: REWARDS_ABI,
338
+ functionName: 'getRewardForDuration',
339
+ args: [token],
340
+ });
341
+ tokenMapping.push({
342
+ vaultIndex: i,
343
+ token,
344
+ type: 'boost',
345
+ callType: 'rewardForDuration',
346
+ });
347
+ }
348
+ }
349
+ let stage2Results = [];
350
+ if (stage2Contracts.length > 0) {
351
+ stage2Results = await client.multicall({
352
+ contracts: stage2Contracts,
353
+ allowFailure: true,
354
+ });
355
+ }
356
+ // Parse Stage 2 results and build reward data maps
357
+ const vaultRewardData = stage1Data.map(() => ({
358
+ scaleRewardsData: {},
359
+ boostRewardsData: {},
360
+ }));
361
+ for (let i = 0; i < tokenMapping.length; i++) {
362
+ const mapping = tokenMapping[i];
363
+ const tokenKey = mapping.token.toLowerCase();
364
+ const targetMap = mapping.type === 'scale'
365
+ ? vaultRewardData[mapping.vaultIndex].scaleRewardsData
366
+ : vaultRewardData[mapping.vaultIndex].boostRewardsData;
367
+ // Initialize if not exists
368
+ if (!targetMap[tokenKey]) {
369
+ targetMap[tokenKey] = {
370
+ periodFinish: 0n,
371
+ rewardRate: 0n,
372
+ lastUpdateTime: 0n,
373
+ rewardPerTokenStored: 0n,
374
+ rewardForDuration: 0n,
375
+ };
376
+ }
377
+ const result = stage2Results[i];
378
+ if (result.status === 'success' && result.result !== undefined) {
379
+ if (mapping.callType === 'rewardData') {
380
+ const [periodFinish, rewardRate, lastUpdateTime, rewardPerTokenStored] = result.result;
381
+ targetMap[tokenKey].periodFinish = periodFinish;
382
+ targetMap[tokenKey].rewardRate = rewardRate;
383
+ targetMap[tokenKey].lastUpdateTime = lastUpdateTime;
384
+ targetMap[tokenKey].rewardPerTokenStored = rewardPerTokenStored;
385
+ }
386
+ else {
387
+ targetMap[tokenKey].rewardForDuration = result.result;
388
+ }
389
+ }
390
+ }
391
+ // Stage 3 (Optional): User data including Scale pending rewards
392
+ let userDataMap = {};
393
+ if (userAddress) {
394
+ // Stage 3a: Get basic user data (activeBalance for Scale, earned for Boost)
395
+ const stage3Contracts = [];
396
+ const userDataMapping = [];
397
+ for (let i = 0; i < stage1Data.length; i++) {
398
+ const data = stage1Data[i];
399
+ // Active balance for Scale
400
+ stage3Contracts.push({
401
+ address: data.vaultAddress,
402
+ abi: REWARDS_ABI,
403
+ functionName: 'activeBalance',
404
+ args: [userAddress],
405
+ });
406
+ userDataMapping.push({ vaultIndex: i, type: 'activeBalance' });
407
+ // Earned for each boost token
408
+ for (const token of data.boostTokens) {
409
+ stage3Contracts.push({
410
+ address: data.vaultAddress,
411
+ abi: REWARDS_ABI,
412
+ functionName: 'earned',
413
+ args: [userAddress, token],
414
+ });
415
+ userDataMapping.push({ vaultIndex: i, type: 'earned', token });
416
+ }
417
+ }
418
+ // Initialize user data map
419
+ for (let i = 0; i < stage1Data.length; i++) {
420
+ userDataMap[i] = {
421
+ activeBalance: 0n,
422
+ scalePendingByToken: {},
423
+ earnedByToken: {},
424
+ };
425
+ }
426
+ if (stage3Contracts.length > 0) {
427
+ const stage3Results = await client.multicall({
428
+ contracts: stage3Contracts,
429
+ allowFailure: true,
430
+ });
431
+ // Parse Stage 3 results
432
+ for (let i = 0; i < userDataMapping.length; i++) {
433
+ const mapping = userDataMapping[i];
434
+ const result = stage3Results[i];
435
+ if (result.status === 'success') {
436
+ if (mapping.type === 'activeBalance') {
437
+ userDataMap[mapping.vaultIndex].activeBalance =
438
+ result.result;
439
+ }
440
+ else if (mapping.type === 'earned' && mapping.token) {
441
+ userDataMap[mapping.vaultIndex].earnedByToken[mapping.token.toLowerCase()] = result.result;
442
+ }
443
+ }
444
+ }
445
+ }
446
+ // Stage 3b: Calculate Scale pending rewards via storage slots
447
+ // JSON-RPC batching is enabled in the client, so Promise.all will batch automatically
448
+ // First, read gauge controller addresses from vault storage
449
+ const gaugeControllerSlot = toHex(BigInt(FACTOR_GAUGE_SLOT) + 2n, {
450
+ size: 32,
451
+ });
452
+ const gaugeControllerResults = await Promise.all(stage1Data.map((data) => client.getStorageAt({
453
+ address: data.vaultAddress,
454
+ slot: gaugeControllerSlot,
455
+ })));
456
+ const gaugeControllers = gaugeControllerResults.map((result) => {
457
+ if (!result ||
458
+ result ===
459
+ '0x0000000000000000000000000000000000000000000000000000000000000000') {
460
+ return null;
461
+ }
462
+ return ('0x' + result.slice(-40));
463
+ });
464
+ // Get gauge controller reward data for all vaults
465
+ const gaugeControllerContracts = stage1Data
466
+ .map((data, i) => {
467
+ const gc = gaugeControllers[i];
468
+ if (!gc)
469
+ return null;
470
+ return {
471
+ address: gc,
472
+ abi: GAUGE_CONTROLLER_ABI,
473
+ functionName: 'rewardData',
474
+ args: [data.vaultAddress],
475
+ };
476
+ })
477
+ .filter((c) => c !== null);
478
+ const gcIndexMap = [];
479
+ stage1Data.forEach((_, i) => {
480
+ if (gaugeControllers[i]) {
481
+ gcIndexMap.push(i);
482
+ }
483
+ });
484
+ let gcRewardDataResults = [];
485
+ if (gaugeControllerContracts.length > 0) {
486
+ gcRewardDataResults = await client.multicall({
487
+ contracts: gaugeControllerContracts,
488
+ allowFailure: true,
489
+ });
490
+ }
491
+ const gcRewardData = {};
492
+ for (let i = 0; i < gcRewardDataResults.length; i++) {
493
+ const vaultIndex = gcIndexMap[i];
494
+ const result = gcRewardDataResults[i];
495
+ if (result.status === 'success' && result.result) {
496
+ const [fctrPerSec, accumulatedFctr, lastUpdated, incentiveEndsAt] = result.result;
497
+ gcRewardData[vaultIndex] = {
498
+ fctrPerSec,
499
+ accumulatedFctr,
500
+ lastUpdated,
501
+ incentiveEndsAt,
502
+ };
503
+ }
504
+ }
505
+ // Read storage slots for reward state and user reward
506
+ const storageReadPromises = [];
507
+ for (let i = 0; i < stage1Data.length; i++) {
508
+ const data = stage1Data[i];
509
+ for (const token of data.scaleTokens) {
510
+ const rewardStateBaseSlot = toHex(BigInt(REWARD_MANAGER_SLOT) + 2n, {
511
+ size: 32,
512
+ });
513
+ const rewardStateSlot = getMappingSlot(token, rewardStateBaseSlot);
514
+ const userRewardBaseSlot = toHex(BigInt(REWARD_MANAGER_SLOT) + 1n, {
515
+ size: 32,
516
+ });
517
+ const userRewardSlot = getNestedMappingSlot(token, userAddress, userRewardBaseSlot);
518
+ storageReadPromises.push(Promise.all([
519
+ client.getStorageAt({
520
+ address: data.vaultAddress,
521
+ slot: rewardStateSlot,
522
+ }),
523
+ client.getStorageAt({
524
+ address: data.vaultAddress,
525
+ slot: userRewardSlot,
526
+ }),
527
+ ]).then(([rewardStateData, userRewardData]) => ({
528
+ vaultIndex: i,
529
+ token,
530
+ rewardStateData,
531
+ userRewardData,
532
+ })));
533
+ }
534
+ }
535
+ const storageResults = await Promise.all(storageReadPromises);
536
+ // Calculate Scale pending rewards for each vault/token
537
+ const WAD = 10n ** 18n;
538
+ const INITIAL_REWARD_INDEX = 1n;
539
+ const currentTime = BigInt(Math.floor(Date.now() / 1000));
540
+ for (const storageResult of storageResults) {
541
+ const { vaultIndex, token, rewardStateData, userRewardData } = storageResult;
542
+ const tokenKey = token.toLowerCase();
543
+ const gcData = gcRewardData[vaultIndex];
544
+ const totalActiveSupply = stage1Data[vaultIndex].totalActiveSupply;
545
+ const userActiveBalance = userDataMap[vaultIndex].activeBalance;
546
+ if (!gcData) {
547
+ userDataMap[vaultIndex].scalePendingByToken[tokenKey] = 0n;
548
+ continue;
549
+ }
550
+ // Parse packed structs
551
+ const rewardStateValue = BigInt(rewardStateData || '0x0');
552
+ const globalIndex = rewardStateValue & ((1n << 128n) - 1n);
553
+ const userRewardValue = BigInt(userRewardData || '0x0');
554
+ const userIndex = userRewardValue & ((1n << 128n) - 1n);
555
+ const userAccrued = userRewardValue >> 128n;
556
+ // Calculate new accumulated from gauge controller
557
+ const effectiveTime = currentTime < gcData.incentiveEndsAt
558
+ ? currentTime
559
+ : gcData.incentiveEndsAt;
560
+ let newAccumulatedFromController = gcData.accumulatedFctr;
561
+ if (effectiveTime > gcData.lastUpdated) {
562
+ newAccumulatedFromController +=
563
+ gcData.fctrPerSec * (effectiveTime - gcData.lastUpdated);
564
+ }
565
+ // Calculate new global index
566
+ let index = globalIndex;
567
+ if (index === 0n)
568
+ index = INITIAL_REWARD_INDEX;
569
+ if (totalActiveSupply > 0n) {
570
+ index += (newAccumulatedFromController * WAD) / totalActiveSupply;
571
+ }
572
+ // Calculate user reward
573
+ let effectiveUserIndex = userIndex;
574
+ if (effectiveUserIndex === 0n)
575
+ effectiveUserIndex = INITIAL_REWARD_INDEX;
576
+ const deltaIndex = index > effectiveUserIndex ? index - effectiveUserIndex : 0n;
577
+ const rewardDelta = (userActiveBalance * deltaIndex) / WAD;
578
+ const pendingRewards = userAccrued + rewardDelta;
579
+ userDataMap[vaultIndex].scalePendingByToken[tokenKey] = pendingRewards;
580
+ }
581
+ }
582
+ // Build final results with formatted values
583
+ const results = [];
584
+ for (let i = 0; i < stage1Data.length; i++) {
585
+ const data = stage1Data[i];
586
+ const rawRewardData = vaultRewardData[i];
587
+ // Build Scale reward tokens with metadata
588
+ const scaleTokenInfos = data.scaleTokens.map((t) => {
589
+ const tokenKey = t.toLowerCase();
590
+ const meta = tokenMetadata[tokenKey] || { symbol: null, decimals: 18 };
591
+ return {
592
+ address: tokenKey,
593
+ symbol: meta.symbol,
594
+ decimals: meta.decimals,
595
+ };
596
+ });
597
+ // Build Boost reward tokens with metadata
598
+ const boostTokenInfos = data.boostTokens.map((t) => {
599
+ const tokenKey = t.toLowerCase();
600
+ const meta = tokenMetadata[tokenKey] || { symbol: null, decimals: 18 };
601
+ return {
602
+ address: tokenKey,
603
+ symbol: meta.symbol,
604
+ decimals: meta.decimals,
605
+ };
606
+ });
607
+ // Format Scale rewards data
608
+ const scaleRewardsData = {};
609
+ for (const [tokenKey, raw] of Object.entries(rawRewardData.scaleRewardsData)) {
610
+ const decimals = tokenMetadata[tokenKey]?.decimals ?? 18;
611
+ scaleRewardsData[tokenKey] = {
612
+ periodFinish: raw.periodFinish,
613
+ periodFinishDate: raw.periodFinish > 0n
614
+ ? new Date(Number(raw.periodFinish) * 1000)
615
+ : null,
616
+ rewardRate: raw.rewardRate,
617
+ rewardRateFmt: formatWithDecimals(raw.rewardRate, decimals),
618
+ lastUpdateTime: raw.lastUpdateTime,
619
+ rewardPerTokenStored: raw.rewardPerTokenStored,
620
+ rewardForDuration: raw.rewardForDuration,
621
+ rewardForDurationFmt: formatWithDecimals(raw.rewardForDuration, decimals),
622
+ };
623
+ }
624
+ // Format Boost rewards data
625
+ const boostRewardsData = {};
626
+ for (const [tokenKey, raw] of Object.entries(rawRewardData.boostRewardsData)) {
627
+ const decimals = tokenMetadata[tokenKey]?.decimals ?? 18;
628
+ boostRewardsData[tokenKey] = {
629
+ periodFinish: raw.periodFinish,
630
+ periodFinishDate: raw.periodFinish > 0n
631
+ ? new Date(Number(raw.periodFinish) * 1000)
632
+ : null,
633
+ rewardRate: raw.rewardRate,
634
+ rewardRateFmt: formatWithDecimals(raw.rewardRate, decimals),
635
+ lastUpdateTime: raw.lastUpdateTime,
636
+ rewardPerTokenStored: raw.rewardPerTokenStored,
637
+ rewardForDuration: raw.rewardForDuration,
638
+ rewardForDurationFmt: formatWithDecimals(raw.rewardForDuration, decimals),
639
+ };
640
+ }
641
+ const result = {
642
+ vaultAddress: data.vaultAddress,
643
+ chainId: vaults[i].chainId,
644
+ scale: {
645
+ rewardTokens: scaleTokenInfos,
646
+ totalActiveSupply: data.totalActiveSupply,
647
+ totalActiveSupplyFmt: formatWithDecimals(data.totalActiveSupply, 18), // vault shares are 18 decimals
648
+ rewardsData: scaleRewardsData,
649
+ },
650
+ boost: {
651
+ rewardTokens: boostTokenInfos,
652
+ rewardsData: boostRewardsData,
653
+ },
654
+ };
655
+ if (userAddress && userDataMap[i]) {
656
+ const userData = userDataMap[i];
657
+ // Format Scale pending rewards
658
+ const scalePendingByTokenFmt = {};
659
+ for (const [tokenKey, pending] of Object.entries(userData.scalePendingByToken)) {
660
+ const decimals = tokenMetadata[tokenKey]?.decimals ?? 18;
661
+ scalePendingByTokenFmt[tokenKey] = formatWithDecimals(pending, decimals);
662
+ }
663
+ // Format Boost earned rewards
664
+ const earnedByTokenFmt = {};
665
+ for (const [tokenKey, earned] of Object.entries(userData.earnedByToken)) {
666
+ const decimals = tokenMetadata[tokenKey]?.decimals ?? 18;
667
+ earnedByTokenFmt[tokenKey] = formatWithDecimals(earned, decimals);
668
+ }
669
+ result.user = {
670
+ activeBalance: userData.activeBalance,
671
+ activeBalanceFmt: formatWithDecimals(userData.activeBalance, 18), // vault shares are 18 decimals
672
+ scalePendingByToken: userData.scalePendingByToken,
673
+ scalePendingByTokenFmt,
674
+ earnedByToken: userData.earnedByToken,
675
+ earnedByTokenFmt,
676
+ };
677
+ }
678
+ results.push(result);
679
+ }
680
+ return results;
681
+ }
682
+ //# sourceMappingURL=rewards-multicall.js.map