From 595eb0dd8c7ca429c4f69e6240ef53e501c13499 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Sat, 13 Jul 2024 03:17:24 +0200 Subject: [PATCH] Build dashboard and results page --- backend.votee/.gitignore | 4 - backend.votee/.gitkeep | 0 backend.votee/constants/index.ts | 3 - backend.votee/contracts/AllowAllACL.sol | 52 -- backend.votee/contracts/NativeBalanceACL.sol | 94 ---- backend.votee/contracts/PollManager.sol | 370 -------------- backend.votee/hardhat.config.ts | 96 ---- backend.votee/interfaces/IPollACL.sol | 26 - backend.votee/interfaces/IPollManager.sol | 14 - backend.votee/interfaces/IPollManagerACL.sol | 8 - backend.votee/package.json | 44 -- backend.votee/scripts/close.ts | 41 -- backend.votee/scripts/create.ts | 65 --- backend.votee/tasks/deploy.ts | 112 ----- backend.votee/test/PollManager.spec.ts | 81 --- backend.votee/tsconfig.json | 15 - frontend.demo/src/components/AppPoll.vue | 4 +- frontend.demo/src/stores/ethereum.ts | 3 + frontend.demo/src/views/PollView.vue | 6 + frontend/index.html | 2 +- frontend/package.json | 7 +- frontend/public/background.svg | 3 + frontend/public/cappybara.webp | Bin 51330 -> 0 bytes frontend/public/fox.webp | Bin 74806 -> 0 bytes frontend/public/owl.webp | Bin 71524 -> 0 bytes frontend/src/App.tsx | 44 +- .../src/components/Button/index.module.css | 3 +- .../components/ConnectWallet/index.module.css | 1 + .../src/components/ConnectWallet/index.tsx | 10 +- frontend/src/components/Layout/index.tsx | 62 ++- frontend/src/components/LayoutBase/index.tsx | 62 +-- .../components/MascotCard/index.module.css | 91 ---- frontend/src/components/MascotCard/index.tsx | 56 --- .../components/MascotTieCard/index.module.css | 39 -- .../src/components/MascotTieCard/index.tsx | 18 - .../MascotTieSplit/index.module.css | 27 - .../src/components/MascotTieSplit/index.tsx | 18 - .../src/components/PieChart/index.module.css | 11 - frontend/src/components/PieChart/index.tsx | 37 -- .../src/components/PollCard/index.module.css | 68 +++ frontend/src/components/PollCard/index.tsx | 58 +++ frontend/src/components/icons/LogoIcon.tsx | 28 +- frontend/src/constants/config.ts | 28 -- frontend/src/hooks/useContracts.ts | 96 ++++ frontend/src/hooks/useDashboardData.ts | 177 +++++++ frontend/src/hooks/useEthereum.ts | 11 + frontend/src/hooks/usePollData.ts | 469 ++++++++++++++++++ frontend/src/index.css | 48 +- .../src/pages/DashboardPage/index.module.css | 5 + frontend/src/pages/DashboardPage/index.tsx | 38 ++ frontend/src/pages/HomePage/index.module.css | 147 ------ frontend/src/pages/HomePage/index.tsx | 282 ----------- frontend/src/pages/LandingPage/index.tsx | 18 + .../src/pages/PollPage/CompletedPollPage.tsx | 48 ++ frontend/src/pages/PollPage/index.module.css | 90 ++++ frontend/src/pages/PollPage/index.tsx | 54 ++ .../src/pages/ResultsPage/index.module.css | 149 ------ frontend/src/pages/ResultsPage/index.tsx | 186 ------- .../pages/UpcomingVotePage/index.module.css | 81 --- frontend/src/pages/UpcomingVotePage/index.tsx | 52 -- frontend/src/providers/AppStateContext.ts | 7 - frontend/src/providers/AppStateProvider.tsx | 86 +--- .../src/providers/DemoEthereumProvider.tsx | 259 ++++++++++ frontend/src/providers/EthereumContext.ts | 23 + frontend/src/providers/Web3Context.ts | 15 +- frontend/src/providers/Web3Provider.tsx | 162 +----- frontend/src/types/index.ts | 5 +- frontend/src/types/mascot-choices.ts | 1 - frontend/src/types/poll-choice.ts | 5 - frontend/src/types/poll.ts | 13 +- frontend/src/types/votes-storage.ts | 10 - frontend/src/utils/Pinata.ts | 41 ++ frontend/src/utils/crypto.demo.ts | 85 ++++ frontend/src/utils/eip1193.demo.ts | 48 ++ frontend/src/utils/errors.ts | 1 + frontend/src/utils/number.utils.ts | 8 +- frontend/src/utils/storage.ts | 46 -- pnpm-lock.yaml | 322 +----------- pnpm-workspace.yaml | 2 +- 79 files changed, 1790 insertions(+), 3011 deletions(-) delete mode 100644 backend.votee/.gitignore delete mode 100644 backend.votee/.gitkeep delete mode 100644 backend.votee/constants/index.ts delete mode 100644 backend.votee/contracts/AllowAllACL.sol delete mode 100644 backend.votee/contracts/NativeBalanceACL.sol delete mode 100644 backend.votee/contracts/PollManager.sol delete mode 100644 backend.votee/hardhat.config.ts delete mode 100644 backend.votee/interfaces/IPollACL.sol delete mode 100644 backend.votee/interfaces/IPollManager.sol delete mode 100644 backend.votee/interfaces/IPollManagerACL.sol delete mode 100644 backend.votee/package.json delete mode 100644 backend.votee/scripts/close.ts delete mode 100644 backend.votee/scripts/create.ts delete mode 100644 backend.votee/tasks/deploy.ts delete mode 100644 backend.votee/test/PollManager.spec.ts delete mode 100644 backend.votee/tsconfig.json create mode 100644 frontend/public/background.svg delete mode 100644 frontend/public/cappybara.webp delete mode 100644 frontend/public/fox.webp delete mode 100644 frontend/public/owl.webp delete mode 100644 frontend/src/components/MascotCard/index.module.css delete mode 100644 frontend/src/components/MascotCard/index.tsx delete mode 100644 frontend/src/components/MascotTieCard/index.module.css delete mode 100644 frontend/src/components/MascotTieCard/index.tsx delete mode 100644 frontend/src/components/MascotTieSplit/index.module.css delete mode 100644 frontend/src/components/MascotTieSplit/index.tsx delete mode 100644 frontend/src/components/PieChart/index.module.css delete mode 100644 frontend/src/components/PieChart/index.tsx create mode 100644 frontend/src/components/PollCard/index.module.css create mode 100644 frontend/src/components/PollCard/index.tsx create mode 100644 frontend/src/hooks/useContracts.ts create mode 100644 frontend/src/hooks/useDashboardData.ts create mode 100644 frontend/src/hooks/useEthereum.ts create mode 100644 frontend/src/hooks/usePollData.ts create mode 100644 frontend/src/pages/DashboardPage/index.module.css create mode 100644 frontend/src/pages/DashboardPage/index.tsx delete mode 100644 frontend/src/pages/HomePage/index.module.css delete mode 100644 frontend/src/pages/HomePage/index.tsx create mode 100644 frontend/src/pages/LandingPage/index.tsx create mode 100644 frontend/src/pages/PollPage/CompletedPollPage.tsx create mode 100644 frontend/src/pages/PollPage/index.module.css create mode 100644 frontend/src/pages/PollPage/index.tsx delete mode 100644 frontend/src/pages/ResultsPage/index.module.css delete mode 100644 frontend/src/pages/ResultsPage/index.tsx delete mode 100644 frontend/src/pages/UpcomingVotePage/index.module.css delete mode 100644 frontend/src/pages/UpcomingVotePage/index.tsx create mode 100644 frontend/src/providers/DemoEthereumProvider.tsx create mode 100644 frontend/src/providers/EthereumContext.ts delete mode 100644 frontend/src/types/mascot-choices.ts delete mode 100644 frontend/src/types/poll-choice.ts delete mode 100644 frontend/src/types/votes-storage.ts create mode 100644 frontend/src/utils/Pinata.ts create mode 100644 frontend/src/utils/crypto.demo.ts create mode 100644 frontend/src/utils/eip1193.demo.ts delete mode 100644 frontend/src/utils/storage.ts diff --git a/backend.votee/.gitignore b/backend.votee/.gitignore deleted file mode 100644 index efc67ff..0000000 --- a/backend.votee/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -abis -artifacts -cache -src diff --git a/backend.votee/.gitkeep b/backend.votee/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/backend.votee/constants/index.ts b/backend.votee/constants/index.ts deleted file mode 100644 index 7402a56..0000000 --- a/backend.votee/constants/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { parseEther } from 'ethers' - -export const MIN_BALANCE = parseEther('99.999999999999999999') diff --git a/backend.votee/contracts/AllowAllACL.sol b/backend.votee/contracts/AllowAllACL.sol deleted file mode 100644 index 4c0daf0..0000000 --- a/backend.votee/contracts/AllowAllACL.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -import { IPollACL } from "../interfaces/IPollACL.sol"; -import { IPollManagerACL } from "../interfaces/IPollManagerACL.sol"; - -contract AllowAllACL is IPollACL, IPollManagerACL -{ - function supportsInterface(bytes4 interfaceId) - public pure - returns(bool) - { - return interfaceId == type(IPollACL).interfaceId - || interfaceId == type(IPollManagerACL).interfaceId; - } - - function canCreatePoll(address, address) - external pure - returns(bool) - { - return true; - } - - function onPollCreated(bytes32, address, bytes calldata) - external - { - // Do nothing - } - - function onPollClosed(bytes32) - external - { - // Do nothing - } - - function canManagePoll(address, bytes32, address) - external pure - returns(bool) - { - // Anyone can manage any poll - return true; - } - - function canVoteOnPoll(address, bytes32, address, bytes calldata) - external pure - returns(uint) - { - // Anyone can vote on any poll - return 1; - } -} diff --git a/backend.votee/contracts/NativeBalanceACL.sol b/backend.votee/contracts/NativeBalanceACL.sol deleted file mode 100644 index 2e3ffc0..0000000 --- a/backend.votee/contracts/NativeBalanceACL.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -import { IPollACL } from "../interfaces/IPollACL.sol"; -import { IPollManagerACL } from "../interfaces/IPollManagerACL.sol"; - -contract NativeBalanceACL is IPollACL, IPollManagerACL -{ - struct ProposalOptions { - address owner; - uint256 minBalance; - } - - mapping(bytes32 => ProposalOptions) private m_proposals; - - function internal_id(bytes32 in_proposalId, address in_pm) - internal pure - returns (bytes32) - { - return keccak256(abi.encodePacked(in_proposalId, in_pm)); - } - - function internal_getProposal(bytes32 in_proposalId, address in_pm) - internal view - returns (ProposalOptions storage) - { - bytes32 id = internal_id(in_proposalId, in_pm); - - ProposalOptions storage prop = m_proposals[id]; - - require( prop.owner != address(0), "404" ); - - return prop; - } - - function supportsInterface(bytes4 interfaceId) - public pure - returns(bool) - { - return interfaceId == type(IPollACL).interfaceId - || interfaceId == type(IPollManagerACL).interfaceId; - } - - function canCreatePoll(address, address) - external pure - returns(bool) - { - return true; - } - - function onPollCreated(bytes32 in_proposalId, address in_creator, bytes calldata in_data) - external - { - (uint256 minBalance) = abi.decode(in_data, (uint256)); - - bytes32 id = internal_id(in_proposalId, msg.sender); - require( m_proposals[id].owner == address(0), "404" ); - - m_proposals[id] = ProposalOptions(in_creator, minBalance); - } - - function onPollClosed(bytes32 in_proposalId) - external - { - bytes32 id = internal_id(in_proposalId, msg.sender); - - ProposalOptions storage prop = m_proposals[id]; - - require( prop.owner != address(0), "404" ); - - delete m_proposals[id]; - } - - function canManagePoll(address in_pm, bytes32 in_proposalId, address in_who) - external view - returns(bool) - { - ProposalOptions storage prop = internal_getProposal(in_proposalId, in_pm); - - return prop.owner == in_who; - } - - function canVoteOnPoll(address in_pm, bytes32 in_proposalId, address who, bytes calldata) - external view - returns(uint) - { - ProposalOptions storage prop = internal_getProposal(in_proposalId, in_pm); - - require(who.balance > prop.minBalance); - - return 1; - } -} diff --git a/backend.votee/contracts/PollManager.sol b/backend.votee/contracts/PollManager.sol deleted file mode 100644 index 6227c3b..0000000 --- a/backend.votee/contracts/PollManager.sol +++ /dev/null @@ -1,370 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; - -import { IPollManager } from "../interfaces/IPollManager.sol"; -import { IPollACL } from "../interfaces/IPollACL.sol"; -import { IPollManagerACL } from "../interfaces/IPollManagerACL.sol"; - -contract PollManager is IERC165, IPollManager { - using EnumerableSet for EnumerableSet.Bytes32Set; - - uint256 public constant MAX_CHOICES = 8; - - // ------------------------------------------------------------------------ - // ERRORS - - // Errors relating to the creation of polls - error Create_NotAllowed(); - error Create_AlreadyExists(); - error Create_NoChoices(); - error Create_InvalidACL(); - error Create_TooManyChoices(); - - // Errors relating to voting on polls - error Vote_NotAllowed(); - error Vote_NotActive(); - error Vote_UnknownChoice(); - - // Errors relating to the closing of polls - error Close_NotAllowed(); - error Close_NotActive(); - - // Misc. errors relating to info about polls - error Poll_NotPublishingVotes(); - error Poll_StillActive(); - error Poll_NotActive(); - - - // ------------------------------------------------------------------------ - // EVENTS - - event ProposalCreated(bytes32 id); - - event ProposalClosed(bytes32 indexed id, uint256 topChoice); - - - // ------------------------------------------------------------------------ - // DATA STRUCTURES - - struct ProposalParams { - bytes ipfsHash; - uint8 numChoices; - uint64 closeTimestamp; - IPollACL acl; - } - - struct Proposal { - bool active; - uint8 topChoice; - ProposalParams params; - } - - struct ProposalWithId { - bytes32 id; - Proposal proposal; - } - - struct Choice { - uint weight; - uint8 choice; - } - - struct Ballot { - /// voter -> choice id - mapping(address => Choice) votes; - /// list of voters that submitted their vote - address[] voters; - /// Obscure votes using this xor mask - uint256 xorMask; - /// choice id -> vote count - uint256[MAX_CHOICES] voteCounts; - uint totalVotes; - } - - - // ------------------------------------------------------------------------ - // CONFIDENTIAL STORAGE - - mapping(bytes32 => Ballot) private s_ballots; - - IPollManagerACL private immutable s_managerACL; - - - // ------------------------------------------------------------------------ - // PUBLIC STORAGE - - mapping(bytes32 => Proposal) public PROPOSALS; - - EnumerableSet.Bytes32Set private ACTIVE_PROPOSALS; - - bytes32[] public PAST_PROPOSALS; - - - // ------------------------------------------------------------------------ - - constructor(IPollManagerACL in_managerACL) - { - s_managerACL = in_managerACL; - } - - // IERC165 - function supportsInterface(bytes4 interfaceId) - external pure - returns (bool) - { - return interfaceId == type(IERC165).interfaceId - || interfaceId == type(IPollManager).interfaceId; - } - - function getACL() - external view - returns (IPollManagerACL) - { - return s_managerACL; - } - - function getPollACL(bytes32 proposalId) - external view - returns (IPollACL) - { - return PROPOSALS[proposalId].params.acl; - } - - function create( - ProposalParams calldata in_params, - bytes calldata in_aclData - ) - external - returns (bytes32) - { - if (!s_managerACL.canCreatePoll(address(this), msg.sender)) { - revert Create_NotAllowed(); - } - - // User-provided ACL must adhere to IPollACL interface - if( ! in_params.acl.supportsInterface(type(IPollACL).interfaceId) ) { - revert Create_InvalidACL(); - } - - if (in_params.numChoices == 0) { - revert Create_NoChoices(); - } - - if (in_params.numChoices > MAX_CHOICES) { - revert Create_TooManyChoices(); - } - - bytes32 proposalId = keccak256(abi.encode(msg.sender, in_params, in_aclData)); - - if (PROPOSALS[proposalId].active) { - revert Create_AlreadyExists(); - } - - PROPOSALS[proposalId] = Proposal({ - active: true, - params: in_params, - topChoice:0 - }); - - ACTIVE_PROPOSALS.add(proposalId); - - Ballot storage ballot = s_ballots[proposalId]; - - uint xorMask = ballot.xorMask = uint256(keccak256(abi.encodePacked(address(this), msg.sender))); - - for (uint256 i; i < in_params.numChoices; ++i) - { - ballot.voteCounts[i] = xorMask; - } - - if( in_params.acl != IPollACL(address(0)) ) - { - in_params.acl.onPollCreated(proposalId, msg.sender, in_aclData); - } - - emit ProposalCreated(proposalId); - - return proposalId; - } - - function bool2int(bool a) - internal pure - returns (uint b) - { - assembly { - b := a - } - } - - function canVoteOnPoll(bytes32 in_proposalId, address in_voter, bytes calldata in_data) - public view - returns (uint out_weight) - { - Proposal storage proposal = PROPOSALS[in_proposalId]; - - // Proposal must be active to vote - if (!proposal.active) { - revert Vote_NotActive(); - } - - // No votes allowed after it's closed - uint closeTimestamp = proposal.params.closeTimestamp; - if( closeTimestamp != 0 ) { - if( block.timestamp >= closeTimestamp ) { - revert Vote_NotActive(); - } - } - - out_weight = proposal.params.acl.canVoteOnPoll(address(this), in_proposalId, in_voter, in_data); - if( out_weight == 0 ) { - revert Vote_NotAllowed(); - } - } - - function internal_castVote( - address in_voter, - bytes32 in_proposalId, - uint8 in_choiceId, - bytes calldata in_data - ) - internal - { - uint weight = canVoteOnPoll(in_proposalId, in_voter, in_data); - - Proposal storage proposal = PROPOSALS[in_proposalId]; - - uint256 numChoices = proposal.params.numChoices; - - if (in_choiceId >= numChoices) { - require(false, "Vote_UnknownChoice()"); - } - - Ballot storage ballot = s_ballots[in_proposalId]; - - Choice storage existingVote = ballot.votes[in_voter]; - - // Use the first weight that was provided - // As varying weights mid-poll will mess up the ballot voteCounts - uint existingWeight = existingVote.weight; - weight = (bool2int(existingWeight == 0) * weight) - + (bool2int(existingWeight != 0) * existingWeight); - - // Cycle the xor mask on each vote - // Ensures storage I/O patterns are uniform across all votes - uint xorMask = ballot.xorMask; - uint nextXorMask = uint256(keccak256(abi.encodePacked(xorMask))); - uint existingChoice = existingVote.choice; - for (uint256 i; i < numChoices; ++i) - { - // Modify the vote count in constant time - uint z = ballot.voteCounts[i]; - uint a = bool2int(i == existingChoice) * existingWeight; - uint b = bool2int(i == in_choiceId) * weight; - z ^= xorMask; - z -= a; - z += b; - z ^= nextXorMask; - ballot.voteCounts[i] = z; - } - - ballot.xorMask = nextXorMask; - - // Note: this code path reveals (via gas) whether the vote is the first - // or if it's somebody changing their vote - if( 0 == existingWeight ) - { - ballot.totalVotes += existingWeight; - } - - existingVote.weight = weight; - existingVote.choice = in_choiceId; - } - - function vote(bytes32 in_proposalId, uint8 in_choiceId, bytes calldata in_data) - external - { - internal_castVote(msg.sender, in_proposalId, in_choiceId, in_data); - } - - function close(bytes32 in_proposalId) - external - { - Proposal storage proposal = PROPOSALS[in_proposalId]; - if (!proposal.active) { - revert Close_NotActive(); - } - - if (!proposal.params.acl.canManagePoll(address(this), in_proposalId, msg.sender)) - { - // If no timestamp is specified, only poll creator can close (at any time) - uint closeTimestamp = proposal.params.closeTimestamp; - if( closeTimestamp != 0 ) - { - // Otherwise, anybody can close, $now >= closeTimestamp - if( block.timestamp < closeTimestamp ) - { - revert Close_NotAllowed(); - } - } - else { - revert Close_NotAllowed(); - } - } - - Ballot storage ballot = s_ballots[in_proposalId]; - - uint256 topChoice; - uint256 topChoiceCount; - uint256 xorMask = ballot.xorMask; - for (uint8 i; i < proposal.params.numChoices; ++i) - { - uint256 choiceVoteCount = ballot.voteCounts[i] ^ xorMask; - if (choiceVoteCount > topChoiceCount) - { - topChoice = i; - topChoiceCount = choiceVoteCount; - } - } - - PROPOSALS[in_proposalId].topChoice = uint8(topChoice); - PROPOSALS[in_proposalId].active = false; - ACTIVE_PROPOSALS.remove(in_proposalId); - PAST_PROPOSALS.push(in_proposalId); - - proposal.params.acl.onPollClosed(in_proposalId); - - emit ProposalClosed(in_proposalId, topChoice); - } - - function getVoteCounts(bytes32 in_proposalId) - external view - returns (uint256[] memory) - { - Proposal storage proposal = PROPOSALS[in_proposalId]; - - Ballot storage ballot = s_ballots[in_proposalId]; - - // Cannot get vote counts while poll is still active - if (proposal.active) { - revert Poll_StillActive(); - } - - uint256[] memory unmaskedVoteCounts = new uint256[](proposal.params.numChoices); - uint256 xorMask = ballot.xorMask; - for (uint256 i; i { - await runSuper() - await hre.run(TASK_EXPORT_ABIS) -}) - -task(TASK_EXPORT_ABIS, async (_args, hre) => { - const srcDir = path.basename(hre.config.paths.sources) - const outDir = path.join(hre.config.paths.root, 'abis') - - const [artifactNames] = await Promise.all([ - hre.artifacts.getAllFullyQualifiedNames(), - fs.mkdir(outDir, { recursive: true }), - ]) - - await Promise.all( - artifactNames.map(async fqn => { - const { abi, bytecode, contractName, sourceName } = await hre.artifacts.readArtifact(fqn) - if (abi.length === 0 || !sourceName.startsWith(srcDir) || contractName.endsWith('Test')) { - return - } - await fs.writeFile(`${path.join(outDir, contractName)}.json`, `${canonicalize(abi)}\n`) - await fs.writeFile(`${path.join(outDir, contractName)}.bin`, bytecode) - }) - ) -}).setDescription('Saves ABI and bytecode to the "abis" directory') - -const TEST_HDWALLET = { - mnemonic: 'test test test test test test test test test test test junk', - path: "m/44'/60'/0'/0", - initialIndex: 0, - count: 20, - passphrase: '', -} - -const accounts = process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : TEST_HDWALLET - -const config: HardhatUserConfig = { - networks: { - hardhat: { - chainId: 1337, // @see https://hardhat.org/metamask-issue.html - }, - hardhat_local: { - url: 'http://127.0.0.1:8545/', - }, - sapphire: { - url: 'https://sapphire.oasis.io', - chainId: 0x5afe, - accounts, - }, - 'sapphire-testnet': { - url: 'https://testnet.sapphire.oasis.dev', - chainId: 0x5aff, - accounts, - }, - 'sapphire-localnet': { - url: 'http://127.0.0.1:8545', - chainId: 0x5afd, - accounts, - }, - }, - solidity: { - version: '0.8.23', - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - viaIR: true, - }, - }, - typechain: { - target: 'ethers-v6', - outDir: 'src/contracts', - }, - mocha: { - require: ['ts-node/register/files'], - timeout: 50_000, - }, -} - -export default config diff --git a/backend.votee/interfaces/IPollACL.sol b/backend.votee/interfaces/IPollACL.sol deleted file mode 100644 index 455fb6a..0000000 --- a/backend.votee/interfaces/IPollACL.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; - -/** - * @title Generic ACL interface for DAO polls. - * - * Write functions for setting actual permissions are not part of this - * interface and should be specific to ACL implementations and specific dApp. - */ -interface IPollACL is IERC165 { - // DAO callback function when a new poll was created. This is typically invoked - // to assign poll creator as its manager. - function onPollCreated(bytes32 proposalId, address creator, bytes calldata data) external; - - function onPollClosed(bytes32 proposalId) external; - - // Can a given user manage poll (e.g. close the poll, add eligible voters). - function canManagePoll(address dao, bytes32 proposalId, address user) external view returns(bool); - - // Is a given user eligible voter for the given poll. - // Returns the weight of their votes, 0 if they're not allowed to vote! - function canVoteOnPoll(address dao, bytes32 proposalId, address user, bytes calldata data) external view returns(uint); -} diff --git a/backend.votee/interfaces/IPollManager.sol b/backend.votee/interfaces/IPollManager.sol deleted file mode 100644 index 18bd253..0000000 --- a/backend.votee/interfaces/IPollManager.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -import { IPollManagerACL } from "./IPollManagerACL.sol"; -import { IPollACL } from "./IPollACL.sol"; - -interface IPollManager { - function getACL() external view returns (IPollManagerACL); - - function getPollACL(bytes32 proposalId) external view returns (IPollACL); - - function canVoteOnPoll(bytes32 in_proposalId, address in_voter, bytes calldata in_data) external view returns (uint out_weight); -} diff --git a/backend.votee/interfaces/IPollManagerACL.sol b/backend.votee/interfaces/IPollManagerACL.sol deleted file mode 100644 index c150544..0000000 --- a/backend.votee/interfaces/IPollManagerACL.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -interface IPollManagerACL { - // Can a given user create a new poll. - function canCreatePoll(address in_dao, address in_creator) external view returns(bool); -} diff --git a/backend.votee/package.json b/backend.votee/package.json deleted file mode 100644 index a722c81..0000000 --- a/backend.votee/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "private": true, - "name": "@oasisprotocol/dapp-voting-backend", - "version": "1.0.0", - "license": "MIT", - "main": "./lib/cjs/index.js", - "type": "commonjs", - "engines": { - "node": ">=18", - "pnpm": ">=8" - }, - "files": [ - "contracts", - "lib", - "src" - ], - "scripts": { - "build:compile": "hardhat compile --quiet", - "build": "npm-run-all build:compile", - "test": "hardhat test", - "prepublishOnly": "pnpm build" - }, - "dependencies": { - "@openzeppelin/contracts": "^4.9.5", - "ethers": "6.10.0" - }, - "devDependencies": { - "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@oasisprotocol/sapphire-contracts": "^0.2.7", - "@oasisprotocol/sapphire-hardhat": "^2.19.4", - "@typechain/ethers-v6": "^0.5.1", - "@typechain/hardhat": "^9.1.0", - "@types/chai": "^4.3.11", - "@types/mocha": "^10.0.6", - "@types/node": "^20.11.5", - "canonicalize": "^2.0.0", - "chai": "^4.4.1", - "hardhat": "^2.21.0", - "npm-run-all": "^4.1.5", - "ts-node": "^10.9.2", - "typechain": "^8.3.2", - "typescript": "^5.2.2" - } -} diff --git a/backend.votee/scripts/close.ts b/backend.votee/scripts/close.ts deleted file mode 100644 index 2c29511..0000000 --- a/backend.votee/scripts/close.ts +++ /dev/null @@ -1,41 +0,0 @@ -import hre from 'hardhat' -import { PollManager__factory } from '../src/contracts' - -async function main() { - console.log('HARDHAT_NETWORK', process.env.HARDHAT_NETWORK) - console.log('HARDHAT_POLL_MANAGER_CONTRACT', process.env.HARDHAT_POLL_MANAGER_CONTRACT) - console.log('HARDHAT_PROPOSAL_ID', process.env.HARDHAT_PROPOSAL_ID) - - const pollManagerContract = process.env.HARDHAT_POLL_MANAGER_CONTRACT ?? '' - if (!pollManagerContract) { - throw new Error('[HARDHAT_POLL_MANAGER_CONTRACT] is required!') - } - - const proposalId = process.env.HARDHAT_PROPOSAL_ID ?? '' - if (!proposalId) { - throw new Error('[HARDHAT_PROPOSAL_ID] is required!') - } - - const [signer] = await hre.ethers.getSigners() - - const pollManager = PollManager__factory.connect(pollManagerContract, signer) - - const unsignedTx = await pollManager.close.populateTransaction(proposalId) - unsignedTx.gasLimit = 300000n - unsignedTx.value = 0n - - const tx = await signer.sendTransaction(unsignedTx) - const receipt = (await tx.wait())! - - const proposalClosedFilter = pollManager.filters.ProposalClosed() - const events = await pollManager.queryFilter(proposalClosedFilter, receipt.blockNumber) - const [{ args }] = events - const [hash] = args - - console.log('\x1b[32m', `Closed proposal with ID: "${hash}"`) -} - -main().catch(error => { - console.error(error) - process.exitCode = 1 -}) diff --git a/backend.votee/scripts/create.ts b/backend.votee/scripts/create.ts deleted file mode 100644 index 9a59555..0000000 --- a/backend.votee/scripts/create.ts +++ /dev/null @@ -1,65 +0,0 @@ -import hre from 'hardhat' -import { AbiCoder } from 'ethers' -import { PollManager__factory } from '../src/contracts' -import { MIN_BALANCE } from '../constants' - -async function main() { - console.log('HARDHAT_NETWORK', process.env.HARDHAT_NETWORK) - console.log('HARDHAT_POLL_MANAGER_CONTRACT', process.env.HARDHAT_POLL_MANAGER_CONTRACT) - console.log('HARDHAT_ACL_NATIVE_BALANCE_CONTRACT', process.env.HARDHAT_ACL_NATIVE_BALANCE_CONTRACT) - console.log('HARDHAT_CLOSE_TIMESTAMP', process.env.HARDHAT_CLOSE_TIMESTAMP) - console.log('HARDHAT_MIN_BALANCE', process.env.HARDHAT_MIN_BALANCE) - - const pollManagerContract = process.env.HARDHAT_POLL_MANAGER_CONTRACT ?? '' - if (!pollManagerContract) { - throw new Error('[HARDHAT_POLL_MANAGER_CONTRACT] is required!') - } - - const aclNativeBalanceContract = process.env.HARDHAT_ACL_NATIVE_BALANCE_CONTRACT ?? '' - if (!aclNativeBalanceContract) { - throw new Error('[HARDHAT_ACL_NATIVE_BALANCE_CONTRACT] is required!') - } - - const closeTimestamp = BigInt(process.env.HARDHAT_CLOSE_TIMESTAMP ?? '0') ?? 0n - const minBalance = BigInt(process.env.HARDHAT_MIN_BALANCE ?? '0') ?? 0n - - const [signer] = await hre.ethers.getSigners() - - const pollManager = PollManager__factory.connect(pollManagerContract, signer) - - const unsignedTx = await pollManager.create.populateTransaction( - { - ipfsHash: new Uint8Array([]), - numChoices: 3, - closeTimestamp, - acl: aclNativeBalanceContract, - }, - AbiCoder.defaultAbiCoder().encode(['uint256'], [minBalance !== 0n ? minBalance : MIN_BALANCE]) - ) - unsignedTx.gasLimit = 500000n - unsignedTx.value = 0n - - const tx = await signer.sendTransaction(unsignedTx) - const receipt = (await tx.wait())! - - const proposalCreatedFilter = pollManager.filters.ProposalCreated() - const events = await pollManager.queryFilter(proposalCreatedFilter, receipt.blockNumber) - const [{ args }] = events - const [hash] = args - - console.log( - '\x1b[32m', - ` -Created proposal with ID: "${hash}" - -Update .env: -VITE_PROPOSAL_ID=${hash} -VITE_ACL_NATIVEBALANCE_MIN_BALANCE_WEI=${MIN_BALANCE} - ` - ) -} - -main().catch(error => { - console.error(error) - process.exitCode = 1 -}) diff --git a/backend.votee/tasks/deploy.ts b/backend.votee/tasks/deploy.ts deleted file mode 100644 index f7e6bbe..0000000 --- a/backend.votee/tasks/deploy.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { task } from 'hardhat/config' -import { existsSync, promises as fs } from 'fs' -import { HardhatRuntimeEnvironment, HttpNetworkUserConfig } from 'hardhat/types' -import { AbiCoder, BytesLike, EventLog } from 'ethers' -import { MIN_BALANCE } from '../constants' - -function maketee(filename?: string) { - return async function tee(line?: string) { - if (line !== undefined) { - console.log(line) - if (filename) { - await fs.appendFile(filename, line + '\n') - } - } - } -} - -interface DeployArgs { - viteenv: string | undefined -} - -async function deploy_acls(hre: HardhatRuntimeEnvironment, tee: ReturnType) { - // Deploy AllowAllACL - const factory_AllowAllACL = await hre.ethers.getContractFactory('AllowAllACL') - const contract_AllowAllACL = await factory_AllowAllACL.deploy() - await tee('') - await tee(`# AllowAllACL tx ${contract_AllowAllACL.deploymentTransaction()?.hash}`) - await contract_AllowAllACL.waitForDeployment() - const addr_AllowAllACL = await contract_AllowAllACL.getAddress() - await tee(`VITE_CONTRACT_ACL_ALLOWALL=${addr_AllowAllACL}`) - - // Deploy NativeBalanceACL - const factory_NativeBalanceACL = await hre.ethers.getContractFactory('NativeBalanceACL') - const contract_NativeBalanceACL = await factory_NativeBalanceACL.deploy() - await tee('') - await tee(`# NativeBalanceACL tx ${contract_AllowAllACL.deploymentTransaction()?.hash}`) - await contract_NativeBalanceACL.waitForDeployment() - const addr_NativeBalanceACL = await contract_NativeBalanceACL.getAddress() - await tee(`VITE_CONTRACT_ACL_NATIVEBALANCE=${addr_NativeBalanceACL}`) - - return { addr_AllowAllACL, addr_NativeBalanceACL } -} - -// Default DAO deployment, no permissions. -task('deploy') - .addParam('viteenv', 'Output contract addresses to environment file', '') - .setAction(async (args: DeployArgs, hre) => { - await hre.run('compile', { quiet: true }) - - if (args.viteenv) { - console.log(`# Saving environment to ${args.viteenv}`) - if (existsSync(args.viteenv)) { - await fs.unlink(args.viteenv) - } - } - - const tee = maketee(args.viteenv) - - // Export RPC info etc. from current hardhat config - const currentNetwork = Object.values(hre.config.networks).find( - x => x.chainId === hre.network.config.chainId - ) - const currentNetworkUrl = (currentNetwork! as HttpNetworkUserConfig).url - tee(`VITE_NETWORK=${hre.network.config.chainId}`) - if (!currentNetworkUrl) { - tee('VITE_WEB3_GATEWAY=http://localhost:8545') - } else { - tee(`VITE_WEB3_GATEWAY=${currentNetworkUrl}`) - } - - const { addr_AllowAllACL, addr_NativeBalanceACL } = await deploy_acls(hre, tee) - - // Deploy PollManager - const factory_PollManager = await hre.ethers.getContractFactory('PollManager') - const contract_PollManager = await factory_PollManager.deploy(addr_AllowAllACL) - await tee('') - await tee(`# PollManager tx ${contract_PollManager.deploymentTransaction()?.hash}`) - await contract_PollManager.waitForDeployment() - await tee(`VITE_CONTRACT_POLLMANAGER=${await contract_PollManager.getAddress()}`) - - // Set the default PollManager ACL, so frontend doesn't have to query contract - await tee('') - await tee('# IPollManagerACL used by PollManager') - await tee(`VITE_CONTRACT_POLLMANAGER_ACL=${addr_AllowAllACL}`) - - // Create a poll with 3 options - const tx = await contract_PollManager.create( - { - ipfsHash: new Uint8Array([]), - numChoices: 3, - closeTimestamp: 1711551600, - acl: addr_NativeBalanceACL, - }, - AbiCoder.defaultAbiCoder().encode(['uint256'], [MIN_BALANCE]) - ) - - await tee('') - await tee( - '# Enables frontend check to ensure wallet has enough balance for MIN_BALANCE + gas (0 = DISABLED)' - ) - await tee(`VITE_ACL_NATIVEBALANCE_MIN_BALANCE_WEI=${MIN_BALANCE}`) - - const receipt = await tx.wait() - const createEvent = receipt!.logs.find( - event => (event as EventLog).fragment.name === 'ProposalCreated' - ) as EventLog - const proposalId = createEvent!.args![0] as BytesLike - - await tee('') - await tee('# Proposal ID for poll pre-created poll') - await tee(`VITE_PROPOSAL_ID=${proposalId}`) - }) diff --git a/backend.votee/test/PollManager.spec.ts b/backend.votee/test/PollManager.spec.ts deleted file mode 100644 index c33893c..0000000 --- a/backend.votee/test/PollManager.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { expect } from 'chai' -import { ethers } from 'hardhat' -import { AbiCoder, BytesLike, EventLog, getBytes, parseEther } from 'ethers' -import { PollManager, AllowAllACL, NativeBalanceACL } from '../src/contracts' - -async function addProposal( - dao: PollManager, - proposal: PollManager.ProposalParamsStruct, - aclData?: Uint8Array -) { - const tx = await dao.create(proposal, aclData ?? new Uint8Array([])) - const receipt = await tx.wait() - expect(receipt!.logs).to.not.be.undefined - const createEvent = receipt!.logs.find( - event => (event as EventLog).fragment.name === 'ProposalCreated' - ) as EventLog - expect(createEvent).to.not.be.undefined - expect(createEvent!.args).to.not.be.undefined - return createEvent!.args![0] as BytesLike -} - -async function closeProposal(dao: PollManager, proposalId: BytesLike) { - const tx = await dao.close(proposalId) - const receipt = await tx.wait() - expect(receipt!.logs).to.not.be.undefined - const closeEvent = receipt!.logs.find(event => (event as EventLog).fragment.name === 'ProposalClosed') as - | EventLog - | undefined - expect(closeEvent).to.not.be.undefined - expect(closeEvent!.args).to.not.be.undefined - const [, topChoice] = closeEvent!.args - return topChoice as bigint -} - -async function deployContract(name: string, ...args: unknown[]) { - const c = await (await ethers.getContractFactory(name)).deploy(...args) - await c.waitForDeployment() - return c -} - -describe('PollManager', function () { - let acl_allowall: AllowAllACL - let acl_nativebalance: NativeBalanceACL - let pm: PollManager - - before(async () => { - acl_allowall = (await deployContract('AllowAllACL')) as AllowAllACL - acl_nativebalance = (await deployContract('NativeBalanceACL')) as NativeBalanceACL - - const acl_allowall_addr = await acl_allowall.getAddress() - - pm = (await deployContract('PollManager', acl_allowall_addr)) as PollManager - }) - - it('Proposals', async function () { - const acl_nativebalance_addr = await acl_nativebalance.getAddress() - - const aclData = getBytes(AbiCoder.defaultAbiCoder().encode(['uint256'], [parseEther('1')])) - - const proposalId = await addProposal( - pm, - { - ipfsHash: '0xdef0', - numChoices: 3n, - closeTimestamp: 0n, - acl: acl_nativebalance_addr, - }, - aclData - ) - - const voteTx = await pm.vote(proposalId, 1, new Uint8Array([])) - await voteTx.wait() - - await closeProposal(pm, proposalId) - - const counts = await pm.getVoteCounts(proposalId) - expect(counts[0]).eq(0n) - expect(counts[1]).eq(1n) - expect(counts[2]).eq(0n) - }) -}) diff --git a/backend.votee/tsconfig.json b/backend.votee/tsconfig.json deleted file mode 100644 index b120f12..0000000 --- a/backend.votee/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "include": ["src", "tasks", "scripts", "test", "constants"], - "files": ["hardhat.config.ts"], - "compilerOptions": { - "noEmit": true, - "declaration": true, - "esModuleInterop": true, - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "strict": true, - "target": "es2020", - "outDir": "lib/esm" - } -} diff --git a/frontend.demo/src/components/AppPoll.vue b/frontend.demo/src/components/AppPoll.vue index d427846..1c676b7 100644 --- a/frontend.demo/src/components/AppPoll.vue +++ b/frontend.demo/src/components/AppPoll.vue @@ -6,8 +6,8 @@
{{ - abbrAddr(creatorAddress) - }} + abbrAddr(creatorAddress) + }}

