3 /*jshint maxcomplexity:25 */
5 require("dotenv").config({ path: ".env" });
6 require("dotenv").config({ path: ".env.secret" });
8 let pkg = require("../package.json");
10 let Fs = require("fs").promises;
12 let CrowdNode = require("../lib/crowdnode.js");
13 let Dash = require("../lib/dash.js");
14 let Insight = require("../lib/insight.js");
15 let Qr = require("../lib/qr.js");
16 let Ws = require("../lib/ws.js");
18 let Dashcore = require("@dashevo/dashcore-lib");
20 const DUFFS = 100000000;
21 let qrWidth = 2 + 67 + 2;
23 // 0.00236608 // required for signup
24 // 0.00002000 // TX fee estimate
25 // 0.00238608 // minimum recommended amount
28 let signupOnly = CrowdNode.requests.signupForApi + CrowdNode.requests.offset;
29 let acceptOnly = CrowdNode.requests.acceptTerms + CrowdNode.requests.offset;
30 let signupFees = signupOnly + acceptOnly;
31 let feeEstimate = 500;
32 let signupTotal = signupFees + 2 * feeEstimate;
34 function showQr(signupAddr, duffs = 0) {
35 let signupUri = `dash://${signupAddr}`;
37 signupUri += `?amount=${duffs}`;
40 let signupQr = Qr.ascii(signupUri, { indent: 4 });
41 let addrPad = Math.ceil((qrWidth - signupUri.length) / 2);
43 console.info(signupQr);
45 console.info(" ".repeat(addrPad) + signupUri);
48 function showVersion() {
49 console.info(`${pkg.name} v${pkg.version} - ${pkg.description}`);
56 console.info("Usage:");
57 console.info(" crowdnode help");
58 console.info(" crowdnode status ./privkey.wif");
59 console.info(" crowdnode signup ./privkey.wif");
60 console.info(" crowdnode accept ./privkey.wif");
62 " crowdnode deposit ./privkey.wif [dash-amount] [--no-reserve]",
65 " crowdnode withdrawal ./privkey.wif <percent> # 1.0-100.0 (steps by 0.1)",
69 console.info("Helpful Extras:");
70 console.info(" crowdnode generate [./privkey.wif]");
71 console.info(" crowdnode load [./privkey.wif]");
72 console.info(" crowdnode balance ./privkey.wif");
74 " crowdnode transfer ./source.wif <key-file-or-pub-addr> [dash-amount]",
78 console.info("CrowdNode HTTP RPC:");
79 console.info(" crowdnode http FundsOpen <addr>");
80 console.info(" crowdnode http VotingOpen <addr>");
81 console.info(" crowdnode http GetFunds <addr>");
82 console.info(" crowdnode http GetFundsFrom <addr> <seconds-since-epoch>");
83 console.info(" crowdnode http GetBalance <addr>");
84 console.info(" crowdnode http GetMessages <addr>");
85 console.info(" crowdnode http IsAddressInUse <addr>");
86 // TODO create signature rather than requiring it
87 console.info(" crowdnode http SetEmail ./privkey.wif <email> <signature>");
88 console.info(" crowdnode http Vote ./privkey.wif <gobject-hash> ");
89 console.info(" <Yes|No|Abstain|Delegate|DoNothing> <signature>");
91 " crowdnode http SetReferral ./privkey.wif <referral-id> <signature>",
96 function removeItem(arr, item) {
97 let index = arr.indexOf(item);
99 return arr.splice(index, 1)[0];
104 async function main() {
106 // crowdnode <subcommand> [flags] <privkey> [options]
108 // crowdnode withdrawal ./Xxxxpubaddr.wif 100.0
110 let args = process.argv.slice(2);
113 let forceConfirm = removeItem(args, "--unconfirmed");
114 let noReserve = removeItem(args, "--no-reserve");
116 let subcommand = args.shift();
118 if (!subcommand || ["--help", "-h", "help"].includes(subcommand)) {
124 if (["--version", "-V", "version"].includes(subcommand)) {
130 if ("generate" === subcommand) {
131 await generate(args.shift());
136 process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
137 let insightApi = Insight.create({ baseUrl: insightBaseUrl });
138 let dashApi = Dash.create({ insightApi: insightApi });
140 if ("load" === subcommand) {
144 insightBaseUrl: insightBaseUrl,
145 insightApi: insightApi,
152 process.stdout.write("Checking CrowdNode API... ");
153 await CrowdNode.init({
154 baseUrl: "https://app.crowdnode.io",
156 insightApi: insightApi,
158 console.info(`hotwallet is ${CrowdNode.main.hotwallet}`);
161 if ("http" === subcommand) {
163 let keyfile = args.shift();
164 let pub = await wifFileToAddr(keyfile);
166 // ex: http <rpc>(<pub>, ...)
168 let hasRpc = rpc in CrowdNode.http;
170 console.error(`Unrecognized rpc command ${rpc}`);
175 let result = await CrowdNode.http[rpc].apply(null, args);
176 if ("string" === typeof result) {
177 console.info(result);
179 console.info(JSON.stringify(result, null, 2));
184 let keyfile = args.shift();
187 privKey = await Fs.readFile(keyfile, "utf8");
188 privKey = privKey.trim();
190 privKey = process.env.PRIVATE_KEY;
193 // TODO generate private key?
196 `Error: you must provide either the WIF key file path or PRIVATE_KEY in .env`,
202 let pk = new Dashcore.PrivateKey(privKey);
203 let pub = pk.toPublicKey().toAddress().toString();
205 // deposit if balance is over 100,000 (0.00100000)
206 process.stdout.write("Checking balance... ");
207 let balanceInfo = await dashApi.getInstantBalance(pub);
208 let balanceDash = toDash(balanceInfo.balanceSat);
209 console.info(`${balanceInfo.balanceSat} (Đ${balanceDash})`);
211 let balanceInfo = await insightApi.getBalance(pub);
212 if (balanceInfo.unconfirmedBalanceSat || balanceInfo.unconfirmedAppearances) {
215 `Error: This address has pending transactions. Please try again in 1-2 minutes or use --unconfirmed.`,
217 console.error(balanceInfo);
218 if ("status" !== subcommand) {
227 balanceInfo: balanceInfo,
229 forceConfirm: forceConfirm,
230 hotwallet: CrowdNode.main.hotwallet,
231 insightBaseUrl: insightBaseUrl,
232 insightApi: insightApi,
233 noReserve: noReserve,
248 if ("balance" === subcommand) {
249 await balance(args, state);
254 // helper for debugging
255 if ("transfer" === subcommand) {
256 await transfer(args, state);
260 state.status = await CrowdNode.status(pub, state.hotwallet);
261 if (state.status?.signup) {
264 if (state.status?.accept) {
267 if (state.status?.deposit) {
271 if ("status" === subcommand) {
272 await status(args, state);
276 if ("signup" === subcommand) {
277 await signup(args, state);
281 if ("accept" === subcommand) {
282 await accept(args, state);
286 if ("deposit" === subcommand) {
287 await deposit(args, state);
291 if ("withdrawal" === subcommand) {
292 await withdrawal(args, state);
296 console.error(`Unrecognized subcommand ${subcommand}`);
304 async function generate(name) {
305 let pk = new Dashcore.PrivateKey();
307 let pub = pk.toAddress().toString();
308 let wif = pk.toWIF();
310 let filepath = `./${pub}.wif`;
314 note = `\n(for pubkey address ${pub})`;
318 let testDuff = toDuff(testDash);
320 let err = await Fs.access(filepath).catch(Object);
322 // TODO show QR anyway
323 //wif = await Fs.readFile(filepath, 'utf8')
324 //showQr(pub, testDuff);
325 console.info(`'${filepath}' already exists (will not overwrite)`);
330 await Fs.writeFile(filepath, wif, "utf8");
333 `Use the QR Code below to load a test deposit of Đ${testDash} onto your staking key.`,
336 showQr(pub, testDuff);
339 `Use the QR Code above to load a test deposit of Đ${testDash} onto your staking key.`,
342 console.info(`Generated ${filepath} ${note}`);
346 async function load(psuedoState, args) {
347 let name = args.shift();
349 // TODO factor out the common bits from generate?
350 let pk = new Dashcore.PrivateKey();
352 let pub = pk.toAddress().toString();
353 let wif = pk.toWIF();
355 let filepath = `./${pub}.wif`;
359 note = `\n(for pubkey address ${pub})`;
362 let err = await Fs.access(filepath).catch(Object);
364 console.info(`'${filepath}' already exists (will not overwrite)`);
366 await Fs.writeFile(filepath, wif, "utf8");
367 console.info(`Generated ${filepath} ${note}`);
370 let desiredAmountDash = parseFloat(args.shift() || 0);
371 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
372 let effectiveDuff = desiredAmountDuff;
373 let effectiveDash = "";
374 if (!effectiveDuff) {
375 effectiveDuff = CrowdNode.stakeMinimum + signupTotal + feeEstimate;
376 effectiveDash = toDash(effectiveDuff);
377 // Round to the nearest mDash
378 // ex: 0.50238108 => 0.50300000
379 effectiveDuff = toDuff(Math.ceil(effectiveDash * 1000) / 1000);
380 effectiveDash = toDash(effectiveDuff);
384 showQr(pub, effectiveDuff);
387 `Use the QR Code above to load ${effectiveDuff} (Đ${effectiveDash}) onto your staking key.`,
390 console.info(`(waiting...)`);
392 let payment = await Ws.waitForVout(psuedoState.insightBaseUrl, pub, 0);
393 console.info(`Received ${payment.satoshis}`);
397 async function balance(args, state) {
398 console.info(state.balanceInfo);
403 // ex: node ./bin/crowdnode.js transfer ./priv.wif 'pub' 0.01
404 async function transfer(args, state) {
405 let newAddr = await wifFileToAddr(process.argv[4]);
406 let dashAmount = parseFloat(process.argv[5] || 0);
407 let duffAmount = Math.round(dashAmount * DUFFS);
410 tx = await state.dashApi.createPayment(state.privKey, newAddr, duffAmount);
412 tx = await state.dashApi.createBalanceTransfer(state.privKey, newAddr);
415 let dashAmountStr = toDash(duffAmount);
417 `Transferring ${duffAmount} (Đ${dashAmountStr}) to ${newAddr}...`,
420 console.info(`Transferring balance to ${newAddr}...`);
422 await state.insightApi.instantSend(tx);
423 console.info(`Queued...`);
424 setTimeout(function () {
425 // TODO take a cleaner approach
426 // (waitForVout needs a reasonable timeout)
427 console.error(`Error: Transfer did not complete.`);
428 if (state.forceConfirm) {
429 console.error(`(using --unconfirmed may lead to rejected double spends)`);
433 await Ws.waitForVout(state.insightBaseUrl, newAddr, 0);
434 console.info(`Accepted!`);
439 async function status(args, state) {
441 console.info(`API Actions Complete for ${state.pub}:`);
442 console.info(` ${state.signup} SignUpForApi`);
443 console.info(` ${state.accept} AcceptTerms`);
444 console.info(` ${state.deposit} DepositReceived`);
446 let pk = new Dashcore.PrivateKey(state.privKey);
447 let pub = pk.toPublicKey().toAddress().toString();
448 let crowdNodeBalance = await CrowdNode.http.GetBalance(pub);
449 let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance);
451 `CrowdNode Stake: ${crowdNodeBalance.TotalBalance} (Đ${crowdNodeDash})`,
458 async function signup(args, state) {
459 if (state.status?.signup) {
461 `${state.pub} is already signed up. Here's the account status:`,
463 console.info(` ${state.signup} SignUpForApi`);
464 console.info(` ${state.accept} AcceptTerms`);
465 console.info(` ${state.deposit} DepositReceived`);
470 let hasEnough = state.balanceInfo.balanceSat > signupOnly + feeEstimate;
472 await collectSignupFees(state.insightBaseUrl, state.pub);
474 console.info("Requesting account...");
475 await CrowdNode.signup(state.privKey, state.hotwallet);
477 console.info(` ${state.signup} SignUpForApi`);
478 console.info(` ${state.accept} AcceptTerms`);
483 async function accept(args, state) {
484 if (state.status?.accept) {
486 `${state.pub} is already signed up. Here's the account status:`,
488 console.info(` ${state.signup} SignUpForApi`);
489 console.info(` ${state.accept} AcceptTerms`);
490 console.info(` ${state.deposit} DepositReceived`);
494 let hasEnough = state.balanceInfo.balanceSat > acceptOnly + feeEstimate;
496 await collectSignupFees(state.insightBaseUrl, state.pub);
498 console.info("Accepting terms...");
499 await CrowdNode.accept(state.privKey, state.hotwallet);
501 console.info(` ${state.signup} SignUpForApi`);
502 console.info(` ${state.accept} AcceptTerms`);
503 console.info(` ${state.deposit} DepositReceived`);
508 async function deposit(args, state) {
509 if (!state.status?.accept) {
510 console.error(`no account for address ${state.pub}`);
515 // this would allow for at least 2 withdrawals costing (21000 + 1000)
517 let reserveDash = toDash(reserve);
518 if (!state.noReserve) {
520 `reserving ${reserve} (Đ${reserveDash}) for withdrawals (--no-reserve to disable)`,
526 // TODO if unconfirmed, check utxos instead
528 // deposit what the user asks, or all that we have,
529 // or all that the user deposits - but at least 2x the reserve
530 let desiredAmountDash = parseFloat(args.shift() || 0);
531 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
532 let effectiveAmount = desiredAmountDuff;
533 if (!effectiveAmount) {
534 effectiveAmount = state.balanceInfo.balanceSat - reserve;
536 let needed = Math.max(reserve * 2, effectiveAmount + reserve);
538 if (state.balanceInfo.balanceSat < needed) {
540 if (desiredAmountDuff) {
541 ask = desiredAmountDuff + reserve + -state.balanceInfo.balanceSat;
543 await collectDeposit(state.insightBaseUrl, state.pub, ask);
544 state.balanceInfo = await state.dashApi.getInstantBalance(state.pub);
545 if (state.balanceInfo.balanceSat < needed) {
546 let balanceDash = toDash(state.balanceInfo.balanceSat);
548 `Balance is still too small: ${state.balanceInfo.balanceSat} (Đ${balanceDash})`,
554 if (!desiredAmountDuff) {
555 effectiveAmount = state.balanceInfo.balanceSat - reserve;
558 let effectiveDash = toDash(effectiveAmount);
560 `Initiating deposit of ${effectiveAmount} (Đ${effectiveDash})...`,
562 await CrowdNode.deposit(state.privKey, state.hotwallet, effectiveAmount);
564 console.info(` ${state.deposit} DepositReceived`);
569 async function withdrawal(args, state) {
570 if (!state.status?.accept) {
571 console.error(`no account for address ${state.pub}`);
576 let percentStr = args.shift() || "100.0";
577 // pass: .1 0.1, 1, 1.0, 10, 10.0, 100, 100.0
579 if (!/^1?\d?\d?(\.\d)?$/.test(percentStr)) {
580 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
583 let percent = parseFloat(percentStr);
585 let permil = Math.round(percent * 10);
586 if (permil <= 0 || permil > 1000) {
587 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
591 let realPercentStr = (permil / 10).toFixed(1);
592 console.info(`Initiating withdrawal of ${realPercentStr}...`);
594 let paid = await CrowdNode.withdrawal(state.privKey, state.hotwallet, permil);
595 //let paidFloat = (paid.satoshis / DUFFS).toFixed(8);
596 //let paidInt = paid.satoshis.toString().padStart(9, "0");
597 console.info(`API Response: ${paid.api}`);
603 async function stake(args, state) {
611 async function wifFileToAddr(keyfile) {
612 let privKey = keyfile;
614 let err = await Fs.access(keyfile).catch(Object);
616 privKey = await Fs.readFile(keyfile, "utf8");
617 privKey = privKey.trim();
620 if (34 === privKey.length) {
621 // actually payment addr
625 if (52 === privKey.length) {
626 let pk = new Dashcore.PrivateKey(privKey);
627 let pub = pk.toPublicKey().toAddress().toString();
631 throw new Error("bad file path or address");
634 async function collectSignupFees(insightBaseUrl, pub) {
638 let signupTotalDash = toDash(signupTotal);
639 let signupMsg = `Please send >= ${signupTotal} (Đ${signupTotalDash}) to Sign Up to CrowdNode`;
640 let msgPad = Math.ceil((qrWidth - signupMsg.length) / 2);
641 let subMsg = "(plus whatever you'd like to deposit)";
642 let subMsgPad = Math.ceil((qrWidth - subMsg.length) / 2);
645 console.info(" ".repeat(msgPad) + signupMsg);
646 console.info(" ".repeat(subMsgPad) + subMsg);
650 console.info("(waiting...)");
652 let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
653 console.info(`Received ${payment.satoshis}`);
656 async function collectDeposit(insightBaseUrl, pub, duffAmount) {
658 showQr(pub, duffAmount);
660 let depositMsg = `Please send what you wish to deposit to ${pub}`;
662 let depositDash = toDash(duffAmount);
663 depositMsg = `Please deposit ${duffAmount} (Đ${depositDash}) to ${pub}`;
666 let msgPad = Math.ceil((qrWidth - depositMsg.length) / 2);
667 msgPad = Math.max(0, msgPad);
670 console.info(" ".repeat(msgPad) + depositMsg);
674 console.info("(waiting...)");
676 let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
677 console.info(`Received ${payment.satoshis}`);
680 function toDash(duffs) {
681 return (duffs / DUFFS).toFixed(8);
684 function toDuff(dash) {
685 return Math.round(parseFloat(dash) * DUFFS);
690 main().catch(function (err) {
691 console.error("Fail:");
692 console.error(err.stack || err);