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
29 CrowdNode.requests.signupForApi +
30 CrowdNode.requests.acceptTerms +
31 2 * CrowdNode.requests.offset;
32 let feeEstimate = 2 * 1000;
34 let signupTotal = signupFees + feeEstimate;
36 function showQr(signupAddr, amount = 0) {
37 let signupUri = `dash://${signupAddr}`;
39 signupUri += `?amount=${amount}`;
42 let signupQr = Qr.ascii(signupUri, { indent: 4 });
43 let addrPad = Math.ceil((qrWidth - signupUri.length) / 2);
45 console.info(signupQr);
47 console.info(" ".repeat(addrPad) + signupUri);
50 function showVersion() {
51 console.info(`${pkg.name} v${pkg.version} - ${pkg.description}`);
58 console.info("Usage:");
59 console.info(" crowdnode help");
60 console.info(" crowdnode status ./privkey.wif");
61 console.info(" crowdnode signup ./privkey.wif");
62 console.info(" crowdnode accept ./privkey.wif");
63 console.info(" crowdnode deposit ./privkey.wif [amount] [--no-reserve]");
65 " crowdnode withdrawal ./privkey.wif <permil> # 1-1000 (1.0-100.0%)",
69 console.info("Helpful Extras:");
70 console.info(" crowdnode generate [./privkey.wif]");
71 console.info(" crowdnode balance ./privkey.wif");
72 console.info(" crowdnode transfer ./source.wif <key-file-or-pub-addr>");
75 console.info("CrowdNode HTTP RPC:");
76 console.info(" crowdnode http FundsOpen <addr>");
77 console.info(" crowdnode http VotingOpen <addr>");
78 console.info(" crowdnode http GetFunds <addr>");
79 console.info(" crowdnode http GetFundsFrom <addr> <seconds-since-epoch>");
80 console.info(" crowdnode http GetBalance <addr>");
81 console.info(" crowdnode http GetMessages <addr>");
82 console.info(" crowdnode http IsAddressInUse <addr>");
83 // TODO create signature rather than requiring it
84 console.info(" crowdnode http SetEmail ./privkey.wif <email> <signature>");
85 console.info(" crowdnode http Vote ./privkey.wif <gobject-hash> ");
86 console.info(" <Yes|No|Abstain|Delegate|DoNothing> <signature>");
88 " crowdnode http SetReferral ./privkey.wif <referral-id> <signature>",
93 function removeItem(arr, item) {
94 let index = arr.indexOf(item);
96 return arr.splice(index, 1)[0];
101 async function main() {
103 // crowdnode <subcommand> [flags] <privkey> [options]
105 // crowdnode withdrawal --unconfirmed ./Xxxxpubaddr.wif 1000
107 let args = process.argv.slice(2);
110 let forceConfirm = removeItem(args, "--unconfirmed");
111 let noReserve = removeItem(args, "--no-reserve");
113 let subcommand = args.shift();
115 if (!subcommand || ["--help", "-h", "help"].includes(subcommand)) {
121 if (["--version", "-V", "version"].includes(subcommand)) {
127 if ("generate" === subcommand) {
128 await generate(args.shift());
133 process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
134 let insightApi = Insight.create({ baseUrl: insightBaseUrl });
135 let dashApi = Dash.create({ insightApi: insightApi });
137 process.stdout.write("Checking CrowdNode API... ");
138 await CrowdNode.init({
139 baseUrl: "https://app.crowdnode.io",
141 insightApi: insightApi,
143 console.info(`hotwallet is ${CrowdNode.main.hotwallet}`);
146 if ("http" === subcommand) {
148 let keyfile = args.shift();
149 let pub = await wifFileToAddr(keyfile);
151 // ex: http <rpc>(<pub>, ...)
153 let hasRpc = rpc in CrowdNode.http;
155 console.error(`Unrecognized rpc command ${rpc}`);
160 let result = await CrowdNode.http[rpc].apply(null, args);
161 if ("string" === typeof result) {
162 console.info(result);
164 console.info(JSON.stringify(result, null, 2));
169 let keyfile = args.shift();
172 privKey = await Fs.readFile(keyfile, "utf8");
173 privKey = privKey.trim();
175 privKey = process.env.PRIVATE_KEY;
178 // TODO generate private key?
181 `Error: you must provide either the WIF key file path or PRIVATE_KEY in .env`,
187 let pk = new Dashcore.PrivateKey(privKey);
188 let pub = pk.toPublicKey().toAddress().toString();
190 // deposit if balance is over 100,000 (0.00100000)
191 process.stdout.write("Checking balance... ");
192 let balanceInfo = await dashApi.getInstantBalance(pub);
193 console.info(`${balanceInfo.balanceSat} (${balanceInfo.balance})`);
195 let balanceInfo = await insightApi.getBalance(pub);
196 if (balanceInfo.unconfirmedBalanceSat || balanceInfo.unconfirmedAppearances) {
199 `Error: This address has pending transactions. Please try again in 1-2 minutes or use --unconfirmed.`,
201 console.error(balanceInfo);
202 if ("status" !== subcommand) {
211 balanceInfo: balanceInfo,
213 forceConfirm: forceConfirm,
214 hotwallet: CrowdNode.main.hotwallet,
215 insightBaseUrl: insightBaseUrl,
216 insightApi: insightApi,
217 noReserve: noReserve,
232 if ("balance" === subcommand) {
233 await balance(args, state);
238 // helper for debugging
239 if ("transfer" === subcommand) {
240 await transfer(args, state);
244 state.status = await CrowdNode.status(pub, state.hotwallet);
245 if (state.status?.signup) {
248 if (state.status?.accept) {
251 if (state.status?.deposit) {
255 if ("status" === subcommand) {
256 await status(args, state);
260 if ("signup" === subcommand) {
261 await signup(args, state);
265 if ("accept" === subcommand) {
266 await accept(args, state);
270 if ("deposit" === subcommand) {
271 await deposit(args, state);
275 if ("withdrawal" === subcommand) {
276 await withdrawal(args, state);
280 console.error(`Unrecognized subcommand ${subcommand}`);
288 async function generate(name) {
289 let pk = new Dashcore.PrivateKey();
291 let pub = pk.toAddress().toString();
292 let wif = pk.toWIF();
294 let filepath = `./${pub}.wif`;
298 note = `\n(for pubkey address ${pub})`;
301 let err = await Fs.access(filepath).catch(Object);
303 // TODO show QR anyway
304 //wif = await Fs.readFile(filepath, 'utf8')
306 console.info(`'${filepath}' already exists (will not overwrite)`);
311 await Fs.writeFile(filepath, wif, "utf8").then(function () {
314 `Use the QR Code below to load a test deposit of Đ0.01 onto your staking key.`,
320 `Use the QR Code above to load a test deposit of Đ0.01 onto your staking key.`,
323 console.info(`Generated ${filepath} ${note}`);
328 async function balance(args, state) {
329 console.info(state.balanceInfo);
334 async function transfer(args, state) {
335 let newAddr = await wifFileToAddr(process.argv[4]);
336 let amount = parseInt(process.argv[5] || 0, 10);
339 tx = await state.dashApi.createPayment(state.privKey, newAddr, amount);
341 tx = await state.dashApi.createBalanceTransfer(state.privKey, newAddr);
344 console.info(`Transferring ${amount} to ${newAddr}...`);
346 console.info(`Transferring balance to ${newAddr}...`);
348 await state.insightApi.instantSend(tx);
349 console.info(`Queued...`);
350 setTimeout(function () {
351 // TODO take a cleaner approach
352 // (waitForVout needs a reasonable timeout)
353 console.error(`Error: Transfer did not complete.`);
354 if (state.forceConfirm) {
355 console.error(`(using --unconfirmed may lead to rejected double spends)`);
359 await Ws.waitForVout(state.insightBaseUrl, newAddr, 0);
360 console.info(`Accepted!`);
365 async function status(args, state) {
367 console.info(`API Actions Complete for ${state.pub}:`);
368 console.info(` ${state.signup} SignUpForApi`);
369 console.info(` ${state.accept} AcceptTerms`);
370 console.info(` ${state.deposit} DepositReceived`);
376 async function signup(args, state) {
377 if (state.status?.signup) {
379 `${state.pub} is already signed up. Here's the account status:`,
381 console.info(` ${state.signup} SignUpForApi`);
382 console.info(` ${state.accept} AcceptTerms`);
383 console.info(` ${state.deposit} DepositReceived`);
388 let hasEnough = state.balanceInfo.balanceSat > signupTotal;
390 await collectSignupFees(state.insightBaseUrl, state.pub);
392 console.info("Requesting account...");
393 await CrowdNode.signup(state.privKey, state.hotwallet);
395 console.info(` ${state.signup} SignUpForApi`);
396 console.info(` ${state.accept} AcceptTerms`);
401 async function accept(args, state) {
402 if (state.status?.accept) {
404 `${state.pub} is already signed up. Here's the account status:`,
406 console.info(` ${state.signup} SignUpForApi`);
407 console.info(` ${state.accept} AcceptTerms`);
408 console.info(` ${state.deposit} DepositReceived`);
412 let hasEnough = state.balanceInfo.balanceSat > signupTotal;
414 await collectSignupFees(state.insightBaseUrl, state.pub);
416 console.info("Accepting terms...");
417 await CrowdNode.accept(state.privKey, state.hotwallet);
419 console.info(` ${state.signup} SignUpForApi`);
420 console.info(` ${state.accept} AcceptTerms`);
421 console.info(` ${state.deposit} DepositReceived`);
426 async function deposit(args, state) {
427 if (!state.status?.accept) {
428 console.error(`no account for address ${state.pub}`);
433 // this would allow for at least 2 withdrawals costing (21000 + 1000)
435 let reserveDash = (reserve / DUFFS).toFixed(8);
436 if (!state.noReserve) {
438 `reserving ${reserve} (${reserveDash}) for withdrawals (--no-reserve to disable)`,
444 // TODO if unconfirmed, check utxos instead
446 // deposit what the user asks, or all that we have,
447 // or all that the user deposits - but at least 2x the reserve
448 let desiredAmount = parseInt(args.shift() || 0, 10);
449 let effectiveAmount = desiredAmount;
450 if (!effectiveAmount) {
451 effectiveAmount = state.balanceInfo.balanceSat - reserve;
453 let needed = Math.max(reserve * 2, effectiveAmount + reserve);
455 if (state.balanceInfo.balanceSat < needed) {
458 ask = desiredAmount + reserve + -state.balanceInfo.balanceSat;
460 await collectDeposit(state.insightBaseUrl, state.pub, ask);
461 state.balanceInfo = await state.dashApi.getInstantBalance(state.pub);
462 if (state.balanceInfo.balanceSat < needed) {
464 `Balance is still too small: ${state.balanceInfo.balanceSat}`,
470 if (!desiredAmount) {
471 effectiveAmount = state.balanceInfo.balanceSat - reserve;
474 console.info(`(holding ${reserve} in reserve for API calls)`);
475 console.info(`Initiating deposit of ${effectiveAmount}...`);
476 await CrowdNode.deposit(state.privKey, state.hotwallet, effectiveAmount);
478 console.info(` ${state.deposit} DepositReceived`);
483 async function withdrawal(args, state) {
484 if (!state.status?.accept) {
485 console.error(`no account for address ${state.pub}`);
490 let amount = parseInt(args.shift() || 1000, 10);
492 console.info("Initiating withdrawal...");
493 let paid = await CrowdNode.withdrawal(state.privKey, state.hotwallet, amount);
494 //let paidFloat = (paid.satoshis / DUFFS).toFixed(8);
495 //let paidInt = paid.satoshis.toString().padStart(9, "0");
496 console.info(`API Response: ${paid.api}`);
502 async function stake(args, state) {
510 async function wifFileToAddr(keyfile) {
511 let privKey = keyfile;
513 let err = await Fs.access(keyfile).catch(Object);
515 privKey = await Fs.readFile(keyfile, "utf8");
516 privKey = privKey.trim();
519 if (34 === privKey.length) {
520 // actually payment addr
524 if (52 === privKey.length) {
525 let pk = new Dashcore.PrivateKey(privKey);
526 let pub = pk.toPublicKey().toAddress().toString();
530 throw new Error("bad file path or address");
533 async function collectSignupFees(insightBaseUrl, pub) {
536 let signupTotalDash = (signupTotal / DUFFS).toFixed(8);
537 let signupMsg = `Please send >= ${signupTotal} (${signupTotalDash}) to Sign Up to CrowdNode`;
538 let msgPad = Math.ceil((qrWidth - signupMsg.length) / 2);
539 let subMsg = "(plus whatever you'd like to deposit)";
540 let subMsgPad = Math.ceil((qrWidth - subMsg.length) / 2);
543 console.info(" ".repeat(msgPad) + signupMsg);
544 console.info(" ".repeat(subMsgPad) + subMsg);
548 console.info("(waiting...)");
550 let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
551 console.info(`Received ${payment.satoshis}`);
554 async function collectDeposit(insightBaseUrl, pub, amount) {
557 let depositMsg = `Please send what you wish to deposit to ${pub}`;
559 let depositDash = (amount / DUFFS).toFixed(8);
560 depositMsg = `Please deposit ${amount} (${depositDash}) to ${pub}`;
563 let msgPad = Math.ceil((qrWidth - depositMsg.length) / 2);
564 msgPad = Math.max(0, msgPad);
567 console.info(" ".repeat(msgPad) + depositMsg);
571 console.info("(waiting...)");
573 let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
574 console.info(`Received ${payment.satoshis}`);
579 main().catch(function (err) {
580 console.error("Fail:");
581 console.error(err.stack || err);