@moonwell-fi/moonwell-sdk 0.13.0 → 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 +10 -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/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/getWellPrice.js +26 -0
- package/_cjs/actions/governance/getWellPrice.js.map +1 -0
- package/_cjs/actions/morpho/user-rewards/common.js +10 -3
- package/_cjs/actions/morpho/user-rewards/common.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/errors/version.js +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/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/getWellPrice.js +50 -0
- package/_esm/actions/governance/getWellPrice.js.map +1 -0
- package/_esm/actions/morpho/user-rewards/common.js +10 -3
- package/_esm/actions/morpho/user-rewards/common.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/errors/version.js +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/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/getWellPrice.d.ts +29 -0
- package/_types/actions/governance/getWellPrice.d.ts.map +1 -0
- package/_types/actions/morpho/user-rewards/common.d.ts.map +1 -1
- package/_types/actions/morpho/vaults/common.d.ts.map +1 -1
- package/_types/errors/version.d.ts +1 -1
- package/actions/core/markets/common.ts +11 -6
- package/actions/core/user-rewards/common.ts +6 -5
- package/actions/governance/getStakingInfo.ts +195 -87
- package/actions/governance/getUserStakingInfo.ts +168 -54
- package/actions/governance/getWellPrice.ts +66 -0
- package/actions/morpho/user-rewards/common.ts +10 -3
- package/actions/morpho/vaults/common.ts +19 -12
- package/errors/version.ts +1 -1
- 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
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Environment,
|
|
3
|
+
publicEnvironments,
|
|
4
|
+
} from "../../environments/index.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reads WELL/USD from Base's lending oracle via getUnderlyingPrice(mWELL).
|
|
8
|
+
*
|
|
9
|
+
* The Base oracle is Chainlink-fed and shared by the lending markets, so it's
|
|
10
|
+
* the authoritative WELL/USD source. Used in place of the per-chain
|
|
11
|
+
* views.getGovernanceTokenPrice() which is unreliable on Moonbeam (returns
|
|
12
|
+
* stale data) and Base (returns 0).
|
|
13
|
+
*
|
|
14
|
+
* Returns a uint256 already scaled to 18 decimals.
|
|
15
|
+
*
|
|
16
|
+
* @param baseEnvironment Pass the caller's Base environment when available so
|
|
17
|
+
* user-configured RPCs / onError handlers are honored. Falls back to the
|
|
18
|
+
* SDK's default public Base environment otherwise.
|
|
19
|
+
*/
|
|
20
|
+
export async function getWellPriceFromBaseOracle(
|
|
21
|
+
baseEnvironment?: Environment,
|
|
22
|
+
): Promise<bigint> {
|
|
23
|
+
const baseEnv = baseEnvironment ?? publicEnvironments.base;
|
|
24
|
+
const tokens = baseEnv.config.tokens as Record<
|
|
25
|
+
string,
|
|
26
|
+
{ address: `0x${string}` } | undefined
|
|
27
|
+
>;
|
|
28
|
+
const mWELL = tokens.MOONWELL_WELL?.address;
|
|
29
|
+
const oracle = baseEnv.contracts.oracle;
|
|
30
|
+
if (!mWELL || !oracle) {
|
|
31
|
+
// A custom Base env without MOONWELL_WELL or an oracle would silently
|
|
32
|
+
// zero out every WELL-priced read across the SDK. Surface it instead.
|
|
33
|
+
baseEnv.onError?.(
|
|
34
|
+
new Error(
|
|
35
|
+
`getWellPriceFromBaseOracle: missing ${!mWELL ? "MOONWELL_WELL token" : "oracle contract"} on Base env`,
|
|
36
|
+
),
|
|
37
|
+
{ source: "well-price", chainId: baseEnv.chainId },
|
|
38
|
+
);
|
|
39
|
+
return 0n;
|
|
40
|
+
}
|
|
41
|
+
return await oracle.read.getUnderlyingPrice([mWELL]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the governance-token-in-USD price for an environment.
|
|
46
|
+
*
|
|
47
|
+
* - For WELL-governed chains (Base, Optimism, Moonbeam), reads from the Base
|
|
48
|
+
* lending oracle's mWELL underlying price (authoritative, Chainlink-fed).
|
|
49
|
+
* - For non-WELL chains (currently only Moonriver / MFAM), reads from the
|
|
50
|
+
* env's own views.getGovernanceTokenPrice() — Moonriver has its own MFAM
|
|
51
|
+
* oracle and isn't priced from Base.
|
|
52
|
+
*
|
|
53
|
+
* Returns 0n if the lookup fails.
|
|
54
|
+
*/
|
|
55
|
+
export async function getGovernanceTokenPriceFor(
|
|
56
|
+
environment: Environment,
|
|
57
|
+
baseEnvironment?: Environment,
|
|
58
|
+
): Promise<bigint> {
|
|
59
|
+
if (environment.custom?.governance?.token === "WELL") {
|
|
60
|
+
return getWellPriceFromBaseOracle(baseEnvironment);
|
|
61
|
+
}
|
|
62
|
+
// Non-WELL (e.g. Moonriver / MFAM): the env itself is the governance "home".
|
|
63
|
+
const views = environment.contracts.views;
|
|
64
|
+
if (!views) return 0n;
|
|
65
|
+
return (await views.read.getGovernanceTokenPrice()) ?? 0n;
|
|
66
|
+
}
|