|
|
|
|
@ -125,13 +125,245 @@ def _health_payload(app):
|
|
|
|
|
return payload, http_code |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wallet_balance_format(raw_value, decimals): |
|
|
|
|
try: |
|
|
|
|
from decimal import Decimal, getcontext |
|
|
|
|
getcontext().prec = 60 |
|
|
|
|
value = Decimal(int(raw_value)) / (Decimal(10) ** int(decimals)) |
|
|
|
|
fixed = f"{value:.8f}".rstrip("0").rstrip(".") |
|
|
|
|
return fixed if fixed else "0" |
|
|
|
|
except Exception: |
|
|
|
|
return "error" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _split_rpc_urls(value, defaults): |
|
|
|
|
urls = [] |
|
|
|
|
|
|
|
|
|
if value: |
|
|
|
|
for item in str(value).replace(",", " ").split(): |
|
|
|
|
item = item.strip() |
|
|
|
|
if item: |
|
|
|
|
urls.append(item) |
|
|
|
|
|
|
|
|
|
for item in defaults: |
|
|
|
|
if item and item not in urls: |
|
|
|
|
urls.append(item) |
|
|
|
|
|
|
|
|
|
return urls |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wallet_rpc_call_one(rpc_url, method, params, timeout=5): |
|
|
|
|
import json |
|
|
|
|
import urllib.request |
|
|
|
|
|
|
|
|
|
payload = json.dumps({ |
|
|
|
|
"jsonrpc": "2.0", |
|
|
|
|
"id": 1, |
|
|
|
|
"method": method, |
|
|
|
|
"params": params, |
|
|
|
|
}).encode("utf-8") |
|
|
|
|
|
|
|
|
|
req = urllib.request.Request( |
|
|
|
|
rpc_url, |
|
|
|
|
data=payload, |
|
|
|
|
headers={ |
|
|
|
|
"Content-Type": "application/json", |
|
|
|
|
"Accept": "application/json", |
|
|
|
|
"User-Agent": "OTB-Billing-Health/1.0", |
|
|
|
|
}, |
|
|
|
|
method="POST", |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp: |
|
|
|
|
data = json.loads(resp.read().decode("utf-8")) |
|
|
|
|
|
|
|
|
|
if data.get("error"): |
|
|
|
|
raise RuntimeError(str(data["error"])) |
|
|
|
|
|
|
|
|
|
return data.get("result") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wallet_rpc_call(rpc_urls, method, params, timeout=5): |
|
|
|
|
last_error = None |
|
|
|
|
|
|
|
|
|
if isinstance(rpc_urls, str): |
|
|
|
|
rpc_urls = [rpc_urls] |
|
|
|
|
|
|
|
|
|
for rpc_url in rpc_urls: |
|
|
|
|
try: |
|
|
|
|
return _wallet_rpc_call_one(rpc_url, method, params, timeout=timeout) |
|
|
|
|
except Exception as exc: |
|
|
|
|
last_error = f"{rpc_url}: {exc}" |
|
|
|
|
|
|
|
|
|
raise RuntimeError(last_error or "No RPC URLs available") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wallet_native_balance(rpc_urls, wallet_address, decimals=18): |
|
|
|
|
result = _wallet_rpc_call(rpc_urls, "eth_getBalance", [wallet_address, "latest"]) |
|
|
|
|
return _wallet_balance_format(int(result or "0x0", 16), decimals) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wallet_erc20_balance(rpc_urls, token_contract, wallet_address, decimals): |
|
|
|
|
# ERC20 balanceOf(address) selector: 0x70a08231 |
|
|
|
|
clean_addr = wallet_address.lower().replace("0x", "").rjust(64, "0") |
|
|
|
|
data = "0x70a08231" + clean_addr |
|
|
|
|
result = _wallet_rpc_call( |
|
|
|
|
rpc_urls, |
|
|
|
|
"eth_call", |
|
|
|
|
[{"to": token_contract, "data": data}, "latest"], |
|
|
|
|
) |
|
|
|
|
return _wallet_balance_format(int(result or "0x0", 16), decimals) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wallet_balances(wallet_address=None, label=None): |
|
|
|
|
import os |
|
|
|
|
|
|
|
|
|
wallet = wallet_address or os.environ.get( |
|
|
|
|
"OTB_OPERATIONS_WALLET", |
|
|
|
|
"0x44f6c44C42e6ae0392E7289F032384C0d37F56D5", |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
arbitrum_rpcs = _split_rpc_urls( |
|
|
|
|
os.environ.get("RPC_ARBITRUM_URLS") or os.environ.get("RPC_ARBITRUM_URL"), |
|
|
|
|
[ |
|
|
|
|
"https://arb1.arbitrum.io/rpc", |
|
|
|
|
"https://arbitrum-one-rpc.publicnode.com", |
|
|
|
|
"https://arbitrum.drpc.org", |
|
|
|
|
], |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
ethereum_rpcs = _split_rpc_urls( |
|
|
|
|
os.environ.get("RPC_ETHEREUM_URLS") or os.environ.get("RPC_ETHEREUM_URL"), |
|
|
|
|
[ |
|
|
|
|
"https://ethereum-rpc.publicnode.com", |
|
|
|
|
"https://cloudflare-eth.com", |
|
|
|
|
"https://eth.llamarpc.com", |
|
|
|
|
], |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
etho_rpcs = _split_rpc_urls( |
|
|
|
|
os.environ.get("RPC_ETHO_URLS") or os.environ.get("RPC_ETHO_URL"), |
|
|
|
|
[ |
|
|
|
|
"https://rpc.ethoprotocol.com", |
|
|
|
|
"https://rpc4.ethoprotocol.com", |
|
|
|
|
], |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
etica_rpcs = _split_rpc_urls( |
|
|
|
|
os.environ.get("RPC_ETICA_URLS") or os.environ.get("RPC_ETICA_URL"), |
|
|
|
|
[ |
|
|
|
|
"https://rpc.etica-stats.org", |
|
|
|
|
"https://eticamainnet.eticaprotocol.org", |
|
|
|
|
], |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
assets = [ |
|
|
|
|
{ |
|
|
|
|
"coin": "USDC", |
|
|
|
|
"chain": "Arbitrum", |
|
|
|
|
"kind": "erc20", |
|
|
|
|
"decimals": 6, |
|
|
|
|
"rpc": arbitrum_rpcs, |
|
|
|
|
"token": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
"coin": "ETH", |
|
|
|
|
"chain": "Ethereum", |
|
|
|
|
"kind": "native", |
|
|
|
|
"decimals": 18, |
|
|
|
|
"rpc": ethereum_rpcs, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
"coin": "ETHO", |
|
|
|
|
"chain": "Etho", |
|
|
|
|
"kind": "native", |
|
|
|
|
"decimals": 18, |
|
|
|
|
"rpc": etho_rpcs, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
"coin": "EGAZ", |
|
|
|
|
"chain": "Etica", |
|
|
|
|
"kind": "native", |
|
|
|
|
"decimals": 18, |
|
|
|
|
"rpc": etica_rpcs, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
"coin": "ETI", |
|
|
|
|
"chain": "Etica", |
|
|
|
|
"kind": "erc20", |
|
|
|
|
"decimals": 18, |
|
|
|
|
"rpc": etica_rpcs, |
|
|
|
|
"token": "0x34c61EA91bAcdA647269d4e310A86b875c09946f", |
|
|
|
|
}, |
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
rows = [] |
|
|
|
|
for asset in assets: |
|
|
|
|
row = { |
|
|
|
|
"coin": asset["coin"], |
|
|
|
|
"chain": asset["chain"], |
|
|
|
|
"balance": "error", |
|
|
|
|
"ok": False, |
|
|
|
|
"error": None, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
if asset["kind"] == "native": |
|
|
|
|
row["balance"] = _wallet_native_balance( |
|
|
|
|
asset["rpc"], |
|
|
|
|
wallet, |
|
|
|
|
asset["decimals"], |
|
|
|
|
) |
|
|
|
|
else: |
|
|
|
|
row["balance"] = _wallet_erc20_balance( |
|
|
|
|
asset["rpc"], |
|
|
|
|
asset["token"], |
|
|
|
|
wallet, |
|
|
|
|
asset["decimals"], |
|
|
|
|
) |
|
|
|
|
row["ok"] = True |
|
|
|
|
except Exception as exc: |
|
|
|
|
row["error"] = str(exc)[:220] |
|
|
|
|
|
|
|
|
|
rows.append(row) |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
"label": label, |
|
|
|
|
"wallet": wallet, |
|
|
|
|
"assets": rows, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_health_routes(app): |
|
|
|
|
@app.route("/health", methods=["GET"]) |
|
|
|
|
def health_page(): |
|
|
|
|
health, http_code = _health_payload(app) |
|
|
|
|
health["operations_balances"] = _wallet_balances( |
|
|
|
|
os.environ.get("OTB_OPERATIONS_WALLET", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5"), |
|
|
|
|
"Operations Bal", |
|
|
|
|
) |
|
|
|
|
health["treasury_balances"] = _wallet_balances( |
|
|
|
|
os.environ.get("OTB_TREASURY_WALLET", "0xbe1fdc8c69f712d62cfcd3bf23f636de1dbd213f"), |
|
|
|
|
"Treasury Bal", |
|
|
|
|
) |
|
|
|
|
# Backward compatible JSON key for anything already reading health.json. |
|
|
|
|
health["wallet_balances"] = health["operations_balances"] |
|
|
|
|
return render_template("health.html", health=health), http_code |
|
|
|
|
|
|
|
|
|
@app.route("/health.json", methods=["GET"]) |
|
|
|
|
def health_json(): |
|
|
|
|
health, http_code = _health_payload(app) |
|
|
|
|
health["operations_balances"] = _wallet_balances( |
|
|
|
|
os.environ.get("OTB_OPERATIONS_WALLET", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5"), |
|
|
|
|
"Operations Bal", |
|
|
|
|
) |
|
|
|
|
health["treasury_balances"] = _wallet_balances( |
|
|
|
|
os.environ.get("OTB_TREASURY_WALLET", "0xbe1fdc8c69f712d62cfcd3bf23f636de1dbd213f"), |
|
|
|
|
"Treasury Bal", |
|
|
|
|
) |
|
|
|
|
# Backward compatible JSON key for anything already reading health.json. |
|
|
|
|
health["wallet_balances"] = health["operations_balances"] |
|
|
|
|
return jsonify(health), http_code |
|
|
|
|
|