├── .env.development ├── .env.production ├── .gitignore ├── LICENSE ├── README.md ├── clarity ├── ballot.clar └── nft.clar ├── common └── constants.js ├── components ├── builder │ ├── BuilderComponent.js │ └── Preview.component.js ├── common │ ├── DashboardNavBarComponent.js │ ├── HeaderComponent.js │ ├── InformationComponent.js │ └── MyVotesPopup.js ├── dashboard │ └── DashboardAllPollsComponent.js ├── home │ └── accordion.js ├── poll │ ├── PollComponent.js │ ├── PollService.js │ └── QRCodePopup.js └── summary │ ├── BuilderComponent.js │ ├── ChoosePollsPopup.js │ └── SummaryComponent.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [...id].js ├── [param].js ├── _app.js ├── all-polls.js ├── api │ └── hello.js ├── builder │ └── [[...id]].js ├── index.js └── summary │ └── index.js ├── public ├── favicon.ico ├── images │ ├── ballot-meta.png │ └── logo │ │ └── ballot.png └── vercel.svg ├── services ├── auth.js ├── contract.js └── utils.js ├── styles ├── Accordion.module.css ├── Builder.module.css ├── ChoosePollsPopup.module.css ├── Dashboard.module.css ├── Home.module.css ├── Poll.module.css ├── QRCodePopup.module.css └── globals.css └── yarn.lock /.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_STACKS_MAINNET_FLAG=false 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_STACKS_MAINNET_FLAG=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 BlockSurvey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ballot.gg 2 | 3 | Decentralized Polls on Stacks 4 | 5 | ## FAQ 6 | 7 | 1. What is Ballot.gg? 8 | The Ballot is a decentralized polling app for DAO, NFT, DeFi, and Web 3 projects that puts community members at the center to come to a consensus on important decisions. Polls will be gated based on holdings of tokens, .BTC namespaces, and NFTs. 9 | 10 | 2. How does Ballot.gg help? 11 | Ballot.gg will help projects in the Stacks community to utilize tokens to govern decision-making on their platform. It will allow DAOs, NFTs, and DeFi's to get broad community consensus regarding proposed changes or ideas in a transparent and verifiable way. 12 | 13 | 3. How does Ballot.gg help Stacks community? 14 | Polling for consensus has been around for years and is today used in politics to make decisions (eg. Brexit in the UK). Ballot makes it easy to deploy or integrate a poll into your project. Stacks community members can create polls for almost anything they want to know as a collective. Ballot will open up Stacks community members to be actively engaged and get to know how other community members think about things. 15 | 16 | 4. Is Ballot.gg open source? 17 | Yes. The source of the smart contact is available here. 18 | 19 | 5. Is Ballot.gg free? 20 | Yes. There are no charges for creating polls in Ballot. 21 | 22 | 6. Who is the team behind Ballot.gg? 23 | We are developers from Team [BlockSurvey ↗](https://blocksurvey.io/?ref=ballot). 24 | 25 | ## Getting Started 26 | 27 | Ballot.gg is built using Next.js and interacts with Stacks Blockchain. 28 | 29 | Run the development server: 30 | 31 | ```bash 32 | npm run dev 33 | # or 34 | yarn dev 35 | ``` 36 | 37 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 38 | 39 | ## License 40 | 41 | [MIT License](LICENSE) 42 | -------------------------------------------------------------------------------- /clarity/ballot.clar: -------------------------------------------------------------------------------- 1 | 2 | ;; ballot 3 | 4 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 5 | ;; Constants 6 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 7 | (define-constant CONTRACT-OWNER tx-sender) 8 | ;; Errors 9 | (define-constant ERR-NOT-STARTED (err u1001)) 10 | (define-constant ERR-ENDED (err u1002)) 11 | (define-constant ERR-ALREADY-VOTED (err u1003)) 12 | (define-constant ERR-FAILED-STRATEGY (err u1004)) 13 | (define-constant ERR-NOT-VOTED (err u1005)) 14 | 15 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 16 | ;; data maps and vars 17 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 18 | (define-data-var title (string-utf8 512) u"") 19 | (define-data-var description (string-utf8 512) u"") 20 | (define-data-var voting-system (string-ascii 512) "") 21 | (define-data-var start uint u0) 22 | (define-data-var end uint u0) 23 | (define-map token-ids-map {token-id: uint} {user: principal, vote-id: uint}) 24 | (define-map btc-holder-map {domain: (buff 20), namespace: (buff 48)} {user: principal, vote-id: uint}) 25 | (define-map results {id: (string-ascii 36)} {count: uint, name: (string-utf8 256)} ) 26 | (define-map users {id: principal} {id: uint, vote: (list 2 (string-ascii 36)), volume: (list 2 uint), voting-power: uint}) 27 | (define-map register {id: uint} {user: principal, vote: (list 2 (string-ascii 36)), volume: (list 2 uint), voting-power: uint}) 28 | (define-data-var total uint u0) 29 | (define-data-var total-votes uint u0) 30 | (define-data-var options (list 2 (string-ascii 36)) (list)) 31 | (define-data-var temp-voting-power uint u0) 32 | 33 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 34 | ;; private functions 35 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 36 | (define-private (get-voting-power-by-bns-holder (domain (buff 20)) (namespace (buff 48))) 37 | (let 38 | ( 39 | (bns-owner (get owner (unwrap-panic (contract-call? 'SP000000000000000000002Q6VF78.bns name-resolve domain namespace)))) 40 | ) 41 | 42 | (if (is-eq tx-sender bns-owner) 43 | (match (map-get? btc-holder-map {domain: domain, namespace: namespace}) 44 | result 45 | u0 46 | u1 47 | ) 48 | u0 49 | ) 50 | ) 51 | ) 52 | 53 | (define-private (validate-nft-ownership (token-id uint)) 54 | (let 55 | ( 56 | (vote-id (+ u1 (var-get total))) 57 | (nft-owner-optional (unwrap-panic (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract get-owner token-id))) 58 | ) 59 | 60 | (match nft-owner-optional 61 | nft-owner 62 | (if (is-eq tx-sender nft-owner) 63 | (match (map-get? token-ids-map {token-id: token-id}) 64 | result 65 | u0 66 | (if (map-set token-ids-map {token-id: token-id} {user: tx-sender, vote-id: vote-id}) 67 | u1 68 | u0 69 | ) 70 | ) 71 | u0 72 | ) 73 | u0 74 | ) 75 | ) 76 | ) 77 | 78 | (define-private (get-voting-power-by-nft-holdings (token-ids (list 60000 uint))) 79 | (fold + (map validate-nft-ownership token-ids) u0) 80 | ) 81 | 82 | (define-private (get-voting-power-by-stx-holdings) 83 | (let 84 | ( 85 | (stx-balance (stx-get-balance tx-sender)) 86 | ) 87 | (if (> stx-balance u0) 88 | (/ stx-balance u1000000) 89 | stx-balance 90 | ) 91 | ) 92 | ) 93 | 94 | (define-read-only (get-voting-power-by-ft-holdings) 95 | (let 96 | ( 97 | (ft-balance (unwrap-panic (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract get-balance tx-sender))) 98 | (ft-decimals (unwrap-panic (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract get-decimals))) 99 | ) 100 | 101 | (if (> ft-balance u0) 102 | (if (> ft-decimals u0) 103 | (/ ft-balance (pow u10 ft-decimals)) 104 | ft-balance 105 | ) 106 | ft-balance 107 | ) 108 | ) 109 | ) 110 | 111 | (define-private (have-i-voted) 112 | (match (map-get? users {id: tx-sender}) 113 | success true 114 | false 115 | ) 116 | ) 117 | 118 | (define-private (fold-boolean (left bool) (right bool)) 119 | (and (is-eq left true) (is-eq right true)) 120 | ) 121 | 122 | (define-private (check-volume (each-volume uint)) 123 | (> each-volume u0) 124 | ) 125 | 126 | (define-private (validate-vote-volume (volume (list 2 uint))) 127 | (begin 128 | (fold fold-boolean (map check-volume volume) true) 129 | ) 130 | ) 131 | 132 | (define-private (get-volume-by-voting-power (volume uint)) 133 | (var-get temp-voting-power) 134 | ) 135 | 136 | (define-private (get-pow-value (volume uint)) 137 | (pow volume u2) 138 | ) 139 | 140 | (define-private (process-my-vote (option-id (string-ascii 36)) (volume uint)) 141 | (match (map-get? results {id: option-id}) 142 | result (let 143 | ( 144 | (new-count-tuple {count: (+ volume (get count result))}) 145 | ) 146 | 147 | ;; Capture the vote 148 | (map-set results {id: option-id} (merge result new-count-tuple)) 149 | 150 | ;; Return 151 | true 152 | ) 153 | false 154 | ) 155 | ) 156 | 157 | (define-private (get-single-result (option-id (string-ascii 36))) 158 | (let 159 | ( 160 | (volume (default-to u0 (get count (map-get? results {id: option-id})))) 161 | ) 162 | 163 | ;; Return volume 164 | volume 165 | ) 166 | ) 167 | 168 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 169 | ;; public functions for all 170 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 171 | (define-public (cast-my-vote (vote (list 2 (string-ascii 36))) (volume (list 2 uint)) 172 | (bns (string-ascii 256)) (domain (buff 20)) (namespace (buff 48)) (token-ids (list 60000 uint)) 173 | ) 174 | (let 175 | ( 176 | (vote-id (+ u1 (var-get total))) 177 | ;; Strategy applied 178 | ;; (voting-power (get-voting-power-by-bns-holder domain namespace)) 179 | ;; (voting-power (get-voting-power-by-nft-holdings token-ids)) 180 | ;; (voting-power (get-voting-power-by-stx-holdings)) 181 | ;; (voting-power (get-voting-power-by-ft-holdings)) 182 | 183 | ;; No strategy 184 | ;; FPTP and Block voting - No strategy 185 | ;; (voting-power u1) 186 | 187 | ;; Quadratic - No strategy 188 | ;; (voting-power (fold + (map get-pow-value volume) u0)) 189 | 190 | ;; Weighted voting - No strategy 191 | (voting-power (fold + volume u0)) 192 | 193 | ;; FPTP and Block voting 194 | ;; (temp (var-set temp-voting-power voting-power)) 195 | ;; (volume-by-voting-power (map get-volume-by-voting-power volume)) 196 | ;; FPTP and Block voting - Number of votes 197 | ;; (my-votes voting-power) 198 | 199 | ;; Quadratic or Weighted voting 200 | (volume-by-voting-power volume) 201 | ;; Quadratic or Weighted voting - Number of votes 202 | (my-votes (fold + volume u0)) 203 | ) 204 | ;; Validation 205 | (asserts! (and (> (len vote) u0) (is-eq (len vote) (len volume-by-voting-power)) (validate-vote-volume volume-by-voting-power)) ERR-NOT-VOTED) 206 | (asserts! (>= tenure-height (var-get start)) ERR-NOT-STARTED) 207 | (asserts! (<= tenure-height (var-get end)) ERR-ENDED) 208 | (asserts! (not (have-i-voted)) ERR-ALREADY-VOTED) 209 | 210 | ;; FPTP and Block voting 211 | ;; (asserts! (> voting-power u0) ERR-FAILED-STRATEGY) 212 | 213 | ;; Quadratic voting 214 | ;; (asserts! (>= voting-power (fold + (map get-pow-value volume-by-voting-power) u0)) ERR-FAILED-STRATEGY) 215 | 216 | ;; Weigted voting 217 | (asserts! (>= voting-power (fold + volume-by-voting-power u0)) ERR-FAILED-STRATEGY) 218 | 219 | ;; Business logic 220 | ;; Process my vote 221 | (map process-my-vote vote volume-by-voting-power) 222 | 223 | ;; Register for reference 224 | (map-set users {id: tx-sender} {id: vote-id, vote: vote, volume: volume-by-voting-power, voting-power: voting-power}) 225 | (map-set register {id: vote-id} {user: tx-sender, vote: vote, volume: volume-by-voting-power, voting-power: voting-power}) 226 | 227 | ;; Increase the total votes 228 | (var-set total-votes (+ my-votes (var-get total-votes))) 229 | 230 | ;; Increase the total 231 | (var-set total vote-id) 232 | 233 | ;; Return 234 | (ok true) 235 | ) 236 | ) 237 | 238 | (define-read-only (get-results) 239 | (begin 240 | (ok { 241 | total: (var-get total), 242 | total-votes: (var-get total-votes), 243 | options: (var-get options), 244 | results: (map get-single-result (var-get options)) 245 | }) 246 | ) 247 | ) 248 | 249 | (define-read-only (get-result-at-position (position uint)) 250 | (ok (map-get? register {id: position})) 251 | ) 252 | 253 | (define-read-only (get-result-by-user (user principal)) 254 | (ok (map-get? users {id: user})) 255 | ) 256 | 257 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 258 | ;; Default assignments 259 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 260 | (var-set title u"BlockSurvey Poll") 261 | (var-set description u"Description") 262 | (var-set voting-system "First past the post") 263 | (var-set options (list "option1" "option2")) 264 | (var-set start u1) 265 | (var-set end u1) 266 | (map-set results {id: "option1"} {count: u0, name: u"Yes"}) 267 | (map-set results {id: "option2"} {count: u0, name: u"No"}) -------------------------------------------------------------------------------- /clarity/nft.clar: -------------------------------------------------------------------------------- 1 | ;; (impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) 2 | 3 | (define-constant contract-owner tx-sender) 4 | (define-constant err-owner-only (err u100)) 5 | (define-constant err-not-token-owner (err u101)) 6 | 7 | (define-non-fungible-token stacksies uint) 8 | 9 | (define-data-var last-token-id uint u0) 10 | 11 | (define-read-only (get-last-token-id) 12 | (ok (var-get last-token-id)) 13 | ) 14 | 15 | (define-read-only (get-token-uri (token-id uint)) 16 | (ok none) 17 | ) 18 | 19 | (define-read-only (get-owner (token-id uint)) 20 | (ok (nft-get-owner? stacksies token-id)) 21 | ) 22 | 23 | (define-public (transfer (token-id uint) (sender principal) (recipient principal)) 24 | (begin 25 | (asserts! (is-eq tx-sender sender) err-not-token-owner) 26 | (nft-transfer? stacksies token-id sender recipient) 27 | ) 28 | ) 29 | 30 | (define-public (mint (recipient principal)) 31 | (let 32 | ( 33 | (token-id (+ (var-get last-token-id) u1)) 34 | ) 35 | (asserts! (is-eq tx-sender contract-owner) err-owner-only) 36 | (try! (nft-mint? stacksies token-id recipient)) 37 | (var-set last-token-id token-id) 38 | (ok token-id) 39 | ) 40 | ) -------------------------------------------------------------------------------- /common/constants.js: -------------------------------------------------------------------------------- 1 | export const Constants = { 2 | // Stacks mainnet network flag 3 | STACKS_MAINNET_FLAG: process.env.NEXT_PUBLIC_STACKS_MAINNET_FLAG === "false" ? false : true, 4 | 5 | GAIA_HUB_PREFIX: "https://gaia.blockstack.org/hub/", 6 | 7 | // Stacks API URLs 8 | STACKS_MAINNET_API_URL: "https://api.mainnet.hiro.so", 9 | STACKS_TESTNET_API_URL: "https://api.testnet.hiro.so", 10 | 11 | // IPFS gateway 12 | IPFS_GATEWAY: "https://cloudflare-ipfs.com/ipfs/", 13 | 14 | VOTING_SYSTEMS: [ 15 | { 16 | "id": "fptp", 17 | "name": "First-past-the-post" 18 | }, { 19 | "id": "block", 20 | "name": "Block Voting" 21 | }, { 22 | "id": "quadratic", 23 | "name": "Quadratic Voting" 24 | }, { 25 | "id": "weighted", 26 | "name": "Weighted Voting" 27 | } 28 | ], 29 | 30 | STRATEGY_TEMPLATES: [ 31 | { 32 | "id": "alex", 33 | "name": "ALEX", 34 | "strategyTokenType": "ft", 35 | "strategyTokenName": "alex", 36 | "strategyContractName": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token", 37 | "strategyTokenDecimals": "8" 38 | }, { 39 | "id": "blocksurvey", 40 | "name": "BlockSurvey", 41 | "strategyTokenType": "nft", 42 | "strategyTokenName": "blocksurvey", 43 | "strategyContractName": "SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.blocksurvey" 44 | }, { 45 | "id": "btcholders", 46 | "name": ".btc Namespace", 47 | "strategyTokenType": "nft", 48 | "strategyTokenName": ".btc Namespace", 49 | "strategyContractName": "" 50 | }, { 51 | "id": "crashpunks", 52 | "name": "CrashPunks", 53 | "strategyTokenType": "nft", 54 | "strategyTokenName": "crashpunks-v2", 55 | "strategyContractName": "SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2" 56 | }, { 57 | "id": "miamicoin", 58 | "name": "MIA", 59 | "strategyTokenType": "ft", 60 | "strategyTokenName": "miamicoin", 61 | "strategyContractName": "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2", 62 | "strategyTokenDecimals": "6" 63 | }, { 64 | "id": "newyorkcitycoin", 65 | "name": "NYC", 66 | "strategyTokenType": "ft", 67 | "strategyTokenName": "newyorkcitycoin", 68 | "strategyContractName": "SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5.newyorkcitycoin-token", 69 | "strategyTokenDecimals": "0" 70 | }, { 71 | "id": "satoshibles", 72 | "name": "Satoshibles", 73 | "strategyTokenType": "nft", 74 | "strategyTokenName": "Satoshibles", 75 | "strategyContractName": "SP6P4EJF0VG8V0RB3TQQKJBHDQKEF6NVRD1KZE3C.satoshibles" 76 | }, { 77 | "id": "stacksparrots", 78 | "name": "Stacks Parrots", 79 | "strategyTokenType": "nft", 80 | "strategyTokenName": "stacks-parrots", 81 | "strategyContractName": "SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.byzantion-stacks-parrots" 82 | }, { 83 | "id": "stx", 84 | "name": "STX", 85 | "strategyTokenType": "ft", 86 | "strategyTokenName": "STX", 87 | "strategyContractName": "", 88 | "strategyTokenDecimals": "6" 89 | }, { 90 | "id": "theexplorerguild", 91 | "name": "The Explorer Guild", 92 | "strategyTokenType": "nft", 93 | "strategyTokenName": "The-Explorer-Guild", 94 | "strategyContractName": "SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.the-explorer-guild" 95 | } 96 | ], 97 | 98 | TOKEN_TYPES: [{ id: "nft", name: "Non Fungible Token" }, { id: "ft", name: "Fungible Token" }], 99 | 100 | // FAQs 101 | FAQ: [ 102 | { 103 | question: "What is Ballot.gg?", 104 | answer: `The Ballot is a decentralized polling app for DAO, NFT, DeFi, and Web 3 projects that puts community members at the center to come to a consensus on important decisions. Polls will be gated based on holdings of tokens, .BTC namespaces, and NFTs.`, 105 | }, 106 | { 107 | question: "How does Ballot.gg help?", 108 | answer: `Ballot.gg will help projects in the Stacks community to utilize tokens to govern decision-making on their platform. It will allow DAOs, NFTs, and DeFi's to get broad community consensus regarding proposed changes or ideas in a transparent and verifiable way.`, 109 | }, 110 | { 111 | question: "How does Ballot.gg help Stacks community?", 112 | answer: `Polling for consensus has been around for years and is today used in politics to make decisions (eg. Brexit in the UK). Ballot makes it easy to deploy or integrate a poll into your project. Stacks community members can create polls for almost anything they want to know as a collective. Ballot will open up Stacks community members to be actively engaged and get to know how other community members think about things.`, 113 | }, 114 | { 115 | question: "Is Ballot.gg open source?", 116 | answer: `Yes. The source of the UI and Smart Contract ↗ is available here.`, 117 | }, 118 | { 119 | question: "Is Ballot.gg free?", 120 | answer: `Yes. There are no charges for creating polls in Ballot.`, 121 | }, 122 | { 123 | question: "Who are the developers of Ballot.gg?", 124 | answer: `We are developers from Team BlockSurvey ↗ , 125 | Owl Link ↗ , 126 | Checklist ↗.`, 127 | } 128 | ], 129 | 130 | // Voting system document links 131 | VOTING_SYSTEM_DOCUMENTATION: { 132 | "fptp": { 133 | "id": "fptp", 134 | "name": "First-past-the-post", 135 | "link": "https://docs.ballot.gg/ballot.gg/voting-system/first-past-the-post" 136 | }, "block": { 137 | "id": "block", 138 | "name": "Block Voting", 139 | "link": "https://docs.ballot.gg/ballot.gg/voting-system/block-voting" 140 | }, "quadratic": { 141 | "id": "quadratic", 142 | "name": "Quadratic Voting", 143 | "link": "https://docs.ballot.gg/ballot.gg/voting-system/quadratic-voting" 144 | }, "weighted": { 145 | "id": "weighted", 146 | "name": "Weighted Voting", 147 | "link": "https://docs.ballot.gg/ballot.gg/voting-system/weighted-voting" 148 | } 149 | }, 150 | 151 | // Ballot.gg wallet address for donations 152 | MAINNET_DONATION_ADDRESS: "SP1FQ3G3MYSXW68CWPY4GW342T3Y9HQCCXXCKENPH", 153 | TESTNET_DONATION_ADDRESS: "ST2FYE64JK2NMRS1640FE9SKJS37CYYJ3B1EHB6AR", 154 | }; 155 | -------------------------------------------------------------------------------- /components/builder/Preview.component.js: -------------------------------------------------------------------------------- 1 | import { Modal } from "react-bootstrap"; 2 | import PollComponent from "../poll/PollComponent"; 3 | 4 | export default function PreviewComponent(props) { 5 | const { show, handleClose, pollObject } = props; 6 | 7 | return ( 8 | <> 9 | 10 | 11 | Preview 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } -------------------------------------------------------------------------------- /components/common/DashboardNavBarComponent.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import { Button } from "react-bootstrap"; 4 | import Dropdown from 'react-bootstrap/Dropdown'; 5 | import DropdownButton from 'react-bootstrap/DropdownButton'; 6 | import { authenticate, signOut, switchAccount, userSession } from "../../services/auth"; 7 | import { getDomainNamesFromBlockchain } from "../../services/utils"; 8 | import MyVotePopup from "./MyVotesPopup"; 9 | 10 | export function DashboardNavBarComponent() { 11 | // Variables 12 | const [displayUsername, setDisplayUsername] = useState(); 13 | const [isUserSignedIn, setIsUserSignedIn] = useState(false); 14 | 15 | // My votes popup 16 | const [showMyVotesPopup, setShowMyVotesPopup] = useState(false); 17 | const handleCloseMyVotesPopup = () => 18 | setShowMyVotesPopup(false); 19 | const handleShowMyVotesPopup = () => setShowMyVotesPopup(true); 20 | 21 | // Feedback hidden button 22 | const feedbackButton = useRef(null); 23 | 24 | // Functions 25 | useEffect(() => { 26 | getDisplayUsername(); 27 | 28 | if (userSession && userSession.isUserSignedIn()) { 29 | setIsUserSignedIn(true) 30 | } 31 | }, []); 32 | 33 | const getDisplayUsername = async () => { 34 | const _username = await getDomainNamesFromBlockchain(); 35 | setDisplayUsername(_username); 36 | } 37 | 38 | // UI 39 | return ( 40 | <> 41 |
45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 | { 53 | isUserSignedIn ? 54 |
55 |
56 | 57 | 63 | 64 |
65 | 66 | {/*
67 | 68 | 71 | 72 |
*/} 73 | 74 | {/* Profile */} 75 |
76 | 82 | { 84 | handleShowMyVotesPopup(); 85 | }} 86 | > 87 | My votes 88 | 89 | 91 | Summary page 92 | 93 | { 95 | switchAccount(window?.location?.href); 96 | }} 97 | > 98 | Switch account 99 | 100 | 101 | 104 | Share feedback 105 | 106 | { signOut() }}>Logout 107 | 108 |
109 |
110 | : 111 |
112 | 116 |
117 | } 118 |
119 | 120 | {/* My votes popup */} 121 | 125 | 126 | ); 127 | } -------------------------------------------------------------------------------- /components/common/HeaderComponent.js: -------------------------------------------------------------------------------- 1 | import { 2 | formStacksExplorerUrl, 3 | openFacebookUrl, 4 | openLinkedinUrl, 5 | openRedditUrl, 6 | openTelegramUrl, 7 | openTwitterUrl, 8 | openWhatsappUrl 9 | } from "../../services/utils"; 10 | import Tooltip from "react-bootstrap/Tooltip"; 11 | import { useState } from "react"; 12 | import OverlayTrigger from "react-bootstrap/OverlayTrigger"; 13 | import QRCodePopup from "../poll/QRCodePopup"; 14 | import { Dropdown } from "react-bootstrap"; 15 | import styles from "../../styles/Dashboard.module.css"; 16 | 17 | export default function HeaderComponent(props) { 18 | // Variables 19 | const { pollObject, publicUrl, txStatus } = props; 20 | 21 | const [copyText, setCopyText] = useState("Copy"); 22 | const [showQRCodePopupFlag, setShowQRCodePopupFlag] = useState(false); 23 | 24 | // Function 25 | const copyToClipBoard = () => { 26 | if (pollObject?.id) { 27 | // Update tooltip message 28 | setCopyText("Copied"); 29 | 30 | navigator.clipboard.writeText(publicUrl); 31 | } 32 | }; 33 | 34 | const convertToHrefLink = (text) => { 35 | const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; 36 | return text.replace(urlRegex, function (url) { 37 | return '' + url + ''; 38 | }); 39 | } 40 | 41 | // Design 42 | return ( 43 | <> 44 | {pollObject && pollObject.id && 45 | <> 46 | {/* Title */} 47 |

{pollObject?.title}

48 | 49 | {/* Info Bar */} 50 |
54 |
55 | {/* Created by */} 56 |
57 | Created by {' '} 58 | {pollObject?.userStxAddress && 59 | 60 | 61 | {pollObject?.userStxAddress?.substring(0, 10)} { } 62 | 69 | 75 | 76 | 77 | 78 | } 79 |
80 | 81 | {/* Status */} 82 | {(txStatus && txStatus == "success") && 83 |
84 | { 85 | pollObject?.status == "draft" ? "Draft" : 86 | ((pollObject?.endAtDate && (new Date(pollObject?.endAtDate) < new Date())) ? "Closed" : "Active") 87 | } 88 |
89 | } 90 | {(txStatus && txStatus == "pending") && 91 |
92 | Contract pending 93 |
94 | } 95 | {(txStatus && txStatus != "success" && txStatus != "pending") && 96 |
97 | Contract failed 98 |
99 | } 100 |
101 | 102 |
103 | {/* Copy link */} 104 | {copyText}}> 107 | 115 | 116 | 117 | {/* QR code */} 118 | QR code}> 121 | 127 | 128 | 129 | {/* Share icon */} 130 | 131 | 135 | Share}> 138 |
139 | 140 | 141 | 142 |
143 |
144 |
145 | 146 | 147 | { 149 | openTwitterUrl( 150 | publicUrl, 151 | pollObject?.title 152 | ); 153 | }} 154 | > 155 | Twitter 156 | 157 | { 159 | openFacebookUrl( 160 | publicUrl, 161 | pollObject?.title 162 | ); 163 | }} 164 | > 165 | Facebook 166 | 167 | { 169 | openLinkedinUrl( 170 | publicUrl, 171 | pollObject?.title 172 | ); 173 | }} 174 | > 175 | LinkedIn 176 | 177 | { 179 | openWhatsappUrl( 180 | publicUrl, 181 | pollObject?.title 182 | ); 183 | }} 184 | > 185 | WhatsApp 186 | 187 | { 189 | openTelegramUrl( 190 | publicUrl, 191 | pollObject?.title 192 | ); 193 | }} 194 | > 195 | Telegram 196 | 197 | { 199 | openRedditUrl( 200 | publicUrl, 201 | pollObject?.title 202 | ); 203 | }} 204 | > 205 | Reddit 206 | 207 | 208 |
209 |
210 |
211 | 212 | {/* Description */} 213 |
214 |

