Merge branch 'feature-wallet' into node-stratum-proxy

This commit is contained in:
Dobromir Popov
2025-09-30 12:10:09 +03:00
45 changed files with 6122 additions and 1 deletions

View File

@@ -32,3 +32,18 @@ https://zergpool.com/setup
or in RIN
rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q
0.00000296 BTC
<pre>/mnt/shared/DEV/repos/d-popov.com/mines/rin/miner/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://rinhash.eu.mine.zergpool.com:7148 -u bc1qjn4m6rmrveuxhk02a5qhe4r6kdcsvvt3vhdn9j -p c=BTC,mc=RIN,m=solo -t 32
</pre>
<pre>/mnt/shared/DEV/repos/d-popov.com/mines/rin/miner/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://1localhost:3333 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q -p m=solo -t 32
</pre>
<pre>/mnt/shared/DEV/repos/d-popov.com/mines/rin/miner/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://stratum.gettomine.com:3498 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q -p m=solo </pre>
stratum+tcp://mine.evepool.pw:7148
stratum+ssl://mine.evepool.pw:17148
/mnt/shared/DEV/repos/d-popov.com/mines/rin/miner/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+ssl://mine.evepool.pw:17148 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q -p m=solo
/mnt/shared/DEV/repos/d-popov.com/mines/rin/miner/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+ssl://mine.evepool.pw:17148 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q

25
rin/node/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
build-essential libtool autotools-dev automake pkg-config bsdmainutils \
libevent-dev libboost-all-dev libssl-dev \
libdb5.3-dev libdb5.3++-dev libfmt-dev libsqlite3-dev \
git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt
RUN git clone https://github.com/Rin-coin/rincoin.git && \
cd rincoin && \
./autogen.sh && \
./configure --with-incompatible-bdb && \
make -j$(nproc) && \
make install
# runtime
RUN useradd -m rin && mkdir -p /data && chown -R rin:rin /data
USER rin
VOLUME ["/data"]
EXPOSE 9555 9556
ENTRYPOINT ["/usr/local/bin/rincoind"]
CMD ["-datadir=/data", "-conf=/data/rincoin.conf", "-printtoconsole"]

18
rin/node/container.yml Normal file
View File

@@ -0,0 +1,18 @@
version: "3.8"
services:
rincoin-node:
container_name: rincoin-node
image: rincoin-node:latest
restart: unless-stopped
ports:
- "9555:9555"
- "9556:9556"
volumes:
- /mnt/data/docker_vol/rincoin/rincoin-node/data:/data
- /mnt/data/docker_vol/rincoin/rincoin-node/rincoin.conf:/data/rincoin.conf:ro
command:
- rincoind
- -datadir=/data
- -conf=/data/rincoin.conf
- -printtoconsole

13
rin/node/rincoin.conf Normal file
View File

@@ -0,0 +1,13 @@
server=1
daemon=0
listen=1
txindex=1
rpcuser=rinrpc
rpcpassword=745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90
rpcallowip=0.0.0.0/0
rpcport=9556
# performance
maxconnections=64
dbcache=2048

View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Kill RinCoin Stratum Proxy processes
echo "=== Killing RinCoin Stratum Proxy ==="
echo ""
# Find and kill Python processes running stratum_proxy.py
PIDS=$(ps aux | grep "stratum_proxy.py" | grep -v grep | awk '{print $2}')
if [ -n "$PIDS" ]; then
echo "Found Stratum Proxy processes: $PIDS"
echo "Killing processes..."
for pid in $PIDS; do
kill -9 "$pid" 2>/dev/null && echo "Killed PID: $pid" || echo "Failed to kill PID: $pid"
done
else
echo "No Stratum Proxy processes found"
fi
# Also kill any process using port 3333
echo ""
echo "Checking port 3333..."
PORT_PIDS=$(sudo lsof -ti:3333 2>/dev/null)
if [ -n "$PORT_PIDS" ]; then
echo "Found processes using port 3333: $PORT_PIDS"
echo "Killing processes..."
for pid in $PORT_PIDS; do
sudo kill -9 "$pid" 2>/dev/null && echo "Killed PID: $pid" || echo "Failed to kill PID: $pid"
done
else
echo "No processes using port 3333"
fi
echo ""
echo "✅ Cleanup complete!"
echo ""
echo "Port 3333 status:"
netstat -tln | grep ":3333 " || echo "Port 3333 is free"

View File

