# cardano-checkout Python SDK for merchant-side Cardano payments + NFT certificate-of-authenticity minting. **Zero-custody by design:** the merchant provides a wallet xpub. The SDK derives unique receive addresses per invoice, polls the chain for payment, and optionally mints a CIP-25 NFT cert on confirmation. The platform never holds or moves funds. Extracted from [TradeCraft](http://192.168.0.5:3001/TradeCraft/tradecraft)'s `services/cardano_*.py` modules (2,400+ lines of production code running on the Cardano mainnet) and packaged for reuse across the Sulkta Coop product family. ## Status **v0.2.0-dev — Protocol-first core + live mint path.** Monitor and scheduler have been refactored off SQLAlchemy and onto the `InvoiceStore` Protocol. Mint builds real transaction bodies against a local Ogmios endpoint and returns an `UnsignedMint` for cold-signing. | Module | Status | Notes | |---|---|---| | `addresses` | ✅ stable | CIP-1852 HD derivation via pycardano `HDWallet` soft derive | | `oracles` | ✅ stable | ADA/USD price via CoinGecko + DexHunter, 5-min cache | | `invoice` + `store` | ✅ stable | Framework-agnostic invoice + `InMemoryStore` reference impl | | `mint` | ✅ v0.2 | CIP-25 v2 metadata + real tx body → `UnsignedMint` bundle | | `ipfs` | ✅ stable | kubo HTTP API client w/ optional mirror-pin | | `monitor` | ✅ v0.2 | Operates purely through `InvoiceStore` — no ORM coupling | | `scheduler` | ✅ v0.2 | `InvoiceScheduler` drives check + reprice against the store | | `tradecraft_compat` | 🟡 compat shim | Keeps TradeCraft's subscription + grace-period jobs alive during migration | | `txbuild` | ✅ v0.2 | OgmiosChainContext wiring + submit_signed_tx + address UTxO queries | **Migration status for TradeCraft:** still imports the old module paths. See the [v0.2 migration guide](#v02-migration-guide-for-tradecraft) below. ## Design ``` ┌────────────────────────────────────────────────────────┐ │ Merchant App │ │ (TradeCraft / chromaticcraft / your-product) │ └──────────────┬───────────────────────┬─────────────────┘ │ │ uses │ implements │ imports ▼ ▼ ┌──────────────┐ ┌────────────────────────┐ │ InvoiceStore │ ◄────── │ cardano_checkout SDK │ │ (your DB) │ │ │ └──────────────┘ │ addresses ← pure │ │ oracles ← pure │ │ invoice ← dataclass │ │ store ← Protocol + InMemoryStore │ │ monitor ← polls chain via store │ │ scheduler ← bg loop │ │ mint ← NFT cert (cold-signer) │ │ ipfs ← upload │ │ txbuild ← Ogmios wrappers │ └────────────────────────┘ │ talks to │ ▼ ┌────────────────────────┐ │ Koios + Ogmios + kubo │ └────────────────────────┘ ``` The merchant app provides: 1. A wallet xpub (account-level extended public key). 2. An `InvoiceStore` implementation (SQLAlchemy, Postgres, SQLite, in-memory — whatever). The SDK provides: 1. Address derivation from the xpub. 2. Per-invoice payment monitoring against Koios. 3. ADA ↔ USD price conversion. 4. CIP-25 v2 NFT cert minting with a cold-signer hand-off. 5. IPFS upload + pinning for NFT image metadata. ## Quick start ```python import asyncio from cardano_checkout import addresses, oracles # Derive a receive address for invoice #42 addr = addresses.derive_address( xpub_hex="", index=42, network="mainnet", ) # Convert a USD price to lovelace at current market async def main() -> None: lovelace = await oracles.convert_usd_to_lovelace(99.00) ada = lovelace / 1_000_000 print(f"Customer owes {ada:.4f} ADA for $99") asyncio.run(main()) ``` ## Payment monitoring ```python import asyncio from cardano_checkout import InMemoryStore, Invoice, InvoiceStatus, InvoiceScheduler store = InMemoryStore() # swap for your real SQLAlchemy / asyncpg / SQLite adapter # Create an invoice (typically you'd derive the address here via addresses.derive_address) invoice = Invoice( id="ord-0042", merchant_id="chromaticcraft", derivation_index=42, receive_address="addr1q...", expected_lovelace=5_000_000, usd_amount=2.50, ) asyncio.run(store.create(invoice)) # Wire the background scheduler — same 15s check / 60s reprice cadence as TradeCraft. scheduler = InvoiceScheduler(store=store) asyncio.run(scheduler.start()) # ... your app runs ... asyncio.run(scheduler.stop()) ``` ## IPFS: bake-then-mirror pattern The SDK's `IPFSClient` expects a local kubo daemon (typically in the same Docker image as the web app) for upload and primary pin, and takes an optional list of mirror endpoints to `pin add` the CID on a second node for archival redundancy. Typical chromaticcraft deployment: ```python from cardano_checkout import ipfs client = ipfs.IPFSClient( api_url="http://127.0.0.1:5001", # local kubo in the same container mirror_api_urls=["http://192.168.254.5:5001"], # Lucy's kubo over the LAN/VPN ) cid = await client.add(photo_bytes, filename="order-0001.jpg") # Image now served by Rackham (low latency) AND pinned on Lucy (durability) ``` ## NFT cert-of-authenticity design One minting policy per merchant studio. Policy is a native script (no Plutus required), optionally time-locked to make "no more editions after X" a cryptographically verifiable claim. CIP-25 v2 metadata. Single NFT per order. Policy skey never leaves the custody host (Lucy in Sulkta's pattern — 2-of-2 native script: Cobb + Kayos). The SDK builds the metadata envelope + tx body on the hot node and returns an `UnsignedMint` bundle; an external offline signer provides the vkey witnesses; the hot node submits the assembled CBOR. The full operator runbook — including the exact byte-movement sequence, verification checklist, and preprod dry-run procedure — lives in [`docs/minting-workflow.md`](docs/minting-workflow.md). ## v0.2 migration guide for TradeCraft The generic invoice jobs moved to a Protocol-based API. The subscription + grace-period jobs stayed TradeCraft-specific and live in `cardano_checkout.tradecraft_compat`. **Import changes when TradeCraft adopts the SDK:** | Was | Becomes | |---|---| | `from services.cardano_monitor import check_pending_payments, reprice_expired_payments` | `from cardano_checkout.monitor import check_pending_invoices, reprice_expired_invoices` | | `from services.cardano_monitor import _check_address_utxos, _evaluate_payment` | `from cardano_checkout.monitor import check_address_utxos, evaluate_utxos` (or import from `tradecraft_compat` for the exact old names) | | `from services.cardano_scheduler import start_cardano_scheduler, stop_cardano_scheduler` | `from cardano_checkout.scheduler import InvoiceScheduler` (instantiate with your store) | | `from services.cardano_scheduler import _check_subscription_payments, _reprice_subscription_payments, _enforce_grace_period` | `from cardano_checkout.tradecraft_compat import check_subscription_payments, reprice_subscription_payments, enforce_grace_period` (verbatim jobs — TradeCraft still drives them directly) | | `from services.cardano_price import *` | `from cardano_checkout.oracles import *` | | `from services.cardano_addresses import derive_address` | `from cardano_checkout.addresses import derive_address` | **What TradeCraft still needs to write:** A SQLAlchemy adapter implementing `InvoiceStore` against the existing `CardanoPayment` table. `list_by_status` maps to a `SELECT ... WHERE status = :s`, `next_derivation_index` to a `SELECT MAX(derivation_index) + 1`, etc. That's 80-ish lines of wrapper code — nothing exotic. Once that's landed, the generic jobs run through `InvoiceScheduler(store=SQLAlchemyInvoiceStore(...))` and the subscription jobs keep running through `tradecraft_compat` unchanged. **TODO for future sprints:** - Ship a `cardano_checkout.adapters.sqlalchemy.SQLAlchemyInvoiceStore` so TradeCraft doesn't have to write the adapter from scratch. - Once TradeCraft's subscription jobs are migrated to a subscription- specific Protocol, delete `tradecraft_compat`. - Refund-path `build_payment_tx` in `txbuild.py` (v0.3). - Batched mints (sell-sheet of 10 NFTs at once). ## Testing ```bash pip install -e '.[test]' pytest # 42 tests, all offline ``` The test suite mocks the chain context for mint-tx construction and monkey-patches Koios + the oracle for monitor tests — CI never touches a live node. The address-derivation tests use a deterministic test-vector xpub from the standard "test ... junk" mnemonic so they can't drift. ## Installation ``` pip install 'cardano-checkout[sqlalchemy]' # if you're using SQLAlchemy pip install cardano-checkout # core only ``` ## License Apache-2.0 — matches upstream Cardano tooling.