@moonwell-fi/moonwell-sdk
Version:
TypeScript Interface for Moonwell
111 lines • 5.13 kB
JavaScript
import { isAddress } from "viem";
import { base, moonbeam, optimism } from "viem/chains";
import { publicEnvironments, } from "../../environments/index.js";
import * as logger from "../../logger/console.js";
import { getWithRetry } from "../axiosWithRetry.js";
import { fetchAllVoters } from "./governor-api-client.js";
/**
* Returns a list of delegates with voting power and activity stats
* Data is fetched from Governor API, enriched with forum profiles and on-chain voting power
* All delegates are returned sorted by total voting power
* The app should handle pagination locally for better performance and caching
*/
export async function getDelegates(client) {
const logId = logger.start("getDelegates", "Starting to get delegates...");
const governanceEnvironment = publicEnvironments.moonbeam;
const apiVoters = await fetchAllVoters(governanceEnvironment);
const forumProfiles = await getForumProfiles();
// TODO: include mainnet.id once Ethereum's views contract ships, to match
// WELL.chainIds in environments/definitions/governance.ts.
const targetChainIds = [moonbeam.id, base.id, optimism.id];
const envs = Object.values(client.environments).filter((env) => env.contracts.views !== undefined &&
targetChainIds.includes(env.chainId));
const votingPowers = await Promise.all(apiVoters.map(async (voter) => Promise.all(envs.map((environment) => environment.contracts.views?.read.getUserVotingPower([
voter.id,
])))));
let delegates = apiVoters.map((voter, index) => {
const forumProfile = forumProfiles.find((p) => p.wallet.toLowerCase() === voter.id.toLowerCase());
const votingPower = {};
const userVotingPowers = votingPowers[index];
if (userVotingPowers) {
envs.forEach((env, reduceIndex) => {
const { claimsVotes, stakingVotes, tokenVotes } = userVotingPowers[reduceIndex];
const totalVotes = claimsVotes.delegatedVotingPower +
stakingVotes.delegatedVotingPower +
tokenVotes.delegatedVotingPower;
votingPower[env.chainId] = Number(totalVotes / BigInt(10 ** 18));
});
}
const delegate = {
avatar: forumProfile?.avatar || "",
name: forumProfile?.name || voter.id,
wallet: voter.id,
pitch: forumProfile?.pitch || { intro: "", url: "" },
proposals: {
all: {
created: voter.createdProposalIds.length,
voted: voter.votedProposalIds.length,
},
},
votingPower,
};
return delegate;
});
delegates = delegates.sort((a, b) => {
const aTotalPower = Object.values(a.votingPower || {}).reduce((sum, power) => sum + power, 0);
const bTotalPower = Object.values(b.votingPower || {}).reduce((sum, power) => sum + power, 0);
return bTotalPower - aTotalPower;
});
logger.end(logId);
return delegates;
}
/**
* Helper function to fetch delegate profiles from forum
*/
const getForumProfiles = async () => {
const profiles = [];
const getUsersPaginated = async (page = 0) => {
try {
const response = await getWithRetry(`https://forum.moonwell.fi/directory_items.json?period=all&order=Delegate+Wallet+Address&user_field_ids=2%7C1%7C3&page=${page}`);
if (response.status !== 200 || !response.data) {
return false;
}
const results = response.data.directory_items
.filter((item) => item.user.user_fields["1"] !== undefined &&
item.user.user_fields["1"].value !== undefined &&
item.user.user_fields["1"].value[0] !== undefined &&
isAddress(item.user.user_fields["1"].value[0]) &&
item.user.user_fields["2"] !== undefined &&
item.user.user_fields["2"].value !== undefined &&
item.user.user_fields["2"].value[0] !== undefined)
.map((item) => {
const avatar = item.user.avatar_template.replace("{size}", "160");
return {
avatar: avatar.startsWith("/user_avatar")
? `https://dub1.discourse-cdn.com/flex017${avatar}`
: avatar,
name: item.user.username,
wallet: item.user.user_fields["1"].value[0],
pitch: {
intro: item.user.user_fields["2"].value[0],
url: item.user.user_fields["3"]?.value[0],
},
};
});
profiles.push(...results);
return response.data.directory_items.length > 0;
}
catch (error) {
// If forum fetch fails, just return what we have
return false;
}
};
let page = 0;
let hasMore = true;
while (hasMore) {
hasMore = await getUsersPaginated(page);
page++;
}
return profiles;
};
//# sourceMappingURL=getDelegates.js.map