Files
plm-lottery/tests/unit/test_pending_address.py
T
davide 4124dc08e6 Check confirmation/existence via scripthash history, not verbose replies (B-41)
poll_once and reconcile.py's existence check both called
blockchain.transaction.get(txid, verbose=True). Several Electrum server
implementations and versions reject the verbose flag outright
("verbose transactions are currently unsupported"), which would have
meant no confirmations and no reconciliation ever running against such
a server, read as a plain transport error. reconcile.py additionally
decided whether to abandon a transaction - releasing its funds - by
substring-matching the error text ("missing", "not found", ...), which
only works against ElectrumX's specific wording.

Both now ask blockchain.scripthash.get_history for the address that
owns every input of the transaction (a user's own address for a
bet/withdrawal, the pool address for a payout) and look for the txid in
the result: present with height > 0 means confirmed, present with
height <= 0 means still in the mempool, absent means the server
doesn't know it. get_history is a plain, universally-supported Electrum
method, and "not in the list" replaces the old substring-matching
entirely - no more guessing at error wording to decide whether to
release funds. History is cached per scripthash within one pass, since
every "payout" row shares the same pool address.

New app/tx/pending_address.py factors out own_address_for (the
address derivation was previously duplicated informally inside
tx/broadcast.py's signing context) so confirmation.py and reconcile.py
share one definition instead of two that could compute different
addresses for the same row.

tests/unit/test_confirmation.py and test_reconcile.py needed real User
rows and a master-key bootstrap they didn't have before, since address
derivation is now exercised for real rather than assumed. Suite grows
from 217 to 222 tests. BUGS.md moves B-41 to Previously fixed - no
Medium-severity finding remains open.
2026-07-27 15:27:58 +02:00

56 lines
1.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_regardless_of_user_id(session_factory):
from app.wallet.hd import derive_pool_address
async with session_factory() as session:
address = await own_address_for(session, "payout", None)
assert address == derive_pool_address()
@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)