feat: a pretty decent, tested, working SDK + CLI
[crowdnode.js/.git] / lib / dash.js
1 "use strict";
2
3 let Dash = module.exports;
4
5 const DUFFS = 100000000;
6 const DUST = 10000;
7 const FEE = 1000;
8
9 let Dashcore = require("@dashevo/dashcore-lib");
10 let Transaction = Dashcore.Transaction;
11
12 Dash.create = function ({
13   //@ts-ignore TODO
14   insightApi,
15 }) {
16   let dashApi = {};
17
18   /**
19    * Instant Balance is accurate with Instant Send
20    * @param {String} address
21    * @returns {Promise<InstantBalance>}
22    */
23   dashApi.getInstantBalance = async function (address) {
24     let body = await insightApi.getUtxos(address);
25     let utxos = await getUtxos(body);
26     let balance = utxos.reduce(function (total, utxo) {
27       return total + utxo.satoshis;
28     }, 0);
29     // because 0.1 + 0.2 = 0.30000000000000004,
30     // but we would only want 0.30000000
31     let floatBalance = parseFloat((balance / DUFFS).toFixed(8));
32
33     return {
34       addrStr: address,
35       balance: floatBalance,
36       balanceSat: balance,
37       _utxoCount: utxos.length,
38       _utxoAmounts: utxos.map(function (utxo) {
39         return utxo.satoshis;
40       }),
41     };
42   };
43
44   /**
45    * Full Send!
46    * @param {String} privKey
47    * @param {String} pub
48    */
49   dashApi.createBalanceTransfer = async function (privKey, pub) {
50     let pk = new Dashcore.PrivateKey(privKey);
51     let changeAddr = pk.toPublicKey().toAddress().toString();
52
53     let body = await insightApi.getUtxos(changeAddr);
54     let utxos = await getUtxos(body);
55     let balance = utxos.reduce(function (total, utxo) {
56       return total + utxo.satoshis;
57     }, 0);
58
59     //@ts-ignore - no input required, actually
60     let tmpTx = new Transaction()
61       //@ts-ignore - allows single value or array
62       .from(utxos);
63     tmpTx.to(pub, balance - 1000);
64     tmpTx.sign(pk);
65
66     // TODO getsmartfeeestimate??
67     // fee = 1duff/byte (2 chars hex is 1 byte)
68     //       +10 to be safe (the tmpTx may be a few bytes off)
69     let fee = 10 + tmpTx.toString().length / 2;
70
71     //@ts-ignore - no input required, actually
72     let tx = new Transaction()
73       //@ts-ignore - allows single value or array
74       .from(utxos);
75     tx.to(pub, balance - fee);
76     tx.fee(fee);
77     tx.sign(pk);
78
79     return tx;
80   };
81
82   /**
83    * Send with change back
84    * @param {String} privKey
85    * @param {(String|import('@dashevo/dashcore-lib').Address)} payAddr
86    * @param {Number} amount
87    * @param {(String|import('@dashevo/dashcore-lib').Address)} [changeAddr]
88    */
89   dashApi.createPayment = async function (
90     privKey,
91     payAddr,
92     amount,
93     changeAddr,
94   ) {
95     let pk = new Dashcore.PrivateKey(privKey);
96     let utxoAddr = pk.toPublicKey().toAddress().toString();
97     if (!changeAddr) {
98       changeAddr = utxoAddr;
99     }
100
101     // TODO make more accurate?
102     let feePreEstimate = 1000;
103     let utxos = await getOptimalUtxos(utxoAddr, amount + feePreEstimate);
104     let balance = getBalance(utxos);
105
106     if (!utxos.length) {
107       throw new Error(`not enough funds available in utxos for ${utxoAddr}`);
108     }
109
110     // (estimate) don't send dust back as change
111     if (balance - amount <= DUST + FEE) {
112       amount = balance;
113     }
114
115     //@ts-ignore - no input required, actually
116     let tmpTx = new Transaction()
117       //@ts-ignore - allows single value or array
118       .from(utxos);
119     tmpTx.to(payAddr, amount);
120     //@ts-ignore - the JSDoc is wrong in dashcore-lib/lib/transaction/transaction.js
121     tmpTx.change(changeAddr);
122     tmpTx.sign(pk);
123
124     // TODO getsmartfeeestimate??
125     // fee = 1duff/byte (2 chars hex is 1 byte)
126     //       +10 to be safe (the tmpTx may be a few bytes off - probably only 4 -
127     //       due to how small numbers are encoded)
128     let fee = 10 + tmpTx.toString().length / 2;
129
130     // (adjusted) don't send dust back as change
131     if (balance + -amount + -fee <= DUST) {
132       amount = balance - fee;
133     }
134
135     //@ts-ignore - no input required, actually
136     let tx = new Transaction()
137       //@ts-ignore - allows single value or array
138       .from(utxos);
139     tx.to(payAddr, amount);
140     tx.fee(fee);
141     //@ts-ignore - see above
142     tx.change(changeAddr);
143     tx.sign(pk);
144
145     return tx;
146   };
147
148   // TODO make more optimal
149   /**
150    * @param {String} utxoAddr
151    * @param {Number} fullAmount - including fee estimate
152    */
153   async function getOptimalUtxos(utxoAddr, fullAmount) {
154     // get smallest coin larger than transaction
155     // if that would create dust, donate it as tx fee
156     let body = await insightApi.getUtxos(utxoAddr);
157     let utxos = await getUtxos(body);
158     let balance = getBalance(utxos);
159
160     if (balance < fullAmount) {
161       return [];
162     }
163
164     // from largest to smallest
165     utxos.sort(function (a, b) {
166       return b.satoshis - a.satoshis;
167     });
168
169     /** @type Array<CoreUtxo> */
170     let included = [];
171     let total = 0;
172
173     // try to get just one
174     utxos.every(function (utxo) {
175       if (utxo.satoshis > fullAmount) {
176         included[0] = utxo;
177         total = utxo.satoshis;
178         return true;
179       }
180       return false;
181     });
182     if (total) {
183       return included;
184     }
185
186     // try to use as few coins as possible
187     utxos.some(function (utxo) {
188       included.push(utxo);
189       total += utxo.satoshis;
190       return total >= fullAmount;
191     });
192     return included;
193   }
194
195   /**
196    * @param {Array<CoreUtxo>} utxos
197    */
198   function getBalance(utxos) {
199     return utxos.reduce(function (total, utxo) {
200       return total + utxo.satoshis;
201     }, 0);
202   }
203
204   /**
205    * @param {Array<InsightUtxo>} body
206    */
207   async function getUtxos(body) {
208     /** @type Array<CoreUtxo> */
209     let utxos = [];
210
211     await body.reduce(async function (promise, utxo) {
212       await promise;
213
214       let data = await insightApi.getTx(utxo.txid);
215
216       // TODO the ideal would be the smallest amount that is greater than the required amount
217
218       let utxoIndex = -1;
219       data.vout.some(function (vout, index) {
220         if (!vout.scriptPubKey?.addresses?.includes(utxo.address)) {
221           return false;
222         }
223
224         let satoshis = Math.round(parseFloat(vout.value) * DUFFS);
225         if (utxo.satoshis !== satoshis) {
226           return false;
227         }
228
229         utxoIndex = index;
230         return true;
231       });
232
233       utxos.push({
234         txId: utxo.txid,
235         outputIndex: utxoIndex,
236         address: utxo.address,
237         script: utxo.scriptPubKey,
238         satoshis: utxo.satoshis,
239       });
240     }, Promise.resolve());
241
242     return utxos;
243   }
244
245   return dashApi;
246 };