3 let request = require("./request.js");
5 let CrowdNode = module.exports;
7 const DUFFS = 100000000;
9 let Dash = require("./dash.js");
10 let Dashcore = require("@dashevo/dashcore-lib");
11 let Insight = require("./insight.js");
12 let Ws = require("./ws.js");
14 CrowdNode._insightBaseUrl = "";
15 // TODO don't require these shims
16 CrowdNode._insightApi = Insight.create({ baseUrl: "" });
17 CrowdNode._dashApi = Dash.create({ insightApi: CrowdNode._insightApi });
20 baseUrl: "https://app.crowdnode.io",
25 baseUrl: "https://test.crowdnode.io",
29 CrowdNode._baseUrl = CrowdNode.main.baseUrl;
31 CrowdNode.offset = 20000;
32 CrowdNode.duffs = 100000000;
33 CrowdNode.depositMinimum = 10000;
34 CrowdNode.stakeMinimum = toDuff(0.5);
37 * @type {Record<String, Number>}
39 CrowdNode.requests = {
43 toggleInstantPayout: 4096,
49 * @type {Record<Number, String>}
51 CrowdNode._responses = {
52 2: "PleaseAcceptTerms",
53 4: "WelcomeToCrowdNodeBlockChainAPI",
55 16: "WithdrawalQueued",
56 32: "WithdrawalFailed", // Most likely too small amount requested for withdrawal.
57 64: "AutoWithdrawalEnabled",
58 128: "AutoWithdrawalDisabled",
61 * @type {Record<String, Number>}
63 CrowdNode.responses = {
65 WelcomeToCrowdNodeBlockChainAPI: 4,
69 AutoWithdrawalEnabled: 64,
70 AutoWithdrawalDisabled: 128,
74 * @param {Object} opts
75 * @param {String} opts.baseUrl
76 * @param {String} opts.insightBaseUrl
78 CrowdNode.init = async function ({ baseUrl, insightBaseUrl }) {
80 // See https://github.com/dashhive/crowdnode.js/issues/3
82 CrowdNode._baseUrl = baseUrl;
84 //hotwallet in Mainnet is XjbaGWaGnvEtuQAUoBgDxJWe8ZNv45upG2
85 CrowdNode.main.hotwallet = await request({
86 // TODO https://app.crowdnode.io/odata/apifundings/HotWallet
87 url: "https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide",
88 }).then(createAddrParser("hotwallet in Main"));
90 //hotwallet in Test is yMY5bqWcknGy5xYBHSsh2xvHZiJsRucjuy
91 CrowdNode.test.hotwallet = await request({
92 // TODO https://test.crowdnode.io/odata/apifundings/HotWallet
93 url: "https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide",
94 }).then(createAddrParser("hotwallet in Test"));
96 CrowdNode._insightBaseUrl = insightBaseUrl;
97 CrowdNode._insightApi = Insight.create({
98 baseUrl: insightBaseUrl,
100 CrowdNode._dashApi = Dash.create({ insightApi: CrowdNode._insightApi });
104 * @param {String} signupAddr
105 * @param {String} hotwallet
107 CrowdNode.status = async function (signupAddr, hotwallet) {
109 let data = await CrowdNode._insightApi.getTxs(signupAddr, maxPages);
116 data.txs.forEach(function (tx) {
117 // all inputs (utxos) must come from hotwallet
118 let fromHotwallet = tx.vin.every(function (vin) {
119 return vin.addr === hotwallet;
121 if (!fromHotwallet) {
125 // must have one output matching the "welcome" value to the signupAddr
126 tx.vout.forEach(function (vout) {
127 if (vout.scriptPubKey.addresses[0] !== signupAddr) {
130 let amount = Math.round(parseFloat(vout.value) * DUFFS);
131 let msg = amount - CrowdNode.offset;
133 if (CrowdNode.responses.DepositReceived === msg) {
134 status.deposit = tx.time;
135 status.signup = status.signup || 1;
136 status.accept = status.accept || 1;
140 if (CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI === msg) {
141 status.signup = status.signup || 1;
142 status.accept = tx.time || 1;
146 if (CrowdNode.responses.PleaseAcceptTerms === msg) {
147 status.signup = tx.time;
153 if (!status.signup) {
160 * @param {String} wif
161 * @param {String} hotwallet
163 CrowdNode.signup = async function (wif, hotwallet) {
164 // Send Request Message
165 let pk = new Dashcore.PrivateKey(wif);
166 let msg = CrowdNode.offset + CrowdNode.requests.signupForApi;
167 let changeAddr = pk.toPublicKey().toAddress().toString();
168 let tx = await CrowdNode._dashApi.createPayment(
174 await CrowdNode._insightApi.instantSend(tx.serialize());
176 let reply = CrowdNode.offset + CrowdNode.responses.PleaseAcceptTerms;
177 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
181 * @param {String} wif
182 * @param {String} hotwallet
184 CrowdNode.accept = async function (wif, hotwallet) {
185 // Send Request Message
186 let pk = new Dashcore.PrivateKey(wif);
187 let msg = CrowdNode.offset + CrowdNode.requests.acceptTerms;
188 let changeAddr = pk.toPublicKey().toAddress().toString();
189 let tx = await CrowdNode._dashApi.createPayment(
195 await CrowdNode._insightApi.instantSend(tx.serialize());
198 CrowdNode.offset + CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI;
199 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
203 * @param {String} wif
204 * @param {String} hotwallet
205 * @param {Number} amount - Duffs (1/100000000 Dash)
207 CrowdNode.deposit = async function (wif, hotwallet, amount) {
208 // Send Request Message
209 let pk = new Dashcore.PrivateKey(wif);
210 let changeAddr = pk.toPublicKey().toAddress().toString();
212 // TODO reserve a balance
215 tx = await CrowdNode._dashApi.createPayment(
222 tx = await CrowdNode._dashApi.createBalanceTransfer(wif, hotwallet);
224 await CrowdNode._insightApi.instantSend(tx.serialize());
226 let reply = CrowdNode.offset + CrowdNode.responses.DepositReceived;
227 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
231 * @param {String} wif
232 * @param {String} hotwallet
233 * @param {Number} permil - 1/1000 (1/10 of a percent) 500 permille = 50.0 percent
235 CrowdNode.withdrawal = async function (wif, hotwallet, permil) {
236 let valid = permil > 0 && permil <= 1000;
237 valid = valid && Math.round(permil) === permil;
239 throw new Error(`'permil' must be between 1 and 1000, not '${permil}'`);
242 // Send Request Message
243 let pk = new Dashcore.PrivateKey(wif);
244 let msg = CrowdNode.offset + permil;
245 let changeAddr = pk.toPublicKey().toAddress().toString();
246 let tx = await CrowdNode._dashApi.createPayment(
252 await CrowdNode._insightApi.instantSend(tx.serialize());
254 // Listen for Response
263 return await Ws.listen(CrowdNode._insightBaseUrl, findResponse);
266 * @param {String} evname
267 * @param {InsightSocketEventData} data
269 function findResponse(evname, data) {
270 if (!["tx", "txlock"].includes(evname)) {
274 let now = Date.now();
276 // don't wait longer than 3s for a txlock
277 if (now - mempoolTx.at > 3000) {
283 // TODO should fetch tx and match hotwallet as vin
284 data.vout.some(function (vout) {
285 return Object.keys(vout).some(function (addr) {
286 if (addr !== changeAddr) {
290 let duffs = vout[addr];
291 let msg = duffs - CrowdNode.offset;
292 let api = CrowdNode._responses[msg];
294 // the withdrawal often happens before the queued message
295 console.warn(` => received '${duffs}' (${evname})`);
308 if ("txlock" !== evname) {
309 // wait up to 3s for a txlock
325 // See ./bin/crowdnode-list-apis.sh
329 * @param {String} baseUrl
330 * @param {String} pub
332 CrowdNode.http.FundsOpen = async function (pub) {
333 return `Open <${CrowdNode._baseUrl}/FundsOpen/${pub}> in your browser.`;
337 * @param {String} baseUrl
338 * @param {String} pub
340 CrowdNode.http.VotingOpen = async function (pub) {
341 return `Open <${CrowdNode._baseUrl}/VotingOpen/${pub}> in your browser.`;
345 * @param {String} baseUrl
346 * @param {String} pub
348 CrowdNode.http.GetFunds = createApi(
349 `/odata/apifundings/GetFunds(address='{1}')`,
353 * @param {String} baseUrl
354 * @param {String} pub
355 * @param {String} secondsSinceEpoch
357 CrowdNode.http.GetFundsFrom = createApi(
358 `/odata/apifundings/GetFundsFrom(address='{1}',fromUnixTime={2})`,
362 * @param {String} baseUrl
363 * @param {String} pub
364 * @returns {CrowdNodeBalance}
366 CrowdNode.http.GetBalance = createApi(
367 `/odata/apifundings/GetBalance(address='{1}')`,
371 * @param {String} baseUrl
372 * @param {String} pub
374 CrowdNode.http.GetMessages = createApi(
375 `/odata/apimessages/GetMessages(address='{1}')`,
379 * @param {String} baseUrl
380 * @param {String} pub
382 CrowdNode.http.IsAddressInUse = createApi(
383 `/odata/apiaddresses/IsAddressInUse(address='{1}')`,
387 * Set Email Address: messagetype=1
388 * @param {String} baseUrl
389 * @param {String} pub - pay to pubkey base58check address
390 * @param {String} email
391 * @param {String} signature
393 CrowdNode.http.SetEmail = createApi(
394 `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=1)`,
398 * Vote on Governance Objects: messagetype=2
399 * @param {String} baseUrl
400 * @param {String} pub - pay to pubkey base58check address
401 * @param {String} gobject-hash
402 * @param {String} choice - Yes|No|Abstain|Delegate|DoNothing
403 * @param {String} signature
405 CrowdNode.http.Vote = createApi(
406 `/odata/apimessages/SendMessage(address='{1}',message='{2},{3}',signature={4}',messagetype=2)`,
410 * Set Referral: messagetype=3
411 * @param {String} baseUrl
412 * @param {String} pub - pay to pubkey base58check address
413 * @param {String} referralId
414 * @param {String} signature
416 CrowdNode.http.SetReferral = createApi(
417 `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=3)`,
421 * @param {String} tmplUrl
423 function createApi(tmplUrl) {
425 * @param {Array<String>} arguments - typically just 'pub', unless SendMessage
427 return async function () {
428 /** @type Array<String> */
429 //@ts-ignore - arguments
430 let args = [].slice.call(arguments, 0);
433 let url = `${CrowdNode._baseUrl}${tmplUrl}`;
434 args.forEach(function (arg, i) {
436 url = url.replace(new RegExp(`\\{${n}\\}`, "g"), arg);
439 let resp = await request({
440 // TODO https://app.crowdnode.io/odata/apifundings/HotWallet
447 `http error: ${resp.statusCode} ${resp.body.message}`,
450 err.response = resp.toJSON();
459 * @param {String} prefix
461 function createAddrParser(prefix) {
463 * @param {import('http').IncomingMessage} resp
465 return function (resp) {
467 let html = resp.body;
468 return parseAddr(prefix, html);
473 * @param {String} prefix
474 * @param {String} html
476 function parseAddr(prefix, html) {
477 // TODO escape prefix
478 // TODO restrict to true base58 (not base62)
479 let addrRe = new RegExp(prefix + "[^X]+\\b([Xy][a-z0-9]{33})\\b", "i");
481 let m = html.match(addrRe);
483 throw new Error("could not find hotwallet address");
486 let hotwallet = m[1];
490 if (require.main === module) {
491 (async function main() {
493 await CrowdNode.init({
495 baseUrl: CrowdNode.main.baseUrl,
496 insightBaseUrl: "https://insight.dash.org",
498 console.info(CrowdNode);
499 })().catch(function (err) {
504 function toDuff(dash) {
505 return Math.round(parseFloat(dash) * DUFFS);