UNPKG

crowdnode

Version:

Manage your stake in Đash with the CrowdNode Blockchain API

267 lines (225 loc) 7.27 kB
(function (exports) { "use strict"; let Dash = {}; //@ts-ignore exports.DashApi = Dash; const DUFFS = 100000000; const DUST = 10000; const FEE = 1000; //@ts-ignore let Dashcore = exports.dashcore || require("./lib/dashcore.js"); let Transaction = Dashcore.Transaction; Dash.create = function ({ //@ts-ignore TODO insightApi, }) { let dashApi = {}; /** * Instant Balance is accurate with Instant Send * @param {String} address * @returns {Promise<InstantBalance>} */ dashApi.getInstantBalance = async function (address) { let body = await insightApi.getUtxos(address); let utxos = await getUtxos(body); let balance = utxos.reduce(function (total, utxo) { return total + utxo.satoshis; }, 0); // because 0.1 + 0.2 = 0.30000000000000004, // but we would only want 0.30000000 let floatBalance = parseFloat((balance / DUFFS).toFixed(8)); return { addrStr: address, balance: floatBalance, balanceSat: balance, _utxoCount: utxos.length, _utxoAmounts: utxos.map(function (utxo) { return utxo.satoshis; }), }; }; /** * Full Send! * @param {String} privKey * @param {String} pub */ dashApi.createBalanceTransfer = async function (privKey, pub) { let pk = new Dashcore.PrivateKey(privKey); let changeAddr = pk.toPublicKey().toAddress().toString(); let body = await insightApi.getUtxos(changeAddr); let utxos = await getUtxos(body); let balance = utxos.reduce(function (total, utxo) { return total + utxo.satoshis; }, 0); //@ts-ignore - no input required, actually let tmpTx = new Transaction() //@ts-ignore - allows single value or array .from(utxos); tmpTx.to(pub, balance - 1000); tmpTx.sign(pk); // TODO getsmartfeeestimate?? // fee = 1duff/byte (2 chars hex is 1 byte) // +10 to be safe (the tmpTx may be a few bytes off) let fee = 10 + tmpTx.toString().length / 2; //@ts-ignore - no input required, actually let tx = new Transaction() //@ts-ignore - allows single value or array .from(utxos); tx.to(pub, balance - fee); tx.fee(fee); tx.sign(pk); return tx; }; /** * Send with change back * @param {String} privKey * @param {(String|import('@dashevo/dashcore-lib').Address)} payAddr * @param {Number} amount * @param {(String|import('@dashevo/dashcore-lib').Address)} [changeAddr] */ dashApi.createPayment = async function ( privKey, payAddr, amount, changeAddr, ) { let pk = new Dashcore.PrivateKey(privKey); let utxoAddr = pk.toPublicKey().toAddress().toString(); if (!changeAddr) { changeAddr = utxoAddr; } // TODO make more accurate? let feePreEstimate = 1000; let utxos = await getOptimalUtxos(utxoAddr, amount + feePreEstimate); let balance = getBalance(utxos); if (!utxos.length) { throw new Error(`not enough funds available in utxos for ${utxoAddr}`); } // (estimate) don't send dust back as change if (balance - amount <= DUST + FEE) { amount = balance; } //@ts-ignore - no input required, actually let tmpTx = new Transaction() //@ts-ignore - allows single value or array .from(utxos); tmpTx.to(payAddr, amount); //@ts-ignore - the JSDoc is wrong in dashcore-lib/lib/transaction/transaction.js tmpTx.change(changeAddr); tmpTx.sign(pk); // TODO getsmartfeeestimate?? // fee = 1duff/byte (2 chars hex is 1 byte) // +10 to be safe (the tmpTx may be a few bytes off - probably only 4 - // due to how small numbers are encoded) let fee = 10 + tmpTx.toString().length / 2; // (adjusted) don't send dust back as change if (balance + -amount + -fee <= DUST) { amount = balance - fee; } //@ts-ignore - no input required, actually let tx = new Transaction() //@ts-ignore - allows single value or array .from(utxos); tx.to(payAddr, amount); tx.fee(fee); //@ts-ignore - see above tx.change(changeAddr); tx.sign(pk); return tx; }; // TODO make more optimal /** * @param {String} utxoAddr * @param {Number} fullAmount - including fee estimate */ async function getOptimalUtxos(utxoAddr, fullAmount) { // get smallest coin larger than transaction // if that would create dust, donate it as tx fee let body = await insightApi.getUtxos(utxoAddr); let utxos = await getUtxos(body); let balance = getBalance(utxos); if (balance < fullAmount) { return []; } // from largest to smallest utxos.sort(function (a, b) { return b.satoshis - a.satoshis; }); /** @type Array<CoreUtxo> */ let included = []; let total = 0; // try to get just one utxos.every(function (utxo) { if (utxo.satoshis > fullAmount) { included[0] = utxo; total = utxo.satoshis; return true; } return false; }); if (total) { return included; } // try to use as few coins as possible utxos.some(function (utxo) { included.push(utxo); total += utxo.satoshis; return total >= fullAmount; }); return included; } /** * @param {Array<CoreUtxo>} utxos */ function getBalance(utxos) { return utxos.reduce(function (total, utxo) { return total + utxo.satoshis; }, 0); } /** * @param {Array<InsightUtxo>} body * @returns {Promise<Array<CoreUtxo>>} */ async function getUtxos(body) { /** @type Array<CoreUtxo> */ let utxos = []; await body.reduce(async function (promise, utxo) { await promise; let data = await insightApi.getTx(utxo.txid); // TODO the ideal would be the smallest amount that is greater than the required amount let utxoIndex = -1; /** * @template {InsightTxVout} T * @param {T} vout * @param {Number} index * @returns {Boolean} */ function findAndSetUtxoIndex(vout, index) { if (!vout.scriptPubKey?.addresses?.includes(utxo.address)) { return false; } let satoshis = Math.round(parseFloat(vout.value) * DUFFS); if (utxo.satoshis !== satoshis) { return false; } utxoIndex = index; return true; } data.vout.some(findAndSetUtxoIndex); // TODO test without txid utxos.push({ txId: utxo.txid, outputIndex: utxoIndex, address: utxo.address, script: utxo.scriptPubKey, satoshis: utxo.satoshis, }); }, Promise.resolve()); return utxos; } return dashApi; }; if ("undefined" !== typeof module) { module.exports = Dash; } })(("undefined" !== typeof module && module.exports) || window);