3 /*jshint maxcomplexity:25 */
5 require("dotenv").config({ path: ".env" });
6 require("dotenv").config({ path: ".env.secret" });
8 let HOME = process.env.HOME || "";
11 let pkg = require("../package.json");
13 let Fs = require("fs").promises;
14 let Path = require("path");
16 let Cipher = require("./_cipher.js");
17 let CrowdNode = require("../lib/crowdnode.js");
18 let Dash = require("../lib/dash.js");
19 let Insight = require("../lib/insight.js");
20 let Prompt = require("./_prompt.js");
21 let Qr = require("../lib/qr.js");
22 let Ws = require("../lib/ws.js");
24 let Dashcore = require("@dashevo/dashcore-lib");
28 const NO_SHADOW = "NONE";
29 const DUFFS = 100000000;
31 let shownDefault = false;
32 let qrWidth = 2 + 33 + 2;
34 // 0.00236608 // required for signup
35 // 0.00002000 // TX fee estimate
36 // 0.00238608 // minimum recommended amount
39 let signupOnly = CrowdNode.requests.signupForApi + CrowdNode.requests.offset;
40 let acceptOnly = CrowdNode.requests.acceptTerms + CrowdNode.requests.offset;
41 let signupFees = signupOnly + acceptOnly;
42 let feeEstimate = 500;
43 let signupTotal = signupFees + 2 * feeEstimate;
46 let configdir = `.config/crowdnode`;
47 let keysDir = Path.join(HOME, `${configdir}/keys`);
48 let keysDirRel = `~/${configdir}/keys`;
49 let shadowPath = Path.join(HOME, `${configdir}/shadow`);
50 let defaultWifPath = Path.join(HOME, `${configdir}/default`);
54 console.error.apply(console, arguments);
57 function showVersion() {
58 console.info(`${pkg.name} v${pkg.version} - ${pkg.description}`);
65 console.info("Quick Start:");
66 // technically this also has [--no-reserve]
67 console.info(" crowdnode stake [addr-or-import-key | --create-new]");
70 console.info("Usage:");
71 console.info(" crowdnode help");
72 console.info(" crowdnode status [keyfile-or-addr]");
73 console.info(" crowdnode signup [keyfile-or-addr]");
74 console.info(" crowdnode accept [keyfile-or-addr]");
76 " crowdnode deposit [keyfile-or-addr] [dash-amount] [--no-reserve]",
79 " crowdnode withdrawal [keyfile-or-addr] <percent> # 1.0-100.0 (steps by 0.1)",
83 console.info("Helpful Extras:");
84 console.info(" crowdnode balance [keyfile-or-addr]"); // addr
85 console.info(" crowdnode load [keyfile-or-addr] [dash-amount]"); // addr
87 " crowdnode transfer <from-keyfile-or-addr> <to-keyfile-or-addr> [dash-amount]",
91 console.info("Key Management & Encryption:");
92 console.info(" crowdnode init");
93 console.info(" crowdnode generate [--plain-text] [./privkey.wif]");
94 console.info(" crowdnode encrypt"); // TODO allow encrypting one-by-one?
95 console.info(" crowdnode list");
96 console.info(" crowdnode use <addr>");
97 console.info(" crowdnode import <keyfile>");
98 //console.info(" crowdnode import <(dash-cli dumpprivkey <addr>)"); // TODO
99 //console.info(" crowdnode export <addr> <keyfile>"); // TODO
100 console.info(" crowdnode passphrase # set or change passphrase");
101 console.info(" crowdnode decrypt"); // TODO allow decrypting one-by-one?
102 console.info(" crowdnode delete <addr>");
105 console.info("CrowdNode HTTP RPC:");
106 console.info(" crowdnode http FundsOpen <addr>");
107 console.info(" crowdnode http VotingOpen <addr>");
108 console.info(" crowdnode http GetFunds <addr>");
109 console.info(" crowdnode http GetFundsFrom <addr> <seconds-since-epoch>");
110 console.info(" crowdnode http GetBalance <addr>");
111 console.info(" crowdnode http GetMessages <addr>");
112 console.info(" crowdnode http IsAddressInUse <addr>");
113 // TODO create signature rather than requiring it
114 console.info(" crowdnode http SetEmail ./privkey.wif <email> <signature>");
115 console.info(" crowdnode http Vote ./privkey.wif <gobject-hash> ");
116 console.info(" <Yes|No|Abstain|Delegate|DoNothing> <signature>");
118 " crowdnode http SetReferral ./privkey.wif <referral-id> <signature>",
121 console.info("Official CrowdNode Resources");
123 console.info("Homepage:");
124 console.info(" https://crowdnode.io/");
126 console.info("Terms of Service:");
127 console.info(" https://crowdnode.io/terms/");
129 console.info("BlockChain API Guide:");
131 " https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide",
138 async function main() {
139 /*jshint maxcomplexity:40 */
140 /*jshint maxstatements:500 */
143 // crowdnode <subcommand> [flags] <privkey> [options]
145 // crowdnode withdrawal ./Xxxxpubaddr.wif 100.0
147 let args = process.argv.slice(2);
150 let forceGenerate = removeItem(args, "--create-new");
151 let forceConfirm = removeItem(args, "--unconfirmed");
152 let plainText = removeItem(args, "--plain-text");
153 let noReserve = removeItem(args, "--no-reserve");
155 let subcommand = args.shift();
157 if (!subcommand || ["--help", "-h", "help"].includes(subcommand)) {
163 if (["--version", "-V", "version"].includes(subcommand)) {
171 // find addr by name or by file or by string
172 await Fs.mkdir(keysDir, {
176 let defaultAddr = await Fs.readFile(defaultWifPath, "utf8").catch(
177 emptyStringOnErrEnoent,
179 defaultAddr = defaultAddr.trim();
182 process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
183 let insightApi = Insight.create({ baseUrl: insightBaseUrl });
184 let dashApi = Dash.create({ insightApi: insightApi });
186 if ("stake" === subcommand) {
202 if ("list" === subcommand) {
203 await listKeys({ dashApi, defaultAddr }, args);
208 if ("init" === subcommand) {
209 await initKeystore({ defaultAddr });
214 if ("generate" === subcommand) {
215 await generateKey({ defaultKey: defaultAddr, plainText }, args);
220 if ("passphrase" === subcommand) {
221 await setPassphrase({}, args);
226 if ("import" === subcommand) {
227 let keypath = args.shift() || "";
228 await importKey({ keypath });
233 if ("encrypt" === subcommand) {
234 let addr = args.shift() || "";
236 await encryptAll(null);
241 let keypath = await findWif(addr);
243 console.error(`no managed key matches '${addr}'`);
247 let key = await maybeReadKeyFileRaw(keypath);
249 throw new Error("impossible error");
251 await encryptAll([key]);
256 if ("decrypt" === subcommand) {
257 let addr = args.shift() || "";
259 await decryptAll(null);
260 await Fs.writeFile(shadowPath, NO_SHADOW, "utf8").catch(
261 emptyStringOnErrEnoent,
266 let keypath = await findWif(addr);
268 console.error(`no managed key matches '${addr}'`);
272 let key = await maybeReadKeyFileRaw(keypath);
274 throw new Error("impossible error");
276 await decryptAll([key]);
281 // use or select or default... ?
282 if ("use" === subcommand) {
283 await setDefault(null, args);
288 // helper for debugging
289 if ("transfer" === subcommand) {
290 await transferBalance(
291 { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
299 if ("http" === subcommand) {
300 rpc = args.shift() || "";
307 let [addr] = await mustGetAddr({ defaultAddr }, args);
309 await initCrowdNode(insightBaseUrl);
310 // ex: http <rpc>(<pub>, ...)
312 let hasRpc = rpc in CrowdNode.http;
314 console.error(`Unrecognized rpc command ${rpc}`);
319 //@ts-ignore - TODO use `switch` or make Record Type
320 let result = await CrowdNode.http[rpc].apply(null, args);
322 console.info(`${rpc} ${addr}:`);
323 if ("string" === typeof result) {
324 console.info(result);
326 console.info(JSON.stringify(result, null, 2));
332 if ("load" === subcommand) {
333 await loadAddr({ defaultAddr, insightBaseUrl }, args);
338 // keeping rm for backwards compat
339 if ("rm" === subcommand || "delete" === subcommand) {
340 await initCrowdNode(insightBaseUrl);
341 let [addr, filepath] = await mustGetAddr({ defaultAddr }, args);
342 await removeKey({ addr, dashApi, filepath, insightBaseUrl }, args);
347 if ("balance" === subcommand) {
349 await getBalance({ dashApi, defaultAddr }, args);
354 await getAllBalances({ dashApi, defaultAddr }, args);
359 if ("status" === subcommand) {
360 await getStatus({ dashApi, defaultAddr, insightBaseUrl }, args);
365 if ("signup" === subcommand) {
366 await sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args);
371 if ("accept" === subcommand) {
372 await acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args);
377 if ("deposit" === subcommand) {
379 { dashApi, defaultAddr, insightBaseUrl, noReserve },
386 if ("withdrawal" === subcommand) {
387 await withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args);
392 console.error(`Unrecognized subcommand ${subcommand}`);
399 * @param {Object} opts
400 * @param {any} opts.dashApi - TODO
401 * @param {String} opts.defaultAddr
402 * @param {Boolean} opts.forceGenerate
403 * @param {String} opts.insightBaseUrl
404 * @param {any} opts.insightApi
405 * @param {Boolean} opts.noReserve
406 * @param {Array<String>} args
408 async function stakeDash(
419 let err = await Fs.access(args[0]).catch(Object);
422 let keypath = args.shift() || "";
423 addr = await importKey({ keypath });
424 } else if (forceGenerate) {
425 addr = await generateKey({ defaultKey: defaultAddr }, []);
427 addr = await initKeystore({ defaultAddr });
431 let [_addr] = await mustGetAddr({ defaultAddr }, args);
435 let extra = feeEstimate;
436 console.info("Checking CrowdNode account... ");
437 await CrowdNode.init({
438 baseUrl: "https://app.crowdnode.io",
441 let hotwallet = CrowdNode.main.hotwallet;
442 let state = await getCrowdNodeStatus({ addr, hotwallet });
444 if (!state.status?.accept) {
445 if (!state.status?.signup) {
446 let signUpDeposit = signupOnly + feeEstimate;
448 ` ${TODO} SignUpForApi deposit is ${signupOnly} (+ tx fee)`,
450 extra += signUpDeposit;
452 console.info(` ${DONE} SignUpForApi complete`);
454 let acceptDeposit = acceptOnly + feeEstimate;
455 console.info(` ${TODO} AcceptTerms deposit is ${acceptOnly} (+ tx fee)`);
456 extra += acceptDeposit;
459 let desiredAmountDash = args.shift() || "0.5";
460 let effectiveDuff = toDuff(desiredAmountDash);
461 effectiveDuff += extra;
463 let balanceInfo = await dashApi.getInstantBalance(addr);
464 effectiveDuff -= balanceInfo.balanceSat;
466 if (effectiveDuff > 0) {
467 effectiveDuff = roundDuff(effectiveDuff, 3);
468 let effectiveDash = toDash(effectiveDuff);
469 await plainLoadAddr({
477 if (!state.status?.accept) {
478 if (!state.status?.signup) {
479 await sendSignup({ dashApi, defaultAddr: addr, insightBaseUrl }, [addr]);
481 await acceptTerms({ dashApi, defaultAddr: addr, insightBaseUrl }, [addr]);
485 { dashApi, defaultAddr: addr, insightBaseUrl, noReserve },
491 * @param {Object} opts
492 * @param {String} opts.defaultAddr
494 async function initKeystore({ defaultAddr }) {
495 // if we have no keys, make one
496 let wifnames = await listManagedKeynames();
497 if (!wifnames.length) {
498 return await generateKey({ defaultKey: defaultAddr }, []);
500 // if we have no passphrase, ask about it
501 await initPassphrase();
502 return defaultAddr || wifnames[0];
506 * @param {String} insightBaseUrl
508 async function initCrowdNode(insightBaseUrl) {
509 if (CrowdNode.main.hotwallet) {
512 process.stdout.write("Checking CrowdNode API... ");
513 await CrowdNode.init({
514 baseUrl: "https://app.crowdnode.io",
517 console.info(`(hotwallet ${CrowdNode.main.hotwallet})`);
521 * @param {String} addr - Base58Check pubKeyHash address
522 * @param {Number} duffs - 1/100000000 of a DASH
524 function showQr(addr, duffs = 0) {
525 let dashAmount = toDash(duffs);
526 let dashUri = `dash://${addr}`;
528 dashUri += `?amount=${dashAmount}`;
531 let dashQr = Qr.ascii(dashUri, { indent: 4, size: "mini" });
532 let addrPad = Math.max(0, Math.ceil((qrWidth - dashUri.length) / 2));
534 console.info(dashQr);
536 console.info(" ".repeat(addrPad) + dashUri);
540 * @param {Array<any>} arr
543 function removeItem(arr, item) {
544 let index = arr.indexOf(item);
546 return arr.splice(index, 1)[0];
552 * @param {Object} opts
553 * @param {String} opts.addr
554 * @param {String} opts.hotwallet
556 async function getCrowdNodeStatus({ addr, hotwallet }) {
568 //@ts-ignore - TODO why warnings?
569 let status = await CrowdNode.status(addr, hotwallet);
571 state.status = status;
573 if (state.status?.signup) {
576 if (state.status?.accept) {
579 if (state.status?.deposit) {
580 state.deposit = DONE;
586 * @param {Object} opts
587 * @param {String} opts.addr
588 * @param {any} opts.dashApi - TODO
590 async function checkBalance({ addr, dashApi }) {
591 // deposit if balance is over 100,000 (0.00100000)
592 console.info("Checking balance... ");
593 let balanceInfo = await dashApi.getInstantBalance(addr);
594 let balanceDASH = toDASH(balanceInfo.balanceSat);
596 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
597 if (!crowdNodeBalance.TotalBalance) {
598 crowdNodeBalance.TotalBalance = 0;
599 crowdNodeBalance.TotalDividend = 0;
602 let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance);
603 let crowdNodeDASH = toDASH(crowdNodeDuffNum);
605 let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend);
606 let crowdNodeDASHDiv = toDASH(crowdNodeDivNum);
608 console.info(`Key: ${balanceDASH}`);
609 console.info(`CrowdNode: ${crowdNodeDASH}`);
610 console.info(`Dividends: ${crowdNodeDASHDiv}`);
613 let balanceInfo = await insightApi.getBalance(pub);
614 if (balanceInfo.unconfirmedBalanceSat || balanceInfo.unconfirmedAppearances) {
617 `Error: This address has pending transactions. Please try again in 1-2 minutes or use --unconfirmed.`,
619 console.error(balanceInfo);
620 if ("status" !== subcommand) {
631 * @param {Object} opts
632 * @param {String} opts.defaultAddr
633 * @param {Array<String>} args
634 * @returns {Promise<[String, String]>}
636 async function mustGetAddr({ defaultAddr }, args) {
637 let name = args.shift() ?? "";
638 if (34 === name.length) {
639 // looks like addr already
640 // TODO make function for addr-lookin' check
644 let addr = await maybeReadKeyPaths(name, { wif: false });
646 if (34 === addr.length) {
649 //let pk = new Dashcore.PrivateKey(wif);
650 //let addr = pk.toAddress().toString();
654 let isNum = !isNaN(parseFloat(name));
662 console.error(`could not read '${name}' in ./ or match in ${keysDirRel}/.`);
668 addr = await mustGetDefaultWif(defaultAddr, { wif: false });
670 // TODO we don't need defaultAddr, right? because it could be old?
675 * @param {Object} opts
676 * @param {String} opts.defaultAddr
677 * @param {Array<String>} args
679 async function mustGetWif({ defaultAddr }, args) {
680 let name = args.shift() ?? "";
682 let wif = await maybeReadKeyPaths(name, { wif: true });
687 let isNum = !isNaN(parseFloat(name));
696 `'${name}' does not match a staking key in ./ or ${keysDirRel}/`,
703 wif = await mustGetDefaultWif(defaultAddr);
709 * @param {String} name
710 * @param {Object} opts
711 * @param {Boolean} opts.wif
712 * @returns {Promise<String>} - wif
714 async function maybeReadKeyPaths(name, opts) {
717 // prefix match in .../keys/
718 let wifname = await findWif(name);
723 if (false === opts.wif) {
724 return wifname.slice(0, -".wif".length);
727 let filepath = Path.join(keysDir, wifname);
728 privKey = await maybeReadKeyFile(filepath);
731 privKey = await maybeReadKeyFile(name);
738 * @param {String} defaultAddr
739 * @param {Object} [opts]
740 * @param {Boolean} opts.wif
742 async function mustGetDefaultWif(defaultAddr, opts) {
745 let keyfile = Path.join(keysDir, `${defaultAddr}.wif`);
746 let raw = await maybeReadKeyFileRaw(keyfile, opts);
747 // misnomering wif here a bit
748 defaultWif = raw?.wif || raw?.addr || "";
750 if (defaultWif && !shownDefault) {
752 debug(`Selected default staking key ${defaultAddr}`);
757 console.error(`Error: no default staking key selected.`);
759 console.error(`Select a different address:`);
760 console.error(` crowdnode list`);
761 console.error(` crowdnode use <addr>`);
763 console.error(`Or create a new staking key:`);
764 console.error(` crowdnode generate`);
773 * @param {Object} psuedoState
774 * @param {String} psuedoState.defaultKey - addr name of default key
775 * @param {Boolean} [psuedoState.plainText] - don't encrypt
776 * @param {Array<String>} args
778 async function generateKey({ defaultKey, plainText }, args) {
779 let name = args.shift();
780 //@ts-ignore - TODO submit JSDoc PR for Dashcore
781 let pk = new Dashcore.PrivateKey();
783 let addr = pk.toAddress().toString();
784 let plainWif = pk.toWIF();
788 wif = await maybeEncrypt(plainWif);
791 let filename = `~/${configdir}/keys/${addr}.wif`;
792 let filepath = Path.join(`${keysDir}/${addr}.wif`);
797 note = `\n(for pubkey address ${addr})`;
798 let err = await Fs.access(filepath).catch(Object);
801 console.info(`'${filepath}' already exists (will not overwrite)`);
807 await Fs.writeFile(filepath, wif, "utf8");
808 if (!name && !defaultKey) {
809 await Fs.writeFile(defaultWifPath, addr, "utf8");
813 console.info(`Generated ${filename} ${note}`);
818 async function initPassphrase() {
819 let needsInit = false;
820 let shadow = await Fs.readFile(shadowPath, "utf8").catch(
821 emptyStringOnErrEnoent,
827 await cmds.getPassphrase({}, []);
832 * @param {Object} state
833 * @param {Boolean} [state._askPreviousPassphrase] - don't ask for passphrase again
834 * @param {Array<String>} args
836 async function setPassphrase({ _askPreviousPassphrase }, args) {
841 let date = getFsDateString();
843 // get the old passphrase
844 if (false !== _askPreviousPassphrase) {
845 // TODO should contain the shadow?
846 await cmds.getPassphrase({ _rotatePassphrase: true }, []);
849 // get the new passphrase
850 let newPassphrase = await promptPassphrase();
851 let curShadow = await Fs.readFile(shadowPath, "utf8").catch(
852 emptyStringOnErrEnoent,
855 let newShadow = await Cipher.shadowPassphrase(newPassphrase);
856 await Fs.writeFile(shadowPath, newShadow, "utf8");
858 let rawKeys = await readAllKeys();
859 let encAddrs = rawKeys
860 .map(function (raw) {
867 // backup all currently encrypted files
869 if (encAddrs.length) {
870 let filepath = Path.join(HOME, `${configdir}/keys.${date}.bak`);
872 console.info(`Backing up previous (encrypted) keys:`);
873 encAddrs.unshift(`SHADOW:${curShadow}`);
874 await Fs.writeFile(filepath, encAddrs.join("\n") + "\n", "utf8");
875 console.info(` ~/${configdir}/keys.${date}.bak`);
878 cmds._setPassphrase(newPassphrase);
880 await encryptAll(rawKeys, { rotateKey: true });
882 result.passphrase = newPassphrase;
883 result.changed = true;
887 async function promptPassphrase() {
890 newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", {
893 newPassphrase = newPassphrase.trim();
895 let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", {
898 _newPassphrase = _newPassphrase.trim();
900 let match = Cipher.secureCompare(newPassphrase, _newPassphrase);
905 console.error("passphrases do not match");
907 return newPassphrase;
912 * @param {Object} opts
913 * @param {String} opts.keypath
915 async function importKey({ keypath }) {
916 let key = await maybeReadKeyFileRaw(keypath);
918 console.error(`no key found for '${keypath}'`);
923 let encWif = await maybeEncrypt(key.wif);
925 if (encWif.includes(":")) {
928 let date = getFsDateString();
931 Path.join(keysDir, `${key.addr}.wif`),
933 Path.join(keysDir, `${key.addr}.${date}.bak`),
936 console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`);
943 * @param {Object} opts
944 * @param {Boolean} [opts._rotatePassphrase]
945 * @param {Boolean} [opts._force]
946 * @param {Array<String>} args
948 cmds.getPassphrase = async function ({ _rotatePassphrase, _force }, args) {
954 if (!_rotatePassphrase) {
955 let cachedphrase = cmds._getPassphrase();
962 // Three possible states:
963 // 1. no shadow file yet (ask to set one)
964 // 2. empty shadow file (initialized, but not set - don't ask to set one)
965 // 3. encrypted shadow file (initialized, requires passphrase)
966 let needsInit = false;
967 let shadow = await Fs.readFile(shadowPath, "utf8").catch(
968 emptyStringOnErrEnoent,
972 } else if (NO_SHADOW === shadow && _force) {
976 // State 1: not initialized, what does the user want?
981 no = await Prompt.prompt(
982 "Would you like to set an encryption passphrase? [Y/n]: ",
986 // Set a passphrase and create shadow file
987 if (!no || ["yes", "y"].includes(no.toLowerCase())) {
988 result = await setPassphrase({ _askPreviousPassphrase: false }, args);
989 cmds._setPassphrase(result.passphrase);
994 if (!["no", "n"].includes(no.toLowerCase())) {
998 // No passphrase, create a NONE shadow file
999 await Fs.writeFile(shadowPath, NO_SHADOW, "utf8");
1004 // State 2: shadow already initialized to empty
1005 // (user doesn't want a passphrase)
1007 cmds._setPassphrase("");
1011 // State 3: passphrase & shadow already in use
1013 let prompt = `Enter passphrase: `;
1014 if (_rotatePassphrase) {
1015 prompt = `Enter (current) passphrase: `;
1017 result.passphrase = await Prompt.prompt(prompt, {
1020 result.passphrase = result.passphrase.trim();
1021 if (!result.passphrase || "q" === result.passphrase) {
1022 console.error("cancel: no passphrase");
1027 let match = await Cipher.checkPassphrase(result.passphrase, shadow);
1029 cmds._setPassphrase(result.passphrase);
1034 console.error("incorrect passphrase");
1037 throw new Error("SANITY FAIL: unreachable return");
1040 cmds._getPassphrase = function () {
1045 * @param {String} passphrase
1047 cmds._setPassphrase = function (passphrase) {
1048 // Look Ma! A private variable!
1049 cmds._getPassphrase = function () {
1055 * Encrypt ALL-the-things!
1056 * @param {Object} [opts]
1057 * @param {Boolean} opts.rotateKey
1058 * @param {Array<RawKey>?} rawKeys
1060 async function encryptAll(rawKeys, opts) {
1062 rawKeys = await readAllKeys();
1064 let date = getFsDateString();
1066 let passphrase = cmds._getPassphrase();
1068 let result = await cmds.getPassphrase({ _force: true }, []);
1069 if (result.changed) {
1070 // encryptAll was already called on rotation
1073 passphrase = result.passphrase;
1076 console.info(`Encrypting...`);
1078 await rawKeys.reduce(async function (promise, key) {
1081 if (key.encrypted && !opts?.rotateKey) {
1082 console.info(`🙈 ${key.addr} [already encrypted]`);
1085 let encWif = await maybeEncrypt(key.wif, { force: true });
1087 Path.join(keysDir, `${key.addr}.wif`),
1089 Path.join(keysDir, `${key.addr}.${date}.bak`),
1091 console.info(`🔑 ${key.addr}`);
1092 }, Promise.resolve());
1094 console.info(`Done 🔐`);
1099 * Decrypt ALL-the-things!
1100 * @param {Array<RawKey>?} rawKeys
1102 async function decryptAll(rawKeys) {
1104 rawKeys = await readAllKeys();
1106 let date = getFsDateString();
1109 console.info(`Decrypting...`);
1111 await rawKeys.reduce(async function (promise, key) {
1114 if (!key.encrypted) {
1115 console.info(`📖 ${key.addr} [already decrypted]`);
1119 Path.join(keysDir, `${key.addr}.wif`),
1121 Path.join(keysDir, `${key.addr}.${date}.bak`),
1123 console.info(`🔓 ${key.addr}`);
1124 }, Promise.resolve());
1126 console.info(`Done ${DONE}`);
1130 function getFsDateString() {
1131 // YYYY-MM-DD_hh-mm_ss
1132 let date = new Date()
1136 .replace(/\.\d{3}.*/, "");
1141 * @param {String} filepath
1142 * @param {String} wif
1143 * @param {String} bakpath
1145 async function safeSave(filepath, wif, bakpath) {
1146 let tmpPath = `${bakpath}.tmp`;
1147 await Fs.writeFile(tmpPath, wif, "utf8");
1148 let err = await Fs.access(filepath).catch(Object);
1150 await Fs.rename(filepath, bakpath);
1152 await Fs.rename(tmpPath, filepath);
1154 await Fs.unlink(bakpath);
1159 * @typedef {Object} RawKey
1160 * @property {String} addr
1161 * @property {Boolean} encrypted
1162 * @property {String} wif
1168 async function readAllKeys() {
1169 let wifnames = await listManagedKeynames();
1171 /** @type Array<RawKey> */
1173 await wifnames.reduce(async function (promise, wifname) {
1176 let keypath = Path.join(keysDir, wifname);
1177 let key = await maybeReadKeyFileRaw(keypath);
1182 if (`${key.addr}.wif` !== wifname) {
1184 `computed pubkey '${key.addr}' of WIF does not match filename '${keypath}'`,
1189 }, Promise.resolve());
1195 * @param {String} filepath
1196 * @param {Object} [opts]
1197 * @param {Boolean} opts.wif
1198 * @returns {Promise<String>}
1200 async function maybeReadKeyFile(filepath, opts) {
1201 let key = await maybeReadKeyFileRaw(filepath, opts);
1202 if (false === opts?.wif) {
1203 return key?.addr || "";
1205 return key?.wif || "";
1209 * @param {String} filepath
1210 * @param {Object} [opts]
1211 * @param {Boolean} opts.wif
1212 * @returns {Promise<RawKey?>}
1214 async function maybeReadKeyFileRaw(filepath, opts) {
1215 let privKey = await Fs.readFile(filepath, "utf8").catch(
1216 emptyStringOnErrEnoent,
1218 privKey = privKey.trim();
1223 let encrypted = false;
1224 if (privKey.includes(":")) {
1227 if (false !== opts?.wif) {
1228 privKey = await decrypt(privKey);
1232 console.error(err.message);
1233 console.error(`passphrase does not match for key ${filepath}`);
1237 if (false === opts?.wif) {
1239 addr: Path.basename(filepath, ".wif"),
1240 encrypted: encrypted,
1245 let pk = new Dashcore.PrivateKey(privKey);
1246 let pub = pk.toAddress().toString();
1250 encrypted: encrypted,
1256 * @param {String} encWif
1258 async function decrypt(encWif) {
1259 let passphrase = cmds._getPassphrase();
1261 let result = await cmds.getPassphrase({}, []);
1262 passphrase = result.passphrase;
1263 // we don't return just in case they're setting a passphrase to
1264 // decrypt a previously encrypted file (i.e. for recovery from elsewhere)
1266 let key128 = await Cipher.deriveKey(passphrase);
1267 let cipher = Cipher.create(key128);
1269 return cipher.decrypt(encWif);
1272 // tuple example {Promise<[String, Boolean]>}
1274 * @param {Object} [opts]
1275 * @param {Boolean} [opts.force]
1276 * @param {String} plainWif
1278 async function maybeEncrypt(plainWif, opts) {
1279 let passphrase = cmds._getPassphrase();
1281 let result = await cmds.getPassphrase({}, []);
1282 passphrase = result.passphrase;
1286 throw new Error(`no passphrase with which to encrypt file`);
1291 let key128 = await Cipher.deriveKey(passphrase);
1292 let cipher = Cipher.create(key128);
1293 return cipher.encrypt(plainWif);
1298 * @param {Array<String>} args
1300 async function setDefault(_, args) {
1301 let addr = args.shift() || "";
1303 let keyname = await findWif(addr);
1305 console.error(`no key matches '${addr}'`);
1310 let filepath = Path.join(keysDir, keyname);
1311 let wif = await maybeReadKeyFile(filepath);
1312 let pk = new Dashcore.PrivateKey(wif);
1313 let pub = pk.toAddress().toString();
1315 console.info("set", defaultWifPath, pub);
1316 await Fs.writeFile(defaultWifPath, pub, "utf8");
1319 // TODO option to specify config dir
1322 * @param {Object} opts
1323 * @param {any} opts.dashApi - TODO
1324 * @param {String} opts.defaultAddr
1325 * @param {Array<String>} args
1327 async function listKeys({ dashApi, defaultAddr }, args) {
1328 let wifnames = await listManagedKeynames();
1331 // to print 'default staking key' message
1332 await mustGetAddr({ defaultAddr }, args);
1336 * @type Array<{ node: String, error: Error }>
1339 // console.error because console.debug goes to stdout, not stderr
1341 debug(`Staking keys: (in ${keysDirRel}/)`);
1344 await wifnames.reduce(async function (promise, wifname) {
1347 let wifpath = Path.join(keysDir, wifname);
1348 let addr = await maybeReadKeyFile(wifpath, { wif: false }).catch(function (
1351 warns.push({ node: wifname, error: err });
1358 console.info(`${addr}`);
1359 }, Promise.resolve());
1363 console.warn(`Warnings:`);
1364 warns.forEach(function (warn) {
1365 console.warn(`${warn.node}: ${warn.error.message}`);
1372 * @param {Object} opts
1373 * @param {any} opts.dashApi - TODO
1374 * @param {String} opts.defaultAddr
1375 * @param {Array<String>} args
1377 async function getAllBalances({ dashApi, defaultAddr }, args) {
1378 let wifnames = await listManagedKeynames();
1388 if (wifnames.length) {
1389 // to print 'default staking key' message
1390 await mustGetAddr({ defaultAddr }, args);
1394 * @type Array<{ node: String, error: Error }>
1397 // console.error because console.debug goes to stdout, not stderr
1399 debug(`Staking keys: (in ${keysDirRel}/)`);
1402 `| | 🔑 Holdings | 🪧 Stakings | 💸 Earnings |`,
1405 `| ---------------------------------: | ------------: | ------------: | ------------: |`,
1407 if (!wifnames.length) {
1408 console.info(` (none)`);
1410 await wifnames.reduce(async function (promise, wifname) {
1413 let wifpath = Path.join(keysDir, wifname);
1414 let addr = await maybeReadKeyFile(wifpath, { wif: false }).catch(function (
1417 warns.push({ node: wifname, error: err });
1425 let pk = new Dashcore.PrivateKey(wif);
1426 let pub = pk.toAddress().toString();
1427 if (`${pub}.wif` !== wifname) {
1432 `computed pubkey '${pub}' of WIF does not match filename '${wifname}'`,
1439 process.stdout.write(`| ${addr} |`);
1441 let balanceInfo = await dashApi.getInstantBalance(addr);
1442 let balanceDASH = toDASH(balanceInfo.balanceSat);
1444 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1445 if (!crowdNodeBalance.TotalBalance) {
1446 crowdNodeBalance.TotalBalance = 0;
1447 crowdNodeBalance.TotalDividend = 0;
1449 let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance);
1450 let crowdNodeDASH = toDASH(crowdNodeDuffNum);
1452 let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend);
1453 let crowdNodeDivDASH = toDASH(crowdNodeDivNum);
1454 process.stdout.write(
1455 ` ${balanceDASH} | ${crowdNodeDASH} | ${crowdNodeDivDASH} |`,
1458 totals.key += balanceInfo.balanceSat;
1459 totals.dividend += crowdNodeBalance.TotalDividend;
1460 totals.stake += crowdNodeBalance.TotalBalance;
1463 }, Promise.resolve());
1467 let total = `| Totals`;
1468 totals.keyDash = toDASH(toDuff(totals.key.toString()));
1469 totals.stakeDash = toDASH(toDuff(totals.stake.toString()));
1470 totals.dividendDash = toDASH(toDuff(totals.dividend.toString()));
1472 `${total} | ${totals.stakeDash} | ${totals.stakeDash} | ${totals.dividendDash} |`,
1477 console.warn(`Warnings:`);
1478 warns.forEach(function (warn) {
1479 console.warn(`${warn.node}: ${warn.error.message}`);
1486 * @param {String} name - ex: Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.wif.enc
1488 function isNamedLikeKey(name) {
1489 // TODO distinguish with .enc extension?
1490 let hasGoodLength = 34 + 4 === name.length || 34 + 4 + 4 === name.length;
1491 let knownExt = name.endsWith(".wif") || name.endsWith(".wif.enc");
1492 let isTmp = name.startsWith(".") || name.startsWith("_");
1493 return hasGoodLength && knownExt && !isTmp;
1497 * @param {Object} opts
1498 * @param {any} opts.dashApi - TODO
1499 * @param {String} opts.addr
1500 * @param {String} opts.filepath
1501 * @param {String} opts.insightBaseUrl
1502 * @param {Array<String>} args
1504 async function removeKey({ addr, dashApi, filepath, insightBaseUrl }, args) {
1505 let balanceInfo = await dashApi.getInstantBalance(addr);
1507 let balanceDash = toDash(balanceInfo.balanceSat);
1508 if (balanceInfo.balanceSat) {
1510 console.error(`Error: ${addr}`);
1512 ` still has a balance of ${balanceInfo.balanceSat} (Đ${balanceDash})`,
1514 console.error(` (transfer to another address before deleting)`);
1520 await initCrowdNode(insightBaseUrl);
1521 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1522 if (!crowdNodeBalance) {
1523 // may be janky if not registered
1524 crowdNodeBalance = {};
1526 if (!crowdNodeBalance.TotalBalance) {
1527 crowdNodeBalance.TotalBalance = 0;
1529 let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance);
1530 if (crowdNodeBalance.TotalBalance) {
1532 console.error(`Error: ${addr}`);
1534 ` still staking ${crowdNodeBalance.TotalBalance} (Đ${crowdNodeDash}) on CrowdNode`,
1537 ` (withdrawal 100.0 and transfer to another address before deleting)`,
1544 let wifname = await findWif(addr);
1545 let fullpath = Path.join(keysDir, wifname);
1546 let wif = await maybeReadKeyPaths(filepath, { wif: true });
1548 await Fs.unlink(fullpath).catch(function (err) {
1549 console.error(`could not remove ${filepath}: ${err.message}`);
1553 let wifnames = await listManagedKeynames();
1555 console.info(`No balances found. Removing ${filepath}.`);
1557 console.info(`Backup (just in case):`);
1558 console.info(` ${wif}`);
1560 if (!wifnames.length) {
1561 console.info(`No keys left.`);
1564 let newAddr = wifnames[0];
1565 debug(`Selected ${newAddr} as new default staking key.`);
1566 await Fs.writeFile(defaultWifPath, addr.replace(".wif", ""), "utf8");
1572 * @param {String} pre
1574 async function findWif(pre) {
1579 let names = await listManagedKeynames();
1580 names = names.filter(function (name) {
1581 return name.startsWith(pre);
1584 if (!names.length) {
1588 if (names.length > 1) {
1589 console.error(`'${pre}' is ambiguous:`, names.join(", "));
1597 async function listManagedKeynames() {
1598 let nodes = await Fs.readdir(keysDir);
1600 return nodes.filter(isNamedLikeKey);
1604 * @param {Object} opts
1605 * @param {String} opts.defaultAddr
1606 * @param {String} opts.insightBaseUrl
1607 * @param {Array<String>} args
1609 async function loadAddr({ defaultAddr, insightBaseUrl }, args) {
1610 let [addr] = await mustGetAddr({ defaultAddr }, args);
1612 let desiredAmountDash = parseFloat(args.shift() || "0");
1613 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
1615 let effectiveDuff = desiredAmountDuff;
1616 let effectiveDash = "";
1617 if (!effectiveDuff) {
1618 effectiveDuff = CrowdNode.stakeMinimum + signupTotal + feeEstimate;
1619 effectiveDuff = roundDuff(effectiveDuff, 3);
1620 effectiveDash = toDash(effectiveDuff);
1623 await plainLoadAddr({ addr, effectiveDash, effectiveDuff, insightBaseUrl });
1629 * 1000 to Round to the nearest mDash
1630 * ex: 0.50238108 => 0.50300000
1631 * @param {Number} effectiveDuff
1632 * @param {Number} numDigits
1634 function roundDuff(effectiveDuff, numDigits) {
1635 let n = Math.pow(10, numDigits);
1636 let effectiveDash = toDash(effectiveDuff);
1637 effectiveDuff = toDuff(
1638 (Math.ceil(parseFloat(effectiveDash) * n) / n).toString(),
1640 return effectiveDuff;
1644 * @param {Object} opts
1645 * @param {String} opts.addr
1646 * @param {String} opts.effectiveDash
1647 * @param {Number} opts.effectiveDuff
1648 * @param {String} opts.insightBaseUrl
1650 async function plainLoadAddr({
1657 showQr(addr, effectiveDuff);
1660 `Use the QR Code above to load ${effectiveDuff} (Đ${effectiveDash}) onto your staking key.`,
1663 console.info(`(waiting...)`);
1665 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
1666 console.info(`Received ${payment.satoshis}`);
1670 * @param {Object} opts
1671 * @param {String} opts.defaultAddr
1672 * @param {any} opts.dashApi - TODO
1673 * @param {Array<String>} args
1675 async function getBalance({ dashApi, defaultAddr }, args) {
1676 let [addr] = await mustGetAddr({ defaultAddr }, args);
1677 await checkBalance({ addr, dashApi });
1678 //let balanceInfo = await checkBalance({ addr, dashApi });
1679 //console.info(balanceInfo);
1684 * @param {Object} opts
1685 * @param {any} opts.dashApi - TODO
1686 * @param {String} opts.defaultAddr
1687 * @param {Boolean} opts.forceConfirm
1688 * @param {String} opts.insightBaseUrl
1689 * @param {any} opts.insightApi
1690 * @param {Array<String>} args
1692 // ex: node ./bin/crowdnode.js transfer ./priv.wif 'pub' 0.01
1693 async function transferBalance(
1694 { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
1697 let wif = await mustGetWif({ defaultAddr }, args);
1699 let keyname = args.shift() || "";
1700 let newAddr = await wifFileToAddr(keyname);
1701 let dashAmount = parseFloat(args.shift() || "0");
1702 let duffAmount = Math.round(dashAmount * DUFFS);
1705 tx = await dashApi.createPayment(wif, newAddr, duffAmount);
1707 tx = await dashApi.createBalanceTransfer(wif, newAddr);
1710 let dashAmountStr = toDash(duffAmount);
1712 `Transferring ${duffAmount} (Đ${dashAmountStr}) to ${newAddr}...`,
1715 console.info(`Transferring balance to ${newAddr}...`);
1717 await insightApi.instantSend(tx);
1718 console.info(`Queued...`);
1719 setTimeout(function () {
1720 // TODO take a cleaner approach
1721 // (waitForVout needs a reasonable timeout)
1722 console.error(`Error: Transfer did not complete.`);
1724 console.error(`(using --unconfirmed may lead to rejected double spends)`);
1728 await Ws.waitForVout(insightBaseUrl, newAddr, 0);
1729 console.info(`Accepted!`);
1734 * @param {Object} opts
1735 * @param {any} opts.dashApi - TODO
1736 * @param {String} opts.defaultAddr
1737 * @param {String} opts.insightBaseUrl
1738 * @param {Array<String>} args
1740 async function getStatus({ dashApi, defaultAddr, insightBaseUrl }, args) {
1741 let [addr] = await mustGetAddr({ defaultAddr }, args);
1742 await initCrowdNode(insightBaseUrl);
1743 let hotwallet = CrowdNode.main.hotwallet;
1744 let state = await getCrowdNodeStatus({ addr, hotwallet });
1747 console.info(`API Actions Complete for ${addr}:`);
1748 console.info(` ${state.signup} SignUpForApi`);
1749 console.info(` ${state.accept} AcceptTerms`);
1750 console.info(` ${state.deposit} DepositReceived`);
1752 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1753 // may be unregistered / undefined
1756 * '@odata.context': 'https://app.crowdnode.io/odata/$metadata#Edm.String',
1757 * value: 'Address not found.'
1760 if (!crowdNodeBalance.TotalBalance) {
1761 crowdNodeBalance.TotalBalance = 0;
1763 let crowdNodeDuff = toDuff(crowdNodeBalance.TotalBalance);
1765 `CrowdNode Stake: ${crowdNodeDuff} (Đ${crowdNodeBalance.TotalBalance})`,
1772 * @param {Object} opts
1773 * @param {any} opts.dashApi - TODO
1774 * @param {String} opts.defaultAddr
1775 * @param {String} opts.insightBaseUrl
1776 * @param {Array<String>} args
1778 async function sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args) {
1779 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1780 await initCrowdNode(insightBaseUrl);
1781 let hotwallet = CrowdNode.main.hotwallet;
1782 let state = await getCrowdNodeStatus({ addr, hotwallet });
1783 let balanceInfo = await checkBalance({ addr, dashApi });
1785 if (state.status?.signup) {
1786 console.info(`${addr} is already signed up. Here's the account status:`);
1787 console.info(` ${state.signup} SignUpForApi`);
1788 console.info(` ${state.accept} AcceptTerms`);
1789 console.info(` ${state.deposit} DepositReceived`);
1793 let hasEnough = balanceInfo.balanceSat > signupOnly + feeEstimate;
1795 await collectSignupFees(insightBaseUrl, addr);
1798 let wif = await maybeReadKeyPaths(name, { wif: true });
1800 console.info("Requesting account...");
1801 await CrowdNode.signup(wif, hotwallet);
1802 state.signup = DONE;
1803 console.info(` ${state.signup} SignUpForApi`);
1808 * @param {Object} opts
1809 * @param {any} opts.dashApi - TODO
1810 * @param {String} opts.defaultAddr
1811 * @param {String} opts.insightBaseUrl
1812 * @param {Array<String>} args
1814 async function acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args) {
1815 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1817 await initCrowdNode(insightBaseUrl);
1818 let hotwallet = CrowdNode.main.hotwallet;
1819 let state = await getCrowdNodeStatus({ addr, hotwallet });
1820 let balanceInfo = await dashApi.getInstantBalance(addr);
1822 if (!state.status?.signup) {
1823 console.info(`${addr} is not signed up yet. Here's the account status:`);
1824 console.info(` ${state.signup} SignUpForApi`);
1825 console.info(` ${state.accept} AcceptTerms`);
1830 if (state.status?.accept) {
1831 console.info(`${addr} is already signed up. Here's the account status:`);
1832 console.info(` ${state.signup} SignUpForApi`);
1833 console.info(` ${state.accept} AcceptTerms`);
1834 console.info(` ${state.deposit} DepositReceived`);
1837 let hasEnough = balanceInfo.balanceSat > acceptOnly + feeEstimate;
1839 await collectSignupFees(insightBaseUrl, addr);
1842 let wif = await maybeReadKeyPaths(name, { wif: true });
1844 console.info("Accepting terms...");
1845 await CrowdNode.accept(wif, hotwallet);
1846 state.accept = DONE;
1847 console.info(` ${state.accept} AcceptTerms`);
1852 * @param {Object} opts
1853 * @param {any} opts.dashApi - TODO
1854 * @param {String} opts.defaultAddr
1855 * @param {String} opts.insightBaseUrl
1856 * @param {Boolean} opts.noReserve
1857 * @param {Array<String>} args
1859 async function depositDash(
1860 { dashApi, defaultAddr, insightBaseUrl, noReserve },
1863 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1864 await initCrowdNode(insightBaseUrl);
1865 let hotwallet = CrowdNode.main.hotwallet;
1866 let state = await getCrowdNodeStatus({ addr, hotwallet });
1867 let balanceInfo = await dashApi.getInstantBalance(addr);
1869 if (!state.status?.accept) {
1870 console.error(`no account for address ${addr}`);
1875 // this would allow for at least 2 withdrawals costing (21000 + 1000)
1876 let reserve = 50000;
1877 let reserveDash = toDash(reserve);
1880 `reserving ${reserve} (Đ${reserveDash}) for withdrawals (--no-reserve to disable)`,
1886 // TODO if unconfirmed, check utxos instead
1888 // deposit what the user asks, or all that we have,
1889 // or all that the user deposits - but at least 2x the reserve
1890 let desiredAmountDash = parseFloat(args.shift() || "0");
1891 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
1892 let effectiveAmount = desiredAmountDuff;
1893 if (!effectiveAmount) {
1894 effectiveAmount = balanceInfo.balanceSat - reserve;
1896 let needed = Math.max(reserve * 2, effectiveAmount + reserve);
1898 if (balanceInfo.balanceSat < needed) {
1900 if (desiredAmountDuff) {
1901 ask = desiredAmountDuff + reserve + -balanceInfo.balanceSat;
1903 await collectDeposit(insightBaseUrl, addr, ask);
1904 balanceInfo = await dashApi.getInstantBalance(addr);
1905 if (balanceInfo.balanceSat < needed) {
1906 let balanceDash = toDash(balanceInfo.balanceSat);
1908 `Balance is still too small: ${balanceInfo.balanceSat} (Đ${balanceDash})`,
1914 if (!desiredAmountDuff) {
1915 effectiveAmount = balanceInfo.balanceSat - reserve;
1918 let effectiveDash = toDash(effectiveAmount);
1920 `Initiating deposit of ${effectiveAmount} (Đ${effectiveDash})...`,
1923 let wif = await maybeReadKeyPaths(name, { wif: true });
1925 await CrowdNode.deposit(wif, hotwallet, effectiveAmount);
1926 state.deposit = DONE;
1927 console.info(` ${state.deposit} DepositReceived`);
1932 * @param {Object} opts
1933 * @param {any} opts.dashApi - TODO
1934 * @param {String} opts.defaultAddr
1935 * @param {String} opts.insightBaseUrl
1936 * @param {Array<String>} args
1938 async function withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args) {
1939 let [addr] = await mustGetAddr({ defaultAddr }, args);
1940 await initCrowdNode(insightBaseUrl);
1941 let hotwallet = CrowdNode.main.hotwallet;
1942 let state = await getCrowdNodeStatus({ addr, hotwallet });
1944 if (!state.status?.accept) {
1945 console.error(`no account for address ${addr}`);
1950 let percentStr = args.shift() || "100.0";
1951 // pass: .1 0.1, 1, 1.0, 10, 10.0, 100, 100.0
1952 // fail: 1000, 10.00
1953 if (!/^1?\d?\d?(\.\d)?$/.test(percentStr)) {
1954 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
1957 let percent = parseFloat(percentStr);
1959 let permil = Math.round(percent * 10);
1960 if (permil <= 0 || permil > 1000) {
1961 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
1965 let realPercentStr = (permil / 10).toFixed(1);
1966 console.info(`Initiating withdrawal of ${realPercentStr}%...`);
1968 let wifname = await findWif(addr);
1969 let filepath = Path.join(keysDir, wifname);
1970 let wif = await maybeReadKeyFile(filepath);
1971 let paid = await CrowdNode.withdrawal(wif, hotwallet, permil);
1972 //let paidFloat = (paid.satoshis / DUFFS).toFixed(8);
1973 //let paidInt = paid.satoshis.toString().padStart(9, "0");
1974 console.info(`API Response: ${paid.api}`);
1981 * Convert prefix, addr, keyname, or filepath to pub addr
1982 * @param {String} name
1985 async function wifFileToAddr(name) {
1986 if (34 === name.length) {
1987 // actually payment addr
1993 let wifname = await findWif(name);
1995 let filepath = Path.join(keysDir, wifname);
1996 privKey = await maybeReadKeyFile(filepath);
1999 privKey = await maybeReadKeyFile(name);
2002 throw new Error("bad file path or address");
2005 let pk = new Dashcore.PrivateKey(privKey);
2006 let pub = pk.toPublicKey().toAddress().toString();
2011 * @param {String} insightBaseUrl
2012 * @param {String} addr
2014 async function collectSignupFees(insightBaseUrl, addr) {
2018 let signupTotalDash = toDash(signupTotal);
2019 let signupMsg = `Please send >= ${signupTotal} (Đ${signupTotalDash}) to Sign Up to CrowdNode`;
2020 let msgPad = Math.ceil((qrWidth - signupMsg.length) / 2);
2021 let subMsg = "(plus whatever you'd like to deposit)";
2022 let subMsgPad = Math.ceil((qrWidth - subMsg.length) / 2);
2025 console.info(" ".repeat(msgPad) + signupMsg);
2026 console.info(" ".repeat(subMsgPad) + subMsg);
2030 console.info("(waiting...)");
2032 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
2033 console.info(`Received ${payment.satoshis}`);
2037 * @param {String} insightBaseUrl
2038 * @param {String} addr
2039 * @param {Number} duffAmount
2041 async function collectDeposit(insightBaseUrl, addr, duffAmount) {
2043 showQr(addr, duffAmount);
2045 let depositMsg = `Please send what you wish to deposit to ${addr}`;
2047 let dashAmount = toDash(duffAmount);
2048 depositMsg = `Please deposit ${duffAmount} (Đ${dashAmount}) to ${addr}`;
2051 let msgPad = Math.ceil((qrWidth - depositMsg.length) / 2);
2052 msgPad = Math.max(0, msgPad);
2055 console.info(" ".repeat(msgPad) + depositMsg);
2059 console.info("(waiting...)");
2061 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
2062 console.info(`Received ${payment.satoshis}`);
2066 * @param {Error & { code: String }} err
2069 function emptyStringOnErrEnoent(err) {
2070 if ("ENOENT" !== err.code) {
2077 * @param {Number} duffs - ex: 00000000
2079 function toDash(duffs) {
2080 return (duffs / DUFFS).toFixed(8);
2084 * @param {Number} duffs - ex: 00000000
2086 function toDASH(duffs) {
2087 let dash = (duffs / DUFFS).toFixed(8);
2088 return `Đ` + dash.padStart(12, " ");
2092 * @param {String} dash - ex: 0.00000000
2094 function toDuff(dash) {
2095 return Math.round(parseFloat(dash) * DUFFS);
2100 main().catch(function (err) {
2101 console.error("Fail:");
2102 console.error(err.stack || err);