/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var walletPubKeyRegex = /^(bc1|[13]|D)[a-zA-HJ-NP-Z0-9]{25,}$/; var paymentRequestRegex = /^(?:bitcoin|dogecoin):([a-zA-Z0-9]{20,40})(?:\?(.*))?$/; var walletPrivateKeyRegex = /^[0-9A-Za-z]+$/; /** * Parse a crypto URI and extract the info in it. * Copyright (c) 2019 Robin Linus, MIT license, https://github.com/coins/bitcoin-uri-js * @param {type} uri * @returns {parsePaymentURI.parsed} */ function parsePaymentURI(uri) { const legalKeys = ['address', 'amount', 'value', 'message', 'send', 'tx']; const match = paymentRequestRegex.exec(uri); if (!match) { return null; } const parsed = {uri: uri} if (match[2]) { const queries = match[2].split('&'); for (let i = 0; i < queries.length; i++) { const query = queries[i].split('='); const key = query[0]; if (query.length === 2 && legalKeys.includes(key)) { parsed[key] = decodeURIComponent(query[1].replace(/\+/g, '%20')); } } } parsed.address = match[1]; return parsed; } function openWalletPage(walletaddress) { var navuri = '/crypto/' + walletaddress; if (typeof router.currentRoute.query.paymenturi != 'undefined') { var parsed = parsePaymentURI(router.currentRoute.query.paymenturi); if (parsed != null) { navuri += '/' + parsed.address; if (typeof parsed['amount'] != 'undefined') { navuri += '/' + parsed.amount; } } } router.navigate(navuri); } function scanWalletQrCode(callback) { scanBarcode(function (result) { if (walletPubKeyRegex.test(result)) { callback(result); } else { app.dialog.alert("That doesn't look like a valid wallet address.", "Error"); return; } }, function () { app.dialog.alert("Something went wrong and we can't scan right now.", "Error"); }); } function scanPrivateKeyQrCode(callback) { scanBarcode(function (result) { if (walletPrivateKeyRegex.test(result)) { callback(result); } else { app.dialog.alert("That doesn't look like a valid wallet address.", "Error"); return; } }, function () { app.dialog.alert("Something went wrong and we can't scan right now.", "Error"); }); } /** * Create and sign a crypto transaction. * * @param {type} bitcoreLib Bitcore, Litecore, Dogecore, etc. * @param {type} privateKeyString Private key from wallet QR code * @param {type} sourceAddress Sender's wallet address * @param {type} destinationAddress Recipient's wallet address * @param {Array} utxos Unspent transaction inputs, as array. See createUtxo() * @param {type} outputSatoshis Amount to send to recipient's wallet * @returns {string} Hex of serialized transaction, suitable for broadcast via Bitcoin Core or an API. */ function createSignedTransaction(bitcoreLib, privateKeyString, sourceAddress, destinationAddress, utxos, outputSatoshis, feePerByte) { if (typeof feePerByte == "undefined") { feePerByte = -1; } try { var privateKey = new bitcoreLib.PrivateKey(privateKeyString); var transaction = new bitcoreLib.Transaction() .from(utxos) .to(destinationAddress, outputSatoshis) .change(sourceAddress); var size = transaction._estimateSize(); var fee = size * feePerByte; if (feePerByte > -1) { // use our fee transaction = transaction.fee(fee); } else { // use lib's fee fee = transaction.getFee(); } transaction = transaction.sign(privateKey); var inputTotal = transaction._getInputAmount(); var outputTotal = fee + outputSatoshis; } catch (ex) { throw new Error("There was an internal error while creating the transaction. Details: " + ex.message); } console.log(inputTotal, outputTotal); if (outputTotal > inputTotal) { throw new Error("You have insufficient funds to cover the payment and transaction fees."); } try { return {serialized: transaction.serialize(), fee: fee, sendamount: outputSatoshis, totalspent: outputTotal}; } catch (ex) { throw new Error("Couldn't create the transaction. It's likely you typed something wrong. Check that you have enough funds."); } } /** * Create a UTXO. * * @param {type} sourceAddress Sender's wallet address * @param {type} txHash From UTXO (unspent output) * @param {type} txOutputIndex From UTXO (unspent output) * @param {type} script From UTXO (unspent output) * @param {type} inputSatoshis From UTXO (unspent output) * @returns {createUtxo.utxo} */ function createUtxo(sourceAddress, txHash, txOutputIndex, script, inputSatoshis) { var utxo = { "txId": txHash, "outputIndex": txOutputIndex, "address": sourceAddress, "script": script, "satoshis": inputSatoshis }; return utxo; } /** * Get unspent outputs for a wallet address. * @param {string} walletaddress * @param {function} successCallback Passes object with {utxos: [{txHash,txOutputIndex,script,value}], currency: "DOGE", label: "Dogecoin"} * @param {function} errorCallback Passes string error message suitable for display * @returns {undefined} */ function getUTXOData(walletaddress, successCallback, errorCallback) { apirequest(SETTINGS.apis.getutxo, { walletaddress: walletaddress }, function (resp) { if (resp.status == "OK") { successCallback({ utxos: resp.unspent_outputs, currency: resp.currency, label: resp.label }); } else { errorCallback(resp.msg); } }, function (errorData) { try { var error = $.parseJSON(errorData.responseText); if (error && typeof error.msg != 'undefined') { errorCallback(resp.msg); sendErrorReport("Crypto", "Couldn't get UTXO data", error.msg); } else { errorCallback("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe."); sendErrorReport("Crypto", "Couldn't get UTXO data", "Server/network problem: " + errorData.status + ": " + errorData.statusText); } } catch (ex) { errorCallback("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe."); sendErrorReport("Crypto", "Couldn't get UTXO data", "Server/network problem: " + errorData.status + ": " + errorData.statusText); } }); } function sendCoins(privatekey, fromaddress, toaddress, amount) { var progressdialog = app.dialog.progress("Querying blockchain...", 20); getUTXOData(fromaddress, function (success) { progressdialog.setProgress(40); progressdialog.setText("Creating transaction..."); if (success.utxos.length == 0) { app.dialog.close(); app.dialog.alert("Your wallet has no available funds (ZERO_LENGTH_UTXO).", "Error"); return; } var utxos = []; for (var i = 0; i < success.utxos.length; i++) { utxos.push(createUtxo(fromaddress, success.utxos[i].txHash, success.utxos[i].txOutputIndex, success.utxos[i].script, success.utxos[i].value)); } var bitcore = null; var satoshis = parseInt((amount * 100000000).toFixed(0)); // Make sure it's an int and not something like 10.0000000001 or 9.532999999999 switch (success.currency) { case "DOGE": bitcore = require("bitcore-lib-doge"); break; case "BTC": bitcore = require("bitcore-lib"); break; default: app.dialog.close(); app.dialog.alert("This app version doesn't support " + success.currency + ".", "Error"); return; } progressdialog.setProgress(60); progressdialog.setText("Calculating fees..."); apirequest(SETTINGS.apis.cryptofees, { currency: success.currency }, function (resp) { if (resp.status == "OK") { try { var txdata = createSignedTransaction(bitcore, privatekey, fromaddress, toaddress, utxos, satoshis, resp.feePerByte); } catch (ex) { console.error(ex); app.dialog.close(); app.dialog.alert(ex.message, "Error"); return; } progressdialog.close(); app.dialog.confirm("Sending " + (txdata.sendamount / 100000000) + " " + success.currency + " with a fee of " + (txdata.fee / 100000000) + " " + success.currency + " for a total spend of " + (txdata.totalspent / 100000000) + " " + success.currency + ".", "Confirm Transaction", function (ok) { progressdialog = app.dialog.progress("Sending payment...", 80); apirequest(SETTINGS.apis.broadcasttransaction, { transactiondata: txdata.serialized, currency: success.currency }, function (resp) { if (resp.status == "OK") { app.dialog.close(); app.dialog.alert("Sent " + amount + " " + success.currency + " to " + toaddress.substring(0, 5) + "..." + toaddress.substring(toaddress.length - 5, 999), "Success!"); $('#walletPrivateKey').val(""); // clear private key input box app.popup.close(); return; } else { app.dialog.close(); app.dialog.alert(resp.msg, "Error"); } }, function (errorData) { app.dialog.close(); try { var error = $.parseJSON(errorData.responseText); if (error && typeof error.msg != 'undefined') { app.dialog.alert(error.msg, "Error"); sendErrorReport("Crypto", "Couldn't broadcast transaction", error.msg); } else { app.dialog.alert("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe.", "Error"); sendErrorReport("Crypto", "Couldn't broadcast transaction", "Server/network problem: " + errorData.status + ": " + errorData.statusText); } } catch (ex) { app.dialog.alert("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe.", "Error"); sendErrorReport("Crypto", "Couldn't broadcast transaction", "Server/network problem: " + errorData.status + ": " + errorData.statusText); } }); }); } else { app.dialog.close(); app.dialog.alert(resp.msg, "Error"); } }, function (errorData) { app.dialog.close(); try { var error = $.parseJSON(errorData.responseText); if (error && typeof error.msg != 'undefined') { app.dialog.alert(error.msg, "Error"); sendErrorReport("Crypto", "Couldn't get transaction fees", error.msg); } else { app.dialog.alert("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe.", "Error"); sendErrorReport("Crypto", "Couldn't get transaction fees", "Server/network problem: " + errorData.status + ": " + errorData.statusText); } } catch (ex) { app.dialog.alert("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe.", "Error"); sendErrorReport("Crypto", "Couldn't get transaction fees", "Server/network problem: " + errorData.status + ": " + errorData.statusText); } }); }, function (error) { app.dialog.close(); app.dialog.alert(error, "Error"); }); } function walletGUISendCoins() { if (!walletPubKeyRegex.test($('#walletAddress').text())) { app.dialog.alert("Your wallet address doesn't look right. Check it and try again.", "Error"); return; } if (isNaN($('#transactionAmount').val()) || $('#transactionAmount').val() < 0.00000001) { app.dialog.alert("The amount to send doesn't look right. Check it and try again.", "Error"); return; } // Remove payment request URL stuff if ($('#walletToAddress').val().startsWith("bitcoin:")) { $('#walletToAddress').val($('#walletToAddress').val().replace("bitcoin:", "")); } if ($('#walletToAddress').val().startsWith("dogecoin:")) { $('#walletToAddress').val($('#walletToAddress').val().replace("dogecoin:", "")); } if (!walletPubKeyRegex.test($('#walletToAddress').val())) { app.dialog.alert("The recipient's wallet address doesn't look right. Check it and try again.", "Error"); return; } sendCoins($('#walletPrivateKey').val(), $('#walletAddress').text(), $('#walletToAddress').val(), parseFloat($('#transactionAmount').val())); } function openWalletBalancePage( {to, resolve, reject}) { var address = to.params.walletaddress; if (!walletPubKeyRegex.test(address)) { app.dialog.alert("That doesn't look like a valid wallet address.", "Error"); reject(); return; } app.dialog.preloader("Loading..."); apirequest(SETTINGS.apis.walletbalance, { walletaddress: address }, function (resp) { app.dialog.close(); if (resp.status == "OK") { var context = { balance: resp.balance, currencyunit: resp.currency, fiatvalue: resp.usdvalue, currencyname: resp.label, attribution: resp.attribution, exchangerate: resp.exchangerates.usd, logo: "./assets/images/crypto/" + resp.currency + ".svg", walletaddress: to.params.walletaddress, sendtoaddress: (typeof to.params.toaddress != "undefined" ? to.params.toaddress : ""), sendtoamount: (typeof to.params.amount != "undefined" ? to.params.amount : "") }; resolve({ content: compiledPages.crypto_wallet(context) }); } else { reject(); app.dialog.alert(resp.msg, "Error"); } }, function (error) { reject(); app.dialog.close(); try { var error = $.parseJSON(error.responseText); if (error && typeof error.msg != 'undefined') { app.dialog.alert(error.msg, "Error"); sendErrorReport("Crypto", "Couldn't get wallet balance", error.msg); } else { app.dialog.alert("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe.", "Error"); sendErrorReport("Crypto", "Couldn't get wallet balance", "Server/network problem: " + error.status + ": " + error.statusText); } } catch (ex) { app.dialog.alert("There's a server or network problem. Check your Internet connection or try again later. Your funds are safe.", "Error"); sendErrorReport("Crypto", "Couldn't get wallet balance", "Server/network problem: " + error.status + ": " + error.statusText); } }); } /** * Setup an input for specifying amount to send in USD, with conversion to crypto. * @param {string} walletAddress Detects cryptocurrency from wallet address * @returns {undefined} */ function setupFiatConversion(walletAddress) { apirequest(SETTINGS.apis.walletbalance, { walletaddress: walletAddress }, function (resp) { if (resp.status != "OK") { return; } if (resp.exchangerates.usd == -1) { return; } $("#cryptoFiatInputItem").css("display", ""); $("#cryptoAmountSendCurrencyLabel").text(resp.currency); $("#cryptoAmountSendFiatLabel").text("$"); $("#transactionAmountFiat").data("exchange-rate", resp.exchangerates.usd); $("#transactionAmountFiat").data("cryptocurrency", resp.currency); $("#transactionAmount").off("input change paste keyup"); $("#transactionAmountFiat").off("input change paste keyup"); $("#transactionAmount").on("input change paste keyup", function () { if ($("#cryptoFiatInputItem").css("display") == "none") { return; } var amount = parseFloat($("#transactionAmount").val()); var exchangerate = parseFloat($("#transactionAmountFiat").data("exchange-rate")); $("#transactionAmountFiat").val((amount * exchangerate).toFixed(2)); //$("#transactionAmount").val(amount.toFixed(8)); }); $("#transactionAmountFiat").on("input change paste keyup", function () { if ($("#cryptoFiatInputItem").css("display") == "none") { return; } var fiatamount = parseFloat($("#transactionAmountFiat").val()); var exchangerate = parseFloat($("#transactionAmountFiat").data("exchange-rate")); $("#transactionAmount").val((fiatamount / exchangerate).toFixed(8)); }); if ($("#transactionAmount").val() != "") { // Update the fiat conversion calculation if there's an amount prefilled $("#transactionAmount").trigger("input"); } }); } function setupReceiveFiatConversion() { var exchangerate = $("#receiveAmountFiat").data("exchangerate"); var fiatlabel = $("#receiveAmountFiat").data("currencylabel"); if (exchangerate == -1) { return; } $("#cryptoAmountReceiveFiatLI").css("display", ""); $("#cryptoAmountReceiveFiatLabel").text(fiatlabel); $("#receiveAmount").off("input change paste keyup"); $("#receiveAmountFiat").off("input change paste keyup"); $("#receiveAmount").on("input change paste keyup", function () { if ($("#cryptoAmountReceiveFiatLI").css("display") == "none") { return; } var amount = parseFloat($("#receiveAmount").val()); var exchangerate = parseFloat($("#receiveAmountFiat").data("exchangerate")); $("#receiveAmountFiat").val((amount * exchangerate).toFixed(2)); }); $("#receiveAmountFiat").on("input change paste keyup", function () { var fiatamount = parseFloat($("#receiveAmountFiat").val()); var exchangerate = parseFloat($("#receiveAmountFiat").data("exchangerate")); $("#receiveAmount").val((fiatamount / exchangerate).toFixed(8)); }); if ($("#receiveAmount").val() != "") { // Update the fiat conversion calculation if there's an amount prefilled $("#receiveAmount").trigger("input"); } } /** * Hides the fiat conversion input box. * @returns {undefined} */ function unsetupFiatConversion() { $("#cryptoFiatInputItem").css("display", "none"); $("#cryptoAmountSendCurrencyLabel").text(""); $("#transactionAmountFiat").removeData("exchange-rate"); $("#transactionAmountFiat").removeData("cryptocurrency"); $("#transactionAmount").off("input change paste keyup"); $("#transactionAmountFiat").off("input change paste keyup"); } function showPaymentRequestQRCode() { var paymenturi = ""; switch ($("#receiveAmount").data("currency")) { case "DOGE": paymenturi = "dogecoin:"; break; case "BTC": paymenturi = "bitcoin:"; break; } paymenturi += $('#walletAddress').text(); if ($("#receiveAmount").val() > 0) { paymenturi += "?amount=" + $("#receiveAmount").val(); } $("#paymentRequestQRCodeContainer").html(""); new QRCode(document.getElementById("paymentRequestQRCodeContainer"), paymenturi); } $("#app").on("click", "#sendCryptoOpenPopupBtn", function () { if (walletPubKeyRegex.test($("#walletAddress").text())) { setupFiatConversion($("#walletAddress").text()); } else { unsetupFiatConversion(); } });