@@ -0,0 +1,529 @@
#!/usr/bin/env python3
"""
RinCoin Mining Pool Web Interface
Provides web dashboard for pool statistics and miner management
"""
import json
import sqlite3
import requests
from datetime import datetime, timedelta
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
import time
from requests.auth import HTTPBasicAuth
class PoolWebInterface:
def __init__(self, pool_db, host='0.0.0.0', port=8080, rpc_host='127.0.0.1', rpc_port=9556,
rpc_user='rinrpc', rpc_password='745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90'):
self.pool_db = pool_db
self.host = host
self.port = port
self.rpc_host = rpc_host
self.rpc_port = rpc_port
self.rpc_user = rpc_user
self.rpc_password = rpc_password
self.chart_time_window = 3600 # 1 hour default, adjustable
def set_chart_time_window(self, seconds):
"""Set the chart time window"""
self.chart_time_window = seconds
def format_hashrate(self, hashrate):
"""Format hashrate in human readable format"""
if hashrate >= 1e12:
return f"{hashrate/1e12:.2f} TH/s"
elif hashrate >= 1e9:
return f"{hashrate/1e9:.2f} GH/s"
elif hashrate >= 1e6:
return f"{hashrate/1e6:.2f} MH/s"
elif hashrate >= 1e3:
return f"{hashrate/1e3:.2f} KH/s"
elif hashrate >= 0.01:
return f"{hashrate:.2f} H/s"
elif hashrate > 0:
return f"{hashrate*1000:.2f} mH/s"
else:
return "0.00 H/s"
def get_pool_balance(self):
"""Get pool wallet balance via RPC"""
try:
url = f"http://{self.rpc_host}:{self.rpc_port}/"
headers = {'content-type': 'text/plain'}
auth = HTTPBasicAuth(self.rpc_user, self.rpc_password)
payload = {
"jsonrpc": "1.0",
"id": "pool_balance",
"method": "getbalance",
"params": []
}
response = requests.post(url, json=payload, headers=headers, auth=auth, timeout=10)
if response.status_code == 200:
result = response.json()
if 'error' in result and result['error'] is not None:
print(f"RPC Error getting balance: {result['error']}")
return 0.0
balance = result.get('result', 0)
return float(balance) / 100000000 # Convert from satoshis to RIN
else:
print(f"HTTP Error getting balance: {response.status_code}")
return 0.0
except Exception as e:
print(f"Error getting pool balance: {e}")
return 0.0
def get_pool_stats(self):
"""Get current pool statistics"""
try:
cursor = self.pool_db.cursor()
# Total miners (ever registered)
cursor.execute('SELECT COUNT(DISTINCT id) FROM miners')
total_miners = cursor.fetchone()[0]
# Active miners (last 5 minutes)
cursor.execute('''
SELECT COUNT(DISTINCT m.id) FROM miners m
JOIN shares s ON m.id = s.miner_id
WHERE s.submitted > datetime('now', '-5 minutes')
''')
active_miners = cursor.fetchone()[0]
# Total shares (last 24 hours)
cursor.execute('''
SELECT COUNT(*) FROM shares
WHERE submitted > datetime('now', '-24 hours')
''')
total_shares_24h = cursor.fetchone()[0]
# Pool hashrate: sum of miners.last_hashrate (instantaneous)
cursor.execute('SELECT COALESCE(SUM(last_hashrate), 0) FROM miners')
hashrate = cursor.fetchone()[0] or 0.0
# Debug stats
cursor.execute('''
SELECT SUM(difficulty), COUNT(*) FROM shares
WHERE submitted > datetime('now', '-5 minutes')
''')
rd = cursor.fetchone()
recent_difficulty = rd[0] if rd and rd[0] else 0
recent_share_count = rd[1] if rd and rd[1] else 0
# Get historical hashrate data for chart
cursor.execute('''
SELECT
strftime('%H:%M', submitted) as time,
COUNT(*) as shares,
SUM(difficulty) as total_difficulty
FROM shares
WHERE submitted > datetime('now', '-{} seconds')
GROUP BY strftime('%Y-%m-%d %H:%M', submitted)
ORDER BY submitted DESC
LIMIT 60
'''.format(self.chart_time_window))
historical_data = cursor.fetchall()
# Calculate individual miner hashrates
cursor.execute('''
SELECT
m.user, m.worker,
COUNT(s.id) as shares,
SUM(s.difficulty) as total_difficulty,
m.last_share
FROM miners m
LEFT JOIN shares s ON m.id = s.miner_id
AND s.submitted > datetime('now', '-5 minutes')
GROUP BY m.id, m.user, m.worker
ORDER BY shares DESC
''')
miner_stats = cursor.fetchall()
# Calculate individual hashrates (use miners.last_hashrate)
miner_hashrates = []
for user, worker, shares, difficulty, last_share in miner_stats:
cursor.execute('SELECT last_hashrate FROM miners WHERE user = ? AND worker = ? LIMIT 1', (user, worker))
row = cursor.fetchone()
miner_hashrate = row[0] if row and row[0] else 0.0
miner_hashrates.append((user, worker, shares, miner_hashrate, last_share))
# Total blocks found
cursor.execute('SELECT COUNT(*) FROM blocks')
total_blocks = cursor.fetchone()[0]
# Recent blocks
cursor.execute('''
SELECT block_hash, height, reward, found_at
FROM blocks
ORDER BY found_at DESC
LIMIT 10
''')
recent_blocks = cursor.fetchall()
# Top miners (last 24 hours) - show all miners, even without shares
cursor.execute('''
SELECT m.user, m.worker,
COALESCE(COUNT(s.id), 0) as shares,
m.last_share,
m.created
FROM miners m
LEFT JOIN shares s ON m.id = s.miner_id
AND s.submitted > datetime('now', '-24 hours')
GROUP BY m.id, m.user, m.worker
ORDER BY shares DESC, m.created DESC
LIMIT 20
''')
top_miners = cursor.fetchall()
# All active miners (for better visibility)
cursor.execute('''
SELECT user, worker, created, last_share
FROM miners
ORDER BY created DESC
LIMIT 10
''')
all_miners = cursor.fetchall()
# Get pool balance
pool_balance = self.get_pool_balance()
return {
'total_miners': total_miners,
'active_miners': active_miners,
'total_shares_24h': total_shares_24h,
'hashrate': hashrate,
'total_blocks': total_blocks,
'recent_blocks': recent_blocks,
'top_miners': top_miners,
'all_miners': all_miners,
'miner_hashrates': miner_hashrates,
'historical_data': historical_data,
'pool_balance': pool_balance,
'debug': {
'recent_difficulty': recent_difficulty,
'recent_share_count': recent_share_count,
'total_shares_24h': total_shares_24h
}
}
except Exception as e:
print(f"Error getting pool stats: {e}")
return {}
def generate_html(self, stats):
"""Generate HTML dashboard"""
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>RinCoin Mining Pool</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
.container {{ max-width: 1200px; margin: 0 auto; }}
.header {{ background: #2c3e50; color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
.stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }}
.stat-card {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
.stat-value {{ font-size: 2em; font-weight: bold; color: #3498db; }}
.stat-label {{ color: #7f8c8d; margin-top: 5px; }}
.section {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }}
.section h2 {{ margin-top: 0; color: #2c3e50; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
th {{ background: #f8f9fa; font-weight: bold; }}
.block-hash {{ font-family: monospace; font-size: 0.9em; }}
.refresh-btn {{ background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }}
.refresh-btn:hover {{ background: #2980b9; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏊‍♂️ RinCoin Mining Pool</h1>
<p>Distribute block rewards among multiple miners</p>
</div>
<button class="refresh-btn" onclick="location.reload()">🔄 Refresh</button>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{stats.get('total_miners', 0)}</div>
<div class="stat-label">Total Miners</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats.get('active_miners', 0)}</div>
<div class="stat-label">Active Miners</div>
</div>
<div class="stat-card">
<div class="stat-value">{self.format_hashrate(stats.get('hashrate', 0))}</div>
<div class="stat-label">Hashrate</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats.get('total_blocks', 0)}</div>
<div class="stat-label">Blocks Found</div>
</div>
<div class="stat-card">
<div class="stat-value">{stats.get('pool_balance', 0):.2f}</div>
<div class="stat-label">Pool Balance (RIN)</div>
</div>
</div>
<div class="section">
<h2>📊 Pool Statistics</h2>
<p><strong>24h Shares:</strong> {stats.get('total_shares_24h', 0):,}</p>
<p><strong>Pool Fee:</strong> 1%</p>
<p><strong>Pool Balance:</strong> {stats.get('pool_balance', 0):.8f} RIN</p>
<p><strong>Connection String:</strong> <code>stratum+tcp://YOUR_IP:3333</code></p>
<!-- Debug info -->
<details style="margin-top: 20px; padding: 10px; background: #f8f9fa; border-radius: 4px;">
<summary style="cursor: pointer; font-weight: bold;">🔍 Debug Info</summary>
<p><strong>Recent Difficulty (5min):</strong> {stats.get('debug', {}).get('recent_difficulty', 0):.6f}</p>
<p><strong>Recent Share Count (5min):</strong> {stats.get('debug', {}).get('recent_share_count', 0)}</p>
<p><strong>Total Shares (24h):</strong> {stats.get('debug', {}).get('total_shares_24h', 0):,}</p>
<p><strong>Active Miners:</strong> {stats.get('active_miners', 0)} (last 5 minutes)</p>
<p><strong>Total Miners:</strong> {stats.get('total_miners', 0)} (ever registered)</p>
</details>
</div>
<div class="section">
<h2>📈 Hashrate Chart</h2>
<div class="chart-controls">
<label>Time Window: </label>
<select onchange="changeTimeWindow(this.value)">
<option value="3600">1 Hour</option>
<option value="7200">2 Hours</option>
<option value="14400">4 Hours</option>
<option value="86400">24 Hours</option>
</select>
</div>
<div class="chart-container">
<canvas id="hashrateChart"></canvas>
</div>
</div>
<div class="section">
<h2>👥 Connected Miners</h2>
<table>
<tr>
<th>User</th>
<th>Worker</th>
<th>Connected</th>
<th>Last Share</th>
</tr>
"""
for miner in stats.get('all_miners', []):
user, worker, created, last_share = miner
html += f"""
<tr>
<td>{user}</td>
<td>{worker}</td>
<td>{created}</td>
<td>{last_share or 'Never'}</td>
</tr>
"""
if not stats.get('all_miners', []):
html += """
<tr>
<td colspan="4" style="text-align: center; color: #7f8c8d;">No miners connected</td>
</tr>
"""
html += """
</table>
</div>
<div class="section">
<h2>🏆 Top Miners (24h Shares)</h2>
<table>
<tr>
<th>User</th>
<th>Worker</th>
<th>Shares</th>
<th>Hashrate</th>
<th>Last Share</th>
</tr>
"""
for miner in stats.get('miner_hashrates', []):
user, worker, shares, hashrate, last_share = miner
html += f"""
<tr>
<td>{user}</td>
<td>{worker}</td>
<td>{shares:,}</td>
<td>{self.format_hashrate(hashrate)}</td>
<td>{last_share or 'Never'}</td>
</tr>
"""
if not stats.get('top_miners', []):
html += """
<tr>
<td colspan="4" style="text-align: center; color: #7f8c8d;">No shares submitted yet</td>
</tr>
"""
html += """
</table>
</div>
<div class="section">
<h2>🏆 Recent Blocks</h2>
<table>
<tr>
<th>Height</th>
<th>Hash</th>
<th>Reward</th>
<th>Found At</th>
</tr>
"""
for block in stats.get('recent_blocks', []):
block_hash, height, reward, found_at = block
html += f"""
<tr>
<td>{height}</td>
<td class="block-hash">{block_hash[:16]}...</td>
<td>{reward:.8f} RIN</td>
<td>{found_at}</td>
</tr>
"""
html += """
</table>
</div>
<div class="section">
<h2>🔗 Connect to Pool</h2>
<p>Use any RinHash-compatible miner:</p>
<pre><code>./cpuminer -a rinhash -o stratum+tcp://YOUR_IP:3333 -u username.workername -p x</code></pre>
<p><strong>Replace YOUR_IP with your server's public IP address</strong></p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Historical data for chart
const historicalData = {json.dumps([{
'time': row[0],
'shares': row[1],
'difficulty': row[2] or 0
} for row in stats.get('historical_data', [])])};
// Create hashrate chart
const ctx = document.getElementById('hashrateChart').getContext('2d');
const chart = new Chart(ctx, {{
type: 'line',
data: {{
labels: historicalData.map(d => d.time).reverse(),
datasets: [{{
label: 'Hashrate (H/s)',
data: historicalData.map(d => (d.shares / 60.0) * 0.001).reverse(),
borderColor: '#3498db',
backgroundColor: 'rgba(52, 152, 219, 0.1)',
tension: 0.4
}}]
}},
options: {{
responsive: true,
maintainAspectRatio: false,
scales: {{
y: {{
beginAtZero: true,
title: {{
display: true,
text: 'Hashrate (H/s)'
}}
}},
x: {{
title: {{
display: true,
text: 'Time'
}}
}}
}}
}}
}});
function changeTimeWindow(seconds) {{
// Reload page with new time window
const url = new URL(window.location);
url.searchParams.set('window', seconds);
window.location.href = url.toString();
}}
</script>
<script>
// Auto-refresh every 30 seconds
setTimeout(() => location.reload(), 30000);
</script>
</body>
</html>
"""
return html
class PoolWebHandler(BaseHTTPRequestHandler):
def __init__(self, *args, pool_interface=None, **kwargs):
self.pool_interface = pool_interface
super().__init__(*args, **kwargs)
def do_GET(self):
if self.path == '/':
stats = self.pool_interface.get_pool_stats()
html = self.pool_interface.generate_html(stats)
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode('utf-8'))
elif self.path == '/api/stats':
stats = self.pool_interface.get_pool_stats()
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(stats).encode('utf-8'))
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b'Not Found')
def log_message(self, format, *args):
# Suppress access logs
pass
def start_web_interface(pool_db, host='0.0.0.0', port=8083, rpc_host='127.0.0.1', rpc_port=9556,
rpc_user='rinrpc', rpc_password='745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90'):
"""Start the web interface server"""
interface = PoolWebInterface(pool_db, host, port, rpc_host, rpc_port, rpc_user, rpc_password)
class Handler(PoolWebHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, pool_interface=interface, **kwargs)
try:
server = HTTPServer((host, port), Handler)
print(f"🌐 Web interface running on http://{host}:{port}")
print("Press Ctrl+C to stop")
server.serve_forever()
except OSError as e:
if "Address already in use" in str(e):
print(f"⚠️ Port {port} is already in use, web interface not started")
print(f"💡 Try a different port or kill the process using port {port}")
else:
print(f"❌ Failed to start web interface: {e}")
except KeyboardInterrupt:
print("\n🛑 Shutting down web interface...")
server.shutdown()
if __name__ == "__main__":
# This would be called from the main pool server
print("Web interface module loaded")

View File

@@ -0,0 +1,47 @@
#!/bin/bash
# Solo Mining Script for RinCoin
# Uses local RinCoin node for solo mining
echo "=== RinCoin Solo Mining Setup ==="
echo ""
# Check if rincoin-node container is running
if ! sudo docker ps | grep -q "rincoin-node"; then
echo "Error: rincoin-node container is not running!"
echo "Please start it first:"
echo "sudo docker start rincoin-node"
exit 1
fi
# Get wallet address
RIN_ADDRESS=$(sudo docker exec rincoin-node rincoin-cli -datadir=/data -conf=/data/rincoin.conf -rpcwallet=main getnewaddress 2>/dev/null)
if [ -z "$RIN_ADDRESS" ]; then
echo "Error: Could not get RinCoin address!"
echo "Make sure the wallet is created and the node is synced."
exit 1
fi
echo "RinCoin Address: $RIN_ADDRESS"
echo ""
# Check node sync status
SYNC_STATUS=$(sudo docker exec rincoin-node rincoin-cli -datadir=/data -conf=/data/rincoin.conf getblockchaininfo | grep -o '"initialblockdownload": [^,]*' | cut -d' ' -f2)
if [ "$SYNC_STATUS" = "true" ]; then
echo "⚠️ WARNING: Node is still syncing (initialblockdownload: true)"
echo "Solo mining may not work properly until sync is complete."
echo ""
fi
echo "Starting solo mining with cpuminer-opt-rin..."
echo "Algorithm: rinhash"
echo "Target: Local RinCoin node (127.0.0.1:9555)"
echo "Wallet: $RIN_ADDRESS"
echo ""
echo "Press Ctrl+C to stop mining"
echo ""
# Start solo mining
sudo docker exec -it amd-strix-halo-llama-rocm bash -c "/mnt/dl/rinhash/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://127.0.0.1:9555 -u $RIN_ADDRESS -p x -t 32"

View File

@@ -0,0 +1,171 @@
#!/bin/bash
# RinCoin Solo Mining using Built-in Core Mining
# Uses RinCoin Core's generatetoaddress command
# Default address (can be overridden with command line parameter)
DEFAULT_ADDRESS="rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q"
# Get total CPU cores for default thread count
TOTAL_CORES=$(nproc)
DEFAULT_THREADS=$TOTAL_CORES
# Parse command line arguments
RIN_ADDRESS=""
THREAD_COUNT=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-a|--address)
RIN_ADDRESS="$2"
shift 2
;;
-t|--threads)
THREAD_COUNT="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -a, --address ADDRESS RinCoin address to mine to (default: $DEFAULT_ADDRESS)"
echo " -t, --threads COUNT Number of threads to use (default: $DEFAULT_THREADS)"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Use defaults (all cores, default address)"
echo " $0 -a rin1q... -t 16 # Custom address and 16 threads"
echo " $0 --address rin1q... --threads 8 # Custom address and 8 threads"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
esac
done
# Set defaults if not provided
if [ -z "$RIN_ADDRESS" ]; then
RIN_ADDRESS="$DEFAULT_ADDRESS"
echo "No address provided, using default: $RIN_ADDRESS"
fi
if [ -z "$THREAD_COUNT" ]; then
THREAD_COUNT="$DEFAULT_THREADS"
echo "No thread count provided, using all cores: $THREAD_COUNT"
fi
# Validate thread count
if ! [[ "$THREAD_COUNT" =~ ^[0-9]+$ ]] || [ "$THREAD_COUNT" -lt 1 ] || [ "$THREAD_COUNT" -gt "$TOTAL_CORES" ]; then
echo "❌ Error: Invalid thread count: $THREAD_COUNT"
echo "Thread count must be between 1 and $TOTAL_CORES"
exit 1
fi
echo "=== RinCoin Solo Mining (Built-in Core Mining) ==="
echo "CPU Cores Available: $TOTAL_CORES"
echo "Threads to Use: $THREAD_COUNT"
echo "Target Address: $RIN_ADDRESS"
echo ""
echo ""
# Configuration
RPC_HOST="127.0.0.1"
RPC_PORT="9556"
RPC_USER="rinrpc"
RPC_PASS="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
# Function to call RPC
call_rpc() {
local method="$1"
local params="$2"
curl -s --user "$RPC_USER:$RPC_PASS" \
-H 'content-type: text/plain' \
--data "{\"jsonrpc\":\"1.0\",\"id\":\"curl\",\"method\":\"$method\",\"params\":$params}" \
"http://$RPC_HOST:$RPC_PORT/"
}
# Wait for node to be ready
echo "Waiting for RinCoin node to be ready..."
while true; do
response=$(call_rpc "getblockchaininfo" "[]")
if [[ $response != *"Loading block index"* ]]; then
break
fi
echo "Node still loading... waiting 10 seconds"
sleep 10
done
echo "✅ Node is ready!"
echo ""
# Load wallet if not already loaded
echo "Loading wallet..."
wallet_response=$(call_rpc "loadwallet" "[\"main\"]")
if [[ $wallet_response == *"error"* ]] && [[ $wallet_response == *"already loaded"* ]]; then
echo "✅ Wallet already loaded"
else
echo "✅ Wallet loaded successfully"
fi
echo ""
# Validate the provided address (basic check)
if [[ ! "$RIN_ADDRESS" =~ ^rin1[a-zA-Z0-9]{25,}$ ]]; then
echo "❌ Error: Invalid RinCoin address format: $RIN_ADDRESS"
echo "RinCoin addresses should start with 'rin1' and be ~30 characters long"
exit 1
fi
echo "✅ Using RinCoin Address: $RIN_ADDRESS"
echo ""
# Get blockchain info
echo "Blockchain Status:"
blockchain_info=$(call_rpc "getblockchaininfo" "[]")
blocks=$(echo "$blockchain_info" | grep -o '"blocks":[^,]*' | cut -d':' -f2)
headers=$(echo "$blockchain_info" | grep -o '"headers":[^,]*' | cut -d':' -f2)
difficulty=$(echo "$blockchain_info" | grep -o '"difficulty":[^,]*' | cut -d':' -f2)
echo "Blocks: $blocks"
echo "Headers: $headers"
echo "Difficulty: $difficulty"
echo ""
echo "⚠️ IMPORTANT: Built-in Core Mining Limitations:"
echo "1. Uses CPU only (not GPU)"
echo "2. Very low hashpower compared to specialized miners"
echo "3. Extremely low chance of finding blocks solo"
echo "4. Best for testing, not profitable mining"
echo "5. Thread count affects mining attempts per cycle"
echo ""
echo "🚀 Starting Built-in Solo Mining..."
echo "Target Address: $RIN_ADDRESS"
echo "Threads: $THREAD_COUNT"
echo "Press Ctrl+C to stop mining"
echo ""
# Start built-in mining with specified thread count
# Note: RinCoin Core's generatetoaddress doesn't directly support thread count
# but we can run multiple instances or adjust the maxtries parameter
while true; do
echo "Attempting to mine 1 block with $THREAD_COUNT threads..."
# Adjust maxtries based on thread count for better distribution
adjusted_tries=$((1000000 * THREAD_COUNT / TOTAL_CORES))
mining_result=$(call_rpc "generatetoaddress" "[1, \"$RIN_ADDRESS\", $adjusted_tries]")
if [[ $mining_result == *"result"* ]] && [[ $mining_result != *"[]"* ]]; then
echo "🎉 BLOCK FOUND!"
echo "Result: $mining_result"
break
else
echo "No block found in this attempt (tries: $adjusted_tries). Retrying..."
sleep 5
fi
done

View File

@@ -0,0 +1,81 @@
#!/bin/bash
# Remote Solo Mining Script for RinCoin
# Connects to RinCoin node over network/RPC
# Configuration
RPC_HOST="127.0.0.1" # Change to your server IP for remote access
RPC_PORT="9556"
RPC_USER="rinrpc"
RPC_PASS="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
echo "=== Remote RinCoin Solo Mining Setup ==="
echo "RPC Host: $RPC_HOST:$RPC_PORT"
echo ""
# Test RPC connection
echo "Testing RPC connection..."
RPC_RESPONSE=$(curl -s --user "$RPC_USER:$RPC_PASS" \
-H 'content-type: text/plain' \
--data '{"jsonrpc":"1.0","id":"curl","method":"getblockchaininfo","params":[]}' \
"http://$RPC_HOST:$RPC_PORT/")
if [[ $RPC_RESPONSE == *"error"* ]]; then
echo "❌ Error: Could not connect to RinCoin RPC!"
echo "Response: $RPC_RESPONSE"
echo ""
echo "Check:"
echo "1. RinCoin node is running"
echo "2. RPC port $RPC_PORT is accessible"
echo "3. Firewall allows connections to port $RPC_PORT"
exit 1
fi
echo "✅ RPC connection successful!"
echo ""
# Get wallet address via RPC
echo "Getting wallet address..."
WALLET_RESPONSE=$(curl -s --user "$RPC_USER:$RPC_PASS" \
-H 'content-type: text/plain' \
--data '{"jsonrpc":"1.0","id":"curl","method":"getnewaddress","params":[]}' \
"http://$RPC_HOST:$RPC_PORT/")
RIN_ADDRESS=$(echo "$WALLET_RESPONSE" | grep -o '"result":"[^"]*"' | cut -d'"' -f4)
if [ -z "$RIN_ADDRESS" ]; then
echo "❌ Error: Could not get RinCoin address!"
echo "Response: $WALLET_RESPONSE"
echo ""
echo "Make sure wallet 'main' exists:"
echo "curl --user $RPC_USER:$RPC_PASS -H 'content-type: text/plain' --data '{\"jsonrpc\":\"1.0\",\"id\":\"curl\",\"method\":\"createwallet\",\"params\":[\"main\"]}' http://$RPC_HOST:$RPC_PORT/"
exit 1
fi
echo "✅ RinCoin Address: $RIN_ADDRESS"
echo ""
# Check node sync status
SYNC_RESPONSE=$(curl -s --user "$RPC_USER:$RPC_PASS" \
-H 'content-type: text/plain' \
--data '{"jsonrpc":"1.0","id":"curl","method":"getblockchaininfo","params":[]}' \
"http://$RPC_HOST:$RPC_PORT/")
SYNC_STATUS=$(echo "$SYNC_RESPONSE" | grep -o '"initialblockdownload":[^,]*' | cut -d':' -f2 | tr -d ' ')
if [ "$SYNC_STATUS" = "true" ]; then
echo "⚠️ WARNING: Node is still syncing (initialblockdownload: true)"
echo "Solo mining may not work properly until sync is complete."
echo ""
fi
echo "Starting remote solo mining with cpuminer-opt-rin..."
echo "Algorithm: rinhash"
echo "Target: RinCoin node at $RPC_HOST:9555"
echo "Wallet: $RIN_ADDRESS"
echo ""
echo "Press Ctrl+C to stop mining"
echo ""
# Start solo mining (connect to P2P port, not RPC)
sudo docker exec -it amd-strix-halo-llama-rocm bash -c "/mnt/dl/rinhash/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://$RPC_HOST:9555 -u $RIN_ADDRESS -p x -t 32"

View File

@@ -0,0 +1,76 @@
#!/bin/bash
# RinCoin Solo Mining via RPC
# This script uses RinCoin's RPC interface for solo mining
echo "=== RinCoin Solo Mining via RPC ==="
echo ""
# Configuration
RPC_HOST="127.0.0.1"
RPC_PORT="9556"
RPC_USER="rinrpc"
RPC_PASS="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
# Function to call RPC
call_rpc() {
local method="$1"
local params="$2"
curl -s --user "$RPC_USER:$RPC_PASS" \
-H 'content-type: text/plain' \
--data "{\"jsonrpc\":\"1.0\",\"id\":\"curl\",\"method\":\"$method\",\"params\":$params}" \
"http://$RPC_HOST:$RPC_PORT/"
}
# Wait for node to be ready
echo "Waiting for RinCoin node to be ready..."
while true; do
response=$(call_rpc "getblockchaininfo" "[]")
if [[ $response != *"Loading block index"* ]]; then
break
fi
echo "Node still loading... waiting 10 seconds"
sleep 10
done
echo "✅ Node is ready!"
echo ""
# Get wallet address
echo "Getting wallet address..."
wallet_response=$(call_rpc "getnewaddress" "[]")
rin_address=$(echo "$wallet_response" | grep -o '"result":"[^"]*"' | cut -d'"' -f4)
if [ -z "$rin_address" ]; then
echo "❌ Error: Could not get RinCoin address!"
echo "Response: $wallet_response"
exit 1
fi
echo "✅ RinCoin Address: $rin_address"
echo ""
# Get blockchain info
echo "Blockchain Status:"
blockchain_info=$(call_rpc "getblockchaininfo" "[]")
blocks=$(echo "$blockchain_info" | grep -o '"blocks":[^,]*' | cut -d':' -f2)
headers=$(echo "$blockchain_info" | grep -o '"headers":[^,]*' | cut -d':' -f2)
difficulty=$(echo "$blockchain_info" | grep -o '"difficulty":[^,]*' | cut -d':' -f2)
echo "Blocks: $blocks"
echo "Headers: $headers"
echo "Difficulty: $difficulty"
echo ""
echo "⚠️ IMPORTANT: RinCoin solo mining requires:"
echo "1. A fully synced node (currently at block $blocks of $headers)"
echo "2. Mining software that supports RinCoin's RPC mining protocol"
echo "3. Very high hashpower to find blocks solo"
echo ""
echo "For now, we recommend pool mining for consistent rewards:"
echo ""
echo "Pool Mining Command:"
echo "sudo docker exec -it amd-strix-halo-llama-rocm bash -c \"/mnt/dl/rinhash/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://rinhash.mine.zergpool.com:7148 -u bc1qjn4m6rmrveuxhk02a5qhe4r6kdcsvvt3vhdn9j -p c=BTC,mc=RIN,ID=StrixHalo -t 32\""
echo ""
echo "Your RinCoin address for solo mining: $rin_address"

View File

@@ -0,0 +1,76 @@
#!/bin/bash
# RinCoin Solo Mining via RPC
# This script uses RinCoin's RPC interface for solo mining
echo "=== RinCoin Solo Mining via RPC ==="
echo ""
# Configuration
RPC_HOST="127.0.0.1"
RPC_PORT="9556"
RPC_USER="rinrpc"
RPC_PASS="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
# Function to call RPC
call_rpc() {
local method="$1"
local params="$2"
curl -s --user "$RPC_USER:$RPC_PASS" \
-H 'content-type: text/plain' \
--data "{\"jsonrpc\":\"1.0\",\"id\":\"curl\",\"method\":\"$method\",\"params\":$params}" \
"http://$RPC_HOST:$RPC_PORT/"
}
# Wait for node to be ready
echo "Waiting for RinCoin node to be ready..."
while true; do
response=$(call_rpc "getblockchaininfo" "[]")
if [[ $response != *"Loading block index"* ]]; then
break
fi
echo "Node still loading... waiting 10 seconds"
sleep 10
done
echo "✅ Node is ready!"
echo ""
# Get wallet address
echo "Getting wallet address..."
wallet_response=$(call_rpc "getnewaddress" "[]")
rin_address=$(echo "$wallet_response" | grep -o '"result":"[^"]*"' | cut -d'"' -f4)
if [ -z "$rin_address" ]; then
echo "❌ Error: Could not get RinCoin address!"
echo "Response: $wallet_response"
exit 1
fi
echo "✅ RinCoin Address: $rin_address"
echo ""
# Get blockchain info
echo "Blockchain Status:"
blockchain_info=$(call_rpc "getblockchaininfo" "[]")
blocks=$(echo "$blockchain_info" | grep -o '"blocks":[^,]*' | cut -d':' -f2)
headers=$(echo "$blockchain_info" | grep -o '"headers":[^,]*' | cut -d':' -f2)
difficulty=$(echo "$blockchain_info" | grep -o '"difficulty":[^,]*' | cut -d':' -f2)
echo "Blocks: $blocks"
echo "Headers: $headers"
echo "Difficulty: $difficulty"
echo ""
echo "⚠️ IMPORTANT: RinCoin solo mining requires:"
echo "1. A fully synced node (currently at block $blocks of $headers)"
echo "2. Mining software that supports RinCoin's RPC mining protocol"
echo "3. Very high hashpower to find blocks solo"
echo ""
echo "For now, we recommend pool mining for consistent rewards:"
echo ""
echo "Pool Mining Command:"
echo "sudo docker exec -it amd-strix-halo-llama-rocm bash -c \"/mnt/dl/rinhash/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://rinhash.mine.zergpool.com:7148 -u bc1qjn4m6rmrveuxhk02a5qhe4r6kdcsvvt3vhdn9j -p c=BTC,mc=RIN,ID=StrixHalo -t 32\""
echo ""
echo "Your RinCoin address for solo mining: $rin_address"

View File

@@ -0,0 +1,83 @@
#!/bin/bash
# RinCoin Mining Pool Server Startup Script
# Distributes block rewards among multiple miners
echo "=== RinCoin Mining Pool Server ==="
echo ""
# Check if RinCoin node is running
echo "Checking RinCoin node status..."
if ! curl -s -u rinrpc:745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90 \
-H 'content-type: text/plain' \
--data '{"jsonrpc":"1.0","id":"curl","method":"getblockchaininfo","params":[]}' \
http://127.0.0.1:9556/ > /dev/null; then
echo "❌ RinCoin node is not running!"
echo "Start it first with: docker start rincoin-node"
exit 1
fi
echo "✅ RinCoin node is running"
# Check Python dependencies
echo "Checking Python dependencies..."
python3 -c "import requests, sqlite3" 2>/dev/null || {
echo "Installing python3-requests..."
sudo apt-get update && sudo apt-get install -y python3-requests
}
echo "✅ Python dependencies ready"
# Check if port 3333 is already in use
if netstat -tln | grep -q ":3333 "; then
echo ""
echo "⚠️ Port 3333 is already in use!"
echo ""
echo "🔍 Process using port 3333:"
sudo netstat -tlnp | grep ":3333 " || echo "Could not determine process"
echo ""
echo "🛑 To kill existing process:"
echo "sudo lsof -ti:3333 | xargs sudo kill -9"
echo ""
read -p "Kill existing process and continue? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Killing processes using port 3333..."
sudo lsof -ti:3333 | xargs sudo kill -9 2>/dev/null || echo "No processes to kill"
sleep 2
else
echo "Exiting..."
exit 1
fi
fi
echo ""
echo "🚀 Starting Mining Pool Server..."
echo "This will distribute block rewards among multiple miners"
echo ""
echo "Pool Features:"
echo "- Multiple miner support"
echo "- Share-based reward distribution"
echo "- Pool fee: 1%"
echo "- Real-time statistics"
echo "- Web dashboard on port 8083"
echo ""
echo "After it starts, miners can connect with:"
echo ""
echo "Option 1: Address as username"
echo "./cpuminer -a rinhash -o stratum+tcp://YOUR_IP:3333 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q -p x"
echo ""
echo "Option 2: Address.workername format"
echo "./cpuminer -a rinhash -o stratum+tcp://YOUR_IP:3333 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q.worker1 -p x"
echo ""
echo "Option 3: Traditional username (rewards to pool address)"
echo "./cpuminer -a rinhash -o stratum+tcp://YOUR_IP:3333 -u username.workername -p x"
echo ""
echo "🌐 Web Dashboard: http://YOUR_IP:8083"
echo "📊 View real-time pool statistics, miners, and blocks"
echo ""
echo "Press Ctrl+C to stop the pool"
# Start the mining pool
python3 MINE/rin/stratum_pool.py

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Start RinCoin Stratum Proxy Server
# Bridges cpuminer-opt-rin to RinCoin node
echo "=== RinCoin Stratum Proxy Server ==="
echo ""
# Check if RinCoin node is running
if ! sudo docker ps | grep -q "rincoin-node"; then
echo "❌ Error: rincoin-node container is not running!"
echo "Please start it first:"
echo "sudo docker start rincoin-node"
exit 1
fi
echo "✅ RinCoin node is running"
# Check if Python3 and requests are available
if ! command -v python3 &> /dev/null; then
echo "❌ Error: python3 is not installed!"
echo "Please install it: sudo apt-get install python3"
exit 1
fi
# Install requests if not available
python3 -c "import requests" 2>/dev/null || {
echo "Installing python3-requests..."
sudo apt-get update && sudo apt-get install -y python3-requests
}
echo "✅ Python dependencies ready"
# Check if port 3334 is already in use
if netstat -tln | grep -q ":3334 "; then
echo ""
echo "⚠️ Port 3334 is already in use!"
echo ""
echo "🔍 Process using port 3334:"
sudo netstat -tlnp | grep ":3334 " || echo "Could not determine process"
echo ""
echo "🛑 To kill existing process:"
echo "sudo lsof -ti:3334 | xargs sudo kill -9"
echo ""
read -p "Kill existing process and continue? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Killing processes using port 3334..."
sudo lsof -ti:3334 | xargs sudo kill -9 2>/dev/null || echo "No processes to kill"
sleep 2
else
echo "Exiting..."
exit 1
fi
fi
echo ""
echo "🚀 Starting Stratum Proxy Server..."
echo ""
echo "After it starts, connect your miner with:"
echo "sudo docker exec -it amd-strix-halo-llama-rocm bash -c \"/mnt/dl/rinhash/cpuminer-opt-rin/cpuminer -a rinhash -o stratum+tcp://127.0.0.1:3334 -u user -p pass -t 28\""
echo ""
echo "Press Ctrl+C to stop the proxy"
echo ""
# Start the proxy
cd "$(dirname "$0")"
python3 stratum_proxy.py --submit-all-blocks

View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="/mnt/shared/DEV/repos/d-popov.com/scripts/MINE/rin/web_wallet"
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required"
exit 1
fi
python3 "${SCRIPT_DIR}/server.py"

View File

@@ -0,0 +1,602 @@
# #!/usr/bin/env python3
# """
# RinCoin Mining Pool Server
# Distributes block rewards among multiple miners based on share contributions
# """
# import socket
# import threading
# import json
# import time
# import requests
# import hashlib
# import struct
# import sqlite3
# from datetime import datetime
# from requests.auth import HTTPBasicAuth
# # Import web interface
# from pool_web_interface import start_web_interface
# # Import stratum base class
# from stratum_proxy import RinCoinStratumBase
# class RinCoinMiningPool(RinCoinStratumBase):
# def __init__(self, stratum_host='0.0.0.0', stratum_port=3333,
# rpc_host='127.0.0.1', rpc_port=9556,
# rpc_user='rinrpc', rpc_password='745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90',
# pool_address='rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q',
# pool_fee_percent=1.0):
# # Initialize base class
# super().__init__(stratum_host, stratum_port, rpc_host, rpc_port, rpc_user, rpc_password, pool_address)
# self.pool_address = pool_address
# self.pool_fee_percent = pool_fee_percent
# # Pool statistics
# self.total_shares = 0
# self.total_blocks = 0
# self.pool_hashrate = 0
# # Database for persistent storage
# self.init_database()
# print(f"=== RinCoin Mining Pool Server ===")
# print(f"Stratum: {stratum_host}:{stratum_port}")
# print(f"RPC: {rpc_host}:{rpc_port}")
# print(f"Pool Address: {pool_address}")
# print(f"Pool Fee: {pool_fee_percent}%")
# def init_database(self):
# """Initialize SQLite database for miner tracking"""
# self.db = sqlite3.connect(':memory:', check_same_thread=False)
# cursor = self.db.cursor()
# # Create tables
# cursor.execute('''
# CREATE TABLE IF NOT EXISTS miners (
# id INTEGER PRIMARY KEY,
# user TEXT NOT NULL,
# worker TEXT NOT NULL,
# address TEXT,
# shares INTEGER DEFAULT 0,
# last_share TIMESTAMP,
# last_hashrate REAL DEFAULT 0,
# created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
# )
# ''')
# cursor.execute('''
# CREATE TABLE IF NOT EXISTS shares (
# id INTEGER PRIMARY KEY,
# miner_id INTEGER,
# job_id TEXT,
# difficulty REAL,
# submitted TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
# FOREIGN KEY (miner_id) REFERENCES miners (id)
# )
# ''')
# cursor.execute('''
# CREATE TABLE IF NOT EXISTS blocks (
# id INTEGER PRIMARY KEY,
# block_hash TEXT,
# height INTEGER,
# reward REAL,
# pool_fee REAL,
# miner_rewards TEXT, -- JSON of {address: amount}
# found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
# )
# ''')
# # Samples for pool hashrate chart
# cursor.execute('''
# CREATE TABLE IF NOT EXISTS hashrate_samples (
# id INTEGER PRIMARY KEY,
# ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
# hashrate REAL
# )
# ''')
# self.db.commit()
# def get_pool_block_template(self):
# """Get new block template and create pool-style job"""
# template = super().get_block_template()
# if template:
# # Convert to pool-style job format if needed
# job = self.current_job
# if job:
# # Add pool-specific fields
# job["coinb1"] = "01000000" + "0" * 60
# job["coinb2"] = "ffffffff"
# job["merkle_branch"] = []
# job["clean_jobs"] = True
# return job
# return None
# def validate_rincoin_address(self, address):
# """Validate if an address is a valid RinCoin address"""
# try:
# return self.decode_bech32_address(address) is not None
# except:
# return False
# def register_miner(self, user, worker, address=None):
# """Register or update miner in database"""
# cursor = self.db.cursor()
# # Check if miner exists
# cursor.execute('SELECT id, address FROM miners WHERE user = ? AND worker = ?', (user, worker))
# result = cursor.fetchone()
# if result:
# miner_id, existing_address = result
# if address and not existing_address:
# cursor.execute('UPDATE miners SET address = ? WHERE id = ?', (address, miner_id))
# self.db.commit()
# return miner_id
# else:
# # Create new miner
# cursor.execute('INSERT INTO miners (user, worker, address) VALUES (?, ?, ?)', (user, worker, address))
# self.db.commit()
# return cursor.lastrowid
# def record_share(self, miner_id, job_id, difficulty):
# """Record a share submission"""
# cursor = self.db.cursor()
# # Record share
# cursor.execute('INSERT INTO shares (miner_id, job_id, difficulty) VALUES (?, ?, ?)',
# (miner_id, job_id, difficulty))
# # Update miner stats
# cursor.execute('UPDATE miners SET shares = shares + 1, last_share = CURRENT_TIMESTAMP WHERE id = ?', (miner_id,))
# self.db.commit()
# self.total_shares += 1
# def distribute_block_reward(self, block_hash, block_height, total_reward):
# """Distribute block reward among miners based on their shares"""
# cursor = self.db.cursor()
# # Calculate pool fee
# pool_fee = total_reward * (self.pool_fee_percent / 100.0)
# miner_reward = total_reward - pool_fee
# # Get shares from last 24 hours
# cursor.execute('''
# SELECT m.address, COUNT(s.id) as share_count, SUM(s.difficulty) as total_difficulty
# FROM miners m
# JOIN shares s ON m.id = s.miner_id
# WHERE s.submitted > datetime('now', '-1 day')
# GROUP BY m.id, m.address
# HAVING share_count > 0
# ''')
# miners = cursor.fetchall()
# if not miners:
# print("No miners with shares in last 24 hours")
# return
# # Calculate total difficulty
# total_difficulty = sum(row[2] for row in miners)
# # Separate miners with and without addresses
# miners_with_addresses = []
# miners_without_addresses = []
# total_difficulty_with_addresses = 0
# total_difficulty_without_addresses = 0
# for address, share_count, difficulty in miners:
# if address:
# miners_with_addresses.append((address, share_count, difficulty))
# total_difficulty_with_addresses += difficulty
# else:
# miners_without_addresses.append((address, share_count, difficulty))
# total_difficulty_without_addresses += difficulty
# # Calculate total difficulty
# total_difficulty = total_difficulty_with_addresses + total_difficulty_without_addresses
# if total_difficulty == 0:
# print("No valid difficulty found")
# return
# # Distribute rewards
# miner_rewards = {}
# # First, distribute to miners with valid addresses
# if miners_with_addresses:
# for address, share_count, difficulty in miners_with_addresses:
# reward_share = (difficulty / total_difficulty) * miner_reward
# miner_rewards[address] = reward_share
# print(f"💰 Miner {address}: {reward_share:.8f} RIN ({difficulty} difficulty)")
# # Calculate undistributed rewards (from miners without addresses)
# if miners_without_addresses:
# undistributed_reward = 0
# for address, share_count, difficulty in miners_without_addresses:
# undistributed_reward += (difficulty / total_difficulty) * miner_reward
# print(f"⚠️ Miner without address: {difficulty} difficulty -> {undistributed_reward:.8f} RIN to pool")
# # Keep undistributed rewards for pool (no redistribution)
# print(f"💰 Pool keeps {undistributed_reward:.8f} RIN from miners without addresses")
# # Record block
# cursor.execute('''
# INSERT INTO blocks (block_hash, height, reward, pool_fee, miner_rewards)
# VALUES (?, ?, ?, ?, ?)
# ''', (block_hash, block_height, total_reward, pool_fee, json.dumps(miner_rewards)))
# self.db.commit()
# self.total_blocks += 1
# print(f"🎉 Block {block_height} reward distributed!")
# print(f"💰 Pool fee: {pool_fee:.8f} RIN")
# print(f"💰 Total distributed: {sum(miner_rewards.values()):.8f} RIN")
# # Summary
# if miners_without_addresses:
# print(f"📊 Summary: {len(miners_with_addresses)} miners with addresses, {len(miners_without_addresses)} without (rewards to pool)")
# # Use inherited send_stratum_response and send_stratum_notification from base class
# def handle_stratum_message(self, client, addr, message):
# """Handle incoming Stratum message from miner"""
# try:
# data = json.loads(message.strip())
# method = data.get("method")
# msg_id = data.get("id")
# params = data.get("params", [])
# print(f"[{addr}] {method}: {params}")
# if method == "mining.subscribe":
# # Subscribe response
# self.send_stratum_response(client, msg_id, [
# [["mining.set_difficulty", "subscription_id"], ["mining.notify", "subscription_id"]],
# "extranonce1",
# 4
# ])
# # Send difficulty (lower for CPU mining)
# self.send_stratum_notification(client, "mining.set_difficulty", [0.0001])
# # Send initial job
# if self.get_pool_block_template():
# job = self.current_job
# self.send_stratum_notification(client, "mining.notify", [
# job["job_id"],
# job["prevhash"],
# job["coinb1"],
# job["coinb2"],
# job["merkle_branch"],
# f"{job['version']:08x}",
# job["bits"],
# job["ntime"],
# job["clean_jobs"]
# ])
# elif method == "mining.extranonce.subscribe":
# # Handle extranonce subscription
# print(f"[{addr}] Extranonce subscription requested")
# self.send_stratum_response(client, msg_id, True)
# elif method == "mining.authorize":
# # Parse user.worker format
# if len(params) >= 2:
# user_worker = params[0]
# password = params[1] if len(params) > 1 else ""
# # Extract user and worker
# if '.' in user_worker:
# user, worker = user_worker.split('.', 1)
# else:
# user = user_worker
# worker = "default"
# # Check if user contains a RinCoin address (starts with 'rin')
# miner_address = None
# if user.startswith('rin'):
# # User is a RinCoin address
# if self.validate_rincoin_address(user):
# miner_address = user
# user = f"miner_{miner_address[:8]}" # Create a user ID from address
# print(f"[{addr}] ✅ Miner using valid RinCoin address: {miner_address}")
# else:
# print(f"[{addr}] ❌ Invalid RinCoin address: {user}")
# self.send_stratum_response(client, msg_id, False, "Invalid RinCoin address")
# return
# elif '.' in user and user.split('.')[0].startswith('rin'):
# # Format: rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q.workername
# address_part, worker_part = user.split('.', 1)
# if address_part.startswith('rin'):
# if self.validate_rincoin_address(address_part):
# miner_address = address_part
# user = f"miner_{miner_address[:8]}"
# worker = worker_part
# print(f"[{addr}] ✅ Miner using valid RinCoin address format: {miner_address}.{worker}")
# else:
# print(f"[{addr}] ❌ Invalid RinCoin address: {address_part}")
# self.send_stratum_response(client, msg_id, False, "Invalid RinCoin address")
# return
# # Register miner with address
# miner_id = self.register_miner(user, worker, miner_address)
# # Store client info
# self.clients[addr] = {
# 'client': client,
# 'user': user,
# 'worker': worker,
# 'miner_id': miner_id,
# 'address': miner_address,
# 'shares': 0,
# 'last_share': time.time(),
# 'extranonce1': '00000000' # Default extranonce1
# }
# if miner_address:
# print(f"[{addr}] ✅ Authorized: {user}.{worker} -> {miner_address}")
# else:
# print(f"[{addr}] ⚠️ Authorized: {user}.{worker} (rewards will go to pool address)")
# self.send_stratum_response(client, msg_id, True)
# else:
# self.send_stratum_response(client, msg_id, False, "Invalid authorization")
# elif method == "mining.submit":
# # Submit share
# if addr not in self.clients:
# self.send_stratum_response(client, msg_id, False, "Not authorized")
# return
# miner_info = self.clients[addr]
# try:
# if self.current_job and len(params) >= 5:
# username = params[0]
# job_id = params[1]
# extranonce2 = params[2]
# ntime = params[3]
# nonce = params[4]
# # Use base class to validate and submit share
# extranonce1 = miner_info.get('extranonce1', '00000000')
# miner_address = miner_info.get('address')
# # For pool mining, always mine to pool address
# success, message = self.submit_share(
# self.current_job, extranonce1, extranonce2, ntime, nonce,
# target_address=self.pool_address
# )
# if success:
# # Record share with estimated difficulty
# actual_difficulty = 0.00133 # Estimated for ~381 kH/s
# self.record_share(miner_info['miner_id'], job_id, actual_difficulty)
# # Update miner stats
# now_ts = time.time()
# prev_ts = miner_info.get('last_share') or now_ts
# dt = max(now_ts - prev_ts, 1e-3)
# miner_hashrate = actual_difficulty * (2**32) / dt
# if miner_info['shares'] == 0:
# miner_hashrate = 381000 # Default estimate
# miner_info['shares'] += 1
# miner_info['last_share'] = now_ts
# # Update database
# try:
# cursor = self.db.cursor()
# cursor.execute('UPDATE miners SET last_share = CURRENT_TIMESTAMP, last_hashrate = ? WHERE id = ?',
# (miner_hashrate, miner_info['miner_id']))
# self.db.commit()
# except Exception as e:
# print(f"DB update error: {e}")
# print(f"[{addr}] ✅ Share accepted from {miner_info['user']}.{miner_info['worker']} (Total: {miner_info['shares']})")
# self.send_stratum_response(client, msg_id, True)
# # If block was found, distribute rewards
# if "Block found" in message:
# print(f"🎉 [{addr}] BLOCK FOUND!")
# # Get block info and distribute rewards
# total_reward = self.current_job['coinbasevalue'] / 100000000 if self.current_job else 25.0
# self.distribute_block_reward("pending", self.current_job['height'] if self.current_job else 0, total_reward)
# else:
# # Accept as share for pool statistics even if block validation fails
# self.send_stratum_response(client, msg_id, True)
# else:
# print(f"[{addr}] Invalid share parameters")
# self.send_stratum_response(client, msg_id, False, "Invalid parameters")
# except Exception as e:
# print(f"[{addr}] Share processing error: {e}")
# # Still accept the share for mining statistics
# self.send_stratum_response(client, msg_id, True)
# else:
# print(f"[{addr}] ⚠️ Unknown method: {method}")
# # Send null result for unknown methods (standard Stratum behavior)
# self.send_stratum_response(client, msg_id, None, None)
# except json.JSONDecodeError:
# print(f"[{addr}] Invalid JSON: {message}")
# except Exception as e:
# print(f"[{addr}] Message handling error: {e}")
# def handle_client(self, client, addr):
# """Handle individual client connection"""
# print(f"[{addr}] Connected")
# try:
# while self.running:
# data = client.recv(4096)
# if not data:
# break
# # Handle multiple messages in one packet
# messages = data.decode('utf-8').strip().split('\n')
# for message in messages:
# if message:
# self.handle_stratum_message(client, addr, message)
# except Exception as e:
# print(f"[{addr}] Client error: {e}")
# finally:
# client.close()
# if addr in self.clients:
# del self.clients[addr]
# print(f"[{addr}] Disconnected")
# def job_updater(self):
# """Periodically update mining jobs"""
# last_job_time = 0
# last_block_height = 0
# while self.running:
# try:
# # Check for new blocks every 10 seconds
# time.sleep(10)
# # Get current blockchain info
# blockchain_info = self.rpc_call("getblockchaininfo")
# if blockchain_info:
# current_height = blockchain_info.get('blocks', 0)
# # Create new job if:
# # 1. New block detected
# # 2. 30+ seconds since last job
# # 3. No current job exists
# should_create_job = (
# current_height != last_block_height or
# time.time() - last_job_time > 30 or
# not self.current_job
# )
# if should_create_job:
# if self.get_pool_block_template():
# job = self.current_job
# last_job_time = time.time()
# last_block_height = current_height
# print(f"📦 New job created: {job['job_id']} (block {current_height})")
# # Send to all connected clients
# for addr, miner_info in list(self.clients.items()):
# try:
# self.send_stratum_notification(miner_info['client'], "mining.notify", [
# job["job_id"],
# job["prevhash"],
# job["coinb1"],
# job["coinb2"],
# job["merkle_branch"],
# f"{job['version']:08x}",
# job["bits"],
# job["ntime"],
# job["clean_jobs"]
# ])
# except Exception as e:
# print(f"Failed to send job to {addr}: {e}")
# except Exception as e:
# print(f"Job updater error: {e}")
# def stats_updater(self):
# """Periodically update pool statistics"""
# while self.running:
# try:
# time.sleep(60) # Update every minute
# cursor = self.db.cursor()
# # Pool hashrate is the sum of miners' last hashrates
# cursor.execute('SELECT COALESCE(SUM(last_hashrate), 0) FROM miners')
# self.pool_hashrate = cursor.fetchone()[0] or 0.0
# # Sample for chart
# cursor.execute('INSERT INTO hashrate_samples (hashrate) VALUES (?)', (self.pool_hashrate,))
# self.db.commit()
# print(f"📊 Pool Stats: {len(self.clients)} miners, {self.total_shares} shares, {self.pool_hashrate/1000:.2f} kH/s")
# except Exception as e:
# print(f"Stats updater error: {e}")
# def start(self):
# """Start the mining pool server"""
# try:
# # Test RPC connection
# blockchain_info = self.rpc_call("getblockchaininfo")
# if not blockchain_info:
# print("❌ Failed to connect to RinCoin node!")
# return
# print(f"✅ Connected to RinCoin node (block {blockchain_info.get('blocks', 'unknown')})")
# # Start background threads
# job_thread = threading.Thread(target=self.job_updater, daemon=True)
# job_thread.start()
# stats_thread = threading.Thread(target=self.stats_updater, daemon=True)
# stats_thread.start()
# # Start web interface in background
# web_thread = threading.Thread(target=start_web_interface,
# args=(self.db, '0.0.0.0', 8083,
# self.rpc_host, self.rpc_port,
# self.rpc_user, self.rpc_password),
# daemon=True)
# web_thread.start()
# print(f"🌐 Web dashboard started on http://0.0.0.0:8083")
# # Start Stratum server
# server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# server_socket.bind((self.stratum_host, self.stratum_port))
# server_socket.listen(10)
# print(f"🚀 Mining pool listening on {self.stratum_host}:{self.stratum_port}")
# print("Ready for multiple miners...")
# print("")
# print(f"💰 Pool address: {self.pool_address}")
# print(f"💰 Pool fee: {self.pool_fee_percent}%")
# print("")
# print("Connect miners with:")
# print(f"./cpuminer -a rinhash -o stratum+tcp://{self.stratum_host}:{self.stratum_port} -u username.workername -p x")
# print("")
# while self.running:
# try:
# client, addr = server_socket.accept()
# client_thread = threading.Thread(target=self.handle_client, args=(client, addr), daemon=True)
# client_thread.start()
# except KeyboardInterrupt:
# print("\n🛑 Shutting down pool...")
# self.running = False
# break
# except Exception as e:
# print(f"Server error: {e}")
# except OSError as e:
# if "Address already in use" in str(e):
# print(f"❌ Port {self.stratum_port} is already in use!")
# print("")
# print("🔍 Check what's using the port:")
# print(f"sudo netstat -tlnp | grep :{self.stratum_port}")
# print("")
# print("🛑 Kill existing process:")
# print(f"sudo lsof -ti:{self.stratum_port} | xargs sudo kill -9")
# print("")
# print("🔄 Or use a different port by editing the script")
# else:
# print(f"Failed to start server: {e}")
# except Exception as e:
# print(f"Failed to start server: {e}")
# finally:
# print("Pool server stopped")
# if __name__ == "__main__":
# pool = RinCoinMiningPool()
# pool.start()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
#!/bin/bash
# Test Reward Redistribution Logic
echo "=== Testing Reward Redistribution Logic ==="
echo ""
# Kill any existing processes
./MINE/rin/kill_stratum_proxy.sh
echo "🧪 Testing reward distribution scenarios:"
echo ""
echo "Scenario 1: All miners have valid addresses"
echo "Expected: Normal distribution"
echo ""
echo "Scenario 2: Some miners without addresses"
echo "Expected: Redistribution of their rewards to miners with addresses"
echo ""
echo "Scenario 3: All miners without addresses"
echo "Expected: All rewards go to pool"
echo ""
echo "🚀 Starting mining pool..."
./MINE/rin/start_mining_pool.sh &
POOL_PID=$!
echo ""
echo "⏳ Waiting for pool to start..."
sleep 5
echo ""
echo "🧪 Test 1: Miner with valid address"
echo "Expected: Gets full share of rewards"
timeout 5s ./cpuminer -a rinhash -o stratum+tcp://127.0.0.1:3333 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q.worker1 -p x -t 1
echo ""
echo "🧪 Test 2: Miner without address"
echo "Expected: Contributes to difficulty but gets no direct rewards"
timeout 5s ./cpuminer -a rinhash -o stratum+tcp://127.0.0.1:3333 -u user.worker2 -p x -t 1
echo ""
echo "🧪 Test 3: Another miner with valid address"
echo "Expected: Gets base reward + redistribution from miner without address"
timeout 5s ./cpuminer -a rinhash -o stratum+tcp://127.0.0.1:3333 -u rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q.worker3 -p x -t 1
echo ""
echo "📊 Pool Log Analysis:"
echo "Look for these patterns in the logs above:"
echo "1. '💰 Miner rin1q...: X.XX RIN (difficulty)' - Base rewards"
echo "2. '⚠️ Miner without address: X difficulty -> X.XX RIN to pool' - Undistributed"
echo "3. '💰 Pool keeps X.XX RIN from miners without addresses' - Pool keeps rewards"
echo "4. '📊 Summary: X miners with addresses, Y without (rewards to pool)' - Final summary"
echo ""
echo "🧹 Cleaning up..."
kill $POOL_PID 2>/dev/null
./MINE/rin/kill_stratum_proxy.sh
echo ""
echo "📋 Reward Distribution Logic Summary:"
echo ""
echo "✅ Miners with valid RinCoin addresses:"
echo " - Get reward based on their difficulty"
echo " - Rewards sent directly to their addresses"
echo ""
echo "⚠️ Miners without addresses:"
echo " - Contribute to total difficulty"
echo " - Their reward share goes to pool address"
echo " - No direct rewards received"
echo ""
echo "💰 Pool fee: Always 1% of total block reward"
echo "💰 Pool bonus: Additional rewards from miners without addresses"

View File

@@ -0,0 +1,450 @@
# Proper Self-Custody Wallet Solution for RinCoin
## ❌ The Problem
You're absolutely right - the wallet file backup method is NOT user-friendly for self-custody:
- Not a simple 12-word phrase
- Large binary files
- Can't easily write down or memorize
- Not compatible with standard wallet UX
## ✅ The REAL Solution: BIP39 Mnemonic with Key Derivation
**We need to implement BIP39 support in the web wallet layer, NOT rely on RinCoin's RPC!**
### How It Works:
```
User's 12 Words (BIP39)
↓ (in web wallet)
Master Seed (BIP32)
↓ (derive keys)
Private Keys for Addresses
↓ (import to RinCoin)
RinCoin Wallet (via importprivkey)
```
### Key Insight:
**The web wallet handles BIP39/BIP32 derivation, then imports the derived keys into RinCoin!**
## 🎯 Implementation Architecture
### Layer 1: Web Wallet (BIP39 Support)
The browser extension/web wallet handles:
- BIP39 mnemonic generation and validation
- BIP32 key derivation
- Address generation
- Key management
### Layer 2: RinCoin Node (Key Storage Only)
The RinCoin node just stores imported keys:
- Receives derived private keys via `importprivkey`
- Tracks balances and transactions
- Signs transactions when needed
## 🔧 Technical Implementation
### 1. Backup Flow (Generate Mnemonic)
```javascript
// In Web Wallet
import * as bip39 from 'bip39';
import { BIP32Factory } from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
// Generate new wallet with mnemonic
async function createNewWallet() {
// 1. Generate BIP39 mnemonic (12 or 24 words)
const mnemonic = bip39.generateMnemonic(128); // 12 words
// 2. Convert to seed
const seed = await bip39.mnemonicToSeed(mnemonic);
// 3. Create master key
const root = bip32.fromSeed(seed);
// 4. Derive keys for RinCoin (using Litecoin derivation path)
// m/44'/2'/0'/0/0 (Litecoin path, which RinCoin is based on)
const masterPath = "m/44'/2'/0'/0";
// 5. Generate first 20 addresses
const addresses = [];
for (let i = 0; i < 20; i++) {
const child = root.derivePath(`${masterPath}/${i}`);
const privateKey = child.toWIF();
const address = deriveRinCoinAddress(child.publicKey);
addresses.push({
index: i,
address: address,
privateKey: privateKey
});
}
// 6. Import keys to RinCoin node
for (const addr of addresses) {
await importPrivateKeyToNode(addr.privateKey, addr.address);
}
// 7. Return mnemonic to user (SHOW ONCE, USER MUST WRITE DOWN)
return {
mnemonic: mnemonic,
addresses: addresses.map(a => a.address)
};
}
// Import single key to RinCoin
async function importPrivateKeyToNode(privateKey, label) {
return await rpcCall('importprivkey', [privateKey, label, false]);
}
```
### 2. Restore Flow (From Mnemonic)
```javascript
// In Web Wallet
async function restoreWalletFromMnemonic(mnemonic) {
// 1. Validate mnemonic
if (!bip39.validateMnemonic(mnemonic)) {
throw new Error('Invalid mnemonic phrase');
}
// 2. Convert to seed
const seed = await bip39.mnemonicToSeed(mnemonic);
// 3. Create master key
const root = bip32.fromSeed(seed);
// 4. Create new wallet on node
await rpcCall('createwallet', [walletName, false, false]);
// 5. Derive and import keys with gap limit
const masterPath = "m/44'/2'/0'/0";
let consecutiveUnused = 0;
const GAP_LIMIT = 20;
for (let i = 0; consecutiveUnused < GAP_LIMIT; i++) {
const child = root.derivePath(`${masterPath}/${i}`);
const privateKey = child.toWIF();
const address = deriveRinCoinAddress(child.publicKey);
// Import key
await importPrivateKeyToNode(privateKey, `addr_${i}`);
// Check if address has been used (has transactions)
const hasTransactions = await checkAddressUsage(address);
if (hasTransactions) {
consecutiveUnused = 0; // Reset counter
} else {
consecutiveUnused++;
}
}
// 6. Rescan blockchain to find all transactions
await rpcCall('rescanblockchain', []);
// 7. Get balance
const balance = await rpcCall('getbalance', []);
return {
success: true,
balance: balance,
message: 'Wallet restored successfully!'
};
}
```
### 3. Address Derivation for RinCoin
```javascript
import * as bitcoin from 'bitcoinjs-lib';
// RinCoin uses Litecoin's address format
function deriveRinCoinAddress(publicKey) {
// Use Litecoin network parameters (RinCoin is Litecoin-based)
const rincoinNetwork = {
messagePrefix: '\x19RinCoin Signed Message:\n',
bech32: 'rin', // For SegWit addresses (rin1q...)
bip32: {
public: 0x019da462, // Litecoin's public key version
private: 0x019d9cfe // Litecoin's private key version
},
pubKeyHash: 0x30, // Litecoin pubkey hash (for legacy addresses)
scriptHash: 0x32, // Litecoin script hash
wif: 0xb0 // Litecoin WIF
};
// Generate SegWit address (rin1q...)
const { address } = bitcoin.payments.p2wpkh({
pubkey: publicKey,
network: rincoinNetwork
});
return address;
}
```
## 📱 User Experience Design
### Creating New Wallet:
```
┌─────────────────────────────────┐
│ Create RinCoin Wallet │
├─────────────────────────────────┤
│ │
│ [Generate New Wallet] │
│ │
│ ↓ │
│ │
│ 🔐 Your Recovery Phrase: │
│ ┌───────────────────────────┐ │
│ │ witch collapse practice │ │
│ │ feed shame open despair │ │
│ │ creek road again ice least │ │
│ └───────────────────────────┘ │
│ │
│ ⚠️ WRITE THIS DOWN! │
│ These 12 words are the ONLY │
│ way to recover your wallet. │
│ │
│ ☐ I have written down my │
│ recovery phrase │
│ │
│ [Continue] ─────────────────→ │
└─────────────────────────────────┘
```
### Restoring Wallet:
```
┌─────────────────────────────────┐
│ Restore RinCoin Wallet │
├─────────────────────────────────┤
│ │
│ Enter your 12-word recovery │
│ phrase: │
│ │
│ ┌───────────────────────────┐ │
│ │ 1. witch 7. despair │ │
│ │ 2. collapse 8. creek │ │
│ │ 3. practice 9. road │ │
│ │ 4. feed 10. again │ │
│ │ 5. shame 11. ice │ │
│ │ 6. open 12. least │ │
│ └───────────────────────────┘ │
│ │
│ [Restore Wallet] │
│ │
│ ↓ │
│ │
│ 🔄 Restoring wallet... │
│ • Deriving keys... ✓ │
│ • Importing to node... ✓ │
│ • Scanning blockchain... ⏳ │
│ │
│ ✅ Restored! │
│ Balance: 1059.00 RIN │
└─────────────────────────────────┘
```
## 🔐 Security Features
### 1. Mnemonic Only (No Key Storage)
```javascript
// NEVER store the mnemonic permanently!
// Only derive keys when needed
class SecureWallet {
// User enters mnemonic each session
async unlockWallet(mnemonic) {
this.root = await deriveRootKey(mnemonic);
// Keep in memory only
}
// Clear on logout
lockWallet() {
this.root = null;
// Clear all key material from memory
}
// Derive key on-demand
async getPrivateKey(index) {
if (!this.root) throw new Error('Wallet locked');
return this.root.derivePath(`m/44'/2'/0'/0/${index}`);
}
}
```
### 2. Optional Encryption
```javascript
// For convenience, allow encrypted storage
async function saveEncryptedMnemonic(mnemonic, password) {
const encrypted = await encrypt(mnemonic, password);
localStorage.setItem('encrypted_wallet', encrypted);
}
async function loadEncryptedMnemonic(password) {
const encrypted = localStorage.getItem('encrypted_wallet');
return await decrypt(encrypted, password);
}
```
## 🎯 Complete Implementation Example
```javascript
// rin-web-wallet.js
import * as bip39 from 'bip39';
import { BIP32Factory } from 'bip32';
import * as ecc from 'tiny-secp256k1';
import * as bitcoin from 'bitcoinjs-lib';
const bip32 = BIP32Factory(ecc);
class RinWebWallet {
constructor(rpcUrl, rpcUser, rpcPassword) {
this.rpcUrl = rpcUrl;
this.rpcUser = rpcUser;
this.rpcPassword = rpcPassword;
}
// Create new wallet with mnemonic
async createWallet(walletName) {
// Generate mnemonic
const mnemonic = bip39.generateMnemonic(128); // 12 words
// Create wallet on node
await this.rpcCall('createwallet', [walletName, false, false]);
// Import initial addresses
await this.importAddressesFromMnemonic(mnemonic, walletName, 20);
return {
mnemonic: mnemonic,
message: 'WRITE DOWN THESE 12 WORDS!'
};
}
// Restore wallet from mnemonic
async restoreWallet(mnemonic, walletName) {
// Validate
if (!bip39.validateMnemonic(mnemonic)) {
throw new Error('Invalid mnemonic');
}
// Create wallet on node
await this.rpcCall('createwallet', [walletName, false, false]);
// Import addresses with gap limit
await this.importAddressesFromMnemonic(mnemonic, walletName, 100);
// Rescan blockchain
await this.rpcCall('rescanblockchain', [], walletName);
const balance = await this.rpcCall('getbalance', [], walletName);
return {
success: true,
balance: balance
};
}
// Internal: Import addresses from mnemonic
async importAddressesFromMnemonic(mnemonic, walletName, count) {
const seed = await bip39.mnemonicToSeed(mnemonic);
const root = bip32.fromSeed(seed);
for (let i = 0; i < count; i++) {
const child = root.derivePath(`m/44'/2'/0'/0/${i}`);
const privateKey = child.toWIF();
// Import to node
await this.rpcCall('importprivkey', [
privateKey,
`address_${i}`,
false // Don't rescan yet
], walletName);
}
}
// RPC call helper
async rpcCall(method, params = [], wallet = null) {
const url = wallet ? `${this.rpcUrl}/wallet/${wallet}` : this.rpcUrl;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${this.rpcUser}:${this.rpcPassword}`)
},
body: JSON.stringify({
jsonrpc: '2.0',
id: method,
method: method,
params: params
})
});
const data = await response.json();
if (data.error) throw new Error(data.error.message);
return data.result;
}
}
// Usage
const wallet = new RinWebWallet(
'http://localhost:9556',
'rinrpc',
'745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90'
);
// Create new
const { mnemonic } = await wallet.createWallet('my_wallet');
console.log('SAVE THESE WORDS:', mnemonic);
// Restore
await wallet.restoreWallet('witch collapse practice feed shame open despair creek road again ice least', 'restored_wallet');
```
## ✅ Benefits of This Approach
1. **✅ Standard BIP39** - Compatible with hardware wallets, other software
2. **✅ 12-word backup** - Easy to write down, memorize, store
3. **✅ Self-custody** - User controls the mnemonic
4. **✅ Cross-platform** - Restore on any device
5. **✅ Secure** - Industry-standard cryptography
6. **✅ User-friendly** - Matches MetaMask/Trust Wallet UX
7. **✅ Works with RinCoin** - Uses `importprivkey` for each address
## 🚀 Next Steps
1. Implement BIP39/BIP32 support in web wallet
2. Test key derivation with RinCoin addresses
3. Create beautiful UI for mnemonic generation/restore
4. Add encrypted local storage option
5. Implement proper gap limit scanning
6. Add address book and transaction history
## 📚 Required Libraries
```bash
npm install bip39 bip32 tiny-secp256k1 bitcoinjs-lib
```
Or for Python backend:
```bash
pip install mnemonic bip32utils bitcoinlib
```
---
**YES! You CAN create a secure, self-custody RinCoin wallet with easy mnemonic restore!**
The key is implementing BIP39 support in your web wallet layer, not relying on RinCoin's limited RPC support. 🎉

156
rin/wallet/cmd/README.md Normal file
View File

@@ -0,0 +1,156 @@
# RIN Wallet RPC Scripts
This directory contains scripts for interacting with the RIN cryptocurrency wallet via RPC.
## Prerequisites
- `curl` and `jq` must be installed on your system
- RIN wallet/node must be running with RPC enabled
- RPC credentials must be configured
## Scripts
### send_rin.sh
Send RIN to another wallet address.
```bash
./send_rin.sh <recipient_address> <amount> [rpc_user] [rpc_password] [rpc_host] [rpc_port]
```
**Example:**
```bash
./send_rin.sh rin1qm5qg07qh0fy3tgxc3ftfgq0fujgfgm4n6ggt5f 100.0
```
### check_balance.sh
Check wallet balance.
```bash
./check_balance.sh [account] [rpc_user] [rpc_password] [rpc_host] [rpc_port]
```
**Example:**
```bash
./check_balance.sh # Check all accounts
./check_balance.sh myaccount # Check specific account
```
### get_transaction.sh
Get details of a specific transaction.
```bash
./get_transaction.sh <transaction_id> [rpc_user] [rpc_password] [rpc_host] [rpc_port]
```
**Example:**
```bash
./get_transaction.sh a1b2c3d4e5f6...
```
### rpc_call.sh
General-purpose RPC call script for any RIN RPC method.
```bash
./rpc_call.sh <method> [param1] [param2] ... [rpc_user] [rpc_password] [rpc_host] [rpc_port]
```
**Examples:**
```bash
./rpc_call.sh getinfo
./rpc_call.sh getnewaddress myaccount
./rpc_call.sh listtransactions "*" 10
./rpc_call.sh validateaddress rin1qm5qg07qh0fy3tgxc3ftfgq0fujgfgm4n6ggt5f
./rpc_call.sh loadwallet
```
### list_wallets.sh
List all loaded wallets with balance, transaction count, and used addresses.
```bash
./list_wallets.sh
```
### load_wallet.sh
Load a specific wallet by name.
```bash
./load_wallet.sh main
./load_wallet.sh my-wall
```
### set_web_wallet.sh
Set which wallet the web interface should use.
```bash
./set_web_wallet.sh main
```
### find_address.sh
Check if an address belongs to any loaded wallet.
we need to see what is this address.
```bash
./find_address.sh rin1qvj0yyt9phvled9kxflju3p687a4s7kareglpk5
```
### dump_wallet.sh
Create a secure backup of all private keys in a wallet.
```bash
./dump_wallet.sh
```
### restore_wallet.sh
Restore a wallet from a backup dump file.
```bash
./restore_wallet.sh ~/rin_wallet_backups/rin_wallet_backup_20230923.txt
./restore_wallet.sh ~/rin_wallet_backups/rin_wallet_backup_20230923.txt my_restored_wallet
```
### restore_from_seed.sh
Restore a wallet from just the master private key.
```bash
# Auto-generate wallet name
./restore_from_seed.sh "xprv9s21ZrQH143K3bjynHVk6hBTZLmV9wjqWScL3UyENBYK6RaFo75zu5jnWQtBi932zKbD7c2WARWLJNjBbE3Td2Cc44ym3dmp343qKKFXwxS"
# Specify custom wallet name
./restore_from_seed.sh "xprv9s21ZrQH143K3bjynHVk6hBTZLmV9wjqWScL3UyENBYK6RaFo75zu5jnWQtBi932zKbD7c2WARWLJNjBbE3Td2Cc44ym3dmp343qKKFXwxS" my_restored_wallet
```
## Default RPC Configuration
- **Host:** localhost
- **Port:** 8332
- **User:** rinrpc
- **Password:** password
You can override these defaults by providing them as the last arguments to any script.
## Common RPC Methods
- `getbalance [account]` - Get account balance
- `getinfo` - Get wallet info
- `getnewaddress [account]` - Generate new address
- `sendtoaddress <address> <amount>` - Send coins
- `listtransactions [account] [count]` - List transactions
- `gettransaction <txid>` - Get transaction details
- `validateaddress <address>` - Validate address
- `getaddressesbyaccount [account]` - Get addresses for account
## Security Notes
- These scripts send credentials in plain text (HTTP Basic Auth)
- Consider using HTTPS or local connections only
- Update default RPC credentials for production use
- Store scripts securely and avoid hardcoding sensitive information
<!-- get txnL -->
curl -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc": "1.0", "id":"gettx", "method": "gettransaction", "params": ["bcf19926894272e5f6d9a6cceedeac4bff0a2b23c496f660d168ded8fd49a462"]}' \
http://rinrpc:745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90@127.0.0.1:9556/wallet/main

View File

@@ -0,0 +1,260 @@
# RinCoin Wallet Restoration: What Actually Works
## ❌ The Problem: RinCoin Doesn't Support xprv Key Imports
After extensive testing, we've discovered that **RinCoin does NOT support direct xprv extended private key imports**.
### What We Tried (All Failed):
-`sethdseed` with xprv key → "Invalid private key"
-`importprivkey` with xprv key → "Invalid private key encoding"
- ✗ Direct HD seed restoration → Not supported
### Why This Doesn't Work:
RinCoin, despite being based on Litecoin, appears to have limited RPC support for:
- BIP39 mnemonic seed phrases
- Extended private key (xprv) imports
- HD wallet seed restoration via RPC
## ✅ What DOES Work: Full Wallet Dump Restoration
The **ONLY reliable method** is using the complete wallet dump file:
```bash
./restore_wallet.sh /home/db/rin_wallet_backups/rin_wallet_backup_20250929_221522.txt restored_wallet
```
### Why This Works:
- Uses `importwallet` RPC method (widely supported)
- Imports ALL individual private keys from the dump
- Blockchain rescans and finds all transactions
- **Balance is restored: 1059.00155276 RIN ✓**
### The Address Issue:
The restored wallet shows **different addresses** but **same balance**:
- Original: `rin1q...` (SegWit addresses)
- Restored: `R...` (Legacy P2PKH addresses)
**Why?** The wallet dump contains individual private keys without HD structure, so `importwallet` creates a legacy wallet.
## 🎯 Practical Solutions for User-Friendly Wallets
Since direct key import doesn't work, here are realistic approaches:
### Option 1: Wallet File Backup/Restore (BEST)
**For same device or direct file transfer:**
```bash
# Backup
tar -czf ~/rin_wallet_backup.tar.gz /mnt/data/docker_vol/rincoin/rincoin-node/data/main/
# Restore
tar -xzf ~/rin_wallet_backup.tar.gz -C /new/path/data/
```
**Pros:**
- ✅ Perfect restoration - same addresses, same structure
- ✅ Fast - no blockchain rescan needed
- ✅ Works 100% reliably
**Cons:**
- ✗ Requires file transfer (not just a text string)
- ✗ User must handle binary wallet files
### Option 2: Full Dump File Restoration (CURRENT)
**For text-based backup:**
```bash
# Backup
./dump_wallet.sh # Creates text file with all keys
# Restore
./restore_wallet.sh /path/to/backup.txt restored_wallet
```
**Pros:**
- ✅ Text file - easier to store/transfer
- ✅ Works reliably
- ✅ Balance fully restored
**Cons:**
- ✗ Changes address format (rin1q... → R...)
- ✗ Large file (~6000 lines for active wallet)
- ✗ Slower (requires blockchain rescan)
### Option 3: Web Wallet with Server-Side Management (RECOMMENDED)
**For browser extension/web wallet:**
Instead of user managing keys directly, implement server-side wallet management:
```javascript
// User creates wallet
const walletId = await createWallet(username, password);
// Server stores encrypted wallet file
// User restores wallet
const wallet = await restoreWallet(username, password);
// Server loads encrypted wallet file
```
**Pros:**
- ✅ User-friendly - just username/password
- ✅ No key management complexity
- ✅ Perfect restoration every time
- ✅ Can sync across devices
**Cons:**
- ✗ Requires trust in server
- ✗ Need secure server infrastructure
### Option 4: Hybrid Approach (BALANCED)
**Combine wallet file + optional manual backup:**
```javascript
// Primary: Encrypted wallet file stored locally/cloud
saveEncryptedWallet(walletFile, userPassword);
// Secondary: Export full dump for disaster recovery
exportFullDump(); // User downloads text file "just in case"
```
**Pros:**
- ✅ User-friendly primary method (file sync)
- ✅ Manual backup option for advanced users
- ✅ Best of both worlds
## 🔧 Technical Implementation for Web Wallet
### Current Reality Check:
```javascript
// ❌ This WON'T work (RinCoin doesn't support it):
async function restoreFromMnemonic(mnemonic) {
await rpc('sethdseed', [true, mnemonic]); // FAILS
}
// ❌ This WON'T work either:
async function restoreFromXprv(xprv) {
await rpc('importprivkey', [xprv]); // FAILS
}
// ✅ This DOES work:
async function restoreFromDumpFile(dumpFilePath) {
await rpc('createwallet', [walletName, false, false]);
await rpc('importwallet', [dumpFilePath]); // WORKS!
}
```
### Recommended Web Wallet Architecture:
```javascript
class RinWebWallet {
// Primary method: Wallet file management
async backupWallet() {
// Get wallet directory from node
const walletDir = await getWalletDirectory();
// Create encrypted archive
const encrypted = await encryptWalletFiles(walletDir, userPassword);
// Store locally/cloud
await saveBackup(encrypted);
}
async restoreWallet(encryptedBackup, password) {
// Decrypt backup
const walletFiles = await decrypt(encryptedBackup, password);
// Restore to node's wallet directory
await restoreWalletFiles(walletFiles);
// Load wallet
await rpc('loadwallet', [walletName]);
}
// Secondary method: Dump file for advanced users
async exportDumpFile() {
const dumpFile = await rpc('dumpwallet', ['/tmp/backup.txt']);
return downloadFile(dumpFile);
}
async importDumpFile(dumpFilePath) {
await rpc('createwallet', [walletName]);
await rpc('importwallet', [dumpFilePath]);
// Note: Addresses will be different format but balance same
}
}
```
## 📱 User Experience Design
### For Browser Extension:
```
┌─────────────────────────────┐
│ RinCoin Wallet │
├─────────────────────────────┤
│ │
│ [Create New Wallet] │
│ │
│ [Restore from Backup] │
│ ↓ │
│ • Upload wallet file │ ← Primary method
│ • Import dump file │ ← Fallback method
│ │
└─────────────────────────────┘
```
### Backup Flow:
```
1. User clicks "Backup Wallet"
2. Extension prompts for password
3. Creates encrypted wallet file
4. Downloads: "rincoin-wallet-backup-2025-09-29.enc"
5. Shows: "✓ Backup created! Store this file safely."
```
### Restore Flow:
```
1. User clicks "Restore Wallet"
2. User uploads "rincoin-wallet-backup-2025-09-29.enc"
3. Extension prompts for backup password
4. Restores wallet files to node
5. Shows: "✓ Wallet restored! Balance: 1059.00 RIN"
```
## 🎯 Recommendations
### For MVP (Minimum Viable Product):
1. **Use wallet file backup/restore** - Most reliable
2. **Encrypt with user password** - Security
3. **Store locally in browser storage** - Simple start
4. **Add cloud sync later** - Future enhancement
### For Production:
1. **Primary: Encrypted wallet file sync**
2. **Secondary: Optional dump file export**
3. **Security: End-to-end encryption**
4. **UX: Hide complexity from users**
## ⚠️ Important Disclaimers
### For Users:
- The xprv key in your dump file **cannot be used** for quick restoration
- You **must use the full dump file** or wallet directory
- Different restoration methods may show **different addresses** (but same balance)
### For Developers:
- RinCoin's RPC API has **limited HD wallet support**
- Extended key imports (xprv/xpub) are **not supported**
- BIP39 mnemonic restoration is **not available via RPC**
- The only reliable method is **`importwallet` with full dump**
## 🚀 Moving Forward
For a user-friendly RinCoin wallet/browser extension, **don't try to mimic MetaMask's 12-word restore**. Instead:
1. **Accept the limitation**: RinCoin doesn't support simple key restoration
2. **Design around it**: Use wallet file backups
3. **Make it simple**: Hide the complexity with good UX
4. **Be honest**: Tell users they need to backup their wallet file
The technology constraint is real, but good UX design can still make it user-friendly! 🎨

View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Script to check RIN wallet balance
# Usage: ./check_balance.sh [account] [rpc_user] [rpc_password] [rpc_host] [rpc_port]
ACCOUNT=${1:-"*"} # Default to all accounts
RPC_USER=${2:-"rinrpc"}
RPC_PASSWORD=${3:-"745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"}
RPC_HOST=${4:-"localhost"}
RPC_PORT=${5:-"9556"}
# JSON-RPC request for getbalance
RPC_REQUEST='{
"jsonrpc": "2.0",
"id": "getbalance",
"method": "getbalance",
"params": ["'"$ACCOUNT"'"]
}'
echo "Checking RIN wallet balance..."
# Make the RPC call to wallet-specific endpoint
RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d "$RPC_REQUEST" \
"http://$RPC_HOST:$RPC_PORT/wallet/main")
# Check for errors
ERROR=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null)
if [ "$ERROR" != "null" ] && [ -n "$ERROR" ]; then
echo "Error: $(echo "$ERROR" | jq -r '.message')"
exit 1
fi
# Get balance
BALANCE=$(echo "$RESPONSE" | jq -r '.result' 2>/dev/null)
if [ "$BALANCE" != "null" ] && [ -n "$BALANCE" ]; then
echo "Wallet balance: $BALANCE RIN"
else
echo "Unexpected response: $RESPONSE"
exit 1
fi

View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Script to create a new wallet on the RinCoin node
# Usage: ./create_wallet.sh <wallet_name> [rpc_user] [rpc_password] [rpc_host] [rpc_port]
if [ $# -lt 1 ]; then
echo "Usage: $0 <wallet_name> [rpc_user] [rpc_password] [rpc_host] [rpc_port]"
echo "Example: $0 main"
exit 1
fi
WALLET_NAME=$1
RPC_USER=${2:-"rinrpc"}
RPC_PASSWORD=${3:-"745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"}
RPC_HOST=${4:-"localhost"}
RPC_PORT=${5:-"9556"}
# JSON-RPC request to create wallet
RPC_REQUEST='{
"jsonrpc": "2.0",
"id": "createwallet",
"method": "createwallet",
"params": ["'"$WALLET_NAME"'", false, false, "", false, false, true]
}'
echo "Creating wallet '$WALLET_NAME' on RinCoin node..."
# Make the RPC call
RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d "$RPC_REQUEST" \
"http://$RPC_HOST:$RPC_PORT")
# Check for errors
ERROR=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null)
if [ "$ERROR" != "null" ] && [ -n "$ERROR" ]; then
echo "Error: $(echo "$ERROR" | jq -r '.message')"
exit 1
fi
# Get result
RESULT=$(echo "$RESPONSE" | jq -r '.result' 2>/dev/null)
if [ "$RESULT" != "null" ]; then
echo "Wallet '$WALLET_NAME' created successfully!"
echo "Result: $RESULT"
else
echo "Unexpected response: $RESPONSE"
exit 1
fi

View File

@@ -0,0 +1,98 @@
#!/bin/bash
# RinCoin Wallet Backup Script
# Dumps all private keys to a secure text file for backup.
# WARNING: This file contains sensitive private keys. Store it securely (encrypted, offline).
# Do not share or email it. Anyone with this file can spend your coins.
set -eo pipefail
# Configuration
WALLET="main"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Path as seen by the RIN daemon (relative to its /data directory)
DAEMON_BACKUP_FILE="/data/rin_wallet_backup_${TIMESTAMP}.txt"
# Actual system path where the file will be created
SYSTEM_BACKUP_FILE="/mnt/data/docker_vol/rincoin/rincoin-node/data/rin_wallet_backup_${TIMESTAMP}.txt"
# RPC Configuration
RPC_USER="rinrpc"
RPC_PASSWORD="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
RPC_HOST="localhost"
RPC_PORT="9556"
# No need to create directory - daemon writes to its own /data
# Check if RIN node is running
if ! pgrep -f "rincoind" > /dev/null; then
echo "Error: RinCoin daemon is not running. Start it first."
exit 1
fi
# Ensure wallet is loaded
echo "Checking if wallet '${WALLET}' is loaded..."
LIST_WALLETS_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "listwallets", "method": "listwallets", "params": []}' \
"http://$RPC_HOST:$RPC_PORT")
if ! echo "$LIST_WALLETS_RESPONSE" | grep -q '"main"'; then
echo "Wallet '${WALLET}' not loaded. Attempting to load..."
LOAD_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "loadwallet", "method": "loadwallet", "params": ["'$WALLET'"]}' \
"http://$RPC_HOST:$RPC_PORT")
if echo "$LOAD_RESPONSE" | grep -q '"error"'; then
echo "Failed to load wallet. Response: $LOAD_RESPONSE"
exit 1
fi
fi
echo "Dumping wallet to: $DAEMON_BACKUP_FILE"
echo "This may take a moment..."
# Dump wallet using RPC (use daemon's path)
DUMP_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "dumpwallet", "method": "dumpwallet", "params": ["'$DAEMON_BACKUP_FILE'"]}' \
"http://$RPC_HOST:$RPC_PORT/wallet/$WALLET")
# Check for errors in dump response
if echo "$DUMP_RESPONSE" | grep -q '"error":null'; then
echo "✓ Wallet dump successful"
else
echo "Error dumping wallet: $DUMP_RESPONSE"
exit 1
fi
# Verify the file was created and has content (check system path)
if [[ ! -f "$SYSTEM_BACKUP_FILE" ]]; then
echo "Error: Backup file was not created at $SYSTEM_BACKUP_FILE"
exit 1
fi
LINE_COUNT=$(wc -l < "$SYSTEM_BACKUP_FILE")
if [[ $LINE_COUNT -lt 10 ]]; then
echo "Warning: Backup file seems too small (${LINE_COUNT} lines). Check for errors."
exit 1
fi
# Copy backup to user's home directory for easy access
USER_BACKUP_DIR="${HOME}/rin_wallet_backups"
mkdir -p "$USER_BACKUP_DIR"
USER_BACKUP_FILE="${USER_BACKUP_DIR}/rin_wallet_backup_${TIMESTAMP}.txt"
cp "$SYSTEM_BACKUP_FILE" "$USER_BACKUP_FILE"
echo "✅ Wallet successfully backed up to: $USER_BACKUP_FILE"
echo ""
echo "🔐 SECURITY REMINDERS:"
echo " - This file contains private keys for ALL addresses in the wallet."
echo " - Encrypt it immediately: gpg -c $USER_BACKUP_FILE"
echo " - Store on encrypted media (e.g., USB drive in safe)."
echo " - Delete the unencrypted file after encryption."
echo " - Test restoration on a testnet node before relying on it."
echo ""
echo "File size: $(du -h "$USER_BACKUP_FILE" | cut -f1)"
echo "Lines: $LINE_COUNT"

