@moonwell-fi/moonwell-sdk 0.12.2 → 0.13.1
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/CHANGELOG.md +22 -0
- package/_cjs/actions/core/markets/common.js +11 -4
- package/_cjs/actions/core/markets/common.js.map +1 -1
- package/_cjs/actions/core/user-rewards/common.js +6 -5
- package/_cjs/actions/core/user-rewards/common.js.map +1 -1
- package/_cjs/actions/governance/getDelegates.js.map +1 -1
- package/_cjs/actions/governance/getStakingInfo.js +113 -31
- package/_cjs/actions/governance/getStakingInfo.js.map +1 -1
- package/_cjs/actions/governance/getUserStakingInfo.js +106 -18
- package/_cjs/actions/governance/getUserStakingInfo.js.map +1 -1
- package/_cjs/actions/governance/getUserVotingPowers.js +21 -6
- package/_cjs/actions/governance/getUserVotingPowers.js.map +1 -1
- package/_cjs/actions/governance/getWellPrice.js +26 -0
- package/_cjs/actions/governance/getWellPrice.js.map +1 -0
- package/_cjs/actions/morpho/user-rewards/common.js +87 -12
- package/_cjs/actions/morpho/user-rewards/common.js.map +1 -1
- package/_cjs/actions/morpho/user-rewards/getMorphoUserRewards.js +46 -10
- package/_cjs/actions/morpho/user-rewards/getMorphoUserRewards.js.map +1 -1
- package/_cjs/actions/morpho/vaults/common.js +19 -6
- package/_cjs/actions/morpho/vaults/common.js.map +1 -1
- package/_cjs/environments/definitions/ethereum/contracts.js +12 -0
- package/_cjs/environments/definitions/ethereum/contracts.js.map +1 -0
- package/_cjs/environments/definitions/ethereum/custom.js +18 -0
- package/_cjs/environments/definitions/ethereum/custom.js.map +1 -0
- package/_cjs/environments/definitions/ethereum/environment.js +9 -5
- package/_cjs/environments/definitions/ethereum/environment.js.map +1 -1
- package/_cjs/environments/definitions/ethereum/tokens.js +12 -0
- package/_cjs/environments/definitions/ethereum/tokens.js.map +1 -1
- package/_cjs/environments/definitions/governance.js +1 -1
- package/_cjs/environments/definitions/governance.js.map +1 -1
- package/_cjs/errors/version.js +1 -1
- package/_cjs/index.js +5 -1
- package/_cjs/index.js.map +1 -1
- package/_esm/actions/core/markets/common.js +11 -4
- package/_esm/actions/core/markets/common.js.map +1 -1
- package/_esm/actions/core/user-rewards/common.js +6 -5
- package/_esm/actions/core/user-rewards/common.js.map +1 -1
- package/_esm/actions/governance/getDelegates.js +2 -0
- package/_esm/actions/governance/getDelegates.js.map +1 -1
- package/_esm/actions/governance/getStakingInfo.js +136 -32
- package/_esm/actions/governance/getStakingInfo.js.map +1 -1
- package/_esm/actions/governance/getUserStakingInfo.js +120 -19
- package/_esm/actions/governance/getUserStakingInfo.js.map +1 -1
- package/_esm/actions/governance/getUserVotingPowers.js +24 -6
- package/_esm/actions/governance/getUserVotingPowers.js.map +1 -1
- package/_esm/actions/governance/getWellPrice.js +50 -0
- package/_esm/actions/governance/getWellPrice.js.map +1 -0
- package/_esm/actions/morpho/user-rewards/common.js +93 -12
- package/_esm/actions/morpho/user-rewards/common.js.map +1 -1
- package/_esm/actions/morpho/user-rewards/getMorphoUserRewards.js +51 -11
- package/_esm/actions/morpho/user-rewards/getMorphoUserRewards.js.map +1 -1
- package/_esm/actions/morpho/vaults/common.js +19 -6
- package/_esm/actions/morpho/vaults/common.js.map +1 -1
- package/_esm/environments/definitions/ethereum/contracts.js +10 -0
- package/_esm/environments/definitions/ethereum/contracts.js.map +1 -0
- package/_esm/environments/definitions/ethereum/custom.js +15 -0
- package/_esm/environments/definitions/ethereum/custom.js.map +1 -0
- package/_esm/environments/definitions/ethereum/environment.js +11 -5
- package/_esm/environments/definitions/ethereum/environment.js.map +1 -1
- package/_esm/environments/definitions/ethereum/tokens.js +12 -0
- package/_esm/environments/definitions/ethereum/tokens.js.map +1 -1
- package/_esm/environments/definitions/governance.js +2 -2
- package/_esm/environments/definitions/governance.js.map +1 -1
- package/_esm/errors/version.js +1 -1
- package/_esm/index.js +2 -0
- package/_esm/index.js.map +1 -1
- package/_types/actions/core/markets/common.d.ts.map +1 -1
- package/_types/actions/core/user-rewards/common.d.ts.map +1 -1
- package/_types/actions/governance/getDelegates.d.ts.map +1 -1
- package/_types/actions/governance/getStakingInfo.d.ts +1 -1
- package/_types/actions/governance/getStakingInfo.d.ts.map +1 -1
- package/_types/actions/governance/getUserStakingInfo.d.ts.map +1 -1
- package/_types/actions/governance/getUserVotingPowers.d.ts.map +1 -1
- package/_types/actions/governance/getWellPrice.d.ts +29 -0
- package/_types/actions/governance/getWellPrice.d.ts.map +1 -0
- package/_types/actions/morpho/user-rewards/common.d.ts +23 -0
- package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
- package/_types/actions/morpho/user-rewards/getMorphoUserRewards.d.ts +22 -0
- package/_types/actions/morpho/user-rewards/getMorphoUserRewards.d.ts.map +1 -1
- package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
- package/_types/client/createMoonwellClient.d.ts +62 -2
- package/_types/client/createMoonwellClient.d.ts.map +1 -1
- package/_types/environments/definitions/ethereum/contracts.d.ts +4 -0
- package/_types/environments/definitions/ethereum/contracts.d.ts.map +1 -0
- package/_types/environments/definitions/ethereum/custom.d.ts +18 -0
- package/_types/environments/definitions/ethereum/custom.d.ts.map +1 -0
- package/_types/environments/definitions/ethereum/environment.d.ts +52 -2
- package/_types/environments/definitions/ethereum/environment.d.ts.map +1 -1
- package/_types/environments/definitions/ethereum/tokens.d.ts +12 -0
- package/_types/environments/definitions/ethereum/tokens.d.ts.map +1 -1
- package/_types/environments/definitions/governance.d.ts.map +1 -1
- package/_types/environments/index.d.ts +34 -4
- package/_types/environments/index.d.ts.map +1 -1
- package/_types/errors/version.d.ts +1 -1
- package/_types/index.d.ts +2 -0
- package/_types/index.d.ts.map +1 -1
- package/actions/core/markets/common.ts +11 -6
- package/actions/core/user-rewards/common.ts +6 -5
- package/actions/governance/getDelegates.ts +2 -0
- package/actions/governance/getStakingInfo.ts +195 -87
- package/actions/governance/getUserStakingInfo.ts +168 -54
- package/actions/governance/getUserVotingPowers.ts +30 -15
- package/actions/governance/getWellPrice.ts +66 -0
- package/actions/morpho/user-rewards/common.ts +91 -14
- package/actions/morpho/user-rewards/getMorphoUserRewards.ts +77 -12
- package/actions/morpho/vaults/common.ts +19 -12
- package/environments/definitions/ethereum/contracts.ts +10 -0
- package/environments/definitions/ethereum/custom.ts +15 -0
- package/environments/definitions/ethereum/environment.ts +15 -6
- package/environments/definitions/ethereum/tokens.ts +12 -0
- package/environments/definitions/governance.ts +2 -2
- package/errors/version.ts +1 -1
- package/index.ts +3 -0
- package/package.json +1 -1
|
@@ -7,14 +7,10 @@ import {
|
|
|
7
7
|
getEnvironmentsFromArgs,
|
|
8
8
|
} from "../../common/index.js";
|
|
9
9
|
import type { NetworkParameterType } from "../../common/types.js";
|
|
10
|
-
import {
|
|
11
|
-
type Chain,
|
|
12
|
-
type Environment,
|
|
13
|
-
type TokensType,
|
|
14
|
-
publicEnvironments,
|
|
15
|
-
} from "../../environments/index.js";
|
|
10
|
+
import type { Chain, Environment } from "../../environments/index.js";
|
|
16
11
|
import type { StakingInfo } from "../../types/staking.js";
|
|
17
12
|
import { getMerklStakingApr } from "./common.js";
|
|
13
|
+
import { getGovernanceTokenPriceFor } from "./getWellPrice.js";
|
|
18
14
|
|
|
19
15
|
export type GetStakingInfoParameters<
|
|
20
16
|
environments,
|
|
@@ -23,6 +19,108 @@ export type GetStakingInfoParameters<
|
|
|
23
19
|
|
|
24
20
|
export type GetStakingInfoReturnType = Promise<StakingInfo[]>;
|
|
25
21
|
|
|
22
|
+
type StakingInfoStruct = {
|
|
23
|
+
cooldown: bigint;
|
|
24
|
+
distributionEnd: bigint;
|
|
25
|
+
emissionPerSecond: bigint;
|
|
26
|
+
totalSupply: bigint;
|
|
27
|
+
unstakeWindow: bigint;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const isStakingInfoStruct = (value: unknown): value is StakingInfoStruct => {
|
|
31
|
+
if (typeof value !== "object" || value === null) return false;
|
|
32
|
+
const v = value as Record<string, unknown>;
|
|
33
|
+
return (
|
|
34
|
+
typeof v.cooldown === "bigint" &&
|
|
35
|
+
typeof v.distributionEnd === "bigint" &&
|
|
36
|
+
typeof v.emissionPerSecond === "bigint" &&
|
|
37
|
+
typeof v.totalSupply === "bigint" &&
|
|
38
|
+
typeof v.unstakeWindow === "bigint"
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reads the staking fields directly from the stkWELL token contract when the
|
|
44
|
+
* core views' getStakingInfo() is unavailable or returns zeroed data
|
|
45
|
+
* (e.g. reverts on Moonbeam). Reads each field independently so a single
|
|
46
|
+
* transient RPC failure doesn't erase the whole fallback.
|
|
47
|
+
*
|
|
48
|
+
* The `assets` mapping is keyed by the stkWELL contract's own address — that's
|
|
49
|
+
* the Aave-fork convention (`address(this)` in StakedAave._initialize), not
|
|
50
|
+
* the underlying staked token. Verified on Moonbeam: assets(stkWELL) returns
|
|
51
|
+
* non-zero emissionPerSecond; assets(WELL) returns zero.
|
|
52
|
+
*/
|
|
53
|
+
async function getStakingInfoFromStkWell(
|
|
54
|
+
environment: Environment,
|
|
55
|
+
): Promise<StakingInfoStruct | undefined> {
|
|
56
|
+
const stakingToken = environment.contracts.stakingToken;
|
|
57
|
+
const stakingTokenKey = environment.config.contracts.stakingToken;
|
|
58
|
+
const tokens = environment.config.tokens as Record<
|
|
59
|
+
string,
|
|
60
|
+
{ address: `0x${string}` } | undefined
|
|
61
|
+
>;
|
|
62
|
+
const stakingTokenAddress = stakingTokenKey
|
|
63
|
+
? tokens[stakingTokenKey]?.address
|
|
64
|
+
: undefined;
|
|
65
|
+
if (!stakingToken || !stakingTokenAddress) {
|
|
66
|
+
environment.onError?.(
|
|
67
|
+
new Error("getStakingInfoFromStkWell: missing stkWELL config"),
|
|
68
|
+
{ source: "staking-fallback", chainId: environment.chainId },
|
|
69
|
+
);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [
|
|
74
|
+
cooldownR,
|
|
75
|
+
unstakeWindowR,
|
|
76
|
+
distributionEndR,
|
|
77
|
+
totalSupplyR,
|
|
78
|
+
assetDataR,
|
|
79
|
+
] = await Promise.allSettled([
|
|
80
|
+
stakingToken.read.COOLDOWN_SECONDS(),
|
|
81
|
+
stakingToken.read.UNSTAKE_WINDOW(),
|
|
82
|
+
stakingToken.read.DISTRIBUTION_END(),
|
|
83
|
+
stakingToken.read.totalSupply(),
|
|
84
|
+
stakingToken.read.assets([stakingTokenAddress]),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Surface every rejection so operators don't have to guess which read failed.
|
|
88
|
+
for (const r of [
|
|
89
|
+
cooldownR,
|
|
90
|
+
unstakeWindowR,
|
|
91
|
+
distributionEndR,
|
|
92
|
+
totalSupplyR,
|
|
93
|
+
assetDataR,
|
|
94
|
+
]) {
|
|
95
|
+
if (r.status === "rejected") {
|
|
96
|
+
environment.onError?.(r.reason, {
|
|
97
|
+
source: "staking-fallback",
|
|
98
|
+
chainId: environment.chainId,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// totalSupply is load-bearing for APR; if its read failed (vs. legitimately
|
|
104
|
+
// returning 0n on an empty new chain) we can't produce a sensible struct.
|
|
105
|
+
if (totalSupplyR.status === "rejected") return undefined;
|
|
106
|
+
|
|
107
|
+
const assetData =
|
|
108
|
+
assetDataR.status === "fulfilled" ? assetDataR.value : undefined;
|
|
109
|
+
// viem returns multi-output reads as a tuple (Readonly<[bigint, bigint, bigint]>)
|
|
110
|
+
// even when the ABI names the outputs.
|
|
111
|
+
const emissionPerSecond = assetData ? assetData[0] : 0n;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
cooldown: cooldownR.status === "fulfilled" ? cooldownR.value : 0n,
|
|
115
|
+
unstakeWindow:
|
|
116
|
+
unstakeWindowR.status === "fulfilled" ? unstakeWindowR.value : 0n,
|
|
117
|
+
distributionEnd:
|
|
118
|
+
distributionEndR.status === "fulfilled" ? distributionEndR.value : 0n,
|
|
119
|
+
totalSupply: totalSupplyR.value,
|
|
120
|
+
emissionPerSecond,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
26
124
|
export async function getStakingInfo<
|
|
27
125
|
environments,
|
|
28
126
|
Network extends Chain | undefined,
|
|
@@ -36,85 +134,104 @@ export async function getStakingInfo<
|
|
|
36
134
|
(env) => env.config.contracts.stakingToken,
|
|
37
135
|
);
|
|
38
136
|
|
|
137
|
+
const baseEnvironment = (
|
|
138
|
+
client.environments as { base?: Environment } | undefined
|
|
139
|
+
)?.base;
|
|
140
|
+
|
|
39
141
|
const envStakingInfoSettlements = await Promise.allSettled(
|
|
40
142
|
envsWithStaking.map(async (environment) => {
|
|
41
|
-
const homeEnvironment =
|
|
42
|
-
(Object.values(publicEnvironments) as Environment[]).find((e) =>
|
|
43
|
-
e.custom?.governance?.chainIds?.includes(environment.chainId),
|
|
44
|
-
) || environment;
|
|
45
|
-
|
|
46
143
|
const isBase = environment.chainId === base.id;
|
|
47
144
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
environment.contracts.views?.read.getStakingInfo({
|
|
145
|
+
const [viewsStakingResult, historicalStakingResult, priceResult] =
|
|
146
|
+
await Promise.allSettled([
|
|
147
|
+
environment.contracts.views?.read.getStakingInfo(),
|
|
148
|
+
isBase
|
|
149
|
+
? environment.contracts.views?.read.getStakingInfo({
|
|
54
150
|
blockNumber: BigInt(34149943),
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
151
|
+
})
|
|
152
|
+
: Promise.resolve(undefined),
|
|
153
|
+
getGovernanceTokenPriceFor(environment, baseEnvironment),
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
const viewsStaking =
|
|
157
|
+
viewsStakingResult.status === "fulfilled"
|
|
158
|
+
? viewsStakingResult.value
|
|
159
|
+
: undefined;
|
|
160
|
+
|
|
161
|
+
// Fall back to direct stkWELL reads when the views call rejected (the
|
|
162
|
+
// known Moonbeam failure mode) or returned a fully-zeroed struct
|
|
163
|
+
// (suspicious — a real deployment always has cooldown and unstake
|
|
164
|
+
// window configured > 0). A new chain with zero stakers but real
|
|
165
|
+
// schedule constants still goes through views.
|
|
166
|
+
const allZeroed =
|
|
167
|
+
isStakingInfoStruct(viewsStaking) &&
|
|
168
|
+
viewsStaking.cooldown === 0n &&
|
|
169
|
+
viewsStaking.unstakeWindow === 0n &&
|
|
170
|
+
viewsStaking.totalSupply === 0n &&
|
|
171
|
+
viewsStaking.emissionPerSecond === 0n;
|
|
172
|
+
const viewsValid = isStakingInfoStruct(viewsStaking) && !allZeroed;
|
|
173
|
+
const stakingInfo: StakingInfoStruct | undefined = viewsValid
|
|
174
|
+
? viewsStaking
|
|
175
|
+
: await getStakingInfoFromStkWell(environment);
|
|
176
|
+
|
|
177
|
+
const historicalStaking =
|
|
178
|
+
historicalStakingResult.status === "fulfilled"
|
|
179
|
+
? historicalStakingResult.value
|
|
180
|
+
: undefined;
|
|
181
|
+
|
|
182
|
+
const price = priceResult.status === "fulfilled" ? priceResult.value : 0n;
|
|
183
|
+
|
|
184
|
+
if (priceResult.status === "rejected") {
|
|
185
|
+
environment.onError?.(priceResult.reason, {
|
|
186
|
+
source: "governance-token-price",
|
|
187
|
+
chainId: environment.chainId,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { stakingInfo, historicalStaking, price };
|
|
63
192
|
}),
|
|
64
193
|
);
|
|
65
194
|
|
|
66
|
-
const envStakingInfo = envStakingInfoSettlements
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
s as PromiseFulfilledResult<
|
|
72
|
-
(
|
|
73
|
-
| bigint
|
|
74
|
-
| {
|
|
75
|
-
cooldown: bigint;
|
|
76
|
-
unstakeWindow: bigint;
|
|
77
|
-
distributionEnd: bigint;
|
|
78
|
-
totalSupply: bigint;
|
|
79
|
-
emissionPerSecond: bigint;
|
|
80
|
-
lastUpdateTimestamp: bigint;
|
|
81
|
-
index: bigint;
|
|
82
|
-
}
|
|
83
|
-
| undefined
|
|
84
|
-
)[]
|
|
85
|
-
>
|
|
86
|
-
).value,
|
|
87
|
-
)
|
|
88
|
-
.filter((val) => val !== undefined);
|
|
195
|
+
const envStakingInfo = envStakingInfoSettlements.map((s) =>
|
|
196
|
+
s.status === "fulfilled"
|
|
197
|
+
? s.value
|
|
198
|
+
: { stakingInfo: undefined, historicalStaking: undefined, price: 0n },
|
|
199
|
+
);
|
|
89
200
|
|
|
90
201
|
const baseEnv = envsWithStaking.find((env) => env.chainId === base.id);
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
202
|
+
const baseStakingTokenKey = baseEnv?.config.contracts.stakingToken;
|
|
203
|
+
const baseTokens = baseEnv?.config.tokens as
|
|
204
|
+
| Record<string, { address: `0x${string}` } | undefined>
|
|
205
|
+
| undefined;
|
|
206
|
+
const baseStkTokenAddress = baseStakingTokenKey
|
|
207
|
+
? baseTokens?.[baseStakingTokenKey]?.address
|
|
208
|
+
: undefined;
|
|
209
|
+
const baseStakingApr = baseStkTokenAddress
|
|
210
|
+
? await getMerklStakingApr(baseStkTokenAddress)
|
|
211
|
+
: 0;
|
|
98
212
|
|
|
99
213
|
const result = envsWithStaking.flatMap((curr, index) => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
214
|
+
const govKey = curr.config.contracts.governanceToken;
|
|
215
|
+
const stkKey = curr.config.contracts.stakingToken;
|
|
216
|
+
const currTokens = curr.config.tokens as Record<
|
|
217
|
+
string,
|
|
218
|
+
{ address: `0x${string}`; decimals: number; name: string; symbol: string }
|
|
219
|
+
>;
|
|
220
|
+
if (!govKey || !stkKey) return [];
|
|
221
|
+
const token = currTokens[govKey];
|
|
222
|
+
const stakingToken = currTokens[stkKey];
|
|
223
|
+
if (!token || !stakingToken) return [];
|
|
224
|
+
|
|
225
|
+
// envStakingInfo is built via Promise.allSettled with an explicit
|
|
226
|
+
// fallback object at the .map below, so entries are always defined here.
|
|
227
|
+
const {
|
|
228
|
+
stakingInfo: envStakingInfoData,
|
|
229
|
+
historicalStaking,
|
|
230
|
+
price,
|
|
231
|
+
} = envStakingInfo[index];
|
|
112
232
|
const isBase = curr.chainId === base.id;
|
|
113
233
|
|
|
114
|
-
if (
|
|
115
|
-
!envStakingInfoData ||
|
|
116
|
-
(isBase && !envStakingInfoDataAfterX28Proposal)
|
|
117
|
-
) {
|
|
234
|
+
if (!envStakingInfoData || (isBase && !historicalStaking)) {
|
|
118
235
|
return [];
|
|
119
236
|
}
|
|
120
237
|
|
|
@@ -124,20 +241,9 @@ export async function getStakingInfo<
|
|
|
124
241
|
emissionPerSecond: emissionPerSecondRaw,
|
|
125
242
|
totalSupply: totalSupplyRaw,
|
|
126
243
|
unstakeWindow,
|
|
127
|
-
} = envStakingInfoData
|
|
128
|
-
cooldown: bigint;
|
|
129
|
-
distributionEnd: bigint;
|
|
130
|
-
emissionPerSecond: bigint;
|
|
131
|
-
totalSupply: bigint;
|
|
132
|
-
unstakeWindow: bigint;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
//Quick workaround to get governance token price from some other environment
|
|
136
|
-
const governanceTokenPrice = new Amount(
|
|
137
|
-
(envGovernanceTokenPriceData ?? 0n) as bigint,
|
|
138
|
-
18,
|
|
139
|
-
);
|
|
244
|
+
} = envStakingInfoData;
|
|
140
245
|
|
|
246
|
+
const tokenPrice = new Amount(price, 18);
|
|
141
247
|
const totalSupply = new Amount(totalSupplyRaw, 18);
|
|
142
248
|
const emissionPerSecond = new Amount(emissionPerSecondRaw, 18);
|
|
143
249
|
|
|
@@ -145,7 +251,9 @@ export async function getStakingInfo<
|
|
|
145
251
|
emissionPerSecond.value * SECONDS_PER_DAY * DAYS_PER_YEAR;
|
|
146
252
|
|
|
147
253
|
const apr =
|
|
148
|
-
|
|
254
|
+
totalSupply.value > 0
|
|
255
|
+
? ((emissionPerYear + totalSupply.value) / totalSupply.value - 1) * 100
|
|
256
|
+
: 0;
|
|
149
257
|
|
|
150
258
|
const stakingInfo: StakingInfo = {
|
|
151
259
|
apr: isBase ? baseStakingApr : apr,
|
|
@@ -153,10 +261,10 @@ export async function getStakingInfo<
|
|
|
153
261
|
cooldown: Number(cooldown),
|
|
154
262
|
distributionEnd: Number(distributionEnd),
|
|
155
263
|
token,
|
|
156
|
-
tokenPrice:
|
|
264
|
+
tokenPrice: tokenPrice.value,
|
|
157
265
|
stakingToken,
|
|
158
266
|
totalSupply,
|
|
159
|
-
totalSupplyUSD: totalSupply.value *
|
|
267
|
+
totalSupplyUSD: totalSupply.value * tokenPrice.value,
|
|
160
268
|
unstakeWindow: Number(unstakeWindow),
|
|
161
269
|
};
|
|
162
270
|
|
|
@@ -3,13 +3,10 @@ import { base } from "viem/chains";
|
|
|
3
3
|
import type { MoonwellClient } from "../../client/createMoonwellClient.js";
|
|
4
4
|
import { Amount, getEnvironmentsFromArgs } from "../../common/index.js";
|
|
5
5
|
import type { OptionalNetworkParameterType } from "../../common/types.js";
|
|
6
|
-
import {
|
|
7
|
-
type Environment,
|
|
8
|
-
type TokensType,
|
|
9
|
-
publicEnvironments,
|
|
10
|
-
} from "../../environments/index.js";
|
|
6
|
+
import type { Environment } from "../../environments/index.js";
|
|
11
7
|
import type { UserStakingInfo } from "../../types/staking.js";
|
|
12
8
|
import { getMerklCampaignIds, getMerklRewardsData } from "./common.js";
|
|
9
|
+
import { getGovernanceTokenPriceFor } from "./getWellPrice.js";
|
|
13
10
|
|
|
14
11
|
export type GetUserStakingInfoParameters<
|
|
15
12
|
environments,
|
|
@@ -21,6 +18,113 @@ export type GetUserStakingInfoParameters<
|
|
|
21
18
|
|
|
22
19
|
export type GetUserStakingInfoReturnType = Promise<UserStakingInfo[]>;
|
|
23
20
|
|
|
21
|
+
type UserStakingFields = {
|
|
22
|
+
cooldown: bigint;
|
|
23
|
+
pendingRewards: bigint;
|
|
24
|
+
totalStaked: bigint;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type StakingScheduleFields = {
|
|
28
|
+
cooldown: bigint;
|
|
29
|
+
unstakeWindow: bigint;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads per-user staking fields directly from stkWELL when views.getUserStakingInfo()
|
|
34
|
+
* is unavailable (e.g. reverts on Moonbeam).
|
|
35
|
+
*/
|
|
36
|
+
async function readUserStakingFromStkWell(
|
|
37
|
+
environment: Environment,
|
|
38
|
+
userAddress: Address,
|
|
39
|
+
): Promise<UserStakingFields | undefined> {
|
|
40
|
+
const stakingToken = environment.contracts.stakingToken;
|
|
41
|
+
if (!stakingToken) return undefined;
|
|
42
|
+
|
|
43
|
+
const [cooldownR, rewardsR, balanceR] = await Promise.allSettled([
|
|
44
|
+
stakingToken.read.stakersCooldowns([userAddress]),
|
|
45
|
+
stakingToken.read.getTotalRewardsBalance([userAddress]),
|
|
46
|
+
stakingToken.read.balanceOf([userAddress]),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Surface every rejection so a single failed read (e.g. balanceOf throttled)
|
|
50
|
+
// doesn't silently zero a field that the UI would then display as "no stake".
|
|
51
|
+
for (const r of [cooldownR, rewardsR, balanceR]) {
|
|
52
|
+
if (r.status === "rejected") {
|
|
53
|
+
environment.onError?.(r.reason, {
|
|
54
|
+
source: "user-staking-fallback",
|
|
55
|
+
chainId: environment.chainId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
cooldownR.status === "rejected" &&
|
|
62
|
+
rewardsR.status === "rejected" &&
|
|
63
|
+
balanceR.status === "rejected"
|
|
64
|
+
) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
cooldown: cooldownR.status === "fulfilled" ? cooldownR.value : 0n,
|
|
70
|
+
pendingRewards: rewardsR.status === "fulfilled" ? rewardsR.value : 0n,
|
|
71
|
+
totalStaked: balanceR.status === "fulfilled" ? balanceR.value : 0n,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reads global cooldown/unstakeWindow constants from stkWELL when
|
|
77
|
+
* views.getStakingInfo() is unavailable.
|
|
78
|
+
*/
|
|
79
|
+
async function readScheduleFromStkWell(
|
|
80
|
+
environment: Environment,
|
|
81
|
+
): Promise<StakingScheduleFields | undefined> {
|
|
82
|
+
const stakingToken = environment.contracts.stakingToken;
|
|
83
|
+
if (!stakingToken) return undefined;
|
|
84
|
+
|
|
85
|
+
const [cooldownR, unstakeWindowR] = await Promise.allSettled([
|
|
86
|
+
stakingToken.read.COOLDOWN_SECONDS(),
|
|
87
|
+
stakingToken.read.UNSTAKE_WINDOW(),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Surface every rejection. A silent cooldown=0n would corrupt downstream
|
|
91
|
+
// cooldownEnding math even if the unstakeWindow read succeeded.
|
|
92
|
+
for (const r of [cooldownR, unstakeWindowR]) {
|
|
93
|
+
if (r.status === "rejected") {
|
|
94
|
+
environment.onError?.(r.reason, {
|
|
95
|
+
source: "user-staking-schedule-fallback",
|
|
96
|
+
chainId: environment.chainId,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (cooldownR.status === "rejected" && unstakeWindowR.status === "rejected") {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
cooldown: cooldownR.status === "fulfilled" ? cooldownR.value : 0n,
|
|
107
|
+
unstakeWindow:
|
|
108
|
+
unstakeWindowR.status === "fulfilled" ? unstakeWindowR.value : 0n,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isUserStakingShape = (value: unknown): value is UserStakingFields => {
|
|
113
|
+
if (typeof value !== "object" || value === null) return false;
|
|
114
|
+
const v = value as Record<string, unknown>;
|
|
115
|
+
return (
|
|
116
|
+
typeof v.cooldown === "bigint" &&
|
|
117
|
+
typeof v.pendingRewards === "bigint" &&
|
|
118
|
+
typeof v.totalStaked === "bigint"
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const isScheduleShape = (value: unknown): value is StakingScheduleFields => {
|
|
123
|
+
if (typeof value !== "object" || value === null) return false;
|
|
124
|
+
const v = value as Record<string, unknown>;
|
|
125
|
+
return typeof v.cooldown === "bigint" && typeof v.unstakeWindow === "bigint";
|
|
126
|
+
};
|
|
127
|
+
|
|
24
128
|
export async function getUserStakingInfo<
|
|
25
129
|
environments,
|
|
26
130
|
Network extends Chain | undefined,
|
|
@@ -35,23 +139,50 @@ export async function getUserStakingInfo<
|
|
|
35
139
|
const envsWithStaking = environments.filter(
|
|
36
140
|
(env) => env.contracts.stakingToken,
|
|
37
141
|
);
|
|
142
|
+
|
|
143
|
+
const baseEnvironment = (
|
|
144
|
+
client.environments as { base?: Environment } | undefined
|
|
145
|
+
)?.base;
|
|
146
|
+
|
|
38
147
|
const envStakingInfo = await Promise.all(
|
|
39
148
|
envsWithStaking.map(async (environment) => {
|
|
40
|
-
const homeEnvironment =
|
|
41
|
-
(Object.values(publicEnvironments) as Environment[]).find((e) =>
|
|
42
|
-
e.custom?.governance?.chainIds?.includes(environment.chainId),
|
|
43
|
-
) || environment;
|
|
44
|
-
|
|
45
149
|
const settled = await Promise.allSettled([
|
|
46
150
|
environment.contracts.views?.read.getUserStakingInfo([userAddress]),
|
|
47
151
|
environment.contracts.governanceToken?.read.balanceOf([userAddress]),
|
|
48
|
-
homeEnvironment.contracts.views?.read.getGovernanceTokenPrice(),
|
|
49
152
|
environment.contracts.views?.read.getStakingInfo(),
|
|
153
|
+
getGovernanceTokenPriceFor(environment, baseEnvironment),
|
|
50
154
|
]);
|
|
51
155
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
const [userStakingR, balanceR, stakingScheduleR, priceR] = settled;
|
|
157
|
+
|
|
158
|
+
const viewsUserStaking =
|
|
159
|
+
userStakingR.status === "fulfilled" ? userStakingR.value : undefined;
|
|
160
|
+
const userStaking = isUserStakingShape(viewsUserStaking)
|
|
161
|
+
? viewsUserStaking
|
|
162
|
+
: await readUserStakingFromStkWell(environment, userAddress);
|
|
163
|
+
|
|
164
|
+
const viewsSchedule =
|
|
165
|
+
stakingScheduleR.status === "fulfilled"
|
|
166
|
+
? stakingScheduleR.value
|
|
167
|
+
: undefined;
|
|
168
|
+
const schedule = isScheduleShape(viewsSchedule)
|
|
169
|
+
? viewsSchedule
|
|
170
|
+
: await readScheduleFromStkWell(environment);
|
|
171
|
+
|
|
172
|
+
const tokenBalance =
|
|
173
|
+
balanceR.status === "fulfilled" && balanceR.value !== undefined
|
|
174
|
+
? balanceR.value
|
|
175
|
+
: 0n;
|
|
176
|
+
|
|
177
|
+
const price = priceR.status === "fulfilled" ? priceR.value : 0n;
|
|
178
|
+
if (priceR.status === "rejected") {
|
|
179
|
+
environment.onError?.(priceR.reason, {
|
|
180
|
+
source: "governance-token-price",
|
|
181
|
+
chainId: environment.chainId,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { userStaking, schedule, tokenBalance, price };
|
|
55
186
|
}),
|
|
56
187
|
);
|
|
57
188
|
|
|
@@ -63,28 +194,27 @@ export async function getUserStakingInfo<
|
|
|
63
194
|
);
|
|
64
195
|
|
|
65
196
|
const result = envsWithStaking.flatMap((curr, index) => {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const { cooldown
|
|
86
|
-
|
|
87
|
-
// merkl rewards (only for base)
|
|
197
|
+
const govKey = curr.config.contracts.governanceToken;
|
|
198
|
+
const stkKey = curr.config.contracts.stakingToken;
|
|
199
|
+
const currTokens = curr.config.tokens as Record<
|
|
200
|
+
string,
|
|
201
|
+
{ address: `0x${string}`; decimals: number; name: string; symbol: string }
|
|
202
|
+
>;
|
|
203
|
+
if (!govKey || !stkKey) return [];
|
|
204
|
+
const token = currTokens[govKey];
|
|
205
|
+
const stakingToken = currTokens[stkKey];
|
|
206
|
+
if (!token || !stakingToken) return [];
|
|
207
|
+
|
|
208
|
+
// envStakingInfo is built via Promise.all over the same envsWithStaking
|
|
209
|
+
// array, so index access is always defined.
|
|
210
|
+
const { userStaking, schedule, tokenBalance, price } =
|
|
211
|
+
envStakingInfo[index];
|
|
212
|
+
|
|
213
|
+
if (!userStaking || !schedule) return [];
|
|
214
|
+
|
|
215
|
+
const { cooldown, pendingRewards, totalStaked } = userStaking;
|
|
216
|
+
const { cooldown: cooldownSeconds, unstakeWindow } = schedule;
|
|
217
|
+
|
|
88
218
|
const isBase = curr.chainId === base.id;
|
|
89
219
|
const merklReward = merklRewards.reduce((acc, r) => {
|
|
90
220
|
if (r.chain === curr.chainId) {
|
|
@@ -94,27 +224,11 @@ export async function getUserStakingInfo<
|
|
|
94
224
|
}, 0n);
|
|
95
225
|
const merklPendingRewards = isBase ? merklReward : 0n;
|
|
96
226
|
|
|
97
|
-
const tokenBalance = (envStakingInfo[index]?.[1] ?? 0n) as bigint;
|
|
98
|
-
|
|
99
|
-
const governanceTokenPriceRaw = (envStakingInfo[index]?.[2] ??
|
|
100
|
-
0n) as bigint;
|
|
101
|
-
|
|
102
|
-
const stakingInfoData = envStakingInfo[index]?.[3] as
|
|
103
|
-
| {
|
|
104
|
-
cooldown: bigint;
|
|
105
|
-
unstakeWindow: bigint;
|
|
106
|
-
}
|
|
107
|
-
| undefined;
|
|
108
|
-
|
|
109
|
-
if (!stakingInfoData) return [];
|
|
110
|
-
|
|
111
|
-
const { cooldown: cooldownSeconds, unstakeWindow } = stakingInfoData;
|
|
112
|
-
|
|
113
227
|
const cooldownEnding = cooldown > 0n ? cooldown + cooldownSeconds : 0n;
|
|
114
228
|
const unstakingEnding =
|
|
115
229
|
cooldown > 0n ? cooldown + cooldownSeconds + unstakeWindow : 0n;
|
|
116
230
|
|
|
117
|
-
const
|
|
231
|
+
const tokenPrice = new Amount(price, 18);
|
|
118
232
|
|
|
119
233
|
const userStakingInfo: UserStakingInfo = {
|
|
120
234
|
chainId: curr.chainId,
|
|
@@ -128,7 +242,7 @@ export async function getUserStakingInfo<
|
|
|
128
242
|
: new Amount(pendingRewards, 18),
|
|
129
243
|
token,
|
|
130
244
|
tokenBalance: new Amount(tokenBalance, 18),
|
|
131
|
-
tokenPrice:
|
|
245
|
+
tokenPrice: tokenPrice.value,
|
|
132
246
|
stakingToken,
|
|
133
247
|
stakingTokenBalance: new Amount(totalStaked, 18),
|
|
134
248
|
};
|
|
@@ -9,6 +9,8 @@ import type { OptionalNetworkParameterType } from "../../common/types.js";
|
|
|
9
9
|
import type { Chain, GovernanceToken } from "../../environments/index.js";
|
|
10
10
|
import type { UserVotingPowers } from "../../types/userVotingPowers.js";
|
|
11
11
|
|
|
12
|
+
const warnedNoViewsEnvs = new Set<string>();
|
|
13
|
+
|
|
12
14
|
export type GetUserVotingPowersParameters<
|
|
13
15
|
environments,
|
|
14
16
|
network extends Chain | undefined,
|
|
@@ -49,14 +51,31 @@ export async function getUserVotingPowers<
|
|
|
49
51
|
|
|
50
52
|
const environments = getEnvironmentsFromArgs(client, args);
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
// A chain can hold a governance token without deploying a views contract
|
|
55
|
+
// (voting reads run on the hub). Skipping the no-views case here lets the
|
|
56
|
+
// read site below call views.read.getUserVotingPower directly.
|
|
57
|
+
const tokenEnvironments = environments.flatMap((env) => {
|
|
58
|
+
if (env.custom?.governance?.token !== governanceToken) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const views = env.contracts.views;
|
|
62
|
+
if (views === undefined) {
|
|
63
|
+
const key = `${env.chainId}:${governanceToken}`;
|
|
64
|
+
if (!warnedNoViewsEnvs.has(key)) {
|
|
65
|
+
warnedNoViewsEnvs.add(key);
|
|
66
|
+
console.warn(
|
|
67
|
+
`[moonwell-sdk] getUserVotingPowers: skipping chainId=${env.chainId} for governanceToken=${governanceToken} — environment holds the token but has no views contract.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return [{ env, views }];
|
|
73
|
+
});
|
|
55
74
|
|
|
56
75
|
const perChainBlockNumbers =
|
|
57
76
|
snapshotTimestamp !== undefined
|
|
58
77
|
? await Promise.all(
|
|
59
|
-
tokenEnvironments.map((env) =>
|
|
78
|
+
tokenEnvironments.map(({ env }) =>
|
|
60
79
|
getBlockNumberAtTimestamp(
|
|
61
80
|
env.publicClient,
|
|
62
81
|
BigInt(snapshotTimestamp),
|
|
@@ -65,23 +84,19 @@ export async function getUserVotingPowers<
|
|
|
65
84
|
)
|
|
66
85
|
: undefined;
|
|
67
86
|
|
|
68
|
-
const
|
|
69
|
-
tokenEnvironments.map((
|
|
87
|
+
const resolvedVotingPowers = await Promise.all(
|
|
88
|
+
tokenEnvironments.map(async ({ env, views }, index) => {
|
|
70
89
|
const blockForChain = perChainBlockNumbers
|
|
71
90
|
? perChainBlockNumbers[index]
|
|
72
91
|
: blockNumber;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
);
|
|
92
|
+
const votingPowers = await views.read.getUserVotingPower([userAddress], {
|
|
93
|
+
blockNumber: blockForChain,
|
|
94
|
+
});
|
|
95
|
+
return { env, votingPowers };
|
|
79
96
|
}),
|
|
80
97
|
);
|
|
81
98
|
|
|
82
|
-
return
|
|
83
|
-
const votingPowers = environmentsUserVotingPowers[index]!;
|
|
84
|
-
|
|
99
|
+
return resolvedVotingPowers.map(({ env: environment, votingPowers }) => {
|
|
85
100
|
return {
|
|
86
101
|
chainId: environment.chainId,
|
|
87
102
|
|