Skip to content

Commit

Permalink
feat: moonbeam voting history
Browse files Browse the repository at this point in the history
  • Loading branch information
andremury committed Dec 19, 2023
1 parent 779ac72 commit 69ea0c8
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default Component.extend({

async fetchVotes() {
set(this, "fetched", false);
set(this, "votes", []);
const votes = await VotingHistory.start(this.profile, {
SiteSettings: this.siteSettings,
});
Expand All @@ -42,8 +43,8 @@ export default Component.extend({
async init() {
this._super(...arguments);
this.daoName = this.oldDaoName = window.selectedDao;
const cli = new KarmaApiClient(this.daoName, "");
if (this.session) {
const cli = new KarmaApiClient(this.daoName, "");
try {
const { allowance } = await cli.isApiAllowed(this.session.csrfToken);
set(this, "hasSetApiKey", !!allowance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
Governance Stats
</b>
</div>
{{#if availableDaos.length}}
{{#if (gt availableDaos.length 1)}}
<div id="__dao-select">
{{#each availableDaos as |dao|}}
{{#if (eq daoName dao.name)}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{#if (and siteSettings.Show_proposal_banner shouldShow)}}
{{#if availableDaos.length}}
{{#if (gt availableDaos.length 1)}}
<div id="__dao-select">
{{#each availableDaos as |dao|}}
{{#if (eq daoName dao.name)}}
Expand Down
45 changes: 45 additions & 0 deletions assets/javascripts/lib/karma-api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,51 @@ class KarmaApiClient {
"X-CSRF-Token": csrfToken,
});
}


/**
* @param {import('karma-score').KarmaApiVotesSummaryRes} summary
* @returns {import('karma-score').ParsedProposal[]}
*/
#parseVotingSummary = (summary) => {
console.info('voting summary', summary)
const { proposals, votes } = summary;
const parsedVotes = [];

votes.sort().forEach((vote) => {
const [id, version] = vote.proposalId.split('-');
console.log('id', id, 'version', version)
const proposal = proposals.find(p => p.id === +id && p.version === version);
console.log('proposal', proposal)
if (!proposal) {
return;
}

parsedVotes.push({
title: proposal?.title,
proposalId: proposal.id,
voteMethod: "Off-chain",
proposal: proposal?.title,
choice: vote.reason,
executed: moment(proposal.endDate).format("MMMM D, YYYY"),
});
})

return parsedVotes.sort((a, b) => moment(a.executed).isBefore(moment(b.executed)) ? 1 : -1);
}

/**
* Get voting summary for moonbeam and moonriver ONLY
* @returns {Promise<import("karma-score").KarmaApiVotesSummaryRes>
*/
async fetchVoteSummary() {
console.info('fetching voting summary')
if (!['moonbeam', 'moonriver', 'moonbase'].includes(this.daoName.toLowerCase())) {
return { proposals: [], votes: [] };
}
const url = `${karmaUrl}/delegate/${this.daoName}/${this.publicAddress}/voting-history`.toLowerCase();
return await request(url, null, "GET");
}
}

export default KarmaApiClient;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const subgraphUrl = new URL("https://hub.snapshot.org/graphql");
* Concat proposal and votes into a common interface
* @param proposals
* @param votes
* @returns {import("karma-score").ParsedProposal[])}
*/
function parseVotes(votes = []) {
const array = [];
Expand Down
16 changes: 15 additions & 1 deletion assets/javascripts/lib/voting-history/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { fetchDaoSnapshotAndOnChainIds } from "../fetch-snapshot-onchain-ids";
import KarmaApiClient from "../karma-api-client";
import { fetchOffChainProposalVotes } from "./gql/off-chain-fetcher";
import { fetchOnChainProposalVotes } from "./gql/on-chain-fetcher";
import { moonriverFetcher } from "./moonbeam/moonbeam";
import template from "./template";

const karma = "https://karmahq.xyz/profile";
Expand All @@ -15,6 +17,13 @@ const VotingHistory = {
return 0;
},

/**
*
* @param {*} profile
* @param {*} ctx
* @param {*} wrapperId
* @returns {Promise<import("karma-score").ParsedProposal[])>}
*/
async start(profile, ctx, wrapperId = ".__karma-stats") {
if (!ctx || !ctx.SiteSettings || !profile) {
return;
Expand All @@ -30,10 +39,15 @@ const VotingHistory = {
const daoName = window.selectedDao;
const amount = this.shouldShowVotingHistory(ctx);

if (['moonbeam', 'moonriver', 'moonbase'].includes(daoName.toLowerCase())) {
console.info('voting history for' + daoName)
const votes = await moonriverFetcher(daoName, profile.address);
return votes.slice(0, amount);
}

// TODO fix this workaround by refactoring this code into components
this.daoIds = (await fetchDaoSnapshotAndOnChainIds(daoName));


let onChain = [];
if (this.daoIds.onChain?.length) {
onChain = await fetchOnChainProposalVotes(
Expand Down
140 changes: 140 additions & 0 deletions assets/javascripts/lib/voting-history/moonbeam/moonbeam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import KarmaApiClient from "../../karma-api-client";

const getVoteReason = (vote) => {
if (!vote.reason || typeof vote.reason === 'boolean') return 'Did not vote';
if (vote.reason.toLowerCase() === 'for') return 1;
if (vote.reason.toLowerCase() === 'abstain') return 'ABSTAIN';
return 0;
};

/**
* Concat proposal and votes into a common interface
* @param proposals
* @param votes
*/
function concatOnChainProposals(proposals, votes) {
const array = [];

votes.forEach((vote) => {
const { proposal } = vote;
const original = proposals.find(item => +item.id === +proposal);
array.push({
voteMethod: 'On-chain',
proposal: original?.description || `Proposal ${proposal}`,
choice: getVoteReason(vote),
solution: vote?.solution,
reason: vote?.reason,
executed: moment
.unix(original?.timestamp || Math.round(Date.now() / 1000))
.format('MMMM D, YYYY'),
executedTimestamp: original?.timestamp || Math.round(Date.now() / 1000),
voteId: proposal,
trackId: Number(original?.trackId),
version: original?.version,
});
});

proposals.forEach(proposal => {
if (!array.find(item => item.voteId && +item.voteId === +proposal.id))
array.push({
voteMethod: 'On-chain',
proposal: proposal.description,
choice: -1,
solution: null,
executed: moment.unix(proposal.timestamp).format('MMMM D, YYYY'),
executedTimestamp: proposal.timestamp,
voteId: proposal.id.toString(),
finished: proposal.finished,
trackId: Number(proposal?.trackId),
version: proposal?.version,
});
});

return array.sort((a, b) => b.executedTimestamp - a.executedTimestamp);
}

async function proposalsWithMetadata(daoName) {
console.log('proposals with metadata')
const url = `https://dapp.karmahq.xyz/api/proposals?dao=${daoName?.toLowerCase()}&source=on-chain`;
const data = await fetch(url, {
method: "GET",
}).then(async (res) => await res.json());
console.info('cu de saco', data)
return data;
}

async function getDaoProposals(
cachedProposals = [],
daoName = 'moonbeam'
) {
const proposals = await proposalsWithMetadata(daoName);
const proposalsMap = proposals.map(proposal => {
const status = Object.entries(proposal.information)[0];
const matchedProposal = cachedProposals.find(
pr =>
+pr.id === +proposal.proposalId &&
(proposal.trackId === null) === (pr.version === 'V1')
);
const timestamp =
(cachedProposals.find(
pr =>
+pr.id === +proposal.proposalId &&
(proposal.trackId === null) === (pr.version === 'V1')
)?.startDate || 0) / 1000;

return {
proposal: proposal.proposalId,
id: `${proposal.proposalId}`,
description:
proposal.proposal || `Proposal ${proposal.proposalId.toString()}`,
timestamp: Math.round(timestamp),
trackId: proposal.trackId,
finished: !status ? true : status[0] !== 'ongoing',
version: matchedProposal?.version,
};
});

// eslint-disable-next-line id-length
return proposalsMap.sort((a, b) => b.timestamp - a.timestamp);
}

async function fetchOnChainVotes(daoName, address) {
if (!daoName || !address) return [];
try {
daoName = [daoName].flat()[0]
const cli = new KarmaApiClient([daoName].flat()[0], address);
const { votes, proposals: cachedProposals } = await cli.fetchVoteSummary();
console.info('deu n vote', votes, cachedProposals)

const voteList = votes.map(vote => ({
proposal: vote.proposalId.split('-')[0],
openGov: vote.proposalId.split('-')[1] === 'V2',
reason: vote.reason,
}));
if (voteList && Array.isArray(voteList)) {
const proposals = await getDaoProposals(cachedProposals, daoName);

return concatOnChainProposals(proposals, voteList);
}
} catch (error) {
console.info(error)
return [];
}
return [];
}

export async function moonriverFetcher(
daoName,
address
) {
console.info('fetching moonriver')
try {
const votes = await fetchOnChainVotes(daoName, address);
console.info('deu vote', votes)
return votes;
} catch (error) {
console.info('deu error')
console.info(error)
return [];
}
}
2 changes: 1 addition & 1 deletion assets/javascripts/lib/voting-history/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function getIcon(choice = "not vote") {
return voteIcon.empty;
}
if (
choice.toLocaleLowerCase().substring(0, 2) === "no" ||
choice?.toLowerCase?.().substring(0, 2) === "no" ||
/agai+nst/gi.test(choice)
) {
return voteIcon.no;
Expand Down
22 changes: 22 additions & 0 deletions spec/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,26 @@ declare module "karma-score" {
event: string;
properties: Record<string, unknown>;
}

declare interface KarmaApiVotesSummaryRes {
proposals: {
id: number;
version: "V1" | "V2";
endDate: number;
startDate: number;
}[];
votes: {
proposalId: string;
reason: string;
}[];
}

declare interface ParsedProposal {
title: string;
proposalId: string;
voteMethod: string;
proposal: string;
choice: string | number;
executed: string;
}
}

0 comments on commit 69ea0c8

Please sign in to comment.