from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.audit.log import write_audit_log from app.db.models import UtxoEvent from app.rounds.events import broadcaster from app.wallet.balance import recompute_balance async def credit_confirmed_utxos(session: AsyncSession, user_id: int, entries: list[dict]) -> int: """Insert utxo_events for newly-confirmed entries from an Electrum `listunspent` response (idempotent on txid+vout), refresh the user's cached balance. Returns the number of newly-credited UTXOs. entries: [{"tx_hash": ..., "tx_pos": ..., "height": ..., "value": ...}, ...] height <= 0 means unconfirmed (mempool) per the Electrum protocol convention — skipped, since the spec requires 1 confirmation before crediting. """ existing_keys = { (txid, vout) for txid, vout in ( await session.execute(select(UtxoEvent.txid, UtxoEvent.vout).where(UtxoEvent.user_id == user_id)) ).all() } newly_credited = 0 for entry in entries: if entry["height"] <= 0: continue key = (entry["tx_hash"], entry["tx_pos"]) if key in existing_keys: continue session.add( UtxoEvent( user_id=user_id, txid=entry["tx_hash"], vout=entry["tx_pos"], amount_sats=entry["value"], confirmed_height=entry["height"], ) ) await write_audit_log( session, "deposit_credited", {"txid": entry["tx_hash"], "vout": entry["tx_pos"], "amount_sats": entry["value"]}, user_id=user_id, ) newly_credited += 1 if newly_credited: await session.flush() await recompute_balance(session, user_id) await session.commit() broadcaster.publish() # nudges this user's dashboard to refetch its balance instantly return newly_credited