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`);
52 function showVersion() {
53 console.info(`${pkg.name} v${pkg.version} - ${pkg.description}`);
60 console.info("Quick Start:");
61 // technically this also has [--no-reserve]
62 console.info(" crowdnode stake [addr-or-import-key | --create-new]");
65 console.info("Usage:");
66 console.info(" crowdnode help");
67 console.info(" crowdnode status [keyfile-or-addr]");
68 console.info(" crowdnode signup [keyfile-or-addr]");
69 console.info(" crowdnode accept [keyfile-or-addr]");
71 " crowdnode deposit [keyfile-or-addr] [dash-amount] [--no-reserve]",
74 " crowdnode withdrawal [keyfile-or-addr] <percent> # 1.0-100.0 (steps by 0.1)",
78 console.info("Helpful Extras:");
79 console.info(" crowdnode balance [keyfile-or-addr]"); // addr
80 console.info(" crowdnode load [keyfile-or-addr] [dash-amount]"); // addr
82 " crowdnode transfer <from-keyfile-or-addr> <to-keyfile-or-addr> [dash-amount]",
86 console.info("Key Management & Encryption:");
87 console.info(" crowdnode init");
88 console.info(" crowdnode generate [--plain-text] [./privkey.wif]");
89 console.info(" crowdnode encrypt"); // TODO allow encrypting one-by-one?
90 console.info(" crowdnode list");
91 console.info(" crowdnode use <addr>");
92 console.info(" crowdnode import <keyfile>");
93 //console.info(" crowdnode import <(dash-cli dumpprivkey <addr>)"); // TODO
94 //console.info(" crowdnode export <addr> <keyfile>"); // TODO
95 console.info(" crowdnode passphrase # set or change passphrase");
96 console.info(" crowdnode decrypt"); // TODO allow decrypting one-by-one?
97 console.info(" crowdnode delete <addr>");
100 console.info("CrowdNode HTTP RPC:");
101 console.info(" crowdnode http FundsOpen <addr>");
102 console.info(" crowdnode http VotingOpen <addr>");
103 console.info(" crowdnode http GetFunds <addr>");
104 console.info(" crowdnode http GetFundsFrom <addr> <seconds-since-epoch>");
105 console.info(" crowdnode http GetBalance <addr>");
106 console.info(" crowdnode http GetMessages <addr>");
107 console.info(" crowdnode http IsAddressInUse <addr>");
108 // TODO create signature rather than requiring it
109 console.info(" crowdnode http SetEmail ./privkey.wif <email> <signature>");
110 console.info(" crowdnode http Vote ./privkey.wif <gobject-hash> ");
111 console.info(" <Yes|No|Abstain|Delegate|DoNothing> <signature>");
113 " crowdnode http SetReferral ./privkey.wif <referral-id> <signature>",
120 async function main() {
121 /*jshint maxcomplexity:40 */
122 /*jshint maxstatements:500 */
125 // crowdnode <subcommand> [flags] <privkey> [options]
127 // crowdnode withdrawal ./Xxxxpubaddr.wif 100.0
129 let args = process.argv.slice(2);
132 let forceGenerate = removeItem(args, "--create-new");
133 let forceConfirm = removeItem(args, "--unconfirmed");
134 let plainText = removeItem(args, "--plain-text");
135 let noReserve = removeItem(args, "--no-reserve");
137 let subcommand = args.shift();
139 if (!subcommand || ["--help", "-h", "help"].includes(subcommand)) {
145 if (["--version", "-V", "version"].includes(subcommand)) {
153 // find addr by name or by file or by string
154 await Fs.mkdir(keysDir, {
158 let defaultAddr = await Fs.readFile(defaultWifPath, "utf8").catch(
159 emptyStringOnErrEnoent,
161 defaultAddr = defaultAddr.trim();
164 process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
165 let insightApi = Insight.create({ baseUrl: insightBaseUrl });
166 let dashApi = Dash.create({ insightApi: insightApi });
168 if ("stake" === subcommand) {
184 if ("list" === subcommand) {
185 await listKeys({ dashApi }, args);
190 if ("init" === subcommand) {
191 await initKeystore({ defaultAddr });
196 if ("generate" === subcommand) {
197 await generateKey({ defaultKey: defaultAddr, plainText }, args);
202 if ("passphrase" === subcommand) {
203 await setPassphrase({}, args);
208 if ("import" === subcommand) {
209 let keypath = args.shift() || "";
210 await importKey({ keypath });
215 if ("encrypt" === subcommand) {
216 let addr = args.shift() || "";
218 await encryptAll(null);
223 let keypath = await findWif(addr);
225 console.error(`no managed key matches '${addr}'`);
229 let key = await maybeReadKeyFileRaw(keypath);
231 throw new Error("impossible error");
233 await encryptAll([key]);
238 if ("decrypt" === subcommand) {
239 let addr = args.shift() || "";
241 await decryptAll(null);
242 await Fs.writeFile(shadowPath, NO_SHADOW, "utf8").catch(
243 emptyStringOnErrEnoent,
248 let keypath = await findWif(addr);
250 console.error(`no managed key matches '${addr}'`);
254 let key = await maybeReadKeyFileRaw(keypath);
256 throw new Error("impossible error");
258 await decryptAll([key]);
263 // use or select or default... ?
264 if ("use" === subcommand) {
265 await setDefault(null, args);
270 // helper for debugging
271 if ("transfer" === subcommand) {
272 await transferBalance(
273 { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
281 if ("http" === subcommand) {
282 rpc = args.shift() || "";
289 let [addr] = await mustGetAddr({ defaultAddr }, args);
291 await initCrowdNode(insightBaseUrl);
292 // ex: http <rpc>(<pub>, ...)
294 let hasRpc = rpc in CrowdNode.http;
296 console.error(`Unrecognized rpc command ${rpc}`);
301 //@ts-ignore - TODO use `switch` or make Record Type
302 let result = await CrowdNode.http[rpc].apply(null, args);
304 console.info(`${rpc} ${addr}:`);
305 if ("string" === typeof result) {
306 console.info(result);
308 console.info(JSON.stringify(result, null, 2));
314 if ("load" === subcommand) {
315 await loadAddr({ defaultAddr, insightBaseUrl }, args);
320 // keeping rm for backwards compat
321 if ("rm" === subcommand || "delete" === subcommand) {
322 await initCrowdNode(insightBaseUrl);
323 let [addr, filepath] = await mustGetAddr({ defaultAddr }, args);
324 await removeKey({ addr, dashApi, filepath, insightBaseUrl }, args);
329 if ("balance" === subcommand) {
330 await getBalance({ dashApi, defaultAddr }, args);
335 if ("status" === subcommand) {
336 await getStatus({ dashApi, defaultAddr, insightBaseUrl }, args);
341 if ("signup" === subcommand) {
342 await sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args);
347 if ("accept" === subcommand) {
348 await acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args);
353 if ("deposit" === subcommand) {
355 { dashApi, defaultAddr, insightBaseUrl, noReserve },
362 if ("withdrawal" === subcommand) {
363 await withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args);
368 console.error(`Unrecognized subcommand ${subcommand}`);
375 * @param {Object} opts
376 * @param {any} opts.dashApi - TODO
377 * @param {String} opts.defaultAddr
378 * @param {Boolean} opts.forceGenerate
379 * @param {String} opts.insightBaseUrl
380 * @param {any} opts.insightApi
381 * @param {Boolean} opts.noReserve
382 * @param {Array<String>} args
384 async function stakeDash(
395 let err = await Fs.access(args[0]).catch(Object);
398 let keypath = args.shift() || "";
399 addr = await importKey({ keypath });
400 } else if (forceGenerate) {
401 addr = await generateKey({ defaultKey: defaultAddr }, []);
403 addr = await initKeystore({ defaultAddr });
407 let [_addr] = await mustGetAddr({ defaultAddr }, args);
411 let extra = feeEstimate;
412 console.info("Checking CrowdNode account... ");
413 await CrowdNode.init({
414 baseUrl: "https://app.crowdnode.io",
417 let hotwallet = CrowdNode.main.hotwallet;
418 let state = await getCrowdNodeStatus({ addr, hotwallet });
420 if (!state.status?.accept) {
421 if (!state.status?.signup) {
422 let signUpDeposit = signupOnly + feeEstimate;
424 ` ${TODO} SignUpForApi deposit is ${signupOnly} (+ tx fee)`,
426 extra += signUpDeposit;
428 console.info(` ${DONE} SignUpForApi complete`);
430 let acceptDeposit = acceptOnly + feeEstimate;
431 console.info(` ${TODO} AcceptTerms deposit is ${acceptOnly} (+ tx fee)`);
432 extra += acceptDeposit;
435 let desiredAmountDash = args.shift() || "0.5";
436 let effectiveDuff = toDuff(desiredAmountDash);
437 effectiveDuff += extra;
439 let balanceInfo = await dashApi.getInstantBalance(addr);
440 effectiveDuff -= balanceInfo.balanceSat;
442 if (effectiveDuff > 0) {
443 effectiveDuff = roundDuff(effectiveDuff, 3);
444 let effectiveDash = toDash(effectiveDuff);
445 await plainLoadAddr({
453 if (!state.status?.accept) {
454 if (!state.status?.signup) {
455 await sendSignup({ dashApi, defaultAddr: addr, insightBaseUrl }, []);
457 await acceptTerms({ dashApi, defaultAddr: addr, insightBaseUrl }, []);
461 { dashApi, defaultAddr: addr, insightBaseUrl, noReserve },
467 * @param {Object} opts
468 * @param {String} opts.defaultAddr
470 async function initKeystore({ defaultAddr }) {
471 // if we have no keys, make one
472 let wifnames = await listManagedKeynames();
473 if (!wifnames.length) {
474 return await generateKey({ defaultKey: defaultAddr }, []);
476 // if we have no passphrase, ask about it
477 await initPassphrase();
478 return defaultAddr || wifnames[0];
482 * @param {String} insightBaseUrl
484 async function initCrowdNode(insightBaseUrl) {
485 if (CrowdNode.main.hotwallet) {
488 process.stdout.write("Checking CrowdNode API... ");
489 await CrowdNode.init({
490 baseUrl: "https://app.crowdnode.io",
493 console.info(`(hotwallet ${CrowdNode.main.hotwallet})`);
497 * @param {String} addr - Base58Check pubKeyHash address
498 * @param {Number} duffs - 1/100000000 of a DASH
500 function showQr(addr, duffs = 0) {
501 let dashAmount = toDash(duffs);
502 let dashUri = `dash://${addr}`;
504 dashUri += `?amount=${dashAmount}`;
507 let dashQr = Qr.ascii(dashUri, { indent: 4, size: "mini" });
508 let addrPad = Math.max(0, Math.ceil((qrWidth - dashUri.length) / 2));
510 console.info(dashQr);
512 console.info(" ".repeat(addrPad) + dashUri);
516 * @param {Array<any>} arr
519 function removeItem(arr, item) {
520 let index = arr.indexOf(item);
522 return arr.splice(index, 1)[0];
528 * @param {Object} opts
529 * @param {String} opts.addr
530 * @param {String} opts.hotwallet
532 async function getCrowdNodeStatus({ addr, hotwallet }) {
544 //@ts-ignore - TODO why warnings?
545 let status = await CrowdNode.status(addr, hotwallet);
547 state.status = status;
549 if (state.status?.signup) {
552 if (state.status?.accept) {
555 if (state.status?.deposit) {
556 state.deposit = DONE;
562 * @param {Object} opts
563 * @param {String} opts.addr
564 * @param {any} opts.dashApi - TODO
566 async function checkBalance({ addr, dashApi }) {
567 // deposit if balance is over 100,000 (0.00100000)
568 console.info("Checking balance... ");
569 let balanceInfo = await dashApi.getInstantBalance(addr);
570 let balanceDASH = toDASH(balanceInfo.balanceSat);
572 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
573 if (!crowdNodeBalance.TotalBalance) {
574 crowdNodeBalance.TotalBalance = 0;
575 crowdNodeBalance.TotalDividend = 0;
578 let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance);
579 let crowdNodeDASH = toDASH(crowdNodeDuffNum);
581 let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend);
582 let crowdNodeDASHDiv = toDASH(crowdNodeDivNum);
584 console.info(`Key: ${balanceDASH}`);
585 console.info(`CrowdNode: ${crowdNodeDASH}`);
586 console.info(`Dividends: ${crowdNodeDASHDiv}`);
589 let balanceInfo = await insightApi.getBalance(pub);
590 if (balanceInfo.unconfirmedBalanceSat || balanceInfo.unconfirmedAppearances) {
593 `Error: This address has pending transactions. Please try again in 1-2 minutes or use --unconfirmed.`,
595 console.error(balanceInfo);
596 if ("status" !== subcommand) {
607 * @param {Object} opts
608 * @param {String} opts.defaultAddr
609 * @param {Array<String>} args
610 * @returns {Promise<[String, String]>}
612 async function mustGetAddr({ defaultAddr }, args) {
613 let name = args.shift() ?? "";
614 if (34 === name.length) {
615 // looks like addr already
616 // TODO make function for addr-lookin' check
620 let addr = await maybeReadKeyPaths(name, { wif: false });
622 if (34 === addr.length) {
625 //let pk = new Dashcore.PrivateKey(wif);
626 //let addr = pk.toAddress().toString();
630 let isNum = !isNaN(parseFloat(name));
638 console.error(`could not read '${name}' in ./ or match in ${keysDirRel}/.`);
644 addr = await mustGetDefaultWif(defaultAddr, { wif: false });
646 // TODO we don't need defaultAddr, right? because it could be old?
651 * @param {Object} opts
652 * @param {String} opts.defaultAddr
653 * @param {Array<String>} args
655 async function mustGetWif({ defaultAddr }, args) {
656 let name = args.shift() ?? "";
658 let wif = await maybeReadKeyPaths(name, { wif: true });
663 let isNum = !isNaN(parseFloat(name));
672 `'${name}' does not match a staking key in ./ or ${keysDirRel}/`,
679 wif = await mustGetDefaultWif(defaultAddr);
685 * @param {String} name
686 * @param {Object} opts
687 * @param {Boolean} opts.wif
688 * @returns {Promise<String>} - wif
690 async function maybeReadKeyPaths(name, opts) {
693 // prefix match in .../keys/
694 let wifname = await findWif(name);
699 if (false === opts.wif) {
700 return wifname.slice(0, -".wif".length);
703 let filepath = Path.join(keysDir, wifname);
704 privKey = await maybeReadKeyFile(filepath);
707 privKey = await maybeReadKeyFile(name);
714 * @param {String} defaultAddr
715 * @param {Object} [opts]
716 * @param {Boolean} opts.wif
718 async function mustGetDefaultWif(defaultAddr, opts) {
721 let keyfile = Path.join(keysDir, `${defaultAddr}.wif`);
722 let raw = await maybeReadKeyFileRaw(keyfile, opts);
723 // misnomering wif here a bit
724 defaultWif = raw?.wif || raw?.addr || "";
726 if (defaultWif && !shownDefault) {
728 console.info(`Selected default staking key ${defaultAddr}`);
733 console.error(`Error: no default staking key selected.`);
735 console.error(`Select a different address:`);
736 console.error(` crowdnode list`);
737 console.error(` crowdnode use <addr>`);
739 console.error(`Or create a new staking key:`);
740 console.error(` crowdnode generate`);
749 * @param {Object} psuedoState
750 * @param {String} psuedoState.defaultKey - addr name of default key
751 * @param {Boolean} [psuedoState.plainText] - don't encrypt
752 * @param {Array<String>} args
754 async function generateKey({ defaultKey, plainText }, args) {
755 let name = args.shift();
756 //@ts-ignore - TODO submit JSDoc PR for Dashcore
757 let pk = new Dashcore.PrivateKey();
759 let addr = pk.toAddress().toString();
760 let plainWif = pk.toWIF();
764 wif = await maybeEncrypt(plainWif);
767 let filename = `~/${configdir}/keys/${addr}.wif`;
768 let filepath = Path.join(`${keysDir}/${addr}.wif`);
773 note = `\n(for pubkey address ${addr})`;
774 let err = await Fs.access(filepath).catch(Object);
777 console.info(`'${filepath}' already exists (will not overwrite)`);
783 await Fs.writeFile(filepath, wif, "utf8");
784 if (!name && !defaultKey) {
785 await Fs.writeFile(defaultWifPath, addr, "utf8");
789 console.info(`Generated ${filename} ${note}`);
794 async function initPassphrase() {
795 let needsInit = false;
796 let shadow = await Fs.readFile(shadowPath, "utf8").catch(
797 emptyStringOnErrEnoent,
803 await cmds.getPassphrase({}, []);
808 * @param {Object} state
809 * @param {Boolean} [state._askPreviousPassphrase] - don't ask for passphrase again
810 * @param {Array<String>} args
812 async function setPassphrase({ _askPreviousPassphrase }, args) {
817 let date = getFsDateString();
819 // get the old passphrase
820 if (false !== _askPreviousPassphrase) {
821 // TODO should contain the shadow?
822 await cmds.getPassphrase({ _rotatePassphrase: true }, []);
825 // get the new passphrase
826 let newPassphrase = await promptPassphrase();
827 let curShadow = await Fs.readFile(shadowPath, "utf8").catch(
828 emptyStringOnErrEnoent,
831 let newShadow = await Cipher.shadowPassphrase(newPassphrase);
832 await Fs.writeFile(shadowPath, newShadow, "utf8");
834 let rawKeys = await readAllKeys();
835 let encAddrs = rawKeys
836 .map(function (raw) {
843 // backup all currently encrypted files
845 if (encAddrs.length) {
846 let filepath = Path.join(HOME, `${configdir}/keys.${date}.bak`);
848 console.info(`Backing up previous (encrypted) keys:`);
849 encAddrs.unshift(`SHADOW:${curShadow}`);
850 await Fs.writeFile(filepath, encAddrs.join("\n") + "\n", "utf8");
851 console.info(` ~/${configdir}/keys.${date}.bak`);
854 cmds._setPassphrase(newPassphrase);
856 await encryptAll(rawKeys, { rotateKey: true });
858 result.passphrase = newPassphrase;
859 result.changed = true;
863 async function promptPassphrase() {
866 newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", {
869 newPassphrase = newPassphrase.trim();
871 let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", {
874 _newPassphrase = _newPassphrase.trim();
876 let match = Cipher.secureCompare(newPassphrase, _newPassphrase);
881 console.error("passphrases do not match");
883 return newPassphrase;
888 * @param {Object} opts
889 * @param {String} opts.keypath
891 async function importKey({ keypath }) {
892 let key = await maybeReadKeyFileRaw(keypath);
894 console.error(`no key found for '${keypath}'`);
899 let encWif = await maybeEncrypt(key.wif);
901 if (encWif.includes(":")) {
904 let date = getFsDateString();
907 Path.join(keysDir, `${key.addr}.wif`),
909 Path.join(keysDir, `${key.addr}.${date}.bak`),
912 console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`);
919 * @param {Object} opts
920 * @param {Boolean} [opts._rotatePassphrase]
921 * @param {Boolean} [opts._force]
922 * @param {Array<String>} args
924 cmds.getPassphrase = async function ({ _rotatePassphrase, _force }, args) {
930 if (!_rotatePassphrase) {
931 let cachedphrase = cmds._getPassphrase();
938 // Three possible states:
939 // 1. no shadow file yet (ask to set one)
940 // 2. empty shadow file (initialized, but not set - don't ask to set one)
941 // 3. encrypted shadow file (initialized, requires passphrase)
942 let needsInit = false;
943 let shadow = await Fs.readFile(shadowPath, "utf8").catch(
944 emptyStringOnErrEnoent,
948 } else if (NO_SHADOW === shadow && _force) {
952 // State 1: not initialized, what does the user want?
957 no = await Prompt.prompt(
958 "Would you like to set an encryption passphrase? [Y/n]: ",
962 // Set a passphrase and create shadow file
963 if (!no || ["yes", "y"].includes(no.toLowerCase())) {
964 result = await setPassphrase({ _askPreviousPassphrase: false }, args);
965 cmds._setPassphrase(result.passphrase);
970 if (!["no", "n"].includes(no.toLowerCase())) {
974 // No passphrase, create a NONE shadow file
975 await Fs.writeFile(shadowPath, NO_SHADOW, "utf8");
980 // State 2: shadow already initialized to empty
981 // (user doesn't want a passphrase)
983 cmds._setPassphrase("");
987 // State 3: passphrase & shadow already in use
989 let prompt = `Enter passphrase: `;
990 if (_rotatePassphrase) {
991 prompt = `Enter (current) passphrase: `;
993 result.passphrase = await Prompt.prompt(prompt, {
996 result.passphrase = result.passphrase.trim();
997 if (!result.passphrase || "q" === result.passphrase) {
998 console.error("cancel: no passphrase");
1003 let match = await Cipher.checkPassphrase(result.passphrase, shadow);
1005 cmds._setPassphrase(result.passphrase);
1010 console.error("incorrect passphrase");
1013 throw new Error("SANITY FAIL: unreachable return");
1016 cmds._getPassphrase = function () {
1021 * @param {String} passphrase
1023 cmds._setPassphrase = function (passphrase) {
1024 // Look Ma! A private variable!
1025 cmds._getPassphrase = function () {
1031 * Encrypt ALL-the-things!
1032 * @param {Object} [opts]
1033 * @param {Boolean} opts.rotateKey
1034 * @param {Array<RawKey>?} rawKeys
1036 async function encryptAll(rawKeys, opts) {
1038 rawKeys = await readAllKeys();
1040 let date = getFsDateString();
1042 let passphrase = cmds._getPassphrase();
1044 let result = await cmds.getPassphrase({ _force: true }, []);
1045 if (result.changed) {
1046 // encryptAll was already called on rotation
1049 passphrase = result.passphrase;
1052 console.info(`Encrypting...`);
1054 await rawKeys.reduce(async function (promise, key) {
1057 if (key.encrypted && !opts?.rotateKey) {
1058 console.info(`🙈 ${key.addr} [already encrypted]`);
1061 let encWif = await maybeEncrypt(key.wif, { force: true });
1063 Path.join(keysDir, `${key.addr}.wif`),
1065 Path.join(keysDir, `${key.addr}.${date}.bak`),
1067 console.info(`🔑 ${key.addr}`);
1068 }, Promise.resolve());
1070 console.info(`Done 🔐`);
1075 * Decrypt ALL-the-things!
1076 * @param {Array<RawKey>?} rawKeys
1078 async function decryptAll(rawKeys) {
1080 rawKeys = await readAllKeys();
1082 let date = getFsDateString();
1085 console.info(`Decrypting...`);
1087 await rawKeys.reduce(async function (promise, key) {
1090 if (!key.encrypted) {
1091 console.info(`📖 ${key.addr} [already decrypted]`);
1095 Path.join(keysDir, `${key.addr}.wif`),
1097 Path.join(keysDir, `${key.addr}.${date}.bak`),
1099 console.info(`🔓 ${key.addr}`);
1100 }, Promise.resolve());
1102 console.info(`Done ${DONE}`);
1106 function getFsDateString() {
1107 // YYYY-MM-DD_hh-mm_ss
1108 let date = new Date()
1112 .replace(/\.\d{3}.*/, "");
1117 * @param {String} filepath
1118 * @param {String} wif
1119 * @param {String} bakpath
1121 async function safeSave(filepath, wif, bakpath) {
1122 let tmpPath = `${bakpath}.tmp`;
1123 await Fs.writeFile(tmpPath, wif, "utf8");
1124 let err = await Fs.access(filepath).catch(Object);
1126 await Fs.rename(filepath, bakpath);
1128 await Fs.rename(tmpPath, filepath);
1130 await Fs.unlink(bakpath);
1135 * @typedef {Object} RawKey
1136 * @property {String} addr
1137 * @property {Boolean} encrypted
1138 * @property {String} wif
1144 async function readAllKeys() {
1145 let wifnames = await listManagedKeynames();
1147 /** @type Array<RawKey> */
1149 await wifnames.reduce(async function (promise, wifname) {
1152 let keypath = Path.join(keysDir, wifname);
1153 let key = await maybeReadKeyFileRaw(keypath);
1158 if (`${key.addr}.wif` !== wifname) {
1160 `computed pubkey '${key.addr}' of WIF does not match filename '${keypath}'`,
1165 }, Promise.resolve());
1171 * @param {String} filepath
1172 * @param {Object} [opts]
1173 * @param {Boolean} opts.wif
1174 * @returns {Promise<String>}
1176 async function maybeReadKeyFile(filepath, opts) {
1177 let key = await maybeReadKeyFileRaw(filepath, opts);
1178 if (false === opts?.wif) {
1179 return key?.addr || "";
1181 return key?.wif || "";
1185 * @param {String} filepath
1186 * @param {Object} [opts]
1187 * @param {Boolean} opts.wif
1188 * @returns {Promise<RawKey?>}
1190 async function maybeReadKeyFileRaw(filepath, opts) {
1191 let privKey = await Fs.readFile(filepath, "utf8").catch(
1192 emptyStringOnErrEnoent,
1194 privKey = privKey.trim();
1199 let encrypted = false;
1200 if (privKey.includes(":")) {
1203 if (false !== opts?.wif) {
1204 privKey = await decrypt(privKey);
1208 console.error(err.message);
1209 console.error(`passphrase does not match for key ${filepath}`);
1213 if (false === opts?.wif) {
1215 addr: Path.basename(filepath, ".wif"),
1216 encrypted: encrypted,
1221 let pk = new Dashcore.PrivateKey(privKey);
1222 let pub = pk.toAddress().toString();
1226 encrypted: encrypted,
1232 * @param {String} encWif
1234 async function decrypt(encWif) {
1235 let passphrase = cmds._getPassphrase();
1237 let result = await cmds.getPassphrase({}, []);
1238 passphrase = result.passphrase;
1239 // we don't return just in case they're setting a passphrase to
1240 // decrypt a previously encrypted file (i.e. for recovery from elsewhere)
1242 let key128 = await Cipher.deriveKey(passphrase);
1243 let cipher = Cipher.create(key128);
1245 return cipher.decrypt(encWif);
1248 // tuple example {Promise<[String, Boolean]>}
1250 * @param {Object} [opts]
1251 * @param {Boolean} [opts.force]
1252 * @param {String} plainWif
1254 async function maybeEncrypt(plainWif, opts) {
1255 let passphrase = cmds._getPassphrase();
1257 let result = await cmds.getPassphrase({}, []);
1258 passphrase = result.passphrase;
1262 throw new Error(`no passphrase with which to encrypt file`);
1267 let key128 = await Cipher.deriveKey(passphrase);
1268 let cipher = Cipher.create(key128);
1269 return cipher.encrypt(plainWif);
1274 * @param {Array<String>} args
1276 async function setDefault(_, args) {
1277 let addr = args.shift() || "";
1279 let keyname = await findWif(addr);
1281 console.error(`no key matches '${addr}'`);
1286 let filepath = Path.join(keysDir, keyname);
1287 let wif = await maybeReadKeyFile(filepath);
1288 let pk = new Dashcore.PrivateKey(wif);
1289 let pub = pk.toAddress().toString();
1291 console.info("set", defaultWifPath, pub);
1292 await Fs.writeFile(defaultWifPath, pub, "utf8");
1295 // TODO option to specify config dir
1298 * @param {Object} opts
1299 * @param {any} opts.dashApi - TODO
1300 * @param {Array<String>} args
1302 async function listKeys({ dashApi }, args) {
1303 let wifnames = await listManagedKeynames();
1306 * @type Array<{ node: String, error: Error }>
1310 console.info(`🔑Holdings 🪧 Stakings 💸Earnings`);
1312 console.info(`Staking keys: (in ${keysDirRel}/)`);
1314 if (!wifnames.length) {
1315 console.info(` (none)`);
1317 await wifnames.reduce(async function (promise, wifname) {
1320 let wifpath = Path.join(keysDir, wifname);
1321 let addr = await maybeReadKeyFile(wifpath, { wif: false }).catch(function (
1324 warns.push({ node: wifname, error: err });
1332 let pk = new Dashcore.PrivateKey(wif);
1333 let pub = pk.toAddress().toString();
1334 if (`${pub}.wif` !== wifname) {
1339 `computed pubkey '${pub}' of WIF does not match filename '${wifname}'`,
1346 process.stdout.write(`${addr}: `);
1348 let balanceInfo = await dashApi.getInstantBalance(addr);
1349 let balanceDASH = toDASH(balanceInfo.balanceSat);
1351 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1352 if (!crowdNodeBalance.TotalBalance) {
1353 crowdNodeBalance.TotalBalance = 0;
1354 crowdNodeBalance.TotalDividend = 0;
1356 let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance);
1357 let crowdNodeDASH = toDASH(crowdNodeDuffNum);
1359 let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend);
1360 let crowdNodeDivDASH = toDASH(crowdNodeDivNum);
1361 process.stdout.write(
1362 `${balanceDASH}🔑 ${crowdNodeDASH}🪧 ${crowdNodeDivDASH}💸`,
1366 }, Promise.resolve());
1370 console.warn(`Warnings:`);
1371 warns.forEach(function (warn) {
1372 console.warn(`${warn.node}: ${warn.error.message}`);
1379 * @param {String} name - ex: Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.wif.enc
1381 function isNamedLikeKey(name) {
1382 // TODO distinguish with .enc extension?
1383 let hasGoodLength = 34 + 4 === name.length || 34 + 4 + 4 === name.length;
1384 let knownExt = name.endsWith(".wif") || name.endsWith(".wif.enc");
1385 let isTmp = name.startsWith(".") || name.startsWith("_");
1386 return hasGoodLength && knownExt && !isTmp;
1390 * @param {Object} opts
1391 * @param {any} opts.dashApi - TODO
1392 * @param {String} opts.addr
1393 * @param {String} opts.filepath
1394 * @param {String} opts.insightBaseUrl
1395 * @param {Array<String>} args
1397 async function removeKey({ addr, dashApi, filepath, insightBaseUrl }, args) {
1398 let balanceInfo = await dashApi.getInstantBalance(addr);
1400 let balanceDash = toDash(balanceInfo.balanceSat);
1401 if (balanceInfo.balanceSat) {
1403 console.error(`Error: ${addr}`);
1405 ` still has a balance of ${balanceInfo.balanceSat} (Đ${balanceDash})`,
1407 console.error(` (transfer to another address before deleting)`);
1413 await initCrowdNode(insightBaseUrl);
1414 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1415 if (!crowdNodeBalance) {
1416 // may be janky if not registered
1417 crowdNodeBalance = {};
1419 if (!crowdNodeBalance.TotalBalance) {
1420 crowdNodeBalance.TotalBalance = 0;
1422 let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance);
1423 if (crowdNodeBalance.TotalBalance) {
1425 console.error(`Error: ${addr}`);
1427 ` still staking ${crowdNodeBalance.TotalBalance} (Đ${crowdNodeDash}) on CrowdNode`,
1430 ` (withdrawal 100.0 and transfer to another address before deleting)`,
1437 let wifname = await findWif(addr);
1438 let fullpath = Path.join(keysDir, wifname);
1439 let wif = await maybeReadKeyPaths(filepath, { wif: true });
1441 await Fs.unlink(fullpath).catch(function (err) {
1442 console.error(`could not remove ${filepath}: ${err.message}`);
1446 let wifnames = await listManagedKeynames();
1448 console.info(`No balances found. Removing ${filepath}.`);
1450 console.info(`Backup (just in case):`);
1451 console.info(` ${wif}`);
1453 if (!wifnames.length) {
1454 console.info(`No keys left.`);
1457 let newAddr = wifnames[0];
1458 console.info(`Selected ${newAddr} as new default staking key.`);
1459 await Fs.writeFile(defaultWifPath, addr.replace(".wif", ""), "utf8");
1465 * @param {String} pre
1467 async function findWif(pre) {
1472 let names = await listManagedKeynames();
1473 names = names.filter(function (name) {
1474 return name.startsWith(pre);
1477 if (!names.length) {
1481 if (names.length > 1) {
1482 console.error(`'${pre}' is ambiguous:`, names.join(", "));
1490 async function listManagedKeynames() {
1491 let nodes = await Fs.readdir(keysDir);
1493 return nodes.filter(isNamedLikeKey);
1497 * @param {Object} opts
1498 * @param {String} opts.defaultAddr
1499 * @param {String} opts.insightBaseUrl
1500 * @param {Array<String>} args
1502 async function loadAddr({ defaultAddr, insightBaseUrl }, args) {
1503 let [addr] = await mustGetAddr({ defaultAddr }, args);
1505 let desiredAmountDash = parseFloat(args.shift() || "0");
1506 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
1508 let effectiveDuff = desiredAmountDuff;
1509 let effectiveDash = "";
1510 if (!effectiveDuff) {
1511 effectiveDuff = CrowdNode.stakeMinimum + signupTotal + feeEstimate;
1512 effectiveDuff = roundDuff(effectiveDuff, 3);
1513 effectiveDash = toDash(effectiveDuff);
1516 await plainLoadAddr({ addr, effectiveDash, effectiveDuff, insightBaseUrl });
1522 * 1000 to Round to the nearest mDash
1523 * ex: 0.50238108 => 0.50300000
1524 * @param {Number} effectiveDuff
1525 * @param {Number} numDigits
1527 function roundDuff(effectiveDuff, numDigits) {
1528 let n = Math.pow(10, numDigits);
1529 let effectiveDash = toDash(effectiveDuff);
1530 effectiveDuff = toDuff(
1531 (Math.ceil(parseFloat(effectiveDash) * n) / n).toString(),
1533 return effectiveDuff;
1537 * @param {Object} opts
1538 * @param {String} opts.addr
1539 * @param {String} opts.effectiveDash
1540 * @param {Number} opts.effectiveDuff
1541 * @param {String} opts.insightBaseUrl
1543 async function plainLoadAddr({
1550 showQr(addr, effectiveDuff);
1553 `Use the QR Code above to load ${effectiveDuff} (Đ${effectiveDash}) onto your staking key.`,
1556 console.info(`(waiting...)`);
1558 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
1559 console.info(`Received ${payment.satoshis}`);
1563 * @param {Object} opts
1564 * @param {String} opts.defaultAddr
1565 * @param {any} opts.dashApi - TODO
1566 * @param {Array<String>} args
1568 async function getBalance({ dashApi, defaultAddr }, args) {
1569 let [addr] = await mustGetAddr({ defaultAddr }, args);
1570 await checkBalance({ addr, dashApi });
1571 //let balanceInfo = await checkBalance({ addr, dashApi });
1572 //console.info(balanceInfo);
1577 * @param {Object} opts
1578 * @param {any} opts.dashApi - TODO
1579 * @param {String} opts.defaultAddr
1580 * @param {Boolean} opts.forceConfirm
1581 * @param {String} opts.insightBaseUrl
1582 * @param {any} opts.insightApi
1583 * @param {Array<String>} args
1585 // ex: node ./bin/crowdnode.js transfer ./priv.wif 'pub' 0.01
1586 async function transferBalance(
1587 { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
1590 let wif = await mustGetWif({ defaultAddr }, args);
1592 let keyname = args.shift() || "";
1593 let newAddr = await wifFileToAddr(keyname);
1594 let dashAmount = parseFloat(args.shift() || "0");
1595 let duffAmount = Math.round(dashAmount * DUFFS);
1598 tx = await dashApi.createPayment(wif, newAddr, duffAmount);
1600 tx = await dashApi.createBalanceTransfer(wif, newAddr);
1603 let dashAmountStr = toDash(duffAmount);
1605 `Transferring ${duffAmount} (Đ${dashAmountStr}) to ${newAddr}...`,
1608 console.info(`Transferring balance to ${newAddr}...`);
1610 await insightApi.instantSend(tx);
1611 console.info(`Queued...`);
1612 setTimeout(function () {
1613 // TODO take a cleaner approach
1614 // (waitForVout needs a reasonable timeout)
1615 console.error(`Error: Transfer did not complete.`);
1617 console.error(`(using --unconfirmed may lead to rejected double spends)`);
1621 await Ws.waitForVout(insightBaseUrl, newAddr, 0);
1622 console.info(`Accepted!`);
1627 * @param {Object} opts
1628 * @param {any} opts.dashApi - TODO
1629 * @param {String} opts.defaultAddr
1630 * @param {String} opts.insightBaseUrl
1631 * @param {Array<String>} args
1633 async function getStatus({ dashApi, defaultAddr, insightBaseUrl }, args) {
1634 let [addr] = await mustGetAddr({ defaultAddr }, args);
1635 await initCrowdNode(insightBaseUrl);
1636 let hotwallet = CrowdNode.main.hotwallet;
1637 let state = await getCrowdNodeStatus({ addr, hotwallet });
1640 console.info(`API Actions Complete for ${addr}:`);
1641 console.info(` ${state.signup} SignUpForApi`);
1642 console.info(` ${state.accept} AcceptTerms`);
1643 console.info(` ${state.deposit} DepositReceived`);
1645 let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
1646 // may be unregistered / undefined
1649 * '@odata.context': 'https://app.crowdnode.io/odata/$metadata#Edm.String',
1650 * value: 'Address not found.'
1653 if (!crowdNodeBalance.TotalBalance) {
1654 crowdNodeBalance.TotalBalance = 0;
1656 let crowdNodeDuff = toDuff(crowdNodeBalance.TotalBalance);
1658 `CrowdNode Stake: ${crowdNodeDuff} (Đ${crowdNodeBalance.TotalBalance})`,
1665 * @param {Object} opts
1666 * @param {any} opts.dashApi - TODO
1667 * @param {String} opts.defaultAddr
1668 * @param {String} opts.insightBaseUrl
1669 * @param {Array<String>} args
1671 async function sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args) {
1672 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1673 await initCrowdNode(insightBaseUrl);
1674 let hotwallet = CrowdNode.main.hotwallet;
1675 let state = await getCrowdNodeStatus({ addr, hotwallet });
1676 let balanceInfo = await checkBalance({ addr, dashApi });
1678 if (state.status?.signup) {
1679 console.info(`${addr} is already signed up. Here's the account status:`);
1680 console.info(` ${state.signup} SignUpForApi`);
1681 console.info(` ${state.accept} AcceptTerms`);
1682 console.info(` ${state.deposit} DepositReceived`);
1686 let hasEnough = balanceInfo.balanceSat > signupOnly + feeEstimate;
1688 await collectSignupFees(insightBaseUrl, addr);
1691 let wif = await maybeReadKeyPaths(name, { wif: true });
1693 console.info("Requesting account...");
1694 await CrowdNode.signup(wif, hotwallet);
1695 state.signup = DONE;
1696 console.info(` ${state.signup} SignUpForApi`);
1701 * @param {Object} opts
1702 * @param {any} opts.dashApi - TODO
1703 * @param {String} opts.defaultAddr
1704 * @param {String} opts.insightBaseUrl
1705 * @param {Array<String>} args
1707 async function acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args) {
1708 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1710 await initCrowdNode(insightBaseUrl);
1711 let hotwallet = CrowdNode.main.hotwallet;
1712 let state = await getCrowdNodeStatus({ addr, hotwallet });
1713 let balanceInfo = await dashApi.getInstantBalance(addr);
1715 if (!state.status?.signup) {
1716 console.info(`${addr} is not signed up yet. Here's the account status:`);
1717 console.info(` ${state.signup} SignUpForApi`);
1718 console.info(` ${state.accept} AcceptTerms`);
1723 if (state.status?.accept) {
1724 console.info(`${addr} is already signed up. Here's the account status:`);
1725 console.info(` ${state.signup} SignUpForApi`);
1726 console.info(` ${state.accept} AcceptTerms`);
1727 console.info(` ${state.deposit} DepositReceived`);
1730 let hasEnough = balanceInfo.balanceSat > acceptOnly + feeEstimate;
1732 await collectSignupFees(insightBaseUrl, addr);
1735 let wif = await maybeReadKeyPaths(name, { wif: true });
1737 console.info("Accepting terms...");
1738 await CrowdNode.accept(wif, hotwallet);
1739 state.accept = DONE;
1740 console.info(` ${state.accept} AcceptTerms`);
1745 * @param {Object} opts
1746 * @param {any} opts.dashApi - TODO
1747 * @param {String} opts.defaultAddr
1748 * @param {String} opts.insightBaseUrl
1749 * @param {Boolean} opts.noReserve
1750 * @param {Array<String>} args
1752 async function depositDash(
1753 { dashApi, defaultAddr, insightBaseUrl, noReserve },
1756 let [addr, name] = await mustGetAddr({ defaultAddr }, args);
1757 await initCrowdNode(insightBaseUrl);
1758 let hotwallet = CrowdNode.main.hotwallet;
1759 let state = await getCrowdNodeStatus({ addr, hotwallet });
1760 let balanceInfo = await dashApi.getInstantBalance(addr);
1762 if (!state.status?.accept) {
1763 console.error(`no account for address ${addr}`);
1768 // this would allow for at least 2 withdrawals costing (21000 + 1000)
1769 let reserve = 50000;
1770 let reserveDash = toDash(reserve);
1773 `reserving ${reserve} (Đ${reserveDash}) for withdrawals (--no-reserve to disable)`,
1779 // TODO if unconfirmed, check utxos instead
1781 // deposit what the user asks, or all that we have,
1782 // or all that the user deposits - but at least 2x the reserve
1783 let desiredAmountDash = parseFloat(args.shift() || "0");
1784 let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
1785 let effectiveAmount = desiredAmountDuff;
1786 if (!effectiveAmount) {
1787 effectiveAmount = balanceInfo.balanceSat - reserve;
1789 let needed = Math.max(reserve * 2, effectiveAmount + reserve);
1791 if (balanceInfo.balanceSat < needed) {
1793 if (desiredAmountDuff) {
1794 ask = desiredAmountDuff + reserve + -balanceInfo.balanceSat;
1796 await collectDeposit(insightBaseUrl, addr, ask);
1797 balanceInfo = await dashApi.getInstantBalance(addr);
1798 if (balanceInfo.balanceSat < needed) {
1799 let balanceDash = toDash(balanceInfo.balanceSat);
1801 `Balance is still too small: ${balanceInfo.balanceSat} (Đ${balanceDash})`,
1807 if (!desiredAmountDuff) {
1808 effectiveAmount = balanceInfo.balanceSat - reserve;
1811 let effectiveDash = toDash(effectiveAmount);
1813 `Initiating deposit of ${effectiveAmount} (Đ${effectiveDash})...`,
1816 let wif = await maybeReadKeyPaths(name, { wif: true });
1818 await CrowdNode.deposit(wif, hotwallet, effectiveAmount);
1819 state.deposit = DONE;
1820 console.info(` ${state.deposit} DepositReceived`);
1825 * @param {Object} opts
1826 * @param {any} opts.dashApi - TODO
1827 * @param {String} opts.defaultAddr
1828 * @param {String} opts.insightBaseUrl
1829 * @param {Array<String>} args
1831 async function withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args) {
1832 let [addr] = await mustGetAddr({ defaultAddr }, args);
1833 await initCrowdNode(insightBaseUrl);
1834 let hotwallet = CrowdNode.main.hotwallet;
1835 let state = await getCrowdNodeStatus({ addr, hotwallet });
1837 if (!state.status?.accept) {
1838 console.error(`no account for address ${addr}`);
1843 let percentStr = args.shift() || "100.0";
1844 // pass: .1 0.1, 1, 1.0, 10, 10.0, 100, 100.0
1845 // fail: 1000, 10.00
1846 if (!/^1?\d?\d?(\.\d)?$/.test(percentStr)) {
1847 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
1850 let percent = parseFloat(percentStr);
1852 let permil = Math.round(percent * 10);
1853 if (permil <= 0 || permil > 1000) {
1854 console.error("Error: withdrawal percent must be between 0.1 and 100.0");
1858 let realPercentStr = (permil / 10).toFixed(1);
1859 console.info(`Initiating withdrawal of ${realPercentStr}...`);
1861 let wifname = await findWif(addr);
1862 let filepath = Path.join(keysDir, wifname);
1863 let wif = await maybeReadKeyFile(filepath);
1864 let paid = await CrowdNode.withdrawal(wif, hotwallet, permil);
1865 //let paidFloat = (paid.satoshis / DUFFS).toFixed(8);
1866 //let paidInt = paid.satoshis.toString().padStart(9, "0");
1867 console.info(`API Response: ${paid.api}`);
1874 * Convert prefix, addr, keyname, or filepath to pub addr
1875 * @param {String} name
1878 async function wifFileToAddr(name) {
1879 if (34 === name.length) {
1880 // actually payment addr
1886 let wifname = await findWif(name);
1888 let filepath = Path.join(keysDir, wifname);
1889 privKey = await maybeReadKeyFile(filepath);
1892 privKey = await maybeReadKeyFile(name);
1895 throw new Error("bad file path or address");
1898 let pk = new Dashcore.PrivateKey(privKey);
1899 let pub = pk.toPublicKey().toAddress().toString();
1904 * @param {String} insightBaseUrl
1905 * @param {String} addr
1907 async function collectSignupFees(insightBaseUrl, addr) {
1911 let signupTotalDash = toDash(signupTotal);
1912 let signupMsg = `Please send >= ${signupTotal} (Đ${signupTotalDash}) to Sign Up to CrowdNode`;
1913 let msgPad = Math.ceil((qrWidth - signupMsg.length) / 2);
1914 let subMsg = "(plus whatever you'd like to deposit)";
1915 let subMsgPad = Math.ceil((qrWidth - subMsg.length) / 2);
1918 console.info(" ".repeat(msgPad) + signupMsg);
1919 console.info(" ".repeat(subMsgPad) + subMsg);
1923 console.info("(waiting...)");
1925 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
1926 console.info(`Received ${payment.satoshis}`);
1930 * @param {String} insightBaseUrl
1931 * @param {String} addr
1932 * @param {Number} duffAmount
1934 async function collectDeposit(insightBaseUrl, addr, duffAmount) {
1936 showQr(addr, duffAmount);
1938 let depositMsg = `Please send what you wish to deposit to ${addr}`;
1940 let dashAmount = toDash(duffAmount);
1941 depositMsg = `Please deposit ${duffAmount} (Đ${dashAmount}) to ${addr}`;
1944 let msgPad = Math.ceil((qrWidth - depositMsg.length) / 2);
1945 msgPad = Math.max(0, msgPad);
1948 console.info(" ".repeat(msgPad) + depositMsg);
1952 console.info("(waiting...)");
1954 let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
1955 console.info(`Received ${payment.satoshis}`);
1959 * @param {Error & { code: String }} err
1962 function emptyStringOnErrEnoent(err) {
1963 if ("ENOENT" !== err.code) {
1970 * @param {Number} duffs - ex: 00000000
1972 function toDash(duffs) {
1973 return (duffs / DUFFS).toFixed(8);
1977 * @param {Number} duffs - ex: 00000000
1979 function toDASH(duffs) {
1980 let dash = (duffs / DUFFS).toFixed(8);
1981 return `Đ${dash}`.padStart(13, " ");
1985 * @param {String} dash - ex: 0.00000000
1987 function toDuff(dash) {
1988 return Math.round(parseFloat(dash) * DUFFS);
1993 main().catch(function (err) {
1994 console.error("Fail:");
1995 console.error(err.stack || err);