chore: remove obsolete payment architecture and integration test documentation

- Remove AITBC_PAYMENT_ARCHITECTURE.md (dual-currency system documentation)
- Remove IMPLEMENTATION_COMPLETE_SUMMARY.md (integration test completion summary)
- Remove INTEGRATION_TEST_FIXES.md (test fixes documentation)
- Remove INTEGRATION_TEST_UPDATES.md (real features implementation notes)
- Remove PAYMENT_INTEGRATION_COMPLETE.md (wallet-coordinator integration docs)
- Remove WALLET_COORDINATOR_INTEGRATION.md (payment
This commit is contained in:
oib
2026-01-29 12:28:43 +01:00
parent 5c99c92ffb
commit ff4554b9dd
94 changed files with 7925 additions and 128 deletions

View File

@@ -3,8 +3,9 @@
from .job import Job
from .miner import Miner
from .job_receipt import JobReceipt
from .marketplace import MarketplaceOffer, MarketplaceBid, OfferStatus
from .marketplace import MarketplaceOffer, MarketplaceBid
from .user import User, Wallet
from .payment import JobPayment, PaymentEscrow
__all__ = [
"Job",
@@ -12,7 +13,8 @@ __all__ = [
"JobReceipt",
"MarketplaceOffer",
"MarketplaceBid",
"OfferStatus",
"User",
"Wallet",
"JobPayment",
"PaymentEscrow",
]

View File

@@ -4,17 +4,18 @@ from datetime import datetime
from typing import Optional
from uuid import uuid4
from sqlalchemy import Column, JSON, String
from sqlmodel import Field, SQLModel, Relationship
from ..types import JobState
from sqlalchemy import Column, JSON, String, ForeignKey
from sqlalchemy.orm import Mapped, relationship
from sqlmodel import Field, SQLModel
class Job(SQLModel, table=True):
__tablename__ = "job"
id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True, index=True)
client_id: str = Field(index=True)
state: JobState = Field(default=JobState.queued, sa_column_kwargs={"nullable": False})
state: str = Field(default="QUEUED", max_length=20)
payload: dict = Field(sa_column=Column(JSON, nullable=False))
constraints: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=False))
@@ -30,8 +31,8 @@ class Job(SQLModel, table=True):
error: Optional[str] = None
# Payment tracking
payment_id: Optional[str] = Field(default=None, foreign_key="job_payments.id", index=True)
payment_id: Optional[str] = Field(default=None, sa_column=Column(String, ForeignKey("job_payments.id"), index=True))
payment_status: Optional[str] = Field(default=None, max_length=20) # pending, escrowed, released, refunded
# Relationships
payment: Optional["JobPayment"] = Relationship(back_populates="jobs")
# payment: Mapped[Optional["JobPayment"]] = relationship(back_populates="jobs")

View File

@@ -1,27 +1,20 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Optional
from uuid import uuid4
from sqlalchemy import Column, Enum as SAEnum, JSON
from sqlalchemy import Column, JSON
from sqlmodel import Field, SQLModel
class OfferStatus(str, Enum):
open = "open"
reserved = "reserved"
closed = "closed"
class MarketplaceOffer(SQLModel, table=True):
id: str = Field(default_factory=lambda: uuid4().hex, primary_key=True)
provider: str = Field(index=True)
capacity: int = Field(default=0, nullable=False)
price: float = Field(default=0.0, nullable=False)
sla: str = Field(default="")
status: OfferStatus = Field(default=OfferStatus.open, sa_column=Column(SAEnum(OfferStatus), nullable=False))
status: str = Field(default="open", max_length=20)
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, index=True)
attributes: dict = Field(default_factory=dict, sa_column=Column(JSON, nullable=False))

View File

