Skip to content

Commit c3a36db

Browse files
author
AJ ONeal
committed
WIP: feat: (1.5 hour) send payment to multiple outputs from CSV
1 parent ada909f commit c3a36db

File tree

2 files changed

+188
-5
lines changed

2 files changed

+188
-5
lines changed

bin/create-tx.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
let Fs = require("fs").promises;
5+
6+
let Dash = require("../lib/dash.js");
7+
let Insight = require("../lib/insight.js");
8+
9+
async function main() {
10+
let insightBaseUrl =
11+
process.env.INSIGHT_BASE_URL || "https://insight.dash.org";
12+
let insightApi = Insight.create({ baseUrl: insightBaseUrl });
13+
let dashApi = Dash.create({ insightApi: insightApi });
14+
15+
let wiffilename = process.argv[2] || "";
16+
if (!wiffilename) {
17+
console.error(`Usage: pay ./source.wif ./targets.csv ./change.b58c`);
18+
process.exit(1);
19+
return;
20+
}
21+
let wif = await Fs.readFile(wiffilename, "utf8");
22+
wif = wif.trim();
23+
24+
let payfilename = process.argv[3] || "";
25+
if (!payfilename) {
26+
console.error(`Usage: pay ./source.wif ./targets.csv ./change.b58c`);
27+
process.exit(1);
28+
return;
29+
}
30+
let paymentsCsv = await Fs.readFile(payfilename, "utf8");
31+
paymentsCsv = paymentsCsv.trim();
32+
/** @type {Array<{ address: String, satoshis: Number }>} */
33+
//@ts-ignore
34+
let payments = paymentsCsv
35+
.split(/\n/)
36+
.map(function (line) {
37+
line = line.trim();
38+
if (!line) {
39+
return null;
40+
}
41+
42+
if (
43+
line.startsWith("#") ||
44+
line.startsWith("//") ||
45+
line.startsWith("-") ||
46+
line.startsWith('"') ||
47+
line.startsWith("'")
48+
) {
49+
return null;
50+
}
51+
52+
let parts = line.split(",");
53+
let addr = parts[0] || "";
54+
let amount = Dash.toDuff(parts[1] || "");
55+
56+
if (!addr.startsWith("X")) {
57+
console.error(`unknown address: ${addr}`);
58+
process.exit(1);
59+
return null;
60+
}
61+
62+
if (isNaN(amount) || !amount) {
63+
console.error(`unknown amount: ${amount}`);
64+
return null;
65+
}
66+
67+
return {
68+
address: addr,
69+
satoshis: amount,
70+
};
71+
})
72+
.filter(Boolean);
73+
74+
let changefilename = process.argv[4] || "";
75+
if (!changefilename) {
76+
console.error(`Usage: pay ./source.wif ./targets.csv ./change.b58c`);
77+
process.exit(1);
78+
return;
79+
}
80+
let changeAddr = await Fs.readFile(changefilename, "utf8");
81+
changeAddr = changeAddr.trim();
82+
83+
let tx = await dashApi.createPayments(wif, payments, changeAddr);
84+
console.info('Transaction:');
85+
console.info(tx.serialize());
86+
87+
if (!process.argv.includes("--send")) {
88+
return;
89+
}
90+
91+
console.info('Instant Send...');
92+
await insightApi.instantSend(tx.serialize());
93+
console.info('Done');
94+
}
95+
96+
// Run
97+
main().catch(function (err) {
98+
console.error("Fail:");
99+
console.error(err.stack || err);
100+
process.exit(1);
101+
});

