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>
56 lines
2.1 KiB
Python
56 lines
2.1 KiB
Python
"""add pool_address to rounds
|
|
|
|
Revision ID: 477bddae1a84
|
|
Revises: 283844a44b4a
|
|
Create Date: 2026-10-14 12:28:11.529107
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '477bddae1a84'
|
|
down_revision: Union[str, Sequence[str], None] = '283844a44b4a'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
_ACTIVE = "'open', 'closing', 'drawing', 'paying_out'"
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Upgrade schema.
|
|
|
|
B-80 (DAT-60): every round gets its own pool address. Rounds that already exist were
|
|
all paid to the one shared pool address, the reserve address m/84'/746'/0'/1/0, so
|
|
that is what they are backfilled with and what their payout keeps spending.
|
|
|
|
The column is added with add_column, never batch_alter_table: on SQLite the latter
|
|
rebuilds the table and silently drops ix_rounds_single_active (B-97, DAT-56). The
|
|
server default is only there to let NOT NULL be added to a table that has rows.
|
|
"""
|
|
op.add_column("rounds", sa.Column("pool_address", sa.String(length=128), nullable=False, server_default=""))
|
|
|
|
connection = op.get_bind()
|
|
has_rounds = connection.execute(sa.text("SELECT COUNT(*) FROM rounds")).scalar()
|
|
if has_rounds:
|
|
# Derived only when there is something to backfill, so a fresh database (and the
|
|
# migration test) needs no master key. Imported here for the same reason.
|
|
from app.wallet.hd import derive_pool_address
|
|
|
|
connection.execute(
|
|
sa.text("UPDATE rounds SET pool_address = :address WHERE pool_address = ''"),
|
|
{"address": derive_pool_address()},
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Downgrade schema."""
|
|
# drop_column on SQLite is a table rebuild too, which loses the expression index:
|
|
# recreate it (B-97).
|
|
op.drop_column("rounds", "pool_address")
|
|
op.execute("DROP INDEX IF EXISTS ix_rounds_single_active")
|
|
op.execute(f"CREATE UNIQUE INDEX ix_rounds_single_active ON rounds ((1)) WHERE status IN ({_ACTIVE})")
|