diff --git a/rin/wallet/web/server.py b/rin/wallet/web/server.py
index 6ca5e8c..1e64a8d 100644
--- a/rin/wallet/web/server.py
+++ b/rin/wallet/web/server.py
@@ -140,13 +140,26 @@ def get_network_info():
mempool = rpc_call("getrawmempool")
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({
"network": {
"connections": network_info.get("connections", 0),
"networkactive": network_info.get("networkactive", False),
"relayfee": network_info.get("relayfee", 0),
},
- "peers": len(peer_info),
+ "peers": peers,
"mempool_size": len(mempool),
"blockchain": {
"blocks": blockchain_info.get("blocks", 0),
@@ -159,6 +172,34 @@ def get_network_info():
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("/")
def root():
return send_from_directory(app.static_folder, "index.html")
diff --git a/rin/wallet/web/static/index.html b/rin/wallet/web/static/index.html
index 560a95a..ef27804 100644
--- a/rin/wallet/web/static/index.html
+++ b/rin/wallet/web/static/index.html
@@ -280,15 +280,39 @@
function renderTx(tx) {
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 confirmations = tx.confirmations || 0;
let status, statusClass, tooltip = '';
+ let ageInfo = '';
+
if (confirmations === 0) {
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 = `
Age: ${ageText}
`;
+ tooltip = `title="0 confirmations - waiting for network confirmation (${ageText})"`;
+ } else {
+ tooltip = 'title="0 confirmations - waiting for network confirmation"';
+ }
} else if (confirmations < 20) {
status = `Immature (${confirmations}/20)`;
statusClass = 'status-immature';
@@ -302,6 +326,7 @@
li.innerHTML = `
${type} ${amount} RIN ${status}
${tx.time ? new Date(tx.time * 1000).toLocaleString() : ''}
+ ${ageInfo}
`;
return li;
@@ -403,55 +428,94 @@
}
async function fetchNetworkInfo() {
- let res = await fetch('/api/network', { headers: authHeaders() });
- let data = await res.json();
+ try {
+ let res = await fetch('/api/network', { headers: authHeaders() });
+ let data = await res.json();
- // If token is invalid, refresh and retry
- if (data.error === 'invalid_token') {
- await refreshToken();
- res = await fetch('/api/network', { headers: authHeaders() });
- data = await res.json();
+ // If token is invalid, refresh and retry
+ if (data.error === 'invalid_token') {
+ await refreshToken();
+ res = await fetch('/api/network', { headers: authHeaders() });
+ data = await res.json();
+ }
+
+ if (data.error) {
+ networkInfo.innerHTML = `Error: ${data.error}
`;
+ return;
+ }
+
+ const network = data.network;
+ const blockchain = data.blockchain;
+
+ // Build peers list
+ let peersHtml = '';
+ if (data.peers && data.peers.length > 0) {
+ peersHtml = 'Connected Peers
';
+ data.peers.forEach(peer => {
+ const relayStatus = peer.relaytxes ? '✓ Relay' : '✗ No Relay';
+ const syncStatus = peer.synced_blocks === blockchain.blocks ? '✓ Synced' : `${peer.synced_blocks}`;
+ peersHtml += `
+
+
${peer.addr}
+
${relayStatus} | ${peer.version} | Ping: ${peer.pingtime}ms | Blocks: ${syncStatus}
+
+ `;
+ });
+ peersHtml += '
';
+ }
+
+ networkInfo.innerHTML = `
+
+
+
Network Connections
+
${network.connections}
+
+
+
Mempool Size
+
${data.mempool_size}
+
+
+
Block Height
+
${blockchain.blocks}
+
+
+
Chain
+
${blockchain.chain}
+
+
+ ${peersHtml}
+
+ Network Active: ${network.networkactive ? 'Yes' : 'No'} |
+ Relay Fee: ${network.relayfee} RIN |
+ Difficulty: ${Number(blockchain.difficulty).toFixed(2)}
+
+ `;
+ } catch (err) {
+ console.error('Network info fetch failed:', err);
+ networkInfo.innerHTML = `Error loading network info: ${err.message}
`;
}
+ }
- if (data.error) {
- networkInfo.innerHTML = `Error: ${data.error}
`;
- return;
+ async function rebroadcastPending() {
+ try {
+ 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 = `
-
-
-
Network Connections
-
${network.connections}
-
-
-
Mempool Size
-
${data.mempool_size}
-
-
-
Block Height
-
${blockchain.blocks}
-
-
-
Chain
-
${blockchain.chain}
-
-
-
- Network Active: ${network.networkactive ? 'Yes' : 'No'} |
- Relay Fee: ${network.relayfee} RIN |
- Difficulty: ${blockchain.difficulty.toFixed(2)}
-
- `;
}
async function refresh() {
await Promise.all([fetchBalances(), fetchTransactions(), fetchNetworkInfo()]);
}
+ // Auto-rebroadcast pending transactions every minute
+ setInterval(rebroadcastPending, 60000);
+
document.getElementById('refreshButton').addEventListener('click', refresh);
document.getElementById('generateButton').addEventListener('click', generateAddress);
document.getElementById('sendButton').addEventListener('click', sendCoins);