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

523 lines
22 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 (platform_type == "cordova") {
if (cordova.platformId == "ios") {
app.dialog.create({
title: "Switch to Web Version",
text: "Apple doesn't allow this feature on the App Store. Tap Continue to launch the web version of this app instead, which is allowed to send crypto. We're sorry for the inconvenience.",
verticalButtons: true,
buttons: [
{
text: "Continue",
bold: true,
keyCodes: [13],
onClick: function () {
openExternalBrowser("https://app.helena.express/?startpage=crypto");
}
},
{
text: "Cancel",
color: "red"
}
]
}).open();
return;
}
}
app.popup.open("#sendCryptoPopup");
if (walletPubKeyRegex.test($("#walletAddress").text())) {
setupFiatConversion($("#walletAddress").text());
} else {
unsetupFiatConversion();
}
});