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.
498 lines
21 KiB
JavaScript
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();
|
|
}
|
|
}); |