README status table moves everything green except the TradeCraft compat shim (still yellow, documented sunset path). Adds a migration guide section mapping every old services/cardano_*.py import to its new cardano_checkout.* equivalent so TradeCraft can adopt in one atomic diff once the SQLAlchemyInvoiceStore adapter lands. docs/minting-workflow.md: step-by-step runbook for the cold-signer pattern — hot host builds UnsignedMint, operator ships three CBOR hex files to Lucy, offline signer produces a signed tx, hot host submits via submit_signed_tx. Covers the tx-id sanity check, skey hygiene rules, time-locked-policy TTL clamp, and the preprod dry-run requirement for every new policy.
220 lines
9.9 KiB
Markdown
220 lines
9.9 KiB
Markdown
# 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="<your wallet xpub>",
|
|
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.
|