Fix miner wallet queries and add tests

This commit is contained in:
Alexa Amundson
2025-11-16 01:50:36 -06:00
parent b2f933762a
commit b2379fddd7
6 changed files with 188 additions and 13 deletions

View File

@@ -6,6 +6,7 @@ from sqlalchemy import select
from app.database import get_db from app.database import get_db
from app.models.user import User from app.models.user import User
from app.models.blockchain import Wallet
from app.schemas.user import UserCreate, UserResponse, Token, UserLogin from app.schemas.user import UserCreate, UserResponse, Token, UserLogin
from app.auth import ( from app.auth import (
verify_password, verify_password,
@@ -38,7 +39,7 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
) )
# Generate wallet # Generate wallet
wallet_address, private_key = BlockchainService.generate_wallet_address() wallet_address, private_key, public_key = BlockchainService.generate_wallet_address()
# Create user # Create user
user = User( user = User(
@@ -53,6 +54,19 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
) )
db.add(user) db.add(user)
await db.flush()
wallet = Wallet(
user_id=user.id,
address=wallet_address,
private_key=private_key,
public_key=public_key,
balance=user.balance,
label="Primary Wallet",
is_primary=True,
)
db.add(wallet)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
@@ -115,3 +129,10 @@ async def get_current_user_info(
async def logout(): async def logout():
"""Logout (client should delete token)""" """Logout (client should delete token)"""
return {"message": "Successfully logged out"} return {"message": "Successfully logged out"}
async def get_current_user(
current_user: User = Depends(get_current_active_user)
) -> User:
"""Reusable dependency that returns the authenticated user."""
return current_user

View File

@@ -138,22 +138,37 @@ async def get_miner_stats(
) )
wallet = wallet_result.scalar_one_or_none() wallet = wallet_result.scalar_one_or_none()
if not wallet:
return MinerStats(
blocks_mined=0,
roadcoins_earned=0.0,
current_hashrate_mhs=miner_state.hashrate_mhs if miner_state.is_mining else 0.0,
average_hashrate_mhs=0.0,
total_shares=miner_state.shares_submitted,
accepted_shares=miner_state.shares_accepted,
rejected_shares=miner_state.shares_submitted - miner_state.shares_accepted,
last_block_time=None,
mining_since=miner_state.started_at,
)
miner_filter = Block.miner_address == wallet.address
# Count blocks mined by this user # Count blocks mined by this user
blocks_count_result = await db.execute( blocks_count_result = await db.execute(
select(func.count(Block.id)).filter(Block.miner == wallet.address if wallet else None) select(func.count(Block.id)).filter(miner_filter)
) )
blocks_mined = blocks_count_result.scalar() or 0 blocks_mined = blocks_count_result.scalar() or 0
# Sum rewards earned # Sum rewards earned
rewards_result = await db.execute( rewards_result = await db.execute(
select(func.sum(Block.reward)).filter(Block.miner == wallet.address if wallet else None) select(func.sum(Block.reward)).filter(miner_filter)
) )
roadcoins_earned = rewards_result.scalar() or 0.0 roadcoins_earned = rewards_result.scalar() or 0.0
# Get last block mined # Get last block mined
last_block_result = await db.execute( last_block_result = await db.execute(
select(Block) select(Block)
.filter(Block.miner == wallet.address if wallet else None) .filter(miner_filter)
.order_by(desc(Block.timestamp)) .order_by(desc(Block.timestamp))
.limit(1) .limit(1)
) )
@@ -197,7 +212,7 @@ async def get_recent_blocks(
# Get recent blocks # Get recent blocks
blocks_result = await db.execute( blocks_result = await db.execute(
select(Block) select(Block)
.filter(Block.miner == wallet.address) .filter(Block.miner_address == wallet.address)
.order_by(desc(Block.timestamp)) .order_by(desc(Block.timestamp))
.limit(limit) .limit(limit)
) )

View File

@@ -127,12 +127,12 @@ class BlockchainService:
return new_block return new_block
@staticmethod @staticmethod
def generate_wallet_address() -> tuple[str, str]: def generate_wallet_address() -> tuple[str, str, str]:
"""Generate a new wallet address and private key""" """Generate a new wallet address, private key and public key"""
private_key = secrets.token_hex(32) private_key = secrets.token_hex(32)
public_key = hashlib.sha256(private_key.encode()).hexdigest() public_key = hashlib.sha256(private_key.encode()).hexdigest()
address = "RD" + hashlib.sha256(public_key.encode()).hexdigest()[:38] address = "RD" + hashlib.sha256(public_key.encode()).hexdigest()[:38]
return address, private_key return address, private_key, public_key
@staticmethod @staticmethod
async def create_transaction( async def create_transaction(

2
backend/pytest.ini Normal file
View File

@@ -0,0 +1,2 @@
[pytest]
asyncio_mode = auto

View File

@@ -1,16 +1,21 @@
"""Pytest configuration and fixtures""" """Pytest configuration and fixtures"""
import pytest
import asyncio import asyncio
import os
from typing import AsyncGenerator from typing import AsyncGenerator
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.main import app from app.main import app
from app.database import get_db, Base from app.database import get_db, Base
from app.config import settings from app.config import settings
# Test database URL # Test database URL
TEST_DATABASE_URL = "postgresql+asyncpg://blackroad:password@localhost:5432/blackroad_test" TEST_DATABASE_URL = os.getenv(
"TEST_DATABASE_URL",
"sqlite+aiosqlite:///./test.db"
)
# Create test engine # Create test engine
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False) test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
@@ -46,7 +51,9 @@ async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
app.dependency_overrides[get_db] = override_get_db app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(app=app, base_url="http://test") as client: transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield client yield client
app.dependency_overrides.clear() app.dependency_overrides.clear()

130
backend/tests/test_miner.py Normal file
View File

@@ -0,0 +1,130 @@
"""Miner integration tests"""
from datetime import datetime, timedelta
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.blockchain import Block
async def _create_block(
db_session: AsyncSession,
*,
index: int,
timestamp: datetime,
miner_id: int,
miner_address: str,
reward: float,
) -> None:
"""Helper to insert a block for tests."""
block = Block(
index=index,
timestamp=timestamp,
nonce=index,
previous_hash=f"prev-{index}",
hash=f"hash-{index}-{miner_address}",
miner_id=miner_id,
miner_address=miner_address,
difficulty=4,
reward=reward,
transaction_count=0,
is_valid=True,
)
db_session.add(block)
@pytest.mark.asyncio
async def test_miner_stats_respects_wallet(
client: AsyncClient,
auth_headers,
db_session: AsyncSession,
test_user,
):
"""Ensure /api/miner/stats reports only the authenticated user's blocks."""
wallet_address = test_user["wallet_address"]
user_id = test_user["id"]
now = datetime.utcnow()
await _create_block(
db_session,
index=1,
timestamp=now - timedelta(minutes=5),
miner_id=user_id,
miner_address=wallet_address,
reward=40.0,
)
await _create_block(
db_session,
index=2,
timestamp=now - timedelta(minutes=1),
miner_id=user_id,
miner_address=wallet_address,
reward=60.0,
)
await _create_block(
db_session,
index=3,
timestamp=now - timedelta(minutes=2),
miner_id=user_id + 100,
miner_address="RDOTHER000000000000000000000000000000", # Different miner
reward=75.0,
)
await db_session.commit()
response = await client.get("/api/miner/stats", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["blocks_mined"] == 2
assert pytest.approx(data["roadcoins_earned"], rel=1e-3) == 100.0
assert data["last_block_time"] is not None
@pytest.mark.asyncio
async def test_miner_blocks_endpoint_returns_only_user_blocks(
client: AsyncClient,
auth_headers,
db_session: AsyncSession,
test_user,
):
"""Ensure /api/miner/blocks only returns the authenticated user's blocks."""
wallet_address = test_user["wallet_address"]
user_id = test_user["id"]
now = datetime.utcnow()
await _create_block(
db_session,
index=5,
timestamp=now - timedelta(minutes=10),
miner_id=user_id,
miner_address=wallet_address,
reward=25.0,
)
await _create_block(
db_session,
index=6,
timestamp=now - timedelta(minutes=3),
miner_id=user_id,
miner_address=wallet_address,
reward=30.0,
)
await _create_block(
db_session,
index=7,
timestamp=now - timedelta(minutes=1),
miner_id=user_id + 200,
miner_address="RDANOTHER0000000000000000000000000000",
reward=55.0,
)
await db_session.commit()
response = await client.get("/api/miner/blocks", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert len(data) == 2
returned_indexes = {block["block_index"] for block in data}
assert returned_indexes == {5, 6}
# Ensure results are sorted by timestamp desc (latest first)
assert data[0]["block_index"] == 6