X-Git-Url: https://git.josue.xyz/?p=crowdnode.js%2F.git;a=blobdiff_plain;f=bin%2Fcrowdnode.js;h=adb6306402070cf5a6c2f3637e55cee0db490aae;hp=8b8ce396ff0be8d4a1ec0bc843052c1bf42e75e4;hb=5dbccedf943f0f768d80a9da1bffff061a5cef3e;hpb=2e6154a9535583dba6f1589eaefc36fab70a58da diff --git a/bin/crowdnode.js b/bin/crowdnode.js index 8b8ce39..adb6306 100755 --- a/bin/crowdnode.js +++ b/bin/crowdnode.js @@ -23,8 +23,13 @@ let Ws = require("../lib/ws.js"); let Dashcore = require("@dashevo/dashcore-lib"); +const DONE = "✅"; +const TODO = "ℹ️"; +const NO_SHADOW = "NONE"; const DUFFS = 100000000; -let qrWidth = 2 + 67 + 2; + +let shownDefault = false; +let qrWidth = 2 + 33 + 2; // Sign Up Fees: // 0.00236608 // required for signup // 0.00002000 // TX fee estimate @@ -44,6 +49,11 @@ let keysDirRel = `~/${configdir}/keys`; let shadowPath = Path.join(HOME, `${configdir}/shadow`); let defaultWifPath = Path.join(HOME, `${configdir}/default`); +function debug() { + //@ts-ignore + console.error.apply(console, arguments); +} + function showVersion() { console.info(`${pkg.name} v${pkg.version} - ${pkg.description}`); console.info(); @@ -52,6 +62,11 @@ function showVersion() { function showHelp() { showVersion(); + console.info("Quick Start:"); + // technically this also has [--no-reserve] + console.info(" crowdnode stake [addr-or-import-key | --create-new]"); + + console.info(""); console.info("Usage:"); console.info(" crowdnode help"); console.info(" crowdnode status [keyfile-or-addr]"); @@ -74,6 +89,7 @@ function showHelp() { console.info(""); console.info("Key Management & Encryption:"); + console.info(" crowdnode init"); console.info(" crowdnode generate [--plain-text] [./privkey.wif]"); console.info(" crowdnode encrypt"); // TODO allow encrypting one-by-one? console.info(" crowdnode list"); @@ -102,6 +118,19 @@ function showHelp() { " crowdnode http SetReferral ./privkey.wif ", ); console.info(""); + console.info("Official CrowdNode Resources"); + console.info(""); + console.info("Homepage:"); + console.info(" https://crowdnode.io/"); + console.info(""); + console.info("Terms of Service:"); + console.info(" https://crowdnode.io/terms/"); + console.info(""); + console.info("BlockChain API Guide:"); + console.info( + " https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide", + ); + console.info(""); } let cmds = {}; @@ -118,6 +147,7 @@ async function main() { let args = process.argv.slice(2); // flags + let forceGenerate = removeItem(args, "--create-new"); let forceConfirm = removeItem(args, "--unconfirmed"); let plainText = removeItem(args, "--plain-text"); let noReserve = removeItem(args, "--no-reserve"); @@ -153,30 +183,58 @@ async function main() { let insightApi = Insight.create({ baseUrl: insightBaseUrl }); let dashApi = Dash.create({ insightApi: insightApi }); + if ("stake" === subcommand) { + await stakeDash( + { + dashApi, + insightApi, + insightBaseUrl, + defaultAddr, + forceGenerate, + noReserve, + }, + args, + ); + process.exit(0); + return; + } + if ("list" === subcommand) { - await listKeys({ dashApi }, args); + await listKeys({ dashApi, defaultAddr }, args); + process.exit(0); + return; + } + + if ("init" === subcommand) { + await initKeystore({ defaultAddr }); + process.exit(0); return; } if ("generate" === subcommand) { await generateKey({ defaultKey: defaultAddr, plainText }, args); + process.exit(0); return; } if ("passphrase" === subcommand) { await setPassphrase({}, args); + process.exit(0); return; } if ("import" === subcommand) { - importKey(null, args); + let keypath = args.shift() || ""; + await importKey({ keypath }); + process.exit(0); return; } if ("encrypt" === subcommand) { let addr = args.shift() || ""; if (!addr) { - encryptAll(null); + await encryptAll(null); + process.exit(0); return; } @@ -190,14 +248,19 @@ async function main() { if (!key) { throw new Error("impossible error"); } - encryptAll([key]); + await encryptAll([key]); + process.exit(0); return; } if ("decrypt" === subcommand) { let addr = args.shift() || ""; if (!addr) { - decryptAll(null); + await decryptAll(null); + await Fs.writeFile(shadowPath, NO_SHADOW, "utf8").catch( + emptyStringOnErrEnoent, + ); + process.exit(0); return; } let keypath = await findWif(addr); @@ -210,13 +273,15 @@ async function main() { if (!key) { throw new Error("impossible error"); } - decryptAll([key]); + await decryptAll([key]); + process.exit(0); return; } // use or select or default... ? if ("use" === subcommand) { await setDefault(null, args); + process.exit(0); return; } @@ -226,6 +291,7 @@ async function main() { { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi }, args, ); + process.exit(0); return; } @@ -259,39 +325,52 @@ async function main() { } else { console.info(JSON.stringify(result, null, 2)); } + process.exit(0); return; } if ("load" === subcommand) { await loadAddr({ defaultAddr, insightBaseUrl }, args); + process.exit(0); return; } // keeping rm for backwards compat if ("rm" === subcommand || "delete" === subcommand) { await initCrowdNode(insightBaseUrl); - await removeKey({ defaultAddr, dashApi, insightBaseUrl }, args); + let [addr, filepath] = await mustGetAddr({ defaultAddr }, args); + await removeKey({ addr, dashApi, filepath, insightBaseUrl }, args); + process.exit(0); return; } if ("balance" === subcommand) { - await getBalance({ dashApi, defaultAddr }, args); + if (args.length) { + await getBalance({ dashApi, defaultAddr }, args); + process.exit(0); + return; + } + + await getAllBalances({ dashApi, defaultAddr }, args); process.exit(0); return; } if ("status" === subcommand) { await getStatus({ dashApi, defaultAddr, insightBaseUrl }, args); + process.exit(0); return; } if ("signup" === subcommand) { await sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args); + process.exit(0); return; } if ("accept" === subcommand) { await acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args); + process.exit(0); return; } @@ -300,11 +379,13 @@ async function main() { { dashApi, defaultAddr, insightBaseUrl, noReserve }, args, ); + process.exit(0); return; } if ("withdrawal" === subcommand) { await withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args); + process.exit(0); return; } @@ -314,10 +395,120 @@ async function main() { process.exit(1); } +/** + * @param {Object} opts + * @param {any} opts.dashApi - TODO + * @param {String} opts.defaultAddr + * @param {Boolean} opts.forceGenerate + * @param {String} opts.insightBaseUrl + * @param {any} opts.insightApi + * @param {Boolean} opts.noReserve + * @param {Array} args + */ +async function stakeDash( + { + dashApi, + defaultAddr, + forceGenerate, + insightApi, + insightBaseUrl, + noReserve, + }, + args, +) { + let err = await Fs.access(args[0]).catch(Object); + let addr; + if (!err) { + let keypath = args.shift() || ""; + addr = await importKey({ keypath }); + } else if (forceGenerate) { + addr = await generateKey({ defaultKey: defaultAddr }, []); + } else { + addr = await initKeystore({ defaultAddr }); + } + + if (!addr) { + let [_addr] = await mustGetAddr({ defaultAddr }, args); + addr = _addr; + } + + let extra = feeEstimate; + console.info("Checking CrowdNode account... "); + await CrowdNode.init({ + baseUrl: "https://app.crowdnode.io", + insightBaseUrl, + }); + let hotwallet = CrowdNode.main.hotwallet; + let state = await getCrowdNodeStatus({ addr, hotwallet }); + + if (!state.status?.accept) { + if (!state.status?.signup) { + let signUpDeposit = signupOnly + feeEstimate; + console.info( + ` ${TODO} SignUpForApi deposit is ${signupOnly} (+ tx fee)`, + ); + extra += signUpDeposit; + } else { + console.info(` ${DONE} SignUpForApi complete`); + } + let acceptDeposit = acceptOnly + feeEstimate; + console.info(` ${TODO} AcceptTerms deposit is ${acceptOnly} (+ tx fee)`); + extra += acceptDeposit; + } + + let desiredAmountDash = args.shift() || "0.5"; + let effectiveDuff = toDuff(desiredAmountDash); + effectiveDuff += extra; + + let balanceInfo = await dashApi.getInstantBalance(addr); + effectiveDuff -= balanceInfo.balanceSat; + + if (effectiveDuff > 0) { + effectiveDuff = roundDuff(effectiveDuff, 3); + let effectiveDash = toDash(effectiveDuff); + await plainLoadAddr({ + addr, + effectiveDash, + effectiveDuff, + insightBaseUrl, + }); + } + + if (!state.status?.accept) { + if (!state.status?.signup) { + await sendSignup({ dashApi, defaultAddr: addr, insightBaseUrl }, [addr]); + } + await acceptTerms({ dashApi, defaultAddr: addr, insightBaseUrl }, [addr]); + } + + await depositDash( + { dashApi, defaultAddr: addr, insightBaseUrl, noReserve }, + [addr].concat(args), + ); +} + +/** + * @param {Object} opts + * @param {String} opts.defaultAddr + */ +async function initKeystore({ defaultAddr }) { + // if we have no keys, make one + let wifnames = await listManagedKeynames(); + if (!wifnames.length) { + return await generateKey({ defaultKey: defaultAddr }, []); + } + // if we have no passphrase, ask about it + await initPassphrase(); + return defaultAddr || wifnames[0]; +} + /** * @param {String} insightBaseUrl */ async function initCrowdNode(insightBaseUrl) { + if (CrowdNode.main.hotwallet) { + return; + } process.stdout.write("Checking CrowdNode API... "); await CrowdNode.init({ baseUrl: "https://app.crowdnode.io", @@ -337,8 +528,8 @@ function showQr(addr, duffs = 0) { dashUri += `?amount=${dashAmount}`; } - let dashQr = Qr.ascii(dashUri, { indent: 4 }); - let addrPad = Math.ceil((qrWidth - dashUri.length) / 2); + let dashQr = Qr.ascii(dashUri, { indent: 4, size: "mini" }); + let addrPad = Math.max(0, Math.ceil((qrWidth - dashUri.length) / 2)); console.info(dashQr); console.info(); @@ -364,9 +555,9 @@ function removeItem(arr, item) { */ async function getCrowdNodeStatus({ addr, hotwallet }) { let state = { - signup: "❌", - accept: "❌", - deposit: "❌", + signup: TODO, + accept: TODO, + deposit: TODO, status: { signup: 0, accept: 0, @@ -375,15 +566,18 @@ async function getCrowdNodeStatus({ addr, hotwallet }) { }; //@ts-ignore - TODO why warnings? - state.status = await CrowdNode.status(addr, hotwallet); + let status = await CrowdNode.status(addr, hotwallet); + if (status) { + state.status = status; + } if (state.status?.signup) { - state.signup = "✅"; + state.signup = DONE; } if (state.status?.accept) { - state.accept = "✅"; + state.accept = DONE; } if (state.status?.deposit) { - state.deposit = "✅"; + state.deposit = DONE; } return state; } @@ -395,10 +589,26 @@ async function getCrowdNodeStatus({ addr, hotwallet }) { */ async function checkBalance({ addr, dashApi }) { // deposit if balance is over 100,000 (0.00100000) - process.stdout.write("Checking balance... "); + console.info("Checking balance... "); let balanceInfo = await dashApi.getInstantBalance(addr); - let balanceDash = toDash(balanceInfo.balanceSat); - console.info(`${balanceInfo.balanceSat} (Đ${balanceDash})`); + let balanceDASH = toDASH(balanceInfo.balanceSat); + + let crowdNodeBalance = await CrowdNode.http.GetBalance(addr); + if (!crowdNodeBalance.TotalBalance) { + crowdNodeBalance.TotalBalance = 0; + crowdNodeBalance.TotalDividend = 0; + } + + let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance); + let crowdNodeDASH = toDASH(crowdNodeDuffNum); + + let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend); + let crowdNodeDASHDiv = toDASH(crowdNodeDivNum); + + console.info(`Key: ${balanceDASH}`); + console.info(`CrowdNode: ${crowdNodeDASH}`); + console.info(`Dividends: ${crowdNodeDASHDiv}`); + console.info(); /* let balanceInfo = await insightApi.getBalance(pub); if (balanceInfo.unconfirmedBalanceSat || balanceInfo.unconfirmedAppearances) { @@ -537,8 +747,9 @@ async function mustGetDefaultWif(defaultAddr, opts) { // misnomering wif here a bit defaultWif = raw?.wif || raw?.addr || ""; } - if (defaultWif) { - console.info(`selected default staking key ${defaultAddr}`); + if (defaultWif && !shownDefault) { + shownDefault = true; + debug(`Selected default staking key ${defaultAddr}`); return defaultWif; } @@ -561,7 +772,7 @@ async function mustGetDefaultWif(defaultAddr, opts) { /** * @param {Object} psuedoState * @param {String} psuedoState.defaultKey - addr name of default key - * @param {Boolean} psuedoState.plainText - don't encrypt + * @param {Boolean} [psuedoState.plainText] - don't encrypt * @param {Array} args */ async function generateKey({ defaultKey, plainText }, args) { @@ -586,6 +797,7 @@ async function generateKey({ defaultKey, plainText }, args) { note = `\n(for pubkey address ${addr})`; let err = await Fs.access(filepath).catch(Object); if (!err) { + // TODO console.info(`'${filepath}' already exists (will not overwrite)`); process.exit(0); return; @@ -600,8 +812,20 @@ async function generateKey({ defaultKey, plainText }, args) { console.info(``); console.info(`Generated ${filename} ${note}`); console.info(``); - process.exit(0); - return; + return addr; +} + +async function initPassphrase() { + let needsInit = false; + let shadow = await Fs.readFile(shadowPath, "utf8").catch( + emptyStringOnErrEnoent, + ); + if (!shadow) { + needsInit = true; + } + if (needsInit) { + await cmds.getPassphrase({}, []); + } } /** @@ -610,33 +834,20 @@ async function generateKey({ defaultKey, plainText }, args) { * @param {Array} args */ async function setPassphrase({ _askPreviousPassphrase }, args) { + let result = { + passphrase: "", + changed: false, + }; let date = getFsDateString(); // get the old passphrase if (false !== _askPreviousPassphrase) { - await cmds.getPassphrase(null, []); + // TODO should contain the shadow? + await cmds.getPassphrase({ _rotatePassphrase: true }, []); } // get the new passphrase - let newPassphrase; - for (;;) { - newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", { - mask: true, - }); - newPassphrase = newPassphrase.trim(); - - let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", { - mask: true, - }); - _newPassphrase = _newPassphrase.trim(); - - let match = Cipher.secureCompare(newPassphrase, _newPassphrase); - if (match) { - break; - } - - console.error("passphrases do not match"); - } + let newPassphrase = await promptPassphrase(); let curShadow = await Fs.readFile(shadowPath, "utf8").catch( emptyStringOnErrEnoent, ); @@ -659,7 +870,7 @@ async function setPassphrase({ _askPreviousPassphrase }, args) { let filepath = Path.join(HOME, `${configdir}/keys.${date}.bak`); console.info(``); console.info(`Backing up previous (encrypted) keys:`); - encAddrs.unshift(curShadow); + encAddrs.unshift(`SHADOW:${curShadow}`); await Fs.writeFile(filepath, encAddrs.join("\n") + "\n", "utf8"); console.info(` ~/${configdir}/keys.${date}.bak`); console.info(``); @@ -668,16 +879,40 @@ async function setPassphrase({ _askPreviousPassphrase }, args) { await encryptAll(rawKeys, { rotateKey: true }); + result.passphrase = newPassphrase; + result.changed = true; + return result; +} + +async function promptPassphrase() { + let newPassphrase; + for (;;) { + newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", { + mask: true, + }); + newPassphrase = newPassphrase.trim(); + + let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", { + mask: true, + }); + _newPassphrase = _newPassphrase.trim(); + + let match = Cipher.secureCompare(newPassphrase, _newPassphrase); + if (match) { + break; + } + + console.error("passphrases do not match"); + } return newPassphrase; } /** * Import and Encrypt - * @param {Null} _ - * @param {Array} args + * @param {Object} opts + * @param {String} opts.keypath */ -async function importKey(_, args) { - let keypath = args.shift() || ""; +async function importKey({ keypath }) { let key = await maybeReadKeyFileRaw(keypath); if (!key?.wif) { console.error(`no key found for '${keypath}'`); @@ -700,8 +935,122 @@ async function importKey(_, args) { console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`); console.info(``); + + return key.addr; } +/** + * @param {Object} opts + * @param {Boolean} [opts._rotatePassphrase] + * @param {Boolean} [opts._force] + * @param {Array} args + */ +cmds.getPassphrase = async function ({ _rotatePassphrase, _force }, args) { + let result = { + passphrase: "", + changed: false, + }; + /* + if (!_rotatePassphrase) { + let cachedphrase = cmds._getPassphrase(); + if (cachedphrase) { + return cachedphrase; + } + } + */ + + // Three possible states: + // 1. no shadow file yet (ask to set one) + // 2. empty shadow file (initialized, but not set - don't ask to set one) + // 3. encrypted shadow file (initialized, requires passphrase) + let needsInit = false; + let shadow = await Fs.readFile(shadowPath, "utf8").catch( + emptyStringOnErrEnoent, + ); + if (!shadow) { + needsInit = true; + } else if (NO_SHADOW === shadow && _force) { + needsInit = true; + } + + // State 1: not initialized, what does the user want? + if (needsInit) { + for (;;) { + let no; + if (!_force) { + no = await Prompt.prompt( + "Would you like to set an encryption passphrase? [Y/n]: ", + ); + } + + // Set a passphrase and create shadow file + if (!no || ["yes", "y"].includes(no.toLowerCase())) { + result = await setPassphrase({ _askPreviousPassphrase: false }, args); + cmds._setPassphrase(result.passphrase); + return result; + } + + // ask user again + if (!["no", "n"].includes(no.toLowerCase())) { + continue; + } + + // No passphrase, create a NONE shadow file + await Fs.writeFile(shadowPath, NO_SHADOW, "utf8"); + return result; + } + } + + // State 2: shadow already initialized to empty + // (user doesn't want a passphrase) + if (!shadow) { + cmds._setPassphrase(""); + return result; + } + + // State 3: passphrase & shadow already in use + for (;;) { + let prompt = `Enter passphrase: `; + if (_rotatePassphrase) { + prompt = `Enter (current) passphrase: `; + } + result.passphrase = await Prompt.prompt(prompt, { + mask: true, + }); + result.passphrase = result.passphrase.trim(); + if (!result.passphrase || "q" === result.passphrase) { + console.error("cancel: no passphrase"); + process.exit(1); + return result; + } + + let match = await Cipher.checkPassphrase(result.passphrase, shadow); + if (match) { + cmds._setPassphrase(result.passphrase); + console.info(``); + return result; + } + + console.error("incorrect passphrase"); + } + + throw new Error("SANITY FAIL: unreachable return"); +}; + +cmds._getPassphrase = function () { + return ""; +}; + +/** + * @param {String} passphrase + */ +cmds._setPassphrase = function (passphrase) { + // Look Ma! A private variable! + cmds._getPassphrase = function () { + return passphrase; + }; +}; + /** * Encrypt ALL-the-things! * @param {Object} [opts] @@ -714,6 +1063,16 @@ async function encryptAll(rawKeys, opts) { } let date = getFsDateString(); + let passphrase = cmds._getPassphrase(); + if (!passphrase) { + let result = await cmds.getPassphrase({ _force: true }, []); + if (result.changed) { + // encryptAll was already called on rotation + return; + } + passphrase = result.passphrase; + } + console.info(`Encrypting...`); console.info(``); await rawKeys.reduce(async function (promise, key) { @@ -723,7 +1082,7 @@ async function encryptAll(rawKeys, opts) { console.info(`🙈 ${key.addr} [already encrypted]`); return; } - let encWif = await maybeEncrypt(key.wif); + let encWif = await maybeEncrypt(key.wif, { force: true }); await safeSave( Path.join(keysDir, `${key.addr}.wif`), encWif, @@ -764,7 +1123,7 @@ async function decryptAll(rawKeys) { console.info(`🔓 ${key.addr}`); }, Promise.resolve()); console.info(``); - console.info(`Done ✅`); + console.info(`Done ${DONE}`); console.info(``); } @@ -796,98 +1155,6 @@ async function safeSave(filepath, wif, bakpath) { } } -/** - * @param {Null} psuedoState - * @param {Array} args - */ -cmds.getPassphrase = async function (psuedoState, args) { - // Three possible states: - // 1. no shadow file yet (ask to set one) - // 2. empty shadow file (initialized, but not set - don't ask to set one) - // 3. encrypted shadow file (initialized, requires passphrase) - let needsInit = false; - let shadow = await Fs.readFile(shadowPath, "utf8").catch(function (err) { - if ("ENOENT" === err.code) { - needsInit = true; - return; - } - throw err; - }); - - // State 1: not initialized, what does the user want? - if (needsInit) { - for (;;) { - let no = await Prompt.prompt( - "Would you like to set an encryption passphrase? [Y/n]: ", - ); - - // Set a passphrase and create shadow file - if (!no || ["yes", "y"].includes(no.toLowerCase())) { - let passphrase = await setPassphrase( - { _askPreviousPassphrase: false }, - args, - ); - cmds._setPassphrase(passphrase); - return passphrase; - } - - // ask user again - if (!["no", "n"].includes(no.toLowerCase())) { - continue; - } - - // No passphrase, create empty shadow file - await Fs.writeFile(shadowPath, "", "utf8"); - return ""; - } - } - - // State 2: shadow already initialized to empty - // (user doesn't want a passphrase) - if (!shadow) { - cmds._setPassphrase(""); - return ""; - } - - // State 3: passphrase & shadow already in use - for (;;) { - let passphrase = await Prompt.prompt("Enter (current) passphrase: ", { - mask: true, - }); - passphrase = passphrase.trim(); - if (!passphrase || "q" === passphrase) { - console.error("cancel: no passphrase"); - process.exit(1); - return; - } - - let match = await Cipher.checkPassphrase(passphrase, shadow); - if (match) { - cmds._setPassphrase(passphrase); - console.info(``); - return passphrase; - } - - console.error("incorrect passphrase"); - } - - throw new Error("SANITY FAIL: unreachable return"); -}; - -cmds._getPassphrase = function () { - return ""; -}; - -/** - * @param {String} passphrase - */ -cmds._setPassphrase = function (passphrase) { - // Look Ma! A private variable! - cmds._getPassphrase = function () { - return passphrase; - }; -}; - /** * @typedef {Object} RawKey * @property {String} addr @@ -991,7 +1258,10 @@ async function maybeReadKeyFileRaw(filepath, opts) { async function decrypt(encWif) { let passphrase = cmds._getPassphrase(); if (!passphrase) { - passphrase = await cmds.getPassphrase(null, []); + let result = await cmds.getPassphrase({}, []); + passphrase = result.passphrase; + // we don't return just in case they're setting a passphrase to + // decrypt a previously encrypted file (i.e. for recovery from elsewhere) } let key128 = await Cipher.deriveKey(passphrase); let cipher = Cipher.create(key128); @@ -999,15 +1269,22 @@ async function decrypt(encWif) { return cipher.decrypt(encWif); } +// tuple example {Promise<[String, Boolean]>} /** + * @param {Object} [opts] + * @param {Boolean} [opts.force] * @param {String} plainWif */ -async function maybeEncrypt(plainWif) { +async function maybeEncrypt(plainWif, opts) { let passphrase = cmds._getPassphrase(); if (!passphrase) { - passphrase = await cmds.getPassphrase(null, []); + let result = await cmds.getPassphrase({}, []); + passphrase = result.passphrase; } if (!passphrase) { + if (opts?.force) { + throw new Error(`no passphrase with which to encrypt file`); + } return plainWif; } @@ -1044,18 +1321,89 @@ async function setDefault(_, args) { /** * @param {Object} opts * @param {any} opts.dashApi - TODO + * @param {String} opts.defaultAddr * @param {Array} args */ -async function listKeys({ dashApi }, args) { +async function listKeys({ dashApi, defaultAddr }, args) { let wifnames = await listManagedKeynames(); + if (wifnames) { + // to print 'default staking key' message + await mustGetAddr({ defaultAddr }, args); + } + /** * @type Array<{ node: String, error: Error }> */ let warns = []; - console.info(``); - console.info(`Staking keys: (in ${keysDirRel}/)`); - console.info(``); + // console.error because console.debug goes to stdout, not stderr + debug(``); + debug(`Staking keys: (in ${keysDirRel}/)`); + debug(``); + + await wifnames.reduce(async function (promise, wifname) { + await promise; + + let wifpath = Path.join(keysDir, wifname); + let addr = await maybeReadKeyFile(wifpath, { wif: false }).catch(function ( + err, + ) { + warns.push({ node: wifname, error: err }); + return ""; + }); + if (!addr) { + return; + } + + console.info(`${addr}`); + }, Promise.resolve()); + debug(``); + + if (warns.length) { + console.warn(`Warnings:`); + warns.forEach(function (warn) { + console.warn(`${warn.node}: ${warn.error.message}`); + }); + console.warn(``); + } +} + +/** + * @param {Object} opts + * @param {any} opts.dashApi - TODO + * @param {String} opts.defaultAddr + * @param {Array} args + */ +async function getAllBalances({ dashApi, defaultAddr }, args) { + let wifnames = await listManagedKeynames(); + let totals = { + key: 0, + stake: 0, + dividend: 0, + keyDash: "", + stakeDash: "", + dividendDash: "", + }; + + if (wifnames.length) { + // to print 'default staking key' message + await mustGetAddr({ defaultAddr }, args); + } + + /** + * @type Array<{ node: String, error: Error }> + */ + let warns = []; + // console.error because console.debug goes to stdout, not stderr + debug(``); + debug(`Staking keys: (in ${keysDirRel}/)`); + debug(``); + console.info( + `| | 🔑 Holdings | 🪧 Stakings | 💸 Earnings |`, + ); + console.info( + `| ---------------------------------: | ------------: | ------------: | ------------: |`, + ); if (!wifnames.length) { console.info(` (none)`); } @@ -1088,12 +1436,42 @@ async function listKeys({ dashApi }, args) { } */ - process.stdout.write(` 🪙 ${addr}: `); + process.stdout.write(`| ${addr} |`); + let balanceInfo = await dashApi.getInstantBalance(addr); - let balanceDash = toDash(balanceInfo.balanceSat); - console.info(`${balanceInfo.balanceSat} (Đ${balanceDash})`); + let balanceDASH = toDASH(balanceInfo.balanceSat); + + let crowdNodeBalance = await CrowdNode.http.GetBalance(addr); + if (!crowdNodeBalance.TotalBalance) { + crowdNodeBalance.TotalBalance = 0; + crowdNodeBalance.TotalDividend = 0; + } + let crowdNodeDuffNum = toDuff(crowdNodeBalance.TotalBalance); + let crowdNodeDASH = toDASH(crowdNodeDuffNum); + + let crowdNodeDivNum = toDuff(crowdNodeBalance.TotalDividend); + let crowdNodeDivDASH = toDASH(crowdNodeDivNum); + process.stdout.write( + ` ${balanceDASH} | ${crowdNodeDASH} | ${crowdNodeDivDASH} |`, + ); + + totals.key += balanceInfo.balanceSat; + totals.dividend += crowdNodeBalance.TotalDividend; + totals.stake += crowdNodeBalance.TotalBalance; + + console.info(); }, Promise.resolve()); - console.info(``); + console.info( + `| | | | |`, + ); + let total = `| Totals`; + totals.keyDash = toDASH(toDuff(totals.key.toString())); + totals.stakeDash = toDASH(toDuff(totals.stake.toString())); + totals.dividendDash = toDASH(toDuff(totals.dividend.toString())); + console.info( + `${total} | ${totals.stakeDash} | ${totals.stakeDash} | ${totals.dividendDash} |`, + ); + debug(``); if (warns.length) { console.warn(`Warnings:`); @@ -1118,12 +1496,12 @@ function isNamedLikeKey(name) { /** * @param {Object} opts * @param {any} opts.dashApi - TODO - * @param {String} opts.defaultAddr + * @param {String} opts.addr + * @param {String} opts.filepath * @param {String} opts.insightBaseUrl * @param {Array} args */ -async function removeKey({ dashApi, defaultAddr, insightBaseUrl }, args) { - let [addr, name] = await mustGetAddr({ defaultAddr }, args); +async function removeKey({ addr, dashApi, filepath, insightBaseUrl }, args) { let balanceInfo = await dashApi.getInstantBalance(addr); let balanceDash = toDash(balanceInfo.balanceSat); @@ -1146,7 +1524,6 @@ async function removeKey({ dashApi, defaultAddr, insightBaseUrl }, args) { crowdNodeBalance = {}; } if (!crowdNodeBalance.TotalBalance) { - //console.log('DEBUG', crowdNodeBalance); crowdNodeBalance.TotalBalance = 0; } let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance); @@ -1165,10 +1542,10 @@ async function removeKey({ dashApi, defaultAddr, insightBaseUrl }, args) { } let wifname = await findWif(addr); - let filepath = Path.join(keysDir, wifname); - let wif = await maybeReadKeyPaths(name, { wif: true }); + let fullpath = Path.join(keysDir, wifname); + let wif = await maybeReadKeyPaths(filepath, { wif: true }); - await Fs.unlink(filepath).catch(function (err) { + await Fs.unlink(fullpath).catch(function (err) { console.error(`could not remove ${filepath}: ${err.message}`); process.exit(1); }); @@ -1185,7 +1562,7 @@ async function removeKey({ dashApi, defaultAddr, insightBaseUrl }, args) { console.info(``); } else { let newAddr = wifnames[0]; - console.info(`Selected ${newAddr} as new default staking key.`); + debug(`Selected ${newAddr} as new default staking key.`); await Fs.writeFile(defaultWifPath, addr.replace(".wif", ""), "utf8"); console.info(``); } @@ -1234,19 +1611,48 @@ async function loadAddr({ defaultAddr, insightBaseUrl }, args) { let desiredAmountDash = parseFloat(args.shift() || "0"); let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS); + let effectiveDuff = desiredAmountDuff; let effectiveDash = ""; if (!effectiveDuff) { effectiveDuff = CrowdNode.stakeMinimum + signupTotal + feeEstimate; - effectiveDash = toDash(effectiveDuff); - // Round to the nearest mDash - // ex: 0.50238108 => 0.50300000 - effectiveDuff = toDuff( - (Math.ceil(parseFloat(effectiveDash) * 1000) / 1000).toString(), - ); + effectiveDuff = roundDuff(effectiveDuff, 3); effectiveDash = toDash(effectiveDuff); } + await plainLoadAddr({ addr, effectiveDash, effectiveDuff, insightBaseUrl }); + + return; +} + +/** + * 1000 to Round to the nearest mDash + * ex: 0.50238108 => 0.50300000 + * @param {Number} effectiveDuff + * @param {Number} numDigits + */ +function roundDuff(effectiveDuff, numDigits) { + let n = Math.pow(10, numDigits); + let effectiveDash = toDash(effectiveDuff); + effectiveDuff = toDuff( + (Math.ceil(parseFloat(effectiveDash) * n) / n).toString(), + ); + return effectiveDuff; +} + +/** + * @param {Object} opts + * @param {String} opts.addr + * @param {String} opts.effectiveDash + * @param {Number} opts.effectiveDuff + * @param {String} opts.insightBaseUrl + */ +async function plainLoadAddr({ + addr, + effectiveDash, + effectiveDuff, + insightBaseUrl, +}) { console.info(``); showQr(addr, effectiveDuff); console.info(``); @@ -1258,7 +1664,6 @@ async function loadAddr({ defaultAddr, insightBaseUrl }, args) { console.info(``); let payment = await Ws.waitForVout(insightBaseUrl, addr, 0); console.info(`Received ${payment.satoshis}`); - process.exit(0); } /** @@ -1269,9 +1674,9 @@ async function loadAddr({ defaultAddr, insightBaseUrl }, args) { */ async function getBalance({ dashApi, defaultAddr }, args) { let [addr] = await mustGetAddr({ defaultAddr }, args); - let balanceInfo = await checkBalance({ addr, dashApi }); - console.info(balanceInfo); - process.exit(0); + await checkBalance({ addr, dashApi }); + //let balanceInfo = await checkBalance({ addr, dashApi }); + //console.info(balanceInfo); return; } @@ -1322,7 +1727,6 @@ async function transferBalance( }, 30 * 1000); await Ws.waitForVout(insightBaseUrl, newAddr, 0); console.info(`Accepted!`); - process.exit(0); return; } @@ -1356,12 +1760,11 @@ async function getStatus({ dashApi, defaultAddr, insightBaseUrl }, args) { if (!crowdNodeBalance.TotalBalance) { crowdNodeBalance.TotalBalance = 0; } - let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance); + let crowdNodeDuff = toDuff(crowdNodeBalance.TotalBalance); console.info( - `CrowdNode Stake: ${crowdNodeBalance.TotalBalance} (Đ${crowdNodeDash})`, + `CrowdNode Stake: ${crowdNodeDuff} (Đ${crowdNodeBalance.TotalBalance})`, ); console.info(); - process.exit(0); return; } @@ -1384,7 +1787,6 @@ async function sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args) { console.info(` ${state.signup} SignUpForApi`); console.info(` ${state.accept} AcceptTerms`); console.info(` ${state.deposit} DepositReceived`); - process.exit(0); return; } @@ -1397,10 +1799,8 @@ async function sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args) { console.info("Requesting account..."); await CrowdNode.signup(wif, hotwallet); - state.signup = "✅"; + state.signup = DONE; console.info(` ${state.signup} SignUpForApi`); - console.info(` ${state.accept} AcceptTerms`); - process.exit(0); return; } @@ -1432,7 +1832,6 @@ async function acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args) { console.info(` ${state.signup} SignUpForApi`); console.info(` ${state.accept} AcceptTerms`); console.info(` ${state.deposit} DepositReceived`); - process.exit(0); return; } let hasEnough = balanceInfo.balanceSat > acceptOnly + feeEstimate; @@ -1444,11 +1843,8 @@ async function acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args) { console.info("Accepting terms..."); await CrowdNode.accept(wif, hotwallet); - state.accept = "✅"; - console.info(` ${state.signup} SignUpForApi`); + state.accept = DONE; console.info(` ${state.accept} AcceptTerms`); - console.info(` ${state.deposit} DepositReceived`); - process.exit(0); return; } @@ -1527,9 +1923,8 @@ async function depositDash( let wif = await maybeReadKeyPaths(name, { wif: true }); await CrowdNode.deposit(wif, hotwallet, effectiveAmount); - state.deposit = "✅"; + state.deposit = DONE; console.info(` ${state.deposit} DepositReceived`); - process.exit(0); return; } @@ -1577,7 +1972,6 @@ async function withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args) { //let paidFloat = (paid.satoshis / DUFFS).toFixed(8); //let paidInt = paid.satoshis.toString().padStart(9, "0"); console.info(`API Response: ${paid.api}`); - process.exit(0); return; } @@ -1686,6 +2080,14 @@ function toDash(duffs) { return (duffs / DUFFS).toFixed(8); } +/** + * @param {Number} duffs - ex: 00000000 + */ +function toDASH(duffs) { + let dash = (duffs / DUFFS).toFixed(8); + return `Đ` + dash.padStart(12, " "); +} + /** * @param {String} dash - ex: 0.00000000 */