Skip to content

Commit

Permalink
Add Kusama society space, #1034 (#1035)
Browse files Browse the repository at this point in the history
* Add kusama society space, #1034

* Check social member, #1034

* fix: space icon, #1034

* fix: proposal threshold check, #1034

* refactor, #1034

* Update, #1034

* refactor request param check, #1034

* Refactor society proposal vote, #1034

* refactor vote, #1034

* Update, #1034

* fix: remove debug comment, #1034

* fix: check society member, #1034

* Show society member warning, #1034

* Show society member check info, #1034

* fix, #1034

* Disable button if not society member, #1034

* fix, #1034

* Mock test account as society member, #1034
  • Loading branch information
hyifeng authored Sep 4, 2024
1 parent d563fd5 commit 7998960
Show file tree
Hide file tree
Showing 87 changed files with 1,559 additions and 698 deletions.
8 changes: 8 additions & 0 deletions backend/packages/backend/src/consts/space.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const Accessibility = {
PUBLIC: "public",
SOCIETY: "society",
};

module.exports = {
Accessibility,
};
1 change: 1 addition & 0 deletions backend/packages/backend/src/consts/voting.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const strategies = Object.freeze({
quadraticBalanceOf: "quadratic-balance-of",
quorumQuadraticBalanceOf: "quorum-quadratic-balance-of",
biasedVoting: "biased-voting",
onePersonOneVote: "one-person-one-vote",
});

module.exports = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { HttpError } = require("../../exc");
const { ContentType, PostTitleLengthLimitation } = require("../../constants");

function checkProposalContent(data) {
const { title, content, contentType } = data;

if (!title) {
throw new HttpError(400, { title: ["Title is missing"] });
}

if (title.length > PostTitleLengthLimitation) {
throw new HttpError(400, {
title: ["Title must be no more than %d characters"],
});
}

if (!content) {
throw new HttpError(400, { content: ["Content is missing"] });
}

if (!contentType) {
throw new HttpError(400, { contentType: ["Content type is missing"] });
}

if (
contentType !== ContentType.Markdown &&
contentType !== ContentType.Html
) {
throw new HttpError(400, { contentType: ["Invalid content type"] });
}
}