216 |

217 |
218 | 219 | } 220 | 221 | 223 | 224 | ); 225 | } -------------------------------------------------------------------------------- /components/common/InformationComponent.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Table } from "react-bootstrap"; 3 | import { Constants } from "../../common/constants"; 4 | import { calculateDateByBlockHeight, convertToDisplayDateFormat, formStacksExplorerUrl, formatUtcDateTime } from "../../services/utils"; 5 | 6 | 7 | export default function InformationComponent(props) { 8 | // Variables 9 | const { pollObject, resultsByOption, currentBlockHeight } = props; 10 | const [votingSystemInfo, setVotingSystemInfo] = useState(); 11 | 12 | // Function 13 | useEffect(() => { 14 | setVotingSystemInfo(Constants.VOTING_SYSTEMS.find(system => system.id == pollObject?.votingSystem)); 15 | }, [pollObject]); 16 | 17 | // Design 18 | return ( 19 | <> 20 | {pollObject && pollObject.id && 21 | <> 22 |
23 | {/* Title */} 24 |
Information
25 | 26 |
27 | { 28 | pollObject?.publishedInfo?.contractAddress && pollObject?.publishedInfo?.contractName && 29 | pollObject?.publishedInfo?.txId && 30 |
31 | Contract 32 | 33 | 34 | {pollObject?.publishedInfo?.contractName.substring(0, 10)} { } 35 | 42 | 48 | 49 | 50 | 51 |
52 | } 53 | { 54 | pollObject?.ipfsLocation && 55 |
56 | IPFS 57 | 58 | 59 | #{pollObject?.ipfsLocation.substring(0, 8)} { } 60 | 67 | 73 | 74 | 75 | 76 |
77 | } 78 | { 79 | pollObject?.strategyContractName && 80 |
81 | Strategy 82 | 83 | 84 | {pollObject?.strategyContractName.substring(0, 10)} { } 85 | 92 | 98 | 99 | 100 | 101 |
102 | } 103 |
104 | Voting System {votingSystemInfo?.name} 105 |
106 |
107 | Start Date 108 | 109 | {pollObject?.startAtDateUTC ? (formatUtcDateTime(pollObject?.startAtDateUTC) + " UTC") : convertToDisplayDateFormat(pollObject?.startAtDate)} 110 | 111 |
112 |
113 | End Date 114 | 115 | {pollObject?.endAtBlock && currentBlockHeight < pollObject?.endAtBlock ? 116 | <> 117 | {formatUtcDateTime(calculateDateByBlockHeight(currentBlockHeight, pollObject?.endAtBlock))} UTC 118 | : 119 | <> 120 | {pollObject?.endAtDateUTC ? (formatUtcDateTime(pollObject?.endAtDateUTC) + " UTC") : convertToDisplayDateFormat(pollObject?.endAtDate)} 121 | 122 | } 123 | 124 |
125 |
126 | Start Tenure Block {pollObject?.startAtBlock} 127 |
128 |
129 | End Tenure Block {pollObject?.endAtBlock} 130 |
131 |
132 | Current Tenure Block {currentBlockHeight} 133 |
134 | {pollObject?.contractAddress && 135 |
136 | Contract Address {pollObject?.contractAddress} 137 |
138 | } 139 |
140 |
141 | 142 |
143 | {/* Title */} 144 |
Current results
145 | 146 |
147 | 148 | 149 | {pollObject?.options?.map((option, index) => ( 150 | 151 | 154 | 163 | 164 | ))} 165 | 166 |
152 | {option?.value} 153 | 155 | 156 | {resultsByOption && resultsByOption[option.id] ? 157 | <> 158 | {resultsByOption[option.id]["percentage"]}% ({resultsByOption[option.id]["total"]}) 159 | 160 | : "-"} 161 | 162 |
167 |
168 |
169 | 170 | } 171 | 172 | ); 173 | } -------------------------------------------------------------------------------- /components/common/MyVotesPopup.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Modal, Spinner, Table } from "react-bootstrap"; 3 | import { getFileFromGaia } from "../../services/auth"; 4 | import { convertToDisplayDateFormat, formStacksExplorerUrl } from "../../services/utils"; 5 | import styles from "../../styles/Dashboard.module.css"; 6 | 7 | export default function MyVotePopup(props) { 8 | const { showMyVotesPopup, handleCloseMyVotesPopup } = props; 9 | 10 | // List of votes 11 | const [votes, setVotes] = useState(); 12 | 13 | // Loading 14 | const [isLoading, setIsLoading] = useState(false); 15 | 16 | useEffect(() => { 17 | if (showMyVotesPopup) { 18 | getMyVotes(); 19 | } 20 | }, [showMyVotesPopup]); 21 | 22 | // Show my polls 23 | const getMyVotes = () => { 24 | // Start loading 25 | setIsLoading(true); 26 | 27 | // Store transaction to Gaia 28 | getFileFromGaia("my_votes_ballot.json").then( 29 | (response) => { 30 | if (response) { 31 | const myVotesObj = JSON.parse(response); 32 | 33 | if ( 34 | myVotesObj && 35 | myVotesObj.votes && 36 | myVotesObj.votes.length > 0 37 | ) { 38 | setVotes(myVotesObj.votes.reverse()); 39 | } else { 40 | // Show empty list 41 | setVotes([]); 42 | } 43 | } else { 44 | // Show empty list 45 | setVotes([]); 46 | } 47 | 48 | // Stop loading 49 | setIsLoading(false); 50 | }, 51 | (error) => { 52 | // File does not exit in gaia 53 | if (error && error.code == "does_not_exist") { 54 | // Show empty list 55 | setVotes([]); 56 | } 57 | 58 | // Stop loading 59 | setIsLoading(false); 60 | } 61 | ); 62 | }; 63 | 64 | const getEachRow = (vote) => { 65 | if (vote?.voteObject && vote?.optionsMap) { 66 | return ( 67 | Object.keys(vote?.voteObject).map((optionId, optionIndex) => ( 68 |
69 | {vote?.optionsMap[optionId]} 70 |
71 | )) 72 | ); 73 | } 74 | 75 | return (<>); 76 | } 77 | 78 | return ( 79 | <> 80 | {/* My transactions popup */} 81 | 88 | {/* Header */} 89 |
90 |
My votes
91 | 110 |
111 | 112 | {/* Body */} 113 |
121 | { 122 | // Loading 123 | isLoading ? ( 124 |
125 | 126 |
127 | ) : // Once data found 128 | votes && votes.length > 0 ? ( 129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | { 142 | votes && votes.length > 0 ? ( 143 | votes.map((vote, index) => ( 144 | 145 | 154 | 155 | 156 | 157 | 166 | 167 | )) 168 | ) : "" 169 | } 170 | 171 |
TitleVoted optionVoting powerVoted atTransaction
146 | 151 | {vote?.title} 152 | 153 | {getEachRow(vote)}{Object.values(vote?.voteObject)?.[0]}{convertToDisplayDateFormat(vote.votedAt)} 158 | 163 | {vote?.txId?.substr(0, 10) + "..."} 164 | 165 |
172 |
173 | ) : ( 174 |
175 | You have not cast your vote yet. 176 |
177 | ) 178 | } 179 |
180 |
181 | ); 182 | } -------------------------------------------------------------------------------- /components/home/accordion.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Constants } from "../../common/constants"; 3 | import styles from "../../styles/Accordion.module.css"; 4 | 5 | function AccordionFAQ() { 6 | const [selected, setSelected] = useState(null); 7 | const toggle = (i) => { 8 | if (selected == i) { 9 | return setSelected(null); 10 | } 11 | setSelected(i); 12 | }; 13 | 14 | // Get faq's from constant.js 15 | const faq = Constants.FAQ; 16 | 17 | return ( 18 |
19 | {faq.map((item, i) => ( 20 |
21 |
toggle(i)}> 22 |

{item.question}

23 | 24 | {selected == i ? ( 25 | 32 | 39 | 40 | ) : ( 41 | 48 | 55 | 56 | )} 57 | 58 |
59 |
64 |
65 |

66 |
67 |
68 |
69 |
70 | ))} 71 |
72 | ); 73 | } 74 | 75 | export default AccordionFAQ; 76 | -------------------------------------------------------------------------------- /components/poll/PollComponent.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Button, Form, Modal, Table } from "react-bootstrap"; 3 | import { authenticate, getFileFromGaia, putFileToGaia, userSession } from "../../services/auth"; 4 | import { castMyVoteContractCall } from "../../services/contract"; 5 | import { formStacksExplorerUrl } from "../../services/utils"; 6 | import styles from "../../styles/Poll.module.css"; 7 | import HeaderComponent from "../common/HeaderComponent"; 8 | import InformationComponent from "../common/InformationComponent"; 9 | import { getIndividualResultByStartAndEndPosition } from "./PollService"; 10 | 11 | export default function PollComponent(props) { 12 | // Variables 13 | const { 14 | pollObject, 15 | isPreview, 16 | optionsMap, 17 | resultsByOption, 18 | resultsByPosition, 19 | setResultsByPosition, 20 | totalVotes, 21 | totalUniqueVotes, 22 | dns, 23 | alreadyVoted, 24 | noHoldingToken, 25 | holdingTokenIdsArray, 26 | votingPower, 27 | publicUrl, 28 | txStatus, 29 | noOfResultsLoaded, 30 | setNoOfResultsLoaded, 31 | currentBlockHeight } = props; 32 | const [voteObject, setVoteObject] = useState({}); 33 | const [errorMessage, setErrorMessage] = useState(); 34 | const [txId, setTxId] = useState(); 35 | const [isUserSignedIn, setIsUserSignedIn] = useState(false); 36 | const [isProcessing, setIsProcessing] = useState(false); 37 | 38 | // Show popup 39 | const [show, setShow] = useState(false); 40 | const handleShow = () => setShow(true); 41 | const handleClose = () => setShow(false); 42 | 43 | // Functions 44 | useEffect(() => { 45 | if (userSession && userSession.isUserSignedIn()) { 46 | setIsUserSignedIn(true) 47 | } 48 | }, []); 49 | 50 | const handleChange = e => { 51 | const { name, value } = e.target; 52 | 53 | if (pollObject?.votingSystem == "fptp") { 54 | voteObject = { 55 | [value]: votingPower 56 | }; 57 | } else { 58 | if (voteObject?.[value]) { 59 | delete voteObject[value]; 60 | } else { 61 | voteObject[value] = votingPower; 62 | } 63 | } 64 | setVoteObject(voteObject); 65 | }; 66 | 67 | const handleChangeVote = (e) => { 68 | const { name, value } = e.target; 69 | 70 | if (value <= 0) { 71 | delete voteObject[name]; 72 | } else { 73 | voteObject[name] = value; 74 | } 75 | } 76 | 77 | const callbackFunction = (data) => { 78 | if (data?.txId) { 79 | setTxId(data.txId); 80 | 81 | // Store my vote to Gaia 82 | processMyVote(data); 83 | 84 | // Show information popup 85 | handleShow(); 86 | } 87 | } 88 | 89 | const processMyVote = (data) => { 90 | // Store my vote to Gaia 91 | getFileFromGaia("my_votes_ballot.json").then( 92 | (response) => { 93 | if (response) { 94 | const myVotesObj = JSON.parse(response); 95 | 96 | if (myVotesObj && myVotesObj.votes) { 97 | saveMyVoteToGaia(myVotesObj, data); 98 | } 99 | } 100 | }, 101 | (error) => { 102 | // File does not exit in gaia 103 | if (error && error.code == "does_not_exist") { 104 | const myVotesObj = { 105 | votes: [ 106 | ], 107 | }; 108 | 109 | saveMyVoteToGaia(myVotesObj, data); 110 | } 111 | } 112 | ); 113 | } 114 | 115 | const saveMyVoteToGaia = (myVotesObj, data) => { 116 | myVotesObj.votes.push({ 117 | title: pollObject?.title, 118 | url: publicUrl, 119 | 120 | voteObject: voteObject, 121 | optionsMap: optionsMap, 122 | 123 | votedAt: Date.now(), 124 | txId: data.txId, 125 | txRaw: data.txRaw 126 | }); 127 | 128 | // Store on gaia 129 | putFileToGaia( 130 | "my_votes_ballot.json", 131 | JSON.stringify(myVotesObj), 132 | { dangerouslyIgnoreEtag: true } 133 | ); 134 | } 135 | 136 | const validate = () => { 137 | // Reset the error message 138 | errorMessage = ""; 139 | 140 | // Not voted 141 | if (!voteObject || Object.keys(voteObject)?.length == 0) { 142 | errorMessage = "Please select option to vote"; 143 | } 144 | 145 | setErrorMessage(errorMessage); 146 | } 147 | 148 | const castMyVote = () => { 149 | if (pollObject?.publishedInfo?.contractAddress && pollObject?.publishedInfo?.contractName) { 150 | // Validation 151 | validate(); 152 | if (errorMessage) { 153 | return; 154 | } 155 | 156 | // Start processing 157 | setIsProcessing(true); 158 | 159 | const contractAddress = pollObject?.publishedInfo?.contractAddress; 160 | const contractName = pollObject?.publishedInfo?.contractName; 161 | castMyVoteContractCall(contractAddress, contractName, voteObject, dns, holdingTokenIdsArray, callbackFunction); 162 | } 163 | } 164 | 165 | const loadMore = () => { 166 | if (pollObject?.publishedInfo?.contractAddress && pollObject?.publishedInfo?.contractName) { 167 | const contractAddress = pollObject?.publishedInfo?.contractAddress; 168 | const contractName = pollObject?.publishedInfo?.contractName; 169 | 170 | // Find start and end position 171 | const start = totalUniqueVotes - noOfResultsLoaded; 172 | let end = start - 10; 173 | 174 | // If end is greater than totalUniqueVotes 175 | if (end < 0) { 176 | end = 0; 177 | } 178 | 179 | // Load next ten results 180 | getIndividualResultByStartAndEndPosition(start, end, totalUniqueVotes, contractAddress, contractName, 181 | resultsByPosition, setResultsByPosition, noOfResultsLoaded, setNoOfResultsLoaded); 182 | } 183 | } 184 | 185 | return ( 186 | <> 187 |
188 | {pollObject && pollObject.id ? 189 | <> 190 |
191 |
192 | {/* Left Side */} 193 |
194 | {/* Header */} 195 | 196 | 197 | {/* Cast your vote */} 198 |
199 |
Cast your vote
200 |
201 |
202 | 203 | {/* Title */} 204 | {pollObject?.title} 205 | 206 | {/* FPTP or Block Voting */} 207 | {(pollObject?.votingSystem == "fptp" || pollObject?.votingSystem == "block") && 208 | pollObject?.options.map((option, index) => ( 209 | new Date())) || 219 | ((pollObject?.endAtDate || pollObject?.endAtDateUTC) && (new Date(pollObject?.endAtDateUTC ? pollObject?.endAtDateUTC : pollObject?.endAtDate) < new Date())) 220 | )} 221 | /> 222 | 223 | //
224 | // 228 | // 229 | //
230 | )) 231 | } 232 | 233 | {/* Quadratic or Weighted Voting */} 234 | {(pollObject?.votingSystem == "quadratic" || pollObject?.votingSystem == "weighted") && 235 | 236 | 237 | 238 | {pollObject?.options.map((option, index) => ( 239 | 240 | 243 | 249 | 250 | )) 251 | } 252 | 253 |
241 | {option.value} 242 | 244 | 248 |
254 | } 255 | 256 | {/* Voting Criteria */} 257 | {(pollObject?.votingStrategyFlag && pollObject?.strategyTokenName) && 258 |
259 |
Voting Criteria
260 | 261 | {`You should hold ${pollObject?.strategyTokenName} to vote.`} 262 | 263 |
264 | } 265 | 266 | {/* Vote button */} 267 | {isUserSignedIn ? 268 |
269 | 277 | {errorMessage && 278 | 279 | {errorMessage} 280 | 281 | } 282 |
283 | : 284 | 288 | } 289 | 290 | {/* Holdings Required */} 291 | {noHoldingToken && 292 |
293 | You should have the {" "} 294 | {pollObject?.strategyTokenName ? pollObject?.strategyTokenName : "strategy token"} 295 | {" "} to vote. 296 |
297 | } 298 | 299 | {/* Already voted */} 300 | {alreadyVoted && 301 |
302 | Your vote has already been cast. 303 |
304 | } 305 |
306 |
307 |
308 |
309 | 310 | {/* Results */} 311 |
312 |
Votes {totalVotes >= 0 ? <>({totalVotes}) : ""}
313 |
314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | {totalUniqueVotes >= 0 && 325 | (totalUniqueVotes > 0 ? 326 | Object.keys(resultsByPosition)?.reverse().map((position, index) => ( 327 | 328 | 349 | 356 | 363 | 366 | 367 | )) 368 | : 369 | 370 | 373 | 374 | ) 375 | } 376 | 377 | {/* Loading */} 378 | {noOfResultsLoaded != Object.keys(resultsByPosition).length && 379 | 380 | 383 | 384 | } 385 | 386 | {/* Load more */} 387 | {(noOfResultsLoaded == Object.keys(resultsByPosition).length && 388 | totalUniqueVotes != Object.keys(resultsByPosition).length) && 389 | 390 | 395 | 396 | } 397 | 398 |
User nameOptionNo. of votesVoting power
{resultsByPosition[position]?.address && 329 | 330 | 331 | {resultsByPosition[position]?.username ? resultsByPosition[position]?.username : resultsByPosition[position]?.address} { } 332 | 339 | 345 | 346 | 347 | 348 | } 350 | {Object.keys(resultsByPosition[position]?.vote).map((optionId, voteIndex) => ( 351 |
352 | {optionsMap[optionId] ? optionsMap[optionId] : "-"} 353 |
354 | ))} 355 |
357 | {Object.values(resultsByPosition[position]?.vote).map((value, voteIndex) => ( 358 |
359 | {value} 360 |
361 | ))} 362 |
364 | {resultsByPosition[position]?.votingPower} 365 |
371 | No data found 372 |
381 | Loading... 382 |
391 | { loadMore(); }}> 392 | Load more 393 | 394 |
399 |
400 |
401 |
402 | 403 | {/* Right Side */} 404 |
405 | {/* Information */} 406 | 407 |
408 |
409 |
410 | 411 | : 412 | <>Loading... 413 | } 414 |
415 | 416 | {/* Success message popup */} 417 | 418 | 419 | Information 420 | 421 | 422 | Voted successfully! Your vote has been cast. Here is a link to your transaction status{" "} 423 | 429 | {"here"} 430 | 437 | 443 | 444 | 445 | 446 | 447 | 448 | ); 449 | } -------------------------------------------------------------------------------- /components/poll/PollService.js: -------------------------------------------------------------------------------- 1 | import { 2 | cvToHex, cvToValue, parseReadOnlyResponse, uintCV 3 | } from "@stacks/transactions"; 4 | import { Constants } from "../../common/constants"; 5 | import { getStacksAPIPrefix } from "../../services/auth"; 6 | 7 | const getIndividualResultByStartAndEndPosition = (start, end, totalUniqueVotes, contractAddress, contractName, 8 | resultsByPosition, setResultsByPosition, noOfResultsLoaded, setNoOfResultsLoaded) => { 9 | // Store, "end" as number of results loaded 10 | if (!noOfResultsLoaded) { 11 | noOfResultsLoaded = (start - end); 12 | setNoOfResultsLoaded(noOfResultsLoaded); 13 | } else { 14 | noOfResultsLoaded = noOfResultsLoaded + (start - end); 15 | setNoOfResultsLoaded(noOfResultsLoaded); 16 | } 17 | 18 | for (let i = start; i > end; i--) { 19 | if (i >= 1) { 20 | getResultAtPosition(i, contractAddress, contractName, resultsByPosition, setResultsByPosition); 21 | } 22 | } 23 | } 24 | 25 | const getResultAtPosition = async (position, contractAddress, contractName, 26 | resultsByPosition, setResultsByPosition) => { 27 | let url = getStacksAPIPrefix() + 28 | "/v2/contracts/call-read/" + 29 | contractAddress + 30 | "/" + 31 | contractName + 32 | "/get-result-at-position"; 33 | 34 | // Fetch gaia URL from stacks blockchain 35 | const rawResponse = await fetch(url, { 36 | method: "POST", 37 | headers: { 38 | Accept: "application/json", 39 | "Content-Type": "application/json", 40 | }, 41 | body: JSON.stringify({ 42 | sender: contractAddress, 43 | arguments: [cvToHex(uintCV(position))] 44 | }), 45 | }); 46 | const content = await rawResponse.json(); 47 | 48 | // If data found on stacks blockchain 49 | if (content && content.okay) { 50 | const results = cvToValue(parseReadOnlyResponse(content))?.value?.value; 51 | 52 | let resultsObj = {}; 53 | results?.vote?.value.forEach((option, index) => { 54 | resultsObj[option?.value] = results?.volume?.value?.[index]?.value; 55 | }); 56 | 57 | resultsByPosition[position] = { 58 | "dns": results?.bns?.value, 59 | "address": results?.user?.value, 60 | "vote": resultsObj, 61 | "votingPower": results?.["voting-power"]?.value 62 | } 63 | 64 | // Testnet code 65 | if (Constants.STACKS_MAINNET_FLAG == true && results?.user?.value) { 66 | // Get btc domain for logged in user 67 | const response = await fetch( 68 | getStacksAPIPrefix() + "/v1/addresses/stacks/" + results?.user?.value 69 | ); 70 | const responseObject = await response.json(); 71 | 72 | // Get btc dns 73 | if (responseObject?.names?.length > 0) { 74 | const btcDNS = responseObject.names.filter((bns) => 75 | bns.endsWith(".btc") 76 | ); 77 | 78 | // Check does BTC dns is available 79 | if (btcDNS && btcDNS.length > 0) { 80 | // BTC holder 81 | const btcNamespace = btcDNS[0]; 82 | resultsByPosition[position]["username"] = btcNamespace; 83 | } 84 | } 85 | } else if (Constants.STACKS_MAINNET_FLAG == false) { 86 | // Testnet 87 | const btcNamespace = results?.user?.value?.substr(-5) + ".btc"; 88 | resultsByPosition[position]["username"] = btcNamespace; 89 | } 90 | 91 | setResultsByPosition({ ...resultsByPosition }); 92 | } 93 | } 94 | 95 | export { 96 | getIndividualResultByStartAndEndPosition 97 | }; 98 | -------------------------------------------------------------------------------- /components/poll/QRCodePopup.js: -------------------------------------------------------------------------------- 1 | import QRCode from "qrcode.react"; 2 | import { Modal } from "react-bootstrap"; 3 | import styles from "../../styles/QRCodePopup.module.css"; 4 | 5 | export default function QRCodePopup(props) { 6 | // Parent parameters 7 | const { pollObject, showQRCodePopupFlag, publicUrl } = props; 8 | 9 | // Variables 10 | // Handle close popup 11 | const handleCloseQrCodePopup = () => { 12 | props.setShowQRCodePopupFlag(false); 13 | }; 14 | 15 | // Functions 16 | // Download QR code 17 | const downloadQRCode = () => { 18 | const qrCodeURL = document 19 | .getElementById("qrCodeEl") 20 | .toDataURL("image/png") 21 | .replace("image/png", "image/octet-stream"); 22 | let aEl = document.createElement("a"); 23 | aEl.href = qrCodeURL; 24 | aEl.download = "Ballot_" + (pollObject?.title ? pollObject?.title.replaceAll(".", "_") : "") + ".png"; 25 | document.body.appendChild(aEl); 26 | aEl.click(); 27 | document.body.removeChild(aEl); 28 | }; 29 | 30 | // View 31 | return ( 32 | <> 33 | {/* QR code */} 34 | 41 | {/* Header */} 42 |
43 |
Ballot QR code
44 | 63 |
64 | 65 | {/* Body */} 66 |
74 | {/* QR code */} 75 |
76 | {/* QR code */} 77 | 78 | 79 |
80 | 86 |
87 | 88 | {/* Download button */} 89 | 108 |
109 |
110 |
111 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /components/summary/BuilderComponent.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useEffect, useState } from "react"; 3 | import { Button, Form } from "react-bootstrap"; 4 | import { Constants } from "../../common/constants"; 5 | import { getFileFromGaia, getMyStxAddress, getStacksAPIPrefix, getUserData, putFileToGaia, userSession } from "../../services/auth"; 6 | import { convertToDisplayDateFormat } from "../../services/utils"; 7 | import styles from "../../styles/Builder.module.css"; 8 | import dashboardStyles from "../../styles/Dashboard.module.css"; 9 | import ChoosePollsPopup from "./ChoosePollsPopup"; 10 | 11 | export default function SummaryBuilderComponent(props) { 12 | // Variables 13 | 14 | // Gaia address 15 | const [gaiaAddress, setGaiaAddress] = useState(); 16 | 17 | // Summary object 18 | const [summaryObject, setSummaryObject] = useState(); 19 | 20 | // Processing flag 21 | const [isProcessing, setIsProcessing] = useState(false); 22 | 23 | // Error message 24 | const [errorMessage, setErrorMessage] = useState(""); 25 | 26 | // Show/Hide ChoosePolls Popup 27 | const [showChoosePollsPopupFlag, setShowChoosePollsPopupFlag] = useState(false); 28 | 29 | const [urlSuffix, setUrlSuffix] = useState(); 30 | 31 | // Functions 32 | useEffect(() => { 33 | let isCancelled = false; 34 | 35 | // Get Summary object 36 | getFileFromGaia("summary.json", { decrypt: false }).then( 37 | (response) => { 38 | if (response && !isCancelled) { 39 | setSummaryObject(JSON.parse(response)); 40 | } 41 | }, 42 | (error) => { 43 | // File does not exit in gaia 44 | if (error && error.code == "does_not_exist" && !isCancelled) { 45 | // Initialize new poll 46 | setSummaryObject(initializeNewSummary()); 47 | } 48 | }); 49 | 50 | // Get gaia address 51 | if (userSession && userSession.isUserSignedIn()) { 52 | setGaiaAddress(getUserData()?.gaiaHubConfig?.address); 53 | } 54 | 55 | // Get .btc address 56 | getBTCDomainFromBlockchain(); 57 | 58 | return () => { 59 | isCancelled = true; 60 | } 61 | }, []); 62 | 63 | const getBTCDomainFromBlockchain = async () => { 64 | // Get btc domain for logged in user 65 | const response = await fetch( 66 | getStacksAPIPrefix() + "/v1/addresses/stacks/" + getMyStxAddress() 67 | ); 68 | const responseObject = await response.json(); 69 | 70 | // Testnet code 71 | if (Constants.STACKS_MAINNET_FLAG == false) { 72 | setUrlSuffix(getUserData()?.gaiaHubConfig?.address); 73 | return; 74 | } 75 | 76 | // Get btc dns 77 | if (responseObject?.names?.length > 0) { 78 | const btcDNS = responseObject.names.filter((bns) => 79 | bns.endsWith(".btc") 80 | ); 81 | 82 | // Check does BTC dns is available 83 | if (btcDNS && btcDNS.length > 0) { 84 | const _dns = btcDNS[0]; 85 | 86 | setUrlSuffix(_dns); 87 | } else { 88 | setUrlSuffix(getUserData()?.gaiaHubConfig?.address); 89 | } 90 | } else { 91 | setUrlSuffix(getUserData()?.gaiaHubConfig?.address); 92 | } 93 | }; 94 | 95 | function initializeNewSummary() { 96 | return { 97 | "title": "", 98 | "description": "", 99 | "polls": { 100 | "list": [], 101 | "ref": {} 102 | } 103 | } 104 | } 105 | 106 | const handleChange = e => { 107 | const { name, value } = e.target; 108 | 109 | // If value is empty, then delete key from previous state 110 | if (!value && summaryObject) { 111 | // Delete key from JSON 112 | delete summaryObject[name]; 113 | } else { 114 | // Update the value 115 | summaryObject[name] = value; 116 | } 117 | 118 | setSummaryObject({ ...summaryObject }); 119 | }; 120 | 121 | function openChoosePollsPopup() { 122 | setShowChoosePollsPopupFlag(true); 123 | } 124 | 125 | function publishSummary() { 126 | // Start processing 127 | setIsProcessing(true); 128 | 129 | // Clear message 130 | setErrorMessage(""); 131 | 132 | // Save to gaia 133 | putFileToGaia("summary.json", JSON.stringify(summaryObject), { "encrypt": false }).then(response => { 134 | // Saved successfully message 135 | setErrorMessage("Summary page is published."); 136 | 137 | // Stop processing 138 | setIsProcessing(false); 139 | }); 140 | } 141 | 142 | function getEachRow(pollIndexObject) { 143 | const gaiaAddress = getUserData()?.gaiaHubConfig?.address; 144 | 145 | return ( 146 |
147 | {/* Title */} 148 |
149 |
150 | {pollIndexObject?.title ? pollIndexObject?.title : "..."} 151 |
152 | 153 | {/* Status */} 154 |
155 | { 156 | pollIndexObject?.status == "draft" ? "Draft" : 157 | ((pollIndexObject?.endAt && (new Date(pollIndexObject?.endAt) < new Date())) ? 158 | "Closed" : "Active") 159 | } 160 |
161 |
162 | 163 | {/* Description */} 164 | { 165 | pollIndexObject?.description ? 166 |

167 | {pollIndexObject?.description ? pollIndexObject?.description : "..."} 168 |

169 | : <> 170 | } 171 | 172 |
173 | 174 | Last Modified : {convertToDisplayDateFormat(pollIndexObject?.updatedAt)} 175 | 176 |
177 | 178 | {/*
179 | 183 |
*/} 184 |
185 | ) 186 | } 187 | 188 | // Design 189 | return ( 190 | <> 191 | {summaryObject ? 192 | <> 193 |
194 | {/* Left side */} 195 |
196 | {/* Title */} 197 |
Summary
198 | 199 | {/* Fields */} 200 |
201 | 202 | Title 203 | 205 | 206 | 207 | 208 | Description 209 | 210 | 211 | 212 | 213 |
214 | 222 |
223 |
224 |
225 | 226 | {/* List of polls */} 227 |
228 | {summaryObject?.polls?.list.map( 229 | (pollId, i) => ( 230 |
231 | {getEachRow(summaryObject?.polls?.ref[pollId])} 232 |
233 | ) 234 | )} 235 |
236 |
237 | 238 | {/* Right side */} 239 |
240 |
241 |
242 | 243 | 244 | 260 | 261 | 262 | 266 | 267 | {/* Error Message */} 268 | {errorMessage && 269 |
270 | {errorMessage} 271 |
272 | } 273 |
274 |
275 |
276 |
277 | 278 | : 279 | <> 280 |
281 | 282 |
283 |
284 | 285 |
286 |
287 | 288 |
289 |
290 | 291 |
292 |
293 | 294 | 295 | } 296 | 297 | {/* Choose polls popup */} 298 | 300 | 301 | ); 302 | } -------------------------------------------------------------------------------- /components/summary/ChoosePollsPopup.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Modal, Spinner, Form, Button } from "react-bootstrap"; 3 | import { getFileFromGaia } from "../../services/auth"; 4 | import { convertToDisplayDateFormat } from "../../services/utils"; 5 | import styles from "../../styles/ChoosePollsPopup.module.css"; 6 | 7 | export default function ChoosePollsPopup(props) { 8 | // Parent parameters 9 | const { showChoosePollsPopupFlag, summaryObject, handleSummaryChange } = props; 10 | 11 | // Variables 12 | // Handle close popup 13 | const handleCloseChoosePollsPopup = () => { 14 | props.setShowChoosePollsPopupFlag(false); 15 | }; 16 | 17 | // Summary polls 18 | const [summaryPolls, setSummaryPolls] = useState(); 19 | 20 | // All polls 21 | const [allPolls, setAllPolls] = useState(); 22 | 23 | // Loading 24 | const [isLoading, setIsLoading] = useState(false); 25 | 26 | // Functions 27 | useEffect(() => { 28 | if (showChoosePollsPopupFlag) { 29 | // Take summary polls 30 | setSummaryPolls(JSON.parse(JSON.stringify(summaryObject?.polls))); 31 | 32 | // Start loading 33 | setIsLoading(true); 34 | 35 | getFileFromGaia("pollIndex.json", {}).then( 36 | (response) => { 37 | if (response) { 38 | setAllPolls(JSON.parse(response)); 39 | } 40 | 41 | // Stop loading 42 | setIsLoading(false); 43 | }, 44 | (error) => { 45 | // File does not exit in gaia 46 | if (error && error.code == "does_not_exist") { 47 | setAllPolls({ 48 | list: [], 49 | ref: {} 50 | }); 51 | } 52 | 53 | // Stop loading 54 | setIsLoading(false); 55 | }); 56 | } 57 | }, [showChoosePollsPopupFlag]); 58 | 59 | function getEachRow(pollIndexObject) { 60 | return ( 61 |
62 | {/* Title */} 63 |
64 |
65 | {pollIndexObject?.title ? pollIndexObject?.title : "..."} 66 |
67 | {/* Status */} 68 |
69 | { 70 | pollIndexObject?.status == "draft" ? "Draft" : 71 | ((pollIndexObject?.endAt && (new Date(pollIndexObject?.endAt) < new Date())) ? 72 | "Closed" : "Active") 73 | } 74 |
75 |
76 | 77 | {/* Description */} 78 | { 79 | pollIndexObject?.description ? 80 |

81 | {pollIndexObject?.description ? pollIndexObject?.description : "..."} 82 |

83 | : <> 84 | } 85 | 86 |
87 | 88 | Last Modified : {convertToDisplayDateFormat(pollIndexObject?.updatedAt)} 89 | 90 |
91 |
92 | ) 93 | } 94 | 95 | function onClickOfPolls(pollId) { 96 | if (!summaryPolls?.ref[pollId]) { 97 | summaryPolls.list.push(pollId); 98 | summaryPolls.ref[pollId] = allPolls?.ref?.[pollId]; 99 | } else { 100 | const summaryIndex = summaryPolls.list.findIndex(item => item == pollId); 101 | if (summaryIndex >= 0) { 102 | summaryPolls.list.splice(summaryIndex, 1); 103 | } 104 | delete summaryPolls.ref[pollId]; 105 | } 106 | 107 | setSummaryPolls({ ...summaryPolls }); 108 | } 109 | 110 | function saveSummaryPolls() { 111 | handleSummaryChange({ 112 | target: { 113 | name: "polls", 114 | value: { ...summaryPolls } 115 | } 116 | }); 117 | 118 | handleCloseChoosePollsPopup(); 119 | } 120 | 121 | const handleChange = e => { 122 | const { name, value } = e.target; 123 | } 124 | 125 | // View 126 | return ( 127 | <> 128 | {/* QR code */} 129 | 136 | {/* Header */} 137 |
138 |
Choose polls
139 | 158 |
159 | 160 | {/* Body */} 161 |
162 | { 163 | // Loading 164 | isLoading ? ( 165 |
166 | 167 |
168 | ) : // Once data found 169 | (allPolls && allPolls?.list?.length > 0) ? ( 170 |
171 | {allPolls?.list.map( 172 | (pollId, i) => ( 173 | (allPolls?.ref?.[pollId] && allPolls?.ref?.[pollId]?.status != "draft") && 174 |
{ onClickOfPolls(pollId) }}> 175 |
176 | 177 | 184 | 185 |
186 |
187 | {getEachRow(allPolls.ref[pollId])} 188 |
189 |
190 | 191 | ) 192 | )} 193 |
194 | ) : ( 195 |
196 | Only published polls will be listed here. 197 |
198 | ) 199 | } 200 |
201 | 202 | {/* Footer */} 203 | 204 | 205 | 206 | 207 |
208 | 209 | ); 210 | } 211 | -------------------------------------------------------------------------------- /components/summary/SummaryComponent.js: -------------------------------------------------------------------------------- 1 | 2 | import Link from "next/link"; 3 | import { convertToDisplayDateFormat } from "../../services/utils"; 4 | import styles from "../../styles/Dashboard.module.css"; 5 | 6 | export default function SummaryComponent(props) { 7 | // Variables 8 | const { 9 | summaryObject, 10 | gaiaAddress, 11 | allPolls } = props; 12 | 13 | // Function 14 | function getEachRow(pollIndexObject) { 15 | return ( 16 | 18 |
19 | {/* Title */} 20 |
21 |
22 | {pollIndexObject?.title ? pollIndexObject?.title : "..."} 23 |
24 | {/* Status */} 25 |
26 | { 27 | pollIndexObject?.status == "draft" ? "Draft" : 28 | ((pollIndexObject?.endAt && (new Date(pollIndexObject?.endAt) < new Date())) ? 29 | "Closed" : "Active") 30 | } 31 |
32 |
33 | 34 | {/* Description */} 35 | { 36 | pollIndexObject?.description ? 37 |

38 | {pollIndexObject?.description ? pollIndexObject?.description : "..."} 39 |

40 | : <> 41 | } 42 | 43 |
44 | 45 | Last Modified : {convertToDisplayDateFormat(pollIndexObject?.updatedAt)} 46 | 47 |
48 |
49 |
50 | ) 51 | } 52 | 53 | // Design 54 | return ( 55 | <> 56 | {summaryObject && 57 |
58 |
59 |
60 | 61 | {/* Title */} 62 |

