auto broadcast pending txns, show net info
This commit is contained in:
@@ -140,13 +140,26 @@ def get_network_info():
|
|||||||
mempool = rpc_call("getrawmempool")
|
mempool = rpc_call("getrawmempool")
|
||||||
blockchain_info = rpc_call("getblockchaininfo")
|
blockchain_info = rpc_call("getblockchaininfo")
|
||||||
|
|
||||||
|
# Format peer information
|
||||||
|
peers = []
|
||||||
|
for peer in peer_info:
|
||||||
|
peers.append({
|
||||||
|
"addr": peer.get("addr", ""),
|
||||||
|
"services": peer.get("servicesnames", []),
|
||||||
|
"relaytxes": peer.get("relaytxes", False),
|
||||||
|
"synced_blocks": peer.get("synced_blocks", 0),
|
||||||
|
"last_transaction": peer.get("last_transaction", 0),
|
||||||
|
"version": peer.get("subver", ""),
|
||||||
|
"pingtime": round(peer.get("pingtime", 0) * 1000, 1),
|
||||||
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"network": {
|
"network": {
|
||||||
"connections": network_info.get("connections", 0),
|
"connections": network_info.get("connections", 0),
|
||||||
"networkactive": network_info.get("networkactive", False),
|
"networkactive": network_info.get("networkactive", False),
|
||||||
"relayfee": network_info.get("relayfee", 0),
|
"relayfee": network_info.get("relayfee", 0),
|
||||||
},
|
},
|
||||||
"peers": len(peer_info),
|
"peers": peers,
|
||||||
"mempool_size": len(mempool),
|
"mempool_size": len(mempool),
|
||||||
"blockchain": {
|
"blockchain": {
|
||||||
"blocks": blockchain_info.get("blocks", 0),
|
"blocks": blockchain_info.get("blocks", 0),
|
||||||
@@ -159,6 +172,34 @@ def get_network_info():
|
|||||||
return jsonify({"error": str(exc)}), 500
|
return jsonify({"error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/rebroadcast", methods=["POST"])
|
||||||
|
@require_token
|
||||||
|
def rebroadcast_pending():
|
||||||
|
try:
|
||||||
|
# Get all pending transactions
|
||||||
|
txs = rpc_call("listtransactions", "*", 100, 0, True)
|
||||||
|
pending = [tx for tx in txs if tx.get("confirmations", 0) == 0]
|
||||||
|
|
||||||
|
rebroadcasted = []
|
||||||
|
for tx in pending:
|
||||||
|
txid = tx.get("txid")
|
||||||
|
if txid:
|
||||||
|
try:
|
||||||
|
# Get raw transaction
|
||||||
|
tx_data = rpc_call("gettransaction", txid, True)
|
||||||
|
raw_hex = tx_data.get("hex")
|
||||||
|
if raw_hex:
|
||||||
|
# Rebroadcast
|
||||||
|
rpc_call("sendrawtransaction", raw_hex)
|
||||||
|
rebroadcasted.append(txid)
|
||||||
|
except Exception:
|
||||||
|
pass # Already in mempool or other error
|
||||||
|
|
||||||
|
return jsonify({"rebroadcasted": rebroadcasted, "count": len(rebroadcasted)})
|
||||||
|
except RuntimeError as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def root():
|
def root():
|
||||||
return send_from_directory(app.static_folder, "index.html")
|
return send_from_directory(app.static_folder, "index.html")
|
||||||
|
|||||||
@@ -280,15 +280,39 @@
|
|||||||
|
|
||||||
function renderTx(tx) {
|
function renderTx(tx) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
const amount = Number(tx.amount).toFixed(8);
|
const amount = Math.abs(Number(tx.amount)).toFixed(8);
|
||||||
const type = tx.category === 'send' ? 'Sent' : 'Received';
|
const type = tx.category === 'send' ? 'Sent' : 'Received';
|
||||||
const confirmations = tx.confirmations || 0;
|
const confirmations = tx.confirmations || 0;
|
||||||
|
|
||||||
let status, statusClass, tooltip = '';
|
let status, statusClass, tooltip = '';
|
||||||
|
let ageInfo = '';
|
||||||
|
|
||||||
if (confirmations === 0) {
|
if (confirmations === 0) {
|
||||||
status = 'Pending';
|
status = 'Pending';
|
||||||
statusClass = 'status-pending';
|
statusClass = 'status-pending';
|
||||||
tooltip = 'title="0 confirmations - waiting for network confirmation"';
|
|
||||||
|
// Calculate transaction age
|
||||||
|
const txTime = tx.time || tx.timereceived;
|
||||||
|
if (txTime) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const ageSeconds = now - txTime;
|
||||||
|
const ageMinutes = Math.floor(ageSeconds / 60);
|
||||||
|
const ageHours = Math.floor(ageMinutes / 60);
|
||||||
|
|
||||||
|
let ageText = '';
|
||||||
|
if (ageHours > 0) {
|
||||||
|
ageText = `${ageHours}h ${ageMinutes % 60}m ago`;
|
||||||
|
} else if (ageMinutes > 0) {
|
||||||
|
ageText = `${ageMinutes}m ago`;
|
||||||
|
} else {
|
||||||
|
ageText = `${ageSeconds}s ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ageInfo = `<div class="muted" style="font-size: 11px;">Age: ${ageText}</div>`;
|
||||||
|
tooltip = `title="0 confirmations - waiting for network confirmation (${ageText})"`;
|
||||||
|
} else {
|
||||||
|
tooltip = 'title="0 confirmations - waiting for network confirmation"';
|
||||||
|
}
|
||||||
} else if (confirmations < 20) {
|
} else if (confirmations < 20) {
|
||||||
status = `Immature (${confirmations}/20)`;
|
status = `Immature (${confirmations}/20)`;
|
||||||
statusClass = 'status-immature';
|
statusClass = 'status-immature';
|
||||||
@@ -302,6 +326,7 @@
|
|||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div><strong>${type}</strong> ${amount} RIN <span class="status-badge ${statusClass}" ${tooltip}>${status}</span></div>
|
<div><strong>${type}</strong> ${amount} RIN <span class="status-badge ${statusClass}" ${tooltip}>${status}</span></div>
|
||||||
<div class="muted">${tx.time ? new Date(tx.time * 1000).toLocaleString() : ''}</div>
|
<div class="muted">${tx.time ? new Date(tx.time * 1000).toLocaleString() : ''}</div>
|
||||||
|
${ageInfo}
|
||||||
<div class="muted"><a href="https://explorer.rin.so/tx/${tx.txid}" target="_blank" class="tx-link">${tx.txid}</a></div>
|
<div class="muted"><a href="https://explorer.rin.so/tx/${tx.txid}" target="_blank" class="tx-link">${tx.txid}</a></div>
|
||||||
`;
|
`;
|
||||||
return li;
|
return li;
|
||||||
@@ -403,55 +428,94 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNetworkInfo() {
|
async function fetchNetworkInfo() {
|
||||||
let res = await fetch('/api/network', { headers: authHeaders() });
|
try {
|
||||||
let data = await res.json();
|
let res = await fetch('/api/network', { headers: authHeaders() });
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
// If token is invalid, refresh and retry
|
// If token is invalid, refresh and retry
|
||||||
if (data.error === 'invalid_token') {
|
if (data.error === 'invalid_token') {
|
||||||
await refreshToken();
|
await refreshToken();
|
||||||
res = await fetch('/api/network', { headers: authHeaders() });
|
res = await fetch('/api/network', { headers: authHeaders() });
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
networkInfo.innerHTML = `<div class="muted">Error: ${data.error}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const network = data.network;
|
||||||
|
const blockchain = data.blockchain;
|
||||||
|
|
||||||
|
// Build peers list
|
||||||
|
let peersHtml = '';
|
||||||
|
if (data.peers && data.peers.length > 0) {
|
||||||
|
peersHtml = '<div style="margin-top: 12px;"><div class="muted" style="font-size: 12px; margin-bottom: 8px;">Connected Peers</div><div style="max-height: 200px; overflow-y: auto;">';
|
||||||
|
data.peers.forEach(peer => {
|
||||||
|
const relayStatus = peer.relaytxes ? '✓ Relay' : '✗ No Relay';
|
||||||
|
const syncStatus = peer.synced_blocks === blockchain.blocks ? '✓ Synced' : `${peer.synced_blocks}`;
|
||||||
|
peersHtml += `
|
||||||
|
<div style="padding: 6px; margin-bottom: 4px; background: rgba(9, 19, 30, 0.6); border-radius: 4px; font-size: 11px;">
|
||||||
|
<div style="font-weight: 600;">${peer.addr}</div>
|
||||||
|
<div class="muted">${relayStatus} | ${peer.version} | Ping: ${peer.pingtime}ms | Blocks: ${syncStatus}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
peersHtml += '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
networkInfo.innerHTML = `
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
||||||
|
<div>
|
||||||
|
<div class="muted">Network Connections</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${network.connections}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="muted">Mempool Size</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${data.mempool_size}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="muted">Block Height</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${blockchain.blocks}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="muted">Chain</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${blockchain.chain}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${peersHtml}
|
||||||
|
<div class="muted" style="font-size: 12px; margin-top: 12px;">
|
||||||
|
Network Active: ${network.networkactive ? 'Yes' : 'No'} |
|
||||||
|
Relay Fee: ${network.relayfee} RIN |
|
||||||
|
Difficulty: ${Number(blockchain.difficulty).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Network info fetch failed:', err);
|
||||||
|
networkInfo.innerHTML = `<div class="muted">Error loading network info: ${err.message}</div>`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.error) {
|
async function rebroadcastPending() {
|
||||||
networkInfo.innerHTML = `<div class="muted">Error: ${data.error}</div>`;
|
try {
|
||||||
return;
|
const res = await fetch('/api/rebroadcast', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeaders(),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
console.log(`Rebroadcasted ${data.count} pending transactions`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Rebroadcast failed:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const network = data.network;
|
|
||||||
const blockchain = data.blockchain;
|
|
||||||
|
|
||||||
networkInfo.innerHTML = `
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
|
||||||
<div>
|
|
||||||
<div class="muted">Network Connections</div>
|
|
||||||
<div style="font-size: 18px; font-weight: 600;">${network.connections}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="muted">Mempool Size</div>
|
|
||||||
<div style="font-size: 18px; font-weight: 600;">${data.mempool_size}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="muted">Block Height</div>
|
|
||||||
<div style="font-size: 18px; font-weight: 600;">${blockchain.blocks}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="muted">Chain</div>
|
|
||||||
<div style="font-size: 18px; font-weight: 600;">${blockchain.chain}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="muted" style="font-size: 12px;">
|
|
||||||
Network Active: ${network.networkactive ? 'Yes' : 'No'} |
|
|
||||||
Relay Fee: ${network.relayfee} RIN |
|
|
||||||
Difficulty: ${blockchain.difficulty.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
await Promise.all([fetchBalances(), fetchTransactions(), fetchNetworkInfo()]);
|
await Promise.all([fetchBalances(), fetchTransactions(), fetchNetworkInfo()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-rebroadcast pending transactions every minute
|
||||||
|
setInterval(rebroadcastPending, 60000);
|
||||||
|
|
||||||
document.getElementById('refreshButton').addEventListener('click', refresh);
|
document.getElementById('refreshButton').addEventListener('click', refresh);
|
||||||
document.getElementById('generateButton').addEventListener('click', generateAddress);
|
document.getElementById('generateButton').addEventListener('click', generateAddress);
|
||||||
document.getElementById('sendButton').addEventListener('click', sendCoins);
|
document.getElementById('sendButton').addEventListener('click', sendCoins);
|
||||||
|
|||||||
Reference in New Issue
Block a user