module.exports = {
checkProposalContent,
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const Router = require("koa-router");
const proposalController = require("./proposal.controller");
const { getProposalById } = require("./getProposalById");

const router = new Router();
router.get("/proposal/:proposalId", proposalController.getProposalById);
router.get("/proposal/:proposalId", getProposalById);

module.exports = router;
259 changes: 259 additions & 0 deletions backend/packages/backend/src/features/proposals/createProposal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
const { HttpError } = require("../../exc");
const proposalService = require("../../services/proposal.service");
const { ChoiceType } = require("../../constants");
const isEmpty = require("lodash.isempty");
const { spaces: spaceServices } = require("../../spaces");
const { Accessibility } = require("../../consts/space");
const { checkProposalContent } = require("./checkProposalContent");
const isEqual = require("lodash.isequal");
const pick = require("lodash.pick");
const { getLatestHeight } = require("../../services/chain.service");

function checkProposalChoices(data) {
const { space, choiceType, choices } = data;

if (!choiceType) {
throw new HttpError(400, { content: ["Choice type is missing"] });
}

if (![ChoiceType.Single, ChoiceType.Multiple].includes(choiceType)) {
throw new HttpError(400, { choiceType: ["Unknown choice type"] });
}

if (!choices) {
throw new HttpError(400, { choices: ["Choices is missing"] });
}

if (
!Array.isArray(choices) ||
choices.length < 2 ||
choices.some((item) => typeof item !== "string")
) {
throw new HttpError(400, {
choices: ["Choices must be array of string with at least 2 items"],
});
}

if (new Set(choices).size < choices.length) {
throw new HttpError(400, { choices: ["All choices should be different"] });
}

const uniqueChoices = Array.from(new Set(choices));
if (uniqueChoices.length < 2) {
throw new HttpError(400, {
choices: ["There must be at least 2 different choices"],
});
}

const spaceService = spaceServices[space];
const maxOptionsCount = spaceService.maxOptionsCount || 10;
if (choices.length > spaceService.maxOptionsCount) {
throw new HttpError(
400,
`Too many options, support up to ${maxOptionsCount} options`,
);
}
}

function checkProposalDate(data) {
const { startDate, endDate } = data;

if (!startDate) {
throw new HttpError(400, { content: ["Start date is missing"] });
}

if (!endDate) {
throw new HttpError(400, { content: ["End date is missing"] });
}

if (endDate <= startDate) {
throw new HttpError(400, "Start date should not be later than end date");
}

const now = new Date();

if (endDate < now.getTime()) {
throw new HttpError(
400,
"End date should not be earlier than current time",
);
}
}

async function checkSnapshotHeights(data) {
const { space, snapshotHeights } = data;

if (!snapshotHeights) {
throw new HttpError(400, { content: ["Snapshot height is missing"] });
}

const spaceService = spaceServices[space];

// Check if the snapshot heights is matching the space configuration
const snapshotNetworks = Object.keys(snapshotHeights || {});
if (
snapshotNetworks.length === 0 ||
snapshotNetworks.length !== spaceService.networks.length
) {
throw new HttpError(400, {
snapshotHeights: [
"The snapshot heights must match the space configuration",
],
});
}

for (const spaceNetwork of spaceService.networks) {
if (snapshotNetworks.includes(spaceNetwork.network)) {
continue;
}

throw new HttpError(400, {
snapshotHeights: [`Missing snapshot height of ${spaceNetwork.network}`],
});
}

await Promise.all(
Object.keys(snapshotHeights).map(async (chain) => {
const lastHeight = await getLatestHeight(chain);
if (lastHeight && snapshotHeights[chain] > lastHeight) {
throw new HttpError(
400,
`Snapshot height should not be higher than the current finalized height: ${chain}`,
);
}
}),
);
}

function checkNetworkConfig(data) {
const { space, networksConfig } = data;

if (isEmpty(networksConfig)) {
throw new HttpError(400, {
networksConfig: ["Networks config is missing"],
});
}

const spaceService = spaceServices[space];
if (
!isEqual(networksConfig, {
...pick(spaceService, [
"symbol",
"decimals",
"networks",
"accessibility",
]),
strategies: spaceService.weightStrategy,
...pick(spaceService, ["quorum", "version"]),
})
) {
throw new HttpError(400, {
networksConfig: [
"The proposal networks config is not matching the space config",
],
});
}
}

async function checkProposalSpace(data) {
const { space } = data;

if (!space) {
throw new HttpError(400, { space: ["Space is missing"] });
}

const spaceConfig = spaceServices[space];
if (!spaceConfig) {
throw new HttpError(400, { space: ["Unknown space"] });
}
}

async function checkProposalOptions(data) {
const { proposerNetwork } = data;

if (!proposerNetwork) {
throw new HttpError(400, {
proposerNetwork: ["Proposer network is missing"],
});
}

checkProposalSpace(data);

checkNetworkConfig(data);

await checkSnapshotHeights(data);

checkProposalContent(data);

checkProposalDate(data);

checkProposalChoices(data);
}

async function createProposal(ctx) {
const { data, address, signature } = ctx.request.body;
const {
space,
networksConfig,
title,
content,
contentType,
choiceType,
choices,
startDate,
endDate,
snapshotHeights,
realProposer,
proposerNetwork,
banner,
} = data;

await checkProposalOptions(data);

const spaceConfig = spaceServices[space];

if (spaceConfig.accessibility === Accessibility.SOCIETY) {
ctx.body = await proposalService.createSocietyProposal({
space,
networksConfig,
title,
content,
contentType,
choiceType,
choices,
startDate,
endDate,
snapshotHeights,
realProposer,
proposerNetwork,
banner,
data,
address,
signature,
});
return;
}

ctx.body = await proposalService.createProposal({
space,
networksConfig,
title,
content,
contentType,
choiceType,
choices,
startDate,
endDate,
snapshotHeights,
realProposer,
proposerNetwork,
banner,
data,
address,
signature,
});
}

module.exports = {
createProposal,
};
11 changes: 11 additions & 0 deletions backend/packages/backend/src/features/proposals/getAddressVote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const proposalService = require("../../services/proposal.service");

async function getAddressVote(ctx) {
const { proposalCid, address } = ctx.params;

ctx.body = await proposalService.getAddressVote(proposalCid, address);
}

module.exports = {
getAddressVote,
};
17 changes: 17 additions & 0 deletions backend/packages/backend/src/features/proposals/getComments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const proposalService = require("../../services/proposal.service");
const { extractPage } = require("../../utils");

async function getComments(ctx) {
const { page, pageSize } = extractPage(ctx);
if (pageSize === 0 || page < 1) {
ctx.status = 400;
return;
}

const { proposalCid } = ctx.params;
ctx.body = await proposalService.getComments(proposalCid, page, pageSize);
}

module.exports = {
getComments,
};
11 changes: 11 additions & 0 deletions backend/packages/backend/src/features/proposals/getProposalById.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const proposalService = require("../../services/proposal.service");

async function getProposalById(ctx) {
const { proposalId } = ctx.params;

ctx.body = await proposalService.getProposalById(proposalId);
}

module.exports = {
getProposalById,
};
Loading

0 comments on commit 7998960

Please sign in to comment.