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
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]");
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");
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");
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);
+ 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);
+ await importKey(null, args);
+ process.exit(0);
return;
}
if ("encrypt" === subcommand) {
let addr = args.shift() || "";
if (!addr) {
- encryptAll(null);
+ await encryptAll(null);
+ process.exit(0);
return;
}
if (!key) {
throw new Error("impossible error");
}
- encryptAll([key]);
+ await encryptAll([key]);
+ process.exit(0);
return;
}
await Fs.writeFile(shadowPath, NO_SHADOW, "utf8").catch(
emptyStringOnErrEnoent,
);
+ process.exit(0);
return;
}
let keypath = await findWif(addr);
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;
}
{ dashApi, defaultAddr, forceConfirm, insightBaseUrl, insightApi },
args,
);
+ process.exit(0);
return;
}
} else {
console.info(JSON.stringify(result, null, 2));
}
+ process.exit(0);
return;
}
if ("load" === subcommand) {
await loadAddr({ defaultAddr, insightBaseUrl }, args);
+ process.exit(0);
return;
}
if ("rm" === subcommand || "delete" === subcommand) {
await initCrowdNode(insightBaseUrl);
await removeKey({ defaultAddr, dashApi, insightBaseUrl }, 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;
}
{ dashApi, defaultAddr, insightBaseUrl, noReserve },
args,
);
+ process.exit(0);
return;
}
if ("withdrawal" === subcommand) {
await withdrawalDash({ dashApi, defaultAddr, insightBaseUrl }, args);
+ process.exit(0);
return;
}
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<String>} args
+ */
+async function stakeDash(
+ {
+ dashApi,
+ defaultAddr,
+ forceGenerate,
+ insightApi,
+ insightBaseUrl,
+ noReserve,
+ },
+ args,
+) {
+ let err = await Fs.access(args[0]).catch(Object);
+ let addr;
+ if (!err) {
+ addr = await importKey(null, [args[0]]);
+ } 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 }, []);
+ }
+ await acceptTerms({ dashApi, defaultAddr: addr, insightBaseUrl }, []);
+ }
+
+ await depositDash(
+ { dashApi, defaultAddr: addr, insightBaseUrl, noReserve },
+ 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",
}
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();
*/
async function getCrowdNodeStatus({ addr, hotwallet }) {
let state = {
- signup: "❌",
- accept: "❌",
- deposit: "❌",
+ signup: TODO,
+ accept: TODO,
+ deposit: TODO,
status: {
signup: 0,
accept: 0,
};
//@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;
}
*/
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) {
// misnomering wif here a bit
defaultWif = raw?.wif || raw?.addr || "";
}
- if (defaultWif) {
- console.info(`selected default staking key ${defaultAddr}`);
+ if (defaultWif && !shownDefault) {
+ shownDefault = true;
+ console.info(`Selected default staking key ${defaultAddr}`);
return defaultWif;
}
/**
* @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<String>} args
*/
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;
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({}, []);
+ }
}
/**
console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`);
console.info(``);
-}
-
-/**
- * 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();
- 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<RawKey>?} 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;
}
/**
};
};
+/**
+ * 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();
+
+ 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<RawKey>?} 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
*/
let warns = [];
console.info(``);
+ console.info(`🔑Holdings 🪧 Stakings 💸Earnings`);
+ console.info(``);
console.info(`Staking keys: (in ${keysDirRel}/)`);
console.info(``);
if (!wifnames.length) {
}
*/
- 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}💸`,
+ );
+
+ console.info();
}, Promise.resolve());
console.info(``);
crowdNodeBalance = {};
}
if (!crowdNodeBalance.TotalBalance) {
- //console.log('DEBUG', crowdNodeBalance);
crowdNodeBalance.TotalBalance = 0;
}
let crowdNodeDash = toDash(crowdNodeBalance.TotalBalance);
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(``);
console.info(``);
let payment = await Ws.waitForVout(insightBaseUrl, addr, 0);
console.info(`Received ${payment.satoshis}`);
- process.exit(0);
}
/**
*/
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;
}
}, 30 * 1000);
await Ws.waitForVout(insightBaseUrl, newAddr, 0);
console.info(`Accepted!`);
- process.exit(0);
return;
}
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;
}
console.info(` ${state.signup} SignUpForApi`);
console.info(` ${state.accept} AcceptTerms`);
console.info(` ${state.deposit} DepositReceived`);
- process.exit(0);
return;
}
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;
}
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;
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;
}
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;
}
//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;
}
return (duffs / DUFFS).toFixed(8);
}
+/**
+ * @param {Number} duffs - ex: 00000000
+ */
+function toDASH(duffs) {
+ let dash = (duffs / DUFFS).toFixed(8);
+ return `Đ${dash}`.padStart(13, " ");
+}
+
/**
* @param {String} dash - ex: 0.00000000
*/