diff --git a/README.md b/README.md index d795861..ce2cb8a 100644 --- a/README.md +++ b/README.md @@ -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