{summaryObject?.title}

63 | 64 | {/* Description */} 65 |
66 |

68 |

69 |
70 | 71 | {/* List of all polls */} 72 | {allPolls?.list && allPolls?.ref ? 73 | (allPolls?.list?.length > 0) && 74 | <> 75 | {/* List of polls */} 76 |
77 | {allPolls?.list.map( 78 | (pollId, i) => ( 79 |
80 | {getEachRow(allPolls.ref[pollId])} 81 |
82 | ) 83 | )} 84 |
85 | 86 | : 87 | <> 88 | {/* Loading */} 89 |
90 | 91 |
92 |
93 |
94 | 95 | } 96 |
97 |
98 |
99 | } 100 | 101 | ); 102 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ballot", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@stacks/connect": "^7.8.0", 13 | "@stacks/network": "^6.17.0", 14 | "@stacks/storage": "^6.17.0", 15 | "@stacks/transactions": "^6.17.0", 16 | "bootstrap": "^5.2.0", 17 | "lodash": "^4.17.21", 18 | "nanoid": "^4.0.0", 19 | "next": "12.2.5", 20 | "qrcode.react": "^3.1.0", 21 | "react": "18.2.0", 22 | "react-bootstrap": "^2.5.0", 23 | "react-dom": "18.2.0", 24 | "uuid": "^8.3.2" 25 | }, 26 | "devDependencies": { 27 | "eslint": "8.22.0", 28 | "eslint-config-next": "12.2.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pages/[...id].js: -------------------------------------------------------------------------------- 1 | import { 2 | cvToHex, cvToValue, hexToCV, parseReadOnlyResponse, standardPrincipalCV 3 | } from "@stacks/transactions"; 4 | import Head from "next/head"; 5 | import { useEffect, useState } from "react"; 6 | import { Col, Container, Row } from "react-bootstrap"; 7 | import { Constants } from "../common/constants"; 8 | import { DashboardNavBarComponent } from "../components/common/DashboardNavBarComponent"; 9 | import PollComponent from "../components/poll/PollComponent"; 10 | import { getIndividualResultByStartAndEndPosition } from "../components/poll/PollService"; 11 | import { getMyStxAddress, getStacksAPIPrefix, userSession } from "../services/auth"; 12 | import { getRecentBlock } from "../services/utils"; 13 | 14 | export default function Poll(props) { 15 | // Variables 16 | const { pollObject, pollId, gaiaAddress, currentBlockHeight } = props; 17 | 18 | // Contract transaction status 19 | const [txStatus, setTxStatus] = useState(); 20 | 21 | const [publicUrl, setPublicUrl] = useState(); 22 | const [optionsMap, setOptionsMap] = useState({}); 23 | const [resultsByOption, setResultsByOption] = useState({}); 24 | const [resultsByPosition, setResultsByPosition] = useState({}); 25 | const [totalVotes, setTotalVotes] = useState(); 26 | const [totalUniqueVotes, setTotalUniqueVotes] = useState(); 27 | const [noOfResultsLoaded, setNoOfResultsLoaded] = useState(); 28 | 29 | // BTC dns name 30 | const [dns, setDns] = useState(); 31 | 32 | // TokenIds 33 | const [alreadyVoted, setAlreadyVoted] = useState(false); 34 | const [noHoldingToken, setNoHoldingToken] = useState(false); 35 | const [holdingTokenIdsArray, setHoldingTokenIdsArray] = useState(); 36 | 37 | // Voting power 38 | const [votingPower, setVotingPower] = useState(); 39 | 40 | const title = `${pollObject?.title} | Ballot`; 41 | const description = pollObject?.description?.substr(0, 160); 42 | const metaImage = "https://ballot.gg/images/ballot-meta.png"; 43 | const displayURL = ""; 44 | 45 | // Functions 46 | useEffect(() => { 47 | // Set shareable public URL 48 | setPublicUrl(`https://ballot.gg/${pollId}/${gaiaAddress}`); 49 | 50 | if (pollObject) { 51 | // Parse poll options 52 | pollObject?.options.forEach(option => { 53 | optionsMap[option.id] = option.value; 54 | }); 55 | setOptionsMap(optionsMap); 56 | 57 | // Get contract transaction status 58 | getContractTransactionStatus(pollObject); 59 | 60 | // Fetch token holdings 61 | fetchTokenHoldings(pollObject); 62 | 63 | // Fetch results 64 | getPollResults(pollObject); 65 | 66 | // Fetch result by user 67 | getResultByUser(pollObject); 68 | } 69 | }, [pollObject, pollId, gaiaAddress]); 70 | 71 | const getContractTransactionStatus = async (pollObject) => { 72 | if (!pollObject?.publishedInfo?.txId) { 73 | return; 74 | } 75 | 76 | // Get btc domain for logged in user 77 | const response = await fetch( 78 | getStacksAPIPrefix() + "/extended/v1/tx/" + pollObject?.publishedInfo?.txId 79 | ); 80 | const responseObject = await response.json(); 81 | setTxStatus(responseObject?.tx_status); 82 | } 83 | 84 | const fetchTokenHoldings = (pollObject) => { 85 | // If user is not signed in, just return 86 | if (!userSession.isUserSignedIn()) { 87 | return; 88 | } 89 | 90 | // Strategy 91 | if (pollObject?.votingStrategyFlag) { 92 | if (pollObject?.strategyTokenType == "nft") { 93 | // Fetch NFT holdings 94 | getNFTHolding(pollObject); 95 | } else if (pollObject?.strategyTokenType == "ft") { 96 | // Fetch FT holdings 97 | getFTHolding(pollObject); 98 | } 99 | } else { 100 | // No strategy 101 | 102 | // Allow anybody to vote 103 | setHoldingTokenIdsArray([]); 104 | setVotingPower(1); 105 | } 106 | } 107 | 108 | const getNFTHolding = async (pollObject) => { 109 | if (pollObject?.votingStrategyTemplate) { 110 | // BTC holders check 111 | if (pollObject?.votingStrategyTemplate === "btcholders") { 112 | // Fetch BTC domain 113 | getBTCDomainFromBlockchain(pollObject); 114 | } else if (pollObject?.strategyContractName && pollObject?.strategyTokenName) { 115 | const limit = 200; 116 | // Get NFT holdings 117 | const responseObject = await makeFetchCall(getStacksAPIPrefix() + "/extended/v1/tokens/nft/holdings?principal=" + getMyStxAddress() + 118 | "&asset_identifiers=" + pollObject?.strategyContractName + "::" + pollObject?.strategyTokenName + "&offset=0&limit=" + limit); 119 | if (responseObject?.total > 0) { 120 | // Set voting power 121 | setVotingPower(responseObject?.total); 122 | 123 | const _holdingTokenIdsArray = []; 124 | responseObject?.results.forEach(eachNFT => { 125 | _holdingTokenIdsArray.push(cvToValue(hexToCV(eachNFT.value.hex))); 126 | }); 127 | 128 | // If there are more than 200, then fetch all 129 | if (responseObject?.total > limit) { 130 | const remainingTotal = (responseObject?.total - limit); 131 | const noOfFetchCallsToBeMade = Math.ceil(remainingTotal / limit); 132 | 133 | let listOfPromises = []; 134 | for (let i = 1; i <= noOfFetchCallsToBeMade; i++) { 135 | const offset = i * limit; 136 | 137 | listOfPromises.push(makeFetchCall(getStacksAPIPrefix() + "/extended/v1/tokens/nft/holdings?principal=" + getMyStxAddress() + 138 | "&asset_identifiers=" + pollObject?.strategyContractName + "::" + pollObject?.strategyTokenName + "&offset=" + offset + "&limit=" + limit)); 139 | } 140 | 141 | await Promise.all(listOfPromises).then(results => { 142 | results?.forEach(responseObject => { 143 | responseObject?.results.forEach(eachNFT => { 144 | _holdingTokenIdsArray.push(cvToValue(hexToCV(eachNFT.value.hex))); 145 | }); 146 | }); 147 | }); 148 | } 149 | 150 | holdingTokenIdsArray = _holdingTokenIdsArray; 151 | setHoldingTokenIdsArray(holdingTokenIdsArray); 152 | } else { 153 | // No holdings to vote 154 | setNoHoldingToken(true); 155 | } 156 | } 157 | } 158 | } 159 | 160 | const makeFetchCall = (url) => { 161 | return new Promise(async (resolve, reject) => { 162 | const response = await fetch(url); 163 | const responseObject = await response.json(); 164 | resolve(responseObject); 165 | }) 166 | } 167 | 168 | const getBTCDomainFromBlockchain = async (pollObject) => { 169 | // If user is not signed in, just return 170 | if (!userSession.isUserSignedIn()) { 171 | return; 172 | } 173 | 174 | // Get btc domain for logged in user 175 | const response = await fetch( 176 | getStacksAPIPrefix() + "/v1/addresses/stacks/" + getMyStxAddress() 177 | ); 178 | const responseObject = await response.json(); 179 | 180 | // Testnet code 181 | if (Constants.STACKS_MAINNET_FLAG == false) { 182 | const _dns = getMyStxAddress().substr(-5) + ".btc"; 183 | setDns(_dns); 184 | return; 185 | } 186 | 187 | // Get btc dns 188 | if (responseObject?.names?.length > 0) { 189 | const btcDNS = responseObject.names.filter((bns) => 190 | bns.endsWith(".btc") 191 | ); 192 | 193 | // Check does BTC dns is available 194 | if (btcDNS && btcDNS.length > 0) { 195 | // BTC holder 196 | const _dns = btcDNS[0]; 197 | 198 | // Take the btc dns name 199 | setDns(_dns); 200 | 201 | // Allow to vote 202 | if (pollObject?.votingStrategyFlag && pollObject?.votingStrategyTemplate === "btcholders") { 203 | setHoldingTokenIdsArray([]); 204 | setVotingPower(1); 205 | } 206 | } else { 207 | // Not a BTC holder 208 | 209 | // Turn flag on 210 | if (pollObject?.votingStrategyFlag && pollObject?.votingStrategyTemplate === "btcholders") { 211 | // No holdings to vote 212 | setNoHoldingToken(true); 213 | } 214 | } 215 | } else { 216 | // Not a BTC holder 217 | 218 | // Turn flag on 219 | if (pollObject?.votingStrategyFlag && pollObject?.votingStrategyTemplate === "btcholders") { 220 | // No holdings to vote 221 | setNoHoldingToken(true); 222 | } 223 | } 224 | } 225 | 226 | const getFTHolding = async (pollObject) => { 227 | if (pollObject?.votingStrategyTemplate) { 228 | // BTC holders check 229 | if (pollObject?.votingStrategyTemplate === "stx") { 230 | // Fetch STX holdings 231 | getSTXHolding(); 232 | } else if (pollObject?.strategyContractName && pollObject?.strategyTokenName) { 233 | const response = await fetch(`${getStacksAPIPrefix()}/extended/v1/address/${getMyStxAddress()}/balances` + 234 | (pollObject?.snapshotBlockHeight ? "?until_block=" + pollObject?.snapshotBlockHeight : "")); 235 | const responseObject = await response.json(); 236 | 237 | if (responseObject?.fungible_tokens && responseObject?.fungible_tokens?.[pollObject?.strategyContractName + "::" + pollObject?.strategyTokenName]) { 238 | const tokenInfo = responseObject?.fungible_tokens?.[pollObject?.strategyContractName + "::" + pollObject?.strategyTokenName]; 239 | 240 | if (tokenInfo?.balance !== "0") { 241 | // Default value 242 | let tokenDecimalsPowerOfTen = 1000000; 243 | if (pollObject?.strategyTokenDecimals && parseInt(pollObject?.strategyTokenDecimals) >= 0) { 244 | tokenDecimalsPowerOfTen = Math.pow(10, parseInt(pollObject?.strategyTokenDecimals)); 245 | } else if (pollObject?.strategyTokenDecimals && parseInt(pollObject?.strategyTokenDecimals) === 0) { 246 | tokenDecimalsPowerOfTen = 1; 247 | } 248 | 249 | const tokenBalance = Math.floor((parseInt(tokenInfo?.balance) / tokenDecimalsPowerOfTen)); 250 | setHoldingTokenIdsArray([]); 251 | setVotingPower(tokenBalance); 252 | } else { 253 | // No holdings to vote 254 | setNoHoldingToken(true); 255 | } 256 | } else { 257 | // No holdings to vote 258 | setNoHoldingToken(true); 259 | } 260 | } 261 | } 262 | } 263 | 264 | const getSTXHolding = async () => { 265 | const response = await fetch(`${getStacksAPIPrefix()}/extended/v1/address/${getMyStxAddress()}/stx` + 266 | (pollObject?.snapshotBlockHeight ? "?until_block=" + pollObject?.snapshotBlockHeight : "")); 267 | const responseObject = await response.json(); 268 | 269 | if (responseObject?.balance !== "0") { 270 | const stxBalance = Math.floor((parseInt(responseObject?.balance) / 1000000)); 271 | setHoldingTokenIdsArray([]); 272 | setVotingPower(stxBalance); 273 | } else { 274 | // No holdings to vote 275 | setNoHoldingToken(true); 276 | } 277 | } 278 | 279 | const getPollResults = async (pollObject) => { 280 | if (pollObject?.publishedInfo?.contractAddress && pollObject?.publishedInfo?.contractName) { 281 | const contractAddress = pollObject?.publishedInfo?.contractAddress; 282 | const contractName = pollObject?.publishedInfo?.contractName; 283 | let url = getStacksAPIPrefix() + 284 | "/v2/contracts/call-read/" + 285 | contractAddress + 286 | "/" + 287 | contractName + 288 | "/get-results"; 289 | 290 | // Fetch gaia URL from stacks blockchain 291 | const rawResponse = await fetch(url, { 292 | method: "POST", 293 | headers: { 294 | Accept: "application/json", 295 | "Content-Type": "application/json", 296 | }, 297 | body: JSON.stringify({ 298 | sender: contractAddress, 299 | arguments: [] 300 | }), 301 | }); 302 | const content = await rawResponse.json(); 303 | 304 | // If data found on stacks blockchain 305 | if (content && content.okay) { 306 | const results = cvToValue(parseReadOnlyResponse(content)).value; 307 | 308 | const total = parseInt(results?.["total-votes"]?.value ? (results?.["total-votes"]?.value) : (results?.total?.value)); 309 | setTotalVotes(total); 310 | 311 | // Total unique vote 312 | totalUniqueVotes = results?.total?.value; 313 | setTotalUniqueVotes(results?.total?.value); 314 | 315 | let resultsObj = {}; 316 | results?.options?.value.forEach((option, index) => { 317 | resultsObj[option?.value] = { 318 | total: results?.results?.value?.[index]?.value, 319 | percentage: results?.results?.value?.[index]?.value == 0 ? 0 : ((results?.results?.value?.[index]?.value / total) * 100).toFixed(2) 320 | }; 321 | }); 322 | setResultsByOption(resultsObj); 323 | 324 | // Get list of individual vote 325 | getIndividualResultByStartAndEndPosition(results?.total?.value, (results?.total?.value > 10 ? (results?.total?.value - 10) : 0), totalUniqueVotes, 326 | contractAddress, contractName, resultsByPosition, setResultsByPosition, noOfResultsLoaded, setNoOfResultsLoaded); 327 | } else { 328 | setTotalVotes(0); 329 | setTotalUniqueVotes(0); 330 | setNoOfResultsLoaded(0); 331 | } 332 | } 333 | }; 334 | 335 | const getResultByUser = async (pollObject) => { 336 | if (userSession.isUserSignedIn() && 337 | pollObject?.publishedInfo?.contractAddress && pollObject?.publishedInfo?.contractName) { 338 | const contractAddress = pollObject?.publishedInfo?.contractAddress; 339 | const contractName = pollObject?.publishedInfo?.contractName; 340 | let url = getStacksAPIPrefix() + 341 | "/v2/contracts/call-read/" + 342 | contractAddress + 343 | "/" + 344 | contractName + 345 | "/get-result-by-user"; 346 | 347 | // Fetch gaia URL from stacks blockchain 348 | const rawResponse = await fetch(url, { 349 | method: "POST", 350 | headers: { 351 | Accept: "application/json", 352 | "Content-Type": "application/json", 353 | }, 354 | body: JSON.stringify({ 355 | sender: contractAddress, 356 | arguments: [cvToHex(standardPrincipalCV(getMyStxAddress()))] 357 | }), 358 | }); 359 | const content = await rawResponse.json(); 360 | 361 | // If data found on stacks blockchain 362 | if (content && content.okay) { 363 | const results = cvToValue(parseReadOnlyResponse(content)).value; 364 | 365 | if (results) { 366 | setAlreadyVoted(true); 367 | } 368 | } 369 | } 370 | }; 371 | 372 | // Return 373 | return ( 374 | <> 375 | 376 | {title} 377 | 378 | 379 | 380 | 381 | {/* Favicon */} 382 | 383 | 384 | {/* Facebook Meta Tags */} 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | {/* Twitter Meta Tags */} 395 | 396 | 397 | 398 | 399 | 400 | {/* */} 401 | 402 | 403 | {/* Outer layer */} 404 | 405 | 406 | 407 | {/* Nav bar */} 408 | 409 | 410 | {/* Body */} 411 | 419 | 420 | 421 | 422 | 423 | ); 424 | } 425 | 426 | // This gets called on every request 427 | export async function getServerSideProps(context) { 428 | // Get path param 429 | const { params } = context; 430 | const { id: pathParams } = params; 431 | let pollObject; 432 | let pollId, gaiaAddress; 433 | 434 | if (pathParams && pathParams?.[0]) { 435 | pollId = pathParams[0]; 436 | } 437 | if (pathParams && pathParams?.[1]) { 438 | gaiaAddress = pathParams[1]; 439 | } 440 | 441 | // Fetch from Gaia 442 | if (pollId && gaiaAddress) { 443 | // Form gaia url 444 | const pollGaiaUrl = Constants.GAIA_HUB_PREFIX + gaiaAddress + "/" + pollId + ".json"; 445 | 446 | const response = await fetch(pollGaiaUrl); 447 | pollObject = await response.json(); 448 | } 449 | 450 | // Get current block height 451 | const currentBlock = await getRecentBlock(); 452 | const currentBlockHeight = currentBlock?.tenure_height || 0; 453 | 454 | // Pass data to the page via props 455 | return { 456 | props: { 457 | pollObject, 458 | pollId, 459 | gaiaAddress, 460 | currentBlockHeight 461 | }, 462 | }; 463 | } -------------------------------------------------------------------------------- /pages/[param].js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { Col, Container, Row } from "react-bootstrap"; 3 | import { Constants } from "../common/constants"; 4 | import SummaryComponent from "../components/summary/SummaryComponent"; 5 | import { getStacksAPIPrefix } from "../services/auth"; 6 | 7 | export default function SummaryPage(props) { 8 | // Variables 9 | const { summaryObject, gaiaAddress, allPolls } = props; 10 | const title = `${summaryObject?.title} | Ballot`; 11 | const description = summaryObject?.description?.substr(0, 160); 12 | const metaImage = "https://ballot.gg/images/ballot-meta.png"; 13 | const displayURL = ""; 14 | 15 | // Functions 16 | 17 | // Design 18 | return ( 19 | <> 20 | 21 | {title} 22 | 23 | 24 | 25 | 26 | {/* Favicon */} 27 | 28 | 29 | {/* Facebook Meta Tags */} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {/* Twitter Meta Tags */} 40 | 41 | 42 | 43 | 44 | 45 | {/* */} 46 | 47 | 48 | {/* Outer layer */} 49 | 50 | 51 | 52 | {/* Body */} 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | // This gets called on every request 62 | export async function getServerSideProps(context) { 63 | // Get path param 64 | const { params } = context; 65 | const { param } = params; 66 | let summaryObject = null; 67 | let gaiaAddress = null; 68 | let allPolls = null; 69 | 70 | // If it is .btc address 71 | if (param?.toString().endsWith(".btc")) { 72 | try { 73 | // Get name details 74 | const getZoneFileUrl = `${getStacksAPIPrefix()}/v1/names/${param}/zonefile`; 75 | const response = await fetch(getZoneFileUrl); 76 | const zoneFile = await response.json(); 77 | 78 | // Get profile details 79 | if (zoneFile && zoneFile["zonefile"]) { 80 | const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; 81 | const profileUrl = zoneFile["zonefile"].match(urlRegex)[0]; 82 | const response = await fetch(profileUrl); 83 | const profileObject = await response.json(); 84 | 85 | // Get gaia address 86 | if (profileObject[0]['decodedToken']?.['payload']?.["claim"]?.['apps']?.["https://ballot.gg"]) { 87 | const gaiaPrefixUrl = profileObject[0]['decodedToken']?.['payload']?.["claim"]?.['apps']?.["https://ballot.gg"]; 88 | const splittedArray = gaiaPrefixUrl.split("/"); 89 | 90 | gaiaAddress = splittedArray.pop(); 91 | while (splittedArray.length > 0 && !gaiaAddress) { 92 | gaiaAddress = splittedArray.pop(); 93 | } 94 | } 95 | } 96 | } catch (error) { 97 | } 98 | } else if (param) { 99 | gaiaAddress = param; 100 | } 101 | 102 | if (gaiaAddress) { 103 | // Form gaia url 104 | let summaryGaiaUrl = Constants.GAIA_HUB_PREFIX + gaiaAddress + "/summary.json"; 105 | 106 | try { 107 | const response = await fetch(summaryGaiaUrl); 108 | summaryObject = await response.json(); 109 | allPolls = summaryObject?.polls ? summaryObject?.polls : null; 110 | } catch (error) { 111 | // Summary not found 112 | summaryObject = null; 113 | allPolls = null; 114 | } 115 | } 116 | 117 | // Pass data to the page via props 118 | return { 119 | props: { 120 | summaryObject, 121 | gaiaAddress, 122 | allPolls 123 | }, 124 | }; 125 | } -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { SSRProvider } from 'react-bootstrap' 3 | import '../styles/globals.css' 4 | 5 | function MyApp({ Component, pageProps }) { 6 | return <> 7 | 8 | {/* Header */} 9 | 10 | {/* Fathom Analytics */} 11 |