Browse Source

Bump OTB Billing to v1.3.0 with health wallet balances

main v1.3.0
def 1 month ago
parent
commit
cd6a7ef4e8
  1. 23
      PROJECT_STATE.md
  2. 22
      README.md
  3. 2
      VERSION
  4. 232
      backend/health.py
  5. 38
      templates/health.html

23
PROJECT_STATE.md

@ -1,3 +1,26 @@
# PROJECT_STATE - OTB Billing
## v1.3.0 - 2026-05-17
Current state:
- OTB Billing health page is working at /health.
- /health.json is working.
- Health page now includes:
- Operations Bal card for 0x44f6c44C42e6ae0392E7289F032384C0d37F56D5
- Treasury Bal card for 0xbe1fdc8c69f712d62cfcd3bf23f636de1dbd213f
- Both cards report payment-asset balances for USDC, ETH, ETHO, EGAZ, and ETI.
- Existing wallet_balances JSON key remains as a backward-compatible alias for operations_balances.
- Service name: otb_billing.service
- Runtime host: outsidethedb
- App port: 5050
- Project path: /home/def/otb_billing
Recent verification:
- /health renders the new Operations Bal and Treasury Bal cards.
- ETHO, EGAZ, and ETI balances resolve through existing project RPCs.
- Arbitrum USDC and Ethereum ETH rows now use fallback-capable RPC helpers.
- OTB Operations wallet alias has been added in both Etica and ETHO Blockscout explorers.
## v0.6.2 - Service Templates Stabilization
- Verified template CRUD working

22
README.md

@ -1,3 +1,25 @@
# OTB Billing - v1.3.0
Build date: 2026-05-17
## v1.3.0 changes
- Added health-page wallet balance cards for the OTB Operations wallet and OTB Treasury wallet.
- Renamed the original wallet card to "Operations Bal".
- Added matching "Treasury Bal" card.
- Balance cards show the configured payment assets:
- USDC on Arbitrum
- ETH on Ethereum
- ETHO on Etho Protocol
- EGAZ on Etica
- ETI on Etica
- Added the same balance data to /health.json using:
- operations_balances
- treasury_balances
- wallet_balances retained as backward-compatible alias for operations_balances.
- Added RPC fallback handling for public Arbitrum/Ethereum RPCs so health checks are less likely to fail on single-provider HTTP 403 responses.
- Confirmed explorer aliases for the OTB Operations wallet on Etica and ETHO Blockscout explorers.
## v1.2.0 (2026-05-03)
- Platform-level Service Agreement
- Improved privacy and data messaging

2
VERSION

@ -1 +1 @@
v1.2.0
v1.3.0

232
backend/health.py

@ -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

38
templates/health.html

@ -132,6 +132,44 @@
<p><strong>Free:</strong> {{ health.disk_root.free_gb }} GB</p>
<p><strong>Used %:</strong> {{ health.disk_root.used_percent }}%</p>
</div>
<div class="health-card">
<h3>Operations Bal</h3>
{% if health.operations_balances %}
{% for asset in health.operations_balances.assets %}
<p>
<strong>{{ asset.coin }}:</strong>
{% if asset.ok %}
{{ asset.balance }}
{% else %}
error
{% endif %}
</p>
{% endfor %}
<p class="monoish"><strong>Wallet:</strong> {{ health.operations_balances.wallet }}</p>
{% else %}
<p>Not available</p>
{% endif %}
</div>
<div class="health-card">
<h3>Treasury Bal</h3>
{% if health.treasury_balances %}
{% for asset in health.treasury_balances.assets %}
<p>
<strong>{{ asset.coin }}:</strong>
{% if asset.ok %}
{{ asset.balance }}
{% else %}
error
{% endif %}
</p>
{% endfor %}
<p class="monoish"><strong>Wallet:</strong> {{ health.treasury_balances.wallet }}</p>
{% else %}
<p>Not available</p>
{% endif %}
</div>
</div>
</div>

Loading…
Cancel
Save