{"id":27401281,"url":"https://github.com/edjcase/motoko_proposal_engine","last_synced_at":"2026-02-09T00:32:48.503Z","repository":{"id":249316996,"uuid":"831178569","full_name":"edjCase/motoko_proposal_engine","owner":"edjCase","description":"Motoko library for creating, voting on and executing proposals","archived":false,"fork":false,"pushed_at":"2025-08-01T23:29:18.000Z","size":152,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-08T05:32:27.510Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Motoko","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edjCase.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-07-19T21:14:49.000Z","updated_at":"2025-08-01T23:29:22.000Z","dependencies_parsed_at":null,"dependency_job_id":"d392f837-d4a4-496e-899f-d2c9208a4407","html_url":"https://github.com/edjCase/motoko_proposal_engine","commit_stats":null,"previous_names":["gekctek/motoko_proposal_engine"],"tags_count":0,"template":false,"template_full_name":"edjCase/motoko-library-template","purl":"pkg:github/edjCase/motoko_proposal_engine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fmotoko_proposal_engine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fmotoko_proposal_engine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fmotoko_proposal_engine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fmotoko_proposal_engine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edjCase","download_url":"https://codeload.github.com/edjCase/motoko_proposal_engine/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fmotoko_proposal_engine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29251542,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-08T22:49:53.206Z","status":"ssl_error","status_checked_at":"2026-02-08T22:49:51.384Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-14T03:54:39.413Z","updated_at":"2026-02-09T00:32:48.475Z","avatar_url":"https://github.com/edjCase.png","language":"Motoko","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Overview\n\nA library for creating, voting on and executing proposals\n\n# Package\n\n### MOPS\n\n# Motoko Proposal Engine\n\nA comprehensive library for creating, voting on, and executing proposals in Motoko. This library supports both simple boolean voting and advanced multi-choice voting with configurable thresholds and voting modes.\n\n## Features\n\n- **Multiple Voting Modes**: Snapshot-based and dynamic voting\n- **Flexible Choices**: Boolean voting or custom choice types\n- **Configurable Thresholds**: Percentage-based voting with optional quorum\n- **Dynamic Member Management**: Add members to proposals during voting\n- **Automatic Execution**: Proposals execute automatically when thresholds are met\n- **Time-bound Voting**: Optional proposal durations with automatic ending\n- **Stable Upgrades**: Full support for canister upgrades\n\n## Package\n\n### MOPS\n\n```bash\nmops install dao-proposal-engine\n```\n\nTo setup MOPS package manage, follow the instructions from the [MOPS Site](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/)\n\n## Quick Start\n\n### Simple Boolean Voting\n\n```motoko\nimport ProposalEngine \"mo:dao-proposal-engine/ProposalEngine\";\n\n// Initialize with stable data\nlet stableData = {\n    proposals = BTree.init\u003cNat, ProposalEngine.ProposalData\u003cMyProposalContent\u003e\u003e(null);\n    proposalDuration = ?#days(7); // 7 day voting period\n    votingThreshold = #percent({ percent = 50; quorum = ?25 });\n    allowVoteChange = false;\n};\n\n// Create proposal engine for boolean voting\nlet engine = ProposalEngine.ProposalEngine\u003csystem, MyProposalContent\u003e(\n    stableData,\n    onProposalAdopt, // Called when proposal passes\n    onProposalReject, // Called when proposal fails\n    onProposalValidate // Validates proposal content\n);\n\n// Create a proposal\nlet members = [\n    { id = principalA; votingPower = 100 },\n    { id = principalB; votingPower = 50 }\n];\nlet proposalId = await* engine.createProposal(\n    proposerId,\n    proposalContent,\n    members,\n    #snapshot // Snapshot voting mode\n);\n\n// Vote on proposal\nlet _ = await* engine.vote(proposalId, voterId, true); // Vote yes\n```\n\n### Advanced Multi-Choice Voting\n\n```motoko\nimport ExtendedProposalEngine \"mo:dao-proposal-engine/ExtendedProposalEngine\";\n\n// Create proposal engine for custom choice voting\nlet engine = ExtendedProposalEngine.ProposalEngine\u003csystem, MyProposalContent, MyChoice\u003e(\n    stableData,\n    onProposalExecute, // Called with winning choice\n    onProposalValidate, // Validates proposal content\n    MyChoice.compare, // Choice compare function\n);\n\n// Create proposal with dynamic voting\nlet proposalId = await* engine.createProposal(\n    proposerId,\n    proposalContent,\n    members,\n    #dynamic({ totalVotingPower = ?1000 }) // Dynamic voting mode\n);\n\n// Add member during voting (only for dynamic mode)\nlet newMember = { id = newPrincipal; votingPower = 75 };\nlet _ = engine.addMember(proposalId, newMember);\n\n// Vote with custom choice\nlet _ = await* engine.vote(proposalId, voterId, myChoice);\n```\n\n## Architecture Overview\n\n### Proposal vs Engine\n\nThe library provides two levels of abstraction for working with proposals:\n\n#### **Proposal Modules** (`Proposal.mo` and `ExtendedProposal.mo`)\n\n- **Pure data structures**: Hold proposal data and voting information\n- **Stateless functions**: Provide utilities for voting, calculating status, and managing proposal data\n- **Manual management**: You handle storage, timers, and state transitions yourself\n- **Direct control**: Full control over when and how proposals are processed\n\n```motoko\n// Direct proposal management\nimport Proposal \"mo:dao-proposal-engine/Proposal\";\n\nlet proposal = Proposal.create(...);\nlet voteResult = Proposal.vote(proposal, voterId, true, allowVoteChange);\nlet status = Proposal.calculateVoteStatus(proposal, threshold, forceEnd);\n// You handle storage and execution yourself\n```\n\n#### **Engine Classes** (`ProposalEngine.mo` and `ExtendedProposalEngine.mo`)\n\n- **Complete management system**: Handles proposal storage, lifecycle, and execution\n- **Automatic features**:\n  - Timer-based proposal ending\n  - Automatic status transitions\n  - Auto-execution when thresholds are met\n  - Stable data management for upgrades\n- **Event-driven**: Callbacks for proposal adoption, rejection, and validation\n- **Production-ready**: Handles all the complex state management for you\n\n```motoko\n// Managed proposal system\nimport ProposalEngine \"mo:dao-proposal-engine/ProposalEngine\";\n\nlet engine = ProposalEngine.ProposalEngine\u003csystem, MyContent\u003e(...);\nlet proposalId = await* engine.createProposal(...); // Stored automatically\nlet _ = await* engine.vote(proposalId, voterId, true); // Auto-executes if threshold met\n// Engine handles timers, storage, and execution automatically\n```\n\n### Standard vs Extended Proposals\n\n#### **Standard Proposals** (`Proposal.mo` and `ProposalEngine.mo`)\n\n- **Boolean voting**: Simple adopt (true) or reject (false) decisions\n- **Two outcomes**: Proposals either pass or fail\n- **Simplified API**: Easier to use for basic governance needs\n- **Type safety**: Enforced boolean voting prevents choice errors\n\n```motoko\n// Boolean voting - simple and clear\nlet _ = await* engine.vote(proposalId, voterId, true); // Vote to adopt\nlet _ = await* engine.vote(proposalId, voterId, false); // Vote to reject\n```\n\n#### **Extended Proposals** (`ExtendedProposal.mo` and `ExtendedProposalEngine.mo`)\n\n- **Custom choice types**: Any type can be used for voting choices\n- **Multi-choice voting**: Support for complex decision-making scenarios\n- **Flexible outcomes**: Winners determined by plurality or custom logic\n- **Advanced scenarios**: Budget allocation, candidate selection, configuration options\n\n```motoko\n// Multi-choice voting with custom types\ntype BudgetChoice = {\n  #allocateToMarketing: Nat;\n  #allocateToEngineering: Nat;\n  #allocateToOperations: Nat;\n  #rejectBudget;\n};\n\nlet _ = await* extendedEngine.vote(proposalId, voterId, #allocateToEngineering(500_000));\n```\n\n## API Reference\n\n### Core Types\n\n#### StableData\n\n```motoko\ntype StableData\u003cTProposalContent, TChoice\u003e = {\n    proposals : [Proposal\u003cTProposalContent, TChoice\u003e];\n    proposalDuration : ?Duration;\n    votingThreshold : VotingThreshold;\n    allowVoteChange : Bool;\n};\n```\n\n#### PagedResult\n\n```motoko\ntype PagedResult\u003cT\u003e = {\n    data : [T];\n    offset : Nat;\n    count : Nat;\n    totalCount : Nat;\n};\n```\n\n#### VotingMode\n\n```motoko\ntype VotingMode = {\n    #snapshot; // Fixed member list at creation\n    #dynamic : { totalVotingPower : ?Nat }; // Members can be added during voting\n};\n```\n\n#### VotingThreshold\n\n```motoko\ntype VotingThreshold = {\n    #percent : { percent : Nat; quorum : ?Nat }; // Percentage (0-100) with optional quorum\n};\n```\n\n#### Duration\n\n```motoko\ntype Duration = {\n    #days : Nat;\n    #nanoseconds : Nat;\n};\n```\n\n#### Member\n\n```motoko\ntype Member = {\n    id : Principal;\n    votingPower : Nat;\n};\n```\n\n#### Proposal\n\n```motoko\ntype Proposal\u003cTProposalContent, TChoice\u003e = {\n    id : Nat;\n    proposerId : Principal;\n    timeStart : Int;\n    timeEnd : ?Int;\n    votingMode : VotingMode;\n    content : TProposalContent;\n    votes : BTree\u003cPrincipal, Vote\u003cTChoice\u003e\u003e;\n    status : ProposalStatus\u003cTChoice\u003e;\n};\n```\n\n#### ProposalStatus\n\n```motoko\ntype ProposalStatus\u003cTChoice\u003e = {\n    #open;\n    #executing : { executingTime : Time; choice : ?TChoice };\n    #executed : { executingTime : Time; executedTime : Time; choice : ?TChoice };\n    #failedToExecute : { executingTime : Time; failedTime : Time; choice : ?TChoice; error : Text };\n};\n```\n\n#### Vote\n\n```motoko\ntype Vote\u003cTChoice\u003e = {\n    choice : ?TChoice;\n    votingPower : Nat;\n};\n```\n\n#### VotingSummary\n\n```motoko\ntype VotingSummary\u003cTChoice\u003e = {\n    votingPowerByChoice : [ChoiceVotingPower\u003cTChoice\u003e];\n    totalVotingPower : Nat;\n    undecidedVotingPower : Nat;\n};\n```\n\n#### ChoiceVotingPower\n\n```motoko\ntype ChoiceVotingPower\u003cTChoice\u003e = {\n    choice : TChoice;\n    votingPower : Nat;\n};\n```\n\n### Error Types\n\n#### VoteError\n\n```motoko\ntype VoteError = {\n    #notEligible; // Voter is not a member of the proposal\n    #alreadyVoted; // Voter has already voted (when vote changes are disabled)\n    #votingClosed; // Voting period has ended or proposal is not open\n    #proposalNotFound; // Proposal ID does not exist (ExtendedProposalEngine only)\n};\n```\n\n#### CreateProposalError\n\n```motoko\ntype CreateProposalError = {\n    #notEligible; // Proposer is not eligible to create proposals\n    #invalid : [Text]; // Proposal content failed validation\n};\n```\n\n#### AddMemberResult\n\n```motoko\ntype AddMemberResult = {\n    #ok; // Member added successfully\n    #alreadyExists; // Member already exists in the proposal\n    #proposalNotFound; // Proposal ID does not exist\n    #votingNotDynamic; // Proposal is not in dynamic voting mode\n    #votingClosed; // Voting period has ended\n};\n```\n\n### ProposalEngine (Boolean Voting)\n\n#### Constructor\n\n```motoko\nProposalEngine\u003csystem, TProposalContent\u003e(\n    data: StableData\u003cTProposalContent\u003e,\n    onProposalAdopt: Proposal\u003cTProposalContent\u003e -\u003e async* Result.Result\u003c(), Text\u003e,\n    onProposalReject: Proposal\u003cTProposalContent\u003e -\u003e async* (),\n    onProposalValidate: TProposalContent -\u003e async* Result.Result\u003c(), [Text])\n)\n```\n\n#### Methods\n\n**`getProposal(id: Nat) : ?Proposal\u003cTProposalContent\u003e`**\n\nReturns a proposal by its ID.\n\n**`getProposals(count: Nat, offset: Nat) : PagedResult\u003cProposal\u003cTProposalContent\u003e\u003e`**\n\nRetrieves a paged list of proposals, sorted by creation time (newest first).\n\n**`getVote(proposalId: Nat, voterId: Principal) : ?Vote\u003cBool\u003e`**\n\nRetrieves a specific voter's vote on a proposal.\n\n**`buildVotingSummary(proposalId: Nat) : VotingSummary`**\n\nBuilds a voting summary showing vote tallies and statistics.\n\n**`vote(proposalId: Nat, voterId: Principal, vote: Bool) : async* Result.Result\u003c(), VoteError\u003e`**\n\nCasts a vote on a proposal. Returns error if voter is not eligible or voting is closed.\n\n**`createProposal\u003csystem\u003e(proposerId: Principal, content: TProposalContent, members: [Member], votingMode: VotingMode) : async* Result.Result\u003cNat, CreateProposalError\u003e`**\n\nCreates a new proposal. Returns the proposal ID on success.\n\n**`addMember(proposalId: Nat, member: Member) : Result.Result\u003c(), AddMemberResult\u003e`**\n\nAdds a member to a dynamic proposal during voting.\n\n**`endProposal(proposalId: Nat) : async* Result.Result\u003c(), { #alreadyEnded }\u003e`**\n\nManually ends a proposal before its natural end time.\n\n**`toStableData() : StableData\u003cTProposalContent\u003e`**\n\nConverts the current state to stable data for upgrades.\n\n### ExtendedProposalEngine (Multi-Choice Voting)\n\n#### Constructor\n\n```motoko\nProposalEngine\u003csystem, TProposalContent, TChoice\u003e(\n    data: StableData\u003cTProposalContent, TChoice\u003e,\n    onProposalExecute: (?TChoice, Proposal\u003cTProposalContent, TChoice\u003e) -\u003e async* Result.Result\u003c(), Text\u003e,\n    onProposalValidate: TProposalContent -\u003e async* Result.Result\u003c(), [Text]),\n    compareChoice: (TChoice, TChoice) -\u003e Order.Order,\n)\n```\n\n#### Methods\n\n**`getProposal(id: Nat) : ?Proposal\u003cTProposalContent, TChoice\u003e`**\n\nReturns a proposal by its ID.\n\n**`getProposals(count: Nat, offset: Nat) : PagedResult\u003cProposal\u003cTProposalContent, TChoice\u003e\u003e`**\n\nRetrieves a paged list of proposals, sorted by creation time (newest first).\n\n**`getVote(proposalId: Nat, voterId: Principal) : ?Vote\u003cTChoice\u003e`**\n\nRetrieves a specific voter's vote on a proposal.\n\n**`buildVotingSummary(proposalId: Nat) : VotingSummary\u003cTChoice\u003e`**\n\nBuilds a voting summary showing vote tallies and statistics.\n\n**`vote(proposalId: Nat, voterId: Principal, vote: TChoice) : async* Result.Result\u003c(), VoteError\u003e`**\n\nCasts a vote on a proposal with a custom choice type.\n\n**`createProposal\u003csystem\u003e(proposerId: Principal, content: TProposalContent, members: [Member], votingMode: VotingMode) : async* Result.Result\u003cNat, CreateProposalError\u003e`**\n\nCreates a new proposal. Returns the proposal ID on success.\n\n**`addMember(proposalId: Nat, member: Member) : Result.Result\u003c(), AddMemberResult\u003e`**\n\nAdds a member to a dynamic proposal during voting.\n\n**`endProposal(proposalId: Nat) : async* Result.Result\u003c(), { #alreadyEnded }\u003e`**\n\nManually ends a proposal before its natural end time.\n\n**`toStableData() : StableData\u003cTProposalContent, TChoice\u003e`**\n\nConverts the current state to stable data for upgrades.\n\n## Voting Modes\n\n### Snapshot Mode (`#snapshot`)\n\n- Member list is fixed at proposal creation\n- No members can be added during voting\n- Suitable for formal governance where membership is predetermined\n\n### Dynamic Mode (`#dynamic`)\n\n- Members can be added during the voting period\n- Optionally specify total voting power for threshold calculations\n- Suitable for evolving communities or stake-based voting\n\n## Voting Thresholds\n\n### Percentage Threshold\n\n```motoko\n#percent({ percent = 50; quorum = ?25 })\n```\n\n- `percent`: Required percentage of votes to pass (0-100)\n- `quorum`: Optional minimum participation percentage\n\n**Threshold Calculation:**\n\n- Before proposal end: Threshold applies to total possible voting power\n- After proposal end: Threshold applies only to votes cast\n- Dynamic proposals: Stay undetermined even when threshold is met (manual execution required)\n\n## Examples\n\n### Governance Proposal\n\n```motoko\ntype GovernanceProposal = {\n    title: Text;\n    description: Text;\n    action: {\n        #updateConfig: { key: Text; value: Text };\n        #addMember: Principal;\n        #removeMember: Principal;\n    };\n};\n\nlet proposal = await* engine.createProposal(\n    caller,\n    {\n        title = \"Update Configuration\";\n        description = \"Change max proposal duration to 14 days\";\n        action = #updateConfig({ key = \"maxDuration\"; value = \"14\" });\n    },\n    members,\n    #snapshot\n);\n```\n\n### Multi-Choice Budget Proposal\n\n```motoko\ntype BudgetChoice = {\n    #allocateToMarketing: Nat;\n    #allocateToEngineering: Nat;\n    #allocateToOperations: Nat;\n    #rejectBudget;\n};\n\nlet proposalId = await* extendedEngine.createProposal(\n    caller,\n    budgetProposalContent,\n    stakeholders,\n    #dynamic({ totalVotingPower = ?totalStake })\n);\n\n// Stakeholders vote on budget allocation\nlet _ = await* extendedEngine.vote(proposalId, stakeholderA, #allocateToEngineering(500_000));\nlet _ = await* extendedEngine.vote(proposalId, stakeholderB, #allocateToMarketing(300_000));\n```\n\n## Testing\n\n```bash\nmops test\n```\n\n## License\n\nThis project is licensed under the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedjcase%2Fmotoko_proposal_engine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedjcase%2Fmotoko_proposal_engine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedjcase%2Fmotoko_proposal_engine/lists"}