diff --git a/README.md b/README.md index d20dcf9..4682d5a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,44 @@ # cardano-checkout -Merchant-side Cardano payment lifecycle in Python. Zero-custody. +Merchant-side Cardano payment lifecycle for Python. Zero-custody. -Ships the invoice state machine + UTxO watcher + reprice loop. -Per-invoice HD-derived receive addresses, Koios polling, confirm / -underpay / overpay classification, time-windowed repricing against -your own oracle. +`cardano-checkout` ships the invoice state machine, an on-chain UTxO +watcher, and a quote-reprice loop for accepting ADA payments at +per-invoice, HD-derived receive addresses. It polls [Koios](https://koios.rest) +for payment, classifies each invoice as confirmed / underpaid / overpaid +within a tolerance, and reprices expired quotes against a price oracle you +supply. -Does NOT ship Cardano primitives. Address derivation, chain context, -transaction building, native-script minting, signing — use -[pycardano](https://github.com/Python-Cardano/pycardano) directly. -This library slots next to it. +It does **not** reimplement Cardano primitives. Address derivation, chain +context, transaction building, native-script minting, and signing are all +[pycardano](https://github.com/Python-Cardano/pycardano)'s job — this +library slots in next to it and owns only the merchant payment lifecycle. + +## Why + +Accepting on-chain payments for a shop is mostly bookkeeping, not +cryptography: + +- one fresh receive address per order (derived from your wallet xpub), +- watch the chain until the expected amount lands, +- decide confirmed / underpaid / overpaid within a tolerance, +- if the quote window lapses before payment, reprice and try again. + +`cardano-checkout` is that bookkeeping, behind small consumer-supplied +interfaces — a persistence `Protocol` and a pricing callable — so it drops +into any stack: SQLAlchemy, asyncpg, SQLite, or plain in-memory. + +## Install + +``` +pip install cardano-checkout # core +pip install 'cardano-checkout[sqlalchemy]' # + SQLAlchemy extra +``` + +Two runtime dependencies: `httpx` (Koios HTTP) and `apscheduler` +(background loop). There is **no** `pycardano` dependency — see +[Deriving addresses with pycardano](#deriving-addresses-with-pycardano) +for the pairing pattern. ## Quick start @@ -25,7 +53,7 @@ from cardano_checkout import ( # Your oracle. Anything async returning int lovelace works. 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, a DEX, a fixed rate, etc. return int(round(usd / rate * 1_000_000)) @@ -53,8 +81,15 @@ async def main() -> None: asyncio.run(main()) ``` +If you price your invoices in fixed ADA you can omit `price_fn` — the +reprice job becomes a no-op and invoices simply expire at `expires_at`. + ## Deriving addresses with pycardano +The receive address for each invoice is derived from your **account-level +xpub** (a public key — not a secret). Customer funds flow directly to your +wallet; this library never touches keys. + ```python from pycardano import HDWallet, Address, Network @@ -78,7 +113,10 @@ addr = derive_address(account, index=42) ## NFT cert: CIP-25 v2 metadata -Copy-paste builder for an on-chain cert per paid order. No dep. +Need a certificate-of-authenticity NFT per paid order? Here is a +dependency-free builder for the CIP-25 v2 metadata envelope. Hand the +result to pycardano's `AuxiliaryData(Metadata({...}))` when you build the +mint transaction. ```python def build_cip25_metadata( @@ -119,12 +157,9 @@ def build_cip25_metadata( } ``` -Hand the dict to pycardano's `AuxiliaryData(Metadata({...}))` when -building the mint tx. - ## Implementing your own InvoiceStore -`InvoiceStore` is a Protocol — implement six methods against whatever +`InvoiceStore` is a `Protocol` — implement six methods against whatever backend you want (SQLAlchemy, asyncpg, SQLite, in-memory). ```python @@ -139,7 +174,8 @@ class MySqliteStore: async def record_tx(self, invoice_id: str, tx_hash: str, lovelace_delta: int) -> None: ... ``` -See `InMemoryStore` in `cardano_checkout/store.py` for a reference impl. +See `InMemoryStore` in `cardano_checkout/store.py` for a reference +implementation (it also backs the test suite). ## Modules @@ -150,18 +186,34 @@ See `InMemoryStore` in `cardano_checkout/store.py` for a reference impl. | `monitor.py` | `check_address_utxos` (Koios), `evaluate_utxos`, `check_pending_invoices`, `reprice_expired_invoices` | | `scheduler.py` | `InvoiceScheduler` — APScheduler wrapper, 15s check + 60s reprice | -Two direct deps: `httpx`, `apscheduler`. No pycardano dep. - ## Design -1. **Protocol-first.** Persistence, pricing, side-effects through - consumer-supplied interfaces. -2. **Use pycardano directly.** No wrapping of primitives. +1. **Protocol-first.** Persistence, pricing, and side-effects all go + through consumer-supplied interfaces. +2. **Use pycardano directly.** No wrapping of Cardano primitives. 3. **Zero-custody.** Merchant keys never touch this code. xpub-derived addresses, UTxO observation, state transitions. Funds flow directly between customer and merchant wallets. -4. **Offline-first tests.** Koios + price oracles stubbed via fixture. +4. **Offline-first tests.** Koios and price oracles are stubbed via + fixtures — the suite never touches a live node. + +## Contributing + +Issues and pull requests are welcome. A couple of house rules keep the +library focused: + +- **Keep Cardano primitives out.** Anything pycardano already does + belongs in the consumer, not here. +- **Tests stay offline.** Koios and any price oracle must be stubbed via + fixtures so CI never hits a live node or a real wallet. + +Run the suite before opening a PR: + +``` +pip install -e '.[test]' +pytest +``` ## License -Apache-2.0. +Apache-2.0. See [LICENSE](LICENSE).