Run a Simple Vote#

This guide builds a poll as a dApp. Each address can vote once, for one of a fixed set of options, until a deadline. Votes and counts are stored in the dApp’s account data storage, so anyone can count them. Once deployed, nobody can change them, not even the account that created the poll.

Note

The script compiles on the public Testnet node (2026-10-25) with complexity 54 for vote and 1 for the verifier. The TypeScript is typechecked against @decentralchain/transactions@5.0.1. It has not been broadcast as part of this documentation pass.

Before You Start#

You need Node.js 24 or later, the compileRide helper from Compile a Script, and two Testnet accounts with DCC from the faucet. Use a fresh poll account: once its script is set, it can never send a transaction again. The voter account needs 0.005 DCC for each vote.

npm install @decentralchain/transactions@5.0.1

Write the Script#

Save this as ride/voting.ride:

{-# STDLIB_VERSION 6 #-}
{-# CONTENT_TYPE DAPP #-}
{-# SCRIPT_TYPE ACCOUNT #-}

# One vote per address, for one of a fixed set of options, until the deadline
# height. The poll account writes "deadline" with a Data transaction before it
# sets this script: the verifier below then locks the account for good, so
# nobody, the poll's creator included, can change a vote or a count.

let options = ["yes", "no", "abstain"]

@Callable(i)
func vote(option: String) = {
  let deadline = getIntegerValue(this, "deadline")
  let voterKey = "vote_" + toString(i.caller)
  if (height > deadline) then throw("Voting closed at height " + toString(deadline))
  else if (!containsElement(options, option)) then throw("Unknown option: " + option)
  else if (isDefined(getString(this, voterKey))) then throw("This address has already voted")
  else {
    let count = valueOrElse(getInteger(this, "count_" + option), 0)
    [
      StringEntry(voterKey, option),
      IntegerEntry("count_" + option, count + 1)
    ]
  }
}

@Verifier(tx)
func verify() = false
  • i.caller is the address that sent the call, so vote_<address> records one vote per address. A second call from the same address is rejected.

  • count_<option> keeps a running total, so reading the result takes one lookup per option.

  • The verifier returns false. The poll account can’t sign anything after the script is set, so it can’t rewrite the data either. That is also why the deadline has to be written before the script.

One address is one vote. If a voter can create addresses freely, so can a cheater. To weight votes by stake instead, make vote require a payment in a token, or read the voter’s token balance with assetBalance(i.caller, assetId).

Deploy the Poll#

import { readFile } from 'node:fs/promises';
import { data, setScript, invokeScript, broadcast, waitForTx, libs, nodeInteraction } from '@decentralchain/transactions';
import { compileRide } from './compile-ride.js';

const nodeUrl = 'https://testnet-node.decentralchain.io';
const pollSeed = 'poll account secret seed phrase here';
const voterSeed = 'voter secret seed phrase here';
const poll = libs.crypto.address(pollSeed, '!');
const wait = (id: string) => waitForTx(id, { apiBase: nodeUrl, timeout: 120_000 });

// 1. Write the deadline: voting stays open for about one day of 30-second blocks
const deadline = (await nodeInteraction.currentHeight(nodeUrl)) + 2_880;
const settings = data({ data: [{ key: 'deadline', type: 'integer', value: deadline }], chainId: '!' }, pollSeed);
await broadcast(settings, nodeUrl);
await wait(settings.id);

// 2. Compile the script and set it on the poll account
const source = await readFile(new URL('./ride/voting.ride', import.meta.url), 'utf8');
const deploy = setScript({ script: await compileRide(source, nodeUrl), chainId: '!' }, pollSeed);
await broadcast(deploy, nodeUrl);
await wait(deploy.id);

Vote#

// 3. Vote
const vote = invokeScript(
  {
    dApp: poll,
    call: { function: 'vote', args: [{ type: 'string', value: 'yes' }] },
    chainId: '!',
  },
  voterSeed,
);
await broadcast(vote, nodeUrl);
await wait(vote.id);

Each argument of a call is an object with a type (string, integer, boolean, binary or list) and a value. The types must match the parameters of the callable function.

Count the Votes#

// 4. Count the votes: the script keeps a running count per option. An option nobody
// voted for has no entry yet, so read all the counts at once and treat a missing one as 0.
const counts = await nodeInteraction.accountData({ address: poll, match: 'count_.*' }, nodeUrl);
for (const option of ['yes', 'no', 'abstain']) {
  console.log(`${option}: ${counts[`count_${option}`]?.value ?? 0}`);
}

Anyone can read the counts without a transaction, from any node. To list every vote, read the dApp’s data entries whose keys start with vote_.