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.insightBaseUrl
76 CrowdNode.init = async function ({ baseUrl, insightBaseUrl }) {
78 // See https://github.com/dashhive/crowdnode.js/issues/3
80 CrowdNode._baseUrl = baseUrl;
82 //hotwallet in Mainnet is XjbaGWaGnvEtuQAUoBgDxJWe8ZNv45upG2
83 CrowdNode.main.hotwallet = await request({
84 // TODO https://app.crowdnode.io/odata/apifundings/HotWallet
85 url: "https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide",
86 }).then(createAddrParser("hotwallet in Main"));
88 //hotwallet in Test is yMY5bqWcknGy5xYBHSsh2xvHZiJsRucjuy
89 CrowdNode.test.hotwallet = await request({
90 // TODO https://test.crowdnode.io/odata/apifundings/HotWallet
91 url: "https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide",
92 }).then(createAddrParser("hotwallet in Test"));
94 CrowdNode._insightBaseUrl = insightBaseUrl;
95 CrowdNode._insightApi = Insight.create({
96 baseUrl: insightBaseUrl,
98 CrowdNode._dashApi = Dash.create({ insightApi: CrowdNode._insightApi });
102 * @param {String} signupAddr
103 * @param {String} hotwallet
105 CrowdNode.status = async function (signupAddr, hotwallet) {
107 let data = await CrowdNode._insightApi.getTxs(signupAddr, maxPages);
114 data.txs.forEach(function (tx) {
115 // all inputs (utxos) must come from hotwallet
116 let fromHotwallet = tx.vin.every(function (vin) {
117 return vin.addr === hotwallet;
119 if (!fromHotwallet) {
123 // must have one output matching the "welcome" value to the signupAddr
124 tx.vout.forEach(function (vout) {
125 if (vout.scriptPubKey.addresses[0] !== signupAddr) {
128 let amount = Math.round(parseFloat(vout.value) * DUFFS);
129 let msg = amount - CrowdNode.offset;
131 if (CrowdNode.responses.DepositReceived === msg) {
132 status.deposit = tx.time;
133 status.signup = status.signup || 1;
134 status.accept = status.accept || 1;
138 if (CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI === msg) {
139 status.signup = status.signup || 1;
140 status.accept = tx.time || 1;
144 if (CrowdNode.responses.PleaseAcceptTerms === msg) {
145 status.signup = tx.time;
151 if (!status.signup) {
158 * @param {String} wif
159 * @param {String} hotwallet
161 CrowdNode.signup = async function (wif, hotwallet) {
162 // Send Request Message
163 let pk = new Dashcore.PrivateKey(wif);
164 let msg = CrowdNode.offset + CrowdNode.requests.signupForApi;
165 let changeAddr = pk.toPublicKey().toAddress().toString();
166 let tx = await CrowdNode._dashApi.createPayment(
172 await CrowdNode._insightApi.instantSend(tx.serialize());
174 let reply = CrowdNode.offset + CrowdNode.responses.PleaseAcceptTerms;
175 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
179 * @param {String} wif
180 * @param {String} hotwallet
182 CrowdNode.accept = async function (wif, hotwallet) {
183 // Send Request Message
184 let pk = new Dashcore.PrivateKey(wif);
185 let msg = CrowdNode.offset + CrowdNode.requests.acceptTerms;
186 let changeAddr = pk.toPublicKey().toAddress().toString();
187 let tx = await CrowdNode._dashApi.createPayment(
193 await CrowdNode._insightApi.instantSend(tx.serialize());
196 CrowdNode.offset + CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI;
197 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
201 * @param {String} wif
202 * @param {String} hotwallet
203 * @param {Number} amount
205 CrowdNode.deposit = async function (wif, hotwallet, amount) {
206 // Send Request Message
207 let pk = new Dashcore.PrivateKey(wif);
208 let changeAddr = pk.toPublicKey().toAddress().toString();
210 // TODO reserve a balance
213 tx = await CrowdNode._dashApi.createPayment(
220 tx = await CrowdNode._dashApi.createBalanceTransfer(wif, hotwallet);
222 await CrowdNode._insightApi.instantSend(tx.serialize());
224 let reply = CrowdNode.offset + CrowdNode.responses.DepositReceived;
225 return await Ws.waitForVout(CrowdNode._insightBaseUrl, changeAddr, reply);
229 * @param {String} wif
230 * @param {String} hotwallet
231 * @param {Number} permil - 1/1000 (percent, but per thousand)
233 CrowdNode.withdrawal = async function (wif, hotwallet, permil) {
234 let valid = permil > 0 && permil <= 1000;
235 valid = valid && Math.round(permil) === permil;
237 throw new Error(`'permil' must be between 1 and 1000, not '${permil}'`);
240 // Send Request Message
241 let pk = new Dashcore.PrivateKey(wif);
242 let msg = CrowdNode.offset + permil;
243 let changeAddr = pk.toPublicKey().toAddress().toString();
244 let tx = await CrowdNode._dashApi.createPayment(
250 await CrowdNode._insightApi.instantSend(tx.serialize());
252 // Listen for Response
261 return await Ws.listen(CrowdNode._insightBaseUrl, findResponse);
264 * @param {String} evname
265 * @param {InsightSocketEventData} data
267 function findResponse(evname, data) {
268 if (!["tx", "txlock"].includes(evname)) {
272 let now = Date.now();
274 // don't wait longer than 3s for a txlock
275 if (now - mempoolTx.at > 3000) {
281 // TODO should fetch tx and match hotwallet as vin
282 data.vout.some(function (vout) {
283 return Object.keys(vout).some(function (addr) {
284 if (addr !== changeAddr) {
288 let duffs = vout[addr];
289 let msg = duffs - CrowdNode.offset;
290 let api = CrowdNode._responses[msg];
292 // the withdrawal often happens before the queued message
293 console.warn(` => received '${duffs}' (${evname})`);
306 if ("txlock" !== evname) {
307 // wait up to 3s for a txlock
323 // See ./bin/crowdnode-list-apis.sh
327 * @param {String} baseUrl
328 * @param {String} pub
330 CrowdNode.http.FundsOpen = async function (pub) {
331 return `Open <${CrowdNode._baseUrl}/FundsOpen/${pub}> in your browser.`;
335 * @param {String} baseUrl
336 * @param {String} pub
338 CrowdNode.http.VotingOpen = async function (pub) {
339 return `Open <${CrowdNode._baseUrl}/VotingOpen/${pub}> in your browser.`;
343 * @param {String} baseUrl
344 * @param {String} pub
346 CrowdNode.http.GetFunds = createApi(
347 `/odata/apifundings/GetFunds(address='{1}')`,
351 * @param {String} baseUrl
352 * @param {String} pub
353 * @param {String} secondsSinceEpoch
355 CrowdNode.http.GetFundsFrom = createApi(
356 `/odata/apifundings/GetFundsFrom(address='{1}',fromUnixTime={2})`,
360 * @param {String} baseUrl
361 * @param {String} pub
362 * @returns {CrowdNodeBalance}
364 CrowdNode.http.GetBalance = createApi(
365 `/odata/apifundings/GetBalance(address='{1}')`,
369 * @param {String} baseUrl
370 * @param {String} pub
372 CrowdNode.http.GetMessages = createApi(
373 `/odata/apimessages/GetMessages(address='{1}')`,
377 * @param {String} baseUrl
378 * @param {String} pub
380 CrowdNode.http.IsAddressInUse = createApi(
381 `/odata/apiaddresses/IsAddressInUse(address='{1}')`,
385 * Set Email Address: messagetype=1
386 * @param {String} baseUrl
387 * @param {String} pub - pay to pubkey base58check address
388 * @param {String} email
389 * @param {String} signature
391 CrowdNode.http.SetEmail = createApi(
392 `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=1)`,
396 * Vote on Governance Objects: messagetype=2
397 * @param {String} baseUrl
398 * @param {String} pub - pay to pubkey base58check address
399 * @param {String} gobject-hash
400 * @param {String} choice - Yes|No|Abstain|Delegate|DoNothing
401 * @param {String} signature
403 CrowdNode.http.Vote = createApi(
404 `/odata/apimessages/SendMessage(address='{1}',message='{2},{3}',signature={4}',messagetype=2)`,
408 * Set Referral: messagetype=3
409 * @param {String} baseUrl
410 * @param {String} pub - pay to pubkey base58check address
411 * @param {String} referralId
412 * @param {String} signature
414 CrowdNode.http.SetReferral = createApi(
415 `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=3)`,
419 * @param {String} tmplUrl
421 function createApi(tmplUrl) {
423 * @param {Array<String>} arguments - typically just 'pub', unless SendMessage
425 return async function () {
426 /** @type Array<String> */
427 //@ts-ignore - arguments
428 let args = [].slice.call(arguments, 1);
431 let url = `${CrowdNode._baseUrl}${tmplUrl}`;
432 args.forEach(function (arg, i) {
434 url = url.replace(new RegExp(`\\{${n}\\}`, "g"), arg);
437 let resp = await request({
438 // TODO https://app.crowdnode.io/odata/apifundings/HotWallet
445 `http error: ${resp.statusCode} ${resp.body.message}`,
448 err.response = resp.toJSON();
457 * @param {String} prefix
459 function createAddrParser(prefix) {
461 * @param {import('http').IncomingMessage} resp
463 return function (resp) {
465 let html = resp.body;
466 return parseAddr(prefix, html);
471 * @param {String} prefix
472 * @param {String} html
474 function parseAddr(prefix, html) {
475 // TODO escape prefix
476 // TODO restrict to true base58 (not base62)
477 let addrRe = new RegExp(prefix + "[^X]+\\b([Xy][a-z0-9]{33})\\b", "i");
479 let m = html.match(addrRe);
481 throw new Error("could not find hotwallet address");
484 let hotwallet = m[1];
488 if (require.main === module) {
489 (async function main() {
491 await CrowdNode.init();
492 console.info(CrowdNode);
493 })().catch(function (err) {