You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HelenaExpressApp/www/assets/js/crypto.js

498 lines
21 KiB
JavaScript

/*
* 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();
}
});