v0.2: README rewrite + cold-signer runbook

This commit is contained in:
Sulkta 2026-04-23 20:00:49 -07:00
parent 5782d80a3b
commit c31518309d

126
README.md
View file

@ -12,21 +12,25 @@ Cardano mainnet) and packaged for reuse across the Sulkta Coop product family.
## Status
**v0.1.0-dev — alpha extraction.** Pure modules lifted verbatim from the host app.
DB-coupled modules (monitor, scheduler) ship with a `TODO: refactor to Store
protocol` marker — they work as-is when paired with the host app's SQLAlchemy models
but will be refactored to the generic `InvoiceStore` Protocol in v0.2.
**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; pure pycardano |
| `oracles` | ✅ stable | ADA/USD price via Koios with 5-min cache |
| `invoice` + `store` | ✅ new | Framework-agnostic invoice + persistence Protocol |
| `mint` | ⏳ stub | CIP-25 v2 metadata builder works; tx submission in v0.2 |
| `ipfs` | ✅ working | kubo HTTP API client w/ optional mirror-pin |
| `monitor` | 🟡 SQLAlchemy-coupled | v0.2 target: refactor around `InvoiceStore` |
| `scheduler` | 🟡 SQLAlchemy-coupled | v0.2 target: same |
| `txbuild` | ❌ v0.2 | Full PyCardano tx construction via Ogmios |
| `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 |
| `hostapp_compat` | 🟡 compat shim | Keeps the host app's subscription + grace-period jobs alive during migration |
| `txbuild` | ✅ v0.2 | OgmiosChainContext wiring + submit_signed_tx + address UTxO queries |
**Migration status for the host app:** still imports the old module paths. See
the [v0.2 migration guide](#v02-migration-guide-for-hostapp) below.
## Design
@ -44,11 +48,12 @@ but will be refactored to the generic `InvoiceStore` Protocol in v0.2.
└──────────────┘ │ addresses ← pure │
│ oracles ← pure │
│ invoice ← dataclass │
│ monitor ← polls chain │
│ scheduler ← bg loop │
│ mint ← NFT cert │
│ ipfs ← upload │
│ txbuild ← PyCardano wrappers │
│ store ← Protocol + InMemoryStore │
│ monitor ← polls chain via store │
│ scheduler ← bg loop │
│ mint ← NFT cert (cold-signer) │
│ ipfs ← upload │
│ txbuild ← Ogmios wrappers │
└────────────────────────┘
talks to │
@ -59,14 +64,16 @@ but will be refactored to the generic `InvoiceStore` Protocol in v0.2.
```
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 (v0.2).
4. CIP-25 v2 NFT cert minting with a cold-signer hand-off.
5. IPFS upload + pinning for NFT image metadata.
## Quick start
@ -91,6 +98,32 @@ async def main() -> None:
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="example-studio",
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 the host app.
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
@ -119,8 +152,61 @@ 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 (the cold host in Sulkta's pattern). The SDK builds the metadata envelope + tx;
external signer does the signature.
host (the cold host in Sulkta's pattern — 2-of-2 native script: signer 1 + signer 2). 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 the host app
The generic invoice jobs moved to a Protocol-based API. The
subscription + grace-period jobs stayed the host app-specific and live in
`cardano_checkout.hostapp_compat`.
**Import changes when the host app 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 `hostapp_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.hostapp_compat import check_subscription_payments, reprice_subscription_payments, enforce_grace_period` (verbatim jobs — the host app 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 the host app 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 `hostapp_compat` unchanged.
**TODO for future sprints:**
- Ship a `cardano_checkout.adapters.sqlalchemy.SQLAlchemyInvoiceStore` so
the host app doesn't have to write the adapter from scratch.
- Once the host app's subscription jobs are migrated to a subscription-
specific Protocol, delete `hostapp_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