Public-flip audit: drop audit-ticket prefixes + topology refs + AI scaffolding
cardano-api: strip 'Fix #N:' audit-ticket prefixes from inline comments (was 50+ in main.py), drop hardening-pass changelog blocks from module docstring, rewrite README to drop deploy paths + marketing sections, keep tier/auth/TTL + policy IDs. cardano-checkout-py: drop TradeCraft lineage refs, swap chromaticcraft/tradecraft test fixtures for acme/globex, repository URL → git.sulkta.com.
This commit is contained in:
parent
af41f945b1
commit
c592a58148
10 changed files with 72 additions and 141 deletions
108
README.md
108
README.md
|
|
@ -1,35 +1,16 @@
|
||||||
# cardano-checkout
|
# cardano-checkout
|
||||||
|
|
||||||
Merchant-side Cardano payment lifecycle in Python. Zero-custody by design.
|
Merchant-side Cardano payment lifecycle in Python. Zero-custody.
|
||||||
|
|
||||||
**What we ship:** the invoice state machine + UTxO watcher + reprice
|
Ships the invoice state machine + UTxO watcher + reprice loop.
|
||||||
loop. Per-invoice HD-derived receive addresses, Koios polling, confirm
|
Per-invoice HD-derived receive addresses, Koios polling, confirm /
|
||||||
/ underpay / overpay classification, time-windowed repricing against
|
underpay / overpay classification, time-windowed repricing against
|
||||||
your own oracle.
|
your own oracle.
|
||||||
|
|
||||||
**What we don't ship:** Cardano primitives. Address derivation, chain
|
Does NOT ship Cardano primitives. Address derivation, chain context,
|
||||||
context, transaction building, native-script minting, signing — those
|
transaction building, native-script minting, signing — use
|
||||||
are all [pycardano](https://github.com/Python-Cardano/pycardano)'s job.
|
[pycardano](https://github.com/Python-Cardano/pycardano) directly.
|
||||||
pycardano is mature, actively maintained (0.19.x as of 2026), and
|
This library slots next to it.
|
||||||
covers every primitive cleanly. This library slots next to it — no
|
|
||||||
wrapping, no leaky abstraction, no second API to learn.
|
|
||||||
|
|
||||||
## Why this exists
|
|
||||||
|
|
||||||
Nothing else in the Python ecosystem (or any ecosystem — we checked)
|
|
||||||
packages zero-custody merchant Cardano payments as a reusable library.
|
|
||||||
Closest adjacents are all one of:
|
|
||||||
|
|
||||||
- CIP-30 browser-wallet plugins (customer-signs, not server-watches)
|
|
||||||
- cardano-cli vending machines that watch a single static address (no
|
|
||||||
xpub, no per-invoice derivation)
|
|
||||||
- SaaS APIs (NMKR) — not libraries
|
|
||||||
- Dormant / pre-1.0 grabs from the 2021-2023 era
|
|
||||||
|
|
||||||
The merchant state machine — "derive an address, watch for payment,
|
|
||||||
confirm within tolerance, reprice if the quote lapses, emit a confirmed
|
|
||||||
callback" — is what we package. You keep full control of everything
|
|
||||||
else by using pycardano directly.
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
|
@ -42,7 +23,7 @@ from cardano_checkout import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Your oracle — we don't ship one. Anything async returning int lovelace works.
|
# Your oracle. Anything async returning int lovelace works.
|
||||||
async def my_price_fn(usd: float) -> int:
|
async def my_price_fn(usd: float) -> int:
|
||||||
rate = await fetch_ada_usd_somewhere() # CoinGecko, Koios, fixed rate, etc.
|
rate = await fetch_ada_usd_somewhere() # CoinGecko, Koios, fixed rate, etc.
|
||||||
return int(round(usd / rate * 1_000_000))
|
return int(round(usd / rate * 1_000_000))
|
||||||
|
|
@ -51,20 +32,17 @@ async def my_price_fn(usd: float) -> int:
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
store = InMemoryStore() # swap for your SQLAlchemy / asyncpg / sqlite adapter
|
store = InMemoryStore() # swap for your SQLAlchemy / asyncpg / sqlite adapter
|
||||||
|
|
||||||
# Create an invoice. In production you'd derive the receive address from
|
|
||||||
# your wallet xpub via pycardano — see the "Deriving addresses" section.
|
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
id="ord-0042",
|
id="ord-0042",
|
||||||
merchant_id="chromaticcraft",
|
merchant_id="my-shop",
|
||||||
derivation_index=42,
|
derivation_index=42,
|
||||||
receive_address="addr1q...", # derived via pycardano — your code
|
receive_address="addr1q...", # derive via pycardano
|
||||||
expected_lovelace=5_000_000,
|
expected_lovelace=5_000_000,
|
||||||
usd_amount=2.50,
|
usd_amount=2.50,
|
||||||
expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
|
expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
|
||||||
)
|
)
|
||||||
await store.create(invoice)
|
await store.create(invoice)
|
||||||
|
|
||||||
# Run the background scheduler — Koios poll every 15s + reprice every 60s.
|
|
||||||
scheduler = InvoiceScheduler(store=store, price_fn=my_price_fn)
|
scheduler = InvoiceScheduler(store=store, price_fn=my_price_fn)
|
||||||
await scheduler.start()
|
await scheduler.start()
|
||||||
|
|
||||||
|
|
@ -77,12 +55,10 @@ asyncio.run(main())
|
||||||
|
|
||||||
## Deriving addresses with pycardano
|
## Deriving addresses with pycardano
|
||||||
|
|
||||||
We used to wrap this. You don't need the wrapper.
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pycardano import HDWallet, Address, Network
|
from pycardano import HDWallet, Address, Network
|
||||||
|
|
||||||
# Your merchant's account-level xpub — the xpub is public, not a secret.
|
# Account-level xpub — public, not a secret.
|
||||||
xpub_hex = "..."
|
xpub_hex = "..."
|
||||||
|
|
||||||
account = HDWallet.from_xpub(bytes.fromhex(xpub_hex))
|
account = HDWallet.from_xpub(bytes.fromhex(xpub_hex))
|
||||||
|
|
@ -100,16 +76,9 @@ def derive_address(account: HDWallet, index: int, network=Network.MAINNET) -> st
|
||||||
addr = derive_address(account, index=42)
|
addr = derive_address(account, index=42)
|
||||||
```
|
```
|
||||||
|
|
||||||
Six lines of pycardano that read cleanly against
|
## NFT cert: CIP-25 v2 metadata
|
||||||
[their docs](https://pycardano.readthedocs.io/). Our old wrapper would
|
|
||||||
have added one function call but made you learn our API instead of
|
|
||||||
pycardano's. Skip it.
|
|
||||||
|
|
||||||
## NFT cert-of-authenticity: CIP-25 v2 metadata
|
Copy-paste builder for an on-chain cert per paid order. No dep.
|
||||||
|
|
||||||
If you want each paid order to ship with an on-chain NFT cert, here's
|
|
||||||
the CIP-25 v2 metadata builder as a copy-paste. It fits in your own
|
|
||||||
code — no dep, no wrapper.
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def build_cip25_metadata(
|
def build_cip25_metadata(
|
||||||
|
|
@ -150,20 +119,18 @@ def build_cip25_metadata(
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Hand that dict to pycardano's `AuxiliaryData(Metadata({...}))` when you
|
Hand the dict to pycardano's `AuxiliaryData(Metadata({...}))` when
|
||||||
build the mint tx. Straight pycardano from there on.
|
building the mint tx.
|
||||||
|
|
||||||
## Implementing your own InvoiceStore
|
## Implementing your own InvoiceStore
|
||||||
|
|
||||||
The SDK's `InvoiceStore` is a Protocol — implement the six methods
|
`InvoiceStore` is a Protocol — implement six methods against whatever
|
||||||
against whatever backend you want (SQLAlchemy, asyncpg, SQLite,
|
backend you want (SQLAlchemy, asyncpg, SQLite, in-memory).
|
||||||
in-memory for tests).
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from cardano_checkout import Invoice, InvoiceStatus, InvoiceStore
|
from cardano_checkout import Invoice, InvoiceStatus, InvoiceStore
|
||||||
|
|
||||||
class MySqliteStore:
|
class MySqliteStore:
|
||||||
# Implement these six methods and you're a valid InvoiceStore.
|
|
||||||
async def create(self, invoice: Invoice) -> None: ...
|
async def create(self, invoice: Invoice) -> None: ...
|
||||||
async def get(self, invoice_id: str) -> Invoice | None: ...
|
async def get(self, invoice_id: str) -> Invoice | None: ...
|
||||||
async def list_by_status(self, status: InvoiceStatus, limit: int = 100) -> list[Invoice]: ...
|
async def list_by_status(self, status: InvoiceStatus, limit: int = 100) -> list[Invoice]: ...
|
||||||
|
|
@ -172,38 +139,29 @@ class MySqliteStore:
|
||||||
async def record_tx(self, invoice_id: str, tx_hash: str, lovelace_delta: int) -> None: ...
|
async def record_tx(self, invoice_id: str, tx_hash: str, lovelace_delta: int) -> None: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
See `InMemoryStore` in `cardano_checkout/store.py` for a 90-line
|
See `InMemoryStore` in `cardano_checkout/store.py` for a reference impl.
|
||||||
reference implementation.
|
|
||||||
|
|
||||||
## Status (1.0.0-dev)
|
## Modules
|
||||||
|
|
||||||
| Module | Purpose |
|
| Module | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `invoice.py` | `Invoice` dataclass + `InvoiceStatus` enum — payment lifecycle states |
|
| `invoice.py` | `Invoice` dataclass + `InvoiceStatus` enum |
|
||||||
| `store.py` | `InvoiceStore` Protocol + `InMemoryStore` reference impl |
|
| `store.py` | `InvoiceStore` Protocol + `InMemoryStore` reference impl |
|
||||||
| `monitor.py` | `check_address_utxos` (Koios), `evaluate_utxos` (ADA matching + tolerance), `check_pending_invoices`, `reprice_expired_invoices` (takes your `price_fn`) |
|
| `monitor.py` | `check_address_utxos` (Koios), `evaluate_utxos`, `check_pending_invoices`, `reprice_expired_invoices` |
|
||||||
| `scheduler.py` | `InvoiceScheduler` — APScheduler wrapper, runs check/reprice on the same 15s/60s cadence TradeCraft's used in production since 2025 |
|
| `scheduler.py` | `InvoiceScheduler` — APScheduler wrapper, 15s check + 60s reprice |
|
||||||
|
|
||||||
All tests offline, 26/26 green. Two direct deps: `httpx` (Koios calls),
|
Two direct deps: `httpx`, `apscheduler`. No pycardano dep.
|
||||||
`apscheduler` (background scheduling). No pycardano dep — that's the
|
|
||||||
consumer's pairing.
|
|
||||||
|
|
||||||
## Design principles
|
## Design
|
||||||
|
|
||||||
1. **Protocol-first.** Persistence, pricing, and any other side-effect
|
1. **Protocol-first.** Persistence, pricing, side-effects through
|
||||||
concern goes through a consumer-supplied interface. The SDK has no
|
consumer-supplied interfaces.
|
||||||
opinion about your database, your oracle, or your ORM.
|
2. **Use pycardano directly.** No wrapping of primitives.
|
||||||
2. **Use pycardano directly.** We don't wrap primitives. If you need
|
3. **Zero-custody.** Merchant keys never touch this code. xpub-derived
|
||||||
address derivation, chain context, or transaction building, import
|
addresses, UTxO observation, state transitions. Funds flow directly
|
||||||
pycardano. Our package sits next to it, not on top.
|
between customer and merchant wallets.
|
||||||
3. **Zero-custody.** The merchant's keys never touch this code. We
|
4. **Offline-first tests.** Koios + price oracles stubbed via fixture.
|
||||||
handle xpub-derived addresses (public), UTxO observation (chain),
|
|
||||||
and state transitions (the store). Funds flow directly between
|
|
||||||
customer and merchant wallets. We are not a custodian.
|
|
||||||
4. **Offline-first tests.** Koios HTTP and price oracles are stubbed
|
|
||||||
or swapped via fixture. No network in CI. Live tests (preprod mint
|
|
||||||
round-trips, real Koios) are a consumer-side concern.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache-2.0 — matches the broader Cardano tooling ecosystem.
|
Apache-2.0.
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
"""cardano-checkout — merchant-side Cardano payment lifecycle in Python.
|
"""cardano-checkout — merchant-side Cardano payment lifecycle in Python.
|
||||||
|
|
||||||
Zero-custody by design: the merchant brings a wallet xpub and an
|
Zero-custody: the merchant brings a wallet xpub and an
|
||||||
:class:`~cardano_checkout.store.InvoiceStore` implementation. The SDK
|
:class:`~cardano_checkout.store.InvoiceStore` implementation. The SDK
|
||||||
owns the payment lifecycle — per-invoice receive-address bookkeeping,
|
owns the payment lifecycle — per-invoice receive-address bookkeeping,
|
||||||
Koios UTxO polling, confirm / underpay / overpay classification, and
|
Koios UTxO polling, confirm / underpay / overpay classification, and
|
||||||
time-windowed repricing against a consumer-supplied oracle.
|
time-windowed repricing against a consumer-supplied oracle.
|
||||||
|
|
||||||
**The SDK deliberately does NOT ship Cardano primitives.** Address
|
Does NOT ship Cardano primitives. Address derivation, transaction
|
||||||
derivation, transaction building, chain context, and native-script
|
building, chain context, and native-script minting live in
|
||||||
minting all live in `pycardano <https://github.com/Python-Cardano/pycardano>`_
|
`pycardano <https://github.com/Python-Cardano/pycardano>`_. See the
|
||||||
and are consumer concerns. See the README for the pairing pattern and
|
README for the pairing pattern and the CIP-25 v2 metadata-builder
|
||||||
for the CIP-25 v2 metadata-builder snippet (a 60-line helper that fits
|
snippet.
|
||||||
anywhere in your own code without needing a separate dep).
|
|
||||||
|
|
||||||
Quick start::
|
Quick start::
|
||||||
|
|
||||||
|
|
@ -20,7 +19,6 @@ Quick start::
|
||||||
store = InMemoryStore() # or your SQLAlchemy / asyncpg / sqlite adapter
|
store = InMemoryStore() # or your SQLAlchemy / asyncpg / sqlite adapter
|
||||||
|
|
||||||
async def my_price_fn(usd: float) -> int:
|
async def my_price_fn(usd: float) -> int:
|
||||||
# your oracle — CoinGecko / Koios ticker / fixed rate in tests
|
|
||||||
rate = await fetch_ada_usd_rate()
|
rate = await fetch_ada_usd_rate()
|
||||||
return int(round(usd / rate * 1_000_000))
|
return int(round(usd / rate * 1_000_000))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ derived from the merchant's xpub, an expected amount in lovelace, a
|
||||||
USD-denominated label, and a lifecycle state that transitions as the
|
USD-denominated label, and a lifecycle state that transitions as the
|
||||||
chain confirms payment.
|
chain confirms payment.
|
||||||
|
|
||||||
The Invoice is deliberately framework-agnostic — persistence is
|
Persistence is delegated to an :class:`InvoiceStore`
|
||||||
delegated to an :class:`InvoiceStore` (see :mod:`cardano_checkout.store`).
|
(see :mod:`cardano_checkout.store`).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,12 @@ Koios endpoint used::
|
||||||
POST https://api.koios.rest/api/v1/address_utxos
|
POST https://api.koios.rest/api/v1/address_utxos
|
||||||
Body: {"_addresses": ["addr1..."]}
|
Body: {"_addresses": ["addr1..."]}
|
||||||
|
|
||||||
Status transitions applied here::
|
Status transitions::
|
||||||
|
|
||||||
PENDING ──► CONFIRMED (received >= expected * CONFIRM_TOLERANCE)
|
PENDING ──► CONFIRMED (received >= expected * CONFIRM_TOLERANCE)
|
||||||
PENDING ──► UNDERPAID (received > 0 but below tolerance)
|
PENDING ──► UNDERPAID (received > 0 but below tolerance)
|
||||||
PENDING ──► OVERPAID (received >= expected * OVERPAY_THRESHOLD)
|
PENDING ──► OVERPAID (received >= expected * OVERPAY_THRESHOLD)
|
||||||
PENDING ──► EXPIRED (after reprice_count exhausts — see reprice_expired_invoices)
|
PENDING ──► EXPIRED (after reprice_count exhausts — see reprice_expired_invoices)
|
||||||
|
|
||||||
Behavioral shape is identical to the original TradeCraft ``services/cardano_monitor.py``:
|
|
||||||
same polling intervals, same Koios URL, same 2% confirm / overpay tolerances.
|
|
||||||
The only change is that persistence is now delegated to the store Protocol
|
|
||||||
instead of being welded to SQLAlchemy + the ``CardanoPayment`` model.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -41,10 +36,6 @@ from cardano_checkout.store import InvoiceStore
|
||||||
# returns the current-market lovelace equivalent as int. Invoked by
|
# returns the current-market lovelace equivalent as int. Invoked by
|
||||||
# :func:`reprice_expired_invoices` to generate fresh quotes when an
|
# :func:`reprice_expired_invoices` to generate fresh quotes when an
|
||||||
# invoice's quote window lapses without payment.
|
# 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]]
|
PriceFn = Callable[[float], Awaitable[int]]
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -52,11 +43,11 @@ logger = logging.getLogger(__name__)
|
||||||
KOIOS_URL = "https://api.koios.rest/api/v1/address_utxos"
|
KOIOS_URL = "https://api.koios.rest/api/v1/address_utxos"
|
||||||
KOIOS_TIMEOUT = 15 # seconds
|
KOIOS_TIMEOUT = 15 # seconds
|
||||||
|
|
||||||
# Tolerance for confirming payment (2%) — unchanged from v0.1 / TradeCraft.
|
# Tolerance for confirming payment (2%).
|
||||||
CONFIRM_TOLERANCE = 0.98
|
CONFIRM_TOLERANCE = 0.98
|
||||||
OVERPAY_THRESHOLD = 1.02
|
OVERPAY_THRESHOLD = 1.02
|
||||||
|
|
||||||
# Default reprice cap + window (matches TradeCraft defaults).
|
# Default reprice cap + window.
|
||||||
DEFAULT_MAX_REPRICINGS = 3
|
DEFAULT_MAX_REPRICINGS = 3
|
||||||
DEFAULT_PAYMENT_WINDOW_MINUTES = 15
|
DEFAULT_PAYMENT_WINDOW_MINUTES = 15
|
||||||
|
|
||||||
|
|
@ -108,8 +99,7 @@ async def check_address_utxos(
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
# Backwards-compatible alias — monitor.py in TradeCraft imports the private name.
|
# Leading-underscore alias kept for callers that imported the private name.
|
||||||
# Keeping a leading-underscore alias so the TradeCraft shim can still reach it.
|
|
||||||
_check_address_utxos = check_address_utxos
|
_check_address_utxos = check_address_utxos
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,7 +123,7 @@ async def evaluate_utxos(
|
||||||
- ``received_assets`` — ``{policy_id.asset_name_hex: quantity}``.
|
- ``received_assets`` — ``{policy_id.asset_name_hex: quantity}``.
|
||||||
- ``latest_tx_hash`` — most recent observed tx hash, or None if no UTXOs.
|
- ``latest_tx_hash`` — most recent observed tx hash, or None if no UTXOs.
|
||||||
|
|
||||||
Status rules mirror TradeCraft exactly:
|
Status rules:
|
||||||
|
|
||||||
- No UTXOs → ``PENDING`` (no change)
|
- No UTXOs → ``PENDING`` (no change)
|
||||||
- ``total_value >= expected * OVERPAY_THRESHOLD`` → ``OVERPAID`` (treated as confirmed)
|
- ``total_value >= expected * OVERPAY_THRESHOLD`` → ``OVERPAID`` (treated as confirmed)
|
||||||
|
|
@ -168,11 +158,10 @@ async def evaluate_utxos(
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
received_assets[asset_id] = received_assets.get(asset_id, 0) + qty
|
received_assets[asset_id] = received_assets.get(asset_id, 0) + qty
|
||||||
|
|
||||||
# ADA-only matching. Any native tokens landed in the same UTxOs are
|
# ADA-only matching. Native tokens in the same UTxOs are recorded in
|
||||||
# recorded in received_assets for visibility but do NOT contribute to
|
# received_assets for visibility but do NOT contribute to the
|
||||||
# the payment-matched total. Consumers who want to accept stablecoins
|
# payment-matched total. Wrap this function with your own
|
||||||
# or other native tokens wrap this function with their own asset-to-
|
# asset-to-lovelace converter to accept native tokens.
|
||||||
# lovelace converter before comparing against expected_lovelace.
|
|
||||||
total_value = raw_lovelace
|
total_value = raw_lovelace
|
||||||
|
|
||||||
if expected_lovelace == 0:
|
if expected_lovelace == 0:
|
||||||
|
|
@ -190,7 +179,7 @@ async def evaluate_utxos(
|
||||||
return new_status, raw_lovelace, total_value, received_assets, latest_tx_hash
|
return new_status, raw_lovelace, total_value, received_assets, latest_tx_hash
|
||||||
|
|
||||||
|
|
||||||
# Backwards-compatible alias.
|
# Leading-underscore alias for callers that imported the private name.
|
||||||
_evaluate_utxos = evaluate_utxos
|
_evaluate_utxos = evaluate_utxos
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -313,18 +302,17 @@ async def reprice_expired_invoices(
|
||||||
Args:
|
Args:
|
||||||
store: Persistence backend.
|
store: Persistence backend.
|
||||||
price_fn: Async callable that takes a USD amount and returns the
|
price_fn: Async callable that takes a USD amount and returns the
|
||||||
current lovelace equivalent. Consumer-supplied — the SDK does
|
current lovelace equivalent. Example::
|
||||||
not ship an oracle. A simple wiring looks like::
|
|
||||||
|
|
||||||
from cardano_checkout.monitor import reprice_expired_invoices
|
from cardano_checkout.monitor import reprice_expired_invoices
|
||||||
|
|
||||||
async def my_price_fn(usd: float) -> int:
|
async def my_price_fn(usd: float) -> int:
|
||||||
rate = await coingecko_fetch_ada_usd() # your code
|
rate = await coingecko_fetch_ada_usd()
|
||||||
return int(round(usd / rate * 1_000_000))
|
return int(round(usd / rate * 1_000_000))
|
||||||
|
|
||||||
await reprice_expired_invoices(store, price_fn=my_price_fn)
|
await reprice_expired_invoices(store, price_fn=my_price_fn)
|
||||||
window_minutes: New expiry window per reprice. TradeCraft default 15.
|
window_minutes: New expiry window per reprice. Default 15.
|
||||||
max_repricings: Give-up threshold. TradeCraft default 3.
|
max_repricings: Give-up threshold. Default 3.
|
||||||
limit: Max pending invoices to process per call.
|
limit: Max pending invoices to process per call.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,6 @@ The scheduler drives two jobs against a consumer-supplied
|
||||||
- :func:`cardano_checkout.monitor.check_pending_invoices` — every 15 seconds
|
- :func:`cardano_checkout.monitor.check_pending_invoices` — every 15 seconds
|
||||||
- :func:`cardano_checkout.monitor.reprice_expired_invoices` — every 60 seconds
|
- :func:`cardano_checkout.monitor.reprice_expired_invoices` — every 60 seconds
|
||||||
|
|
||||||
That's the *full* SDK job surface. The subscription-level + grace-period
|
|
||||||
jobs that the original TradeCraft scheduler shipped are TradeCraft-specific
|
|
||||||
(they touch ``Company``, ``Subscription``, ``SubscriptionPayment`` models
|
|
||||||
that are merchant-specific) — those live in
|
|
||||||
:mod:`cardano_checkout.tradecraft_compat` so TradeCraft can still import the
|
|
||||||
exact wrappers it has always used, without polluting the generic SDK.
|
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
from cardano_checkout.scheduler import InvoiceScheduler
|
from cardano_checkout.scheduler import InvoiceScheduler
|
||||||
|
|
@ -54,7 +47,7 @@ class InvoiceScheduler:
|
||||||
store: Persistence backend. Required.
|
store: Persistence backend. Required.
|
||||||
koios_url: Chain-query endpoint. Override for testnet / custom gateways.
|
koios_url: Chain-query endpoint. Override for testnet / custom gateways.
|
||||||
check_interval_seconds: How often to poll Koios for pending invoices.
|
check_interval_seconds: How often to poll Koios for pending invoices.
|
||||||
Defaults to 15 — identical to TradeCraft's production cadence.
|
Defaults to 15.
|
||||||
reprice_interval_seconds: How often to sweep for expired invoices.
|
reprice_interval_seconds: How often to sweep for expired invoices.
|
||||||
Defaults to 60.
|
Defaults to 60.
|
||||||
payment_window_minutes: Re-expiry window when repricing.
|
payment_window_minutes: Re-expiry window when repricing.
|
||||||
|
|
@ -87,9 +80,8 @@ class InvoiceScheduler:
|
||||||
|
|
||||||
async def _job_reprice_expired(self) -> None:
|
async def _job_reprice_expired(self) -> None:
|
||||||
if self.price_fn is None:
|
if self.price_fn is None:
|
||||||
# No oracle wired — skip repricing silently. Consumers that
|
# No oracle wired — skip repricing. Fixed-ADA invoices don't
|
||||||
# don't care about the USD-lock workflow (e.g. fixed-ADA
|
# need one.
|
||||||
# invoices) will never configure a price_fn; that's fine.
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await reprice_expired_invoices(
|
await reprice_expired_invoices(
|
||||||
|
|
@ -149,12 +141,9 @@ class InvoiceScheduler:
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Backwards-compatible free-function API
|
# Free-function API around a module-level default instance.
|
||||||
|
# Prefer the InvoiceScheduler class for anything nontrivial.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
#
|
|
||||||
# Early adopters may have imported ``start_cardano_scheduler`` / ``stop_cardano_scheduler``
|
|
||||||
# directly. Provide those as thin wrappers around a module-level default instance.
|
|
||||||
# Using the InvoiceScheduler class is preferred for anything nontrivial.
|
|
||||||
|
|
||||||
_default: Optional[InvoiceScheduler] = None
|
_default: Optional[InvoiceScheduler] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
"""Persistence abstraction for Invoice objects.
|
"""Persistence abstraction for Invoice objects.
|
||||||
|
|
||||||
The SDK does not prescribe a database. Consumers implement
|
The SDK does not prescribe a database. Consumers implement
|
||||||
:class:`InvoiceStore` against whatever backend suits them — SQLAlchemy
|
:class:`InvoiceStore` against whatever backend suits them — SQLAlchemy,
|
||||||
(TradeCraft pattern), SQLite (chromaticcraft pattern), Postgres raw
|
asyncpg, SQLite, in-memory dict.
|
||||||
(ADAMaps pattern), in-memory dict (tests).
|
|
||||||
|
|
||||||
All methods are async so the same Protocol works cleanly for both
|
All methods are async so the same Protocol works for both
|
||||||
asyncpg/asyncio-sqlalchemy backends and synchronous backends wrapped
|
asyncpg/asyncio-sqlalchemy backends and synchronous backends wrapped
|
||||||
with ``asyncio.to_thread``.
|
with ``asyncio.to_thread``.
|
||||||
|
|
||||||
This module also ships :class:`InMemoryStore` — a reference implementation
|
Also ships :class:`InMemoryStore` — a reference implementation used by
|
||||||
used by the test suite and useful as a drop-in for local development or
|
the test suite and useful for local development.
|
||||||
ephemeral workflows that don't need durability.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ test = ["pytest>=7", "pytest-asyncio>=0.23"]
|
||||||
dev = ["pytest>=7", "pytest-asyncio>=0.23", "ruff", "mypy"]
|
dev = ["pytest>=7", "pytest-asyncio>=0.23", "ruff", "mypy"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Repository = "http://192.168.0.5:3001/Sulkta-Coop/cardano-checkout-py"
|
Repository = "https://git.sulkta.com/Sulkta-Coop/cardano-checkout-py"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["cardano_checkout*"]
|
include = ["cardano_checkout*"]
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from cardano_checkout.invoice import Invoice, InvoiceStatus
|
||||||
def _make() -> Invoice:
|
def _make() -> Invoice:
|
||||||
return Invoice(
|
return Invoice(
|
||||||
id="inv_001",
|
id="inv_001",
|
||||||
merchant_id="chromaticcraft",
|
merchant_id="acme",
|
||||||
derivation_index=0,
|
derivation_index=0,
|
||||||
receive_address="addr1...",
|
receive_address="addr1...",
|
||||||
expected_lovelace=5_000_000, # 5 ADA
|
expected_lovelace=5_000_000, # 5 ADA
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ def _make(
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
return Invoice(
|
return Invoice(
|
||||||
id=id_,
|
id=id_,
|
||||||
merchant_id="chromaticcraft",
|
merchant_id="acme",
|
||||||
derivation_index=0,
|
derivation_index=0,
|
||||||
receive_address="addr1testreceive",
|
receive_address="addr1testreceive",
|
||||||
expected_lovelace=expected_lovelace,
|
expected_lovelace=expected_lovelace,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from cardano_checkout import InMemoryStore, Invoice, InvoiceStatus, InvoiceStore
|
||||||
|
|
||||||
def _make_invoice(
|
def _make_invoice(
|
||||||
id_: str = "inv_001",
|
id_: str = "inv_001",
|
||||||
merchant: str = "chromaticcraft",
|
merchant: str = "acme",
|
||||||
index: int = 0,
|
index: int = 0,
|
||||||
status: InvoiceStatus = InvoiceStatus.PENDING,
|
status: InvoiceStatus = InvoiceStatus.PENDING,
|
||||||
) -> Invoice:
|
) -> Invoice:
|
||||||
|
|
@ -135,8 +135,8 @@ async def test_list_by_status_honours_limit() -> None:
|
||||||
|
|
||||||
async def test_next_derivation_index_is_monotonic_per_merchant() -> None:
|
async def test_next_derivation_index_is_monotonic_per_merchant() -> None:
|
||||||
store = InMemoryStore()
|
store = InMemoryStore()
|
||||||
m1 = "chromaticcraft"
|
m1 = "acme"
|
||||||
m2 = "tradecraft"
|
m2 = "globex"
|
||||||
|
|
||||||
assert await store.next_derivation_index(m1) == 0
|
assert await store.next_derivation_index(m1) == 0
|
||||||
assert await store.next_derivation_index(m1) == 1
|
assert await store.next_derivation_index(m1) == 1
|
||||||
|
|
@ -149,7 +149,7 @@ async def test_create_bumps_index_cursor_if_higher() -> None:
|
||||||
store = InMemoryStore()
|
store = InMemoryStore()
|
||||||
await store.create(_make_invoice(id_="manual", index=7))
|
await store.create(_make_invoice(id_="manual", index=7))
|
||||||
|
|
||||||
nxt = await store.next_derivation_index("chromaticcraft")
|
nxt = await store.next_derivation_index("acme")
|
||||||
assert nxt == 8
|
assert nxt == 8
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue