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:
Cobb Hayes 2026-05-27 11:15:03 -07:00
parent af41f945b1
commit c592a58148
10 changed files with 72 additions and 141 deletions

108
README.md
View file

@ -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.

View file

@ -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))

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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*"]

View file

@ -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

View file

@ -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,

View file

@ -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