All bets of all rounds paid one address (m/84'/746'/0'/1/0), so its history grew forever, other rounds' money and stray coins mixed with a round's own, and the payout picked from everything it held. - hd.py: derive_pool_key/derive_pool_address(round_id) on branch 2 (m/84'/746'/0'/2/<round_id>); no argument is the reserve address 1/0. derive_round_pool_key checks the key against the address stored on the round, so a payout can never be signed for money its key does not control. - rounds.pool_address (migration 477bddae1a84, existing rounds backfilled with the reserve address since that is where their bets went; add_column, not a batch alter, so the expression indexes stay): written in the transaction that inserts the round, once its id exists. - place_bet pays round.pool_address; the payout reads, signs and takes change at the round's own address; own_address_for and bump_fee's _signing_context follow the round (confirmation poller and reconciler pass round_id). - Withdrawals to any round pool address, the reserve address or fee_address are refused (withdrawal_to_platform_address, 400, all seven languages). - GET /rounds/current and GET /admin/rounds expose pool_address. - RND-22/DAT-11 stay "da implementare" for B-82: the payout still spends every confirmed coin at the round's address, not only the participants' outpoints (participants carry no bet_vout yet). - tests/unit/conftest.py: every test now gets a throwaway master key. Opening a round derives an address, and tests that merely opened a round were silently reading the real key from the local .env. - Existing tests changed because the spec changed: payout fixtures give their round a pool_address; the reconciler's address-cache test uses two payouts of one round (rows of different rounds no longer share an address); own_address_for now takes the round id. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
"""B-41: own_address_for is the single place tx/confirmation.py and
|
|
tx/reconcile.py derive a PendingTransaction's own address from — a payout's
|
|
address must always be the pool's, everything else the actual user's."""
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
from app.config import settings
|
|
from app.db.base import Base
|
|
from app.db.models import User
|
|
from app.tx.pending_address import own_address_for
|
|
|
|
|
|
@pytest.fixture
|
|
async def session_factory(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(settings, "master_key_path", str(tmp_path / "master.xprv.enc"))
|
|
monkeypatch.setattr(
|
|
settings,
|
|
"xprv_encryption_key",
|
|
__import__("cryptography.fernet", fromlist=["Fernet"]).Fernet.generate_key().decode(),
|
|
)
|
|
from app.wallet import hd
|
|
|
|
hd._account_key = None
|
|
hd.generate_master_key()
|
|
|
|
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
yield async_sessionmaker(engine, expire_on_commit=False)
|
|
await engine.dispose()
|
|
hd._account_key = None
|
|
|
|
|
|
async def test_payout_uses_the_pool_address_stored_on_its_round(session_factory):
|
|
"""B-80 (DAT-60): the address comes from the round row, whatever the user_id."""
|
|
from app.db.models import Round
|
|
from app.wallet.hd import derive_pool_address
|
|
|
|
async with session_factory() as session:
|
|
session.add(Round(id=7, status="paying_out", pool_address=derive_pool_address(7)))
|
|
session.add(Round(id=8, status="closed", pool_address=derive_pool_address(8)))
|
|
await session.flush()
|
|
address_7 = await own_address_for(session, "payout", None, 7)
|
|
address_8 = await own_address_for(session, "payout", None, 8)
|
|
|
|
assert address_7 == derive_pool_address(7)
|
|
assert address_8 == derive_pool_address(8)
|
|
assert address_7 != address_8
|
|
|
|
|
|
async def test_payout_of_a_round_without_a_pool_address_is_an_error(session_factory):
|
|
from app.db.models import Round
|
|
|
|
async with session_factory() as session:
|
|
session.add(Round(id=9, status="closed")) # pool_address stays ""
|
|
await session.flush()
|
|
with pytest.raises(ValueError):
|
|
await own_address_for(session, "payout", None, 9)
|
|
with pytest.raises(ValueError):
|
|
await own_address_for(session, "payout", None, None)
|
|
|
|
|
|
@pytest.mark.parametrize("kind", ["bet", "withdrawal"])
|
|
async def test_bet_and_withdrawal_use_the_users_own_address(session_factory, kind):
|
|
from app.wallet.hd import derive_user_address
|
|
|
|
async with session_factory() as session:
|
|
user = User(username="alice", password_hash="x", derivation_index=3, address=derive_user_address(3))
|
|
session.add(user)
|
|
await session.flush()
|
|
|
|
address = await own_address_for(session, kind, user.id)
|
|
|
|
assert address == derive_user_address(3)
|