319 lines
9.2 KiB
HTML
319 lines
9.2 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>RinCoin Web Wallet</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
background: #0b1a28;
|
|
color: #f5f8fc;
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
.sidebar {
|
|
width: 260px;
|
|
background: #122c43;
|
|
padding: 24px;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
}
|
|
.content {
|
|
flex: 1;
|
|
padding: 32px;
|
|
box-sizing: border-box;
|
|
}
|
|
h1 {
|
|
margin: 0 0 16px;
|
|
font-size: 24px;
|
|
}
|
|
h2 {
|
|
margin-top: 32px;
|
|
margin-bottom: 16px;
|
|
font-size: 18px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
padding-bottom: 8px;
|
|
}
|
|
label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-size: 14px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
input {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
background: rgba(9, 19, 30, 0.8);
|
|
color: #fff;
|
|
margin-bottom: 16px;
|
|
box-sizing: border-box;
|
|
}
|
|
button {
|
|
background: #29b6f6;
|
|
border: none;
|
|
padding: 12px 16px;
|
|
border-radius: 6px;
|
|
color: #04121f;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease;
|
|
}
|
|
button:hover {
|
|
background: #4fc3f7;
|
|
}
|
|
.card {
|
|
background: rgba(9, 19, 30, 0.8);
|
|
padding: 24px;
|
|
border-radius: 12px;
|
|
margin-bottom: 24px;
|
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
|
|
}
|
|
.balance {
|
|
font-size: 36px;
|
|
font-weight: 600;
|
|
}
|
|
.muted {
|
|
color: rgba(255, 255, 255, 0.6);
|
|
font-size: 14px;
|
|
}
|
|
.transaction-list {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
.transaction-list li {
|
|
padding: 14px 0;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
.address-box {
|
|
word-break: break-all;
|
|
background: rgba(41, 182, 246, 0.15);
|
|
border-radius: 6px;
|
|
padding: 12px;
|
|
margin: 12px 0;
|
|
}
|
|
.status {
|
|
border-radius: 6px;
|
|
margin-top: 16px;
|
|
padding: 12px 14px;
|
|
}
|
|
.status.success {
|
|
background: rgba(76, 175, 80, 0.2);
|
|
color: #c8e6c9;
|
|
}
|
|
.status.error {
|
|
background: rgba(244, 67, 54, 0.2);
|
|
color: #ffcdd2;
|
|
}
|
|
.token-display {
|
|
background: rgba(9, 19, 30, 0.8);
|
|
padding: 12px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
word-break: break-all;
|
|
}
|
|
.sidebar h2 {
|
|
margin-top: 0;
|
|
}
|
|
.sidebar button {
|
|
width: 100%;
|
|
}
|
|
@media (max-width: 900px) {
|
|
body {
|
|
flex-direction: column;
|
|
}
|
|
.sidebar {
|
|
width: 100%;
|
|
flex-direction: row;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.sidebar section {
|
|
flex: 1 1 200px;
|
|
}
|
|
.content {
|
|
padding: 24px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<aside class="sidebar">
|
|
<section>
|
|
<h2>Session</h2>
|
|
<div class="token-display" id="tokenDisplay">Loading…</div>
|
|
</section>
|
|
<section>
|
|
<button id="refreshButton">Refresh Data</button>
|
|
</section>
|
|
<section>
|
|
<button id="generateButton">Generate Address</button>
|
|
</section>
|
|
</aside>
|
|
<main class="content">
|
|
<h1>RinCoin Wallet</h1>
|
|
|
|
<div class="card">
|
|
<div class="muted">Confirmed Balance</div>
|
|
<div class="balance" id="confirmedBalance">—</div>
|
|
<div class="muted" id="totalBalance">Total: —</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Send RinCoin</h2>
|
|
<label for="sendAddress">Recipient Address</label>
|
|
<input id="sendAddress" type="text" placeholder="rin1..." />
|
|
<label for="sendAmount">Amount (RIN)</label>
|
|
<input id="sendAmount" type="number" step="0.00000001" min="0" />
|
|
<button id="sendButton">Send</button>
|
|
<div id="sendStatus" class="status" style="display:none;"></div>
|
|
</div>
|
|
|
|
<div class="card" id="generatedAddressCard" style="display:none;">
|
|
<h2>New Address</h2>
|
|
<div class="address-box" id="generatedAddress"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Recent Activity</h2>
|
|
<ul class="transaction-list" id="txList"></ul>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const tokenDisplay = document.getElementById('tokenDisplay');
|
|
const confirmedBalance = document.getElementById('confirmedBalance');
|
|
const totalBalance = document.getElementById('totalBalance');
|
|
const txList = document.getElementById('txList');
|
|
const sendStatus = document.getElementById('sendStatus');
|
|
const generatedAddressCard = document.getElementById('generatedAddressCard');
|
|
const generatedAddress = document.getElementById('generatedAddress');
|
|
|
|
let apiToken = localStorage.getItem('rinWebWalletToken');
|
|
|
|
async function fetchSession() {
|
|
const res = await fetch('/api/session');
|
|
const data = await res.json();
|
|
if (!apiToken) {
|
|
apiToken = data.token;
|
|
localStorage.setItem('rinWebWalletToken', apiToken);
|
|
}
|
|
tokenDisplay.textContent = `Wallet: ${data.wallet}`;
|
|
}
|
|
|
|
function authHeaders() {
|
|
return { Authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' };
|
|
}
|
|
|
|
async function fetchBalances() {
|
|
const res = await fetch('/api/balance', { headers: authHeaders() });
|
|
const data = await res.json();
|
|
if (data.error) {
|
|
confirmedBalance.textContent = 'Error';
|
|
totalBalance.textContent = data.error;
|
|
return;
|
|
}
|
|
confirmedBalance.textContent = `${Number(data.confirmed).toFixed(8)} RIN`;
|
|
totalBalance.textContent = `Total: ${Number(data.total).toFixed(8)} RIN`;
|
|
}
|
|
|
|
function renderTx(tx) {
|
|
const li = document.createElement('li');
|
|
const amount = Number(tx.amount).toFixed(8);
|
|
const type = tx.category === 'send' ? 'Sent' : 'Received';
|
|
li.innerHTML = `
|
|
<div><strong>${type}</strong> ${amount} RIN</div>
|
|
<div class="muted">${tx.time ? new Date(tx.time * 1000).toLocaleString() : ''}</div>
|
|
<div class="muted">${tx.txid}</div>
|
|
`;
|
|
return li;
|
|
}
|
|
|
|
async function fetchTransactions() {
|
|
const res = await fetch('/api/transactions', { headers: authHeaders() });
|
|
const data = await res.json();
|
|
txList.innerHTML = '';
|
|
if (data.error) {
|
|
txList.innerHTML = `<li class="muted">${data.error}</li>`;
|
|
return;
|
|
}
|
|
if (!data.transactions.length) {
|
|
txList.innerHTML = '<li class="muted">No transactions yet.</li>';
|
|
return;
|
|
}
|
|
data.transactions.forEach((tx) => txList.appendChild(renderTx(tx)));
|
|
}
|
|
|
|
async function generateAddress() {
|
|
const res = await fetch('/api/address', {
|
|
method: 'POST',
|
|
headers: authHeaders(),
|
|
body: JSON.stringify({}),
|
|
});
|
|
const data = await res.json();
|
|
if (data.error) {
|
|
generatedAddressCard.style.display = 'block';
|
|
generatedAddress.textContent = `Error: ${data.error}`;
|
|
return;
|
|
}
|
|
generatedAddressCard.style.display = 'block';
|
|
generatedAddress.textContent = data.address;
|
|
}
|
|
|
|
async function sendCoins() {
|
|
const address = document.getElementById('sendAddress').value.trim();
|
|
const amount = Number(document.getElementById('sendAmount').value);
|
|
|
|
if (!address || !amount) {
|
|
sendStatus.textContent = 'Destination and amount are required.';
|
|
sendStatus.className = 'status error';
|
|
sendStatus.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
const res = await fetch('/api/send', {
|
|
method: 'POST',
|
|
headers: authHeaders(),
|
|
body: JSON.stringify({ address, amount, subtractFee: true }),
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
sendStatus.textContent = data.error;
|
|
sendStatus.className = 'status error';
|
|
sendStatus.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
sendStatus.textContent = `Transaction broadcast. TXID: ${data.txid}`;
|
|
sendStatus.className = 'status success';
|
|
sendStatus.style.display = 'block';
|
|
await refresh();
|
|
}
|
|
|
|
async function refresh() {
|
|
await Promise.all([fetchBalances(), fetchTransactions()]);
|
|
}
|
|
|
|
document.getElementById('refreshButton').addEventListener('click', refresh);
|
|
document.getElementById('generateButton').addEventListener('click', generateAddress);
|
|
document.getElementById('sendButton').addEventListener('click', sendCoins);
|
|
|
|
(async () => {
|
|
await fetchSession();
|
|
await refresh();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|