v0.1.0-dev: initial public extraction + new abstractions
This commit is contained in:
commit
e38120cf11
17 changed files with 2429 additions and 0 deletions
156
cardano_checkout/mint.py
Normal file
156
cardano_checkout/mint.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
"""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://<CID>`` 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",
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue