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 if ("http" === subcommand) {
137 if ("http" === subcommand) {
138 let keyfile = args.shift();
139 let pub = await wifFileToAddr(keyfile);
141 // ex: http <rpc>(<pub>, ...)
143 let hasRpc = rpc in CrowdNode.http;
145 console.error(`Unrecognized rpc command ${rpc}`);
150 let result = await CrowdNode.http[rpc].apply(null, args);
151 if ("string" === typeof result) {
152 console.info(result);
154 console.info(JSON.stringify(result, null, 2));
159 let keyfile = args.shift();
162 privKey = await Fs.readFile(keyfile, "utf8");
163 privKey = privKey.trim();
165 privKey = process.env.PRIVATE_KEY;
168 // TODO generate private key?
171 `Error: you must provide either the WIF key file path or PRIVATE_KEY in .env`,
178 process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
179 let insightApi = Insight.create({ baseUrl: insightBaseUrl });
180 let dashApi = Dash.create({ insightApi: insightApi });
182 let pk = new Dashcore.PrivateKey(privKey);
183 let pub = pk.toPublicKey().toAddress().toString();
185 // deposit if balance is over 100,000 (0.00100000)
186 process.stdout.write("Checking balance... ");
187 let balanceInfo = await dashApi.getInstantBalance(pub);
188 console.info(`${balanceInfo.balanceSat} (${balanceInfo.balance})`);
190 let balanceInfo = await insightApi.getBalance(pub);
191 if (balanceInfo.unconfirmedBalanceSat || balanceInfo.unconfirmedAppearances) {
194 `Error: This address has pending transactions. Please try again in 1-2 minutes or use --unconfirmed.`,
196 console.error(balanceInfo);
197 if ("status" !== subcommand) {
206 balanceInfo: balanceInfo,
208 forceConfirm: forceConfirm,
210 insightBaseUrl: insightBaseUrl,
211 insightApi: insightApi,
212 noReserve: noReserve,
227 if ("balance" === subcommand) {
228 await balance(args, state);
233 // helper for debugging
234 if ("transfer" === subcommand) {
235 await transfer(args, state);
239 process.stdout.write("Checking CrowdNode API... ");
240 await CrowdNode.init({
241 baseUrl: "https://app.crowdnode.io",
243 insightApi: insightApi,
245 state.hotwallet = CrowdNode.main.hotwallet;
246 console.info(`hotwallet is ${state.hotwallet}`);
248 state.status = await CrowdNode.status(pub, state.hotwallet);
249 if (state.status?.signup) {
252 if (state.status?.accept) {
255 if (state.status?.deposit) {
259 if ("status" === subcommand) {
260 await status(args, state);
264 if ("signup" === subcommand) {
265 await signup(args, state);
269 if ("accept" === subcommand) {
270 await accept(args, state);
274 if ("deposit" === subcommand) {
275 await deposit(args, state);
279 if ("withdrawal" === subcommand) {
280 await withdrawal(args, state);
284 console.error(`Unrecognized subcommand ${subcommand}`);
292 async function generate(name) {
293 let pk = new Dashcore.PrivateKey();
295 let pub = pk.toAddress().toString();
296 let wif = pk.toWIF();
298 let filepath = `./${pub}.wif`;
302 note = `\n(for pubkey address ${pub})`;
305 let err = await Fs.access(filepath).catch(Object);
307 // TODO show QR anyway
308 //wif = await Fs.readFile(filepath, 'utf8')
310 console.info(`'${filepath}' already exists (will not overwrite)`);
315 await Fs.writeFile(filepath, wif, "utf8").then(function () {
318 `Use the QR Code below to load a test deposit of Đ0.01 onto your staking key.`,
324 `Use the QR Code above to load a test deposit of Đ0.01 onto your staking key.`,
327 console.info(`Generated ${filepath} ${note}`);
332 async function balance(args, state) {
333 console.info(state.balanceInfo);
338 async function transfer(args, state) {
339 let newAddr = await wifFileToAddr(process.argv[4]);
340 let amount = parseInt(process.argv[5] || 0, 10);
343 tx = await state.dashApi.createPayment(state.privKey, newAddr, amount);
345 tx = await state.dashApi.createBalanceTransfer(state.privKey, newAddr);
348 console.info(`Transferring ${amount} to ${newAddr}...`);
350 console.info(`Transferring balance to ${newAddr}...`);
352 await state.insightApi.instantSend(tx);
353 console.info(`Queued...`);
354 setTimeout(function () {
355 // TODO take a cleaner approach
356 // (waitForVout needs a reasonable timeout)
357 console.error(`Error: Transfer did not complete.`);
358 if (state.forceConfirm) {
359 console.error(`(using --unconfirmed may lead to rejected double spends)`);
363 await Ws.waitForVout(state.insightBaseUrl, newAddr, 0);
364 console.info(`Accepted!`);
369 async function status(args, state) {
371 console.info(`API Actions Complete for ${state.pub}:`);
372 console.info(` ${state.signup} SignUpForApi`);
373 console.info(` ${state.accept} AcceptTerms`);
374 console.info(` ${state.deposit} DepositReceived`);
380 async function signup(args, state) {
381 if (state.status?.signup) {
383 `${state.pub} is already signed up. Here's the account status:`,
385 console.info(` ${state.signup} SignUpForApi`);
386 console.info(` ${state.accept} AcceptTerms`);
387 console.info(` ${state.deposit} DepositReceived`);
392 let hasEnough = state.balanceInfo.balanceSat > signupTotal;
394 await collectSignupFees(state.insightBaseUrl, state.pub);
396 console.info("Requesting account...");
397 await CrowdNode.signup(state.privKey, state.hotwallet);
399 console.info(` ${state.signup} SignUpForApi`);
400 console.info(` ${state.accept} AcceptTerms`);
405 async function accept(args, state) {
406 if (state.status?.accept) {
408 `${state.pub} is already signed up. Here's the account status:`,
410 console.info(` ${state.signup} SignUpForApi`);
411 console.info(` ${state.accept} AcceptTerms`);
412 console.info(` ${state.deposit} DepositReceived`);
416 let hasEnough = state.balanceInfo.balanceSat > signupTotal;
418 await collectSignupFees(state.insightBaseUrl, state.pub);
420 console.info("Accepting terms...");
421 await CrowdNode.accept(state.privKey, state.hotwallet);
423 console.info(` ${state.signup} SignUpForApi`);
424 console.info(` ${state.accept} AcceptTerms`);
425 console.info(` ${state.deposit} DepositReceived`);
430 async function deposit(args, state) {
431 if (!state.status?.accept) {
432 console.error(`no account for address ${state.pub}`);
437 // this would allow for at least 2 withdrawals costing (21000 + 1000)
439 let reserveDash = (reserve / DUFFS).toFixed(8);
440 if (!state.noReserve) {
442 `reserving ${reserve} (${reserveDash}) for withdrawals (--no-reserve to disable)`,
448 // TODO if unconfirmed, check utxos instead
450 // deposit what the user asks, or all that we have,
451 // or all that the user deposits - but at least 2x the reserve
452 let desiredAmount = parseInt(args.shift() || 0, 10);
453 let effectiveAmount = desiredAmount;
454 if (!effectiveAmount) {
455 effectiveAmount = state.balanceInfo.balanceSat - reserve;
457 let needed = Math.max(reserve * 2, effectiveAmount + reserve);
459 if (state.balanceInfo.balanceSat < needed) {
462 ask = desiredAmount + reserve + -state.balanceInfo.balanceSat;
464 await collectDeposit(state.insightBaseUrl, state.pub, ask);
465 state.balanceInfo = await state.dashApi.getInstantBalance(state.pub);
466 if (state.balanceInfo.balanceSat < needed) {
468 `Balance is still too small: ${state.balanceInfo.balanceSat}`,
474 if (!desiredAmount) {
475 effectiveAmount = state.balanceInfo.balanceSat - reserve;
478 console.info(`(holding ${reserve} in reserve for API calls)`);
479 console.info(`Initiating deposit of ${effectiveAmount}...`);
480 await CrowdNode.deposit(state.privKey, state.hotwallet, effectiveAmount);
482 console.info(` ${state.deposit} DepositReceived`);
487 async function withdrawal(args, state) {
488 if (!state.status?.accept) {
489 console.error(`no account for address ${state.pub}`);
494 let amount = parseInt(args.shift() || 1000, 10);
496 console.info("Initiating withdrawal...");
497 let paid = await CrowdNode.withdrawal(state.privKey, state.hotwallet, amount);
498 //let paidFloat = (paid.satoshis / DUFFS).toFixed(8);
499 //let paidInt = paid.satoshis.toString().padStart(9, "0");
500 console.info(`API Response: ${paid.api}`);
506 async function stake(args, state) {
514 async function wifFileToAddr(keyfile) {
515 let privKey = keyfile;
517 let err = await Fs.access(keyfile).catch(Object);
519 privKey = await Fs.readFile(keyfile, "utf8");
520 privKey = privKey.trim();
523 if (34 === privKey.length) {
524 // actually payment addr
528 if (52 === privKey.length) {
529 let pk = new Dashcore.PrivateKey(privKey);
530 let pub = pk.toPublicKey().toAddress().toString();
534 throw new Error("bad file path or address");
537 async function collectSignupFees(insightBaseUrl, pub) {
540 let signupTotalDash = (signupTotal / DUFFS).toFixed(8);
541 let signupMsg = `Please send >= ${signupTotal} (${signupTotalDash}) to Sign Up to CrowdNode`;
542 let msgPad = Math.ceil((qrWidth - signupMsg.length) / 2);
543 let subMsg = "(plus whatever you'd like to deposit)";
544 let subMsgPad = Math.ceil((qrWidth - subMsg.length) / 2);
547 console.info(" ".repeat(msgPad) + signupMsg);
548 console.info(" ".repeat(subMsgPad) + subMsg);
552 console.info("(waiting...)");
554 let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
555 console.info(`Received ${payment.satoshis}`);
558 async function collectDeposit(insightBaseUrl, pub, amount) {
561 let depositMsg = `Please send what you wish to deposit to ${pub}`;
563 let depositDash = (amount / DUFFS).toFixed(8);
564 depositMsg = `Please deposit ${amount} (${depositDash}) to ${pub}`;
567 let msgPad = Math.ceil((qrWidth - depositMsg.length) / 2);
568 msgPad = Math.max(0, msgPad);
571 console.info(" ".repeat(msgPad) + depositMsg);
575 console.info("(waiting...)");
577 let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
578 console.info(`Received ${payment.satoshis}`);
583 main().catch(function (err) {
584 console.error("Fail:");
585 console.error(err.stack || err);