X-Git-Url: https://git.josue.xyz/?p=crowdnode.js%2F.git;a=blobdiff_plain;f=bin%2Fcrowdnode.js;h=adb6306402070cf5a6c2f3637e55cee0db490aae;hp=57e49a330eff5e32b572b1b861ea58d80bc0c2de;hb=5dbccedf943f0f768d80a9da1bffff061a5cef3e;hpb=1d3075667736acbe1df9b004ceda205e76c3e689 diff --git a/bin/crowdnode.js b/bin/crowdnode.js index 57e49a3..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,7 +248,8 @@ async function main() { if (!key) { throw new Error("impossible error"); } - encryptAll([key]); + await encryptAll([key]); + process.exit(0); return; } @@ -198,7 +257,10 @@ async function main() { let addr = args.shift() || ""; if (!addr) { await decryptAll(null); - await Fs.writeFile(shadowPath, "", "utf8").catch(emptyStringOnErrEnoent); + await Fs.writeFile(shadowPath, NO_SHADOW, "utf8").catch( + emptyStringOnErrEnoent, + ); + process.exit(0); return; } let keypath = await findWif(addr); @@ -211,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; } @@ -227,6 +291,7 @@ async function main() { { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi }, args, ); + process.exit(0); return; } @@ -260,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; } @@ -301,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; } @@ -315,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", @@ -339,7 +529,7 @@ function showQr(addr, duffs = 0) { } let dashQr = Qr.ascii(dashUri, { indent: 4, size: "mini" }); - let addrPad = Math.ceil((qrWidth - dashUri.length) / 2); + let addrPad = Math.max(0, Math.ceil((qrWidth - dashUri.length) / 2)); console.info(dashQr); console.info(); @@ -365,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, @@ -376,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; } @@ -396,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) { @@ -538,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; } @@ -562,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) { @@ -587,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; @@ -601,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({}, []); + } } /** @@ -619,6 +842,7 @@ async function setPassphrase({ _askPreviousPassphrase }, args) { // get the old passphrase if (false !== _askPreviousPassphrase) { + // TODO should contain the shadow? await cmds.getPassphrase({ _rotatePassphrase: true }, []); } @@ -646,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(``); @@ -685,11 +909,10 @@ async function promptPassphrase() { /** * 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}'`); @@ -712,110 +935,8 @@ async function importKey(_, args) { console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`); console.info(``); -} - -/** - * Encrypt ALL-the-things! - * @param {Object} [opts] - * @param {Boolean} opts.rotateKey - * @param {Array?} rawKeys - */ -async function encryptAll(rawKeys, opts) { - if (!rawKeys) { - rawKeys = await readAllKeys(); - } - 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) { - await promise; - - if (key.encrypted && !opts?.rotateKey) { - console.info(`🙈 ${key.addr} [already encrypted]`); - return; - } - let encWif = await maybeEncrypt(key.wif, { force: true }); - await safeSave( - Path.join(keysDir, `${key.addr}.wif`), - encWif, - Path.join(keysDir, `${key.addr}.${date}.bak`), - ); - console.info(`🔑 ${key.addr}`); - }, Promise.resolve()); - console.info(``); - console.info(`Done 🔐`); - console.info(``); -} - -/** - * Decrypt ALL-the-things! - * @param {Array?} rawKeys - */ -async function decryptAll(rawKeys) { - if (!rawKeys) { - rawKeys = await readAllKeys(); - } - let date = getFsDateString(); - - console.info(``); - console.info(`Decrypting...`); - console.info(``); - await rawKeys.reduce(async function (promise, key) { - await promise; - - if (!key.encrypted) { - console.info(`📖 ${key.addr} [already decrypted]`); - return; - } - await safeSave( - Path.join(keysDir, `${key.addr}.wif`), - key.wif, - Path.join(keysDir, `${key.addr}.${date}.bak`), - ); - console.info(`🔓 ${key.addr}`); - }, Promise.resolve()); - console.info(``); - console.info(`Done ✅`); - console.info(``); -} -function getFsDateString() { - // YYYY-MM-DD_hh-mm_ss - let date = new Date() - .toISOString() - .replace(/:/g, ".") - .replace(/T/, "_") - .replace(/\.\d{3}.*/, ""); - return date; -} - -/** - * @param {String} filepath - * @param {String} wif - * @param {String} bakpath - */ -async function safeSave(filepath, wif, bakpath) { - let tmpPath = `${bakpath}.tmp`; - await Fs.writeFile(tmpPath, wif, "utf8"); - let err = await Fs.access(filepath).catch(Object); - if (!err) { - await Fs.rename(filepath, bakpath); - } - await Fs.rename(tmpPath, filepath); - if (!err) { - await Fs.unlink(bakpath); - } + return key.addr; } /** @@ -843,14 +964,12 @@ cmds.getPassphrase = async function ({ _rotatePassphrase, _force }, args) { // 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; - }); - if (!shadow && _force) { + let shadow = await Fs.readFile(shadowPath, "utf8").catch( + emptyStringOnErrEnoent, + ); + if (!shadow) { + needsInit = true; + } else if (NO_SHADOW === shadow && _force) { needsInit = true; } @@ -876,8 +995,8 @@ cmds.getPassphrase = async function ({ _rotatePassphrase, _force }, args) { continue; } - // No passphrase, create empty shadow file - await Fs.writeFile(shadowPath, "", "utf8"); + // No passphrase, create a NONE shadow file + await Fs.writeFile(shadowPath, NO_SHADOW, "utf8"); return result; } } @@ -932,6 +1051,110 @@ cmds._setPassphrase = function (passphrase) { }; }; +/** + * Encrypt ALL-the-things! + * @param {Object} [opts] + * @param {Boolean} opts.rotateKey + * @param {Array?} rawKeys + */ +async function encryptAll(rawKeys, opts) { + if (!rawKeys) { + rawKeys = await readAllKeys(); + } + 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) { + await promise; + + if (key.encrypted && !opts?.rotateKey) { + console.info(`🙈 ${key.addr} [already encrypted]`); + return; + } + let encWif = await maybeEncrypt(key.wif, { force: true }); + await safeSave( + Path.join(keysDir, `${key.addr}.wif`), + encWif, + Path.join(keysDir, `${key.addr}.${date}.bak`), + ); + console.info(`🔑 ${key.addr}`); + }, Promise.resolve()); + console.info(``); + console.info(`Done 🔐`); + console.info(``); +} + +/** + * Decrypt ALL-the-things! + * @param {Array?} rawKeys + */ +async function decryptAll(rawKeys) { + if (!rawKeys) { + rawKeys = await readAllKeys(); + } + let date = getFsDateString(); + + console.info(``); + console.info(`Decrypting...`); + console.info(``); + await rawKeys.reduce(async function (promise, key) { + await promise; + + if (!key.encrypted) { + console.info(`📖 ${key.addr} [already decrypted]`); + return; + } + await safeSave( + Path.join(keysDir, `${key.addr}.wif`), + key.wif, + Path.join(keysDir, `${key.addr}.${date}.bak`), + ); + console.info(`🔓 ${key.addr}`); + }, Promise.resolve()); + console.info(``); + console.info(`Done ${DONE}`); + console.info(``); +} + +function getFsDateString() { + // YYYY-MM-DD_hh-mm_ss + let date = new Date() + .toISOString() + .replace(/:/g, ".") + .replace(/T/, "_") + .replace(/\.\d{3}.*/, ""); + return date; +} + +/** + * @param {String} filepath + * @param {String} wif + * @param {String} bakpath + */ +async function safeSave(filepath, wif, bakpath) { + let tmpPath = `${bakpath}.tmp`; + await Fs.writeFile(tmpPath, wif, "utf8"); + let err = await Fs.access(filepath).catch(Object); + if (!err) { + await Fs.rename(filepath, bakpath); + } + await Fs.rename(tmpPath, filepath); + if (!err) { + await Fs.unlink(bakpath); + } +} + /** * @typedef {Object} RawKey * @property {String} addr @@ -1098,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)`); } @@ -1142,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:`); @@ -1172,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); @@ -1200,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); @@ -1219,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); }); @@ -1239,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(``); } @@ -1288,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(``); @@ -1312,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); } /** @@ -1323,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; } @@ -1376,7 +1727,6 @@ async function transferBalance( }, 30 * 1000); await Ws.waitForVout(insightBaseUrl, newAddr, 0); console.info(`Accepted!`); - process.exit(0); return; } @@ -1410,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; } @@ -1438,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; } @@ -1451,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; } @@ -1486,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; @@ -1498,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; } @@ -1581,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; } @@ -1631,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; } @@ -1740,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 */