Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions bin/create-tx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env node
"use strict";

let Fs = require("fs").promises;

let Dash = require("../lib/dash.js");
let Insight = require("../lib/insight.js");

async function main() {
let insightBaseUrl =
process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
let insightApi = Insight.create({ baseUrl: insightBaseUrl });
let dashApi = Dash.create({ insightApi: insightApi });

let wiffilename = process.argv[2] || "";
if (!wiffilename) {
console.error(`Usage: pay ./source.wif ./targets.csv ./change.b58c`);
process.exit(1);
return;
}
let wif = await Fs.readFile(wiffilename, "utf8");
wif = wif.trim();

let payfilename = process.argv[3] || "";
if (!payfilename) {
console.error(`Usage: pay ./source.wif ./targets.csv ./change.b58c`);
process.exit(1);
return;
}
let paymentsCsv = await Fs.readFile(payfilename, "utf8");
paymentsCsv = paymentsCsv.trim();
/** @type {Array<{ address: String, satoshis: Number }>} */
//@ts-ignore
let payments = paymentsCsv
.split(/\n/)
.map(function (line) {
line = line.trim();
if (!line) {
return null;
}

if (
line.startsWith("#") ||
line.startsWith("//") ||
line.startsWith("-") ||
line.startsWith('"') ||
line.startsWith("'")
) {
return null;
}

let parts = line.split(",");
let addr = parts[0] || "";
let amount = Dash.toDuff(parts[1] || "");

if (!addr.startsWith("X")) {
console.error(`unknown address: ${addr}`);
process.exit(1);
return null;
}

if (isNaN(amount) || !amount) {
console.error(`unknown amount: ${amount}`);
return null;
}

return {
address: addr,
satoshis: amount,
};
})
.filter(Boolean);

let changefilename = process.argv[4] || "";
if (!changefilename) {
console.error(`Usage: pay ./source.wif ./targets.csv ./change.b58c`);
process.exit(1);
return;
}
let changeAddr = await Fs.readFile(changefilename, "utf8");
changeAddr = changeAddr.trim();

let tx = await dashApi.createPayments(wif, payments, changeAddr);
console.info('Transaction:');
console.info(tx.serialize());

if (!process.argv.includes("--send")) {
return;
}

console.info('Instant Send...');
await insightApi.instantSend(tx.serialize());
console.info('Done');
}

// Run
main().catch(function (err) {
console.error("Fail:");
console.error(err.stack || err);
process.exit(1);
});
92 changes: 87 additions & 5 deletions lib/dash.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ Dash.create = function ({

// TODO make more accurate?
let feePreEstimate = 1000;
let utxos = await getOptimalUtxos(utxoAddr, amount + feePreEstimate);
let body = await insightApi.getUtxos(utxoAddr);
let coreUtxos = await getUtxos(body);
let utxos = await getOptimalUtxos(coreUtxos, amount + feePreEstimate);
let balance = getBalance(utxos);

if (!utxos.length) {
Expand Down Expand Up @@ -145,16 +147,89 @@ Dash.create = function ({
return tx;
};

/**
* @typedef {Object} CorePayment
* @property {(String|import('@dashevo/dashcore-lib').Address)} address
* @property {Number} satoshis
*/

/**
* Send with change back
* @param {String} privKey
* @param {Array<CorePayment>} payments
* @param {(String|import('@dashevo/dashcore-lib').Address)} [changeAddr]
*/
dashApi.createPayments = async function (privKey, payments, changeAddr) {
let pk = new Dashcore.PrivateKey(privKey);
let utxoAddr = pk.toPublicKey().toAddress().toString();
if (!changeAddr) {
changeAddr = utxoAddr;
}

// TODO make more accurate?
let amount = payments.reduce(function (total, pay) {
return pay.satoshis;
}, 0);
let body = await insightApi.getUtxos(utxoAddr);
let coreUtxos = await getUtxos(body);
let feePreEstimate = 150 * (payments.length + coreUtxos.length);
let utxos = await getOptimalUtxos(coreUtxos, 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;
}

console.log("DEBUG");
console.log(payments, changeAddr);

//@ts-ignore - no input required, actually
let tmpTx = new Transaction()
//@ts-ignore - allows single value or array
.from(utxos);
// TODO update jsdoc for dashcore
tmpTx.to(payments, 0);
//@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(payments, 0);
tx.fee(fee);
//@ts-ignore - see above
tx.change(changeAddr);
tx.sign(pk);

return tx;
};

// TODO make more optimal
/**
* @param {String} utxoAddr
* @param {Array<CoreUtxo>} utxos
* @param {Number} fullAmount - including fee estimate
*/
async function getOptimalUtxos(utxoAddr, fullAmount) {
async function getOptimalUtxos(utxos, 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) {
Expand Down Expand Up @@ -244,3 +319,10 @@ Dash.create = function ({

return dashApi;
};

/**
* @param {String} dash - ex: 0.00000000
*/
Dash.toDuff = function (dash) {
return Math.round(parseFloat(dash) * DUFFS);
};