From 7bb01617c3bf629fed11c59742dfd31f54a65a22 Mon Sep 17 00:00:00 2001 From: Tom Beynon Date: Sat, 25 Nov 2023 00:40:58 +0000 Subject: [PATCH] Get proposal content from ipfs and show message content --- src/components/App.css | 12 +++- src/components/ProposalDetails.js | 64 ++++++++++++------ src/components/ProposalMessages.js | 101 +++++++++++++++++++++++++++++ src/components/Voting.js | 15 +++-- src/utils/Network.mjs | 11 ++++ src/utils/Proposal.mjs | 32 ++++++--- 6 files changed, 199 insertions(+), 36 deletions(-) create mode 100644 src/components/ProposalMessages.js diff --git a/src/components/App.css b/src/components/App.css index 23dfe3ba..776bf2d5 100644 --- a/src/components/App.css +++ b/src/components/App.css @@ -50,7 +50,7 @@ code { code p{ margin-bottom: 0.5rem; } - + .favourite-on .on, .favourite-off .off { display: block !important; @@ -71,4 +71,12 @@ code p{ .favourite-off:hover .off { display: none !important; } -} \ No newline at end of file +} + +pre.pre-wrap { + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + } diff --git a/src/components/ProposalDetails.js b/src/components/ProposalDetails.js index f622890b..5e2939da 100644 --- a/src/components/ProposalDetails.js +++ b/src/components/ProposalDetails.js @@ -5,10 +5,13 @@ import remarkGfm from 'remark-gfm' import { Table, + Tab, + Nav } from 'react-bootstrap' import Coins from './Coins'; import ProposalProgress from './ProposalProgress'; +import ProposalMessages from './ProposalMessages'; import VoteForm from './VoteForm'; import AlertMessage from './AlertMessage'; import Vote from '../utils/Vote.mjs'; @@ -156,26 +159,47 @@ function ProposalDetails(props) { tally={tally} height={25} /> -
-
-
{title}
- - }} - /> - - + + + + +
+
+
{title}
+
+ }} + /> + + + + +
+
+ +
+
+
+ + ) } diff --git a/src/components/ProposalMessages.js b/src/components/ProposalMessages.js new file mode 100644 index 00000000..b7ac9511 --- /dev/null +++ b/src/components/ProposalMessages.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { + Table, +} from 'react-bootstrap' +import moment from 'moment' +import _ from 'lodash' +import Moment from 'react-moment'; +import Coins from './Coins'; + +function ProposalMessages(props) { + const { proposal, network } = props + + const { content, messages } = proposal + + let contentMessages = [] + + if(messages){ + messages.forEach(message => { + if(message['@type'] === '/cosmos.gov.v1.MsgExecLegacyContent'){ + contentMessages.push(message.content) + }else{ + contentMessages.push(message) + } + }) + }else{ + contentMessages.push(content) + } + + function messageData(message){ + const data = _.omit(message, 'title', 'name', '@type', 'description') + switch (message['@type']) { + case '/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal': + return { + name: message.plan.name, + height: message.plan.height, + estimated_time: () => { + return ( + + {moment().add(network.timeToBlock(message.plan.height), 'seconds')} + + ) + } + } + case '/cosmos.distribution.v1beta1.CommunityPoolSpendProposal': + return { + ...data, + amount: () => { + return data.amount.map(coin => { + return + }) + } + } + default: + return _.mapValues(data, value => { + if(typeof value === 'object' && value !== null){ + return
{JSON.stringify(value, undefined, 2)}
+ }else if(typeof value == "boolean"){ + return value ? 'true' : 'false' + }else{ + return value + } + }) + } + } + + function renderMessage(message){ + const data = messageData(message) + return ( + <> +
{message.title || message.name} {message['@type']}
+
+ + {Object.entries(data).map(([key, value]) => { + return ( + + + + + ) + })} + +
{_.startCase(key)}{typeof value === 'function' ? value() : value}
+ + ) + } + + return ( + <> + {contentMessages.map((message, index) => { + return ( +
+ {index > 0 &&
} + {renderMessage(message)} +
+ ) + })} + + ) +} + +export default ProposalMessages diff --git a/src/components/Voting.js b/src/components/Voting.js index 2a1d70e4..43f68258 100644 --- a/src/components/Voting.js +++ b/src/components/Voting.js @@ -8,7 +8,7 @@ import { useParams, useNavigate } from "react-router-dom"; import AlertMessage from './AlertMessage'; import Proposals from './Proposals'; -import { executeSync } from '../utils/Helpers.mjs'; +import { executeSync, mapSync } from '../utils/Helpers.mjs'; import ProposalModal from './ProposalModal'; import Proposal from '../utils/Proposal.mjs'; import Vote from '../utils/Vote.mjs'; @@ -31,7 +31,7 @@ function Voting(props) { const params = useParams(); const voteGrants = (wallet?.grants || []).filter(grant => { - return grant.authorization['@type'] === '/cosmos.authz.v1beta1.GenericAuthorization' && + return grant.authorization['@type'] === '/cosmos.authz.v1beta1.GenericAuthorization' && grant.authorization.msg === '/cosmos.gov.v1beta1.MsgVote' }) @@ -84,7 +84,12 @@ function Voting(props) { try { let newProposals = await props.queryClient.getProposals() - newProposals = newProposals.map(el => Proposal(el)) + newProposals = await mapSync(newProposals.map(el => { + return async () => { + return await Proposal(el) + } + })) + setError() setProposals(sortProposals(newProposals)) setTallies(newProposals.reduce((sum, proposal) => { @@ -94,7 +99,7 @@ function Voting(props) { return sum }, {})) } catch (error) { - if (!proposals || clearExisting) setProposals([]) + if (clearExisting) setProposals([]) setError(`Failed to load proposals: ${error.message}`); } } @@ -211,4 +216,4 @@ function Voting(props) { ); } -export default Voting; \ No newline at end of file +export default Voting; diff --git a/src/utils/Network.mjs b/src/utils/Network.mjs index 859dc684..49727e6e 100644 --- a/src/utils/Network.mjs +++ b/src/utils/Network.mjs @@ -183,6 +183,17 @@ class Network { this.authzAminoSupport && 'full authz ledger', ]) } + + timeToBlock(height){ + const params = this.chain.params + const currentHeight = params.current_block_height + const blockTime = params.actual_block_time + return (height - currentHeight) * blockTime + } + + assetForDenom(denom){ + return this.assets.find(el => el.base.denom === denom) + } } export default Network; diff --git a/src/utils/Proposal.mjs b/src/utils/Proposal.mjs index d6cf222d..628a725b 100644 --- a/src/utils/Proposal.mjs +++ b/src/utils/Proposal.mjs @@ -1,3 +1,5 @@ +import axios from 'axios' + export const PROPOSAL_STATUSES = { '': 'All', 'PROPOSAL_STATUS_DEPOSIT_PERIOD': 'Deposit Period', @@ -13,18 +15,32 @@ export const PROPOSAL_SCAM_URLS = [ 'cosmos-network.io' ] -const Proposal = (data) => { - let { proposal_id, content, messages, metadata } = data +const Proposal = async (data) => { + let { proposal_id, content, messages, metadata, title, summary: description } = data if(!proposal_id && data.id) proposal_id = data.id - let title, description, typeHuman + let typeHuman if(metadata){ try { metadata = JSON.parse(metadata) - title = metadata.title - description = metadata.summary - } catch (e) { - console.log(e) + title = title || metadata.title + description = description || metadata.summary + } catch { + try { + let ipfsUrl + if(metadata.startsWith('ipfs://')){ + ipfsUrl = metadata.replace("ipfs://", "https://ipfs.io/ipfs/") + }else if(metadata.startsWith('https://')){ + ipfsUrl = metadata + }else{ + ipfsUrl = `https://ipfs.io/ipfs/${metadata}` + } + metadata = await axios.get(ipfsUrl).then(res => res.data) + title = metadata.title + description = metadata.summary || metadata.description + } catch (e) { + console.log(e) + } } } @@ -43,7 +59,6 @@ const Proposal = (data) => { title = title || typeHuman description = description || metadata - const statusHuman = PROPOSAL_STATUSES[data.status] const isDeposit = data.status === 'PROPOSAL_STATUS_DEPOSIT_PERIOD' @@ -59,7 +74,6 @@ const Proposal = (data) => { description, content, metadata, - messages, isDeposit, isVoting, isSpam