355 lines
18 KiB
HTML
355 lines
18 KiB
HTML
<!doctype html>
|
|
<html>
|
|
|
|
<head>
|
|
<title>BTC webWallet</title>
|
|
<style>
|
|
.unconfirmed-tx {
|
|
color: red;
|
|
}
|
|
</style>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
|
|
|
<script type="text/javascript" src="lib.js"></script>
|
|
<script type="text/javascript" src="lib_btc.js"></script>
|
|
</head>
|
|
|
|
<body>
|
|
<nav class="navbar navbar-default">
|
|
<div class="container-fluid">
|
|
<div class="navbar-header">
|
|
<a class="navbar-brand"><i class="glyphicon glyphicon-bitcoin"></i>itcoin WebWallet</a>
|
|
</div>
|
|
<ul id="menu" class="nav navbar-nav">
|
|
<li data-n="1"><a href="#">Address</a></li>
|
|
<li data-n="2"><a href="#">Send</a></li>
|
|
<li data-n="3" class="active"><a href="#">Details</a></li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
<div class="container-fluid">
|
|
<div name="panels" data-n="1" id="address-gen-panel" class="panel panel-default hide">
|
|
<div class="panel-heading">Address Generator</div>
|
|
<div class="panel-body">
|
|
<form id="generate-address" autocomplete="off">
|
|
<div class="form-group">
|
|
<label class="radio-inline"><input type="radio" name="gen-radio" value="generate" checked>Generate</label>
|
|
<label class="radio-inline"><input type="radio" name="gen-radio" value="retrieve">Retrieve</label>
|
|
<button type="button" name="submit" class="btn btn-default">Go</button>
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i>Private</span>
|
|
<input type="password" class="form-control" name="private" placeholder="PrivateKey" disabled>
|
|
<div class="input-group-btn">
|
|
<button class="btn btn-default" name="eye" type="button">
|
|
<i class="glyphicon glyphicon-eye-close"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i>Bech32</i></span>
|
|
<input type="text" class="form-control" name="bech32" placeholder="Bech32Address" disabled>
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i>Segwit</i></span>
|
|
<input type="text" class="form-control" name="segwit" placeholder="SegwitAddress" disabled>
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i>Legacy</i></span>
|
|
<input type="text" class="form-control" name="legacy" placeholder="LegacyAddress" disabled>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div name="panels" data-n="2" id="send-tx-panel" class="panel panel-default hide">
|
|
<div class="panel-heading">Send Transaction</div>
|
|
<div class="panel-body">
|
|
<form id="send-tx">
|
|
<template class="sender-template">
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i>Sender</i></span>
|
|
<input type="text" class="form-control" name="sender" placeholder="Sender ID">
|
|
<span class="input-group-addon"><i>Balance</i></span>
|
|
<input type="text" class="form-control" name="balance" placeholder="Balance" disabled>
|
|
<div class="input-group-btn">
|
|
<button class="btn btn-default" name="rm-sender" type="button">X</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div class="form-group">
|
|
<div class="sender-container"></div>
|
|
<div class="input-group">
|
|
<div class="input-group-btn">
|
|
<button class="btn btn-default" name="check-balance" type="button">Check Balance</button>
|
|
</div>
|
|
<input type="number" class="form-control" name="total_balance" placeholder="Total Balance" disabled>
|
|
</div>
|
|
<button type="button" name="add-sender" class="btn btn-default"><b>+</b></button>
|
|
</div>
|
|
<template class="receiver-template">
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i>Receiver</i></span>
|
|
<input type="text" class="form-control" name="receiver" placeholder="Receiver ID">
|
|
<span class="input-group-addon"><i>Amount</i></span>
|
|
<input type="number" class="form-control" name="amount" placeholder="Amount" min="0" step="0.01">
|
|
<div class="input-group-btn">
|
|
<button class="btn btn-default" name="rm-receiver" type="button">X</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div class="form-group">
|
|
<div class="receiver-container"></div>
|
|
<button type="button" name="add-receiver" class="btn btn-default"><b>+</b></button>
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i>Fee</i></span>
|
|
<input type="number" class="form-control" name="fee" placeholder="Fee" min="0" step="0.000001">
|
|
</div>
|
|
<button type="button" name="submit" class="btn btn-default btn-block"><b>Send</b></button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div name="panels" data-n="3" id="view-details-panel" class="panel panel-default">
|
|
<div class="panel-heading">Address Details</div>
|
|
<div class="panel-body">
|
|
<form id="address-details">
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i>Address</i></span>
|
|
<input type="text" class="form-control" name="addr" placeholder="Address">
|
|
<div class="input-group-btn">
|
|
<button class="btn btn-default" name="submit" type="button"><b class="glyphicon glyphicon-search"></b></button>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-addon"><i class="glyphicon glyphicon-bitcoin"></i></span>
|
|
<input type="number" class="form-control" name="balance" placeholder="Balance" disabled>
|
|
</div>
|
|
</form>
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>Address</th>
|
|
<th>Amount</th>
|
|
<th>Time</th>
|
|
<th>TxID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="view-details"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
</body>
|
|
<script>
|
|
(function() {
|
|
|
|
let menu = document.getElementById('menu').children;
|
|
let panels = document.getElementsByName('panels');
|
|
for (let i = 0; i < menu.length; i++) {
|
|
menu[i].onclick = evt => {
|
|
let n = evt.target.parentElement.dataset.n;
|
|
for (let j = 0; j < menu.length; j++)
|
|
if (menu[j].dataset.n == n)
|
|
menu[j].classList.add('active');
|
|
else
|
|
menu[j].classList.remove('active');
|
|
for (let p = 0; p < panels.length; p++)
|
|
if (panels[p].dataset.n == n)
|
|
panels[p].classList.remove('hide');
|
|
else
|
|
panels[p].classList.add('hide');
|
|
}
|
|
}
|
|
|
|
let addressForm = document.forms['generate-address'];
|
|
for (let i = 0; i < addressForm['gen-radio'].length; i++)
|
|
addressForm['gen-radio'][i].addEventListener('change', evt => addressForm['private'].disabled = evt.target.value === "generate");
|
|
addressForm['eye'].onclick = evt => {
|
|
if (addressForm['private'].type === "password")
|
|
addressForm['private'].type = "text";
|
|
else
|
|
addressForm['private'].type = "password";
|
|
}
|
|
addressForm['submit'].onclick = evt => {
|
|
switch (addressForm['gen-radio'].value) {
|
|
case "generate": {
|
|
let newKeys = btc_api.newKeys;
|
|
addressForm['private'].value = newKeys.wif;
|
|
addressForm['legacy'].value = newKeys.address;
|
|
addressForm['segwit'].value = newKeys.segwitAddress;
|
|
addressForm['bech32'].value = newKeys.bech32Address;
|
|
break;
|
|
}
|
|
case "retrieve": {
|
|
let wif = addressForm['private'].value;
|
|
addressForm['legacy'].value = btc_api.address(wif);
|
|
addressForm['segwit'].value = btc_api.segwitAddress(wif);
|
|
addressForm['bech32'].value = btc_api.bech32Address(wif);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let sendForm = document.forms['send-tx'];
|
|
let sender_template = sendForm.getElementsByClassName('sender-template')[0],
|
|
sender_container = sendForm.getElementsByClassName('sender-container')[0];
|
|
sendForm['add-sender'].onclick = evt => {
|
|
sender_container.appendChild(sender_template.content.cloneNode(true));
|
|
let clone = sender_container.lastElementChild;
|
|
clone.getElementsByTagName("button")[0].onclick = evt => sender_container.removeChild(clone);
|
|
};
|
|
sendForm['add-sender'].click();
|
|
sendForm['check-balance'].onclick = evt => {
|
|
sendForm["total_balance"].value = 0;
|
|
let addresses = [],
|
|
total_balance = 0;
|
|
if (!sendForm["sender"])
|
|
return;
|
|
else if (sendForm["sender"] instanceof RadioNodeList)
|
|
for (let i = 0; i < sendForm["sender"].length; i++)
|
|
addresses.push(sendForm["sender"][i].value);
|
|
else
|
|
addresses.push(sendForm["sender"].value);
|
|
console.debug(addresses);
|
|
let balance_inputs = sendForm["balance"] instanceof RadioNodeList ? sendForm["balance"] : [sendForm["balance"]];
|
|
//sendForm['check-balance'].disabled = true;
|
|
addresses.forEach((addr, i) => btc_api.getBalance(addr).then(result => {
|
|
console.debug(addr, result)
|
|
balance_inputs[i].value = result;
|
|
sendForm["total_balance"].value = parseFloat(sendForm["total_balance"].value) + result;
|
|
}).catch(error => console.error(error)))
|
|
// .finally(_ => sendForm['check-balance'].disabled = false)
|
|
}
|
|
let receiver_template = sendForm.getElementsByClassName('receiver-template')[0],
|
|
receiver_container = sendForm.getElementsByClassName('receiver-container')[0];
|
|
sendForm['add-receiver'].onclick = evt => {
|
|
receiver_container.appendChild(receiver_template.content.cloneNode(true));
|
|
let clone = receiver_container.lastElementChild;
|
|
clone.getElementsByTagName("button")[0].onclick = evt => receiver_container.removeChild(clone);
|
|
};
|
|
sendForm['add-receiver'].click();
|
|
sendForm['submit'].onclick = evt => {
|
|
let senders = [],
|
|
receivers = [],
|
|
amounts = [],
|
|
fee = parseFloat(sendForm["fee"].value);
|
|
//inputs (senders)
|
|
if (!sendForm["sender"])
|
|
return console.warn("sender cannot be empty");
|
|
else if (sendForm["sender"] instanceof RadioNodeList)
|
|
for (let i = 0; i < sendForm["sender"].length; i++) {
|
|
senders.push(sendForm["sender"][i].value);
|
|
}
|
|
else {
|
|
senders.push(sendForm["sender"].value);
|
|
}
|
|
//outputs (receivers and amounts)
|
|
if (!sendForm["receiver"])
|
|
return console.warn("receiver cannot be empty");
|
|
else if (sendForm["receiver"] instanceof RadioNodeList)
|
|
for (let i = 0; i < sendForm["receiver"].length; i++) {
|
|
receivers.push(sendForm["receiver"][i].value);
|
|
amounts.push(parseFloat(sendForm["amount"][i].value));
|
|
}
|
|
else {
|
|
receivers.push(sendForm["receiver"].value);
|
|
amounts.push(parseFloat(sendForm["amount"].value));
|
|
}
|
|
console.debug(senders, receivers, amounts, fee);
|
|
let privkeys = senders.map(s => prompt(`Enter Private Key for ${s}:`))
|
|
sendForm['submit'].disabled = true;
|
|
btc_api.sendTx(senders, privkeys, receivers, amounts, fee).then(result => {
|
|
console.log(result);
|
|
alert("transaction id: " + result.txid);
|
|
}).catch(error => console.error(error)).finally(_ => sendForm['submit'].disabled = false)
|
|
}
|
|
|
|
let detailsForm = document.forms['address-details'];
|
|
detailsForm['submit'].onclick = evt => {
|
|
detailsForm['submit'].disabled = true;
|
|
let address = detailsForm['addr'].value;
|
|
let table = document.getElementById("view-details");
|
|
table.innerHTML = '';
|
|
detailsForm['balance'].value = '';
|
|
getAddressDetails(address).then(result => {
|
|
console.debug(result);
|
|
detailsForm['balance'].value = result.balance;
|
|
result.txs.forEach(tx => {
|
|
let row = table.insertRow();
|
|
if (tx.type === "out") {
|
|
row.insertCell().innerHTML = '↗';
|
|
row.insertCell().innerHTML = tx.receiver.join('<br/>');
|
|
} else if (tx.type === "in") {
|
|
row.insertCell().innerHTML = '↙';
|
|
row.insertCell().innerHTML = tx.sender.join('<br/>');
|
|
} else if (tx.type === "self") {
|
|
row.insertCell().innerHTML = '⟲';
|
|
row.insertCell().textContent = tx.address;
|
|
}
|
|
row.insertCell().textContent = tx.amount;
|
|
row.insertCell().textContent = tx.time;
|
|
row.insertCell().textContent = tx.txid;
|
|
if (tx.block === null)
|
|
row.className = 'unconfirmed-tx';
|
|
});
|
|
}).catch(error => console.error(error)).finally(_ => detailsForm['submit'].disabled = false)
|
|
}
|
|
|
|
function getAddressDetails(address) {
|
|
return new Promise((resolve, reject) => {
|
|
btc_api.getAddressData(address).then(data => {
|
|
console.debug(data);
|
|
let details = {};
|
|
details.balance = data.balance;
|
|
details.address = data.address;
|
|
details.txs = data.txs.map(tx => {
|
|
let d = {
|
|
txid: tx.txid,
|
|
time: tx.time,
|
|
block: tx.block_no
|
|
}
|
|
if (tx.outgoing) {
|
|
d.type = "out";
|
|
d.amount = 0;
|
|
d.receiver = new Set();
|
|
let change = 0;
|
|
tx.outgoing.outputs.forEach(o => {
|
|
if (o.address !== address) {
|
|
d.receiver.add(o.address)
|
|
d.amount += parseFloat(o.value)
|
|
} else
|
|
change += parseFloat(o.value)
|
|
});
|
|
d.receiver = Array.from(d.receiver);
|
|
d.amount = parseFloat(d.amount.toFixed(8))
|
|
d.fee = parseFloat((tx.outgoing.value - (d.amount + change)).toFixed(8))
|
|
if (!d.amount && change > 0) {
|
|
d.type = "self";
|
|
d.amount = change
|
|
delete d.receiver;
|
|
d.address = address;
|
|
}
|
|
} else if (tx.incoming) {
|
|
d.type = "in";
|
|
d.amount = parseFloat(tx.incoming.value);
|
|
d.sender = Array.from(new Set(tx.incoming.inputs.map(i => i.address)));
|
|
}
|
|
return d;
|
|
})
|
|
resolve(details);
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
})();
|
|
</script>
|
|
|
|
</html> |