View File

@@ -0,0 +1,74 @@
#!/bin/bash
# Script to find if an address belongs to any loaded wallet
# Usage: ./find_address.sh <address>
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <address>"
echo "Example: $0 rin1qvj0yyt9phvled9kxflju3p687a4s7kareglpk5"
exit 1
fi
ADDRESS="$1"
# RPC Configuration
RPC_USER="rinrpc"
RPC_PASSWORD="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
RPC_HOST="localhost"
RPC_PORT="9556"
echo "Searching for address: $ADDRESS"
echo ""
# First, get list of all loaded wallets
WALLETS_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "listwallets", "method": "listwallets", "params": []}' \
"http://$RPC_HOST:$RPC_PORT")
if echo "$WALLETS_RESPONSE" | grep -q '"error":null'; then
echo "Loaded wallets found. Checking each wallet..."
# Extract wallet names (this is a simple approach, may need refinement)
WALLET_NAMES=$(echo "$WALLETS_RESPONSE" | grep -o '"[^"]*"' | grep -v '"result"' | grep -v '"error"' | grep -v '"id"' | grep -v '"jsonrpc"' | sed 's/"//g')
if [ -z "$WALLET_NAMES" ]; then
echo "No wallets are currently loaded."
echo "Load a wallet first with: ./rin/wallet/cmd/load_main_wallet.sh"
exit 1
fi
FOUND=false
for wallet in $WALLET_NAMES; do
echo "Checking wallet: $wallet"
# Check if address belongs to this wallet
VALIDATE_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "validateaddress", "method": "validateaddress", "params": ["'$ADDRESS'"]}' \
"http://$RPC_HOST:$RPC_PORT/wallet/$wallet")
if echo "$VALIDATE_RESPONSE" | grep -q '"ismine":true'; then
echo "✓ FOUND! Address belongs to wallet: $wallet"
FOUND=true
# Get more details
echo "Address details:"
echo "$VALIDATE_RESPONSE" | grep -E '"isvalid"|"ismine"|"iswatchonly"|"isscript"|"pubkey"|"hdkeypath"'
echo ""
fi
done
if [ "$FOUND" = false ]; then
echo "❌ Address not found in any loaded wallet."
echo ""
echo "The address might be:"
echo "1. In an unloaded wallet"
echo "2. Not belonging to this node"
echo "3. From a different wallet backup"
fi
else
echo "Error getting wallet list: $WALLETS_RESPONSE"
fi

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# Script to get RIN transaction details
# Usage: ./get_transaction.sh <txid> [rpc_user] [rpc_password] [rpc_host] [rpc_port]
if [ $# -lt 1 ]; then
echo "Usage: $0 <transaction_id> [rpc_user] [rpc_password] [rpc_host] [rpc_port]"
echo "Example: $0 a1b2c3d4... user password localhost 8332"
exit 1
fi
TXID=$1
RPC_USER=${2:-"rinrpc"}
RPC_PASSWORD=${3:-"password"}
RPC_HOST=${4:-"localhost"}
RPC_PORT=${5:-"8332"}
# JSON-RPC request
RPC_REQUEST='{
"jsonrpc": "2.0",
"id": "gettransaction",
"method": "gettransaction",
"params": ["'"$TXID"'"]
}'
echo "Getting transaction details for: $TXID"
# Make the RPC call
RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d "$RPC_REQUEST" \
"http://$RPC_HOST:$RPC_PORT")
# Check for errors
ERROR=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null)
if [ "$ERROR" != "null" ] && [ -n "$ERROR" ]; then
echo "Error: $(echo "$ERROR" | jq -r '.message')"
exit 1
fi
# Display transaction details
RESULT=$(echo "$RESPONSE" | jq -r '.result' 2>/dev/null)
if [ "$RESULT" != "null" ]; then
echo "Transaction Details:"
echo "$RESPONSE" | jq '.result'
else
echo "Unexpected response: $RESPONSE"
exit 1
fi

