diff --git a/.gitignore b/.gitignore index 76c2842..a9e79be 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,13 @@ coverage *.info costs-reports.json node_modules + +# Personal notes and documentation templates +MY_3_POSTS.md +SOCIAL_POSTS_TEMPLATES.md +DEPLOYMENT_STEPS.md +DEPLOYMENT_INFO.md +FEATURE_DOCUMENTATION.md +PR_DESCRIPTION.md +deployments/voting-only.yaml +deployments/deploy-voting-only.yaml diff --git a/Clarinet.toml b/Clarinet.toml index 997f92c..5b59e3e 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -5,10 +5,17 @@ authors = [] telemetry = false cache_dir = './.cache' requirements = [] + [contracts.tic-tac-toe] path = 'contracts/tic-tac-toe.clar' clarity_version = 3 epoch = 3.0 + +[contracts.voting] +path = 'contracts/voting.clar' +clarity_version = 3 +epoch = 3.0 + [repl.analysis] passes = ['check_checker'] @@ -16,4 +23,4 @@ passes = ['check_checker'] strict = false trusted_sender = false trusted_caller = false -callee_filter = false +callee_filter = false \ No newline at end of file diff --git a/contracts/voting.clar b/contracts/voting.clar new file mode 100644 index 0000000..7d5ab57 --- /dev/null +++ b/contracts/voting.clar @@ -0,0 +1,120 @@ +;; Voting Contract for Game Proposals +;; Allows community to vote on game rule changes + +;; Constants +(define-constant contract-owner tx-sender) +(define-constant err-owner-only (err u100)) +(define-constant err-not-found (err u101)) +(define-constant err-already-voted (err u102)) +(define-constant err-proposal-closed (err u103)) + +;; Data Variables +(define-data-var proposal-count uint u0) + +;; Data Maps +(define-map proposals + { proposal-id: uint } + { + title: (string-ascii 100), + description: (string-ascii 500), + proposer: principal, + yes-votes: uint, + no-votes: uint, + is-active: bool, + created-at: uint + } +) + +(define-map votes + { proposal-id: uint, voter: principal } + { vote: bool } +) + +;; Public Functions + +;; Create a new proposal +(define-public (create-proposal (title (string-ascii 100)) (description (string-ascii 500))) + (let + ( + (proposal-id (var-get proposal-count)) + ) + (map-set proposals + { proposal-id: proposal-id } + { + title: title, + description: description, + proposer: tx-sender, + yes-votes: u0, + no-votes: u0, + is-active: true, + created-at: stacks-block-height + } + ) + (var-set proposal-count (+ proposal-id u1)) + (ok proposal-id) + ) +) + +;; Vote on a proposal +(define-public (vote (proposal-id uint) (vote-yes bool)) + (let + ( + (proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) err-not-found)) + (voter tx-sender) + ) + ;; Check if proposal is active + (asserts! (get is-active proposal) err-proposal-closed) + + ;; Check if user hasn't voted yet + (asserts! (is-none (map-get? votes { proposal-id: proposal-id, voter: voter })) err-already-voted) + + ;; Record vote + (map-set votes + { proposal-id: proposal-id, voter: voter } + { vote: vote-yes } + ) + + ;; Update vote counts + (map-set proposals + { proposal-id: proposal-id } + (merge proposal { + yes-votes: (if vote-yes (+ (get yes-votes proposal) u1) (get yes-votes proposal)), + no-votes: (if vote-yes (get no-votes proposal) (+ (get no-votes proposal) u1)) + }) + ) + (ok true) + ) +) + +;; Close a proposal (owner only) +(define-public (close-proposal (proposal-id uint)) + (let + ( + (proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) err-not-found)) + ) + (asserts! (is-eq tx-sender contract-owner) err-owner-only) + (map-set proposals + { proposal-id: proposal-id } + (merge proposal { is-active: false }) + ) + (ok true) + ) +) + +;; Read-only Functions + +(define-read-only (get-proposal (proposal-id uint)) + (map-get? proposals { proposal-id: proposal-id }) +) + +(define-read-only (get-vote (proposal-id uint) (voter principal)) + (map-get? votes { proposal-id: proposal-id, voter: voter }) +) + +(define-read-only (get-proposal-count) + (ok (var-get proposal-count)) +) + +(define-read-only (has-voted (proposal-id uint) (voter principal)) + (is-some (map-get? votes { proposal-id: proposal-id, voter: voter })) +) diff --git a/deployments/default.devnet-plan.yaml b/deployments/default.devnet-plan.yaml new file mode 100644 index 0000000..8e67eab --- /dev/null +++ b/deployments/default.devnet-plan.yaml @@ -0,0 +1,25 @@ +--- +id: 0 +name: Devnet deployment +network: devnet +stacks-node: "http://localhost:20443" +bitcoin-node: "http://devnet:devnet@localhost:18443" +plan: + batches: + - id: 0 + transactions: + - contract-publish: + contract-name: tic-tac-toe + expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + cost: 90730 + path: "contracts\\tic-tac-toe.clar" + anchor-block-only: true + clarity-version: 3 + - contract-publish: + contract-name: voting + expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + cost: 30230 + path: "contracts\\voting.clar" + anchor-block-only: true + clarity-version: 3 + epoch: "3.0" diff --git a/deployments/default.simnet-plan.yaml b/deployments/default.simnet-plan.yaml index 3c90280..b98f562 100644 --- a/deployments/default.simnet-plan.yaml +++ b/deployments/default.simnet-plan.yaml @@ -7,33 +7,43 @@ genesis: - name: deployer address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM balance: "100000000000000" + sbtc-balance: "1000000000" - name: faucet address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_1 address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_2 address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_3 address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_4 address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_5 address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_6 address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_7 address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ balance: "100000000000000" + sbtc-balance: "1000000000" - name: wallet_8 address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP balance: "100000000000000" + sbtc-balance: "1000000000" contracts: - costs - pox @@ -54,4 +64,9 @@ plan: emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM path: contracts/tic-tac-toe.clar clarity-version: 3 + - emulated-contract-publish: + contract-name: voting + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/voting.clar + clarity-version: 3 epoch: "3.0" diff --git a/deployments/default.testnet-plan.yaml b/deployments/default.testnet-plan.yaml index fba5452..0c801bd 100644 --- a/deployments/default.testnet-plan.yaml +++ b/deployments/default.testnet-plan.yaml @@ -10,9 +10,16 @@ plan: transactions: - contract-publish: contract-name: tic-tac-toe - expected-sender: ST3P49R8XXQWG69S66MZASYPTTGNDKK0WW32RRJDN + expected-sender: STA43VC8660WWNRHHWSXGK2VR4BVHGWN0Z63FXGD cost: 90730 - path: contracts/tic-tac-toe.clar + path: "contracts\\tic-tac-toe.clar" + anchor-block-only: true + clarity-version: 3 + - contract-publish: + contract-name: voting + expected-sender: STA43VC8660WWNRHHWSXGK2VR4BVHGWN0Z63FXGD + cost: 30230 + path: "contracts\\voting.clar" anchor-block-only: true clarity-version: 3 epoch: "3.0" diff --git a/frontend/app/voting/page.tsx b/frontend/app/voting/page.tsx new file mode 100644 index 0000000..a865524 --- /dev/null +++ b/frontend/app/voting/page.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { getAllProposals, type Proposal } from "@/lib/contract"; +import { ProposalsList } from "@/components/proposals-list"; +import { CreateProposal } from "@/components/create-proposal"; +import { useEffect, useState } from "react"; + +export default function VotingPage() { + const [proposals, setProposals] = useState([]); + const [loading, setLoading] = useState(true); + + const loadProposals = async () => { + setLoading(true); + try { + const data = await getAllProposals(); + setProposals(data); + } catch (error) { + console.error("Error loading proposals:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadProposals(); + }, []); + + return ( +
+
+

Community Voting

+ + Propose and vote on game improvements + +
+ +
+ + {loading ? ( +
Loading proposals...
+ ) : ( + + )} +
+
+ ); +} diff --git a/frontend/components/create-proposal.tsx b/frontend/components/create-proposal.tsx new file mode 100644 index 0000000..385e2bc --- /dev/null +++ b/frontend/components/create-proposal.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useStacks } from "@/hooks/use-stacks"; +import { useState } from "react"; + +export function CreateProposal() { + const { userData, handleCreateProposal } = useStacks(); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!title || !description) return; + + setIsSubmitting(true); + try { + await handleCreateProposal(title, description); + setTitle(""); + setDescription(""); + } finally { + setIsSubmitting(false); + } + }; + + if (!userData) { + return ( +
+

Connect your wallet to create proposals

+
+ ); + } + + return ( +
+

Create New Proposal

+
+
+ + setTitle(e.target.value)} + maxLength={100} + placeholder="Add 5x5 board option" + className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + disabled={isSubmitting} + required + /> +

{title.length}/100 characters

+
+ +
+ +