Credits a user's internal balance once a UTXO on their deposit address reaches 1 confirmation, keeping utxo_events as the source of truth and cached_balance_sats as a derived read cache. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
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.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()
|
|
|
|
return newly_credited
|