@moonwell-fi/moonwell-sdk
Version:
TypeScript Interface for Moonwell
341 lines • 16.2 kB
JavaScript
import axios from "axios";
import lodash from "lodash";
const { last } = lodash;
import { moonriver } from "viem/chains";
import { Amount } from "../../../common/index.js";
import { publicEnvironments, } from "../../../environments/index.js";
import { MultichainProposalStateMapping, } from "../../../types/proposal.js";
axios.defaults.timeout = 5_000;
export const appendProposalExtendedData = (proposals, extendedDatas) => {
proposals.forEach((proposal) => {
const extendedData = extendedDatas.find((item) => item.id === proposal.id);
if (extendedData) {
// const lastProposalUpdate = first(
// last(extendedData.stateChanges)?.messages,
// );
// // If the xChain timestamp is in the future, but less than 24 hours from now, it is queued
// if (lastProposalUpdate && lastProposalUpdate.timestamp > 0) {
// const now = Math.floor(Date.now() / 1000);
// if (lastProposalUpdate.timestamp + 60 * 60 * 24 > now) {
// proposal.state = ProposalState.MultichainQueued;
// } else {
// proposal.state = ProposalState.MultichainExecuted;
// }
// }
proposal.title = extendedData.title;
proposal.calldatas = extendedData.calldatas;
proposal.description = extendedData.description;
proposal.signatures = extendedData.signatures;
proposal.stateChanges = extendedData.stateChanges;
proposal.subtitle = extendedData.subtitle;
proposal.targets = extendedData.targets;
}
});
};
const extractProposalSubtitle = (input) => {
// Split the input into lines
const lines = input.split("\n");
// Find the first line that starts with a markdown H1 (#)
const h1Line = lines.find((line) => line.startsWith("#"));
if (!h1Line) {
return input ? input.substring(0, 100) : "";
}
// Remove the leading '#' and trim whitespace
let result = h1Line.substring(1).trim();
// If there's a markdown H2 (##) in the line, get the text before it
const h2Index = result.indexOf("##");
if (h2Index !== -1) {
result = result.substring(0, h2Index).trim();
}
// Remove any non-newline '\n' occurrences from the extracted text
result = result.replace(/\\n/g, "").trim();
// If the remaining text is longer than 80 characters, truncate it and add an ellipsis
if (result.length > 80) {
result = `${result.substring(0, 80)}...`;
}
// Special cases for proposals that don't follow the standard naming convention
if (result.includes("Moonbeam")) {
result = result.replace("MIP-B", "MIP-M");
}
if (result.indexOf("MIP-B22: Gauntlet") >= 0) {
result = result.replace("MIP-B22", "MIP-B24");
}
if (result.indexOf("MIP-O01: Gauntlet") >= 0) {
result = result.replace("MIP-O01", "MIP-O03");
}
if (result.indexOf("MIP-M02: Upgrade") >= 0) {
result = result.replace("MIP-M02", "MIP-M03");
}
if (result.indexOf("MIP-R02: Upgrade") >= 0) {
result = result.replace("MIP-R02", "MIP-R03");
}
if (result.indexOf("Proposal: Onboard wstETH") >= 0) {
result = result.replace("Proposal:", "MIP-B08");
}
if (result.indexOf("Gauntlet's Moonriver Recommendations (2024-01-09)") >= 0) {
result = result.replace("Gauntlet", "MIP-R10: Gauntlet");
}
return result;
};
export const getProposalData = async (params) => {
if (params.environment.contracts.governor) {
let count = 0n;
let quorum = 0n;
if (params.environment.chainId === moonriver.id) {
[count, quorum] = await Promise.all([
params.environment.contracts.governor.read.proposalCount(),
params.environment.contracts.governor.read.getQuorum(),
]);
}
else {
[count, quorum] = await Promise.all([
params.environment.contracts.governor.read.proposalCount(),
params.environment.contracts.governor.read.quorumVotes(),
]);
}
//Id out of range
if (params.id) {
if (BigInt(params.id) > count) {
return [];
}
}
const ids = params.id
? [BigInt(params.id)]
: Array.from({ length: Number(count) }, (_, i) => count - BigInt(i));
const proposalDataCall = Promise.all(ids.map((id) => params.environment.contracts.governor?.read.proposals([id])));
const proposalStateCall = Promise.all(ids.map((id) => params.environment.contracts.governor?.read.state([id])));
const [proposalsData, proposalsState] = await Promise.all([
proposalDataCall,
proposalStateCall,
]);
const proposals = proposalsData?.map((item, index) => {
const state = proposalsState?.[index];
const [id, proposer, eta, startTimestamp, endTimestamp, startBlock, forVotes, againstVotes, abstainVotes, totalVotes, canceled, executed,] = item;
const proposal = {
chainId: params.environment.chainId,
id: Number(id),
proposalId: Number(id),
proposer,
eta: Number(eta),
startTimestamp: Number(startTimestamp),
endTimestamp: Number(endTimestamp),
startBlock: Number(startBlock),
forVotes: new Amount(forVotes, 18),
againstVotes: new Amount(againstVotes, 18),
abstainVotes: new Amount(abstainVotes, 18),
totalVotes: new Amount(totalVotes, 18),
canceled,
executed,
quorum: new Amount(quorum, 18),
state,
};
return proposal;
});
return proposals;
}
else {
return [];
}
};
export const getCrossChainProposalData = async (params) => {
if (params.environment.contracts.governor) {
const xcGovernanceSettings = params.environment.custom.governance;
if (params.environment.contracts.multichainGovernor &&
xcGovernanceSettings &&
xcGovernanceSettings.chainIds.length > 0) {
const xcEnvironments = xcGovernanceSettings.chainIds
.map((chainId) => Object.values(publicEnvironments).find((env) => env.chainId === chainId))
.filter((xcEnvironment) => !!xcEnvironment)
.filter((xcEnvironment) => xcEnvironment.custom?.wormhole?.chainId &&
xcEnvironment.contracts.voteCollector);
const [xcCount, xcQuorum] = await Promise.all([
params.environment.contracts.multichainGovernor.read.proposalCount(),
params.environment.contracts.multichainGovernor.read.quorum(),
]);
if (params.id) {
//Fix proposal id
params.id =
Number(params.id) -
(params.environment.custom?.governance?.proposalIdOffset || 0);
if (params.id < 0) {
return [];
}
else {
if (BigInt(params.id) > xcCount) {
return [];
}
}
}
const xcIds = params.id
? [BigInt(params.id)]
: Array.from({ length: Number(xcCount) }, (_, i) => xcCount - BigInt(i));
const xcProposalsDataCall = Promise.all(xcIds.map((id) => params.environment.contracts.multichainGovernor?.read.proposals([id])));
const xcProposalsStateCall = Promise.all(xcIds.map((id) => params.environment.contracts.multichainGovernor?.read.state([id])));
const xcProposalsCollectedVotes = xcEnvironments.map((xcEnvironment) => {
return Promise.all(xcIds.map((id) => params.environment.contracts.multichainGovernor.read.chainVoteCollectorVotes([xcEnvironment.custom.wormhole.chainId, id])));
});
const xcProposalsVotes = xcEnvironments.map((xcEnvironment) => {
return Promise.all(xcIds.map((id) => xcEnvironment.contracts.voteCollector.read.proposalVotes([id])));
});
const [xcProposalsData, xcProposalsState, xcCollectorVotes, xcVotes] = await Promise.all([
xcProposalsDataCall,
xcProposalsStateCall,
Promise.all(xcProposalsCollectedVotes),
Promise.all(xcProposalsVotes),
]);
const proposals = xcIds.map((xcId, proposalIndex) => {
const id = Number(xcId) +
(params.environment.custom?.governance?.proposalIdOffset || 0);
const state = xcProposalsState?.[proposalIndex];
const votesCollected = xcCollectorVotes.reduce((prevCollected, currentCollected, i) => {
const [votesFor, votesAgainst, votesAbstain] = currentCollected[proposalIndex];
const collected = votesFor > 0n || votesAgainst > 0n || votesAbstain > 0n;
return i === 0
? collected
: prevCollected === false
? false
: collected;
}, false);
const votes = xcVotes.reduce((prevVotes, currVotes) => {
const [totalVotes, forVotes, againstVotes, abstainVotes] = currVotes[proposalIndex];
return {
totalVotes: prevVotes.totalVotes + totalVotes,
forVotes: prevVotes.forVotes + forVotes,
againstVotes: prevVotes.againstVotes + againstVotes,
abstainVotes: prevVotes.abstainVotes + abstainVotes,
};
}, { totalVotes: 0n, forVotes: 0n, againstVotes: 0n, abstainVotes: 0n });
const [proposer, _voteSnapshotTimestamp, votingStartTime, votingEndTime, crossChainVoteCollectionEndTimestamp, voteSnapshotBlock, forVotes, againstVotes, abstainVotes, totalVotes, canceled, executed,] = xcProposalsData?.[proposalIndex];
const multichainState = MultichainProposalStateMapping[state];
const proposal = {
chainId: params.environment.chainId,
id,
proposalId: Number(xcId),
proposer,
eta: Number(crossChainVoteCollectionEndTimestamp),
startTimestamp: Number(votingStartTime),
endTimestamp: Number(votingEndTime),
startBlock: Number(voteSnapshotBlock),
forVotes: new Amount(forVotes + votes.forVotes, 18),
againstVotes: new Amount(againstVotes + votes.againstVotes, 18),
abstainVotes: new Amount(abstainVotes + votes.abstainVotes, 18),
totalVotes: new Amount(totalVotes + votes.totalVotes, 18),
canceled,
executed,
quorum: new Amount(xcQuorum, 18),
state: multichainState,
multichain: {
id: Number(xcId),
votesCollected,
},
};
return proposal;
});
return proposals;
}
else {
return [];
}
}
else {
return [];
}
};
export const getExtendedProposalData = async (params) => {
let result = [];
let lastId = -1;
let shouldContinue = true;
while (shouldContinue) {
const response = await axios.post(params.environment.governanceIndexerUrl, {
query: `
query {
proposals(
limit: 1000,
orderDirection: "desc",
orderBy: "proposalId",
where: {
chainId: ${params.environment.chainId}
${params.id ? `, proposalId: ${params.id}` : lastId >= 0 ? `, proposalId_lt: ${lastId}` : ""}
}
) {
items {
id
proposalId
description
targets
calldatas
signatures
stateChanges(orderBy: "blockNumber") {
items {
txnHash
blockNumber
newState
chainId
}
}
}
}
}
`,
});
if (response.status === 200 && response.data?.data?.proposals) {
const proposals = response.data.data.proposals.items.map((item) => {
const extendedProposalData = {
id: item.proposalId, //temp fix while ponder is outdated
title: `Proposal #${item.proposalId}`, //temp fix while ponder is outdated
subtitle: extractProposalSubtitle(item.description),
description: item.description,
calldatas: item.calldatas,
signatures: item.signatures,
stateChanges: item.stateChanges?.items?.map((change) => {
return {
blockNumber: change.blockNumber,
state: change.newState,
transactionHash: change.txnHash,
chainId: change.chainId,
};
}) ?? [],
targets: item.targets,
};
if (extendedProposalData.subtitle.includes("Moonbeam")) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("MIP-B", "MIP-M");
}
if (extendedProposalData.subtitle.indexOf("MIP-B22: Gauntlet") >= 0) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("MIP-B22", "MIP-B24");
}
if (extendedProposalData.subtitle.indexOf("MIP-O01: Gauntlet") >= 0) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("MIP-O01", "MIP-O03");
}
if (extendedProposalData.subtitle.indexOf("MIP-M02: Upgrade") >= 0) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("MIP-M02", "MIP-M03");
}
if (extendedProposalData.subtitle.indexOf("MIP-R02: Upgrade") >= 0) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("MIP-R02", "MIP-R03");
}
if (extendedProposalData.subtitle.indexOf("Proposal: Onboard wstETH") >= 0) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("Proposal:", "MIP-B08");
}
if (extendedProposalData.subtitle.indexOf("Gauntlet's Moonriver Recommendations (2024-01-09)") >= 0) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("Gauntlet", "MIP-R10: Gauntlet");
}
if (extendedProposalData.description.substring(0, 9).includes("MIP-MIP")) {
extendedProposalData.description =
extendedProposalData.description.replace("MIP-", "");
}
if (extendedProposalData.subtitle.substring(0, 7).includes("MIP-MIP")) {
extendedProposalData.subtitle = extendedProposalData.subtitle.replace("MIP-", "");
}
return extendedProposalData;
});
if (proposals.length < 1000 || proposals.length === 0) {
shouldContinue = false;
}
else {
lastId = last(proposals).id;
}
result = result.concat(proposals);
}
}
return result;
};
//# sourceMappingURL=common.js.map