The Vocdoni SDK is a convenient way to interact with the Vocdoni Protocol through the new API, allowing anyone to create, manage and participate in voting processes and collective decision-making.
You can test the SDK here.
No funds needed in your wallet: Metamask or Walletconnect are only used for signing transactions that are sent to the Vocdoni chain! Testing tokens in
dev
environment are automatically sent from faucet once the account is created.
See examples for more examples.
The Vocdoni SDK and the underlying API is WIP. Please beware that it can be broken
at any time if the release is alpha
or beta
. We encourage to review this
repository for any change.
You'll need a working nodejs environment, but other than that, you're free to use any package manager (either npm, yarn, pnpm...). Let's start by adding the SDK to your project:
# with yarn
yarn add @vocdoni/sdk
# with npm
npm i @vocdoni/sdk
# with pnpm
pnpm add @vocdoni/sdk
For creating elections or vote on them, blockchain transactions need to be build, thus a signer is required. Any kind of standard ethers signer should work.
This project has been bundled into many different formats in order for you to
import it based on the environment you're using it. When importing it via
@vocdoni/sdk
it will chose a version based on your current environment
(commonjs or esm). There's also another UMD version which can be accessed via
@vocdoni/sdk/umd
in case you need it.
// Will use the correct version based on your environment
import { VocdoniSDKClient } from '@vocdoni/sdk'
// UMD version
import SDK from '@vocdoni/sdk/umd'
You can see a working ESM example in the examples folder.
The entry point is the SDK constructor, it will instantiate a new client
connected to the API endpoint corresponding to dev
(development) or stg
(staging).
For this readme examples, the signer bootstrapping will be ignored and you'll just see a
signer
constant.
This is the recommended environment for most testing use cases, since the
dev
environment is more subject to blockchain resets and downtimes than
the stg
one.
const client = new VocdoniSDKClient({
env: EnvOptions.STG, // mandatory, can be 'dev' or 'stg'
wallet: signer, // optional, the signer used (Metamask, Walletconnect)
})
const client = new VocdoniSDKClient({
env: EnvOptions.DEV, // mandatory, can be 'dev' or 'stg'
wallet: signer, // optional, the signer used (Metamask, Walletconnect)
})
Before creating any new processes, you should register your account against our blockchain (vochain):
(async () => {
const info = await client.createAccount()
console.log(info) // will show account information
})();
The createAccount
method will try to fetch an existing account first and, if
it does not exist, it will register it against the blockchain.
The account to be registered will be the one of the signer specified in the constructor.
You can safely use createAccount
to fetch any account information, but you can
also decide to just fetch it, without falling back to an account registration:
(async () => {
const info = await client.fetchAccountInfo()
console.log(info) // shows info (only if account exists, otherwise throws error)
})();
Accounts require Vocdoni tokens in order to be able to register against our blokchain. The process above will automatically fetch some tokens from a faucet under development. For production environments, you should contact us (Vocdoni) for a byte64 string faucet, and specify it when creating your account:
(async () => {
const info = await client.createAccount({
faucetPackage: "<b64string>"
})
})();
Accounts also require Vocdoni tokens in order to create new processes.
You can check the balance thanks to the previous methods (createAccount
and/or
fetchAccountInfo
) and, under development, you can request new tokens thanks
to the included faucet:
(async () => {
const info = await client.createAccount()
if (info.balance === 0) {
await client.collectFaucetTokens()
}
})();
Note: the
collectFaucetTokens
method only works under development environment and for accounts with not enough tokens to create new processes.
After you successfully registered your account against Vocdoni's blockchain, you can start creating processes.
A process will require a census of people who will vote it. Let's start creating it:
const census = new PlainCensus()
// accepts any ethereum-alike addresses
census.add(address)
census.add('0x0000000000000000000000000000000000000000')
(async () => {
// random wallet, for example purposes
census.add(await Wallet.createRandom().getAddress())
})();
Note you can also use compressed public keys when adding them to the census:
census.add(computePublicKey(Wallet.createRandom().publicKey, true));
After you got all the addresses for your census, you may as well create the process instance:
The simplest form to create an election with the minimum parameters is:
const election = Election.from({
title: 'Election title',
description: 'Election description',
endDate: new Date('2023-01-23 23:23:23'),
census,
})
Check out the election params interface to see all the allowed params. There are several options for creating custom types of elections or voting methods:
const election = Election.from({
title: {
en: 'This is a test in english',
es: 'Esto es un test en castellano',
default: 'This is the default title',
},
description: 'Election description',
// a header image for your election (this is for example purposes; avoid using random sources)
header: 'https://source.unsplash.com/random/2048x600',
// a stream uri image for your election (this is for example purposes; avoid using random sources)
streamUri: 'https://source.unsplash.com/random/2048x600',
startDate: new Date('2023-01-23 12:00:00'),
endDate: new Date('2023-01-23 23:23:23'),
census,
electionType: {
autoStart: true, // if the election can start automatically at start date
interruptible: true, // if the election can be paused or ended
dynamicCensus: false, // if the census can be changed
secretUntilTheEnd: false, // if the election has to be secret until end date
anonymous: false, // if the election is anonymous
},
voteType: {
uniqueChoices: false, // if the choices are unique when voting
maxVoteOverwrites: 0, // number of times a vote can be overwritten
costFromWeight: false, // for cuadrating voting
costExponent: 10000, // for cuadrating voting
}
})
Of course, you will also need some questions in this voting process, how would people vote otherwise?
election.addQuestion('Ain\'t this process awesome?', [
{
title: 'Yes',
value: 0,
},
{
title: 'No',
value: 1,
},
]).addQuestion('How old are you?', [
{
title: 'Child (0-9 yo)',
value: 0,
},
{
title: 'Kid (10-16 yo)',
value: 1,
},
{
title: 'Adult (17-60 yo)',
value: 2,
},
{
title: 'Elder (60+ yo)',
value: 3,
},
])
If you're a developer, maybe the value set to zero in Yes (and viceversa) confuses you. Note that this is a mapping of values; people voting on Yes will properly set the value as specified (zero in this case), thus printing the results as you expect.
You can finally confirm the transaction in the blockchain by just calling
createElection
:
(async () => {
const id = await client.createElection(election)
console.log(id) // will show the created election id
})();
The election id you got there will be the one you use to access the election. After a few seconds of creating it, you should be able to check it on our explorer (or the dev one if you're using the development environment).
You can always access a process information and metadata using fetchElection
:
(async () => {
const info = await client.fetchElection(id)
console.log(info) // shows election information and metadata
})();
// or...
(async () => {
client.setElectionId(id)
const info = await client.fetchElection()
console.log(info) // shows election information and metadata
})();
See the PublishedElection class details for more information about the returning object.
See the Election lifecycle states details for more information about the election status and the possible status changes once the election is created.
(async () => {
await client.pauseElection(id)
const election = await client.fetchElection(id)
console.log(election.status) // Matches ElectionStatus.PAUSED
})();
(async () => {
await client.cancelElection(id)
const election = await client.fetchElection(id)
console.log(election.status) // Matches ElectionStatus.CANCELED
})();
(async () => {
await client.endElection(id)
const election = await client.fetchElection(id)
console.log(election.status) // Matches ElectionStatus.ENDED
})();
(async () => {
await client.continueElection(id)
const election = await client.fetchElection(id)
console.log(election.status) // Matches ElectionStatus.READY
})();
(async () => {
const isInCensus = await client.isInCensus();
console.log(isInCensus) // true or false
})();
(async () => {
const hasAlreadyVoted = await client.hasAlreadyVoted();
console.log(hasAlreadyVoted) // true or false
})();
(async () => {
const votesLeft = await client.votesLeftCount();
console.log(votesLeft) // number of times the user can submit his vote
})();
(async () => {
const isAbleToVote = await client.isAbleToVote();
console.log(isAbleToVote) // true or false
})();
To vote a process you only need two things: the process id to vote to, and the option (or options) being voted:
(async () => {
client.setElectionId(id)
// votes "Yes" and "Adult (17-60 yo)"
const vote = new Vote([0, 2]);
const voteId = await client.submitVote(vote)
})();
For some cases where the voters don't have an owned Wallet, we can generate a deterministic Wallet based on arbitrary data, like, for example, the user and hash password from a custom CRM.
Here is an example where a Wallet is generated using the username and the hash of the password which we would use to identify the user in our platform. This Wallet can then be used for the census and for voting purposes.
// 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 is the sha256 of 'test'
const userWallet = VocdoniSDKClient.generateWalletFromData(['user1', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08']);
console.log(userWallet) // address is 0x8AF1b3EDB817b5854e3311d583905a3421F49829
The SDK comes with an implementation of the common handler API of a CSP which is explained here.
For creating a CSP based election, a CspCensus
has to be set to the election. This census need the
CSP public key (CSP_PUBKEY
in the example) and the CSP Url (CSP_URL
in the example).
const election = Election.from({
title: 'Election title',
description: 'Election description',
// a header image for your process (this is for example purposes; avoid using random sources)
header: 'https://source.unsplash.com/random/2048x600',
endDate: new Date('2023-01-23 23:23:23'),
census: new CspCensus(CSP_PUBKEY, CSP_URL),
})
// The election can be created the same way from here...
The SDK comes with some wrappers to get a blind signature from the CSP in order to vote. The complete flow is shown here:
// Client initialization
const client = new VocdoniSDKClient({
env: EnvOptions.DEV,
wallet: signer, // the signer used (Metamask, Walletconnect)
electionId: '934234...', // The election identifier
csp_url: CSP_URL // The CSP url defined when creating an election
})
// Auth steps for the CSP (can vary of the type of the CSP)
const step0 = (await client.cspStep(0, ['Name test'])) as ICspIntermediateStepResponse;
const step1 = (await client.cspStep(
1,
[step0.response.reduce((acc, v) => +acc + +v, 0).toString()],
step0.authToken
)) as ICspFinalStepResponse;
// Get the blind signature
const signature = await client.cspSign(signer.address, step1.token);
// Get the vote based on the signature
const vote = client.cspVote(new Vote([index % 2]), signature);
// Vote
const voteId = await client.submitVote(vote);
You can find a full featured CRA application with all the previous steps in the examples folder. In that folder you'll also find a es modules example, creating and voting an election process.
You can find the autogenerated docs in our Developer Portal or you can build them following this guide.
This SDK is licensed under the GNU Affero General Public License v3.0.
Vocdoni API Typescript SDK
Copyright (C) 2022 Vocdoni Roots MCU
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.