From 4a044ab30c7d38a61fd541c03a30715eec6427c8 Mon Sep 17 00:00:00 2001 From: chaojun Date: Tue, 3 Sep 2024 17:10:51 +0800 Subject: [PATCH] Refactor society proposal vote, #1034 --- .../src/features/proposals/createProposal.js | 7 +- .../proposals/saveProposalSettings.js | 2 +- .../backend/src/features/proposals/vote.js | 80 ++++++++++++-- .../services/node.service/getSocietyMember.js | 11 ++ .../proposal.service/createSocietyProposal.js | 13 --- .../src/services/proposal.service/index.js | 2 + .../src/services/proposal.service/vote.js | 95 +++++------------ .../proposal.service/voteSocietyProposal.js | 100 ++++++++++++++++++ .../src/features/society/getSocietyMember.js | 34 ++++++ .../src/features/society/getSocietyMembers.js | 3 +- .../node-api/src/features/society/routes.js | 5 + next/components/listInfo/details.js | 25 +++-- next/components/listInfo/index.js | 25 +++-- next/components/postCreate/index.js | 2 +- next/components/valueDisplay.js | 2 +- 15 files changed, 288 insertions(+), 118 deletions(-) create mode 100644 backend/packages/backend/src/services/node.service/getSocietyMember.js create mode 100644 backend/packages/backend/src/services/proposal.service/voteSocietyProposal.js create mode 100644 backend/packages/node-api/src/features/society/getSocietyMember.js diff --git a/backend/packages/backend/src/features/proposals/createProposal.js b/backend/packages/backend/src/features/proposals/createProposal.js index 23935023..86d867f3 100644 --- a/backend/packages/backend/src/features/proposals/createProposal.js +++ b/backend/packages/backend/src/features/proposals/createProposal.js @@ -137,7 +137,12 @@ function checkNetworkConfig(data) { const spaceService = spaceServices[space]; if ( !isEqual(networksConfig, { - ...pick(spaceService, ["symbol", "decimals", "networks"]), + ...pick(spaceService, [ + "symbol", + "decimals", + "networks", + "accessibility", + ]), strategies: spaceService.weightStrategy, ...pick(spaceService, ["quorum", "version"]), }) diff --git a/backend/packages/backend/src/features/proposals/saveProposalSettings.js b/backend/packages/backend/src/features/proposals/saveProposalSettings.js index fae027ac..f71af70e 100644 --- a/backend/packages/backend/src/features/proposals/saveProposalSettings.js +++ b/backend/packages/backend/src/features/proposals/saveProposalSettings.js @@ -1,7 +1,7 @@ const { HttpError } = require("../../exc"); const { getProposalTemplateCollection } = require("../../mongo"); const { isAdmin } = require("../../utils/admin"); -const { checkProposalContent } = require("./proposal.controller"); +const { checkProposalContent } = require("./checkProposalContent"); async function saveProposalSettings(ctx) { const { data, address } = ctx.request.body; diff --git a/backend/packages/backend/src/features/proposals/vote.js b/backend/packages/backend/src/features/proposals/vote.js index c171a801..38347d04 100644 --- a/backend/packages/backend/src/features/proposals/vote.js +++ b/backend/packages/backend/src/features/proposals/vote.js @@ -1,14 +1,11 @@ +const { ChoiceType } = require("../../constants"); +const { Accessibility } = require("../../consts/space"); const { HttpError } = require("../../exc"); +const { getProposalCollection } = require("../../mongo"); const proposalService = require("../../services/proposal.service"); +const { spaces: spaceServices } = require("../../spaces"); -async function vote(ctx) { - const { data, address, signature } = ctx.request.body; - const { proposalCid, choices, remark, realVoter, voterNetwork } = data; - - if (!proposalCid) { - throw new HttpError(400, { proposalCid: ["Proposal CID is missing"] }); - } - +function checkVoteChoices(choices, proposal) { if (!choices) { throw new HttpError(400, { choices: ["Choices is missing"] }); } @@ -23,10 +20,77 @@ async function vote(ctx) { }); } + if (proposal.choiceType === ChoiceType.Single && choices.length !== 1) { + throw new HttpError(400, "Can vote single choice only"); + } + + for (const choice of choices) { + if (!proposal.choices?.includes(choice)) { + throw new HttpError(400, `Invalid choice: ${choice}`); + } + } +} + +function checkProposal(proposal) { + const now = new Date(); + + if (proposal.startDate > now.getTime()) { + throw new HttpError(400, "The voting is not started yet"); + } + + if (proposal.endDate < now.getTime()) { + throw new HttpError(400, "The voting had already ended"); + } +} + +function checkVoterNetwork(voterNetwork, proposal) { if (!voterNetwork) { throw new HttpError(400, { voterNetwork: ["Voter network is missing"] }); } + const space = proposal.space; + const spaceService = spaceServices[space]; + if (!spaceService) { + throw new HttpError(500, "Unknown space"); + } + + const snapshotNetworks = Object.keys(proposal.snapshotHeights); + if (!snapshotNetworks.includes(voterNetwork)) { + throw new HttpError(400, "Voter network is not supported by this proposal"); + } +} + +async function vote(ctx) { + const { data, address, signature } = ctx.request.body; + const { proposalCid, choices, remark, realVoter, voterNetwork } = data; + + if (!proposalCid) { + throw new HttpError(400, { proposalCid: ["Proposal CID is missing"] }); + } + const proposalCol = await getProposalCollection(); + const proposal = await proposalCol.findOne({ cid: proposalCid }); + if (!proposal) { + throw new HttpError(400, "Proposal not found."); + } + + checkProposal(proposal); + checkVoteChoices(choices, proposal); + checkVoterNetwork(voterNetwork, proposal); + + if (proposal.networksConfig.accessibility === Accessibility.SOCIETY) { + ctx.body = await proposalService.voteSocietyProposal( + proposalCid, + choices, + remark, + realVoter, + data, + address, + voterNetwork, + signature, + ); + return; + } + ctx.body = await proposalService.vote( proposalCid, choices, diff --git a/backend/packages/backend/src/services/node.service/getSocietyMember.js b/backend/packages/backend/src/services/node.service/getSocietyMember.js new file mode 100644 index 00000000..8de284b2 --- /dev/null +++ b/backend/packages/backend/src/services/node.service/getSocietyMember.js @@ -0,0 +1,11 @@ +const { NODE_API_ENDPOINT } = require("../../env"); +const { fetchApi } = require("../../utils/fech.api"); + +async function getSocietyMember(network, address, height) { + const url = `${NODE_API_ENDPOINT}/${network}/society/members/${address}/height/${height}`; + return await fetchApi(url); +} + +module.exports = { + getSocietyMember, +}; diff --git a/backend/packages/backend/src/services/proposal.service/createSocietyProposal.js b/backend/packages/backend/src/services/proposal.service/createSocietyProposal.js index 8f5839da..5bfedf20 100644 --- a/backend/packages/backend/src/services/proposal.service/createSocietyProposal.js +++ b/backend/packages/backend/src/services/proposal.service/createSocietyProposal.js @@ -4,11 +4,8 @@ const { nextPostUid } = require("../status.service"); const { getProposalCollection } = require("../../mongo"); const { HttpError } = require("../../exc"); const { ContentType } = require("../../constants"); -const { getLatestHeight } = require("../chain.service"); const { spaces: spaceServices } = require("../../spaces"); -const { checkDelegation } = require("../../services/node.service"); const { pinData, createSpaceNotifications } = require("./common"); -const { isAdmin } = require("../../utils/admin"); async function createSocietyProposal({ space, @@ -29,18 +26,8 @@ async function createSocietyProposal({ signature, }) { const spaceService = spaceServices[space]; - if (spaceService.onlyAdminCanCreateProposals && !isAdmin(space, address)) { - throw new HttpError(401, `Only the space admins can create proposals`); - } - const weightStrategy = spaceService.weightStrategy; - const lastHeight = await getLatestHeight(proposerNetwork); - - if (realProposer && realProposer !== address) { - await checkDelegation(proposerNetwork, address, realProposer, lastHeight); - } - const proposer = realProposer || address; const { cid, pinHash } = await pinData({ data, address, signature }); diff --git a/backend/packages/backend/src/services/proposal.service/index.js b/backend/packages/backend/src/services/proposal.service/index.js index 91ec170f..b0ab6667 100644 --- a/backend/packages/backend/src/services/proposal.service/index.js +++ b/backend/packages/backend/src/services/proposal.service/index.js @@ -5,6 +5,7 @@ const { getProposalById } = require("./getProposalById"); const { postComment } = require("./postComment"); const { getComments } = require("./getComments"); const { vote } = require("./vote"); +const { voteSocietyProposal } = require("./voteSocietyProposal"); const { getVotes } = require("./getVotes"); const { getAddressVote } = require("./getAddressVote"); const { getStats } = require("./getStats"); @@ -19,6 +20,7 @@ module.exports = { postComment, getComments, vote, + voteSocietyProposal, getVotes, getAddressVote, getStats, diff --git a/backend/packages/backend/src/services/proposal.service/vote.js b/backend/packages/backend/src/services/proposal.service/vote.js index cf945c2c..44374ec1 100644 --- a/backend/packages/backend/src/services/proposal.service/vote.js +++ b/backend/packages/backend/src/services/proposal.service/vote.js @@ -7,17 +7,14 @@ const { getSpaceCollection, } = require("../../mongo"); const { HttpError } = require("../../exc"); -const { spaces: spaceServices } = require("../../spaces"); const { checkDelegation } = require("../../services/node.service"); const { toDecimal128 } = require("../../utils"); const { getBalanceFromNetwork } = require("../../services/node.service"); -const { ChoiceType } = require("../../constants"); const { pinData } = require("./common"); const { getBeenDelegated } = require("../node.service/getBeenDelegated"); const { adaptBalance } = require("../../utils/balance"); const { getDemocracyDelegated } = require("../node.service/getDelegated"); const { findDelegationStrategies } = require("../../utils/delegation"); -const { getSocietyMembers } = require("../node.service/getSocietyMembers"); async function getDelegatorBalances({ proposal, @@ -167,21 +164,33 @@ async function checkVoterDelegation({ } } -async function checkWhoCanVote({ proposal, voterNetwork, address, realVoter }) { - const snapshotHeight = proposal.snapshotHeights?.[voterNetwork]; - const voter = realVoter || address; +async function checkVoteThreshold({ + networkBalanceDetails, + proposal, + voterNetwork, +}) { + let networksConfig = null; + + if (proposal.networksConfig?.version === "4") { + networksConfig = proposal.networksConfig; + } else { + const spaceCol = await getSpaceCollection(); + const spaceConfig = await spaceCol.findOne({ id: proposal.space }); + networksConfig = spaceConfig; + } - const networkCfg = proposal.networksConfig.networks?.find( - (networkCfg) => networkCfg.network === voterNetwork, + const networkConfig = networksConfig?.networks?.find( + (item) => item.network === voterNetwork, ); - if (networkCfg && networkCfg.whoCanVote === "societyMember") { - const societyMembers = await getSocietyMembers( - voterNetwork, - snapshotHeight, + if (networkConfig?.assets) { + const passThreshold = networkConfig?.assets?.some((item) => + networkBalanceDetails?.some((balance) => + new BigNumber(item.votingThreshold || 0).lte(balance.balance), + ), ); - const item = societyMembers.find((item) => item.address === voter); - if (!item) { - throw new HttpError(400, "Cannot vote"); + + if (!passThreshold) { + throw new HttpError(400, "You don't have enough balance to vote"); } } } @@ -202,37 +211,8 @@ async function vote( throw new HttpError(400, "Proposal not found."); } - if (proposal.choiceType === ChoiceType.Single && choices.length !== 1) { - throw new HttpError(400, "Can vote single choice only"); - } - - for (const choice of choices) { - if (!proposal.choices?.includes(choice)) { - throw new HttpError(400, `Invalid choice: ${choice}`); - } - } - const now = new Date(); - if (proposal.startDate > now.getTime()) { - throw new HttpError(400, "The voting is not started yet"); - } - - if (proposal.endDate < now.getTime()) { - throw new HttpError(400, "The voting had already ended"); - } - - const space = proposal.space; - const spaceService = spaceServices[space]; - if (!spaceService) { - throw new HttpError(500, "Unknown space"); - } - - const snapshotNetworks = Object.keys(proposal.snapshotHeights); - if (!snapshotNetworks.includes(voterNetwork)) { - throw new HttpError(400, "Voter network is not supported by this proposal"); - } - await checkVoterDelegation({ proposal, voterNetwork, @@ -243,8 +223,6 @@ async function vote( const snapshotHeight = proposal.snapshotHeights?.[voterNetwork]; const voter = realVoter || address; - await checkWhoCanVote({ proposal, voterNetwork, address, realVoter }); - const networkBalance = await getBalanceFromNetwork({ networksConfig: proposal.networksConfig, networkName: voterNetwork, @@ -255,30 +233,7 @@ async function vote( const balanceOf = networkBalance?.balanceOf; const networkBalanceDetails = networkBalance?.details; - let networksConfig = null; - - if (proposal.networksConfig?.version === "4") { - networksConfig = proposal.networksConfig; - } else { - const spaceCol = await getSpaceCollection(); - const spaceConfig = await spaceCol.findOne({ id: proposal.space }); - networksConfig = spaceConfig; - } - - const networkConfig = networksConfig?.networks?.find( - (item) => item.network === voterNetwork, - ); - if (networkConfig?.assets) { - const passThreshold = networkConfig?.assets?.some((item) => - networkBalanceDetails?.some((balance) => - new BigNumber(item.votingThreshold || 0).lte(balance.balance), - ), - ); - - if (!passThreshold) { - throw new HttpError(400, "You don't have enough balance to vote"); - } - } + await checkVoteThreshold({ networkBalanceDetails, proposal, voterNetwork }); const delegators = await getDelegatorBalances({ proposal, diff --git a/backend/packages/backend/src/services/proposal.service/voteSocietyProposal.js b/backend/packages/backend/src/services/proposal.service/voteSocietyProposal.js new file mode 100644 index 00000000..ce374245 --- /dev/null +++ b/backend/packages/backend/src/services/proposal.service/voteSocietyProposal.js @@ -0,0 +1,100 @@ +const { getProposalCollection, getVoteCollection } = require("../../mongo"); +const { HttpError } = require("../../exc"); +const { toDecimal128 } = require("../../utils"); +const { pinData } = require("./common"); +const { getSocietyMember } = require("../node.service/getSocietyMember"); + +async function checkCanVote({ proposal, voterNetwork, address, realVoter }) { + const snapshotHeight = proposal.snapshotHeights?.[voterNetwork]; + const voter = realVoter || address; + + const societyMember = await getSocietyMember( + voterNetwork, + voter, + snapshotHeight, + ); + if (!societyMember.data) { + throw new HttpError(400, "You are not the society member"); + } +} + +async function voteSocietyProposal( + proposalCid, + choices, + remark, + realVoter, + data, + address, + voterNetwork, + signature, +) { + const proposalCol = await getProposalCollection(); + const proposal = await proposalCol.findOne({ cid: proposalCid }); + if (!proposal) { + throw new HttpError(400, "Proposal not found."); + } + + const now = new Date(); + + await checkCanVote({ proposal, voterNetwork, address, realVoter }); + + const voter = realVoter || address; + + const { cid, pinHash } = await pinData({ + data, + address, + signature, + }); + + const voteCol = await getVoteCollection(); + + await voteCol.updateOne( + { + proposal: proposal._id, + voter, + voterNetwork, + }, + { + $set: { + choices, + remark, + data, + address, + signature, + updatedAt: now, + cid, + pinHash, + weights: { + balanceOf: toDecimal128("0"), + details: [], + }, + // Version 2: multiple network space support + // Version 3: multiple choices support + // Version 4: multi-assets network + version: "4", + // delegators, + }, + $setOnInsert: { + createdAt: now, + }, + }, + { upsert: true }, + ); + + await proposalCol.updateOne( + { cid: proposalCid }, + { + $set: { + lastActivityAt: new Date(), + }, + }, + ); + + return { + success: true, + }; +} + +module.exports = { + voteSocietyProposal, +}; diff --git a/backend/packages/node-api/src/features/society/getSocietyMember.js b/backend/packages/node-api/src/features/society/getSocietyMember.js new file mode 100644 index 00000000..63ac962e --- /dev/null +++ b/backend/packages/node-api/src/features/society/getSocietyMember.js @@ -0,0 +1,34 @@ +const { getApis, getBlockApi } = require("@osn/polkadot-api-container"); +const { chains } = require("../../constants"); + +async function getSocietyMemberFromOneApi(api, address, blockHashOrHeight) { + let blockApi = await getBlockApi(api, blockHashOrHeight); + const societyMember = await blockApi.query.society.members(address); + const data = societyMember.toJSON(); + return { + data, + }; +} + +async function getSocietyMemberFromApis(apis, address, blockHashOrHeight) { + const promises = []; + for (const api of apis) { + promises.push(getSocietyMemberFromOneApi(api, address, blockHashOrHeight)); + } + + return await Promise.any(promises); +} + +async function getSocietyMember(ctx) { + const { chain, address, blockHashOrHeight } = ctx.params; + if (![chains.kusama].includes(chain)) { + ctx.throw(400, `Not support chain ${chain}`); + } + + const apis = getApis(chain); + ctx.body = await getSocietyMemberFromApis(apis, address, blockHashOrHeight); +} + +module.exports = { + getSocietyMember, +}; diff --git a/backend/packages/node-api/src/features/society/getSocietyMembers.js b/backend/packages/node-api/src/features/society/getSocietyMembers.js index fc91f204..b18191f6 100644 --- a/backend/packages/node-api/src/features/society/getSocietyMembers.js +++ b/backend/packages/node-api/src/features/society/getSocietyMembers.js @@ -25,8 +25,7 @@ async function getSocietyMembersFromApis(apis, blockHashOrHeight) { } async function getSocietyMembers(ctx) { - const { chain } = ctx.params; - const { block: blockHashOrHeight } = ctx.query; + const { chain, blockHashOrHeight } = ctx.params; if (![chains.kusama].includes(chain)) { ctx.throw(400, `Not support chain ${chain}`); } diff --git a/backend/packages/node-api/src/features/society/routes.js b/backend/packages/node-api/src/features/society/routes.js index 49209b0e..fe17fc1b 100644 --- a/backend/packages/node-api/src/features/society/routes.js +++ b/backend/packages/node-api/src/features/society/routes.js @@ -1,7 +1,12 @@ const Router = require("koa-router"); const { getSocietyMembers } = require("./getSocietyMembers"); +const { getSocietyMember } = require("./getSocietyMember"); const router = new Router(); +router.get( + "/society/members/:address/height/:blockHashOrHeight?", + getSocietyMember, +); router.get("/society/members/height/:blockHashOrHeight?", getSocietyMembers); module.exports = router; diff --git a/next/components/listInfo/details.js b/next/components/listInfo/details.js index a5a03eed..dbefd079 100644 --- a/next/components/listInfo/details.js +++ b/next/components/listInfo/details.js @@ -72,6 +72,7 @@ const DetailsValue = styled(FlexBetween)` export default function Details({ space }) { const strategyCount = space.weightStrategy?.length || 0; const assets = getSpaceAssets(space); + const isSociety = space.accessibility === "society"; return ( @@ -91,19 +92,21 @@ export default function Details({ space }) { - - Config - - Threshold - - - {space.quorum && ( + {!isSociety && ( + + Config - Quorum - + Threshold + - )} - + {space.quorum && ( + + Quorum + + + )} + + )} Strategies({strategyCount}) diff --git a/next/components/listInfo/index.js b/next/components/listInfo/index.js index 7a4da5e1..98ef4186 100644 --- a/next/components/listInfo/index.js +++ b/next/components/listInfo/index.js @@ -88,6 +88,7 @@ export default function ListInfo({ space }) { const strategyCount = space.weightStrategy?.length || 0; const networkCount = space.networks?.length || 0; + const isSociety = space.accessibility === "society"; const handleShowModal = () => { setModalOpen(true); @@ -123,16 +124,20 @@ export default function ListInfo({ space }) { - - -
- Threshold - - - -
-
- + {!isSociety && ( + <> + + +
+ Threshold + + + +
+
+ + + )}
diff --git a/next/components/postCreate/index.js b/next/components/postCreate/index.js index ecde41c4..8ce6f70f 100644 --- a/next/components/postCreate/index.js +++ b/next/components/postCreate/index.js @@ -206,7 +206,7 @@ export default function PostCreate({ space, settings }) { const proposal = { space: space.id, networksConfig: { - ...pick(space, ["symbol", "decimals", "networks"]), + ...pick(space, ["symbol", "decimals", "networks", "accessibility"]), strategies: space.weightStrategy, ...pick(space, ["quorum", "version"]), }, diff --git a/next/components/valueDisplay.js b/next/components/valueDisplay.js index 3168c239..30683de0 100644 --- a/next/components/valueDisplay.js +++ b/next/components/valueDisplay.js @@ -13,7 +13,7 @@ import { */ export default function ValueDisplay({ - value, + value = 0, space, showAEM = false, tooltipContent,