Skip to content

Commit 90a4640

Browse files
committed
WIP: README
1 parent 7924f5d commit 90a4640

File tree

1 file changed

+293
-73
lines changed

1 file changed

+293
-73
lines changed

README.md

+293-73
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,311 @@
22

33
Utility functions for Dash governance on the blockchain.
44

5-
## Prosposal
5+
# Table of Contents
66

7-
- proposal close (previous end epoch)
8-
- vote close
9-
- superblock (payment)
10-
- proposal open (start epoch)
11-
- vote open
12-
- proposal close (upcoming end epoch)
7+
- [How to Install](#install)
8+
- [How to Use](#use)
9+
- [Node](#node)
10+
- [Browser](#browser)
11+
- [QuickStart](#use)
12+
- [API](#api)
13+
- [Overview](#overview)
14+
- [Notes](#notes)
15+
16+
# How to Install
17+
18+
## with cURL
19+
20+
1. Make a `vendor` folder
21+
```sh
22+
mkdir -p ./vendor/
23+
```
24+
2. Download
25+
```sh
26+
# versioned: https://github.com/dashhive/DashGov.js/blob/v1.0.0/dashgov.js
27+
# latest
28+
curl https://github.com/dashhive/DashGov.js/blob/main/dashgov.js > ./vendor/dashgov.js
29+
```
30+
31+
## with NPM
32+
33+
1. Install `node@22+`
34+
```sh
35+
curl https://webi.sh/node | sh
36+
source ~/.config/envman/PATH.env
37+
```
38+
2. Install `[email protected]`
39+
40+
```sh
41+
npm install --save dashgov@1
42+
```
43+
44+
# How to Use
45+
46+
## Node
1347

1448
```js
15-
let current = {
16-
height: 2114623,
17-
ms: new Date("2024-08-01 22:01:00").valueOf(),
18-
};
19-
let cycles = 3;
20-
let estimates = DashGov.estimateFutureBlocks(current, cycles);
21-
22-
let proposalObj = {
23-
start_epoch: current.proposalOpen[0].seconds,
24-
end_epoch: current.proposalClose[2].seconds,
25-
name: "test-proposal-4",
26-
payment_address: "yM7M4YJFv58hgea9FUxLtjKBpKXCsjxWNX",
27-
payment_amount: 100,
28-
type: 1,
29-
url: "https://www.dashcentral.org/p/test-proposal-4",
30-
};
49+
// node versions < v22.0 may require `node --experimental-require-module`
50+
let DashGov = require(`dashgov`);
51+
52+
// ...
53+
```
54+
55+
## Browser
56+
57+
```html
58+
<script type="importmap">
59+
{
60+
"imports": {
61+
"dashgov": "./node_modules/dashgov/dashgov.js",
62+
"dashgov/": "./node_modules/dashgov/"
63+
}
64+
}
65+
</script>
66+
<script type="module">
67+
import DashGov from "dashgov";
68+
69+
// ...
70+
</script>
71+
```
72+
73+
# QuickStart
74+
75+
There is no real quickstart.
76+
77+
This is a complex process with many steps and several business logic decisions
78+
that you must make for your application.
79+
80+
HOWEVER, the basic process is this:
81+
82+
1. Display Valid Voting and Payment Ranges
83+
84+
```js
85+
let startPeriod = 1;
86+
let count = 3;
87+
let offset = count - 1;
88+
let endPeriod = startPeriod + offset;
89+
90+
let blockinfo = await Boilerplate.getCurrentBlockinfo();
91+
let estimates = DashGov.estimateProposalCycles(
92+
count,
93+
blockinfo.snapshot,
94+
blockinfo.secondsPerBlock,
95+
);
96+
let selected = DashGov.selectEstimates(estimates, startPeriod, endPeriod);
97+
```
98+
99+
2. Create proposal from user choices
100+
```js
101+
let dashAmount = 75;
102+
let gobjData = DashGov.proposal.draftJson(selected, {
103+
name: `DCD_Web-Wallet_QR-Explorer`,
104+
payment_address: `XdNoeWLEGr7U7rz4vobtx1awMMtbzjK5AL`,
105+
payment_amount: 75,
106+
url: `https://digitalcash.dev/proposals/dcd-2-web-wallet-address-explorer/`,
107+
});
108+
```
109+
3. Get & Load Burn WIF
110+
111+
```js
112+
let burnWif = await DashKeys.utils.generateWifNonHd();
113+
let burnAddr = await DashKeys.wifToAddr(burnWif, {
114+
version: network,
115+
});
116+
117+
let minimumAmount = "1.00000250";
118+
let addrQr = new QRCode({
119+
content: `dash:${burnAddr}?amount=${minimumAmount}`,
120+
padding: 4,
121+
width: 256,
122+
height: 256,
123+
color: "#000000",
124+
background: "#ffffff",
125+
ecl: "M",
126+
});
127+
128+
// Show the SVG to the user
129+
let addrSvg = addrQr.svg();
130+
131+
// Check the UTXOs by some interval (10+ seconds to avoid rate limiting)
132+
let utxos = await rpc("getaddressutxos", {
133+
addresses: [burnAddr],
134+
});
135+
let total = DashTx.sum(utxos);
136+
if (sats >= 100100000) {
137+
throw new Error("refusing to burn > 1.001 DASH");
138+
}
139+
if (sats < 100000250) {
140+
throw new Error("need at least 1.000 DASH + 250 dust for fee");
141+
}
142+
```
143+
144+
4. Draft & Check the full Tx and Gobj
145+
146+
```js
147+
let now = Date.now();
148+
let gobj = DashGov.proposal.draft(
149+
now,
150+
selection.start.startMs,
151+
gobjData, // from 'DashGov.proposal.draftJson' (above)
152+
{},
153+
);
31154

32-
let proposalJson = JSON.stringify(proposal);
33-
let encoder = new TextEncoder();
34-
let proposalBytes = encoder.encode(proposalJson);
35-
36-
// JSON *should* be serialized canonically with lexicographically-sorted keys,
37-
// HOWEVER, a hex string (not bytes) is used to guarantee reproducible output.
38-
let proposalHex = Gobj.utils.bytesToHex(proposalBytes);
39-
40-
let now = Date.now();
41-
let secs = now / 1000;
42-
secs = Math.round(secs);
43-
44-
// Note: the full object is shown for completeness, however, most
45-
// gobject args were either deprecated or never implemented
46-
let gobj = {
47-
// hashParent: null,
48-
// revision: 1, // MUST be one
49-
time: secs,
50-
dataHex: proposalHex,
51-
// signature: null,
155+
let gobjBurnBytes = DashGov.serializeForBurnTx(gobj);
156+
let gobjBurnHex = DashGov.utils.bytesToHex(gobjBurnBytes);
157+
158+
let gobjHashBytes = await DashGov.utils.doubleSha256(gobjBurnBytes);
159+
let gobjid = DashGov.utils.hashToId(gobjHashBytes);
160+
161+
let gobjHashBytesReverse = gobjHashBytes.slice();
162+
gobjHashBytesReverse = gobjHashBytesReverse.reverse();
163+
let gobjidLittleEndian = DashGov.utils.hashToId(gobjHashBytesReverse);
164+
165+
let txInfoSigned = await dashTx.hashAndSignAll(txInfo);
166+
let txid = await DashTx.getId(txInfoSigned.transaction);
167+
168+
// Validate the GObject
169+
let gobjResult = await Boilerplate.rpc(
170+
"gobject",
171+
"check",
172+
gobj.dataHex,
173+
).catch(function (err) {
174+
return { error: err.message || err.stack || err.toString() };
175+
});
176+
177+
// { result: { 'Object status': 'OK' }, error: null, id: 5542 }
178+
if (gobjResult?.["Object status"] !== "OK") {
179+
throw new Error(`gobject failed: ${gobjResult?.error}`);
180+
}
181+
```
182+
5. TODO
183+
184+
And the Boilerplate functions can be implemented like this: \
185+
(this is not included due to dependency issues with many bundlers)
186+
187+
```js
188+
let network = `mainnet`;
189+
190+
let Boilerplate = {};
191+
192+
let DashKeys = require("dashkeys");
193+
let DashTx = require("dashtx");
194+
let Secp256k1 = require("Secp256k1");
195+
196+
Boilerplate.rpc = async function (method, ...params) {
197+
let rpcBasicAuth = `api:null`;
198+
let rpcBaseUrl = `https://${rpcBasicAuth}@rpc.digitalcash.dev/`;
199+
let rpcExplorer = "https://rpc.digitalcash.dev/";
200+
201+
// from DashTx
202+
let result = await DashTx.utils.rpc(rpcBaseUrl, method, ...params);
203+
return result;
52204
};
53205
54-
gobj = DashGov.normalize(gobj);
206+
Boilerplate.getCurrentBlockinfo = async function () {
207+
let rootResult = await Boilerplate.rpc("getblockhash", rootHeight);
208+
let rootInfoResult = await Boilerplate.rpc("getblock", rootResult, 1);
209+
let root = {
210+
block: blockInfoResult.height - 25000, // for reasonable estimates
211+
ms: rootInfoResult.time * 1000,
212+
};
55213
56-
let gobjBytes = DashGov.serializeForCollateralTx(gobj);
57-
let gobjOpReturn = await DashGov.utils.doubleSha256(gobjBytes);
214+
let tipsResult = await Boilerplate.rpc("getbestblockhash");
215+
let blockInfoResult = await Boilerplate.rpc("getblock", tipsResult, 1);
216+
let snapshot = {
217+
ms: blockInfoResult.time * 1000,
218+
block: blockInfoResult.height,
219+
};
220+
221+
let secondsPerBlock = DashGov.measureSecondsPerBlock(snapshot, root);
222+
223+
return {
224+
secondsPerBlock,
225+
snapshot,
226+
};
227+
};
58228
59229
let keyUtils = {
60-
getPrivateKey: function (info) {
61-
// lookup by info.address or similar
62-
let privKeyBytes = doStuff();
63-
return privKeyBytes;
64-
},
230+
/**
231+
* @param {DashTx.TxInputForSig} txInput
232+
* @param {Number} [i]
233+
*/
234+
getPrivateKey: async function (txInput, i) {
235+
return DashKeys.wifToPrivKey(burnWif, { version: network });
236+
},
237+
238+
/**
239+
* @param {DashTx.TxInputForSig} txInput
240+
* @param {Number} [i]
241+
*/
242+
getPublicKey: async function (txInput, i) {
243+
let privKeyBytes = await keyUtils.getPrivateKey(txInput, i);
244+
let pubKeyBytes = await keyUtils.toPublicKey(privKeyBytes);
245+
246+
return pubKeyBytes;
247+
},
248+
249+
/**
250+
* @param {Uint8Array} privKeyBytes
251+
* @param {Uint8Array} txHashBytes
252+
*/
253+
sign: async function (privKeyBytes, txHashBytes) {
254+
// extraEntropy set to null to make gobject transactions idempotent
255+
let sigOpts = { canonical: true, extraEntropy: null };
256+
let sigBytes = await Secp256k1.sign(txHashBytes, privKeyBytes, sigOpts);
257+
258+
return sigBytes;
259+
},
260+
261+
/**
262+
* @param {Uint8Array} privKeyBytes
263+
*/
264+
toPublicKey: async function (privKeyBytes) {
265+
let isCompressed = true;
266+
let pubKeyBytes = Secp256k1.getPublicKey(privKeyBytes, isCompressed);
267+
268+
return pubKeyBytes;
269+
},
65270
};
66-
let dashTx = DashTx.create(keyUtils);
67-
let txraw = await dashTx.createMemo({ bytes: gobjOpReturnBytes });
68-
69-
let result = await RPC.request({
70-
method: "sendrawtransaction",
71-
params: [txraw.hex],
72-
});
73-
74-
// poll for txid to appear in recent blocks
75-
let txid = result.txid;
76-
77-
let result = await RPC.request({
78-
method: "gobject",
79-
params: [
80-
"submit",
81-
gobj.hashParent,
82-
gobj.revision,
83-
gobj.time,
84-
gobj.dataHex,
85-
txid,
86-
],
87-
});
271+
272+
Boilerplate.dashtx = DashTx.create(keyUtils);
88273
```
89274

275+
# API
276+
277+
## Overview
278+
279+
- Estimation Utils
280+
281+
```js
282+
DashGov.PROPOSAL_LEAD_MS; // PROPOSAL_LEAD_MS
283+
DashGov.SUPERBLOCK_INTERVAL; // SUPERBLOCK_INTERVAL
284+
285+
DashGov.measureSecondsPerBlock(snapshot, root); // sPerBlock
286+
DashGov.estimateSecondsPerBlock(snapshot); // spb
287+
DashGov.estimateBlockHeight(ms, secondsPerBlock); // height
288+
DashGov.getNthNextSuperblock(height, offset); // superblockHeight
289+
DashGov.estimateProposalCycles(cycles, snapshot, rate, leadtime); // estimates
290+
291+
DashGov.selectEstimates(estimates, startPeriod, endPeriod); // selection
292+
DashGov.estimateNthNextGovCycle(snapshot, secondsPerBlock, offset); // estimate
293+
294+
DashGov.proposal.draftJson(selected, proposalData); // normalizedData
295+
DashGov.proposal.draft(now, startEpochMs, data, gobj); // normalGObj
296+
DashGov.proposal.sortAndEncodeJson(normalizedData); // hex
297+
298+
DashGov.serializeForBurnTx({ hashParent, revision, time, dataHex }); // bytes
299+
```
300+
301+
- Convenience Utils
302+
```js
303+
DashGov.utils.bytesToHex(bytes); // hex
304+
await DashGov.utils.sleep(ms); // void
305+
await DashGov.utils.doubleSha256(bytes); // gobjHash
306+
DashGov.utils.hashToId(hashBytes); // id
307+
DashGov.utils.toVarIntSize(n); // size
308+
```
309+
90310
# Notes
91311

92312
```text

0 commit comments

Comments
 (0)