cardano-checkout-py/README.md
Cobb Hayes c592a58148 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.
2026-05-27 11:15:03 -07:00

167 lines
5 KiB
Markdown

# cardano-checkout
Merchant-side Cardano payment lifecycle in 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.
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.
## Quick start
```python
import asyncio
from datetime import datetime, timedelta, timezone
from cardano_checkout import (
Invoice, InvoiceStatus, InMemoryStore, InvoiceScheduler,
)
# 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.
return int(round(usd / rate * 1_000_000))
async def main() -> None:
store = InMemoryStore() # swap for your SQLAlchemy / asyncpg / sqlite adapter
invoice = Invoice(
id="ord-0042",
merchant_id="my-shop",
derivation_index=42,
receive_address="addr1q...", # derive via pycardano
expected_lovelace=5_000_000,
usd_amount=2.50,
expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
)
await store.create(invoice)
scheduler = InvoiceScheduler(store=store, price_fn=my_price_fn)
await scheduler.start()
# ... app runs ...
await scheduler.stop()
asyncio.run(main())
```
## Deriving addresses with pycardano
```python
from pycardano import HDWallet, Address, Network
# Account-level xpub — public, not a secret.
xpub_hex = "..."
account = HDWallet.from_xpub(bytes.fromhex(xpub_hex))
def derive_address(account: HDWallet, index: int, network=Network.MAINNET) -> str:
payment = account.derive(0).derive(index) # external chain, address index
staking = account.derive(2).derive(0) # staking chain, always index 0
addr = Address(
payment_part=payment.public_key.hash(),
staking_part=staking.public_key.hash(),
network=network,
)
return str(addr)
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.
```python
def build_cip25_metadata(
*,
policy_id: str,
asset_name: str,
name: str,
image_cid: str,
description: str = "",
media_type: str = "image/jpeg",
properties: dict | None = None,
) -> dict:
"""Build a CIP-25 v2 metadata envelope.
Returns a dict ready to submit as transaction metadatum label 721.
Handles the 64-char chunking rule for long descriptions.
"""
def chunk64(s: str) -> list[str]:
if len(s) <= 64:
return [s]
return [s[i:i + 64] for i in range(0, len(s), 64)]
body: dict = {
"name": name,
"image": f"ipfs://{image_cid}",
"mediaType": media_type,
}
if description:
body["description"] = description if len(description) <= 64 else chunk64(description)
if properties:
body.update(properties)
return {
"721": {
policy_id: {asset_name: body},
"version": "2.0",
}
}
```
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
backend you want (SQLAlchemy, asyncpg, SQLite, in-memory).
```python
from cardano_checkout import Invoice, InvoiceStatus, InvoiceStore
class MySqliteStore:
async def create(self, invoice: 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 update(self, invoice: Invoice) -> None: ...
async def next_derivation_index(self, merchant_id: str) -> int: ...
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.
## Modules
| Module | Purpose |
|---|---|
| `invoice.py` | `Invoice` dataclass + `InvoiceStatus` enum |
| `store.py` | `InvoiceStore` Protocol + `InMemoryStore` 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.
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.
## License
Apache-2.0.