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;
36 * @type {Record<String, Number>}
38 CrowdNode.requests = {
42 toggleInstantPayout: 4096,
48 * @type {Record<Number, String>}
50 CrowdNode._responses = {
51 2: "PleaseAcceptTerms",
52 4: "WelcomeToCrowdNodeBlockChainAPI",
54 16: "WithdrawalQueued",
55 32: "WithdrawalFailed", // Most likely too small amount requested for withdrawal.
56 64: "AutoWithdrawalEnabled",
57 128: "AutoWithdrawalDisabled",
60 * @type {Record<String, Number>}
62 CrowdNode.responses = {
64 WelcomeToCrowdNodeBlockChainAPI: 4,
68 AutoWithdrawalEnabled: 64,
69 AutoWithdrawalDisabled: 128,
73 * @param {Object} opts
74 * @param {String} opts.baseUrl
75 * @param {String} opts.insightBaseUrl
77 CrowdNode.init = async function ({ baseUrl, insightBaseUrl }) {
79 // See https://github.com/dashhive/crowdnode.js/issues/3
81 CrowdNode._baseUrl = baseUrl;
83 //hotwallet in Mainnet is XjbaGWaGnvEtuQAUoBgDxJWe8ZNv45upG2
84 CrowdNode.main.hotwallet = await request({
85 // TODO https://app.crowdnode.io/odata/apifundings/HotWallet
86 url: "https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide",
87 }).then(createAddrParser("hotwallet in Main"));
89 //hotwallet in Test is yMY5bqWcknGy5xYBHSsh2xvHZiJsRucjuy
90 CrowdNode.test.hotwallet = await request({
91 // TODO https://test.crowdnode.io/odata/apifundings/HotWallet
92 url: "https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide",
93 }).then(createAddrParser("hotwallet in Test"));
95 CrowdNode._insightBaseUrl = insightBaseUrl;
96 CrowdNode._insightApi = Insight.create({
97 baseUrl: insightBaseUrl,
99 CrowdNode._dashApi = Dash.create({ insightApi: CrowdNode._insightApi });
103 * @param {String} signupAddr
104 * @param {String} hotwallet
106 CrowdNode.status = async function (signupAddr, hotwallet) {
108 let data = await CrowdNode._insightApi.getTxs(signupAddr, maxPages);
115 data.txs.forEach(function (tx) {
116 // all inputs (utxos) must come from hotwallet
117 let fromHotwallet = tx.vin.every(function (vin) {
118 return vin.addr === hotwallet;
120 if (!fromHotwallet) {
124 // must have one output matching the "welcome" value to the signupAddr
125 tx.vout.forEach(function (vout) {
126 if (vout.scriptPubKey.addresses[0] !== signupAddr) {
129 let amount = Math.round(parseFloat(vout.value) * DUFFS);
130 let msg = amount - CrowdNode.offset;
132 if (CrowdNode.responses.DepositReceived === msg) {
133 status.deposit = tx.time;
134 status.signup = status.signup || 1;
135 status.accept = status.accept || 1;
139 if (CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI === msg) {
140 status.signup = status.signup || 1;
141 status.accept = tx.time || 1;
145 if (CrowdNode.responses.PleaseAcceptTerms === msg) {
146 status.signup = tx.time;
152 if (!status.signup) {
159 * @param {String} wif
160 * @param {String} hotwallet
162 CrowdNode.signup = async function (wif, hotwallet) {
163 // Send Request Message
164 let pk = new Dashcore.PrivateKey(wif);
165 let msg = CrowdNode.offset + CrowdNode.requests.signupForApi;
166 let changeAddr = pk.toPublicKey().toAddress().toString();
167 let tx = await CrowdNode._dashApi.createPayment(
173 await CrowdNode._insightApi.instantSend(tx.serialize());
175 let reply = CrowdNode.offset + CrowdNode.responses.PleaseAcceptTerms;
176 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
180 * @param {String} wif
181 * @param {String} hotwallet
183 CrowdNode.accept = async function (wif, hotwallet) {
184 // Send Request Message
185 let pk = new Dashcore.PrivateKey(wif);
186 let msg = CrowdNode.offset + CrowdNode.requests.acceptTerms;
187 let changeAddr = pk.toPublicKey().toAddress().toString();
188 let tx = await CrowdNode._dashApi.createPayment(
194 await CrowdNode._insightApi.instantSend(tx.serialize());
197 CrowdNode.offset + CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI;
198 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
202 * @param {String} wif
203 * @param {String} hotwallet
204 * @param {Number} amount - Duffs (1/100000000 Dash)
206 CrowdNode.deposit = async function (wif, hotwallet, amount) {
207 // Send Request Message
208 let pk = new Dashcore.PrivateKey(wif);
209 let changeAddr = pk.toPublicKey().toAddress().toString();
211 // TODO reserve a balance
214 tx = await CrowdNode._dashApi.createPayment(
221 tx = await CrowdNode._dashApi.createBalanceTransfer(wif, hotwallet);
223 await CrowdNode._insightApi.instantSend(tx.serialize());
225 let reply = CrowdNode.offset + CrowdNode.responses.DepositReceived;
226 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
230 * @param {String} wif
231 * @param {String} hotwallet
232 * @param {Number} permil - 1/1000 (1/10 of a percent) 500 permille = 50.0 percent
234 CrowdNode.withdrawal = async function (wif, hotwallet, permil) {
235 let valid = permil > 0 && permil <= 1000;
236 valid = valid && Math.round(permil) === permil;
238 throw new Error(`'permil' must be between 1 and 1000, not '${permil}'`);
241 // Send Request Message
242 let pk = new Dashcore.PrivateKey(wif);
243 let msg = CrowdNode.offset + permil;
244 let changeAddr = pk.toPublicKey().toAddress().toString();
245 let tx = await CrowdNode._dashApi.createPayment(
251 await CrowdNode._insightApi.instantSend(tx.serialize());
253 // Listen for Response
262 return await Ws.listen(CrowdNode._insightBaseUrl, findResponse);
265 * @param {String} evname
266 * @param {InsightSocketEventData} data
268 function findResponse(evname, data) {
269 if (!["tx", "txlock"].includes(evname)) {
273 let now = Date.now();
275 // don't wait longer than 3s for a txlock
276 if (now - mempoolTx.at > 3000) {
282 // TODO should fetch tx and match hotwallet as vin
283 data.vout.some(function (vout) {
284 return Object.keys(vout).some(function (addr) {
285 if (addr !== changeAddr) {
289 let duffs = vout[addr];
290 let msg = duffs - CrowdNode.offset;
291 let api = CrowdNode._responses[msg];
293 // the withdrawal often happens before the queued message
294 console.warn(` => received '${duffs}' (${evname})`);
307 if ("txlock" !== evname) {
308 // wait up to 3s for a txlock
324 // See ./bin/crowdnode-list-apis.sh
328 * @param {String} baseUrl
329 * @param {String} pub
331 CrowdNode.http.FundsOpen = async function (pub) {
332 return `Open <${CrowdNode._baseUrl}/FundsOpen/${pub}> in your browser.`;
336 * @param {String} baseUrl
337 * @param {String} pub
339 CrowdNode.http.VotingOpen = async function (pub) {
340 return `Open <${CrowdNode._baseUrl}/VotingOpen/${pub}> in your browser.`;
344 * @param {String} baseUrl
345 * @param {String} pub
347 CrowdNode.http.GetFunds = createApi(
348 `/odata/apifundings/GetFunds(address='{1}')`,
352 * @param {String} baseUrl
353 * @param {String} pub
354 * @param {String} secondsSinceEpoch
356 CrowdNode.http.GetFundsFrom = createApi(
357 `/odata/apifundings/GetFundsFrom(address='{1}',fromUnixTime={2})`,
361 * @param {String} baseUrl
362 * @param {String} pub
363 * @returns {CrowdNodeBalance}
365 CrowdNode.http.GetBalance = createApi(
366 `/odata/apifundings/GetBalance(address='{1}')`,
370 * @param {String} baseUrl
371 * @param {String} pub
373 CrowdNode.http.GetMessages = createApi(
374 `/odata/apimessages/GetMessages(address='{1}')`,
378 * @param {String} baseUrl
379 * @param {String} pub
381 CrowdNode.http.IsAddressInUse = createApi(
382 `/odata/apiaddresses/IsAddressInUse(address='{1}')`,
386 * Set Email Address: messagetype=1
387 * @param {String} baseUrl
388 * @param {String} pub - pay to pubkey base58check address
389 * @param {String} email
390 * @param {String} signature
392 CrowdNode.http.SetEmail = createApi(
393 `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=1)`,
397 * Vote on Governance Objects: messagetype=2
398 * @param {String} baseUrl
399 * @param {String} pub - pay to pubkey base58check address
400 * @param {String} gobject-hash
401 * @param {String} choice - Yes|No|Abstain|Delegate|DoNothing
402 * @param {String} signature
404 CrowdNode.http.Vote = createApi(
405 `/odata/apimessages/SendMessage(address='{1}',message='{2},{3}',signature={4}',messagetype=2)`,
409 * Set Referral: messagetype=3
410 * @param {String} baseUrl
411 * @param {String} pub - pay to pubkey base58check address
412 * @param {String} referralId
413 * @param {String} signature
415 CrowdNode.http.SetReferral = createApi(
416 `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=3)`,
420 * @param {String} tmplUrl
422 function createApi(tmplUrl) {
424 * @param {Array<String>} arguments - typically just 'pub', unless SendMessage
426 return async function () {
427 /** @type Array<String> */
428 //@ts-ignore - arguments
429 let args = [].slice.call(arguments, 0);
432 let url = `${CrowdNode._baseUrl}${tmplUrl}`;
433 args.forEach(function (arg, i) {
435 url = url.replace(new RegExp(`\\{${n}\\}`, "g"), arg);
438 let resp = await request({
439 // TODO https://app.crowdnode.io/odata/apifundings/HotWallet
446 `http error: ${resp.statusCode} ${resp.body.message}`,
449 err.response = resp.toJSON();
458 * @param {String} prefix
460 function createAddrParser(prefix) {
462 * @param {import('http').IncomingMessage} resp
464 return function (resp) {
466 let html = resp.body;
467 return parseAddr(prefix, html);
472 * @param {String} prefix
473 * @param {String} html
475 function parseAddr(prefix, html) {
476 // TODO escape prefix
477 // TODO restrict to true base58 (not base62)
478 let addrRe = new RegExp(prefix + "[^X]+\\b([Xy][a-z0-9]{33})\\b", "i");
480 let m = html.match(addrRe);
482 throw new Error("could not find hotwallet address");
485 let hotwallet = m[1];
489 if (require.main === module) {
490 (async function main() {
492 await CrowdNode.init({
494 baseUrl: CrowdNode.main.baseUrl,
495 insightBaseUrl: "https://insight.dash.org",
497 console.info(CrowdNode);
498 })().catch(function (err) {