@@ -6,10 +6,9 @@ from datetime import datetime
from typing import Optional, List
from uuid import uuid4
from sqlalchemy import Column, String, DateTime, Numeric, ForeignKey
from sqlmodel import Field, SQLModel, Relationship
from ..schemas.payments import PaymentStatus, PaymentMethod
from sqlalchemy import Column, String, DateTime, Numeric, ForeignKey, JSON
from sqlalchemy.orm import Mapped, relationship
from sqlmodel import Field, SQLModel
class JobPayment(SQLModel, table=True):
@@ -23,8 +22,8 @@ class JobPayment(SQLModel, table=True):
# Payment details
amount: float = Field(sa_column=Column(Numeric(20, 8), nullable=False))
currency: str = Field(default="AITBC", max_length=10)
status: PaymentStatus = Field(default=PaymentStatus.PENDING)
payment_method: PaymentMethod = Field(default=PaymentMethod.AITBC_TOKEN)
status: str = Field(default="pending", max_length=20)
payment_method: str = Field(default="aitbc_token", max_length=20)
# Addresses
escrow_address: Optional[str] = Field(default=None, max_length=100)
@@ -43,10 +42,10 @@ class JobPayment(SQLModel, table=True):
expires_at: Optional[datetime] = None
# Additional metadata
metadata: Optional[dict] = Field(default=None)
meta_data: Optional[dict] = Field(default=None, sa_column=Column(JSON))
# Relationships
jobs: List["Job"] = Relationship(back_populates="payment")
# jobs: Mapped[List["Job"]] = relationship(back_populates="payment")
class PaymentEscrow(SQLModel, table=True):

View File

@@ -6,13 +6,6 @@ from sqlmodel import SQLModel, Field, Relationship, Column
from sqlalchemy import JSON
from datetime import datetime
from typing import Optional, List
from enum import Enum
class UserStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
SUSPENDED = "suspended"
class User(SQLModel, table=True):
@@ -20,7 +13,7 @@ class User(SQLModel, table=True):
id: str = Field(primary_key=True)
email: str = Field(unique=True, index=True)
username: str = Field(unique=True, index=True)
status: UserStatus = Field(default=UserStatus.ACTIVE)
status: str = Field(default="active", max_length=20)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
last_login: Optional[datetime] = None
@@ -44,28 +37,13 @@ class Wallet(SQLModel, table=True):
transactions: List["Transaction"] = Relationship(back_populates="wallet")
class TransactionType(str, Enum):
DEPOSIT = "deposit"
WITHDRAWAL = "withdrawal"
PURCHASE = "purchase"
REWARD = "reward"
REFUND = "refund"
class TransactionStatus(str, Enum):
PENDING = "pending"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class Transaction(SQLModel, table=True):
"""Transaction model"""
id: str = Field(primary_key=True)
user_id: str = Field(foreign_key="user.id")
wallet_id: Optional[int] = Field(foreign_key="wallet.id")
type: TransactionType
status: TransactionStatus = Field(default=TransactionStatus.PENDING)
type: str = Field(max_length=20)
status: str = Field(default="pending", max_length=20)
amount: float
fee: float = Field(default=0.0)
description: Optional[str] = None

View File

@@ -56,6 +56,8 @@ from ..domain import (
MarketplaceBid,
User,
Wallet,
JobPayment,
PaymentEscrow,
)
# Service-specific models
@@ -101,4 +103,6 @@ __all__ = [
"LLMRequest",
"FFmpegRequest",
"BlenderRequest",
"JobPayment",
"PaymentEscrow",
]

View File

@@ -4,7 +4,7 @@ Service schemas for common GPU workloads
from typing import Any, Dict, List, Optional, Union
from enum import Enum
from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Field, field_validator
import re
@@ -123,7 +123,8 @@ class StableDiffusionRequest(BaseModel):
lora: Optional[str] = Field(None, description="LoRA model to use")
lora_scale: float = Field(1.0, ge=0.0, le=2.0, description="LoRA strength")
@validator('seed')
@field_validator('seed')
@classmethod
def validate_seed(cls, v):
if v is not None and isinstance(v, list):
if len(v) > 4:
@@ -289,9 +290,10 @@ class BlenderRequest(BaseModel):
transparent: bool = Field(False, description="Transparent background")
custom_args: Optional[List[str]] = Field(None, description="Custom Blender arguments")
@validator('frame_end')
def validate_frame_range(cls, v, values):
if 'frame_start' in values and v < values['frame_start']:
@field_validator('frame_end')
@classmethod
def validate_frame_range(cls, v, info):
if info and info.data and 'frame_start' in info.data and v < info.data['frame_start']:
raise ValueError("frame_end must be >= frame_start")
return v

View File