View File

@@ -0,0 +1,98 @@
#!/bin/bash
# Script to list all loaded wallets on the RinCoin node
# Usage: ./list_wallets.sh [rpc_user] [rpc_password] [rpc_host] [rpc_port]
RPC_USER=${1:-"rinrpc"}
RPC_PASSWORD=${2:-"745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"}
RPC_HOST=${3:-"localhost"}
RPC_PORT=${4:-"9556"}
# JSON-RPC request to list wallets
RPC_REQUEST='{
"jsonrpc": "2.0",
"id": "listwallets",
"method": "listwallets",
"params": []
}'
echo "Listing loaded wallets on RinCoin node..."
# Make the RPC call
RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d "$RPC_REQUEST" \
"http://$RPC_HOST:$RPC_PORT")
# Show raw response for debugging
echo "Raw response: $RESPONSE"
echo ""
# Check for errors (without jq)
if echo "$RESPONSE" | grep -q '"error":null'; then
echo "No errors in response"
# Extract wallet names from the result array
# Look for pattern: "result":["wallet1","wallet2"]
WALLET_SECTION=$(echo "$RESPONSE" | grep -o '"result":\[[^]]*\]')
if [ -n "$WALLET_SECTION" ]; then
# Extract wallet names between quotes
WALLETS=$(echo "$WALLET_SECTION" | grep -o '"[^"]*"' | grep -v '"result"' | sed 's/"//g')
if [ -n "$WALLETS" ]; then
echo "Loaded wallets:"
echo "$WALLETS" | while read -r wallet; do
echo " - $wallet"
# Get wallet info
WALLET_INFO=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "getwalletinfo", "method": "getwalletinfo", "params": []}' \
"http://$RPC_HOST:$RPC_PORT/wallet/$wallet")
if echo "$WALLET_INFO" | grep -q '"error":null'; then
# Extract balance
BALANCE=$(echo "$WALLET_INFO" | grep -o '"balance":[0-9.]*' | cut -d: -f2)
# Extract address count
ADDRESS_COUNT=$(echo "$WALLET_INFO" | grep -o '"txcount":[0-9]*' | cut -d: -f2)
echo " Balance: ${BALANCE:-0} RIN"
echo " Transactions: ${ADDRESS_COUNT:-0}"
# Get used addresses
echo " Used addresses:"
ADDRESSES_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "listreceivedbyaddress", "method": "listreceivedbyaddress", "params": [0, true, true]}' \
"http://$RPC_HOST:$RPC_PORT/wallet/$wallet")
if echo "$ADDRESSES_RESPONSE" | grep -q '"error":null'; then
# Extract addresses from the response
ADDRESSES=$(echo "$ADDRESSES_RESPONSE" | grep -o '"address":"[^"]*"' | cut -d'"' -f4 | head -5)
if [ -n "$ADDRESSES" ]; then
echo "$ADDRESSES" | while read -r addr; do
if [ -n "$addr" ]; then
echo " $addr"
fi
done
else
echo " (No used addresses found)"
fi
else
echo " (Could not retrieve addresses - wallet may be empty)"
fi
fi
echo ""
done
else
echo "No wallets are currently loaded."
fi
else
echo "No wallet result found in response."
fi
else
echo "Error in response: $RESPONSE"
fi

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Script to load the main wallet
# Usage: ./load_main_wallet.sh
RPC_USER="rinrpc"
RPC_PASSWORD="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
RPC_HOST="localhost"
RPC_PORT="9556"
echo "Loading main wallet..."
LOAD_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "loadwallet", "method": "loadwallet", "params": ["main"]}' \
"http://$RPC_HOST:$RPC_PORT")
echo "Response: $LOAD_RESPONSE"
if echo "$LOAD_RESPONSE" | grep -q '"error":null'; then
echo "✓ Main wallet loaded successfully"
elif echo "$LOAD_RESPONSE" | grep -q "already loaded"; then
echo "✓ Main wallet is already loaded"
else
echo "Failed to load main wallet"
fi

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# Script to load a specific wallet by name
# Usage: ./load_wallet.sh <wallet_name>
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <wallet_name>"
echo "Example: $0 main"
echo "Example: $0 my-wall"
exit 1
fi
WALLET_NAME="$1"
# RPC Configuration
RPC_USER="rinrpc"
RPC_PASSWORD="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
RPC_HOST="localhost"
RPC_PORT="9556"
echo "Loading wallet: $WALLET_NAME"
LOAD_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "loadwallet", "method": "loadwallet", "params": ["'$WALLET_NAME'"]}' \
"http://$RPC_HOST:$RPC_PORT")
echo "Response: $LOAD_RESPONSE"
if echo "$LOAD_RESPONSE" | grep -q '"error":null'; then
echo "✓ Wallet '$WALLET_NAME' loaded successfully"
# Show balance
BALANCE_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "getbalance", "method": "getbalance", "params": []}' \
"http://$RPC_HOST:$RPC_PORT/wallet/$WALLET_NAME")
if echo "$BALANCE_RESPONSE" | grep -q '"error":null'; then
BALANCE=$(echo "$BALANCE_RESPONSE" | grep -o '"result":[0-9.]*' | cut -d: -f2)
echo "Balance: $BALANCE RIN"
fi
elif echo "$LOAD_RESPONSE" | grep -q "already loaded"; then
echo "✓ Wallet '$WALLET_NAME' is already loaded"
else
echo "❌ Failed to load wallet '$WALLET_NAME'"
echo "Make sure the wallet exists in the data directory"
fi

