@moonwell-fi/moonwell-sdk 0.15.0 → 0.16.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 +14 -0
- package/_cjs/actions/governance/getDelegates.js +6 -1
- package/_cjs/actions/governance/getDelegates.js.map +1 -1
- package/_cjs/actions/governance/getUserVotingPowers.js +0 -10
- package/_cjs/actions/governance/getUserVotingPowers.js.map +1 -1
- package/_cjs/actions/governance/ipfs.js +45 -0
- package/_cjs/actions/governance/ipfs.js.map +1 -0
- package/_cjs/actions/governance/proposals/common.js +27 -3
- package/_cjs/actions/governance/proposals/common.js.map +1 -1
- package/_cjs/actions/governance/proposals/getProposal.js +6 -1
- package/_cjs/actions/governance/proposals/getProposal.js.map +1 -1
- package/_cjs/actions/governance/proposals/getProposals.js +13 -3
- package/_cjs/actions/governance/proposals/getProposals.js.map +1 -1
- package/_cjs/environments/definitions/ethereum/contracts.js +2 -1
- package/_cjs/environments/definitions/ethereum/contracts.js.map +1 -1
- package/_cjs/errors/version.js +1 -1
- package/_esm/actions/governance/getDelegates.js +7 -5
- package/_esm/actions/governance/getDelegates.js.map +1 -1
- package/_esm/actions/governance/getUserVotingPowers.js +0 -13
- package/_esm/actions/governance/getUserVotingPowers.js.map +1 -1
- package/_esm/actions/governance/ipfs.js +69 -0
- package/_esm/actions/governance/ipfs.js.map +1 -0
- package/_esm/actions/governance/proposals/common.js +43 -5
- package/_esm/actions/governance/proposals/common.js.map +1 -1
- package/_esm/actions/governance/proposals/getProposal.js +7 -2
- package/_esm/actions/governance/proposals/getProposal.js.map +1 -1
- package/_esm/actions/governance/proposals/getProposals.js +16 -4
- package/_esm/actions/governance/proposals/getProposals.js.map +1 -1
- package/_esm/environments/definitions/ethereum/contracts.js +2 -1
- package/_esm/environments/definitions/ethereum/contracts.js.map +1 -1
- package/_esm/errors/version.js +1 -1
- package/_types/actions/governance/getDelegates.d.ts.map +1 -1
- package/_types/actions/governance/getUserVotingPowers.d.ts.map +1 -1
- package/_types/actions/governance/ipfs.d.ts +33 -0
- package/_types/actions/governance/ipfs.d.ts.map +1 -0
- package/_types/actions/governance/proposals/common.d.ts +16 -1
- package/_types/actions/governance/proposals/common.d.ts.map +1 -1
- package/_types/actions/governance/proposals/getProposal.d.ts.map +1 -1
- package/_types/actions/governance/proposals/getProposals.d.ts.map +1 -1
- package/_types/client/createMoonwellClient.d.ts +4 -2
- package/_types/client/createMoonwellClient.d.ts.map +1 -1
- package/_types/environments/definitions/ethereum/contracts.d.ts +2 -1
- package/_types/environments/definitions/ethereum/contracts.d.ts.map +1 -1
- package/_types/environments/index.d.ts +2 -1
- package/_types/environments/index.d.ts.map +1 -1
- package/_types/errors/version.d.ts +1 -1
- package/actions/governance/getDelegates.ts +7 -5
- package/actions/governance/getUserVotingPowers.ts +0 -16
- package/actions/governance/ipfs.ts +82 -0
- package/actions/governance/proposals/common.ts +56 -4
- package/actions/governance/proposals/getProposal.ts +8 -0
- package/actions/governance/proposals/getProposals.ts +16 -2
- package/environments/definitions/ethereum/contracts.ts +2 -1
- package/errors/version.ts +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAddress } from "viem";
|
|
2
|
-
import { base, moonbeam, optimism } from "viem/chains";
|
|
2
|
+
import { base, mainnet, moonbeam, optimism } from "viem/chains";
|
|
3
3
|
import type { MoonwellClient } from "../../client/createMoonwellClient.js";
|
|
4
4
|
import {
|
|
5
5
|
type Environment,
|
|
@@ -28,10 +28,12 @@ export async function getDelegates(
|
|
|
28
28
|
const apiVoters = await fetchAllVoters(governanceEnvironment);
|
|
29
29
|
const forumProfiles = await getForumProfiles();
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const targetChainIds = [
|
|
32
|
+
moonbeam.id,
|
|
33
|
+
base.id,
|
|
34
|
+
optimism.id,
|
|
35
|
+
mainnet.id,
|
|
36
|
+
] as const;
|
|
35
37
|
const envs = Object.values(client.environments as Environment[]).filter(
|
|
36
38
|
(env) =>
|
|
37
39
|
env.contracts.views !== undefined &&
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type Address, zeroAddress } from "viem";
|
|
2
|
-
import { mainnet } from "viem/chains";
|
|
3
2
|
import type { MoonwellClient } from "../../client/createMoonwellClient.js";
|
|
4
3
|
import {
|
|
5
4
|
Amount,
|
|
@@ -12,11 +11,6 @@ import type { UserVotingPowers } from "../../types/userVotingPowers.js";
|
|
|
12
11
|
|
|
13
12
|
const warnedSkippedEnvs = new Set<string>();
|
|
14
13
|
|
|
15
|
-
// Ethereum's views contract is staking-only (exposes getUserStakingVotingPower
|
|
16
|
-
// but not the cross-source getUserVotingPower used here). Reading it on Eth
|
|
17
|
-
// would revert at runtime, so we skip those chains explicitly.
|
|
18
|
-
const STAKING_ONLY_VIEWS_CHAIN_IDS: ReadonlySet<number> = new Set([mainnet.id]);
|
|
19
|
-
|
|
20
14
|
export type GetUserVotingPowersParameters<
|
|
21
15
|
environments,
|
|
22
16
|
network extends Chain | undefined,
|
|
@@ -75,16 +69,6 @@ export async function getUserVotingPowers<
|
|
|
75
69
|
}
|
|
76
70
|
return [];
|
|
77
71
|
}
|
|
78
|
-
if (STAKING_ONLY_VIEWS_CHAIN_IDS.has(env.chainId)) {
|
|
79
|
-
const key = `${env.chainId}:${governanceToken}:staking-only`;
|
|
80
|
-
if (!warnedSkippedEnvs.has(key)) {
|
|
81
|
-
warnedSkippedEnvs.add(key);
|
|
82
|
-
console.warn(
|
|
83
|
-
`[moonwell-sdk] getUserVotingPowers: skipping chainId=${env.chainId} for governanceToken=${governanceToken} — views contract is staking-only and does not expose getUserVotingPower.`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
return [];
|
|
87
|
-
}
|
|
88
72
|
return [{ env, views }];
|
|
89
73
|
});
|
|
90
74
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Environment } from "../../environments/index.js";
|
|
2
|
+
import { getWithRetry } from "../axiosWithRetry.js";
|
|
3
|
+
import type { ApiProposal } from "./governor-api-client.js";
|
|
4
|
+
|
|
5
|
+
const PINATA_GATEWAY = "https://d4529a05.mypinata.cloud/ipfs";
|
|
6
|
+
|
|
7
|
+
// IPFS content is content-addressed, so a per-hash result is immutable and the
|
|
8
|
+
// cache lives for the process lifetime — no TTL needed. Repeated getProposals()
|
|
9
|
+
// calls in the same Node/browser session reuse the resolved markdown.
|
|
10
|
+
const ipfsContentCache = new Map<string, string>();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Strip the `ipfs://` prefix from an indexer-supplied URI. Returns null for
|
|
14
|
+
* anything that isn't an IPFS URI (including empty strings and undefined),
|
|
15
|
+
* which lets callers skip the fetch with a single check.
|
|
16
|
+
*/
|
|
17
|
+
export const parseIpfsHash = (uri: string | undefined): string | null => {
|
|
18
|
+
if (!uri) return null;
|
|
19
|
+
return uri.match(/^ipfs:\/\/(.+)$/)?.[1] ?? null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fetch a single IPFS resource via the Pinata gateway. Cached by hash.
|
|
24
|
+
*
|
|
25
|
+
* `responseType: "text"` disables axios's default JSON auto-parse so we keep
|
|
26
|
+
* the markdown body verbatim. We additionally reject non-string responses so
|
|
27
|
+
* an HTML error page or JSON payload doesn't poison the cache as
|
|
28
|
+
* `"[object Object]"` — the caller's per-proposal catch keeps the `ipfs://`
|
|
29
|
+
* URI in place when that happens.
|
|
30
|
+
*/
|
|
31
|
+
export const fetchIpfsContent = async (hash: string): Promise<string> => {
|
|
32
|
+
const cached = ipfsContentCache.get(hash);
|
|
33
|
+
if (cached !== undefined) return cached;
|
|
34
|
+
|
|
35
|
+
const response = await getWithRetry<string>(`${PINATA_GATEWAY}/${hash}`, {
|
|
36
|
+
responseType: "text",
|
|
37
|
+
});
|
|
38
|
+
if (typeof response.data !== "string") {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[fetchIpfsContent] non-string body for hash=${hash} (typeof=${typeof response.data})`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
ipfsContentCache.set(hash, response.data);
|
|
44
|
+
return response.data;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* For every proposal whose `description` is an `ipfs://` URI, fetch the
|
|
49
|
+
* underlying markdown from Pinata and replace `description` in place. All
|
|
50
|
+
* fetches run in parallel.
|
|
51
|
+
*
|
|
52
|
+
* Failures are logged via console.warn AND surfaced through
|
|
53
|
+
* `env.onError` (matching the convention used by `getProposalData` /
|
|
54
|
+
* `getExtendedProposalData`), then swallowed per-proposal: the `ipfs://` URI
|
|
55
|
+
* is left untouched so consumers can detect (e.g. via
|
|
56
|
+
* `description.startsWith("ipfs://")`) and render a fallback. The bulk call
|
|
57
|
+
* never rejects, so a single bad pin doesn't kill `getProposals()` for
|
|
58
|
+
* everyone.
|
|
59
|
+
*/
|
|
60
|
+
export const resolveIpfsDescriptions = async (
|
|
61
|
+
proposals: ApiProposal[],
|
|
62
|
+
env: Environment,
|
|
63
|
+
): Promise<void> => {
|
|
64
|
+
await Promise.all(
|
|
65
|
+
proposals.map(async (proposal) => {
|
|
66
|
+
const hash = parseIpfsHash(proposal.description);
|
|
67
|
+
if (!hash) return;
|
|
68
|
+
try {
|
|
69
|
+
proposal.description = await fetchIpfsContent(hash);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[resolveIpfsDescriptions] failed to fetch hash=${hash}:`,
|
|
73
|
+
error,
|
|
74
|
+
);
|
|
75
|
+
env.onError?.(error, {
|
|
76
|
+
source: "governance-ipfs-description",
|
|
77
|
+
chainId: proposal.chainId,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -262,12 +262,61 @@ const getLegacyArtemisMaxId = async (
|
|
|
262
262
|
}
|
|
263
263
|
};
|
|
264
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Reads the multichain governor's quorum on every chain that holds an active
|
|
267
|
+
* proposal but isn't the one we'd normally read through. Returns a chainId →
|
|
268
|
+
* quorum map; chains with no `multichainGovernor` wired are omitted, as are
|
|
269
|
+
* chains whose quorum read reverted. Callers substitute `0n` for misses via
|
|
270
|
+
* the `options.crossChainQuorums?.get(...) ?? 0n` pattern.
|
|
271
|
+
*
|
|
272
|
+
* Read failures are routed through `onError` so Sentry-wired consumers see
|
|
273
|
+
* them — matching `getProposalData` / `getCrossChainProposalData` /
|
|
274
|
+
* `getExtendedProposalData`. The caller's `governanceEnvironment` carries
|
|
275
|
+
* `onError` since we don't have one per foreign chain in scope.
|
|
276
|
+
*/
|
|
277
|
+
export const readCrossChainQuorums = async (
|
|
278
|
+
apiProposals: ApiProposal[],
|
|
279
|
+
governanceEnvironment: Environment,
|
|
280
|
+
): Promise<Map<number, bigint>> => {
|
|
281
|
+
const otherChainIds = Array.from(
|
|
282
|
+
new Set(
|
|
283
|
+
apiProposals
|
|
284
|
+
.map((p) => p.chainId)
|
|
285
|
+
.filter((c) => c !== governanceEnvironment.chainId),
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
const quorums = new Map<number, bigint>();
|
|
289
|
+
await Promise.all(
|
|
290
|
+
otherChainIds.map(async (chainId) => {
|
|
291
|
+
const env = (Object.values(publicEnvironments) as Environment[]).find(
|
|
292
|
+
(e) => e.chainId === chainId,
|
|
293
|
+
);
|
|
294
|
+
const mg = env?.contracts.multichainGovernor;
|
|
295
|
+
if (!mg) return;
|
|
296
|
+
try {
|
|
297
|
+
quorums.set(chainId, await mg.read.quorum());
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.warn(
|
|
300
|
+
`[readCrossChainQuorums] quorum read failed for chainId=${chainId}:`,
|
|
301
|
+
error,
|
|
302
|
+
);
|
|
303
|
+
governanceEnvironment.onError?.(error, {
|
|
304
|
+
source: "governance-cross-chain-quorum",
|
|
305
|
+
chainId,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
return quorums;
|
|
311
|
+
};
|
|
312
|
+
|
|
265
313
|
/**
|
|
266
314
|
* Fetches on-chain data for multiple proposals
|
|
267
315
|
*/
|
|
268
316
|
export const getProposalsOnChainData = async (
|
|
269
317
|
apiProposals: ApiProposal[],
|
|
270
318
|
governanceEnvironment: Environment,
|
|
319
|
+
options?: { crossChainQuorums?: Map<number, bigint> },
|
|
271
320
|
): Promise<ProposalOnChainData[]> => {
|
|
272
321
|
let quorum = 0n;
|
|
273
322
|
|
|
@@ -289,9 +338,12 @@ export const getProposalsOnChainData = async (
|
|
|
289
338
|
const onChainDataList = await Promise.all(
|
|
290
339
|
apiProposals.map(async (p) => {
|
|
291
340
|
// Proposals from a different chain than the governance env (e.g. chainId=1
|
|
292
|
-
// Ethereum proposals fetched through the Moonbeam env) can't
|
|
293
|
-
// this env's contracts — derive state from API events here
|
|
294
|
-
// never see a misleading `state: 0` default for finalized
|
|
341
|
+
// Ethereum proposals fetched through the Moonbeam env) can't have their
|
|
342
|
+
// state read from this env's contracts — derive state from API events here
|
|
343
|
+
// so callers never see a misleading `state: 0` default for finalized
|
|
344
|
+
// proposals. Quorum, however, comes from a per-chain map populated by the
|
|
345
|
+
// caller via `readCrossChainQuorums` (defaulting to 0n when the caller
|
|
346
|
+
// didn't pre-fetch, e.g. in unit tests).
|
|
295
347
|
if (p.chainId !== governanceEnvironment.chainId) {
|
|
296
348
|
const formatted = formatApiProposalData(p);
|
|
297
349
|
return {
|
|
@@ -299,7 +351,7 @@ export const getProposalsOnChainData = async (
|
|
|
299
351
|
proposalData: null,
|
|
300
352
|
eta: 0,
|
|
301
353
|
votesCollected: false,
|
|
302
|
-
quorum: 0n,
|
|
354
|
+
quorum: options?.crossChainQuorums?.get(p.chainId) ?? 0n,
|
|
303
355
|
};
|
|
304
356
|
}
|
|
305
357
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
fetchProposal,
|
|
11
11
|
isNotFoundError,
|
|
12
12
|
} from "../governor-api-client.js";
|
|
13
|
+
import { resolveIpfsDescriptions } from "../ipfs.js";
|
|
13
14
|
import {
|
|
14
15
|
appendProposalExtendedData,
|
|
15
16
|
formatApiProposalData,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
getProposalData,
|
|
19
20
|
getProposalsOnChainData,
|
|
20
21
|
isMultichainProposal,
|
|
22
|
+
readCrossChainQuorums,
|
|
21
23
|
} from "./common.js";
|
|
22
24
|
|
|
23
25
|
export type GetProposalParameters<
|
|
@@ -91,10 +93,16 @@ async function getMoonbeamProposal(
|
|
|
91
93
|
return undefined;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
const [, crossChainQuorums] = await Promise.all([
|
|
97
|
+
resolveIpfsDescriptions([apiProposal], governanceEnvironment),
|
|
98
|
+
readCrossChainQuorums([apiProposal], governanceEnvironment),
|
|
99
|
+
]);
|
|
100
|
+
|
|
94
101
|
const formattedData = formatApiProposalData(apiProposal);
|
|
95
102
|
const onChainDataList = await getProposalsOnChainData(
|
|
96
103
|
[apiProposal],
|
|
97
104
|
governanceEnvironment,
|
|
105
|
+
{ crossChainQuorums },
|
|
98
106
|
);
|
|
99
107
|
const onChainData = onChainDataList[0]!;
|
|
100
108
|
const isMultichain = isMultichainProposal(apiProposal.targets);
|
|
@@ -6,6 +6,7 @@ import type { Chain, Environment } from "../../../environments/index.js";
|
|
|
6
6
|
import * as logger from "../../../logger/console.js";
|
|
7
7
|
import { type Proposal, ProposalState } from "../../../types/proposal.js";
|
|
8
8
|
import { type ApiProposal, fetchAllProposals } from "../governor-api-client.js";
|
|
9
|
+
import { resolveIpfsDescriptions } from "../ipfs.js";
|
|
9
10
|
import {
|
|
10
11
|
appendProposalExtendedData,
|
|
11
12
|
formatApiProposalData,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
getProposalData,
|
|
15
16
|
getProposalsOnChainData,
|
|
16
17
|
isMultichainProposal,
|
|
18
|
+
readCrossChainQuorums,
|
|
17
19
|
} from "./common.js";
|
|
18
20
|
|
|
19
21
|
export type GetProposalsParameters<
|
|
@@ -93,19 +95,31 @@ async function getMoonbeamProposals(
|
|
|
93
95
|
const chainsAttempted: ReadonlyArray<1 | 1284> = [1, 1284];
|
|
94
96
|
const apiProposals: ApiProposal[] = [];
|
|
95
97
|
results.forEach((result, index) => {
|
|
98
|
+
const chainId = chainsAttempted[index];
|
|
96
99
|
if (result.status === "fulfilled") {
|
|
97
100
|
apiProposals.push(...result.value);
|
|
98
|
-
} else {
|
|
101
|
+
} else if (chainId !== undefined) {
|
|
99
102
|
console.warn(
|
|
100
|
-
`[getProposals] Failed to fetch proposals for chainId=${
|
|
103
|
+
`[getProposals] Failed to fetch proposals for chainId=${chainId}; continuing with remaining chains.`,
|
|
101
104
|
result.reason,
|
|
102
105
|
);
|
|
106
|
+
governanceEnvironment.onError?.(result.reason, {
|
|
107
|
+
source: "governance-proposals",
|
|
108
|
+
chainId,
|
|
109
|
+
});
|
|
103
110
|
}
|
|
104
111
|
});
|
|
105
112
|
|
|
113
|
+
// IPFS resolution and cross-chain quorum reads are independent — run them in
|
|
114
|
+
// parallel to save one network round-trip on the proposal list path.
|
|
115
|
+
const [, crossChainQuorums] = await Promise.all([
|
|
116
|
+
resolveIpfsDescriptions(apiProposals, governanceEnvironment),
|
|
117
|
+
readCrossChainQuorums(apiProposals, governanceEnvironment),
|
|
118
|
+
]);
|
|
106
119
|
const onChainDataList = await getProposalsOnChainData(
|
|
107
120
|
apiProposals,
|
|
108
121
|
governanceEnvironment,
|
|
122
|
+
{ crossChainQuorums },
|
|
109
123
|
);
|
|
110
124
|
|
|
111
125
|
const proposals: Proposal[] = apiProposals.map((apiProposal, index) => {
|
|
@@ -6,6 +6,7 @@ export const contracts = createContractsConfig({
|
|
|
6
6
|
contracts: {
|
|
7
7
|
stakingToken: "stkWELL",
|
|
8
8
|
governanceToken: "WELL",
|
|
9
|
-
views: "
|
|
9
|
+
views: "0x2d85b9c48a8c582f0AA244e134e9C6f30Cf7786e",
|
|
10
|
+
multichainGovernor: "0x8769B70ac7c93AF0e75de0D69877709B66d75838",
|
|
10
11
|
},
|
|
11
12
|
});
|
package/errors/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.16.1'
|