diff --git a/assets/javascripts/discourse/components/profile-summary-votes.js b/assets/javascripts/discourse/components/profile-summary-votes.js index c1bcad0..72c21e5 100644 --- a/assets/javascripts/discourse/components/profile-summary-votes.js +++ b/assets/javascripts/discourse/components/profile-summary-votes.js @@ -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, }); @@ -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); diff --git a/assets/javascripts/discourse/templates/components/karma-stats.hbs b/assets/javascripts/discourse/templates/components/karma-stats.hbs index 84df85e..6dd9e0d 100644 --- a/assets/javascripts/discourse/templates/components/karma-stats.hbs +++ b/assets/javascripts/discourse/templates/components/karma-stats.hbs @@ -22,7 +22,7 @@ Governance Stats - {{#if availableDaos.length}} + {{#if (gt availableDaos.length 1)}}
{{#each availableDaos as |dao|}} {{#if (eq daoName dao.name)}} diff --git a/assets/javascripts/discourse/templates/components/proposal-banner.hbs b/assets/javascripts/discourse/templates/components/proposal-banner.hbs index 716c25a..bb7a70d 100644 --- a/assets/javascripts/discourse/templates/components/proposal-banner.hbs +++ b/assets/javascripts/discourse/templates/components/proposal-banner.hbs @@ -1,5 +1,5 @@ {{#if (and siteSettings.Show_proposal_banner shouldShow)}} - {{#if availableDaos.length}} + {{#if (gt availableDaos.length 1)}}
{{#each availableDaos as |dao|}} {{#if (eq daoName dao.name)}} diff --git a/assets/javascripts/lib/karma-api-client.js b/assets/javascripts/lib/karma-api-client.js index 7d6a6a0..e9b0c49 100644 --- a/assets/javascripts/lib/karma-api-client.js +++ b/assets/javascripts/lib/karma-api-client.js @@ -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 + */ + 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; diff --git a/assets/javascripts/lib/voting-history/gql/off-chain-fetcher.js b/assets/javascripts/lib/voting-history/gql/off-chain-fetcher.js index 868bd8e..720ff0d 100644 --- a/assets/javascripts/lib/voting-history/gql/off-chain-fetcher.js +++ b/assets/javascripts/lib/voting-history/gql/off-chain-fetcher.js @@ -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 = []; diff --git a/assets/javascripts/lib/voting-history/index.js b/assets/javascripts/lib/voting-history/index.js index 764bab3..0c1fbc6 100644 --- a/assets/javascripts/lib/voting-history/index.js +++ b/assets/javascripts/lib/voting-history/index.js @@ -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"; @@ -15,6 +17,13 @@ const VotingHistory = { return 0; }, + /** + * + * @param {*} profile + * @param {*} ctx + * @param {*} wrapperId + * @returns {Promise} + */ async start(profile, ctx, wrapperId = ".__karma-stats") { if (!ctx || !ctx.SiteSettings || !profile) { return; @@ -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( diff --git a/assets/javascripts/lib/voting-history/moonbeam/moonbeam.js b/assets/javascripts/lib/voting-history/moonbeam/moonbeam.js new file mode 100644 index 0000000..5262c57 --- /dev/null +++ b/assets/javascripts/lib/voting-history/moonbeam/moonbeam.js @@ -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 []; + } +} diff --git a/assets/javascripts/lib/voting-history/template.js b/assets/javascripts/lib/voting-history/template.js index 8a91e8a..cba3a76 100644 --- a/assets/javascripts/lib/voting-history/template.js +++ b/assets/javascripts/lib/voting-history/template.js @@ -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; diff --git a/spec/types.d.ts b/spec/types.d.ts index f140eae..c53140e 100644 --- a/spec/types.d.ts +++ b/spec/types.d.ts @@ -42,4 +42,26 @@ declare module "karma-score" { event: string; properties: Record; } + + 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; + } }