View File

@@ -0,0 +1,179 @@
#!/bin/bash
# RinCoin Wallet File Restoration Script
# This is the RECOMMENDED restoration method for user-friendly wallets!
# Restores a wallet from a complete wallet directory backup
#
# This preserves:
# - All addresses (EXACT format - rin1q...)
# - HD wallet structure
# - Transaction history
# - All wallet metadata
set -euo pipefail
if [[ $# -lt 1 ]] || [[ $# -gt 2 ]]; then
echo "Usage: $0 <backup_file.tar.gz> [new_wallet_name]"
echo ""
echo "Examples:"
echo " $0 ~/rin_wallet_file_backup_main_20250929.tar.gz"
echo " $0 ~/rin_wallet_file_backup_main_20250929.tar.gz restored_main"
echo ""
echo "This restores a wallet from a complete wallet directory backup."
echo "The wallet will have IDENTICAL addresses to the original."
exit 1
fi
BACKUP_FILE="$1"
NEW_WALLET_NAME="${2:-}"
# Wallet paths
DATA_DIR="/mnt/data/docker_vol/rincoin/rincoin-node/data"
# RPC Configuration
RPC_USER="rinrpc"
RPC_PASSWORD="745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90"
RPC_HOST="localhost"
RPC_PORT="9556"
echo "RinCoin Wallet File Restoration"
echo "================================"
echo "Backup file: $BACKUP_FILE"
echo ""
# Check if backup file exists
if [[ ! -f "$BACKUP_FILE" ]]; then
echo "Error: Backup file not found: $BACKUP_FILE"
exit 1
fi
# Check if RIN node is running
if ! pgrep -f "rincoind" > /dev/null; then
echo "Error: RinCoin daemon is not running. Start it first."
exit 1
fi
# Extract wallet name from archive
echo "Examining backup file..."
ORIGINAL_WALLET_NAME=$(tar -tzf "$BACKUP_FILE" | head -1 | cut -d/ -f1)
if [[ -z "$ORIGINAL_WALLET_NAME" ]]; then
echo "Error: Could not determine wallet name from backup file"
exit 1
fi
echo "Original wallet name: $ORIGINAL_WALLET_NAME"
# Determine target wallet name
if [[ -z "$NEW_WALLET_NAME" ]]; then
NEW_WALLET_NAME="$ORIGINAL_WALLET_NAME"
echo "Target wallet name: $NEW_WALLET_NAME (same as original)"
else
echo "Target wallet name: $NEW_WALLET_NAME (renamed)"
fi
TARGET_WALLET_DIR="$DATA_DIR/$NEW_WALLET_NAME"
# Check if target already exists
if [[ -d "$TARGET_WALLET_DIR" ]]; then
echo ""
echo "Warning: Wallet '$NEW_WALLET_NAME' already exists at: $TARGET_WALLET_DIR"
read -p "Do you want to overwrite it? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "Restoration cancelled."
exit 1
fi
# Try to unload if loaded
echo "Unloading existing wallet..."
curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "unloadwallet", "method": "unloadwallet", "params": ["'$NEW_WALLET_NAME'"]}' \
"http://$RPC_HOST:$RPC_PORT" > /dev/null 2>&1
# Remove existing
echo "Removing existing wallet directory..."
rm -rf "$TARGET_WALLET_DIR"
fi
# Extract backup
echo ""
echo "Extracting backup..."
cd "$DATA_DIR"
tar -xzf "$BACKUP_FILE"
# Rename if necessary
if [[ "$ORIGINAL_WALLET_NAME" != "$NEW_WALLET_NAME" ]]; then
mv "$ORIGINAL_WALLET_NAME" "$NEW_WALLET_NAME"
echo "✓ Wallet renamed from '$ORIGINAL_WALLET_NAME' to '$NEW_WALLET_NAME'"
fi
# Verify extraction
if [[ ! -d "$TARGET_WALLET_DIR" ]]; then
echo "❌ Error: Wallet directory was not created"
exit 1
fi
echo "✓ Wallet files extracted successfully"
# Load wallet
echo ""
echo "Loading wallet into RinCoin node..."
LOAD_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "loadwallet", "method": "loadwallet", "params": ["'$NEW_WALLET_NAME'"]}' \
"http://$RPC_HOST:$RPC_PORT")
if echo "$LOAD_RESPONSE" | grep -q '"error":null'; then
echo "✓ Wallet loaded successfully"
else
echo "Error loading wallet: $LOAD_RESPONSE"
echo ""
echo "The wallet files are restored, but RinCoin couldn't load them."
echo "You may need to restart the RinCoin daemon."
exit 1
fi
# Get wallet info
echo ""
echo "Verifying wallet..."
BALANCE_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "getbalance", "method": "getbalance", "params": []}' \
"http://$RPC_HOST:$RPC_PORT/wallet/$NEW_WALLET_NAME")
BALANCE=$(echo "$BALANCE_RESPONSE" | grep -o '"result":[0-9.]*' | cut -d: -f2)
echo "Current balance: $BALANCE RIN"
echo ""
# Show addresses for verification
echo "Restored addresses:"
ADDRESSES_RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": "listreceivedbyaddress", "method": "listreceivedbyaddress", "params": [0, true, true]}' \
"http://$RPC_HOST:$RPC_PORT/wallet/$NEW_WALLET_NAME")
echo "$ADDRESSES_RESPONSE" | grep -o '"address":"[^"]*"' | cut -d'"' -f4 | head -5
echo ""
echo "✅ Wallet restored successfully!"
echo ""
echo "📊 Restoration Summary:"
echo " Wallet name: $NEW_WALLET_NAME"
echo " Balance: $BALANCE RIN"
echo " Status: Ready to use"
echo ""
echo "💡 IMPORTANT:"
echo " - All addresses are IDENTICAL to the original wallet"
echo " - Transaction history is fully preserved"
echo " - No blockchain rescan needed"
echo " - This is the PERFECT restoration method!"
echo ""
echo "🔧 Commands:"
echo " Check balance:"
echo " curl -s -u \"$RPC_USER:$RPC_PASSWORD\" -d '{\"jsonrpc\": \"2.0\", \"id\": \"getbalance\", \"method\": \"getbalance\"}' \"http://$RPC_HOST:$RPC_PORT/wallet/$NEW_WALLET_NAME\""
echo ""
echo " List wallets:"
echo " ./list_wallets.sh"

