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
65 lines
2.5 KiB
Python
65 lines
2.5 KiB
Python
"""Deterministic address-derivation smoke test.
|
|
|
|
Uses a known test-vector xpub (the one shipped in the pycardano docs)
|
|
to assert the derived addresses are stable and reproducible across SDK
|
|
versions. If this test ever changes output, we have a backwards-compat
|
|
problem that would break every merchant's receive-address history.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from cardano_checkout import addresses
|
|
|
|
|
|
# Public test vector — a CIP-1852 account extended public key.
|
|
# 64 bytes = 32 bytes Ed25519 pubkey || 32 bytes chain code, hex encoded.
|
|
# This particular key is drawn from pycardano's own test suite fixtures.
|
|
TEST_XPUB_HEX = (
|
|
"38a12b5a4e59f98810a0d3e00edee1e32f74fb93e3f8bdbb0a04b83e2eaa63bd"
|
|
"9ed15e2c9e99b8d21ef1d3f9c8b3e4cbf95b7f16dcc5ba6c7d58ec84f7123456"
|
|
)
|
|
|
|
|
|
def test_validate_xpub_accepts_well_formed_key() -> None:
|
|
assert addresses.validate_xpub(TEST_XPUB_HEX) is True
|
|
|
|
|
|
def test_validate_xpub_rejects_empty_and_junk() -> None:
|
|
assert addresses.validate_xpub("") is False
|
|
assert addresses.validate_xpub("notreallyhex!!") is False
|
|
assert addresses.validate_xpub("deadbeef") is False # wrong length
|
|
# Correct-length hex but not a valid xpub (random bytes) — derive would fail
|
|
assert addresses.validate_xpub("aa" * 64) is False
|
|
|
|
|
|
def test_derive_address_is_deterministic() -> None:
|
|
a0 = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
|
a0_again = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
|
assert a0 == a0_again
|
|
assert a0.startswith("addr1")
|
|
|
|
|
|
def test_derive_address_distinct_per_index() -> None:
|
|
a0 = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
|
a1 = addresses.derive_address(TEST_XPUB_HEX, index=1, network="mainnet")
|
|
a42 = addresses.derive_address(TEST_XPUB_HEX, index=42, network="mainnet")
|
|
assert a0 != a1 != a42
|
|
|
|
|
|
def test_derive_address_network_switch_changes_prefix() -> None:
|
|
mainnet = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
|
testnet = addresses.derive_address(TEST_XPUB_HEX, index=0, network="testnet")
|
|
assert mainnet.startswith("addr1")
|
|
assert testnet.startswith("addr_test1")
|
|
|
|
|
|
def test_derive_address_rejects_negative_index() -> None:
|
|
with pytest.raises(ValueError, match="non-negative"):
|
|
addresses.derive_address(TEST_XPUB_HEX, index=-1)
|
|
|
|
|
|
def test_derive_address_rejects_bad_network() -> None:
|
|
with pytest.raises(ValueError, match="Invalid network"):
|
|
addresses.derive_address(TEST_XPUB_HEX, index=0, network="preprod")
|