DVote JS
DVoteJS aims to provide utility classes and methods to invoke decentralized operations within a voting process. It covers the typical functionality of Client applications, as well as the Process Manager or the Census Manager.
The intended functionality is to interact with a public Ethereum blockchain, to fetch data from a decentralized filesystem, to enforce data schema validity, to prepare vote packages and using decentralized messaging networks through Gateways.
This library implements the protocol defined on https://docs.vocdoni.io/architecture/components.html
Getting started
npm install dvote-js ethers
- Gateways can serve as a DVote gateway and as a Web3 Gateway
DVoteGateway
and Web3Gateway
- Signers and Wallets are both used to sign Web3 transactions, as well as authenticating DVote requests
Ethers.js
The library depends on Ethers.js providers, wallets and signers. Ethers.js is fully compatible with Web3.
- To interact with the blockchain, we need a Provider.
- In order to send transactions we need a Wallet (with a private key) or a Signer (like Metamask) to sign them.
File API
To upload a file and pin it on IPFS, you need the data as a String
or o as a UInt8Array
and a signer. Gateways will probably not let you upload arbitrary data unless you are authorised to do so.
const { FileApi, Gateway } = require("dvote-js")
const { Wallet } = require("ethers")
const MNEMONIC = "..."
const PATH = "m/44'/60'/0'/0/0"
const GATEWAY_DVOTE_URI = "wss://host:port/dvote"
const GATEWAY_PUB_KEY = "02..."
const NETWORK_ID = ""
const wallet = Wallet.fromMnemonic(MNEMONIC, PATH)
gateway = await Gateway.fromInfo({ uri: GATEWAY_DVOTE_URI, supportedApis: ["file"], publicKey: GATEWAY_PUB_KEY })
await gateway.init()
const wallet = Wallet.fromMnemonic(MNEMONIC)
console.log("SIGNING FROM ADDRESS", wallet.address)
const strData = "HELLO WORLD"
const origin = await FileApi.add(Buffer.from(strData), "my-file.txt", wallet, dvoteGw)
console.log("DATA STORED ON:", origin)
const data = await FileApi.fetchString(origin, dvoteGw)
console.log("DATA:", data)
const data = await FileApi.fetchBytes(origin, dvoteGw)
console.log("DATA:", data)
Entity
Register or update an Entity:
const { EntityApi, Gateway } = require("dvote-js")
const { Wallet, providers } = require("ethers")
const MNEMONIC = "..."
const gw = await Gateway.randomFromDefault("goerli")
await gw.init()
const provider = gw.provider
const wallet = Wallet.fromMnemonic(MNEMONIC, PATH)
const resolverInstance = await gw.getEnsPublicResolverInstance(wallet)
const myEntityAddress = await wallet.getAddress()
const jsonMetadata = { ... }
const contentUri = await EntityApi.setMetadata(myEntityAddress, jsonMetadata, wallet, gw)
console.log("IPFS ORIGIN:", contentUri)
Fetch the metadata of an Entity:
const { EntityApi, Gateway } = require("dvote-js")
const { Wallet, providers } = require("ethers")
const GATEWAY_DVOTE_URI = "wss://host:443/dvote"
const GATEWAY_SUPPORTED_APIS = ["file", "vote", "census"]
const GATEWAY_PUBLIC_KEY = "03..."
const GATEWAY_WEB3_PROVIDER_URI = "https://rpc.slock.it/goerli"
const MNEMONIC = "..."
const provider = new providers.JsonRpcProvider(GATEWAY_WEB3_PROVIDER_URI)
const wallet = Wallet.fromMnemonic(MNEMONIC, PATH)
const myEntityAddress = await wallet.getAddress()
const gwInfo = new GatewayInfo(GATEWAY_DVOTE_URI, GATEWAY_SUPPORTED_APIS, GATEWAY_WEB3_PROVIDER_URI, GATEWAY_PUBLIC_KEY)
const gateway = await Gateway.fromInfo(gwInfo)
await gateway.init()
const meta = await EntityApi.getMetadata(myEntityAddress, gateway)
console.log("JSON METADATA", meta)
Set ENS text records
const { Gateway, ensHashAddress } = require("dvote-js")
const { Wallet, providers } = require("ethers")
const GATEWAY_WEB3_PROVIDER_URI = "https://rpc.slock.it/goerli"
const MNEMONIC = "..."
const wallet = Wallet.fromMnemonic(MNEMONIC, PATH)
const gwInfo = new GatewayInfo(GATEWAY_DVOTE_URI, GATEWAY_SUPPORTED_APIS, GATEWAY_WEB3_PROVIDER_URI, GATEWAY_PUBLIC_KEY)
const gateway = await Gateway.fromInfo(gwInfo)
const resolverInstance = await gateway.getEnsPublicResolverInstance({ provider, wallet })
await gateway.init()
const myEntityAddress = await wallet.getAddress()
const entityNode = ensHashAddress(myEntityAddress)
const tx = await contractInstance.setText(entityNode, "my-key", "1234")
await tx.wait()
const val = await contractInstance.text(entityNode, "my-key")
console.log("Value stored on the blockchain:", val)
Using providers
Ethers.js providers can connect using different sources.
const ethers = require("ethers")
import { ethers } from "ethers"
const provider = ethers.getDefaultProvider('homestead')
const altProvider = new ethers.providers.EtherscanProvider('ropsten')
const web3Provider1 = new ethers.providers.Web3Provider(web3.currentProvider)
const currentProvider2 = new web3.providers.HttpProvider('http://localhost:8545')
const web3Provider2 = new ethers.providers.Web3Provider(currentProvider2)
More information
Wallets
Generating a wallet from a mnemonic (and an optional path and Web3 provider):
const { WalletUtil } = require("dvote-js")
const mnemonic = "my mnemonic ..."
const mnemonicPath = "m/44'/60'/0'/0/0"
const provider = ethers.getDefaultProvider('goerli')
const wallet = WalletUtil.fromMnemonic(mnemonic, mnemonicPath, provider)
wallet.sendTransaction(...)
Generating a standalone deterministic wallet from a passphrase and a (non-private) seed. They are intended to provide wallets where the private key can be accessed.
const { Random, WalletUtil } = require("dvote-js")
const provider = ethers.getDefaultProvider('goerli')
const hexSeed = Random.getHex()
const passphrase = "A very Difficult 1234 passphrase"
const hexSeed = "0xfdbc446f9f3ea732d23c7bcd10c784d041887d48ebc392c4ff51882ae569ca15"
const passphrase = "A very Difficult 1234 passphrase"
const wallet = WalletUtil.fromSeededPassphrase(passphrase, hexSeed, provider)
wallet.signMessage(...)
Accessing the browser wallet or MetaMask:
const { SignerUtil } = require("dvote-js")
const signer = SignerUtil.fromInjectedWeb3()
signer.sendTransaction(...)
Components
Entity
The entity API allows updating and querying the key-value fields of the Entity Resolver contract. On top of the key-value storage, lies a link to the entity's metadata, which is the human readable information about it.
Process
Resembling the likes of a Unix process, a Vocdoni process contains a set of flags defining how an L2 governance process is conducted on the Vochain.
In addition to the flags is the process metadata, which is the human readable content that voters will be prompted for making a choice.
Gateway
Provides utility functions to fetch data from decentralized filesystems, sending messages and adding files to IPFS.
Example
For more involved examples, check out:
example/index.js
for examples about specific componentsexample/standard/index.js
for an end to end standard processexample/bridge/index.js
for an end to end process using an ERC20 based censusexample/ethers/index.js
for general usage tips about Ethers.js Wallets and Providers
Development
Simply run npm run test
. It is an alias for npm run test:unit
and npm run test:integration
.
- Unit testing will start an internal Ganache provider and launch transactions to it
- Integration testing is still a WIP
Builders
In order to avoid tedious and repetitive testing code, you can check out the test/builders
folder. Entity and Process builders deploy a new instance and create an Entity/Process with default values. These default values can be overridden with one-liners, if needed:
const EntityResolverBuilder = require("./test/builders/entity-resolver")
const ProcessBuilder = require("./test/builders/process")
const contractInstance1 = await new EntityResolverBuilder().build()
const contractInstance2 = await new ProcessBuilder().build()
const contractInstance3 = await new EntityResolverBuilder()
.withName("Another name")
.build()
const contractInstance4 = await new ProcessBuilder()
.withVotingPublicKey("...")
.build(3)
Note: This is still under heavy development.
Mocha
When adding new test suites, don't forget to add a call to addCompletionHooks()
. Otherwise, the NodeJS process will keep up indefinitely when testing.
Simulating future timestamps
If you need a transaction to happen in a future timestamp, use test/utils > incrementTimestamp()
instead of forcing your code to wait.
Be aware that from this point, if you use Date.now()
on the Javascript side, values will not match the timestamp of the blockchain. So make sure to call getBlockNumber()
and getBlock(<num>) > timestamp
.
Testing accounts
Use test/web3-service > DevWeb3Service > accounts
to retrieve a list of 10 funded accounts with the following data schema:
{
privateKey: "...",
address: "0x...",
provider: <ethers-js-provider>,
wallet: <ethers-js-wallet>
}
These accounts are connected to an in-memory Ganache RPC node.