Skip to content

Commit

Permalink
WIP: feat: (1.5 hour) send payment to multiple outputs from CSV
Browse files Browse the repository at this point in the history
  • Loading branch information
AJ ONeal committed Jul 4, 2022
1 parent ada909f commit c3a36db
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 5 deletions.
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);
};

0 comments on commit c3a36db

Please sign in to comment.