UNPKG

crowdnode

Version:

Manage your stake in Đash with the CrowdNode Blockchain API

541 lines (476 loc) 14.7 kB
(function (_exports) { "use strict"; /** @type {any} exports */ let exports = _exports; let request = exports.urequest || require("./lib/request.js"); let CrowdNode = {}; exports.CrowdNode = CrowdNode; const DUFFS = 100000000; let Dash = exports.DashApi || require("./dashapi.js"); let Dashcore = exports.dashcore || require("./lib/dashcore.js"); let DashSight = exports.DashSight || require("dashsight"); let Ws = exports.DashSocket || require("dashsight/ws"); CrowdNode._initialized = false; CrowdNode._dashsocketBaseUrl = ""; // TODO don't require these shims CrowdNode._insightApi = DashSight.create({ dashsightBaseUrl: "", dashsocketBaseUrl: "", insightBaseUrl: "", }); CrowdNode._dashApi = Dash.create({ insightApi: CrowdNode._insightApi }); CrowdNode.main = { baseUrl: "https://app.crowdnode.io", // See https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide // TODO https://app.crowdnode.io/odata/apifundings/HotWallet hotwallet: "XjbaGWaGnvEtuQAUoBgDxJWe8ZNv45upG2", }; CrowdNode.test = { baseUrl: "https://test.crowdnode.io", // See https://knowledge.crowdnode.io/en/articles/5963880-blockchain-api-guide // TODO https://test.crowdnode.io/odata/apifundings/HotWallet hotwallet: "yMY5bqWcknGy5xYBHSsh2xvHZiJsRucjuy", }; CrowdNode._baseUrl = CrowdNode.main.baseUrl; CrowdNode.offset = 20000; CrowdNode.duffs = 100000000; CrowdNode.depositMinimum = 10000; CrowdNode.stakeMinimum = toDuff(0.5); /** * @type {Record<String, Number>} */ CrowdNode.requests = { acceptTerms: 65536, offset: 20000, signupForApi: 131072, toggleInstantPayout: 4096, withdrawMin: 1, withdrawMax: 1000, }; /** * @type {Record<Number, String>} */ CrowdNode._responses = { 2: "PleaseAcceptTerms", 4: "WelcomeToCrowdNodeBlockChainAPI", 8: "DepositReceived", 16: "WithdrawalQueued", 32: "WithdrawalFailed", // Most likely too small amount requested for withdraw. 64: "AutoWithdrawalEnabled", 128: "AutoWithdrawalDisabled", }; /** * @type {Record<String, Number>} */ CrowdNode.responses = { PleaseAcceptTerms: 2, WelcomeToCrowdNodeBlockChainAPI: 4, DepositReceived: 8, WithdrawalQueued: 16, WithdrawalFailed: 32, AutoWithdrawalEnabled: 64, AutoWithdrawalDisabled: 128, }; /** * @param {Object} opts * @param {String} opts.baseUrl * @param {String} opts.dashsightBaseUrl - typically ends with /insight-api * @param {String} opts.dashsocketBaseUrl - typically ends with /socket.io * @param {String} opts.insightBaseUrl - typically ends with /insight-api */ CrowdNode.init = async function ({ baseUrl, dashsightBaseUrl, dashsocketBaseUrl, insightBaseUrl, }) { // TODO use API // See https://github.com/dashhive/crowdnode.js/issues/3 CrowdNode._baseUrl = baseUrl; if ("https://insight.dash.org" === insightBaseUrl) { insightBaseUrl = "https://insight.dash.org/insight-api"; if (!dashsocketBaseUrl) { dashsocketBaseUrl = "https://insight.dash.org/socket.io"; } if (!dashsightBaseUrl) { dashsightBaseUrl = "https://dashsight.dashincubator.dev/insight-api"; } } CrowdNode._dashsocketBaseUrl = dashsocketBaseUrl; CrowdNode._insightApi = DashSight.create({ dashsightBaseUrl: dashsightBaseUrl, dashsocketBaseUrl: dashsocketBaseUrl, insightBaseUrl: insightBaseUrl, }); CrowdNode._dashApi = Dash.create({ insightApi: CrowdNode._insightApi }); CrowdNode._initialized = true; }; /** * @param {String} signupAddr * @param {String} hotwallet */ CrowdNode.status = async function (signupAddr, hotwallet) { let maxPages = 10; let data = await CrowdNode._insightApi.getTxs(signupAddr, maxPages); let status = { signup: 0, accept: 0, deposit: 0, }; data.txs.forEach( /** @param {InsightTx} tx */ function (tx) { // all inputs (utxos) must come from hotwallet let fromHotwallet = tx.vin.every(function (vin) { return vin.addr === hotwallet; }); if (!fromHotwallet) { return; } // must have one output matching the "welcome" value to the signupAddr tx.vout.forEach(function (vout) { if (vout.scriptPubKey.addresses[0] !== signupAddr) { return; } let amount = Math.round(parseFloat(vout.value) * DUFFS); let msg = amount - CrowdNode.offset; if (CrowdNode.responses.DepositReceived === msg) { status.deposit = tx.time; status.signup = status.signup || 1; status.accept = status.accept || 1; return; } if (CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI === msg) { status.signup = status.signup || 1; status.accept = tx.time || 1; return; } if (CrowdNode.responses.PleaseAcceptTerms === msg) { status.signup = tx.time; return; } }); }, ); if (!status.signup) { return null; } return status; }; /** * @param {String} wif * @param {String} hotwallet */ CrowdNode.signup = async function (wif, hotwallet) { // Send Request Message let pk = new Dashcore.PrivateKey(wif); let msg = CrowdNode.offset + CrowdNode.requests.signupForApi; let changeAddr = pk.toPublicKey().toAddress().toString(); let tx = await CrowdNode._dashApi.createPayment( wif, hotwallet, msg, changeAddr, ); await CrowdNode._insightApi.instantSend(tx.serialize()); let reply = CrowdNode.offset + CrowdNode.responses.PleaseAcceptTerms; return await Ws.waitForVout( CrowdNode._dashsocketBaseUrl, changeAddr, reply, ); }; /** * @param {String} wif * @param {String} hotwallet */ CrowdNode.accept = async function (wif, hotwallet) { // Send Request Message let pk = new Dashcore.PrivateKey(wif); let msg = CrowdNode.offset + CrowdNode.requests.acceptTerms; let changeAddr = pk.toPublicKey().toAddress().toString(); let tx = await CrowdNode._dashApi.createPayment( wif, hotwallet, msg, changeAddr, ); await CrowdNode._insightApi.instantSend(tx.serialize()); let reply = CrowdNode.offset + CrowdNode.responses.WelcomeToCrowdNodeBlockChainAPI; return await Ws.waitForVout( CrowdNode._dashsocketBaseUrl, changeAddr, reply, ); }; /** * @param {String} wif * @param {String} hotwallet * @param {Number} amount - Duffs (1/100000000 Dash) */ CrowdNode.deposit = async function (wif, hotwallet, amount) { // Send Request Message let pk = new Dashcore.PrivateKey(wif); let changeAddr = pk.toPublicKey().toAddress().toString(); // TODO reserve a balance let tx; if (amount) { tx = await CrowdNode._dashApi.createPayment( wif, hotwallet, amount, changeAddr, ); } else { tx = await CrowdNode._dashApi.createBalanceTransfer(wif, hotwallet); } await CrowdNode._insightApi.instantSend(tx.serialize()); let reply = CrowdNode.offset + CrowdNode.responses.DepositReceived; return await Ws.waitForVout( CrowdNode._dashsocketBaseUrl, changeAddr, reply, ); }; /** * @param {String} wif * @param {String} hotwallet * @param {Number} permil - 1/1000 (1/10 of a percent) 500 permille = 50.0 percent */ CrowdNode.withdraw = async function (wif, hotwallet, permil) { let valid = permil > 0 && permil <= 1000; valid = valid && Math.round(permil) === permil; if (!valid) { throw new Error(`'permil' must be between 1 and 1000, not '${permil}'`); } // Send Request Message let pk = new Dashcore.PrivateKey(wif); let msg = CrowdNode.offset + permil; let changeAddr = pk.toPublicKey().toAddress().toString(); let tx = await CrowdNode._dashApi.createPayment( wif, hotwallet, msg, changeAddr, ); await CrowdNode._insightApi.instantSend(tx.serialize()); // Listen for Response let mempoolTx = { address: "", api: "", at: 0, txid: "", satoshis: 0, txlock: false, }; return await Ws.listen(CrowdNode._dashsocketBaseUrl, findResponse); /** * @param {String} evname * @param {InsightSocketEventData} data */ function findResponse(evname, data) { if (!["tx", "txlock"].includes(evname)) { return; } let now = Date.now(); if (mempoolTx.at) { // don't wait longer than 3s for a txlock if (now - mempoolTx.at > 3000) { return mempoolTx; } } let result; // TODO should fetch tx and match hotwallet as vin data.vout.some(function (vout) { return Object.keys(vout).some(function (addr) { if (addr !== changeAddr) { return false; } let duffs = vout[addr]; let msg = duffs - CrowdNode.offset; let api = CrowdNode._responses[msg]; if (!api) { // the withdraw often happens before the queued message console.warn(` => received '${duffs}' (${evname})`); return false; } let newTx = { address: addr, api: api.toString(), at: now, txid: data.txid, satoshis: duffs, txlock: data.txlock, }; if ("txlock" !== evname) { // wait up to 3s for a txlock if (!mempoolTx) { mempoolTx = newTx; } return false; } result = newTx; return true; }); }); return result; } }; CrowdNode._withdrawalWarned = false; /** * @param {String} wif * @param {String} hotwallet * @param {Number} permil * @ignore - this is deprecated, not to be used */ CrowdNode.withdrawal = async function (wif, hotwallet, permil) { if (!CrowdNode._withdrawalWarned) { CrowdNode._withdrawalWarned = true; console.warn( `[Deprecation Notice] CrowdNode.withdrawal() is a misspelling of CrowdNode.withdraw()`, ); } return await CrowdNode.withdraw(wif, hotwallet, permil); }; // See ./bin/crowdnode-list-apis.sh CrowdNode.http = {}; /** * @param {String} pub */ CrowdNode.http.FundsOpen = async function (pub) { return `Open <${CrowdNode._baseUrl}/FundsOpen/${pub}> in your browser.`; }; /** * @param {String} pub */ CrowdNode.http.VotingOpen = async function (pub) { return `Open <${CrowdNode._baseUrl}/VotingOpen/${pub}> in your browser.`; }; /** * @param {String} pub */ CrowdNode.http.GetFunds = createApi( `/odata/apifundings/GetFunds(address='{1}')`, ); /** * @param {String} pub * @param {String} secondsSinceEpoch */ CrowdNode.http.GetFundsFrom = createApi( `/odata/apifundings/GetFundsFrom(address='{1}',fromUnixTime={2})`, ); /** * @param {String} addr * @returns {Promise<CrowdNodeBalance>} * @ignore */ CrowdNode.http._GetBalance = createApi( `/odata/apifundings/GetBalance(address='{1}')`, ); /** * @param {String} addr * @returns {Promise<CrowdNodeBalance>} */ CrowdNode.http.GetBalance = async function (addr) { let balanceInfo = await CrowdNode.http._GetBalance(addr); if (!("TotalBalance" in balanceInfo)) { return balanceInfo; } // Workaround for https://github.com/dashhive/crowdnode-cli/issues/19 // (we could also `b = Math.round(Math.floor(b * DUFFS) / DUFFS)`) balanceInfo.TotalBalance = parseFloat(balanceInfo.TotalBalance.toFixed(8)); balanceInfo.TotalActiveBalance = parseFloat( balanceInfo.TotalActiveBalance.toFixed(8), ); balanceInfo.TotalDividend = parseFloat( balanceInfo.TotalDividend.toFixed(8), ); return balanceInfo; }; /** * @param {String} pub */ CrowdNode.http.GetMessages = createApi( `/odata/apimessages/GetMessages(address='{1}')`, ); /** * @param {String} pub */ CrowdNode.http.IsAddressInUse = createApi( `/odata/apiaddresses/IsAddressInUse(address='{1}')`, ); /** * Set Email Address: messagetype=1 * @param {String} pub - pay to pubkey base58check address * @param {String} email * @param {String} signature */ CrowdNode.http.SetEmail = createApi( `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=1)`, ); /** * Vote on Governance Objects: messagetype=2 * @param {String} pub - pay to pubkey base58check address * @param {String} gobject-hash * @param {String} choice - Yes|No|Abstain|Delegate|DoNothing * @param {String} signature */ CrowdNode.http.Vote = createApi( `/odata/apimessages/SendMessage(address='{1}',message='{2},{3}',signature={4}',messagetype=2)`, ); /** * Set Referral: messagetype=3 * @param {String} pub - pay to pubkey base58check address * @param {String} referralId * @param {String} signature */ CrowdNode.http.SetReferral = createApi( `/odata/apimessages/SendMessage(address='{1}',message='{2}',signature='{3}',messagetype=3)`, ); /** * @param {String} tmplUrl */ function createApi(tmplUrl) { /** * @param {Array<String>} arguments - typically just 'pub', unless SendMessage */ return async function () { /** @type Array<String> */ //@ts-ignore - arguments let args = [].slice.call(arguments, 0); // ex: let url = `${CrowdNode._baseUrl}${tmplUrl}`; args.forEach(function (arg, i) { let n = i + 1; url = url.replace(new RegExp(`\\{${n}\\}`, "g"), arg); }); let resp = await request({ // TODO https://app.crowdnode.io/odata/apifundings/HotWallet method: "GET", url: url, json: true, }); if (!resp.ok) { let err = new Error( `http error: ${resp.statusCode} ${resp.body.message}`, ); //@ts-ignore err.response = resp.toJSON(); throw err; } return resp.body; }; } /** * @param {String|Number} dash */ function toDuff(dash) { //@ts-ignore let dashF = parseFloat(dash); return Math.round(dashF * DUFFS); } if ("undefined" !== typeof module) { module.exports = CrowdNode; } })(("undefined" !== typeof module && module.exports) || window);