diff --git a/frontend.demo/src/stores/ethereum.ts b/frontend.demo/src/stores/ethereum.ts index 3d29361..6e9b9aa 100644 --- a/frontend.demo/src/stores/ethereum.ts +++ b/frontend.demo/src/stores/ethereum.ts @@ -206,6 +206,7 @@ export const useEthereumStore = defineStore('ethereum', () => { } if (network == Network.SapphireTestnet) { + // TODO: this data is also available in config.ts, remove redundancy! await eth.request({ method: 'wallet_addEthereumChain', params: [ @@ -219,6 +220,7 @@ export const useEthereumStore = defineStore('ethereum', () => { ], }); } else if (network === Network.SapphireMainnet) { + // TODO: this data is also available in config.ts, remove redundancy! await eth.request({ method: 'wallet_addEthereumChain', params: [ @@ -236,6 +238,7 @@ export const useEthereumStore = defineStore('ethereum', () => { ], }); } else if (network === Network.SapphireLocalnet) { + // TODO: this data is also available in config.ts, remove redundancy! await eth.request({ method: 'wallet_addEthereumChain', params: [ diff --git a/frontend.demo/src/views/PollView.vue b/frontend.demo/src/views/PollView.vue index b3c5c48..080d701 100644 --- a/frontend.demo/src/views/PollView.vue +++ b/frontend.demo/src/views/PollView.vue @@ -363,11 +363,15 @@ onMounted(async () => { gvAddrs.value.join(', '), ); } + + console.log("Loaded stuff. active?", proposal.active, "hasVoted?", hasVoted.value, "closed?", isClosed.value) + });