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>",
125 async function main() {
126 /*jshint maxcomplexity:40 */
127 /*jshint maxstatements:500 */
130 // crowdnode <subcommand> [flags] <privkey> [options]
132 // crowdnode withdrawal ./Xxxxpubaddr.wif 100.0
134 let args = process.argv.slice(2);
137 let forceGenerate = removeItem(args, "--create-new");
138 let forceConfirm = removeItem(args, "--unconfirmed");
139 let plainText = removeItem(args, "--plain-text");
140 let noReserve = removeItem(args, "--no-reserve");
142 let subcommand = args.shift();
144 if (!subcommand || ["--help", "-h", "help"].includes(subcommand)) {
150 if (["--version", "-V", "version"].includes(subcommand)) {
158 // find addr by name or by file or by string
159 await Fs.mkdir(keysDir, {
163 let defaultAddr = await Fs.readFile(defaultWifPath, "utf8").catch(
164 emptyStringOnErrEnoent,
166 defaultAddr = defaultAddr.trim();
169 process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
170 let insightApi = Insight.create({ baseUrl: insightBaseUrl });
171 let dashApi = Dash.create({ insightApi: insightApi });
173 if ("stake" === subcommand) {
189 if ("list" === subcommand) {
190 await listKeys({ dashApi, defaultAddr }, args);
195 if ("init" === subcommand) {
196 await initKeystore({ defaultAddr });
201 if ("generate" === subcommand) {
202 await generateKey({ defaultKey: defaultAddr, plainText }, args);
207 if ("passphrase" === subcommand) {
208 await setPassphrase({}, args);
213 if ("import" === subcommand) {
214 let keypath = args.shift() || "";
215 await importKey({ keypath });
220 if ("encrypt" === subcommand) {
221 let addr = args.shift() || "";
223 await encryptAll(null);
228 let keypath = await findWif(addr);
230 console.error(`no managed key matches '${addr}'`);
234 let key = await maybeReadKeyFileRaw(keypath);
236 throw new Error("impossible error");
238 await encryptAll([key]);
243 if ("decrypt" === subcommand) {
244 let addr = args.shift() || "";
246 await decryptAll(null);
247 await Fs.writeFile(shadowPath, NO_SHADOW, "utf8").catch(
248 emptyStringOnErrEnoent,
253 let keypath = await findWif(addr);
255 console.error(`no managed key matches '${addr}'`);
259 let key = await maybeReadKeyFileRaw(keypath);
261 throw new Error("impossible error");
263 await decryptAll([key]);
268 // use or select or default... ?
269 if ("use" === subcommand) {
270 await setDefault(null, args);
275 // helper for debugging
276 if ("transfer" === subcommand) {
277 await transferBalance(
278 { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
286 if ("http" === subcommand) {
287 rpc = args.shift() || "";
294 let [addr] = await mustGetAddr({ defaultAddr }, args);
296 await initCrowdNode(insightBaseUrl);
297 // ex: http <rpc>(<pub>, ...)
299 let hasRpc = rpc in CrowdNode.http;
301 console.error(`Unrecognized rpc command ${rpc}`);
306 //@ts-ignore - TODO use `switch` or make Record Type
307 let result = await CrowdNode.http[rpc].apply(null, args);
309 console.info(`${rpc} ${addr}:`);
310 if ("string" === typeof result) {
311 console.info(result);
313 console.info(JSON.stringify(result, null, 2));
319 if ("load" === subcommand) {
320 await loadAddr({ defaultAddr, insightBaseUrl }, args);
325 // keeping rm for backwards compat
326 if ("rm" === subcommand || "delete" === subcommand) {
327 await initCrowdNode(insightBaseUrl);
328 let [addr, filepath] = await mustGetAddr({ defaultAddr }, args);
329 await removeKey({ addr, dashApi, filepath, insightBaseUrl }, args);
334 if ("balance" === subcommand) {
336 await getBalance({ dashApi, defaultAddr }, args);
341 await getAllBalances({ dashApi, defaultAddr }, args);
346 if ("status" === subcommand) {
347 await getStatus({ dashApi, defaultAddr, insightBaseUrl }, args);
352 if ("signup" === subcommand) {
353 await sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args);
358 if ("accept" === subcommand) {
359 await acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args);
364 if ("deposit" === subcommand) {
366 { dashApi, defaultAddr, insightBaseUrl, noReserve },
373 if ("withdrawal" === subcommand) {
374 await withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args);
379 console.error(`Unrecognized subcommand ${subcommand}`);
386 * @param {Object} opts
387 * @param {any} opts.dashApi - TODO
388 * @param {String} opts.defaultAddr
389 * @param {Boolean} opts.forceGenerate
390 * @param {String} opts.insightBaseUrl
391 * @param {any} opts.insightApi
392 * @param {Boolean} opts.noReserve
393 * @param {Array<String>} args
395 async function stakeDash(
406 let err = await Fs.access(args[0]).catch(Object);
409 let keypath = args.shift() || "";
410 addr = await importKey({ keypath });
411 } else if (forceGenerate) {
412 addr = await generateKey({ defaultKey: defaultAddr }, []);
414 addr = await initKeystore({ defaultAddr });
418 let [_addr] = await mustGetAddr({ defaultAddr }, args);
422 let extra = feeEstimate;
423 console.info("Checking CrowdNode account... ");
424 await CrowdNode.init({
425 baseUrl: "https://app.crowdnode.io",
428 let hotwallet = CrowdNode.main.hotwallet;
429 let state = await getCrowdNodeStatus({ addr, hotwallet });
431 if (!state.status?.accept) {
432 if (!state.status?.signup) {
433 let signUpDeposit = signupOnly + feeEstimate;
435 ` ${TODO} SignUpForApi deposit is ${signupOnly} (+ tx fee)`,
437 extra += signUpDeposit;
439 console.info(` ${DONE} SignUpForApi complete`);
441 let acceptDeposit = acceptOnly + feeEstimate;
442 console.info(` ${TODO} AcceptTerms deposit is ${acceptOnly} (+ tx fee)`);
443 extra += acceptDeposit;
446 let desiredAmountDash = args.shift() || "0.5";
447 let effectiveDuff = toDuff(desiredAmountDash);
448 effectiveDuff += extra;
450 let balanceInfo = await dashApi.getInstantBalance(addr);
451 effectiveDuff -= balanceInfo.balanceSat;
453 if (effectiveDuff > 0) {
454 effectiveDuff = roundDuff(effectiveDuff, 3);
455 let effectiveDash = toDash(effectiveDuff);
456 await plainLoadAddr({
464 if (!state.status?.accept) {
465 if (!state.status?.signup) {
466 await sendSignup({ dashApi, defaultAddr: addr, insightBaseUrl }, [addr]);
468 await acceptTerms({ dashApi, defaultAddr: addr, insightBaseUrl }, [addr]);
472 { dashApi, defaultAddr: addr, insightBaseUrl, noReserve },
478 * @param {Object} opts
479 * @param {String} opts.defaultAddr
481 async function initKeystore({ defaultAddr }) {
482 // if we have no keys, make one
483 let wifnames = await listManagedKeynames();
484 if (!wifnames.length) {
485 return await generateKey({ defaultKey: defaultAddr }, []);
487 // if we have no passphrase, ask about it
488 await initPassphrase();
489 return defaultAddr || wifnames[0];
493 * @param {String} insightBaseUrl
495 async function initCrowdNode(insightBaseUrl) {
496 if (CrowdNode.main.hotwallet) {
499 process.stdout.write("Checking CrowdNode API... ");
500 await CrowdNode.init({
501 baseUrl: "https://app.crowdnode.io",
504 console.info(`(hotwallet ${CrowdNode.main.hotwallet})`);
508 * @param {String} addr - Base58Check pubKeyHash address
509 * @param {Number} duffs - 1/100000000 of a DASH
511 function showQr(addr, duffs = 0) {
512 let dashAmount = toDash(duffs);
513 let dashUri = `dash://${addr}`;
515 dashUri += `?amount=${dashAmount}`;
518 let dashQr = Qr.ascii(dashUri, { indent: 4, size: "mini" });
519 let addrPad = Math.max(0, Math.ceil((qrWidth - dashUri.length) / 2));
521 console.info(dashQr);
523 console.info(" ".repeat(addrPad) + dashUri);
527 * @param {Array<any>} arr
530 function removeItem(arr, item) {
531 let index = arr.indexOf(item);
533 return arr.splice(index, 1)[0];
539 * @param {Object} opts
540 * @param {String} opts.addr
541 * @param {String} opts.hotwallet
543 async function getCrowdNodeStatus({ addr, hotwallet }) {
555 //@ts-ignore - TODO why warnings?
556 let status = await CrowdNode.status(addr, hotwallet);
558 state.status = status;
560 if (state.status?.signup) {
563 if (state.status?.accept) {
566 if (state.status?.deposit) {
567 state.deposit = DONE;
573 * @param {Object} opts
574 * @param {String} opts.addr
575 * @param {any} opts.dashApi - TODO
577 async function checkBalance({ addr, dashApi }) {
578 // deposit if balance is over 100,000 (0.00100000)
579 console.info("Checking balance... ");
580 let balanceInfo = await dashApi.getInstantBalance(addr);
581 let balanceDASH = toDASH(balanceInfo.balanceSat);
583 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
584 if (!crowdNodeBalance.TotalBalance) {
585 crowdNodeBalance.TotalBalance = 0;
586 crowdNodeBalance.TotalDividend = 0;
589 let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance);
590 let crowdNodeDASH = toDASH(crowdNodeDuffNum);
592 let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend);
593 let crowdNodeDASHDiv = toDASH(crowdNodeDivNum);
595 console.info(`Key: ${balanceDASH}`);
596 console.info(`CrowdNode: ${crowdNodeDASH}`);
597 console.info(`Dividends: ${crowdNodeDASHDiv}`);
600 let balanceInfo = await insightApi.getBalance(pub);
601 if (balanceInfo.unconfirmedBalanceSat || balanceInfo.unconfirmedAppearances) {
604 `Error: This address has pending transactions. Please try again in 1-2 minutes or use --unconfirmed.`,
606 console.error(balanceInfo);
607 if ("status" !== subcommand) {
618 * @param {Object} opts
619 * @param {String} opts.defaultAddr
620 * @param {Array<String>} args
621 * @returns {Promise<[String, String]>}
623 async function mustGetAddr({ defaultAddr }, args) {
624 let name = args.shift() ?? "";
625 if (34 === name.length) {
626 // looks like addr already
627 // TODO make function for addr-lookin' check
631 let addr = await maybeReadKeyPaths(name, { wif: false });
633 if (34 === addr.length) {
636 //let pk = new Dashcore.PrivateKey(wif);
637 //let addr = pk.toAddress().toString();
641 let isNum = !isNaN(parseFloat(name));
649 console.error(`could not read '${name}' in ./ or match in ${keysDirRel}/.`);
655 addr = await mustGetDefaultWif(defaultAddr, { wif: false });
657 // TODO we don't need defaultAddr, right? because it could be old?
662 * @param {Object} opts
663 * @param {String} opts.defaultAddr
664 * @param {Array<String>} args
666 async function mustGetWif({ defaultAddr }, args) {
667 let name = args.shift() ?? "";
669 let wif = await maybeReadKeyPaths(name, { wif: true });
674 let isNum = !isNaN(parseFloat(name));
683 `'${name}' does not match a staking key in ./ or ${keysDirRel}/`,
690 wif = await mustGetDefaultWif(defaultAddr);
696 * @param {String} name
697 * @param {Object} opts
698 * @param {Boolean} opts.wif
699 * @returns {Promise<String>} - wif
701 async function maybeReadKeyPaths(name, opts) {
704 // prefix match in .../keys/
705 let wifname = await findWif(name);
710 if (false === opts.wif) {
711 return wifname.slice(0, -".wif".length);
714 let filepath = Path.join(keysDir, wifname);
715 privKey = await maybeReadKeyFile(filepath);
718 privKey = await maybeReadKeyFile(name);
725 * @param {String} defaultAddr
726 * @param {Object} [opts]
727 * @param {Boolean} opts.wif
729 async function mustGetDefaultWif(defaultAddr, opts) {
732 let keyfile = Path.join(keysDir, `${defaultAddr}.wif`);
733 let raw = await maybeReadKeyFileRaw(keyfile, opts);
734 // misnomering wif here a bit
735 defaultWif = raw?.wif || raw?.addr || "";
737 if (defaultWif && !shownDefault) {
739 debug(`Selected default staking key ${defaultAddr}`);
744 console.error(`Error: no default staking key selected.`);
746 console.error(`Select a different address:`);
747 console.error(` crowdnode list`);
748 console.error(` crowdnode use <addr>`);
750 console.error(`Or create a new staking key:`);
751 console.error(` crowdnode generate`);
760 * @param {Object} psuedoState
761 * @param {String} psuedoState.defaultKey - addr name of default key
762 * @param {Boolean} [psuedoState.plainText] - don't encrypt
763 * @param {Array<String>} args
765 async function generateKey({ defaultKey, plainText }, args) {
766 let name = args.shift();
767 //@ts-ignore - TODO submit JSDoc PR for Dashcore
768 let pk = new Dashcore.PrivateKey();
770 let addr = pk.toAddress().toString();
771 let plainWif = pk.toWIF();
775 wif = await maybeEncrypt(plainWif);
778 let filename = `~/${configdir}/keys/${addr}.wif`;
779 let filepath = Path.join(`${keysDir}/${addr}.wif`);
784 note = `\n(for pubkey address ${addr})`;
785 let err = await Fs.access(filepath).catch(Object);
788 console.info(`'${filepath}' already exists (will not overwrite)`);
794 await Fs.writeFile(filepath, wif, "utf8");
795 if (!name && !defaultKey) {
796 await Fs.writeFile(defaultWifPath, addr, "utf8");
800 console.info(`Generated ${filename} ${note}`);
805 async function initPassphrase() {
806 let needsInit = false;
807 let shadow = await Fs.readFile(shadowPath, "utf8").catch(
808 emptyStringOnErrEnoent,
814 await cmds.getPassphrase({}, []);
819 * @param {Object} state
820 * @param {Boolean} [state._askPreviousPassphrase] - don't ask for passphrase again
821 * @param {Array<String>} args
823 async function setPassphrase({ _askPreviousPassphrase }, args) {
828 let date = getFsDateString();
830 // get the old passphrase
831 if (false !== _askPreviousPassphrase) {
832 // TODO should contain the shadow?
833 await cmds.getPassphrase({ _rotatePassphrase: true }, []);
836 // get the new passphrase
837 let newPassphrase = await promptPassphrase();
838 let curShadow = await Fs.readFile(shadowPath, "utf8").catch(
839 emptyStringOnErrEnoent,
842 let newShadow = await Cipher.shadowPassphrase(newPassphrase);
843 await Fs.writeFile(shadowPath, newShadow, "utf8");
845 let rawKeys = await readAllKeys();
846 let encAddrs = rawKeys
847 .map(function (raw) {
854 // backup all currently encrypted files
856 if (encAddrs.length) {
857 let filepath = Path.join(HOME, `${configdir}/keys.${date}.bak`);
859 console.info(`Backing up previous (encrypted) keys:`);
860 encAddrs.unshift(`SHADOW:${curShadow}`);
861 await Fs.writeFile(filepath, encAddrs.join("\n") + "\n", "utf8");
862 console.info(` ~/${configdir}/keys.${date}.bak`);
865 cmds._setPassphrase(newPassphrase);
867 await encryptAll(rawKeys, { rotateKey: true });
869 result.passphrase = newPassphrase;
870 result.changed = true;
874 async function promptPassphrase() {
877 newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", {
880 newPassphrase = newPassphrase.trim();
882 let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", {
885 _newPassphrase = _newPassphrase.trim();
887 let match = Cipher.secureCompare(newPassphrase, _newPassphrase);
892 console.error("passphrases do not match");
894 return newPassphrase;
899 * @param {Object} opts
900 * @param {String} opts.keypath
902 async function importKey({ keypath }) {
903 let key = await maybeReadKeyFileRaw(keypath);
905 console.error(`no key found for '${keypath}'`);
910 let encWif = await maybeEncrypt(key.wif);
912 if (encWif.includes(":")) {
915 let date = getFsDateString();
918 Path.join(keysDir, `${key.addr}.wif`),
920 Path.join(keysDir, `${key.addr}.${date}.bak`),
923 console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`);
930 * @param {Object} opts
931 * @param {Boolean} [opts._rotatePassphrase]
932 * @param {Boolean} [opts._force]
933 * @param {Array<String>} args
935 cmds.getPassphrase = async function ({ _rotatePassphrase, _force }, args) {
941 if (!_rotatePassphrase) {
942 let cachedphrase = cmds._getPassphrase();
949 // Three possible states:
950 // 1. no shadow file yet (ask to set one)
951 // 2. empty shadow file (initialized, but not set - don't ask to set one)
952 // 3. encrypted shadow file (initialized, requires passphrase)
953 let needsInit = false;
954 let shadow = await Fs.readFile(shadowPath, "utf8").catch(
955 emptyStringOnErrEnoent,
959 } else if (NO_SHADOW === shadow && _force) {
963 // State 1: not initialized, what does the user want?
968 no = await Prompt.prompt(
969 "Would you like to set an encryption passphrase? [Y/n]: ",
973 // Set a passphrase and create shadow file
974 if (!no || ["yes", "y"].includes(no.toLowerCase())) {
975 result = await setPassphrase({ _askPreviousPassphrase: false }, args);
976 cmds._setPassphrase(result.passphrase);
981 if (!["no", "n"].includes(no.toLowerCase())) {
985 // No passphrase, create a NONE shadow file
986 await Fs.writeFile(shadowPath, NO_SHADOW, "utf8");
991 // State 2: shadow already initialized to empty
992 // (user doesn't want a passphrase)
994 cmds._setPassphrase("");
998 // State 3: passphrase & shadow already in use
1000 let prompt = `Enter passphrase: `;
1001 if (_rotatePassphrase) {
1002 prompt = `Enter (current) passphrase: `;
1004 result.passphrase = await Prompt.prompt(prompt, {
1007 result.passphrase = result.passphrase.trim();
1008 if (!result.passphrase || "q" === result.passphrase) {
1009 console.error("cancel: no passphrase");
1014 let match = await Cipher.checkPassphrase(result.passphrase, shadow);
1016 cmds._setPassphrase(result.passphrase);
1021 console.error("incorrect passphrase");
1024 throw new Error("SANITY FAIL: unreachable return");
1027 cmds._getPassphrase = function () {
1032 * @param {String} passphrase
1034 cmds._setPassphrase = function (passphrase) {
1035 // Look Ma! A private variable!
1036 cmds._getPassphrase = function () {
1042 * Encrypt ALL-the-things!
1043 * @param {Object} [opts]
1044 * @param {Boolean} opts.rotateKey
1045 * @param {Array<RawKey>?} rawKeys
1047 async function encryptAll(rawKeys, opts) {
1049 rawKeys = await readAllKeys();
1051 let date = getFsDateString();
1053 let passphrase = cmds._getPassphrase();
1055 let result = await cmds.getPassphrase({ _force: true }, []);
1056 if (result.changed) {
1057 // encryptAll was already called on rotation
1060 passphrase = result.passphrase;
1063 console.info(`Encrypting...`);
1065 await rawKeys.reduce(async function (promise, key) {
1068 if (key.encrypted && !opts?.rotateKey) {
1069 console.info(`🙈 ${key.addr} [already encrypted]`);
1072 let encWif = await maybeEncrypt(key.wif, { force: true });
1074 Path.join(keysDir, `${key.addr}.wif`),
1076 Path.join(keysDir, `${key.addr}.${date}.bak`),
1078 console.info(`🔑 ${key.addr}`);
1079 }, Promise.resolve());
1081 console.info(`Done 🔐`);
1086 * Decrypt ALL-the-things!
1087 * @param {Array<RawKey>?} rawKeys
1089 async function decryptAll(rawKeys) {
1091 rawKeys = await readAllKeys();
1093 let date = getFsDateString();
1096 console.info(`Decrypting...`);
1098 await rawKeys.reduce(async function (promise, key) {
1101 if (!key.encrypted) {
1102 console.info(`📖 ${key.addr} [already decrypted]`);
1106 Path.join(keysDir, `${key.addr}.wif`),
1108 Path.join(keysDir, `${key.addr}.${date}.bak`),
1110 console.info(`🔓 ${key.addr}`);
1111 }, Promise.resolve());
1113 console.info(`Done ${DONE}`);
1117 function getFsDateString() {
1118 // YYYY-MM-DD_hh-mm_ss
1119 let date = new Date()
1123 .replace(/\.\d{3}.*/, "");
1128 * @param {String} filepath
1129 * @param {String} wif
1130 * @param {String} bakpath
1132 async function safeSave(filepath, wif, bakpath) {
1133 let tmpPath = `${bakpath}.tmp`;
1134 await Fs.writeFile(tmpPath, wif, "utf8");
1135 let err = await Fs.access(filepath).catch(Object);
1137 await Fs.rename(filepath, bakpath);
1139 await Fs.rename(tmpPath, filepath);
1141 await Fs.unlink(bakpath);
1146 * @typedef {Object} RawKey
1147 * @property {String} addr
1148 * @property {Boolean} encrypted
1149 * @property {String} wif
1155 async function readAllKeys() {
1156 let wifnames = await listManagedKeynames();
1158 /** @type Array<RawKey> */
1160 await wifnames.reduce(async function (promise, wifname) {
1163 let keypath = Path.join(keysDir, wifname);
1164 let key = await maybeReadKeyFileRaw(keypath);
1169 if (`${key.addr}.wif` !== wifname) {
1171 `computed pubkey '${key.addr}' of WIF does not match filename '${keypath}'`,
1176 }, Promise.resolve());
1182 * @param {String} filepath
1183 * @param {Object} [opts]
1184 * @param {Boolean} opts.wif
1185 * @returns {Promise<String>}
1187 async function maybeReadKeyFile(filepath, opts) {
1188 let key = await maybeReadKeyFileRaw(filepath, opts);
1189 if (false === opts?.wif) {
1190 return key?.addr || "";
1192 return key?.wif || "";
1196 * @param {String} filepath
1197 * @param {Object} [opts]
1198 * @param {Boolean} opts.wif
1199 * @returns {Promise<RawKey?>}
1201 async function maybeReadKeyFileRaw(filepath, opts) {
1202 let privKey = await Fs.readFile(filepath, "utf8").catch(
1203 emptyStringOnErrEnoent,
1205 privKey = privKey.trim();
1210 let encrypted = false;
1211 if (privKey.includes(":")) {
1214 if (false !== opts?.wif) {
1215 privKey = await decrypt(privKey);
1219 console.error(err.message);
1220 console.error(`passphrase does not match for key ${filepath}`);
1224 if (false === opts?.wif) {
1226 addr: Path.basename(filepath, ".wif"),
1227 encrypted: encrypted,
1232 let pk = new Dashcore.PrivateKey(privKey);
1233 let pub = pk.toAddress().toString();
1237 encrypted: encrypted,
1243 * @param {String} encWif
1245 async function decrypt(encWif) {
1246 let passphrase = cmds._getPassphrase();
1248 let result = await cmds.getPassphrase({}, []);
1249 passphrase = result.passphrase;
1250 // we don't return just in case they're setting a passphrase to
1251 // decrypt a previously encrypted file (i.e. for recovery from elsewhere)
1253 let key128 = await Cipher.deriveKey(passphrase);
1254 let cipher = Cipher.create(key128);
1256 return cipher.decrypt(encWif);
1259 // tuple example {Promise<[String, Boolean]>}
1261 * @param {Object} [opts]
1262 * @param {Boolean} [opts.force]
1263 * @param {String} plainWif
1265 async function maybeEncrypt(plainWif, opts) {
1266 let passphrase = cmds._getPassphrase();
1268 let result = await cmds.getPassphrase({}, []);
1269 passphrase = result.passphrase;
1273 throw new Error(`no passphrase with which to encrypt file`);
1278 let key128 = await Cipher.deriveKey(passphrase);
1279 let cipher = Cipher.create(key128);
1280 return cipher.encrypt(plainWif);
1285 * @param {Array<String>} args
1287 async function setDefault(_, args) {
1288 let addr = args.shift() || "";
1290 let keyname = await findWif(addr);
1292 console.error(`no key matches '${addr}'`);
1297 let filepath = Path.join(keysDir, keyname);
1298 let wif = await maybeReadKeyFile(filepath);
1299 let pk = new Dashcore.PrivateKey(wif);
1300 let pub = pk.toAddress().toString();
1302 console.info("set", defaultWifPath, pub);
1303 await Fs.writeFile(defaultWifPath, pub, "utf8");
1306 // TODO option to specify config dir
1309 * @param {Object} opts
1310 * @param {any} opts.dashApi - TODO
1311 * @param {String} opts.defaultAddr
1312 * @param {Array<String>} args
1314 async function listKeys({ dashApi, defaultAddr }, args) {
1315 let wifnames = await listManagedKeynames();
1318 // to print 'default staking key' message
1319 await mustGetAddr({ defaultAddr }, args);
1323 * @type Array<{ node: String, error: Error }>
1326 // console.error because console.debug goes to stdout, not stderr
1328 debug(`Staking keys: (in ${keysDirRel}/)`);
1331 await wifnames.reduce(async function (promise, wifname) {
1334 let wifpath = Path.join(keysDir, wifname);
1335 let addr = await maybeReadKeyFile(wifpath, { wif: false }).catch(function (
1338 warns.push({ node: wifname, error: err });
1345 console.info(`${addr}`);
1346 }, Promise.resolve());
1350 console.warn(`Warnings:`);
1351 warns.forEach(function (warn) {
1352 console.warn(`${warn.node}: ${warn.error.message}`);
1359 * @param {Object} opts
1360 * @param {any} opts.dashApi - TODO
1361 * @param {String} opts.defaultAddr
1362 * @param {Array<String>} args
1364 async function getAllBalances({ dashApi, defaultAddr }, args) {
1365 let wifnames = await listManagedKeynames();
1375 if (wifnames.length) {
1376 // to print 'default staking key' message
1377 await mustGetAddr({ defaultAddr }, args);
1381 * @type Array<{ node: String, error: Error }>
1384 // console.error because console.debug goes to stdout, not stderr
1386 debug(`Staking keys: (in ${keysDirRel}/)`);
1389 `| | 🔑 Holdings | 🪧 Stakings | 💸 Earnings |`,
1392 `| ---------------------------------: | ------------: | ------------: | ------------: |`,
1394 if (!wifnames.length) {
1395 console.info(` (none)`);
1397 await wifnames.reduce(async function (promise, wifname) {
1400 let wifpath = Path.join(keysDir, wifname);
1401 let addr = await maybeReadKeyFile(wifpath, { wif: false }).catch(function (
1404 warns.push({ node: wifname, error: err });
1412 let pk = new Dashcore.PrivateKey(wif);
1413 let pub = pk.toAddress().toString();
1414 if (`${pub}.wif` !== wifname) {
1419 `computed pubkey '${pub}' of WIF does not match filename '${wifname}'`,
1426 process.stdout.write(`| ${addr} |`);
1428 let balanceInfo = await dashApi.getInstantBalance(addr);
1429 let balanceDASH = toDASH(balanceInfo.balanceSat);
1431 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1432 if (!crowdNodeBalance.TotalBalance) {
1433 crowdNodeBalance.TotalBalance = 0;
1434 crowdNodeBalance.TotalDividend = 0;
1436 let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance);
1437 let crowdNodeDASH = toDASH(crowdNodeDuffNum);
1439 let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend);
1440 let crowdNodeDivDASH = toDASH(crowdNodeDivNum);
1441 process.stdout.write(
1442 ` ${balanceDASH} | ${crowdNodeDASH} | ${crowdNodeDivDASH} |`,
1445 totals.key += balanceInfo.balanceSat;
1446 totals.dividend += crowdNodeBalance.TotalDividend;
1447 totals.stake += crowdNodeBalance.TotalBalance;
1450 }, Promise.resolve());
1454 let total = `| Totals`;
1455 totals.keyDash = toDASH(toDuff(totals.key.toString()));
1456 totals.stakeDash = toDASH(toDuff(totals.stake.toString()));
1457 totals.dividendDash = toDASH(toDuff(totals.dividend.toString()));
1459 `${total} | ${totals.stakeDash} | ${totals.stakeDash} | ${totals.dividendDash} |`,
1464 console.warn(`Warnings:`);
1465 warns.forEach(function (warn) {
1466 console.warn(`${warn.node}: ${warn.error.message}`);
1473 * @param {String} name - ex: Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.wif.enc
1475 function isNamedLikeKey(name) {
1476 // TODO distinguish with .enc extension?
1477 let hasGoodLength = 34 + 4 === name.length || 34 + 4 + 4 === name.length;
1478 let knownExt = name.endsWith(".wif") || name.endsWith(".wif.enc");
1479 let isTmp = name.startsWith(".") || name.startsWith("_");
1480 return hasGoodLength && knownExt && !isTmp;
1484 * @param {Object} opts
1485 * @param {any} opts.dashApi - TODO
1486 * @param {String} opts.addr
1487 * @param {String} opts.filepath
1488 * @param {String} opts.insightBaseUrl
1489 * @param {Array<String>} args
1491 async function removeKey({ addr, dashApi, filepath, insightBaseUrl }, args) {
1492 let balanceInfo = await dashApi.getInstantBalance(addr);
1494 let balanceDash = toDash(balanceInfo.balanceSat);
1495 if (balanceInfo.balanceSat) {
1497 console.error(`Error: ${addr}`);
1499 ` still has a balance of ${balanceInfo.balanceSat} (Đ${balanceDash})`,
1501 console.error(` (transfer to another address before deleting)`);
1507 await initCrowdNode(insightBaseUrl);
1508 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1509 if (!crowdNodeBalance) {
1510 // may be janky if not registered
1511 crowdNodeBalance = {};
1513 if (!crowdNodeBalance.TotalBalance) {
1514 crowdNodeBalance.TotalBalance = 0;
1516 let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance);
1517 if (crowdNodeBalance.TotalBalance) {
1519 console.error(`Error: ${addr}`);
1521 ` still staking ${crowdNodeBalance.TotalBalance} (Đ${crowdNodeDash}) on CrowdNode`,
1524 ` (withdrawal 100.0 and transfer to another address before deleting)`,
1531 let wifname = await findWif(addr);
1532 let fullpath = Path.join(keysDir, wifname);
1533 let wif = await maybeReadKeyPaths(filepath, { wif: true });
1535 await Fs.unlink(fullpath).catch(function (err) {
1536 console.error(`could not remove ${filepath}: ${err.message}`);
1540 let wifnames = await listManagedKeynames();
1542 console.info(`No balances found. Removing ${filepath}.`);
1544 console.info(`Backup (just in case):`);
1545 console.info(` ${wif}`);
1547 if (!wifnames.length) {
1548 console.info(`No keys left.`);
1551 let newAddr = wifnames[0];
1552 debug(`Selected ${newAddr} as new default staking key.`);
1553 await Fs.writeFile(defaultWifPath, addr.replace(".wif", ""), "utf8");
1559 * @param {String} pre
1561 async function findWif(pre) {
1566 let names = await listManagedKeynames();
1567 names = names.filter(function (name) {
1568 return name.startsWith(pre);
1571 if (!names.length) {
1575 if (names.length > 1) {
1576 console.error(`'${pre}' is ambiguous:`, names.join(", "));
1584 async function listManagedKeynames() {
1585 let nodes = await Fs.readdir(keysDir);
1587 return nodes.filter(isNamedLikeKey);
1591 * @param {Object} opts
1592 * @param {String} opts.defaultAddr
1593 * @param {String} opts.insightBaseUrl
1594 * @param {Array<String>} args
1596 async function loadAddr({ defaultAddr, insightBaseUrl }, args) {
1597 let [addr] = await mustGetAddr({ defaultAddr }, args);
1599 let desiredAmountDash = parseFloat(args.shift() || "0");
1600 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
1602 let effectiveDuff = desiredAmountDuff;
1603 let effectiveDash = "";
1604 if (!effectiveDuff) {
1605 effectiveDuff = CrowdNode.stakeMinimum + signupTotal + feeEstimate;
1606 effectiveDuff = roundDuff(effectiveDuff, 3);
1607 effectiveDash = toDash(effectiveDuff);
1610 await plainLoadAddr({ addr, effectiveDash, effectiveDuff, insightBaseUrl });
1616 * 1000 to Round to the nearest mDash
1617 * ex: 0.50238108 => 0.50300000
1618 * @param {Number} effectiveDuff
1619 * @param {Number} numDigits
1621 function roundDuff(effectiveDuff, numDigits) {
1622 let n = Math.pow(10, numDigits);
1623 let effectiveDash = toDash(effectiveDuff);
1624 effectiveDuff = toDuff(
1625 (Math.ceil(parseFloat(effectiveDash) * n) / n).toString(),
1627 return effectiveDuff;
1631 * @param {Object} opts
1632 * @param {String} opts.addr
1633 * @param {String} opts.effectiveDash
1634 * @param {Number} opts.effectiveDuff
1635 * @param {String} opts.insightBaseUrl
1637 async function plainLoadAddr({
1644 showQr(addr, effectiveDuff);
1647 `Use the QR Code above to load ${effectiveDuff} (Đ${effectiveDash}) onto your staking key.`,
1650 console.info(`(waiting...)`);
1652 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
1653 console.info(`Received ${payment.satoshis}`);
1657 * @param {Object} opts
1658 * @param {String} opts.defaultAddr
1659 * @param {any} opts.dashApi - TODO
1660 * @param {Array<String>} args
1662 async function getBalance({ dashApi, defaultAddr }, args) {
1663 let [addr] = await mustGetAddr({ defaultAddr }, args);
1664 await checkBalance({ addr, dashApi });
1665 //let balanceInfo = await checkBalance({ addr, dashApi });
1666 //console.info(balanceInfo);
1671 * @param {Object} opts
1672 * @param {any} opts.dashApi - TODO
1673 * @param {String} opts.defaultAddr
1674 * @param {Boolean} opts.forceConfirm
1675 * @param {String} opts.insightBaseUrl
1676 * @param {any} opts.insightApi
1677 * @param {Array<String>} args
1679 // ex: node ./bin/crowdnode.js transfer ./priv.wif 'pub' 0.01
1680 async function transferBalance(
1681 { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
1684 let wif = await mustGetWif({ defaultAddr }, args);
1686 let keyname = args.shift() || "";
1687 let newAddr = await wifFileToAddr(keyname);
1688 let dashAmount = parseFloat(args.shift() || "0");
1689 let duffAmount = Math.round(dashAmount * DUFFS);
1692 tx = await dashApi.createPayment(wif, newAddr, duffAmount);
1694 tx = await dashApi.createBalanceTransfer(wif, newAddr);
1697 let dashAmountStr = toDash(duffAmount);
1699 `Transferring ${duffAmount} (Đ${dashAmountStr}) to ${newAddr}...`,
1702 console.info(`Transferring balance to ${newAddr}...`);
1704 await insightApi.instantSend(tx);
1705 console.info(`Queued...`);
1706 setTimeout(function () {
1707 // TODO take a cleaner approach
1708 // (waitForVout needs a reasonable timeout)
1709 console.error(`Error: Transfer did not complete.`);
1711 console.error(`(using --unconfirmed may lead to rejected double spends)`);
1715 await Ws.waitForVout(insightBaseUrl, newAddr, 0);
1716 console.info(`Accepted!`);
1721 * @param {Object} opts
1722 * @param {any} opts.dashApi - TODO
1723 * @param {String} opts.defaultAddr
1724 * @param {String} opts.insightBaseUrl
1725 * @param {Array<String>} args
1727 async function getStatus({ dashApi, defaultAddr, insightBaseUrl }, args) {
1728 let [addr] = await mustGetAddr({ defaultAddr }, args);
1729 await initCrowdNode(insightBaseUrl);
1730 let hotwallet = CrowdNode.main.hotwallet;
1731 let state = await getCrowdNodeStatus({ addr, hotwallet });
1734 console.info(`API Actions Complete for ${addr}:`);
1735 console.info(` ${state.signup} SignUpForApi`);
1736 console.info(` ${state.accept} AcceptTerms`);
1737 console.info(` ${state.deposit} DepositReceived`);
1739 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1740 // may be unregistered / undefined
1743 * '@odata.context': 'https://app.crowdnode.io/odata/$metadata#Edm.String',
1744 * value: 'Address not found.'
1747 if (!crowdNodeBalance.TotalBalance) {
1748 crowdNodeBalance.TotalBalance = 0;
1750 let crowdNodeDuff = toDuff(crowdNodeBalance.TotalBalance);
1752 `CrowdNode Stake: ${crowdNodeDuff} (Đ${crowdNodeBalance.TotalBalance})`,
1759 * @param {Object} opts
1760 * @param {any} opts.dashApi - TODO
1761 * @param {String} opts.defaultAddr
1762 * @param {String} opts.insightBaseUrl
1763 * @param {Array<String>} args
1765 async function sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args) {
1766 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1767 await initCrowdNode(insightBaseUrl);
1768 let hotwallet = CrowdNode.main.hotwallet;
1769 let state = await getCrowdNodeStatus({ addr, hotwallet });
1770 let balanceInfo = await checkBalance({ addr, dashApi });
1772 if (state.status?.signup) {
1773 console.info(`${addr} is already signed up. Here's the account status:`);
1774 console.info(` ${state.signup} SignUpForApi`);
1775 console.info(` ${state.accept} AcceptTerms`);
1776 console.info(` ${state.deposit} DepositReceived`);
1780 let hasEnough = balanceInfo.balanceSat > signupOnly + feeEstimate;
1782 await collectSignupFees(insightBaseUrl, addr);
1785 let wif = await maybeReadKeyPaths(name, { wif: true });
1787 console.info("Requesting account...");
1788 await CrowdNode.signup(wif, hotwallet);
1789 state.signup = DONE;
1790 console.info(` ${state.signup} SignUpForApi`);
1795 * @param {Object} opts
1796 * @param {any} opts.dashApi - TODO
1797 * @param {String} opts.defaultAddr
1798 * @param {String} opts.insightBaseUrl
1799 * @param {Array<String>} args
1801 async function acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args) {
1802 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1804 await initCrowdNode(insightBaseUrl);
1805 let hotwallet = CrowdNode.main.hotwallet;
1806 let state = await getCrowdNodeStatus({ addr, hotwallet });
1807 let balanceInfo = await dashApi.getInstantBalance(addr);
1809 if (!state.status?.signup) {
1810 console.info(`${addr} is not signed up yet. Here's the account status:`);
1811 console.info(` ${state.signup} SignUpForApi`);
1812 console.info(` ${state.accept} AcceptTerms`);
1817 if (state.status?.accept) {
1818 console.info(`${addr} is already signed up. Here's the account status:`);
1819 console.info(` ${state.signup} SignUpForApi`);
1820 console.info(` ${state.accept} AcceptTerms`);
1821 console.info(` ${state.deposit} DepositReceived`);
1824 let hasEnough = balanceInfo.balanceSat > acceptOnly + feeEstimate;
1826 await collectSignupFees(insightBaseUrl, addr);
1829 let wif = await maybeReadKeyPaths(name, { wif: true });
1831 console.info("Accepting terms...");
1832 await CrowdNode.accept(wif, hotwallet);
1833 state.accept = DONE;
1834 console.info(` ${state.accept} AcceptTerms`);
1839 * @param {Object} opts
1840 * @param {any} opts.dashApi - TODO
1841 * @param {String} opts.defaultAddr
1842 * @param {String} opts.insightBaseUrl
1843 * @param {Boolean} opts.noReserve
1844 * @param {Array<String>} args
1846 async function depositDash(
1847 { dashApi, defaultAddr, insightBaseUrl, noReserve },
1850 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1851 await initCrowdNode(insightBaseUrl);
1852 let hotwallet = CrowdNode.main.hotwallet;
1853 let state = await getCrowdNodeStatus({ addr, hotwallet });
1854 let balanceInfo = await dashApi.getInstantBalance(addr);
1856 if (!state.status?.accept) {
1857 console.error(`no account for address ${addr}`);
1862 // this would allow for at least 2 withdrawals costing (21000 + 1000)
1863 let reserve = 50000;
1864 let reserveDash = toDash(reserve);
1867 `reserving ${reserve} (Đ${reserveDash}) for withdrawals (--no-reserve to disable)`,
1873 // TODO if unconfirmed, check utxos instead
1875 // deposit what the user asks, or all that we have,
1876 // or all that the user deposits - but at least 2x the reserve
1877 let desiredAmountDash = parseFloat(args.shift() || "0");
1878 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
1879 let effectiveAmount = desiredAmountDuff;
1880 if (!effectiveAmount) {
1881 effectiveAmount = balanceInfo.balanceSat - reserve;
1883 let needed = Math.max(reserve * 2, effectiveAmount + reserve);
1885 if (balanceInfo.balanceSat < needed) {
1887 if (desiredAmountDuff) {
1888 ask = desiredAmountDuff + reserve + -balanceInfo.balanceSat;
1890 await collectDeposit(insightBaseUrl, addr, ask);
1891 balanceInfo = await dashApi.getInstantBalance(addr);
1892 if (balanceInfo.balanceSat < needed) {
1893 let balanceDash = toDash(balanceInfo.balanceSat);
1895 `Balance is still too small: ${balanceInfo.balanceSat} (Đ${balanceDash})`,
1901 if (!desiredAmountDuff) {
1902 effectiveAmount = balanceInfo.balanceSat - reserve;
1905 let effectiveDash = toDash(effectiveAmount);
1907 `Initiating deposit of ${effectiveAmount} (Đ${effectiveDash})...`,
1910 let wif = await maybeReadKeyPaths(name, { wif: true });
1912 await CrowdNode.deposit(wif, hotwallet, effectiveAmount);
1913 state.deposit = DONE;
1914 console.info(` ${state.deposit} DepositReceived`);
1919 * @param {Object} opts
1920 * @param {any} opts.dashApi - TODO
1921 * @param {String} opts.defaultAddr
1922 * @param {String} opts.insightBaseUrl
1923 * @param {Array<String>} args
1925 async function withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args) {
1926 let [addr] = await mustGetAddr({ defaultAddr }, args);
1927 await initCrowdNode(insightBaseUrl);
1928 let hotwallet = CrowdNode.main.hotwallet;
1929 let state = await getCrowdNodeStatus({ addr, hotwallet });
1931 if (!state.status?.accept) {
1932 console.error(`no account for address ${addr}`);
1937 let percentStr = args.shift() || "100.0";
1938 // pass: .1 0.1, 1, 1.0, 10, 10.0, 100, 100.0
1939 // fail: 1000, 10.00
1940 if (!/^1?\d?\d?(\.\d)?$/.test(percentStr)) {
1941 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
1944 let percent = parseFloat(percentStr);
1946 let permil = Math.round(percent * 10);
1947 if (permil <= 0 || permil > 1000) {
1948 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
1952 let realPercentStr = (permil / 10).toFixed(1);
1953 console.info(`Initiating withdrawal of ${realPercentStr}...`);
1955 let wifname = await findWif(addr);
1956 let filepath = Path.join(keysDir, wifname);
1957 let wif = await maybeReadKeyFile(filepath);
1958 let paid = await CrowdNode.withdrawal(wif, hotwallet, permil);
1959 //let paidFloat = (paid.satoshis / DUFFS).toFixed(8);
1960 //let paidInt = paid.satoshis.toString().padStart(9, "0");
1961 console.info(`API Response: ${paid.api}`);
1968 * Convert prefix, addr, keyname, or filepath to pub addr
1969 * @param {String} name
1972 async function wifFileToAddr(name) {
1973 if (34 === name.length) {
1974 // actually payment addr
1980 let wifname = await findWif(name);
1982 let filepath = Path.join(keysDir, wifname);
1983 privKey = await maybeReadKeyFile(filepath);
1986 privKey = await maybeReadKeyFile(name);
1989 throw new Error("bad file path or address");
1992 let pk = new Dashcore.PrivateKey(privKey);
1993 let pub = pk.toPublicKey().toAddress().toString();
1998 * @param {String} insightBaseUrl
1999 * @param {String} addr
2001 async function collectSignupFees(insightBaseUrl, addr) {
2005 let signupTotalDash = toDash(signupTotal);
2006 let signupMsg = `Please send >= ${signupTotal} (Đ${signupTotalDash}) to Sign Up to CrowdNode`;
2007 let msgPad = Math.ceil((qrWidth - signupMsg.length) / 2);
2008 let subMsg = "(plus whatever you'd like to deposit)";
2009 let subMsgPad = Math.ceil((qrWidth - subMsg.length) / 2);
2012 console.info(" ".repeat(msgPad) + signupMsg);
2013 console.info(" ".repeat(subMsgPad) + subMsg);
2017 console.info("(waiting...)");
2019 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
2020 console.info(`Received ${payment.satoshis}`);
2024 * @param {String} insightBaseUrl
2025 * @param {String} addr
2026 * @param {Number} duffAmount
2028 async function collectDeposit(insightBaseUrl, addr, duffAmount) {
2030 showQr(addr, duffAmount);
2032 let depositMsg = `Please send what you wish to deposit to ${addr}`;
2034 let dashAmount = toDash(duffAmount);
2035 depositMsg = `Please deposit ${duffAmount} (Đ${dashAmount}) to ${addr}`;
2038 let msgPad = Math.ceil((qrWidth - depositMsg.length) / 2);
2039 msgPad = Math.max(0, msgPad);
2042 console.info(" ".repeat(msgPad) + depositMsg);
2046 console.info("(waiting...)");
2048 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
2049 console.info(`Received ${payment.satoshis}`);
2053 * @param {Error & { code: String }} err
2056 function emptyStringOnErrEnoent(err) {
2057 if ("ENOENT" !== err.code) {
2064 * @param {Number} duffs - ex: 00000000
2066 function toDash(duffs) {
2067 return (duffs / DUFFS).toFixed(8);
2071 * @param {Number} duffs - ex: 00000000
2073 function toDASH(duffs) {
2074 let dash = (duffs / DUFFS).toFixed(8);
2075 return `Đ` + dash.padStart(12, " ");
2079 * @param {String} dash - ex: 0.00000000
2081 function toDuff(dash) {
2082 return Math.round(parseFloat(dash) * DUFFS);
2087 main().catch(function (err) {
2088 console.error("Fail:");
2089 console.error(err.stack || err);