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

374 lines
16 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):(bc1|[13]|D)[a-zA-HJ-NP-Z0-9]{25,}$/;
var walletPrivateKeyRegex = /^[0-9A-Za-z]+$/;
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 transaction.serialize();
} 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: " + xhr.status + ": " + xhr.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: " + xhr.status + ": " + xhr.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.setProgress(80);
progressdialog.setText("Sending payment...");
apirequest(SETTINGS.apis.broadcasttransaction, {
transactiondata: txdata,
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
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: " + xhr.status + ": " + xhr.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: " + xhr.status + ": " + xhr.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: " + xhr.status + ": " + xhr.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: " + xhr.status + ": " + xhr.statusText);
}
});
}, function (error) {
app.dialog.close();
app.dialog.alert(error, "Error");
});
}
function walletGUISendCoins() {
if (!walletPubKeyRegex.test($('#walletFromAddress').val())) {
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(), $('#walletFromAddress').val(), $('#walletToAddress').val(), parseFloat($('#transactionAmount').val()));
}
function displayWalletBalance(address) {
if (!walletPubKeyRegex.test(address)) {
app.dialog.alert("That doesn't look like a valid wallet address.", "Error");
return;
}
app.dialog.preloader("Loading...");
apirequest(SETTINGS.apis.walletbalance, {
walletaddress: address
}, function (resp) {
app.dialog.close();
if (resp.status == "OK") {
$("#walletBalancePopup #walletBalanceAmount").text(resp.balance + " " + resp.currency);
$("#walletBalancePopup #walletFiatAmount").text(resp.usdvalue);
$("#walletBalancePopup #walletCurrency").text(resp.label);
$("#walletBalancePopup #walletBalanceAttribution").text(resp.attribution);
$("#walletBalancePopup #walletBalanceLogo").attr("src", "./assets/images/crypto/" + resp.currency + ".svg");
app.popup.open("#walletBalancePopup");
} else {
app.dialog.alert(resp.msg, "Error");
}
}, function (error) {
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: " + xhr.status + ": " + xhr.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: " + xhr.status + ": " + xhr.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);
});
}
/**
* Hides the fiat conversion input box.
* @returns {undefined}
*/
function unsetupFiatConversion() {
$("#cryptoFiatInputItem").css("display", "none");
$("#cryptoAmountSendCurrencyLabel").text("");
$("#transactionAmountFiat").removeData("exchange-rate");
$("#transactionAmountFiat").removeData("cryptocurrency");
}
$("#app").on("input change paste keyup", "#transactionAmountFiat", function () {
var fiatamount = parseFloat($("#transactionAmountFiat").val());
var exchangerate = parseFloat($("#transactionAmountFiat").data("exchange-rate"));
$("#transactionAmount").val((fiatamount / exchangerate).toFixed(8));
//$("#transactionAmountFiat").val(fiatamount.toFixed(2));
});
$("#app").on("paste blur", "#walletFromAddress", function () {
if (walletPubKeyRegex.test($("#walletFromAddress").val())) {
setupFiatConversion($("#walletFromAddress").val());
} else {
unsetupFiatConversion();
}
});
$("#app").on("input change paste keyup", "#transactionAmount", 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));
});