View File

@@ -0,0 +1,88 @@
#!/bin/bash
# General script for RIN RPC calls
# Usage: ./rpc_call.sh <method> [param1] [param2] ... [rpc_user] [rpc_password] [rpc_host] [rpc_port]
if [ $# -lt 1 ]; then
echo "Usage: $0 <method> [param1] [param2] ... [rpc_user] [rpc_password] [rpc_host] [rpc_port]"
echo "Examples:"
echo " $0 getinfo"
echo " $0 getnewaddress myaccount"
echo " $0 listtransactions \"*\" 10"
echo " $0 gettransaction txid"
exit 1
fi
METHOD=$1
shift
# Parse parameters - last 4 optional args are RPC connection details
PARAMS=()
RPC_USER="rinrpc"
RPC_PASSWORD="password"
RPC_HOST="localhost"
RPC_PORT="8332"
# Check if last 4 args are RPC connection details
if [ $# -ge 4 ]; then
# Assume last 4 are RPC details
RPC_PORT="${@: -1}"
RPC_HOST="${@: -2}"
RPC_PASSWORD="${@: -3}"
RPC_USER="${@: -4}"
PARAMS=("${@:1:$#-4}")
else
PARAMS=("$@")
fi
# Build JSON parameters array
PARAMS_JSON=""
if [ ${#PARAMS[@]} -gt 0 ]; then
PARAMS_JSON="["
for param in "${PARAMS[@]}"; do
# Try to parse as number, otherwise treat as string
if [[ $param =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
PARAMS_JSON="$PARAMS_JSON$param,"
else
PARAMS_JSON="$PARAMS_JSON\"$param\","
fi
done
PARAMS_JSON="${PARAMS_JSON%,}]"
else
PARAMS_JSON="[]"
fi
# JSON-RPC request
RPC_REQUEST='{
"jsonrpc": "2.0",
"id": "'"$METHOD"'",
"method": "'"$METHOD"'",
"params": '"$PARAMS_JSON"'
}'
echo "Calling RPC method: $METHOD"
echo "Parameters: $PARAMS_JSON"
# Make the RPC call
RESPONSE=$(curl -s -u "$RPC_USER:$RPC_PASSWORD" \
-H "Content-Type: application/json" \
-d "$RPC_REQUEST" \
"http://$RPC_HOST:$RPC_PORT")
# Check for errors
ERROR=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null)
if [ "$ERROR" != "null" ] && [ -n "$ERROR" ]; then
echo "Error: $(echo "$ERROR" | jq -r '.message')"
exit 1
fi
# Display result
RESULT=$(echo "$RESPONSE" | jq -r '.result' 2>/dev/null)
if [ "$RESULT" != "null" ]; then
echo "Result:"
echo "$RESPONSE" | jq '.result'
else
echo "Response:"
echo "$RESPONSE"
fi

View File

@@ -0,0 +1,55 @@
#!/bin/bash
set -euo pipefail
if [[ ${1-} == "" ]]; then
echo "Usage: $0 <destination_address> [amount]"
echo "Amount defaults to 1 RIN if not specified."
exit 1
fi
ADDRESS="$1"
AMOUNT="${2-1}"
WALLET="main"
CONTAINER="rincoin-node"
CLI_CMD=(sudo docker exec "$CONTAINER" rincoin-cli -datadir=/data -conf=/data/rincoin.conf -rpcwallet="$WALLET")
echo "Checking RinCoin node container..."
if ! sudo docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
echo "Error: container ${CONTAINER} is not running. Start it with 'sudo docker start ${CONTAINER}'."
exit 1
fi
echo "Ensuring wallet '${WALLET}' is loaded..."
if ! "${CLI_CMD[@]//-rpcwallet=$WALLET/}" listwallets | grep -q '"main"'; then
echo "Wallet ${WALLET} not loaded, attempting to load..."
"${CLI_CMD[@]//-rpcwallet=$WALLET/}" loadwallet "$WALLET" >/dev/null
fi
echo "Checking available balance..."
BALANCE_RAW=$("${CLI_CMD[@]}" getbalance)
BALANCE=$(printf '%.8f' "$BALANCE_RAW")
if [[ $(bc <<< "$BALANCE_RAW < $AMOUNT") -eq 1 ]]; then
echo "Error: insufficient balance. Available ${BALANCE} RIN, but ${AMOUNT} RIN requested."
exit 1
fi
echo "Broadcasting transaction..."
set +e
TX_OUTPUT=$("${CLI_CMD[@]}" sendtoaddress "$ADDRESS" "$AMOUNT" '' '' false true)
STATUS=$?
set -e
if [[ $STATUS -ne 0 ]]; then
echo "Failed to send transaction."
if [[ $STATUS -eq 4 ]]; then
echo "Wallet appears to be locked. Unlock it with 'sudo docker exec ${CONTAINER} rincoin-cli -datadir=/data -conf=/data/rincoin.conf -rpcwallet=${WALLET} walletpassphrase <passphrase> 600 true' and rerun."
fi
exit $STATUS
fi
echo "Transaction broadcast. TXID: ${TX_OUTPUT}"
echo "Verify with: sudo docker exec ${CONTAINER} rincoin-cli -datadir=/data -conf=/data/rincoin.conf gettransaction ${TX_OUTPUT}"

View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Script to set which wallet the web interface uses
# Usage: ./set_web_wallet.sh <wallet_name>
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <wallet_name>"
echo "Example: $0 main"
echo "Example: $0 my-wall"
echo ""
echo "This sets the RIN_WALLET_NAME environment variable for the web wallet"
exit 1
fi
WALLET_NAME="$1"
echo "Setting web wallet to use: $WALLET_NAME"
echo ""
echo "To use this wallet with the web interface:"
echo "1. Stop the current web wallet (Ctrl+C)"
echo "2. Start it with:"
echo " RIN_WALLET_NAME=$WALLET_NAME ./rin/wallet/web/start_web_wallet.sh"
echo ""
echo "Or set it permanently in your environment:"
echo " export RIN_WALLET_NAME=$WALLET_NAME"
echo " ./rin/wallet/web/start_web_wallet.sh"
echo ""
echo "Current web wallet configuration:"
echo " Default wallet: main"
echo " New wallet: $WALLET_NAME"

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)
WEB_WALLET_DIR="${SCRIPT_DIR}/web_wallet"
bash /mnt/shared/DEV/repos/d-popov.com/scripts/MINE/rin/send_rin.sh "$@"

View File

@@ -0,0 +1,2 @@
Flask==2.3.3
python-bitcoinrpc==1.0

242
rin/wallet/web/server.py Normal file
View File

@@ -0,0 +1,242 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import secrets
from functools import wraps
from flask import Flask, jsonify, request, send_from_directory
try:
from bitcoinrpc.authproxy import AuthServiceProxy
except ImportError: # pragma: no cover
raise SystemExit("Missing python-bitcoinrpc. Install with: pip install python-bitcoinrpc")
RIN_RPC_HOST = os.environ.get("RIN_RPC_HOST", "127.0.0.1")
RIN_RPC_PORT = int(os.environ.get("RIN_RPC_PORT", "9556"))
RIN_RPC_USER = os.environ.get("RIN_RPC_USER", "rinrpc")
RIN_RPC_PASSWORD = os.environ.get("RIN_RPC_PASSWORD", "745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90")
RIN_WALLET_NAME = os.environ.get("RIN_WALLET_NAME", "main")
API_TOKEN = os.environ.get("RIN_WEB_WALLET_TOKEN")
if not API_TOKEN:
API_TOKEN = secrets.token_urlsafe(32)
print("[web-wallet] No RIN_WEB_WALLET_TOKEN provided. Generated one for this session.")
print(f"[web-wallet] API token: {API_TOKEN}")
def create_base_rpc_client() -> AuthServiceProxy:
url = f"http://{RIN_RPC_USER}:{RIN_RPC_PASSWORD}@{RIN_RPC_HOST}:{RIN_RPC_PORT}"
return AuthServiceProxy(url, timeout=15)
def ensure_wallet_loaded():
"""Ensure the wallet is loaded at startup."""
try:
base_rpc = create_base_rpc_client()
base_rpc.loadwallet(RIN_WALLET_NAME)
print(f"[web-wallet] Loaded wallet: {RIN_WALLET_NAME}")
except Exception as exc:
print(f"[web-wallet] Warning: Could not load wallet {RIN_WALLET_NAME}: {exc}")
# Try to create if loading failed
try:
base_rpc = create_base_rpc_client()
base_rpc.createwallet(RIN_WALLET_NAME, False, True, "", False, True, True)
print(f"[web-wallet] Created and loaded wallet: {RIN_WALLET_NAME}")
except Exception as create_exc:
print(f"[web-wallet] Warning: Could not create wallet {RIN_WALLET_NAME}: {create_exc}")
def create_rpc_client() -> AuthServiceProxy:
url = f"http://{RIN_RPC_USER}:{RIN_RPC_PASSWORD}@{RIN_RPC_HOST}:{RIN_RPC_PORT}/wallet/{RIN_WALLET_NAME}"
return AuthServiceProxy(url, timeout=15)
def require_token(view_func):
@wraps(view_func)
def wrapper(*args, **kwargs):
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return jsonify({"error": "missing_token"}), 401
token = header.split(" ", 1)[1]
if token != API_TOKEN:
return jsonify({"error": "invalid_token"}), 403
return view_func(*args, **kwargs)
return wrapper
app = Flask(__name__, static_folder="static", static_url_path="")
def rpc_call(method: str, *params):
try:
rpc = create_rpc_client()
return rpc.__getattr__(method)(*params)
except Exception as exc: # noqa: BLE001
raise RuntimeError(str(exc)) from exc
@app.route("/api/session", methods=["GET"])
def session_info():
return jsonify({
"wallet": RIN_WALLET_NAME,
"host": RIN_RPC_HOST,
"token": API_TOKEN,
})
@app.route("/api/address", methods=["POST"])
@require_token
def create_address():
data = request.get_json(silent=True) or {}
label = data.get("label", "")
try:
address = rpc_call("getnewaddress", label)
return jsonify({"address": address})
except RuntimeError as exc:
return jsonify({"error": str(exc)}), 500
@app.route("/api/balance", methods=["GET"])
@require_token
def get_balance():
try:
info = rpc_call("getwalletinfo")
confirmed = info.get("balance", 0)
unconfirmed = info.get("unconfirmed_balance", 0)
immature = info.get("immature_balance", 0)
total = confirmed + unconfirmed + immature
return jsonify({
"confirmed": confirmed,
"unconfirmed": unconfirmed,
"immature": immature,
"total": total,
})
except RuntimeError as exc:
return jsonify({"error": str(exc)}), 500
@app.route("/api/send", methods=["POST"])
@require_token
def send():
payload = request.get_json(force=True)
address = payload.get("address")
amount = payload.get("amount")
subtract_fee = bool(payload.get("subtractFee", True))
if not address or not isinstance(amount, (int, float)):
return jsonify({"error": "invalid_request"}), 400
if amount <= 0:
return jsonify({"error": "amount_must_be_positive"}), 400
try:
txid = rpc_call("sendtoaddress", address, float(amount), "", "", subtract_fee)
return jsonify({"txid": txid})
except RuntimeError as exc:
status = 400 if "Invalid RinCoin address" in str(exc) else 500
return jsonify({"error": str(exc)}), status
@app.route("/api/transactions", methods=["GET"])
@require_token
def list_transactions():
count = int(request.args.get("count", 10))
try:
txs = rpc_call("listtransactions", "*", count, 0, True)
return jsonify({"transactions": txs})
except RuntimeError as exc:
return jsonify({"error": str(exc)}), 500
@app.route("/api/network", methods=["GET"])
@require_token
def get_network_info():
try:
network_info = rpc_call("getnetworkinfo")
peer_info = rpc_call("getpeerinfo")
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": peers,
"mempool_size": len(mempool),
"blockchain": {
"blocks": blockchain_info.get("blocks", 0),
"headers": blockchain_info.get("headers", 0),
"difficulty": blockchain_info.get("difficulty", 0),
"chain": blockchain_info.get("chain", "unknown"),
}
})
except RuntimeError as exc:
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")
@app.route("/<path:path>")
def static_proxy(path):
return send_from_directory(app.static_folder, path)
def main():
ensure_wallet_loaded()
port = int(os.environ.get("RIN_WEB_WALLET_PORT", "8787"))
app.run(host="127.0.0.1", port=port, debug=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,45 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)
CONTAINER="rincoin-node2"
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
echo "Error: ${CONTAINER} container is not running. Start it with 'docker start ${CONTAINER}'."
exit 1
fi
# Check prerequisites
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: python3 is required but not installed."
exit 1
fi
if ! python3 -c "import venv" 2>/dev/null; then
echo "Error: python3-venv is required. Install with 'sudo apt install python3-venv' (Ubuntu/Debian) or equivalent."
exit 1
fi
# Setup virtual environment
VENV_DIR="${SCRIPT_DIR}/venv"
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
python3 -m venv "$VENV_DIR"
fi
# Activate virtual environment
echo "Activating virtual environment..."
source "$VENV_DIR/bin/activate"
# Install/update dependencies
echo "Installing/updating dependencies..."
pip install --quiet -r "${SCRIPT_DIR}/requirements.txt"
echo "Starting RinCoin web wallet on http://127.0.0.1:8787"
export FLASK_APP="${SCRIPT_DIR}/server.py"
export FLASK_ENV=production
export PYTHONPATH="${SCRIPT_DIR}"
flask run --host 127.0.0.1 --port 8787
# Note: To clean up the virtual environment, run: rm -rf "${SCRIPT_DIR}/venv"

View File

@@ -0,0 +1,535 @@
<!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%;
}
.status-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-pending {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
}
.status-confirmed {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
}
.status-immature {
background: rgba(255, 152, 0, 0.2);
color: #ff9800;
}
.tx-link {
color: #29b6f6;
text-decoration: none;
word-break: break-all;
}
.tx-link:hover {
text-decoration: underline;
}
@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>
<div class="card">
<h2>Network Status</h2>
<div id="networkInfo">
<div class="muted">Loading network information...</div>
</div>
</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');
const networkInfo = document.getElementById('networkInfo');
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 refreshToken() {
const res = await fetch('/api/session');
const data = await res.json();
apiToken = data.token;
localStorage.setItem('rinWebWalletToken', apiToken);
tokenDisplay.textContent = `Wallet: ${data.wallet}`;
}
async function fetchBalances() {
let res = await fetch('/api/balance', { 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/balance', { headers: authHeaders() });
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 = 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';
// 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) {
status = `Immature (${confirmations}/20)`;
statusClass = 'status-immature';
tooltip = `title="${confirmations} confirmations - needs 20 for full confirmation"`;
} else {
status = 'Confirmed';
statusClass = 'status-confirmed';
tooltip = `title="${confirmations} confirmations - fully confirmed"`;
}
li.innerHTML = `
<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>
${ageInfo}
<div class="muted"><a href="https://explorer.rin.so/tx/${tx.txid}" target="_blank" class="tx-link">${tx.txid}</a></div>
`;
return li;
}
async function fetchTransactions() {
let res = await fetch('/api/transactions', { 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/transactions', { headers: authHeaders() });
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;
}
// Sort transactions by time (newest first)
data.transactions.sort((a, b) => (b.time || 0) - (a.time || 0));
data.transactions.forEach((tx) => txList.appendChild(renderTx(tx)));
}
async function generateAddress() {
let res = await fetch('/api/address', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({}),
});
let data = await res.json();
// If token is invalid, refresh and retry
if (data.error === 'invalid_token') {
await refreshToken();
res = await fetch('/api/address', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({}),
});
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;
}
let res = await fetch('/api/send', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ address, amount, subtractFee: true }),
});
let data = await res.json();
// If token is invalid, refresh and retry
if (data.error === 'invalid_token') {
await refreshToken();
res = await fetch('/api/send', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ address, amount, subtractFee: true }),
});
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 fetchNetworkInfo() {
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 (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>`;
}
}
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);
}
}
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);
(async () => {
await fetchSession();
// Ensure we have a token before making authenticated requests
if (apiToken) {
await refresh();
} else {
console.error('No API token available');
}
})();
</script>
</body>
</html>