Files
bootnode/api/bootnode/db/models.py
T
zeekay e12b842f7d feat: implement Alchemy-style billing and pluggable deployments
Billing System:
- Add compute unit costs for 100+ RPC methods (1-100 CU)
- Implement pricing tiers: Free (30M CU), PAYG ($0.40/M), Enterprise
- Real-time usage tracking with Redis + ClickHouse analytics
- Subscription model with billing cycles
- Billing API endpoints: /usage, /subscription, /invoices, /tiers

Hanzo Commerce Integration:
- HanzoCommerceClient for subscriptions and invoices
- Usage sync worker for PAYG metered billing
- Webhook handler for subscription lifecycle events
- HMAC signature verification for webhooks

Pluggable Deployment Targets:
- Abstract DeployTarget interface
- DockerDeployer - Docker Compose management
- ProcessDeployer - Local dev with subprocesses
- KubernetesDeployer - K8s deployments via python client
- Factory pattern with caching
- Infrastructure API: /services, /scale, /logs

Pricing Page:
- Updated to match Alchemy pricing structure
- Three tiers with feature comparison table
- FAQ section with compute unit explanation

ClickHouse Schema:
- billing_usage_daily with SummingMergeTree
- billing_usage_monthly materialized view
- cu_by_method for analytics dashboard
- rate_limit_events tracking
2026-02-04 09:44:50 -08:00

305 lines
12 KiB
Python

"""Database models."""
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import (
Boolean,
DateTime,
ForeignKey,
Integer,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
"""Base class for all models."""
type_annotation_map = {
dict[str, Any]: JSONB,
}
class User(Base):
"""User model for local authentication."""
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
avatar: Mapped[str | None] = mapped_column(String(500), nullable=True)
roles: Mapped[list[str] | None] = mapped_column(JSONB, default=["user"])
permissions: Mapped[list[str] | None] = mapped_column(JSONB, default=["read", "write"])
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class Project(Base):
"""Project/Organization model."""
__tablename__ = "projects"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
owner_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
settings: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
# Relationships
api_keys: Mapped[list["ApiKey"]] = relationship(back_populates="project")
webhooks: Mapped[list["Webhook"]] = relationship(back_populates="project")
subscription: Mapped["Subscription | None"] = relationship(back_populates="project", uselist=False)
class ApiKey(Base):
"""API Key model."""
__tablename__ = "api_keys"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
key_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
key_prefix: Mapped[str] = mapped_column(String(12), nullable=False) # First chars for display
rate_limit: Mapped[int] = mapped_column(Integer, default=100) # requests per minute
compute_units_limit: Mapped[int] = mapped_column(Integer, default=1000) # CU per minute
allowed_origins: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
allowed_chains: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
# Relationships
project: Mapped["Project"] = relationship(back_populates="api_keys")
class Webhook(Base):
"""Webhook subscription model."""
__tablename__ = "webhooks"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False)
chain: Mapped[str] = mapped_column(String(50), nullable=False)
network: Mapped[str] = mapped_column(String(50), nullable=False, default="mainnet")
event_type: Mapped[str] = mapped_column(String(50), nullable=False)
filters: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
secret: Mapped[str] = mapped_column(String(255), nullable=False) # For HMAC signing
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
failure_count: Mapped[int] = mapped_column(Integer, default=0)
last_triggered_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
# Relationships
project: Mapped["Project"] = relationship(back_populates="webhooks")
deliveries: Mapped[list["WebhookDelivery"]] = relationship(back_populates="webhook")
class WebhookDelivery(Base):
"""Webhook delivery attempt model."""
__tablename__ = "webhook_deliveries"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
webhook_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("webhooks.id", ondelete="CASCADE"), nullable=False
)
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
status_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
response_body: Mapped[str | None] = mapped_column(Text, nullable=True)
attempt_count: Mapped[int] = mapped_column(Integer, default=1)
success: Mapped[bool] = mapped_column(Boolean, default=False)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
delivered_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
# Relationships
webhook: Mapped["Webhook"] = relationship(back_populates="deliveries")
class Usage(Base):
"""API usage tracking model."""
__tablename__ = "usage"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
api_key_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True
)
chain: Mapped[str] = mapped_column(String(50), nullable=False)
network: Mapped[str] = mapped_column(String(50), nullable=False)
method: Mapped[str] = mapped_column(String(100), nullable=False)
compute_units: Mapped[int] = mapped_column(Integer, default=1)
response_time_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
status_code: Mapped[int] = mapped_column(Integer, default=200)
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class SmartWallet(Base):
"""ERC-4337 Smart Wallet model."""
__tablename__ = "smart_wallets"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
address: Mapped[str] = mapped_column(String(42), nullable=False, unique=True)
owner_address: Mapped[str] = mapped_column(String(42), nullable=False)
factory_address: Mapped[str] = mapped_column(String(42), nullable=False)
chain: Mapped[str] = mapped_column(String(50), nullable=False)
network: Mapped[str] = mapped_column(String(50), nullable=False, default="mainnet")
salt: Mapped[str] = mapped_column(String(66), nullable=False) # bytes32 hex
is_deployed: Mapped[bool] = mapped_column(Boolean, default=False)
wallet_metadata: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class TeamMember(Base):
"""Team member model for project collaboration."""
__tablename__ = "team_members"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
email: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
role: Mapped[str] = mapped_column(String(50), nullable=False, default="viewer")
status: Mapped[str] = mapped_column(String(50), nullable=False, default="pending")
invite_token: Mapped[str | None] = mapped_column(String(255), nullable=True)
invited_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
joined_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class GasPolicy(Base):
"""Gas sponsorship policy model."""
__tablename__ = "gas_policies"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
chain: Mapped[str] = mapped_column(String(50), nullable=False)
network: Mapped[str] = mapped_column(String(50), nullable=False, default="mainnet")
rules: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
max_gas_per_op: Mapped[int] = mapped_column(Integer, default=1000000) # gas units
max_spend_per_day_usd: Mapped[int] = mapped_column(Integer, default=100) # in cents
allowed_contracts: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
allowed_methods: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class Subscription(Base):
"""Subscription model for billing and quotas."""
__tablename__ = "subscriptions"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, unique=True
)
tier: Mapped[str] = mapped_column(String(50), nullable=False, default="free")
hanzo_subscription_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
hanzo_customer_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
# Limits (can be overridden from tier defaults)
monthly_cu_limit: Mapped[int] = mapped_column(Integer, default=30_000_000)
rate_limit_per_second: Mapped[int] = mapped_column(Integer, default=25)
max_apps: Mapped[int] = mapped_column(Integer, default=5)
max_webhooks: Mapped[int] = mapped_column(Integer, default=5)
# Usage tracking (real-time in Redis, snapshot here)
current_cu_used: Mapped[int] = mapped_column(Integer, default=0)
# Billing cycle
billing_cycle_start: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
billing_cycle_end: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
# Scheduled changes (for downgrades)
scheduled_tier: Mapped[str | None] = mapped_column(String(50), nullable=True)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
# Relationships
project: Mapped["Project"] = relationship(back_populates="subscription")