@@ -1,8 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from ..deps import require_client_key
from ..schemas import JobCreate, JobView, JobResult
from ..schemas.payments import JobPaymentCreate, PaymentMethod
from ..schemas import JobCreate, JobView, JobResult, JobPaymentCreate
from ..types import JobState
from ..services import JobService
from ..services.payments import PaymentService
@@ -27,11 +26,11 @@ async def submit_job(
job_id=job.id,
amount=req.payment_amount,
currency=req.payment_currency,
payment_method=PaymentMethod.AITBC_TOKEN # Jobs use AITBC tokens
payment_method="aitbc_token" # Jobs use AITBC tokens
)
payment = await payment_service.create_payment(job.id, payment_create)
job.payment_id = payment.id
job.payment_status = payment.status.value
job.payment_status = payment.status
session.commit()
session.refresh(job)

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from ..deps import require_admin_key
from ..domain import MarketplaceOffer, Miner, OfferStatus
from ..domain import MarketplaceOffer, Miner
from ..schemas import MarketplaceOfferView
from ..storage import SessionDep

View File

@@ -1,5 +1,6 @@
from datetime import datetime
from typing import Any
import logging
from fastapi import APIRouter, Depends, HTTPException, Response, status
@@ -9,6 +10,8 @@ from ..services import JobService, MinerService
from ..services.receipts import ReceiptService
from ..storage import SessionDep
logger = logging.getLogger(__name__)
router = APIRouter(tags=["miner"])
@@ -78,6 +81,23 @@ async def submit_result(
job.receipt_id = receipt["receipt_id"] if receipt else None
session.add(job)
session.commit()
# Auto-release payment if job has payment
if job.payment_id and job.payment_status == "escrowed":
from ..services.payments import PaymentService
payment_service = PaymentService(session)
success = await payment_service.release_payment(
job.id,
job.payment_id,
reason="Job completed successfully"
)
if success:
job.payment_status = "released"
session.commit()
logger.info(f"Auto-released payment {job.payment_id} for completed job {job.id}")
else:
logger.error(f"Failed to auto-release payment {job.payment_id} for job {job.id}")
miner_service.release(
miner_id,
success=True,
@@ -106,5 +126,22 @@ async def submit_failure(
job.assigned_miner_id = miner_id
session.add(job)
session.commit()
# Auto-refund payment if job has payment
if job.payment_id and job.payment_status in ["pending", "escrowed"]:
from ..services.payments import PaymentService
payment_service = PaymentService(session)
success = await payment_service.refund_payment(
job.id,
job.payment_id,
reason=f"Job failed: {req.error_code}: {req.error_message}"
)
if success:
job.payment_status = "refunded"
session.commit()
logger.info(f"Auto-refunded payment {job.payment_id} for failed job {job.id}")
else:
logger.error(f"Failed to auto-refund payment {job.payment_id} for job {job.id}")
miner_service.release(miner_id, success=False)
return {"status": "ok"}

View File

@@ -37,7 +37,7 @@ class PartnerResponse(BaseModel):
class WebhookCreate(BaseModel):
"""Create a webhook subscription"""
url: str = Field(..., pattern=r'^https?://')
events: List[str] = Field(..., min_items=1)
events: List[str] = Field(..., min_length=1)
secret: Optional[str] = Field(max_length=100)

View File

@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from ..deps import require_client_key
from ..schemas.payments import (
from ..schemas import (
JobPaymentCreate,
JobPaymentView,
PaymentRequest,

View File

@@ -3,12 +3,75 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, Optional, List
from base64 import b64encode, b64decode
from enum import Enum
from pydantic import BaseModel, Field, ConfigDict
from .types import JobState, Constraints
# Payment schemas
class JobPaymentCreate(BaseModel):
"""Request to create a payment for a job"""
job_id: str
amount: float
currency: str = "AITBC" # Jobs paid with AITBC tokens
payment_method: str = "aitbc_token" # Primary method for job payments
escrow_timeout_seconds: int = 3600 # 1 hour default
class JobPaymentView(BaseModel):
"""Payment information for a job"""
job_id: str
payment_id: str
amount: float
currency: str
status: str
payment_method: str
escrow_address: Optional[str] = None
refund_address: Optional[str] = None
created_at: datetime
updated_at: datetime
released_at: Optional[datetime] = None
refunded_at: Optional[datetime] = None
transaction_hash: Optional[str] = None
refund_transaction_hash: Optional[str] = None
class PaymentRequest(BaseModel):
"""Request to pay for a job"""
job_id: str
amount: float
currency: str = "BTC"
refund_address: Optional[str] = None
class PaymentReceipt(BaseModel):
"""Receipt for a payment"""
payment_id: str
job_id: str
amount: float
currency: str
status: str
transaction_hash: Optional[str] = None
created_at: datetime
verified_at: Optional[datetime] = None
class EscrowRelease(BaseModel):
"""Request to release escrow payment"""
job_id: str
payment_id: str
reason: Optional[str] = None
class RefundRequest(BaseModel):
"""Request to refund a payment"""
job_id: str
payment_id: str
reason: str
# User management schemas
class UserCreate(BaseModel):
email: str

View File

@@ -4,32 +4,16 @@ from __future__ import annotations
from datetime import datetime
from typing import Optional, Dict, Any
from enum import Enum
from pydantic import BaseModel, Field
class PaymentStatus(str, Enum):
"""Payment status values"""
PENDING = "pending"
ESCROWED = "escrowed"
RELEASED = "released"
REFUNDED = "refunded"
FAILED = "failed"
class PaymentMethod(str, Enum):
"""Payment methods"""
AITBC_TOKEN = "aitbc_token" # Primary method for job payments
BITCOIN = "bitcoin" # Only for exchange purchases
class JobPaymentCreate(BaseModel):
"""Request to create a payment for a job"""
job_id: str
amount: float
currency: str = "AITBC" # Jobs paid with AITBC tokens
payment_method: PaymentMethod = PaymentMethod.AITBC_TOKEN
payment_method: str = "aitbc_token" # Primary method for job payments
escrow_timeout_seconds: int = 3600 # 1 hour default
@@ -39,8 +23,8 @@ class JobPaymentView(BaseModel):
payment_id: str
amount: float
currency: str
status: PaymentStatus
payment_method: PaymentMethod
status: str
payment_method: str
escrow_address: Optional[str] = None
refund_address: Optional[str] = None
created_at: datetime
@@ -65,7 +49,7 @@ class PaymentReceipt(BaseModel):
job_id: str
amount: float
currency: str
status: PaymentStatus
status: str
transaction_hash: Optional[str] = None
created_at: datetime
verified_at: Optional[datetime] = None

View File

@@ -32,15 +32,8 @@ class JobService:
# Create payment if amount is specified
if req.payment_amount and req.payment_amount > 0:
from ..schemas.payments import JobPaymentCreate, PaymentMethod
payment_create = JobPaymentCreate(
job_id=job.id,
amount=req.payment_amount,
currency=req.payment_currency,
payment_method=PaymentMethod.BITCOIN
)
# Note: This is async, so we'll handle it in the router
job.payment_pending = True
# Note: Payment creation is handled in the router
pass
return job
@@ -81,6 +74,8 @@ class JobService:
requested_at=job.requested_at,
expires_at=job.expires_at,
error=job.error,
payment_id=job.payment_id,
payment_status=job.payment_status,
)
def to_result(self, job: Job) -> JobResult:

View File

@@ -5,7 +5,7 @@ from typing import Iterable, Optional
from sqlmodel import Session, select
from ..domain import MarketplaceOffer, MarketplaceBid, OfferStatus
from ..domain import MarketplaceOffer, MarketplaceBid
from ..schemas import (
MarketplaceBidRequest,
MarketplaceOfferView,
@@ -62,7 +62,7 @@ class MarketplaceService:
def get_stats(self) -> MarketplaceStatsView:
offers = self.session.exec(select(MarketplaceOffer)).all()
open_offers = [offer for offer in offers if offer.status == OfferStatus.open]
open_offers = [offer for offer in offers if offer.status == "open"]
total_offers = len(offers)
open_capacity = sum(offer.capacity for offer in open_offers)

View File

@@ -6,11 +6,9 @@ import httpx
import logging
from ..domain.payment import JobPayment, PaymentEscrow
from ..schemas.payments import (
from ..schemas import (
JobPaymentCreate,
JobPaymentView,
PaymentStatus,
PaymentMethod,
EscrowRelease,
RefundRequest
)
@@ -44,10 +42,10 @@ class PaymentService:
self.session.refresh(payment)
# For AITBC token payments, use token escrow
if payment_data.payment_method == PaymentMethod.AITBC_TOKEN:
if payment_data.payment_method == "aitbc_token":
await self._create_token_escrow(payment)
# Bitcoin payments only for exchange purchases
elif payment_data.payment_method == PaymentMethod.BITCOIN:
elif payment_data.payment_method == "bitcoin":
await self._create_bitcoin_escrow(payment)
return payment
@@ -61,7 +59,7 @@ class PaymentService:
response = await client.post(
f"{self.exchange_base_url}/api/v1/token/escrow/create",
json={
"amount": payment.amount,
"amount": float(payment.amount),
"currency": payment.currency,
"job_id": payment.job_id,
"timeout_seconds": 3600 # 1 hour
@@ -71,7 +69,7 @@ class PaymentService:
if response.status_code == 200:
escrow_data = response.json()
payment.escrow_address = escrow_data.get("escrow_id")
payment.status = PaymentStatus.ESCROWED
payment.status = "escrowed"
payment.escrowed_at = datetime.utcnow()
payment.updated_at = datetime.utcnow()
@@ -92,7 +90,7 @@ class PaymentService:
except Exception as e:
logger.error(f"Error creating token escrow: {e}")
payment.status = PaymentStatus.FAILED
payment.status = "failed"
payment.updated_at = datetime.utcnow()
self.session.commit()
@@ -104,7 +102,7 @@ class PaymentService:
response = await client.post(
f"{self.wallet_base_url}/api/v1/escrow/create",
json={
"amount": payment.amount,
"amount": float(payment.amount),
"currency": payment.currency,
"timeout_seconds": 3600 # 1 hour
}
@@ -113,7 +111,7 @@ class PaymentService:
if response.status_code == 200:
escrow_data = response.json()
payment.escrow_address = escrow_data["address"]
payment.status = PaymentStatus.ESCROWED
payment.status = "escrowed"
payment.escrowed_at = datetime.utcnow()
payment.updated_at = datetime.utcnow()
@@ -134,7 +132,7 @@ class PaymentService:
except Exception as e:
logger.error(f"Error creating Bitcoin escrow: {e}")
payment.status = PaymentStatus.FAILED
payment.status = "failed"
payment.updated_at = datetime.utcnow()
self.session.commit()
@@ -145,7 +143,7 @@ class PaymentService:
if not payment or payment.job_id != job_id:
return False
if payment.status != PaymentStatus.ESCROWED:
if payment.status != "escrowed":
return False
try:
@@ -161,7 +159,7 @@ class PaymentService:
if response.status_code == 200:
release_data = response.json()
payment.status = PaymentStatus.RELEASED
payment.status = "released"
payment.released_at = datetime.utcnow()
payment.updated_at = datetime.utcnow()
payment.transaction_hash = release_data.get("transaction_hash")
@@ -195,7 +193,7 @@ class PaymentService:
if not payment or payment.job_id != job_id:
return False
if payment.status not in [PaymentStatus.ESCROWED, PaymentStatus.PENDING]:
if payment.status not in ["escrowed", "pending"]:
return False
try:
@@ -206,14 +204,14 @@ class PaymentService:
json={
"payment_id": payment_id,
"address": payment.refund_address,
"amount": payment.amount,
"amount": float(payment.amount),
"reason": reason
}
)
if response.status_code == 200:
refund_data = response.json()
payment.status = PaymentStatus.REFUNDED
payment.status = "refunded"
payment.refunded_at = datetime.utcnow()
payment.updated_at = datetime.utcnow()
payment.refund_transaction_hash = refund_data.get("transaction_hash")

View File

@@ -8,7 +8,7 @@ from sqlalchemy.engine import Engine
from sqlmodel import Session, SQLModel, create_engine
from ..config import settings
from ..domain import Job, Miner, MarketplaceOffer, MarketplaceBid
from ..domain import Job, Miner, MarketplaceOffer, MarketplaceBid, JobPayment, PaymentEscrow
from .models_governance import GovernanceProposal, ProposalVote, TreasuryTransaction, GovernanceParameter
_engine: Engine | None = None

View File

@@ -6,6 +6,7 @@ from sqlmodel import SQLModel, Field, Relationship, Column, JSON
from typing import Optional, Dict, Any
from datetime import datetime
from uuid import uuid4
from pydantic import ConfigDict
class GovernanceProposal(SQLModel, table=True):
@@ -83,10 +84,11 @@ class VotingPowerSnapshot(SQLModel, table=True):
snapshot_time: datetime = Field(default_factory=datetime.utcnow, index=True)
block_number: Optional[int] = Field(index=True)
class Config:
indexes = [
model_config = ConfigDict(
indexes=[
{"name": "ix_user_snapshot", "fields": ["user_id", "snapshot_time"]},
]
)
class ProtocolUpgrade(SQLModel, table=True):