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