Utility functions for Dash governance on the blockchain.
- Make a
vendor
foldermkdir -p ./vendor/
- Download
# versioned: https://github.com/dashhive/DashGov.js/blob/v1.0.0/dashgov.js # latest curl https://github.com/dashhive/DashGov.js/blob/main/dashgov.js > ./vendor/dashgov.js
- Install
node@22+
curl https://webi.sh/node | sh source ~/.config/envman/PATH.env
- Install
[email protected]
npm install --save dashgov@1
// node versions < v22.0 may require `node --experimental-require-module`
let DashGov = require(`dashgov`);
// ...
<script type="importmap">
{
"imports": {
"dashgov": "./node_modules/dashgov/dashgov.js",
"dashgov/": "./node_modules/dashgov/"
}
}
</script>
<script type="module">
import DashGov from "dashgov";
// ...
</script>
There is no real quickstart.
This is a complex process with many steps and several business logic decisions that you must make for your application.
HOWEVER, the basic process is this:
-
Display Valid Voting and Payment Ranges
let startPeriod = 1; let count = 3; let offset = count - 1; let endPeriod = startPeriod + offset; let blockinfo = await Boilerplate.getCurrentBlockinfo(); let estimates = DashGov.estimateProposalCycles( count, blockinfo.snapshot, blockinfo.secondsPerBlock, ); let selected = DashGov.selectEstimates(estimates, startPeriod, endPeriod);
-
Create proposal from user choices
let dashAmount = 75; let gobjData = DashGov.proposal.draftJson(selected, { name: `DCD_Web-Wallet_QR-Explorer`, payment_address: `XdNoeWLEGr7U7rz4vobtx1awMMtbzjK5AL`, payment_amount: 75, url: `https://digitalcash.dev/proposals/dcd-2-web-wallet-address-explorer/`, });
-
Get & Load Burn WIF
let burnWif = await DashKeys.utils.generateWifNonHd(); let burnAddr = await DashKeys.wifToAddr(burnWif, { version: network, }); let minimumAmount = "1.00000250"; let addrQr = new QRCode({ content: `dash:${burnAddr}?amount=${minimumAmount}`, padding: 4, width: 256, height: 256, color: "#000000", background: "#ffffff", ecl: "M", }); // Show the SVG to the user let addrSvg = addrQr.svg(); // Check the UTXOs by some interval (10+ seconds to avoid rate limiting) let utxos = await rpc("getaddressutxos", { addresses: [burnAddr], }); let total = DashTx.sum(utxos); if (sats >= 100100000) { throw new Error("refusing to burn > 1.001 DASH"); } if (sats < 100000250) { throw new Error("need at least 1.000 DASH + 250 dust for fee"); }
-
Draft & Check the full Tx and Gobj
let now = Date.now(); let gobj = DashGov.proposal.draft( now, selection.start.startMs, gobjData, // from 'DashGov.proposal.draftJson' (above) {}, ); let gobjBurnBytes = DashGov.serializeForBurnTx(gobj); let gobjBurnHex = DashGov.utils.bytesToHex(gobjBurnBytes); let gobjHashBytes = await DashGov.utils.doubleSha256(gobjBurnBytes); let gobjid = DashGov.utils.hashToId(gobjHashBytes); let gobjHashBytesReverse = gobjHashBytes.slice(); gobjHashBytesReverse = gobjHashBytesReverse.reverse(); let gobjidLittleEndian = DashGov.utils.hashToId(gobjHashBytesReverse); let txInfoSigned = await dashTx.hashAndSignAll(txInfo); let txid = await DashTx.getId(txInfoSigned.transaction);
-
Validate & Submit Tx & Gobj
let gobjResult = await Boilerplate.rpc( "gobject", "check", gobj.dataHex, ).catch(function (err) { return { error: err.message || err.stack || err.toString() }; }); // { result: { 'Object status': 'OK' }, error: null, id: 5542 } if (gobjResult?.["Object status"] !== "OK") { throw new Error(`gobject failed: ${gobjResult?.error}`); } let txResult = await ProposalApp.rpc( "sendrawtransaction", draft.tx.transaction, ); // wait for confirmation of burn tx for (;;) { let txResult = await ProposalApp.rpc("gettxoutproof", [draft.txid]).catch( function (err) { const E_NOT_IN_BLOCK = -5; if (err.code === E_NOT_IN_BLOCK) { return null; } throw err; }, ); if (txResult) { break; } await DashGov.utils.sleep(10000); } // wait for confirmation of gobj for (;;) { // some of these numbers must be strings for some reason let hashParent = gobj.hashParent?.toString() || "0"; let revision = gobj.revision?.toString() || "1"; let time = gobj.time.toString(); let gobjResult = await ProposalApp.rpc( "gobject", "submit", hashParent, revision, time, gobj.dataHex, txid, ).catch(function (err) { const E_INVALID_COLLATERAL = -32603; if (err.code === E_INVALID_COLLATERAL) { return null; } throw err; }); if (gobjResult) { break; } await DashGov.utils.sleep(10000); }
And the Boilerplate functions can be implemented like this:
(this is not included due to dependency issues with many bundlers)
let network = `mainnet`;
let Boilerplate = {};
let DashKeys = require("dashkeys");
let DashTx = require("dashtx");
let Secp256k1 = require("Secp256k1");
Boilerplate.rpc = async function (method, ...params) {
let rpcBasicAuth = `api:null`;
let rpcBaseUrl = `https://${rpcBasicAuth}@rpc.digitalcash.dev/`;
let rpcExplorer = "https://rpc.digitalcash.dev/";
// from DashTx
let result = await DashTx.utils.rpc(rpcBaseUrl, method, ...params);
return result;
};
Boilerplate.getCurrentBlockinfo = async function () {
let rootResult = await Boilerplate.rpc("getblockhash", rootHeight);
let rootInfoResult = await Boilerplate.rpc("getblock", rootResult, 1);
let root = {
block: blockInfoResult.height - 25000, // for reasonable estimates
ms: rootInfoResult.time * 1000,
};
let tipsResult = await Boilerplate.rpc("getbestblockhash");
let blockInfoResult = await Boilerplate.rpc("getblock", tipsResult, 1);
let snapshot = {
ms: blockInfoResult.time * 1000,
block: blockInfoResult.height,
};
let secondsPerBlock = DashGov.measureSecondsPerBlock(snapshot, root);
return {
secondsPerBlock,
snapshot,
};
};
let keyUtils = {
/**
* @param {DashTx.TxInputForSig} txInput
* @param {Number} [i]
*/
getPrivateKey: async function (txInput, i) {
return DashKeys.wifToPrivKey(burnWif, { version: network });
},
/**
* @param {DashTx.TxInputForSig} txInput
* @param {Number} [i]
*/
getPublicKey: async function (txInput, i) {
let privKeyBytes = await keyUtils.getPrivateKey(txInput, i);
let pubKeyBytes = await keyUtils.toPublicKey(privKeyBytes);
return pubKeyBytes;
},
/**
* @param {Uint8Array} privKeyBytes
* @param {Uint8Array} txHashBytes
*/
sign: async function (privKeyBytes, txHashBytes) {
// extraEntropy set to null to make gobject transactions idempotent
let sigOpts = { canonical: true, extraEntropy: null };
let sigBytes = await Secp256k1.sign(txHashBytes, privKeyBytes, sigOpts);
return sigBytes;
},
/**
* @param {Uint8Array} privKeyBytes
*/
toPublicKey: async function (privKeyBytes) {
let isCompressed = true;
let pubKeyBytes = Secp256k1.getPublicKey(privKeyBytes, isCompressed);
return pubKeyBytes;
},
};
Boilerplate.dashtx = DashTx.create(keyUtils);
-
Estimation Utils
DashGov.PROPOSAL_LEAD_MS; // PROPOSAL_LEAD_MS DashGov.SUPERBLOCK_INTERVAL; // SUPERBLOCK_INTERVAL DashGov.measureSecondsPerBlock(snapshot, root); // sPerBlock DashGov.estimateSecondsPerBlock(snapshot); // spb DashGov.estimateBlockHeight(ms, secondsPerBlock); // height DashGov.getNthNextSuperblock(height, offset); // superblockHeight DashGov.estimateProposalCycles(cycles, snapshot, secsPerBlock, leadtime); // estimates DashGov.selectEstimates(estimates, startPeriod, endPeriod); // selection DashGov.estimateNthNextGovCycle(snapshot, secsPerBlock, offset); // estimate DashGov.proposal.draftJson(selected, proposalData); // normalizedData DashGov.proposal.draft(now, startEpochMs, data, gobj); // normalGObj DashGov.proposal.sortAndEncodeJson(normalizedData); // hex DashGov.serializeForBurnTx(gobj); // bytes
-
Convenience Utils
DashGov.utils.bytesToHex(bytes); // hex await DashGov.utils.sleep(ms); // void await DashGov.utils.doubleSha256(bytes); // gobjHash DashGov.utils.hashToId(hashBytes); // id DashGov.utils.toVarIntSize(n); // size
DashGov.estimateProposalCycles
DashGov.selectEstimates
DashGov.proposal.draftJson
DashGov.proposal.draft
DashGov.serializeForBurnTx
- DashGov.estimateProposalCycles
let estimates = DashGov.estimateProposalCycles( count, blockinfo.snapshot, blockinfo.secondsPerBlock, ); // See definition of 'Estimate' for each of these // { last, lameduck, estimates }
- DashGov.selectEstimates
let selected = DashGov.selectEstimates(estimates, startPeriod, endPeriod); // See definition of 'Estimate' for each of these // { sart, end }
- DashGov.proposal.draftJson
let gobjData = DashGov.proposal.draftJson(selected, { name: `DCD_Web-Wallet_QR-Explorer`, payment_address: `XdNoeWLEGr7U7rz4vobtx1awMMtbzjK5AL`, payment_amount: 75, url: `https://digitalcash.dev/proposals/dcd-2-web-wallet-address-explorer/`, }); // { end_epoch, name, payment_address, // payment_amount, start_epoch, type, url }
- DashGov.proposal.draft
let overrides = {}; let gobj = DashGov.proposal.draft( Date.now(), selection.start.startMs, gobjData, overrides, ); // { end_epoch, name, payment_address, // payment_amount, start_epoch, type, url, // hashParent, revision, time, dataHex, // masternodeOutpoint, collateralTxId, // collateralTxOutputIndex, signature }
- DashGov.serializeForBurnTx
DashGov.serializeForBurnTx(gobj); // Uint8Array
- Typedefs in the source
- Implementation in
./bin/
- Implementation at https://github.com/digitalcashdev/DashProposal
- Copy and paste into an LLM and ask some questions
A block is generated for a self-correcting target average of 155 seconds.
(actually 157.64 seconds over the 7 year average)
Superblock is every 16616 blocks (rounded up from 30 days).
Superblock is when payment occurs.
Voting ends 1662 blocks before the superblock (rounded up from 3 days).
// ~(60*24*3)/2.6
Votes after the block deadline are discarded for that superblock, but will
be counted for the next, if the proposal is still active.