243 lines
7.9 KiB
Python
243 lines
7.9 KiB
Python
#!/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()
|