Sulkta Coop's Python SDK for merchant-side Cardano payments + NFT certificate-of-authenticity minting. Zero-custody by design. Extracted from TradeCraft's services/cardano_*.py (2,400+ lines of production Cardano-mainnet code) and restructured as an installable Python package. Package layout (cardano_checkout/): - addresses.py — lifted verbatim: CIP-1852 HD derivation, pure pycardano - oracles.py — lifted from cardano_price.py: Koios ADA/USD feed w/ 5m cache - monitor.py — lifted verbatim (SQLAlchemy-coupled; v0.2 refactors to Store) - scheduler.py — lifted verbatim (same refactor note) - invoice.py — NEW: framework-agnostic Invoice dataclass + lifecycle enum - store.py — NEW: InvoiceStore Protocol for pluggable persistence - mint.py — NEW: CIP-25 v2 metadata builder (works); tx submission stub for v0.2 - ipfs.py — NEW: kubo HTTP client with primary-pin + mirror-pin pattern - txbuild.py — NEW: v0.2 stub for PyCardano / Ogmios tx construction Design: - Consumers provide xpub + InvoiceStore impl. SDK provides everything else. - IPFS: local kubo for upload + serve, optional mirror pins for archival. Chromaticcraft pattern: Rackham kubo primary, Lucy kubo mirror. - NFT: single native-script policy per merchant studio (CIP-25 v2, not CIP-68 — full wallet coverage, no mutability needed for static certs). Policy skey stays under Sulkta cold-custody (Lucy pattern); signing is an external hand-off like ADAMaps payouts. Tests: pure-module smoke tests pass for invoice, store-protocol, CIP-25 metadata envelope, IPFS client import, txbuild stub module. Address derivation tests ship but require pycardano + will exercise in CI. LICENSE: Apache-2.0 (matches upstream Cardano tooling). Next (v0.2 scope): - Refactor monitor + scheduler around InvoiceStore (drop SQLAlchemy coupling) - Wire mint.mint_nft_cert to PyCardano + local Ogmios on Rackham - txbuild: Ogmios chain-context + cold-signer hand-off shape - chromaticcraft Phase 2 imports the SDK as its first external consumer
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
"""Persistence abstraction for Invoice objects.
|
|
|
|
The SDK does not prescribe a database. Consumers implement
|
|
:class:`InvoiceStore` against whatever backend suits them — SQLAlchemy
|
|
(TradeCraft pattern), SQLite (chromaticcraft pattern), Postgres raw
|
|
(ADAMaps pattern), in-memory dict (tests).
|
|
|
|
All methods are async so the same Protocol works cleanly for both
|
|
asyncpg/asyncio-sqlalchemy backends and synchronous backends wrapped
|
|
with ``asyncio.to_thread``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional, Protocol, runtime_checkable
|
|
|
|
from cardano_checkout.invoice import Invoice, InvoiceStatus
|
|
|
|
|
|
@runtime_checkable
|
|
class InvoiceStore(Protocol):
|
|
"""Persistence backend for invoices.
|
|
|
|
Consumers implement the six methods below. The SDK's monitor + scheduler
|
|
modules operate entirely through this interface, never touching a specific
|
|
ORM or driver.
|
|
"""
|
|
|
|
async def create(self, invoice: Invoice) -> None:
|
|
"""Insert a new invoice. Should raise if an invoice with the same id exists."""
|
|
...
|
|
|
|
async def get(self, invoice_id: str) -> Optional[Invoice]:
|
|
"""Fetch one invoice by id. Returns None if not found."""
|
|
...
|
|
|
|
async def list_by_status(
|
|
self, status: InvoiceStatus, limit: int = 100
|
|
) -> list[Invoice]:
|
|
"""List invoices in a given state, newest-first. Used by the monitor poll loop."""
|
|
...
|
|
|
|
async def update(self, invoice: Invoice) -> None:
|
|
"""Persist the current state of an invoice.
|
|
|
|
Implementations should compare-and-set on ``invoice.id`` — if the row
|
|
doesn't exist the call should raise. Does NOT create; see :meth:`create`.
|
|
"""
|
|
...
|
|
|
|
async def next_derivation_index(self, merchant_id: str) -> int:
|
|
"""Return the next unused receive-address index for a merchant.
|
|
|
|
Should be transactionally safe against concurrent invoice creation;
|
|
consumers typically implement this via ``SELECT COALESCE(MAX(index), -1) + 1 ... FOR UPDATE``
|
|
or an atomic counter row.
|
|
"""
|
|
...
|
|
|
|
async def record_tx(
|
|
self, invoice_id: str, tx_hash: str, lovelace_delta: int
|
|
) -> None:
|
|
"""Record an observed inbound UTxO against an invoice.
|
|
|
|
Must be idempotent on (invoice_id, tx_hash) — monitor loops will
|
|
re-observe the same UTxO until the invoice transitions to a terminal
|
|
state.
|
|
"""
|
|
...
|