diff --git a/README.md b/README.md
index 35975be..15429a0 100644
--- a/README.md
+++ b/README.md
@@ -2,91 +2,434 @@
Utility functions for Dash governance on the blockchain.
-## Prosposal
+# Table of Contents
-- proposal close (previous end epoch)
-- vote close
-- superblock (payment)
-- proposal open (start epoch)
-- vote open
-- proposal close (upcoming end epoch)
+- [How to Install](#how-to-install)
+ - [with cURL](#with-curl)
+ - [with NPM](#with-npm)
+- [How to Use](#how-to-use)
+ - [Node](#node)
+ - [Browser](#browser)
+- [QuickStart](#quickstart)
+ - [Boilerplate](#boilerplate)
+- [API](#api)
+ - [Overview](#overview)
+ - [Core Details](#core-details)
+ - [Additional Details](#additional-details)
+- [Notes](#notes)
+
+# How to Install
+
+## with cURL
+
+1. Make a `vendor` folder
+ ```sh
+ mkdir -p ./vendor/
+ ```
+2. Download
+ ```sh
+ # 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
+ ```
+
+## with NPM
+
+1. Install `node@22+`
+ ```sh
+ curl https://webi.sh/node | sh
+ source ~/.config/envman/PATH.env
+ ```
+2. Install `dashgov@1.x`
+
+```sh
+npm install --save dashgov@1
+```
+
+# How to Use
+
+## Node
```js
-let current = {
- height: 2114623,
- ms: new Date("2024-08-01 22:01:00").valueOf(),
-};
-let cycles = 3;
-let estimates = DashGov.estimateFutureBlocks(current, cycles);
-
-let proposalObj = {
- start_epoch: current.proposalOpen[0].seconds,
- end_epoch: current.proposalClose[2].seconds,
- name: "test-proposal-4",
- payment_address: "yM7M4YJFv58hgea9FUxLtjKBpKXCsjxWNX",
- payment_amount: 100,
- type: 1,
- url: "https://www.dashcentral.org/p/test-proposal-4",
-};
+// node versions < v22.0 may require `node --experimental-require-module`
+let DashGov = require(`dashgov`);
+
+// ...
+```
+
+## Browser
+
+```html
+
+
+```
+
+# QuickStart
+
+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:
+
+1. Display Valid Voting and Payment Ranges
+
+ ```js
+ 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);
+ ```
+
+2. Create proposal from user choices
+ ```js
+ 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/`,
+ });
+ ```
+3. Get & Load Burn WIF
+
+ ```js
+ 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");
+ }
+ ```
+
+4. Draft & Check the full Tx and Gobj
+
+ ```js
+ 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);
+ ```
+
+5. Validate & Submit Tx & Gobj
+
+ ```js
+ 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,
+ );
-let proposalJson = JSON.stringify(proposal);
-let encoder = new TextEncoder();
-let proposalBytes = encoder.encode(proposalJson);
-
-// JSON *should* be serialized canonically with lexicographically-sorted keys,
-// HOWEVER, a hex string (not bytes) is used to guarantee reproducible output.
-let proposalHex = Gobj.utils.bytesToHex(proposalBytes);
-
-let now = Date.now();
-let secs = now / 1000;
-secs = Math.round(secs);
-
-// Note: the full object is shown for completeness, however, most
-// gobject args were either deprecated or never implemented
-let gobj = {
- // hashParent: null,
- // revision: 1, // MUST be one
- time: secs,
- dataHex: proposalHex,
- // signature: null,
+ // 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);
+ }
+ ```
+
+## Boilerplate
+
+And the Boilerplate functions can be implemented like this: \
+(this is not included due to dependency issues with many bundlers)
+
+```js
+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;
};
-gobj = DashGov.normalize(gobj);
+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 gobjBytes = DashGov.serializeForCollateralTx(gobj);
-let gobjOpReturn = await DashGov.utils.doubleSha256(gobjBytes);
+ let secondsPerBlock = DashGov.measureSecondsPerBlock(snapshot, root);
+
+ return {
+ secondsPerBlock,
+ snapshot,
+ };
+};
let keyUtils = {
- getPrivateKey: function (info) {
- // lookup by info.address or similar
- let privKeyBytes = doStuff();
- return privKeyBytes;
+ /**
+ * @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;
},
};
-let dashTx = DashTx.create(keyUtils);
-let txraw = await dashTx.createMemo({ bytes: gobjOpReturnBytes });
-
-let result = await RPC.request({
- method: "sendrawtransaction",
- params: [txraw.hex],
-});
-
-// poll for txid to appear in recent blocks
-let txid = result.txid;
-
-let result = await RPC.request({
- method: "gobject",
- params: [
- "submit",
- gobj.hashParent,
- gobj.revision,
- gobj.time,
- gobj.dataHex,
- txid,
- ],
-});
+
+Boilerplate.dashtx = DashTx.create(keyUtils);
```
+# API
+
+## Overview
+
+- Estimation Utils
+
+ ```js
+ 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
+ ```js
+ 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
+ ```
+
+## Core Details
+
+```
+DashGov.estimateProposalCycles
+DashGov.selectEstimates
+DashGov.proposal.draftJson
+DashGov.proposal.draft
+DashGov.serializeForBurnTx
+```
+
+- DashGov.estimateProposalCycles
+ ```js
+ let estimates = DashGov.estimateProposalCycles(
+ count,
+ blockinfo.snapshot,
+ blockinfo.secondsPerBlock,
+ );
+ // See definition of 'Estimate' for each of these
+ // { last, lameduck, estimates }
+ ```
+- DashGov.selectEstimates
+ ```js
+ let selected = DashGov.selectEstimates(estimates, startPeriod, endPeriod);
+ // See definition of 'Estimate' for each of these
+ // { sart, end }
+ ```
+- DashGov.proposal.draftJson
+ ```js
+ 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
+ ```js
+ 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
+ ```js
+ DashGov.serializeForBurnTx(gobj);
+ // Uint8Array
+ ```
+
+## Additional Details
+
+- Typedefs in the source
+- Implementation in `./bin/`
+- Implementation at
+- Copy and paste into an LLM and ask some questions
+
# Notes
```text