lib/dash.js

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ Dash.create = function ({
100100

101101
// TODO make more accurate?
102102
let feePreEstimate = 1000;
103-
let utxos = await getOptimalUtxos(utxoAddr, amount + feePreEstimate);
103+
let body = await insightApi.getUtxos(utxoAddr);
104+
let coreUtxos = await getUtxos(body);
105+
let utxos = await getOptimalUtxos(coreUtxos, amount + feePreEstimate);
104106
let balance = getBalance(utxos);
105107

106108
if (!utxos.length) {
@@ -145,16 +147,89 @@ Dash.create = function ({
145147
return tx;
146148
};
147149

150+
/**
151+
* @typedef {Object} CorePayment
152+
* @property {(String|import('@dashevo/dashcore-lib').Address)} address
153+
* @property {Number} satoshis
154+
*/
155+
156+
/**
157+
* Send with change back
158+
* @param {String} privKey
159+
* @param {Array<CorePayment>} payments
160+
* @param {(String|import('@dashevo/dashcore-lib').Address)} [changeAddr]
161+
*/
162+
dashApi.createPayments = async function (privKey, payments, changeAddr) {
163+
let pk = new Dashcore.PrivateKey(privKey);
164+
let utxoAddr = pk.toPublicKey().toAddress().toString();
165+
if (!changeAddr) {
166+
changeAddr = utxoAddr;
167+
}
168+
169+
// TODO make more accurate?
170+
let amount = payments.reduce(function (total, pay) {
171+
return pay.satoshis;
172+
}, 0);
173+
let body = await insightApi.getUtxos(utxoAddr);
174+
let coreUtxos = await getUtxos(body);
175+
let feePreEstimate = 150 * (payments.length + coreUtxos.length);
176+
let utxos = await getOptimalUtxos(coreUtxos, amount + feePreEstimate);
177+
let balance = getBalance(utxos);
178+
179+
if (!utxos.length) {
180+
throw new Error(`not enough funds available in utxos for ${utxoAddr}`);
181+
}
182+
183+
// (estimate) don't send dust back as change
184+
if (balance - amount <= DUST + FEE) {
185+
amount = balance;
186+
}
187+
188+
console.log("DEBUG");
189+
console.log(payments, changeAddr);
190+
191+
//@ts-ignore - no input required, actually
192+
let tmpTx = new Transaction()
193+
//@ts-ignore - allows single value or array
194+
.from(utxos);
195+
// TODO update jsdoc for dashcore
196+
tmpTx.to(payments, 0);
197+
//@ts-ignore - the JSDoc is wrong in dashcore-lib/lib/transaction/transaction.js
198+
tmpTx.change(changeAddr);
199+
tmpTx.sign(pk);
200+
201+
// TODO getsmartfeeestimate??
202+
// fee = 1duff/byte (2 chars hex is 1 byte)
203+
// +10 to be safe (the tmpTx may be a few bytes off - probably only 4 -
204+
// due to how small numbers are encoded)
205+
let fee = 10 + tmpTx.toString().length / 2;
206+
207+
// (adjusted) don't send dust back as change
208+
if (balance + -amount + -fee <= DUST) {
209+
amount = balance - fee;
210+
}
211+
212+
//@ts-ignore - no input required, actually
213+
let tx = new Transaction()
214+
//@ts-ignore - allows single value or array
215+
.from(utxos);
216+
tx.to(payments, 0);
217+
tx.fee(fee);
218+
//@ts-ignore - see above
219+
tx.change(changeAddr);
220+
tx.sign(pk);
221+
222+
return tx;
223+
};
224+
148225
// TODO make more optimal
149226
/**
150-
* @param {String} utxoAddr
227+
* @param {Array<CoreUtxo>} utxos
151228
* @param {Number} fullAmount - including fee estimate
152229
*/
153-
async function getOptimalUtxos(utxoAddr, fullAmount) {
230+
async function getOptimalUtxos(utxos, fullAmount) {
154231
// get smallest coin larger than transaction
155232
// if that would create dust, donate it as tx fee
156-
let body = await insightApi.getUtxos(utxoAddr);
157-
let utxos = await getUtxos(body);
158233
let balance = getBalance(utxos);
159234

160235
if (balance < fullAmount) {
@@ -244,3 +319,10 @@ Dash.create = function ({
244319

245320
return dashApi;
246321
};
322+
323+
/**
324+
* @param {String} dash - ex: 0.00000000
325+
*/
326+
Dash.toDuff = function (dash) {
327+
return Math.round(parseFloat(dash) * DUFFS);
328+
};

0 commit comments

Comments
 (0)