CoolFace
Apppublic

codenlighten/scrypt

sourceHugging Faceopenrailupdated 3y agoView on Hugging Face
0likes
voting.md707 linesDownload Raw Back to root
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 11![](../../static/img/voting.gif)12 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 285![](../../static/img/deploy-output.png)286 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 654![](../../static/img/voting-homepage.png)655 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 682![](../../static/img/voting-scripts.png)683 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 700![](../../static/img/gh-pages.png)701 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