Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ that repository unless it is for development reasons.
The contribution workflow is described in [CONTRIBUTING.md](CONTRIBUTING.md)
and useful hints for developers can be found in [doc/developer-notes.md](doc/developer-notes.md).

Node Operator Rewards Portal
----------------------------

The `ambassadors/node-operator-rewards-portal/` directory contains a full-stack rewards dashboard for node operators with a FastAPI backend and a neon glassmorphic frontend. Consult `ambassadors/node-operator-rewards-portal/backend/README.md` and `ambassadors/node-operator-rewards-portal/frontend/README.md` for setup instructions.

Testing
-------

Expand Down
8 changes: 8 additions & 0 deletions ambassadors/node-operator-rewards-portal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Interchained Node Operator Rewards Portal

The Node Operator Rewards Portal is a full-stack experience that tracks uptime and performance for Interchained nodes and allocates daily rewards from a shared pool. It is composed of:

- `backend/` — FastAPI service backed by Redis for authentication, node management, health monitoring, and reward distribution.
- `frontend/` — Dark neon glass UI built with vanilla HTML/CSS/JS that consumes the backend API.

Refer to the individual READMEs for detailed setup instructions.
54 changes: 54 additions & 0 deletions ambassadors/node-operator-rewards-portal/backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Node Operator Rewards Portal Backend

This FastAPI application powers the Interchained Node Operator Rewards Portal. It keeps track of registered nodes, monitors their health and responsiveness, and distributes daily rewards proportionally to performance.

## Features

- JWT-secured authentication endpoints for node operators.
- Node registration and management API.
- Background health monitoring that pings node RPC endpoints every 60 seconds.
- Redis-backed storage for users, nodes, uptime metrics, and reward history.
- Reward engine that performs daily weighted distributions and flags high-performing nodes.
- Administrative APIs for managing the reward pool balance, adjusting daily payouts, moderating nodes, and exporting payout
snapshots.

## Requirements

- Python 3.11+
- Redis server (DB 6 is used by default)

Install dependencies:

```bash
pip install -r requirements.txt
```

Create a `.env` file with the following minimum configuration:

```env
PORTAL_SECRET_KEY="<32+ character random string>"
PORTAL_REDIS_URL="redis://localhost:6379/6"
PORTAL_REWARD_POOL_DAILY=1000
PORTAL_INITIAL_POOL_BALANCE=100000
PORTAL_ADMIN_EMAILS="[email protected],[email protected]"
```

Run the development server:

```bash
uvicorn app.main:app --reload
```

The API documentation is available at `http://localhost:8000/docs` once the server is running.

### Admin access

Any account whose email matches one of the `PORTAL_ADMIN_EMAILS` entries is granted administrative access after registration.
Admin tokens expose the following additional routes:

- `GET /admin/pool` – Inspect the live pool balance, daily distribution amount, and recent payout metadata.
- `POST /admin/pool/adjust` – Deposit or withdraw funds from the pool balance.
- `PUT /admin/pool/daily` – Update the daily payout amount used by the reward engine.
- `GET /admin/nodes` – Review all registered nodes including uptime metrics and owner contact emails.
- `POST /admin/nodes/{id}/reject` / `POST /admin/nodes/{id}/reinstate` – Toggle node eligibility for payouts.
- `GET /admin/exports/payouts` – Generate a CSV-friendly snapshot of the current payout cycle (node, wallet, share).
Empty file.
60 changes: 60 additions & 0 deletions ambassadors/node-operator-rewards-portal/backend/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Configuration helpers for the Node Operator Rewards Portal backend."""

from __future__ import annotations

import os
from functools import lru_cache
from typing import List, Optional

from pydantic import BaseSettings, Field, RedisDsn, validator


class Settings(BaseSettings):
"""Application configuration loaded from environment variables."""

app_name: str = "Interchained Node Operator Rewards Portal"
secret_key: str = Field(..., env="PORTAL_SECRET_KEY")
jwt_algorithm: str = "HS256"
access_token_ttl_seconds: int = 60 * 60 * 4 # 4 hours
redis_url: RedisDsn = Field("redis://localhost:6379/6", env="PORTAL_REDIS_URL")
reward_pool_daily: float = Field(1000.0, env="PORTAL_REWARD_POOL_DAILY")
initial_pool_balance: float = Field(100000.0, env="PORTAL_INITIAL_POOL_BALANCE")
reward_distribution_interval_seconds: int = 60 * 60 * 24
health_check_interval_seconds: int = 60
rpc_timeout_seconds: float = Field(5.0, ge=0.1)
admin_emails: List[str] = Field(default_factory=list, env="PORTAL_ADMIN_EMAILS")

class Config:
env_file = ".env"
env_file_encoding = "utf-8"

@validator("secret_key")
def _validate_secret_key(cls, value: Optional[str]) -> str:
if not value:
raise ValueError(
"PORTAL_SECRET_KEY must be provided via environment variables or a .env file."
)
if len(value) < 32:
raise ValueError("PORTAL_SECRET_KEY must be at least 32 characters long.")
return value

@validator("admin_emails", pre=True)
def _split_admin_emails(cls, value: str | List[str]) -> List[str]:
if isinstance(value, list):
return [email.strip().lower() for email in value if email]
if not value:
return []
return [email.strip().lower() for email in value.split(",") if email.strip()]


@lru_cache
def get_settings() -> Settings:
"""Return cached settings instance."""

return Settings()


def get_redis_url() -> str:
"""Convenience helper for retrieving the Redis connection string."""

return str(get_settings().redis_url)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Reusable FastAPI dependencies."""

from __future__ import annotations

from fastapi import Depends, HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

from .models import AuthenticatedUser
from .security import decode_access_token
from .storage import RedisRepository

security = HTTPBearer(auto_error=True)
repo = RedisRepository()


async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security),
) -> AuthenticatedUser:
payload = decode_access_token(credentials.credentials)
user_id = payload.get("sub")
email = payload.get("email")
if not user_id or not email:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
user_data = await repo.get_user_by_id(str(user_id))
if not user_data:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
role = user_data.get("role", "operator")
is_admin = payload.get("is_admin", role == "admin")
return AuthenticatedUser(id=str(user_id), email=email, is_admin=bool(is_admin))


async def require_admin_user(user: AuthenticatedUser = Depends(get_current_user)) -> AuthenticatedUser:
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
return user
Loading