require("dotenv").config({ path: ".env" });
require("dotenv").config({ path: ".env.secret" });
+let HOME = process.env.HOME || "";
+
+//@ts-ignore
let pkg = require("../package.json");
let Fs = require("fs").promises;
+let Path = require("path");
+let Cipher = require("./_cipher.js");
let CrowdNode = require("../lib/crowdnode.js");
let Dash = require("../lib/dash.js");
let Insight = require("../lib/insight.js");
+let Prompt = require("./_prompt.js");
let Qr = require("../lib/qr.js");
let Ws = require("../lib/ws.js");
let feeEstimate = 500;
let signupTotal = signupFees + 2 * feeEstimate;
-function showQr(signupAddr, duffs = 0) {
- let signupUri = `dash://${signupAddr}`;
- if (duffs) {
- signupUri += `?amount=${duffs}`;
- }
-
- let signupQr = Qr.ascii(signupUri, { indent: 4 });
- let addrPad = Math.ceil((qrWidth - signupUri.length) / 2);
-
- console.info(signupQr);
- console.info();
- console.info(" ".repeat(addrPad) + signupUri);
-}
+//let paths = {};
+let configdir = `.config/crowdnode`;
+let keysDir = Path.join(HOME, `${configdir}/keys`);
+let keysDirRel = `~/${configdir}/keys`;
+let shadowPath = Path.join(HOME, `${configdir}/shadow`);
+let defaultWifPath = Path.join(HOME, `${configdir}/default`);
function showVersion() {
console.info(`${pkg.name} v${pkg.version} - ${pkg.description}`);
console.info("Usage:");
console.info(" crowdnode help");
- console.info(" crowdnode status ./privkey.wif");
- console.info(" crowdnode signup ./privkey.wif");
- console.info(" crowdnode accept ./privkey.wif");
+ console.info(" crowdnode status [keyfile-or-addr]");
+ console.info(" crowdnode signup [keyfile-or-addr]");
+ console.info(" crowdnode accept [keyfile-or-addr]");
console.info(
- " crowdnode deposit ./privkey.wif [dash-amount] [--no-reserve]",
+ " crowdnode deposit [keyfile-or-addr] [dash-amount] [--no-reserve]",
);
console.info(
- " crowdnode withdrawal ./privkey.wif <percent> # 1.0-100.0 (steps by 0.1)",
+ " crowdnode withdrawal [keyfile-or-addr] <percent> # 1.0-100.0 (steps by 0.1)",
);
console.info("");
console.info("Helpful Extras:");
- console.info(" crowdnode generate [./privkey.wif]");
- console.info(" crowdnode load [./privkey.wif]");
- console.info(" crowdnode balance ./privkey.wif");
+ console.info(" crowdnode balance [keyfile-or-addr]"); // addr
+ console.info(" crowdnode load [keyfile-or-addr] [dash-amount]"); // addr
console.info(
- " crowdnode transfer ./source.wif <key-file-or-pub-addr> [dash-amount]",
- );
+ " crowdnode transfer <from-keyfile-or-addr> <to-keyfile-or-addr> [dash-amount]",
+ ); // custom
+ console.info("");
+
+ console.info("Key Management & Encryption:");
+ console.info(" crowdnode generate [./privkey.wif]");
+ console.info(" crowdnode encrypt"); // TODO allow encrypting one-by-one?
+ console.info(" crowdnode list");
+ console.info(" crowdnode use <addr>");
+ console.info(" crowdnode import <keyfile>");
+ //console.info(" crowdnode import <(dash-cli dumpprivkey <addr>)"); // TODO
+ //console.info(" crowdnode export <addr> <keyfile>"); // TODO
+ console.info(" crowdnode passphrase # set or change passphrase");
+ console.info(" crowdnode decrypt"); // TODO allow decrypting one-by-one?
+ console.info(" crowdnode delete <addr>");
console.info("");
console.info("CrowdNode HTTP RPC:");
console.info("");
}
-function removeItem(arr, item) {
- let index = arr.indexOf(item);
- if (index >= 0) {
- return arr.splice(index, 1)[0];
- }
- return null;
-}
+let cmds = {};
async function main() {
+ /*jshint maxcomplexity:40 */
+ /*jshint maxstatements:500 */
+
// Usage:
// crowdnode <subcommand> [flags] <privkey> [options]
// Example:
return;
}
- if ("generate" === subcommand) {
- await generate(args.shift());
- return;
- }
+ //
+ //
+ // find addr by name or by file or by string
+ await Fs.mkdir(keysDir, {
+ recursive: true,
+ });
+
+ let defaultAddr = await Fs.readFile(defaultWifPath, "utf8").catch(
+ emptyStringOnErrEnoent,
+ );
+ defaultAddr = defaultAddr.trim();
let insightBaseUrl =
process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
let insightApi = Insight.create({ baseUrl: insightBaseUrl });
let dashApi = Dash.create({ insightApi: insightApi });
- if ("load" === subcommand) {
- await load(
- {
- dashApi: dashApi,
- insightBaseUrl: insightBaseUrl,
- insightApi: insightApi,
- },
+ if ("list" === subcommand) {
+ await listKeys({ dashApi }, args);
+ return;
+ }
+
+ if ("generate" === subcommand) {
+ await generateKey({ defaultKey: defaultAddr }, args);
+ return;
+ }
+
+ if ("passphrase" === subcommand) {
+ await setPassphrase({}, args);
+ return;
+ }
+
+ if ("import" === subcommand) {
+ importKey(null, args);
+ return;
+ }
+
+ if ("encrypt" === subcommand) {
+ let addr = args.shift() || "";
+ if (!addr) {
+ encryptAll(null);
+ return;
+ }
+
+ let keypath = await findWif(addr);
+ if (!keypath) {
+ console.error(`no managed key matches '${addr}'`);
+ process.exit(1);
+ return;
+ }
+ let key = await maybeReadKeyFileRaw(keypath);
+ if (!key) {
+ throw new Error("impossible error");
+ }
+ encryptAll([key]);
+ return;
+ }
+
+ if ("decrypt" === subcommand) {
+ let addr = args.shift() || "";
+ if (!addr) {
+ decryptAll(null);
+ return;
+ }
+ let keypath = await findWif(addr);
+ if (!keypath) {
+ console.error(`no managed key matches '${addr}'`);
+ process.exit(1);
+ return;
+ }
+ let key = await maybeReadKeyFileRaw(keypath);
+ if (!key) {
+ throw new Error("impossible error");
+ }
+ decryptAll([key]);
+ return;
+ }
+
+ // use or select or default... ?
+ if ("use" === subcommand) {
+ await setDefault(null, args);
+ return;
+ }
+
+ // helper for debugging
+ if ("transfer" === subcommand) {
+ await transferBalance(
+ { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
args,
);
return;
}
- process.stdout.write("Checking CrowdNode API... ");
- await CrowdNode.init({
- baseUrl: "https://app.crowdnode.io",
- insightBaseUrl,
- insightApi: insightApi,
- });
- console.info(`hotwallet is ${CrowdNode.main.hotwallet}`);
-
let rpc = "";
if ("http" === subcommand) {
- rpc = args.shift();
- let keyfile = args.shift();
- let pub = await wifFileToAddr(keyfile);
+ rpc = args.shift() || "";
+ if (!rpc) {
+ showHelp();
+ process.exit(1);
+ return;
+ }
+ let [addr] = await mustGetAddr({ defaultAddr }, args);
+
+ await initCrowdNode(insightBaseUrl);
// ex: http <rpc>(<pub>, ...)
- args.unshift(pub);
+ args.unshift(addr);
let hasRpc = rpc in CrowdNode.http;
if (!hasRpc) {
console.error(`Unrecognized rpc command ${rpc}`);
showHelp();
process.exit(1);
}
+ //@ts-ignore - TODO use `switch` or make Record Type
let result = await CrowdNode.http[rpc].apply(null, args);
+ console.info(``);
+ console.info(`${rpc} ${addr}:`);
if ("string" === typeof result) {
console.info(result);
} else {
return;
}
- let keyfile = args.shift();
- let privKey;
- if (keyfile) {
- privKey = await Fs.readFile(keyfile, "utf8");
- privKey = privKey.trim();
- } else {
- privKey = process.env.PRIVATE_KEY;
+ if ("load" === subcommand) {
+ await loadAddr({ defaultAddr, insightBaseUrl }, args);
+ return;
}
- if (!privKey) {
- // TODO generate private key?
- console.error();
- console.error(
- `Error: you must provide either the WIF key file path or PRIVATE_KEY in .env`,
+
+ // keeping rm for backwards compat
+ if ("rm" === subcommand || "delete" === subcommand) {
+ await initCrowdNode(insightBaseUrl);
+ await removeKey({ defaultAddr, dashApi, insightBaseUrl }, args);
+ return;
+ }
+
+ if ("balance" === subcommand) {
+ await getBalance({ dashApi, defaultAddr }, args);
+ process.exit(0);
+ return;
+ }
+
+ if ("status" === subcommand) {
+ await getStatus({ dashApi, defaultAddr, insightBaseUrl }, args);
+ return;
+ }
+
+ if ("signup" === subcommand) {
+ await sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args);
+ return;
+ }
+
+ if ("accept" === subcommand) {
+ await acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args);
+ return;
+ }
+
+ if ("deposit" === subcommand) {
+ await depositDash(
+ { dashApi, defaultAddr, insightBaseUrl, noReserve },
+ args,
);
- console.error();
- process.exit(1);
+ return;
}
- let pk = new Dashcore.PrivateKey(privKey);
- let pub = pk.toPublicKey().toAddress().toString();
+ if ("withdrawal" === subcommand) {
+ await withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args);
+ return;
+ }
+
+ console.error(`Unrecognized subcommand ${subcommand}`);
+ console.error();
+ showHelp();
+ process.exit(1);
+}
+
+/**
+ * @param {String} insightBaseUrl
+ */
+async function initCrowdNode(insightBaseUrl) {
+ process.stdout.write("Checking CrowdNode API... ");
+ await CrowdNode.init({
+ baseUrl: "https://app.crowdnode.io",
+ insightBaseUrl,
+ });
+ console.info(`(hotwallet ${CrowdNode.main.hotwallet})`);
+}
+
+/**
+ * @param {String} addr - Base58Check pubKeyHash address
+ * @param {Number} duffs - 1/100000000 of a DASH
+ */
+function showQr(addr, duffs = 0) {
+ let dashUri = `dash://${addr}`;
+ if (duffs) {
+ dashUri += `?amount=${duffs}`;
+ }
+
+ let dashQr = Qr.ascii(dashUri, { indent: 4 });
+ let addrPad = Math.ceil((qrWidth - dashUri.length) / 2);
+
+ console.info(dashQr);
+ console.info();
+ console.info(" ".repeat(addrPad) + dashUri);
+}
+/**
+ * @param {Array<any>} arr
+ * @param {any} item
+ */
+function removeItem(arr, item) {
+ let index = arr.indexOf(item);
+ if (index >= 0) {
+ return arr.splice(index, 1)[0];
+ }
+ return null;
+}
+
+/**
+ * @param {Object} opts
+ * @param {String} opts.addr
+ * @param {String} opts.hotwallet
+ */
+async function getCrowdNodeStatus({ addr, hotwallet }) {
+ let state = {
+ signup: "❌",
+ accept: "❌",
+ deposit: "❌",
+ status: {
+ signup: 0,
+ accept: 0,
+ deposit: 0,
+ },
+ };
+
+ //@ts-ignore - TODO why warnings?
+ state.status = await CrowdNode.status(addr, hotwallet);
+ if (state.status?.signup) {
+ state.signup = "✅";
+ }
+ if (state.status?.accept) {
+ state.accept = "✅";
+ }
+ if (state.status?.deposit) {
+ state.deposit = "✅";
+ }
+ return state;
+}
+
+/**
+ * @param {Object} opts
+ * @param {String} opts.addr
+ * @param {any} opts.dashApi - TODO
+ */
+async function checkBalance({ addr, dashApi }) {
// deposit if balance is over 100,000 (0.00100000)
process.stdout.write("Checking balance... ");
- let balanceInfo = await dashApi.getInstantBalance(pub);
+ let balanceInfo = await dashApi.getInstantBalance(addr);
let balanceDash = toDash(balanceInfo.balanceSat);
console.info(`${balanceInfo.balanceSat} (Đ${balanceDash})`);
/*
}
}
*/
+ return balanceInfo;
+}
- let state = {
- balanceInfo: balanceInfo,
- dashApi: dashApi,
- forceConfirm: forceConfirm,
- hotwallet: CrowdNode.main.hotwallet,
- insightBaseUrl: insightBaseUrl,
- insightApi: insightApi,
- noReserve: noReserve,
- privKey: privKey,
- pub: pub,
-
- // status
- status: {
- signup: 0,
- accept: 0,
- deposit: 0,
- },
- signup: "❌",
- accept: "❌",
- deposit: "❌",
- };
+/**
+ * @param {Object} opts
+ * @param {String} opts.defaultAddr
+ * @param {Array<String>} args
+ * @returns {Promise<[String, String]>}
+ */
+async function mustGetAddr({ defaultAddr }, args) {
+ let name = args.shift() ?? "";
+ if (34 === name.length) {
+ // looks like addr already
+ // TODO make function for addr-lookin' check
+ return [name, name];
+ }
- if ("balance" === subcommand) {
- await balance(args, state);
- process.exit(0);
- return;
+ let addr = await maybeReadKeyPaths(name, { wif: false });
+ if (addr) {
+ if (34 === addr.length) {
+ return [addr, name];
+ }
+ //let pk = new Dashcore.PrivateKey(wif);
+ //let addr = pk.toAddress().toString();
+ return [addr, name];
}
- // helper for debugging
- if ("transfer" === subcommand) {
- await transfer(args, state);
- return;
+ let isNum = !isNaN(parseFloat(name));
+ if (isNum) {
+ args.unshift(name);
+ name = "";
}
- state.status = await CrowdNode.status(pub, state.hotwallet);
- if (state.status?.signup) {
- state.signup = "✅";
+ if (name) {
+ console.error();
+ console.error(`could not read '${name}' in ./ or match in ${keysDirRel}/.`);
+ console.error();
+ process.exit(1);
+ return ["", name];
}
- if (state.status?.accept) {
- state.accept = "✅";
+
+ addr = await mustGetDefaultWif(defaultAddr, { wif: false });
+
+ // TODO we don't need defaultAddr, right? because it could be old?
+ return [addr, addr];
+}
+
+/**
+ * @param {Object} opts
+ * @param {String} opts.defaultAddr
+ * @param {Array<String>} args
+ */
+async function mustGetWif({ defaultAddr }, args) {
+ let name = args.shift() ?? "";
+
+ let wif = await maybeReadKeyPaths(name, { wif: true });
+ if (wif) {
+ return wif;
}
- if (state.status?.deposit) {
- state.deposit = "✅";
+
+ let isNum = !isNaN(parseFloat(name));
+ if (isNum) {
+ args.unshift(name);
+ name = "";
}
- if ("status" === subcommand) {
- await status(args, state);
- return;
+ if (name) {
+ console.error();
+ console.error(
+ `'${name}' does not match a staking key in ./ or ${keysDirRel}/`,
+ );
+ console.error();
+ process.exit(1);
+ return "";
}
- if ("signup" === subcommand) {
- await signup(args, state);
- return;
+ wif = await mustGetDefaultWif(defaultAddr);
+
+ return wif;
+}
+
+/**
+ * @param {String} name
+ * @param {Object} opts
+ * @param {Boolean} opts.wif
+ * @returns {Promise<String>} - wif
+ */
+async function maybeReadKeyPaths(name, opts) {
+ let privKey = "";
+
+ // prefix match in .../keys/
+ let wifname = await findWif(name);
+ if (!wifname) {
+ return "";
}
- if ("accept" === subcommand) {
- await accept(args, state);
- return;
+ if (false === opts.wif) {
+ return wifname.slice(0, -".wif".length);
}
- if ("deposit" === subcommand) {
- await deposit(args, state);
- return;
+ let filepath = Path.join(keysDir, wifname);
+ privKey = await maybeReadKeyFile(filepath);
+ if (!privKey) {
+ // local in ./
+ privKey = await maybeReadKeyFile(name);
}
- if ("withdrawal" === subcommand) {
- await withdrawal(args, state);
- return;
+ return privKey;
+}
+
+/**
+ * @param {String} defaultAddr
+ * @param {Object} [opts]
+ * @param {Boolean} opts.wif
+ */
+async function mustGetDefaultWif(defaultAddr, opts) {
+ let defaultWif = "";
+ if (defaultAddr) {
+ let keyfile = Path.join(keysDir, `${defaultAddr}.wif`);
+ let raw = await maybeReadKeyFileRaw(keyfile, opts);
+ // misnomering wif here a bit
+ defaultWif = raw?.wif || raw?.addr || "";
+ }
+ if (defaultWif) {
+ console.info(`selected default staking key ${defaultAddr}`);
+ return defaultWif;
}
- console.error(`Unrecognized subcommand ${subcommand}`);
console.error();
- showHelp();
+ console.error(`Error: no default staking key selected.`);
+ console.error();
+ console.error(`Select a different address:`);
+ console.error(` crowdnode list`);
+ console.error(` crowdnode use <addr>`);
+ console.error(``);
+ console.error(`Or create a new staking key:`);
+ console.error(` crowdnode generate`);
+ console.error();
process.exit(1);
+ return "";
}
// Subcommands
-async function generate(name) {
+/**
+ * @param {Object} psuedoState
+ * @param {String} psuedoState.defaultKey - addr name of default key
+ * @param {Array<String>} args
+ */
+async function generateKey({ defaultKey }, args) {
+ let name = args.shift();
+ //@ts-ignore - TODO submit JSDoc PR for Dashcore
let pk = new Dashcore.PrivateKey();
- let pub = pk.toAddress().toString();
- let wif = pk.toWIF();
+ let addr = pk.toAddress().toString();
+ let plainWif = pk.toWIF();
+
+ let wif = await maybeEncrypt(plainWif);
- let filepath = `./${pub}.wif`;
+ let filename = `~/${configdir}/keys/${addr}.wif`;
+ let filepath = Path.join(`${keysDir}/${addr}.wif`);
let note = "";
if (name) {
+ filename = name;
filepath = name;
- note = `\n(for pubkey address ${pub})`;
+ note = `\n(for pubkey address ${addr})`;
+ let err = await Fs.access(filepath).catch(Object);
+ if (!err) {
+ console.info(`'${filepath}' already exists (will not overwrite)`);
+ process.exit(0);
+ return;
+ }
}
- let testDash = 0.01;
- let testDuff = toDuff(testDash);
+ await Fs.writeFile(filepath, wif, "utf8");
+ if (!name && !defaultKey) {
+ await Fs.writeFile(defaultWifPath, addr, "utf8");
+ }
- let err = await Fs.access(filepath).catch(Object);
- if (!err) {
- // TODO show QR anyway
- //wif = await Fs.readFile(filepath, 'utf8')
- //showQr(pub, testDuff);
- console.info(`'${filepath}' already exists (will not overwrite)`);
- process.exit(0);
+ console.info(``);
+ console.info(`Generated ${filename} ${note}`);
+ console.info(``);
+ process.exit(0);
+ return;
+}
+
+/**
+ * @param {Object} state
+ * @param {Boolean} [state._askPreviousPassphrase] - don't ask for passphrase again
+ * @param {Array<String>} args
+ */
+async function setPassphrase({ _askPreviousPassphrase }, args) {
+ let date = getFsDateString();
+
+ // get the old passphrase
+ if (false !== _askPreviousPassphrase) {
+ await cmds.getPassphrase(null, []);
+ }
+
+ // 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 curShadow = await Fs.readFile(shadowPath, "utf8").catch(
+ emptyStringOnErrEnoent,
+ );
+
+ let newShadow = await Cipher.shadowPassphrase(newPassphrase);
+ await Fs.writeFile(shadowPath, newShadow, "utf8");
+
+ let rawKeys = await readAllKeys();
+ let encAddrs = rawKeys
+ .map(function (raw) {
+ if (raw.encrypted) {
+ return raw.addr;
+ }
+ })
+ .filter(Boolean);
+
+ // backup all currently encrypted files
+ //@ts-ignore
+ if (encAddrs.length) {
+ let filepath = Path.join(HOME, `${configdir}/keys.${date}.bak`);
+ console.info(``);
+ console.info(`Backing up previous (encrypted) keys:`);
+ encAddrs.unshift(curShadow);
+ await Fs.writeFile(filepath, encAddrs.join("\n") + "\n", "utf8");
+ console.info(` ~/${configdir}/keys.${date}.bak`);
+ console.info(``);
+ }
+ cmds._setPassphrase(newPassphrase);
+
+ await encryptAll(rawKeys, { rotateKey: true });
+
+ return newPassphrase;
+}
+
+/**
+ * Import and Encrypt
+ * @param {Null} _
+ * @param {Array<String>} args
+ */
+async function importKey(_, args) {
+ let keypath = args.shift() || "";
+ let key = await maybeReadKeyFileRaw(keypath);
+ if (!key?.wif) {
+ console.error(`no key found for '${keypath}'`);
+ process.exit(1);
return;
}
- await Fs.writeFile(filepath, wif, "utf8");
- console.info(``);
- console.info(
- `Use the QR Code below to load a test deposit of Đ${testDash} onto your staking key.`,
+ let encWif = await maybeEncrypt(key.wif);
+ let icon = "💾";
+ if (encWif.includes(":")) {
+ icon = "🔐";
+ }
+ let date = getFsDateString();
+
+ await safeSave(
+ Path.join(keysDir, `${key.addr}.wif`),
+ encWif,
+ Path.join(keysDir, `${key.addr}.${date}.bak`),
);
+
+ console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`);
console.info(``);
- showQr(pub, testDuff);
+}
+
+/**
+ * Encrypt ALL-the-things!
+ * @param {Object} [opts]
+ * @param {Boolean} opts.rotateKey
+ * @param {Array<RawKey>?} rawKeys
+ */
+async function encryptAll(rawKeys, opts) {
+ if (!rawKeys) {
+ rawKeys = await readAllKeys();
+ }
+ let date = getFsDateString();
+
+ console.info(`Encrypting...`);
console.info(``);
- console.info(
- `Use the QR Code above to load a test deposit of Đ${testDash} onto your staking key.`,
- );
+ 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);
+ 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(``);
- console.info(`Generated ${filepath} ${note}`);
- process.exit(0);
}
-async function load(psuedoState, args) {
- let name = args.shift();
+/**
+ * Decrypt ALL-the-things!
+ * @param {Array<RawKey>?} rawKeys
+ */
+async function decryptAll(rawKeys) {
+ if (!rawKeys) {
+ rawKeys = await readAllKeys();
+ }
+ let date = getFsDateString();
- // TODO factor out the common bits from generate?
- let pk = new Dashcore.PrivateKey();
+ console.info(``);
+ console.info(`Decrypting...`);
+ console.info(``);
+ await rawKeys.reduce(async function (promise, key) {
+ await promise;
- let pub = pk.toAddress().toString();
- let wif = pk.toWIF();
+ 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(``);
+}
- let filepath = `./${pub}.wif`;
- let note = "";
- if (name) {
- filepath = name;
- note = `\n(for pubkey address ${pub})`;
- }
+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) {
- console.info(`'${filepath}' already exists (will not overwrite)`);
+ await Fs.rename(filepath, bakpath);
+ }
+ await Fs.rename(tmpPath, filepath);
+ if (!err) {
+ await Fs.unlink(bakpath);
+ }
+}
+
+/**
+ * @param {Null} psuedoState
+ * @param {Array<String>} 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
+ * @property {Boolean} encrypted
+ * @property {String} wif
+ */
+
+/**
+ * @throws
+ */
+async function readAllKeys() {
+ let wifnames = await listManagedKeynames();
+
+ /** @type Array<RawKey> */
+ let keys = [];
+ await wifnames.reduce(async function (promise, wifname) {
+ await promise;
+
+ let keypath = Path.join(keysDir, wifname);
+ let key = await maybeReadKeyFileRaw(keypath);
+ if (!key?.wif) {
+ return;
+ }
+
+ if (`${key.addr}.wif` !== wifname) {
+ throw new Error(
+ `computed pubkey '${key.addr}' of WIF does not match filename '${keypath}'`,
+ );
+ }
+
+ keys.push(key);
+ }, Promise.resolve());
+
+ return keys;
+}
+
+/**
+ * @param {String} filepath
+ * @param {Object} [opts]
+ * @param {Boolean} opts.wif
+ * @returns {Promise<String>}
+ */
+async function maybeReadKeyFile(filepath, opts) {
+ let key = await maybeReadKeyFileRaw(filepath, opts);
+ if (false === opts?.wif) {
+ return key?.addr || "";
+ }
+ return key?.wif || "";
+}
+
+/**
+ * @param {String} filepath
+ * @param {Object} [opts]
+ * @param {Boolean} opts.wif
+ * @returns {Promise<RawKey?>}
+ */
+async function maybeReadKeyFileRaw(filepath, opts) {
+ let privKey = await Fs.readFile(filepath, "utf8").catch(
+ emptyStringOnErrEnoent,
+ );
+ privKey = privKey.trim();
+ if (!privKey) {
+ return null;
+ }
+
+ let encrypted = false;
+ if (privKey.includes(":")) {
+ encrypted = true;
+ try {
+ if (false !== opts?.wif) {
+ privKey = await decrypt(privKey);
+ }
+ } catch (err) {
+ //@ts-ignore
+ console.error(err.message);
+ console.error(`passphrase does not match for key ${filepath}`);
+ process.exit(1);
+ }
+ }
+ if (false === opts?.wif) {
+ return {
+ addr: Path.basename(filepath, ".wif"),
+ encrypted: encrypted,
+ wif: "",
+ };
+ }
+
+ let pk = new Dashcore.PrivateKey(privKey);
+ let pub = pk.toAddress().toString();
+
+ return {
+ addr: pub,
+ encrypted: encrypted,
+ wif: privKey,
+ };
+}
+
+/**
+ * @param {String} encWif
+ */
+async function decrypt(encWif) {
+ let passphrase = cmds._getPassphrase();
+ if (!passphrase) {
+ passphrase = await cmds.getPassphrase(null, []);
+ }
+ let key128 = await Cipher.deriveKey(passphrase);
+ let cipher = Cipher.create(key128);
+
+ return cipher.decrypt(encWif);
+}
+
+/**
+ * @param {String} plainWif
+ */
+async function maybeEncrypt(plainWif) {
+ let passphrase = cmds._getPassphrase();
+ if (!passphrase) {
+ passphrase = await cmds.getPassphrase(null, []);
+ }
+ if (!passphrase) {
+ return plainWif;
+ }
+
+ let key128 = await Cipher.deriveKey(passphrase);
+ let cipher = Cipher.create(key128);
+ return cipher.encrypt(plainWif);
+}
+
+/**
+ * @param {Null} _
+ * @param {Array<String>} args
+ */
+async function setDefault(_, args) {
+ let addr = args.shift() || "";
+
+ let keyname = await findWif(addr);
+ if (!keyname) {
+ console.error(`no key matches '${addr}'`);
+ process.exit(1);
+ return;
+ }
+
+ let filepath = Path.join(keysDir, keyname);
+ let wif = await maybeReadKeyFile(filepath);
+ let pk = new Dashcore.PrivateKey(wif);
+ let pub = pk.toAddress().toString();
+
+ console.info("set", defaultWifPath, pub);
+ await Fs.writeFile(defaultWifPath, pub, "utf8");
+}
+
+// TODO option to specify config dir
+
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {Array<String>} args
+ */
+async function listKeys({ dashApi }, args) {
+ let wifnames = await listManagedKeynames();
+
+ /**
+ * @type Array<{ node: String, error: Error }>
+ */
+ let warns = [];
+ console.info(``);
+ console.info(`Staking keys: (in ${keysDirRel}/)`);
+ console.info(``);
+ if (!wifnames.length) {
+ console.info(` (none)`);
+ }
+ 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;
+ }
+
+ /*
+ let pk = new Dashcore.PrivateKey(wif);
+ let pub = pk.toAddress().toString();
+ if (`${pub}.wif` !== wifname) {
+ // sanity check
+ warns.push({
+ node: wifname,
+ error: new Error(
+ `computed pubkey '${pub}' of WIF does not match filename '${wifname}'`,
+ ),
+ });
+ return;
+ }
+ */
+
+ process.stdout.write(` 🪙 ${addr}: `);
+ let balanceInfo = await dashApi.getInstantBalance(addr);
+ let balanceDash = toDash(balanceInfo.balanceSat);
+ console.info(`${balanceInfo.balanceSat} (Đ${balanceDash})`);
+ }, Promise.resolve());
+ console.info(``);
+
+ if (warns.length) {
+ console.warn(`Warnings:`);
+ warns.forEach(function (warn) {
+ console.warn(`${warn.node}: ${warn.error.message}`);
+ });
+ console.warn(``);
+ }
+}
+
+/**
+ * @param {String} name - ex: Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.wif.enc
+ */
+function isNamedLikeKey(name) {
+ // TODO distinguish with .enc extension?
+ let hasGoodLength = 34 + 4 === name.length || 34 + 4 + 4 === name.length;
+ let knownExt = name.endsWith(".wif") || name.endsWith(".wif.enc");
+ let isTmp = name.startsWith(".") || name.startsWith("_");
+ return hasGoodLength && knownExt && !isTmp;
+}
+
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {String} opts.defaultAddr
+ * @param {String} opts.insightBaseUrl
+ * @param {Array<String>} args
+ */
+async function removeKey({ dashApi, defaultAddr, insightBaseUrl }, args) {
+ let [addr, name] = await mustGetAddr({ defaultAddr }, args);
+ let balanceInfo = await dashApi.getInstantBalance(addr);
+
+ let balanceDash = toDash(balanceInfo.balanceSat);
+ if (balanceInfo.balanceSat) {
+ console.error(``);
+ console.error(`Error: ${addr}`);
+ console.error(
+ ` still has a balance of ${balanceInfo.balanceSat} (Đ${balanceDash})`,
+ );
+ console.error(` (transfer to another address before deleting)`);
+ console.error(``);
+ process.exit(1);
+ return;
+ }
+
+ await initCrowdNode(insightBaseUrl);
+ let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
+ if (!crowdNodeBalance) {
+ // may be janky if not registered
+ crowdNodeBalance = {};
+ }
+ if (!crowdNodeBalance.TotalBalance) {
+ //console.log('DEBUG', crowdNodeBalance);
+ crowdNodeBalance.TotalBalance = 0;
+ }
+ let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance);
+ if (crowdNodeBalance.TotalBalance) {
+ console.error(``);
+ console.error(`Error: ${addr}`);
+ console.error(
+ ` still staking ${crowdNodeBalance.TotalBalance} (Đ${crowdNodeDash}) on CrowdNode`,
+ );
+ console.error(
+ ` (withdrawal 100.0 and transfer to another address before deleting)`,
+ );
+ console.error(``);
+ process.exit(1);
+ return;
+ }
+
+ let wifname = await findWif(addr);
+ let filepath = Path.join(keysDir, wifname);
+ let wif = await maybeReadKeyPaths(name, { wif: true });
+
+ await Fs.unlink(filepath).catch(function (err) {
+ console.error(`could not remove ${filepath}: ${err.message}`);
+ process.exit(1);
+ });
+
+ let wifnames = await listManagedKeynames();
+ console.info(``);
+ console.info(`No balances found. Removing ${filepath}.`);
+ console.info(``);
+ console.info(`Backup (just in case):`);
+ console.info(` ${wif}`);
+ console.info(``);
+ if (!wifnames.length) {
+ console.info(`No keys left.`);
+ console.info(``);
} else {
- await Fs.writeFile(filepath, wif, "utf8");
- console.info(`Generated ${filepath} ${note}`);
+ let newAddr = wifnames[0];
+ console.info(`Selected ${newAddr} as new default staking key.`);
+ await Fs.writeFile(defaultWifPath, addr.replace(".wif", ""), "utf8");
+ console.info(``);
+ }
+}
+
+/**
+ * @param {String} pre
+ */
+async function findWif(pre) {
+ if (!pre) {
+ return "";
+ }
+
+ let names = await listManagedKeynames();
+ names = names.filter(function (name) {
+ return name.startsWith(pre);
+ });
+
+ if (!names.length) {
+ return "";
}
- let desiredAmountDash = parseFloat(args.shift() || 0);
+ if (names.length > 1) {
+ console.error(`'${pre}' is ambiguous:`, names.join(", "));
+ process.exit(1);
+ return "";
+ }
+
+ return names[0];
+}
+
+async function listManagedKeynames() {
+ let nodes = await Fs.readdir(keysDir);
+
+ return nodes.filter(isNamedLikeKey);
+}
+
+/**
+ * @param {Object} opts
+ * @param {String} opts.defaultAddr
+ * @param {String} opts.insightBaseUrl
+ * @param {Array<String>} args
+ */
+async function loadAddr({ defaultAddr, insightBaseUrl }, args) {
+ let [addr] = await mustGetAddr({ defaultAddr }, args);
+
+ let desiredAmountDash = parseFloat(args.shift() || "0");
let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
let effectiveDuff = desiredAmountDuff;
let effectiveDash = "";
effectiveDash = toDash(effectiveDuff);
// Round to the nearest mDash
// ex: 0.50238108 => 0.50300000
- effectiveDuff = toDuff(Math.ceil(effectiveDash * 1000) / 1000);
+ effectiveDuff = toDuff(
+ (Math.ceil(parseFloat(effectiveDash) * 1000) / 1000).toString(),
+ );
effectiveDash = toDash(effectiveDuff);
}
console.info(``);
- showQr(pub, effectiveDuff);
+ showQr(addr, effectiveDuff);
console.info(``);
console.info(
`Use the QR Code above to load ${effectiveDuff} (Đ${effectiveDash}) onto your staking key.`,
console.info(``);
console.info(`(waiting...)`);
console.info(``);
- let payment = await Ws.waitForVout(psuedoState.insightBaseUrl, pub, 0);
+ let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
console.info(`Received ${payment.satoshis}`);
process.exit(0);
}
-async function balance(args, state) {
- console.info(state.balanceInfo);
+/**
+ * @param {Object} opts
+ * @param {String} opts.defaultAddr
+ * @param {any} opts.dashApi - TODO
+ * @param {Array<String>} 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);
return;
}
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {String} opts.defaultAddr
+ * @param {Boolean} opts.forceConfirm
+ * @param {String} opts.insightBaseUrl
+ * @param {any} opts.insightApi
+ * @param {Array<String>} args
+ */
// ex: node ./bin/crowdnode.js transfer ./priv.wif 'pub' 0.01
-async function transfer(args, state) {
- let newAddr = await wifFileToAddr(process.argv[4]);
- let dashAmount = parseFloat(process.argv[5] || 0);
+async function transferBalance(
+ { dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
+ args,
+) {
+ let wif = await mustGetWif({ defaultAddr }, args);
+
+ let keyname = args.shift() || "";
+ let newAddr = await wifFileToAddr(keyname);
+ let dashAmount = parseFloat(args.shift() || "0");
let duffAmount = Math.round(dashAmount * DUFFS);
let tx;
if (duffAmount) {
- tx = await state.dashApi.createPayment(state.privKey, newAddr, duffAmount);
+ tx = await dashApi.createPayment(wif, newAddr, duffAmount);
} else {
- tx = await state.dashApi.createBalanceTransfer(state.privKey, newAddr);
+ tx = await dashApi.createBalanceTransfer(wif, newAddr);
}
if (duffAmount) {
let dashAmountStr = toDash(duffAmount);
} else {
console.info(`Transferring balance to ${newAddr}...`);
}
- await state.insightApi.instantSend(tx);
+ await insightApi.instantSend(tx);
console.info(`Queued...`);
setTimeout(function () {
// TODO take a cleaner approach
// (waitForVout needs a reasonable timeout)
console.error(`Error: Transfer did not complete.`);
- if (state.forceConfirm) {
+ if (forceConfirm) {
console.error(`(using --unconfirmed may lead to rejected double spends)`);
}
process.exit(1);
}, 30 * 1000);
- await Ws.waitForVout(state.insightBaseUrl, newAddr, 0);
+ await Ws.waitForVout(insightBaseUrl, newAddr, 0);
console.info(`Accepted!`);
process.exit(0);
return;
}
-async function status(args, state) {
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {String} opts.defaultAddr
+ * @param {String} opts.insightBaseUrl
+ * @param {Array<String>} args
+ */
+async function getStatus({ dashApi, defaultAddr, insightBaseUrl }, args) {
+ let [addr] = await mustGetAddr({ defaultAddr }, args);
+ await initCrowdNode(insightBaseUrl);
+ let hotwallet = CrowdNode.main.hotwallet;
+ let state = await getCrowdNodeStatus({ addr, hotwallet });
+
console.info();
- console.info(`API Actions Complete for ${state.pub}:`);
+ console.info(`API Actions Complete for ${addr}:`);
console.info(` ${state.signup} SignUpForApi`);
console.info(` ${state.accept} AcceptTerms`);
console.info(` ${state.deposit} DepositReceived`);
console.info();
- let pk = new Dashcore.PrivateKey(state.privKey);
- let pub = pk.toPublicKey().toAddress().toString();
- let crowdNodeBalance = await CrowdNode.http.GetBalance(pub);
+ let crowdNodeBalance = await CrowdNode.http.GetBalance(addr);
+ // may be unregistered / undefined
+ /*
+ * {
+ * '@odata.context': 'https://app.crowdnode.io/odata/$metadata#Edm.String',
+ * value: 'Address not found.'
+ * }
+ */
+ if (!crowdNodeBalance.TotalBalance) {
+ crowdNodeBalance.TotalBalance = 0;
+ }
let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance);
console.info(
`CrowdNode Stake: ${crowdNodeBalance.TotalBalance} (Đ${crowdNodeDash})`,
return;
}
-async function signup(args, state) {
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {String} opts.defaultAddr
+ * @param {String} opts.insightBaseUrl
+ * @param {Array<String>} args
+ */
+async function sendSignup({ dashApi, defaultAddr, insightBaseUrl }, args) {
+ let [addr, name] = await mustGetAddr({ defaultAddr }, args);
+ await initCrowdNode(insightBaseUrl);
+ let hotwallet = CrowdNode.main.hotwallet;
+ let state = await getCrowdNodeStatus({ addr, hotwallet });
+ let balanceInfo = await checkBalance({ addr, dashApi });
+
if (state.status?.signup) {
- console.info(
- `${state.pub} is already signed up. Here's the account status:`,
- );
+ console.info(`${addr} is already signed up. Here's the account status:`);
console.info(` ${state.signup} SignUpForApi`);
console.info(` ${state.accept} AcceptTerms`);
console.info(` ${state.deposit} DepositReceived`);
return;
}
- let hasEnough = state.balanceInfo.balanceSat > signupOnly + feeEstimate;
+ let hasEnough = balanceInfo.balanceSat > signupOnly + feeEstimate;
if (!hasEnough) {
- await collectSignupFees(state.insightBaseUrl, state.pub);
+ await collectSignupFees(insightBaseUrl, addr);
}
+
+ let wif = await maybeReadKeyPaths(name, { wif: true });
+
console.info("Requesting account...");
- await CrowdNode.signup(state.privKey, state.hotwallet);
+ await CrowdNode.signup(wif, hotwallet);
state.signup = "✅";
console.info(` ${state.signup} SignUpForApi`);
console.info(` ${state.accept} AcceptTerms`);
return;
}
-async function accept(args, state) {
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {String} opts.defaultAddr
+ * @param {String} opts.insightBaseUrl
+ * @param {Array<String>} args
+ */
+async function acceptTerms({ dashApi, defaultAddr, insightBaseUrl }, args) {
+ let [addr, name] = await mustGetAddr({ defaultAddr }, args);
+
+ await initCrowdNode(insightBaseUrl);
+ let hotwallet = CrowdNode.main.hotwallet;
+ let state = await getCrowdNodeStatus({ addr, hotwallet });
+ let balanceInfo = await dashApi.getInstantBalance(addr);
+
+ if (!state.status?.signup) {
+ console.info(`${addr} is not signed up yet. Here's the account status:`);
+ console.info(` ${state.signup} SignUpForApi`);
+ console.info(` ${state.accept} AcceptTerms`);
+ process.exit(1);
+ return;
+ }
+
if (state.status?.accept) {
- console.info(
- `${state.pub} is already signed up. Here's the account status:`,
- );
+ console.info(`${addr} is already signed up. Here's the account status:`);
console.info(` ${state.signup} SignUpForApi`);
console.info(` ${state.accept} AcceptTerms`);
console.info(` ${state.deposit} DepositReceived`);
process.exit(0);
return;
}
- let hasEnough = state.balanceInfo.balanceSat > acceptOnly + feeEstimate;
+ let hasEnough = balanceInfo.balanceSat > acceptOnly + feeEstimate;
if (!hasEnough) {
- await collectSignupFees(state.insightBaseUrl, state.pub);
+ await collectSignupFees(insightBaseUrl, addr);
}
+
+ let wif = await maybeReadKeyPaths(name, { wif: true });
+
console.info("Accepting terms...");
- await CrowdNode.accept(state.privKey, state.hotwallet);
+ await CrowdNode.accept(wif, hotwallet);
state.accept = "✅";
console.info(` ${state.signup} SignUpForApi`);
console.info(` ${state.accept} AcceptTerms`);
return;
}
-async function deposit(args, state) {
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {String} opts.defaultAddr
+ * @param {String} opts.insightBaseUrl
+ * @param {Boolean} opts.noReserve
+ * @param {Array<String>} args
+ */
+async function depositDash(
+ { dashApi, defaultAddr, insightBaseUrl, noReserve },
+ args,
+) {
+ let [addr, name] = await mustGetAddr({ defaultAddr }, args);
+ await initCrowdNode(insightBaseUrl);
+ let hotwallet = CrowdNode.main.hotwallet;
+ let state = await getCrowdNodeStatus({ addr, hotwallet });
+ let balanceInfo = await dashApi.getInstantBalance(addr);
+
if (!state.status?.accept) {
- console.error(`no account for address ${state.pub}`);
+ console.error(`no account for address ${addr}`);
process.exit(1);
return;
}
// this would allow for at least 2 withdrawals costing (21000 + 1000)
let reserve = 50000;
let reserveDash = toDash(reserve);
- if (!state.noReserve) {
+ if (!noReserve) {
console.info(
`reserving ${reserve} (Đ${reserveDash}) for withdrawals (--no-reserve to disable)`,
);
// deposit what the user asks, or all that we have,
// or all that the user deposits - but at least 2x the reserve
- let desiredAmountDash = parseFloat(args.shift() || 0);
+ let desiredAmountDash = parseFloat(args.shift() || "0");
let desiredAmountDuff = Math.round(desiredAmountDash * DUFFS);
let effectiveAmount = desiredAmountDuff;
if (!effectiveAmount) {
- effectiveAmount = state.balanceInfo.balanceSat - reserve;
+ effectiveAmount = balanceInfo.balanceSat - reserve;
}
let needed = Math.max(reserve * 2, effectiveAmount + reserve);
- if (state.balanceInfo.balanceSat < needed) {
+ if (balanceInfo.balanceSat < needed) {
let ask = 0;
if (desiredAmountDuff) {
- ask = desiredAmountDuff + reserve + -state.balanceInfo.balanceSat;
+ ask = desiredAmountDuff + reserve + -balanceInfo.balanceSat;
}
- await collectDeposit(state.insightBaseUrl, state.pub, ask);
- state.balanceInfo = await state.dashApi.getInstantBalance(state.pub);
- if (state.balanceInfo.balanceSat < needed) {
- let balanceDash = toDash(state.balanceInfo.balanceSat);
+ await collectDeposit(insightBaseUrl, addr, ask);
+ balanceInfo = await dashApi.getInstantBalance(addr);
+ if (balanceInfo.balanceSat < needed) {
+ let balanceDash = toDash(balanceInfo.balanceSat);
console.error(
- `Balance is still too small: ${state.balanceInfo.balanceSat} (Đ${balanceDash})`,
+ `Balance is still too small: ${balanceInfo.balanceSat} (Đ${balanceDash})`,
);
process.exit(1);
return;
}
}
if (!desiredAmountDuff) {
- effectiveAmount = state.balanceInfo.balanceSat - reserve;
+ effectiveAmount = balanceInfo.balanceSat - reserve;
}
let effectiveDash = toDash(effectiveAmount);
console.info(
`Initiating deposit of ${effectiveAmount} (Đ${effectiveDash})...`,
);
- await CrowdNode.deposit(state.privKey, state.hotwallet, effectiveAmount);
+
+ let wif = await maybeReadKeyPaths(name, { wif: true });
+
+ await CrowdNode.deposit(wif, hotwallet, effectiveAmount);
state.deposit = "✅";
console.info(` ${state.deposit} DepositReceived`);
process.exit(0);
return;
}
-async function withdrawal(args, state) {
+/**
+ * @param {Object} opts
+ * @param {any} opts.dashApi - TODO
+ * @param {String} opts.defaultAddr
+ * @param {String} opts.insightBaseUrl
+ * @param {Array<String>} args
+ */
+async function withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args) {
+ let [addr] = await mustGetAddr({ defaultAddr }, args);
+ await initCrowdNode(insightBaseUrl);
+ let hotwallet = CrowdNode.main.hotwallet;
+ let state = await getCrowdNodeStatus({ addr, hotwallet });
+
if (!state.status?.accept) {
- console.error(`no account for address ${state.pub}`);
+ console.error(`no account for address ${addr}`);
process.exit(1);
return;
}
let realPercentStr = (permil / 10).toFixed(1);
console.info(`Initiating withdrawal of ${realPercentStr}...`);
- let paid = await CrowdNode.withdrawal(state.privKey, state.hotwallet, permil);
+ let wifname = await findWif(addr);
+ let filepath = Path.join(keysDir, wifname);
+ let wif = await maybeReadKeyFile(filepath);
+ let paid = await CrowdNode.withdrawal(wif, hotwallet, permil);
//let paidFloat = (paid.satoshis / DUFFS).toFixed(8);
//let paidInt = paid.satoshis.toString().padStart(9, "0");
console.info(`API Response: ${paid.api}`);
return;
}
-/*
-async function stake(args, state) {
- // TODO
- return;
-}
-*/
-
// Helpers
-async function wifFileToAddr(keyfile) {
- let privKey = keyfile;
-
- let err = await Fs.access(keyfile).catch(Object);
- if (!err) {
- privKey = await Fs.readFile(keyfile, "utf8");
- privKey = privKey.trim();
- }
-
- if (34 === privKey.length) {
+/**
+ * Convert prefix, addr, keyname, or filepath to pub addr
+ * @param {String} name
+ * @throws
+ */
+async function wifFileToAddr(name) {
+ if (34 === name.length) {
// actually payment addr
- return privKey;
+ return name;
}
- if (52 === privKey.length) {
- let pk = new Dashcore.PrivateKey(privKey);
- let pub = pk.toPublicKey().toAddress().toString();
- return pub;
+ let privKey = "";
+
+ let wifname = await findWif(name);
+ if (wifname) {
+ let filepath = Path.join(keysDir, wifname);
+ privKey = await maybeReadKeyFile(filepath);
+ }
+ if (!privKey) {
+ privKey = await maybeReadKeyFile(name);
+ }
+ if (!privKey) {
+ throw new Error("bad file path or address");
}
- throw new Error("bad file path or address");
+ let pk = new Dashcore.PrivateKey(privKey);
+ let pub = pk.toPublicKey().toAddress().toString();
+ return pub;
}
-async function collectSignupFees(insightBaseUrl, pub) {
+/**
+ * @param {String} insightBaseUrl
+ * @param {String} addr
+ */
+async function collectSignupFees(insightBaseUrl, addr) {
console.info(``);
- showQr(pub);
+ showQr(addr);
let signupTotalDash = toDash(signupTotal);
let signupMsg = `Please send >= ${signupTotal} (Đ${signupTotalDash}) to Sign Up to CrowdNode`;
console.info("");
console.info("(waiting...)");
console.info("");
- let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
+ let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
console.info(`Received ${payment.satoshis}`);
}
-async function collectDeposit(insightBaseUrl, pub, duffAmount) {
+/**
+ * @param {String} insightBaseUrl
+ * @param {String} addr
+ * @param {Number} duffAmount
+ */
+async function collectDeposit(insightBaseUrl, addr, duffAmount) {
console.info(``);
- showQr(pub, duffAmount);
+ showQr(addr, duffAmount);
- let depositMsg = `Please send what you wish to deposit to ${pub}`;
+ let depositMsg = `Please send what you wish to deposit to ${addr}`;
if (duffAmount) {
- let depositDash = toDash(duffAmount);
- depositMsg = `Please deposit ${duffAmount} (Đ${depositDash}) to ${pub}`;
+ let dashAmount = toDash(duffAmount);
+ depositMsg = `Please deposit ${duffAmount} (Đ${dashAmount}) to ${addr}`;
}
let msgPad = Math.ceil((qrWidth - depositMsg.length) / 2);
console.info("");
console.info("(waiting...)");
console.info("");
- let payment = await Ws.waitForVout(insightBaseUrl, pub, 0);
+ let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
console.info(`Received ${payment.satoshis}`);
}
+/**
+ * @param {Error & { code: String }} err
+ * @throws
+ */
+function emptyStringOnErrEnoent(err) {
+ if ("ENOENT" !== err.code) {
+ throw err;
+ }
+ return "";
+}
+
+/**
+ * @param {Number} duffs - ex: 00000000
+ */
function toDash(duffs) {
return (duffs / DUFFS).toFixed(8);
}
+/**
+ * @param {String} dash - ex: 0.00000000
+ */
function toDuff(dash) {
return Math.round(parseFloat(dash) * DUFFS);
}