codenlighten/scrypt
0
1---2sidebar_position: 63---4 5# Tutorial 6: Voting6 7## Overview8 9In this tutorial, we will go over how to use sCrypt to build a full-stack voting dApp on Bitcoin, including the smart contract and an interactive front-end.10 1112 13On the web page, you can see the candidate list. Clicking the like button will cast one vote for the corresponding candidate. This will prompt the wallet to ask for a user's approval. A transaction calling the contract will be sent after her approval.14 15First, we will write and deploy the smart contract step by step. Afterward, we will build a front-end with React that allows users to cast votes and thus interact with the contract.16 17## Contract18 19### Properties20 21For each candidate, there are two properties we need to store in the contract: her name and her votes received so far.22 23We define a type alias of `ByteString` to represent a candidate name.24 25```ts26export type Name = ByteString27```28 29We define a struct to represent a candidate.30 31```ts32export type Candidate = {33 name: Name34 votesReceived: bigint35}36```37 38We use a `FixedArray` to store the list of candidates, which we alias as type `Candidates`.39Since candidates' vote counts can be updated, we mark it [stateful](../how-to-write-a-contract/stateful-contract.md#stateful-properties) by setting `@prop(true)`.40 41```ts42export const N = 243export type Candidates = FixedArray<Candidate, typeof N>44 45export class Voting extends SmartContract { 46 @prop(true)47 candidates: Candidates48 // ...49}50```51 52### Constructor53 54Initialize all the `@prop` properties in the constructor. Note that we only need to pass the candidate names in the argument, because the votes they received would be all 0 at the beginning.55 56```ts57constructor(names: FixedArray<Name, typeof N>) {58 super(...arguments)59 // initialize fixed array60 this.candidates = fill({61 name: toByteString(''),62 votesReceived: 0n63 }, N)64 // set names and set votes they received to 065 for (let i = 0; i < N; i++) {66 this.candidates[i] = { name: names[i], votesReceived: 0n }67 }68}69```70 71### Methods72 73The only way to interact with this contract is to vote for one candidate in the list, so we will have only 1 **public** method `vote`. It takes only 1 parameter: the name of the candidate you want to vote for.74 75```ts76@method()77public vote(name: Name) {78 // 1) change contract state: add one vote to `candidate` in the list79 // 2) propogate the state80}81```82 83We can simply use a `for` loop to implement this: find the corresponding candidate in the list by name, then increment its vote by one. We implement this in a helper method `increaseVotesReceived`.84 85```ts86// cast one vote to a candidate87@method()88increaseVotesReceived(name: Name): void {89 for (let i = 0; i < N; i++) {90 if (this.candidates[i].name === name) {91 this.candidates[i].votesReceived++92 }93 }94}95```96 97After we increment the candidate's votes and update the contract state, we make sure the new state is maintained in the spending transaction's output [as usual](../how-to-write-a-contract/stateful-contract.md#update-states). Another output is added if change is needed.98 99```ts100let outputs: ByteString = this.buildStateOutput(this.ctx.utxo.value)101outputs += this.buildChangeOutput()102assert(this.ctx.hashOutputs === hash256(outputs), 'hashOutputs mismatch')103```104 105The public function `vote` is now finished.106 107```ts108@method()109public vote(name: Name) {110 // change contract state: add one vote to `candidate` in the list111 this.increaseVotesReceived(name)112 113 // restrict tx outputs114 // to contain the latest state with the same balance115 let outputs: ByteString = this.buildStateOutput(this.ctx.utxo.value)116 // to contain the change output when necessary117 outputs += this.buildChangeOutput()118 119 assert(this.ctx.hashOutputs === hash256(outputs), 'hashOutputs mismatch')120}121```122 123### Final Code124 125You have completed the `Voting` contract! The [final complete code](https://github.com/sCrypt-Inc/voting/blob/master/src/contracts/voting.ts) is as follows:126 127```ts128import { assert, ByteString, hash256, method, prop, SmartContract, FixedArray, fill, toByteString } from 'scrypt-ts'129 130export type Name = ByteString131 132export type Candidate = {133 name: Name134 votesReceived: bigint135}136 137export const N = 2138 139export type Candidates = FixedArray<Candidate, typeof N>140 141export class Voting extends SmartContract {142 @prop(true)143 candidates: Candidates144 145 constructor(names: FixedArray<Name, typeof N>) {146 super(...arguments)147 // initialize fixed array148 this.candidates = fill({149 name: toByteString(''),150 votesReceived: 0n,151 }, N)152 // set names and set votes they received to 0153 for (let i = 0; i < N; i++) {154 this.candidates[i] = {155 name: names[i],156 votesReceived: 0n,157 }158 }159 }160 161 /**162 * vote for a candidate163 * @param name candidate's name164 */165 @method()166 public vote(name: Name) {167 // change contract state: add one vote to `candidate` in the list168 this.increaseVotesReceived(name)169 // output containing the latest state and the same balance170 let outputs: ByteString = this.buildStateOutput(this.ctx.utxo.value)171 outputs += this.buildChangeOutput()172 assert(this.ctx.hashOutputs === hash256(outputs), 'hashOutputs mismatch')173 }174 175 @method()176 increaseVotesReceived(name: Name): void {177 for (let i = 0; i < N; i++) {178 if (this.candidates[i].name === name) {179 this.candidates[i].votesReceived++180 }181 }182 }183}184```185 186## Frontend187 188We will add a frontend to the voting smart contract according to [this guide](../how-to-integrate-a-frontend/how-to-integrate-a-frontend.md).189 190### Setup Project191 192The front-end will be created using [Create React App](https://create-react-app.dev/).193 194```bash195npx create-react-app voting --template typescript196```197 198### Install the sCrypt SDK199 200The sCrypt SDK enables you to easily compile, test, deploy, and call contracts.201 202Use the `scrypt-cli` command line to install the SDK.203 204```bash205cd voting206npx scrypt-cli init207```208 209This command will create a contract file at `src\contracts\voting.ts`, replace the content of the file with the contract written [above](#final-code).210 211### Compile Contract212 213Compile the contract with the following command: 214 215```bash216npx scrypt-cli compile217```218 219This command will generate a contract artifact file at `artifacts\src\contracts\voting.json`.220 221### Contract Deployment222 223After [installing the sCrypt SDK](#install-the-scrypt-sdk), you will have a script `deploy.ts` in the project directory, which can be used to deploy our `Voting` contract after some minor modifications.224 225```ts226import { Name, Voting, N } from './src/contracts/voting'227import { bsv, TestWallet, DefaultProvider, toByteString, FixedArray } from 'scrypt-ts'228 229import * as dotenv from 'dotenv'230 231// Load the .env file232dotenv.config()233 234// Read the private key from the .env file.235// The default private key inside the .env file is meant to be used for the Bitcoin testnet.236// See https://scrypt.io/docs/bitcoin-basics/bsv/#private-keys237const privateKey = bsv.PrivateKey.fromWIF(process.env.PRIVATE_KEY || '')238 239// Prepare signer. 240// See https://scrypt.io/docs/how-to-deploy-and-call-a-contract/#prepare-a-signer-and-provider241const signer = new TestWallet(privateKey, new DefaultProvider({242 network: bsv.Networks.testnet243}))244 245async function main() {246 await Voting.compile()247 248 const candidateNames: FixedArray<Name, typeof N> = [249 toByteString('iPhone', true),250 toByteString('Android', true)251 ]252 253 const instance = new Voting(254 candidateNames255 )256 257 // Connect to a signer.258 await instance.connect(signer)259 260 // Contract deployment.261 const amount = 1262 const deployTx = await instance.deploy(amount)263 console.log('Voting contract deployed: ', deployTx.id)264}265 266main()267```268 269Before deploying the contract, we need to create a `.env` file and save your private key in the `PRIVATE_KEY` environment variable.270 271```272PRIVATE_KEY=xxxxx273```274 275If you don't have a private key, you can follow [this guide](../../how-to-deploy-and-call-a-contract/faucet) to generate one using Sensilet wallet, then fund the private key's address with our [faucet](https://scrypt.io/faucet/).276 277Run the following command to deploy the contract.278 279```bash280npm run deploy:contract281```282 283After success, you will see an output similar to the following:284 285286 287#### Contract ID288 289Your can get the deployed contract's ID: the TXID and the output index where the contract is located.290```js291const contract_id = {292 /** the deployment transaction id */293 txId: "6751b645e1579e8e6201e3c59b900ad58e59868aa5e4ee89359d3f8ca1d66c8a",294 /** the output index */295 outputIndex: 0,296};297```298 299### Verify300 301After a successful deployment of a smart contract, you can verify the deployed contract script:302 303```sh304npm run verify:contract305```306 307Upon execution, the designated contract code undergoes verification on sCrypt's servers. If successful, the outcome will be [displayed on WoC](https://test.whatsonchain.com/script/cecb4f8799913df3e5af50bc81a24e3fef3216a92452d27cd97dcd7ccbce1f1b), under the "sCrypt" tab. See the ["How to Verify a Contract"](../how-to-verify-a-contract.md) page for more details.308 309### Load Contract Artifact310 311Before writing the front-end code, we need to load the contract artifact in `src\index.tsx`.312 313```ts314import { Voting } from './contracts/voting';315var artifact = require('../artifacts/src/contracts/voting.json');316Voting.loadArtifact(artifact);317```318 319### Integrate Wallet320 321Use `requestAuth` method of `signer` to request access to the wallet.322 323```ts324// request authentication325const { isAuthenticated, error } = await signer.requestAuth();326if (!isAuthenticated) {327 // something went wrong, throw an Error with `error` message328 throw new Error(error);329}330 331// authenticated332// ...333```334 335### Integrate sCrypt Service336 337To interacte with the voting contract, we need to create a contract instance representing the latest state of the contract on chain. When both Alice and Bob vote on the webpage, we need to ensure that their contract instances are always up to date. After Alice votes, we have to notify Bob that the state of the contract has changed and synchronize his local contract instance to the latest state on chain.338 339Fortunately,`sCrypt` provides such infrastructure service, which abstracts away all the common complexities of communicating with the blockchain, so we do not have to track the contract state, which could be computationally demanding as blockchain grows. We can instead focus on our application's business logic.340 341To use it, we first have to initialize it according to [this guide](../advanced/how-to-integrate-scrypt-service.md).342 343```ts344Scrypt.init({345 apiKey: 'YOUR_API_KEY',346 network: bsv.Networks.testnet347})348```349 350### Connect Signer to `ScryptProvider`351 352It's required to connect your signer to `ScryptProvider` when using sCrypt service.353 354```ts355const provider = new ScryptProvider();356const signer = new SensiletSigner(provider);357 358signerRef.current = signer;359```360 361### Fetch Latest Contract Instance362 363We can fetch a contract's latest instance by calling the `Scrypt.contractApi.getLatestInstance()` using its [contract ID](#contract-id). With this instance, we can easily read a contract's properties to display to the user on the webpage, or update the contract state by calling its public method as [before](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call) when the user votes for a candidate.364 365```ts366function App() {367 const [votingContract, setContract] = useState<Voting>();368 const [error, setError] = React.useState("");369 370 // ...371 372 async function fetchContract() {373 try {374 const instance = await Scrypt.contractApi.getLatestInstance(375 Voting,376 contract_id377 );378 setContract(instance);379 } catch (error: any) {380 console.error("fetchContract error: ", error);381 setError(error.message);382 }383 }384 385 // ...386}387```388 389### Read contract state390 391With the contract instance, we can read its lastest state and render it.392 393```ts394function byteString2utf8(b: ByteString) {395 return Buffer.from(b, "hex").toString("utf8");396}397 398function App() {399 // ...400 401 return (402 <div className="App">403 <header className="App-header">404 <h2>What's your favorite phone?</h2>405 </header>406 <TableContainer407 component={Paper}408 variant="outlined"409 style={{ width: 1200, height: "80vh", margin: "auto" }}410 >411 <Table>412 <TableHead>413 <TableRow>414 <TableCell align="center">Iphone</TableCell>415 <TableCell align="center">Android</TableCell>416 </TableRow>417 </TableHead>418 <TableBody>419 <TableRow>420 <TableCell align="center">421 <Box>422 <Box423 sx={{424 height: 200,425 }}426 component="img"427 alt={"iphone"}428 src={`${process.env.PUBLIC_URL}/${"iphone"}.png`}429 />430 </Box>431 </TableCell>432 <TableCell align="center">433 <Box>434 <Box435 sx={{436 height: 200,437 }}438 component="img"439 alt={"android"}440 src={`${process.env.PUBLIC_URL}/${"android"}.png`}441 />442 </Box>443 </TableCell>444 </TableRow>445 <TableRow>446 <TableCell align="center">447 <Box>448 <Typography variant={"h1"} >449 {votingContract?.candidates[0].votesReceived.toString()}450 </Typography>451 <Button452 variant="text"453 onClick={voting}454 name={votingContract?.candidates[0].name}455 >456 👍457 </Button>458 </Box>459 </TableCell>460 461 <TableCell align="center">462 <Divider orientation="vertical" flexItem />463 <Box>464 <Typography variant={"h1"}>465 {votingContract?.candidates[1].votesReceived.toString()}466 </Typography>467 <Button468 variant="text"469 onClick={voting}470 name={votingContract?.candidates[1].name}471 >472 👍473 </Button>474 </Box>475 </TableCell>476 </TableRow>477 </TableBody>478 </Table>479 </TableContainer>480 <Footer />481 <Snackbar482 open={error !== ""}483 autoHideDuration={6000}484 onClose={handleClose}485 >486 <Alert severity="error">{error}</Alert>487 </Snackbar>488 489 <Snackbar490 open={success.candidate !== "" && success.txId !== ""}491 autoHideDuration={6000}492 onClose={handleSuccessClose}493 >494 <Alert severity="success">495 {" "}496 <Link497 href={`https://test.whatsonchain.com/tx/${success.txId}`}498 target="_blank"499 rel="noreferrer"500 >501 {`"${byteString2utf8(success.candidate)}" got one vote, tx: ${502 success.txId503 }`}504 </Link>505 </Alert>506 </Snackbar>507 </div>508 );509}510```511 512### Update Contract State513 514To update the contract's state, we need to call its public method. We create a function `voting()` to handle the voting event triggered by a user.515 516Calling a contract public method is [the same as before](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#contract-call).517 518```ts519async function voting(e: any) {520 // ...521 522 const signer = signerRef.current as SensiletSigner;523 524 if (votingContract && signer) {525 const { isAuthenticated, error } = await signer.requestAuth();526 if (!isAuthenticated) {527 throw new Error(error);528 }529 530 await votingContract.connect(signer);531 532 // create the next instance from the current533 const nextInstance = votingContract.next();534 535 const candidateName = e.target.name;536 537 // update state538 nextInstance.increaseVotesReceived(candidateName);539 540 // call the method of current instance to apply the updates on chain541 votingContract.methods542 .vote(candidateName, {543 next: {544 instance: nextInstance,545 balance: votingContract.balance,546 },547 })548 .then((result) => {549 console.log(`Voting call tx: ${result.tx.id}`);550 })551 .catch((e) => {552 setError(e.message);553 fetchContract();554 console.error("call error: ", e);555 });556 }557}558```559 560If successful, you will see the following log in the `console`:561 562```563Voting call tx: fc8b3d03b8fa7469d66a165b017fe941fa8ab59c0979457cef2b6415d659e3f7564```565 566### Subscribe to Contract Event567 568So far, we have a fully working app. However, there is a slight problem. When Alice clicks on the like button for a candadate in her browser, the candidate's vote count in Bob's browser does not increase, unless he manually refreshes. 569We need a way to listen to contract event. 570 571We call `Scrypt.contractApi.subscribe(options: SubscribeOptions<T>, cb: (e: ContractCalledEvent<T>) => void): SubScription` to subscribe to events that the contract has been called. When a contract gets called and updated, we refresh the UI in real time, re-render all the content on the page and show the updated vote count.572 573The subscribe function takes 2 parameters:574 5751. `options: SubscribeOptions<T>`: it includes a contract class, a contract ID, and a optional list of method names monitored.576 577```ts578interface SubscribeOptions<T> {579 clazz: new (...args: any) => T;580 id: ContractId;581 methodNames?: Array<string>;582}583```584 585If `methodNames` is set, you will be notified only when public functions in the list are called. Otherwise, you will be notified when ANY public function is called.586 5872. `callback: (event: ContractCalledEvent<T>) => void`: a callback funciton upon receiving notifications. 588 589`ContractCalledEvent<T>` contains the relevant information when the contract is called, such as the public function name and function arguments when the call occurs.590 591```ts592export interface ContractCalledEvent<T> {593 /** name of public function */594 methodName: string;595 /** public function arguments */596 args: SupportedParamType[];597 /** transaction where contract is called from */598 tx: bsv.Transaction;599 /**600 * If a stateful contract is called, `nexts` contains the contract instance containing the new state generated by this call.601 * If a stateless contract is called, `nexts` is empty.602 */603 nexts: Array<T>;604}605```606 607The code to subscribe to contract events is as follows.608 609```ts610useEffect(() => {611 const provider = new ScryptProvider();612 const signer = new SensiletSigner(provider);613 614 signerRef.current = signer;615 616 fetchContract();617 618 // subscribe by contract_id619 const subscription = Scrypt.contractApi.subscribe({620 clazz: Voting,621 id: contract_id622 }, (event: ContractCalledEvent<Voting>) => {623 // update the contract instance 624 setSuccess({625 txId: event.tx.id,626 candidate: event.args[0] as ByteString,627 });628 setContract(event.nexts[0]);629 });630 631 return () => {632 // unsubscribe633 subscription.unsubscribe();634 };635}, []);636```637 638### Deploy to GitHub Pages639 640After pushing the frontend project to your GitHub account, it's easy to [publish a website with GitHub Pages](https://create-react-app.dev/docs/deployment/#github-pages), so that users can interact with your dApp with the browser.641 642#### Step 1. Add `homepage` to `package.json`643 644Open your `package.json` and add a `homepage` field for your project.645 646```json647{648 "name": "voting",649 "homepage": "https://[YOUR-GITHUB-USERNAME].github.io/[YOUR-REPO-NAME]"650 ...651}652```653 654655 656For example, our demo repo is at https://github.com/sCrypt-Inc/voting, so we set657 658```659https://sCrypt-Inc.github.io/voting660```661 662as the homepage, where `sCrypt-Inc` is our GitHub username, and `voting` is the repo name.663 664#### Step 2. Install `gh-pages` and add `scripts` in `package.json`665 666Run the following command to install the dependency.667 668```sh669npm install --save gh-pages670```671 672Then add two scripts in `package.json`.673 674```json675"scripts": {676 "predeploy": "npm run build",677 "deploy": "gh-pages -d build",678 ...679},680```681 682683 684:::note685The `predeploy` script will run automatically before `deploy` is run.686:::687 688#### Step 3. Deploy the site689 690Run the following command to deploy the website.691 692```sh693npm run deploy694```695 696#### Step 4. Update GitHub project settings697 698After running the `deploy` script, don't forget to update your GitHub project settings to use `gh-pages` branch. Go to `Settings --> Code and automation/Pages`, and select `gh-pages` as the branch used by the GitHub Pages site.699 700701 702### Conclusion703 704Congratulations! You have successfully completed a fullstack voting dapp fully on Bitcoin.705 706The repo is [here](https://github.com/sCrypt-Inc/voting). And an online example is [here](http://classic.scrypt.io/voting).707 