v1.0.0-dev: slim to the real product — merchant state machine only
This commit is contained in:
parent
c31518309d
commit
b4a81e0ab8
15 changed files with 286 additions and 2503 deletions
|
|
@ -30,19 +30,23 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from typing import Awaitable, Callable, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from cardano_checkout.invoice import Invoice, InvoiceStatus
|
||||
from cardano_checkout.oracles import (
|
||||
KNOWN_TOKENS,
|
||||
convert_token_to_lovelace,
|
||||
convert_usd_to_lovelace,
|
||||
get_ada_usd_price,
|
||||
)
|
||||
from cardano_checkout.store import InvoiceStore
|
||||
|
||||
# Consumer-supplied pricing callable: takes a USD amount (float),
|
||||
# returns the current-market lovelace equivalent as int. Invoked by
|
||||
# :func:`reprice_expired_invoices` to generate fresh quotes when an
|
||||
# invoice's quote window lapses without payment.
|
||||
#
|
||||
# SDK intentionally does NOT ship an oracle. Consumers wire whatever
|
||||
# price source they trust (CoinGecko, Koios ticker, their own DEX feed,
|
||||
# or a constant for tests).
|
||||
PriceFn = Callable[[float], Awaitable[int]]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KOIOS_URL = "https://api.koios.rest/api/v1/address_utxos"
|
||||
|
|
@ -164,33 +168,12 @@ async def evaluate_utxos(
|
|||
if qty > 0:
|
||||
received_assets[asset_id] = received_assets.get(asset_id, 0) + qty
|
||||
|
||||
# Convert native assets to lovelace equivalent via DexHunter.
|
||||
asset_lovelace = 0
|
||||
for asset_id, qty in received_assets.items():
|
||||
if "." not in asset_id:
|
||||
continue
|
||||
policy_id, asset_name_hex = asset_id.split(".", 1)
|
||||
|
||||
decimals = 0
|
||||
for token_info in KNOWN_TOKENS.values():
|
||||
if token_info.get("policy_id") == policy_id:
|
||||
decimals = token_info.get("decimals", 0)
|
||||
break
|
||||
|
||||
try:
|
||||
lv = await convert_token_to_lovelace(
|
||||
policy_id, asset_name_hex, qty, decimals
|
||||
)
|
||||
if lv is not None:
|
||||
asset_lovelace += lv
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[cardano-monitor] Failed to convert asset %s to lovelace: %s",
|
||||
asset_id[:20],
|
||||
e,
|
||||
)
|
||||
|
||||
total_value = raw_lovelace + asset_lovelace
|
||||
# ADA-only matching. Any native tokens landed in the same UTxOs are
|
||||
# recorded in received_assets for visibility but do NOT contribute to
|
||||
# the payment-matched total. Consumers who want to accept stablecoins
|
||||
# or other native tokens wrap this function with their own asset-to-
|
||||
# lovelace converter before comparing against expected_lovelace.
|
||||
total_value = raw_lovelace
|
||||
|
||||
if expected_lovelace == 0:
|
||||
# Degenerate case — any payment at all counts.
|
||||
|
|
@ -313,23 +296,35 @@ async def check_pending_invoices(
|
|||
|
||||
async def reprice_expired_invoices(
|
||||
store: InvoiceStore,
|
||||
*,
|
||||
price_fn: PriceFn,
|
||||
window_minutes: int = DEFAULT_PAYMENT_WINDOW_MINUTES,
|
||||
max_repricings: int = DEFAULT_MAX_REPRICINGS,
|
||||
limit: int = 100,
|
||||
) -> int:
|
||||
"""Reprice PENDING invoices whose expiry has passed.
|
||||
|
||||
Pulls the current ADA/USD oracle price, recalculates ``expected_lovelace``
|
||||
from the invoice's ``usd_amount``, resets ``expires_at`` to
|
||||
``now + window_minutes``, and tracks reprice count in ``invoice.metadata``
|
||||
under the key ``repriced_count``. After ``max_repricings`` the invoice
|
||||
is transitioned to :class:`InvoiceStatus.EXPIRED`.
|
||||
Calls the consumer-supplied ``price_fn(usd_amount) -> lovelace`` to
|
||||
recompute ``expected_lovelace`` at current market. Resets ``expires_at``
|
||||
to ``now + window_minutes`` and increments ``invoice.metadata["repriced_count"]``.
|
||||
After ``max_repricings`` the invoice transitions to
|
||||
:class:`InvoiceStatus.EXPIRED`.
|
||||
|
||||
Args:
|
||||
store: Persistence backend.
|
||||
window_minutes: New expiry window per reprice. Matches the host app's
|
||||
platform-config-driven value of 15 minutes by default.
|
||||
max_repricings: Give-up threshold. the host app default is 3.
|
||||
price_fn: Async callable that takes a USD amount and returns the
|
||||
current lovelace equivalent. Consumer-supplied — the SDK does
|
||||
not ship an oracle. A simple wiring looks like::
|
||||
|
||||
from cardano_checkout.monitor import reprice_expired_invoices
|
||||
|
||||
async def my_price_fn(usd: float) -> int:
|
||||
rate = await coingecko_fetch_ada_usd() # your code
|
||||
return int(round(usd / rate * 1_000_000))
|
||||
|
||||
await reprice_expired_invoices(store, price_fn=my_price_fn)
|
||||
window_minutes: New expiry window per reprice. the host app default 15.
|
||||
max_repricings: Give-up threshold. the host app default 3.
|
||||
limit: Max pending invoices to process per call.
|
||||
|
||||
Returns:
|
||||
|
|
@ -350,13 +345,6 @@ async def reprice_expired_invoices(
|
|||
"[cardano-monitor] Repricing %d expired invoice(s)", len(expired_candidates)
|
||||
)
|
||||
|
||||
ada_price = await get_ada_usd_price()
|
||||
if ada_price <= 0:
|
||||
logger.warning(
|
||||
"[cardano-monitor] Cannot reprice — ADA price unavailable"
|
||||
)
|
||||
return 0
|
||||
|
||||
new_expires_at = now + timedelta(minutes=window_minutes)
|
||||
updated = 0
|
||||
|
||||
|
|
@ -386,11 +374,20 @@ async def reprice_expired_invoices(
|
|||
)
|
||||
continue
|
||||
|
||||
new_lovelace = await convert_usd_to_lovelace(usd_amount)
|
||||
if new_lovelace == 0:
|
||||
try:
|
||||
new_lovelace = await price_fn(usd_amount)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[cardano-monitor] invoice %s: lovelace conversion returned 0, skipping",
|
||||
"[cardano-monitor] invoice %s: price_fn raised %s, skipping",
|
||||
invoice.id,
|
||||
e,
|
||||
)
|
||||
continue
|
||||
if new_lovelace <= 0:
|
||||
logger.warning(
|
||||
"[cardano-monitor] invoice %s: price_fn returned %d, skipping",
|
||||
invoice.id,
|
||||
new_lovelace,
|
||||
)
|
||||
continue
|
||||
|
||||
|
|
@ -398,18 +395,15 @@ async def reprice_expired_invoices(
|
|||
invoice.expected_lovelace = new_lovelace
|
||||
invoice.expires_at = new_expires_at
|
||||
invoice.metadata["repriced_count"] = repriced_count + 1
|
||||
invoice.metadata["ada_price_usd"] = round(ada_price, 4)
|
||||
|
||||
await store.update(invoice)
|
||||
updated += 1
|
||||
|
||||
logger.info(
|
||||
"[cardano-monitor] Repriced invoice %s: %d -> %d lovelace "
|
||||
"(ADA=$%.4f, reprice #%d)",
|
||||
"[cardano-monitor] Repriced invoice %s: %d -> %d lovelace (reprice #%d)",
|
||||
invoice.id,
|
||||
old_lovelace or 0,
|
||||
new_lovelace,
|
||||
ada_price,
|
||||
repriced_count + 1,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue