"""CIP-25 v2 NFT certificate-of-authenticity minting. This module produces the NFT cert attached to a confirmed merchant order. One NFT per order, pinned-once metadata (image CID from IPFS via :mod:`cardano_checkout.ipfs`), sent directly to the customer's wallet in the same transaction. Design decisions: - **CIP-25 v2** (not CIP-68). CIP-25 is universally supported by every Cardano wallet (Eternl, Lace, Yoroi, Vespr, Typhon). CIP-68 adds reference-NFT mutability we do not need for a static cert. - **Single policy per merchant studio.** All of a studio's certs share one policy_id so wallets group them cleanly. The policy key is a native script under the studio's custody — Sulkta pattern is a multi-sig native script stored on the cold host. - **Policy has a time-lock** (invalid-after slot) so the "no more editions can be minted after X" claim is cryptographically enforceable. Recommended: generous lock (100 years) so policy_id stays stable, but revokable in-contract via ``mint policy revoke`` flow. - **No reference script, no Plutus.** Pure native scripts + standard CIP-25 metadata keeps the tx cheap (~0.18 ADA fee + min-utxo for the NFT output). v0.1.0 ships the signature + stub. Full tx construction against a local Ogmios endpoint lands in v0.2 once the store + monitor integration tests pass. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Optional @dataclass class MintPolicy: """A native-script minting policy under the SDK's custody model. Attributes: policy_id: Hex blake2b-224 hash of the native script CBOR. Stable for the life of the policy — shipped with every cert minted under it. Becomes the Cardano ``policy_id`` of the NFT asset. script_cbor_hex: Hex-encoded CBOR of the native script itself. Submitted alongside the mint tx witness. signing_keys: Paths to skey files needed to sign the tx (all of them, for an all-of multi-sig). The SDK does not read these — callers pass them to :mod:`cardano_checkout.txbuild` which coordinates with an external signer (the cold host cold-store pattern). locked_after_slot: Optional slot beyond which the policy rejects further mints. None = no time lock (not recommended for certificates — a lock makes the "no more editions" claim mathematically verifiable). """ policy_id: str script_cbor_hex: str signing_keys: list[str] = field(default_factory=list) locked_after_slot: Optional[int] = None async def mint_nft_cert( policy: MintPolicy, asset_name: str, metadata: dict, recipient_address: str, ogmios_url: str = "http://127.0.0.1:1337", network: str = "mainnet", ) -> str: """Mint a CIP-25 v2 NFT cert and send it to the recipient. Constructs a transaction that: 1. Mints exactly 1 of ``{policy.policy_id}.{asset_name}``. 2. Sends that single token to ``recipient_address`` in its own UTxO. 3. Attaches CIP-25 v2 metadata under metadatum label 721. 4. Witnesses with all signing keys required by the policy. Args: policy: Merchant's minting policy. asset_name: UTF-8 asset name (will be hex-encoded per CIP-25). Max 32 bytes. metadata: CIP-25 metadata dict. At minimum should include ``name``, ``image`` (ipfs://CID), ``mediaType``, and any studio-specific properties. The SDK wraps this into the proper ``{721: {policy_id: {asset_name: ...}}}`` envelope automatically. recipient_address: Bech32 address of the wallet that receives the NFT. ogmios_url: Endpoint for chain queries + tx submission. network: "mainnet" or "testnet". Returns: Transaction hash (hex) once successfully submitted. Raises: NotImplementedError: v0.1.0 stub. Full implementation lands in v0.2. """ # v0.1.0 surface lock — implementation lands in v0.2 alongside txbuild.py raise NotImplementedError( "mint_nft_cert is stubbed in v0.1.0. Use cardano-cli or PyCardano " "directly until v0.2 ships the full Ogmios-backed mint path." ) def build_cip25_metadata( policy_id: str, asset_name: str, name: str, image_cid: str, description: str = "", media_type: str = "image/jpeg", properties: Optional[dict] = None, ) -> dict: """Assemble the ``{721: {...}}`` metadatum envelope for a single NFT. CIP-25 v2 image field takes an ``ipfs://`` URI. Description, if longer than 64 characters, is split into an array of ≤64-char chunks (CIP-25 constraint from the Cardano metadata schema — strings larger than 64 chars are encoded as a list of chunks). Args: policy_id: Hex policy id (same as on the asset). asset_name: UTF-8 asset name — used as the dict key under policy_id. name: Human-readable NFT title (shown in wallets). image_cid: IPFS CID — the function prepends ``ipfs://``. description: Optional longer text. Will be chunked if > 64 chars. media_type: MIME type of the image. Default ``image/jpeg``. properties: Additional key/value pairs merged into the metadata blob. Returns: Dict ready to submit as tx metadatum label 721. """ 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)] desc: object = description if isinstance(description, str) and len(description) > 64: desc = chunk64(description) body: dict = { "name": name, "image": f"ipfs://{image_cid}", "mediaType": media_type, } if desc: body["description"] = desc if properties: body.update(properties) return { "721": { policy_id: { asset_name: body, }, "version": "2.0", } }