From c3d3491b5ecc54e2ab5594754eadab7423b463a0 Mon Sep 17 00:00:00 2001 From: Asuma Yamada Date: Thu, 12 Mar 2026 15:15:50 +0900 Subject: [PATCH] Add Metaplex skill and align Core mint helpers --- .agents/memory/todo.md | 57 + .agents/skills/metaplex/SKILL.md | 119 ++ .../metaplex/references/cli-bubblegum.md | 126 ++ .../metaplex/references/cli-candy-machine.md | 385 +++++++ .../skills/metaplex/references/cli-core.md | 130 +++ .../skills/metaplex/references/cli-genesis.md | 430 +++++++ .../metaplex/references/cli-token-metadata.md | 59 + .agents/skills/metaplex/references/cli.md | 226 ++++ .../skills/metaplex/references/concepts.md | 330 ++++++ .../metaplex/references/sdk-bubblegum.md | 348 ++++++ .../skills/metaplex/references/sdk-core.md | 417 +++++++ .../skills/metaplex/references/sdk-genesis.md | 1009 +++++++++++++++++ .../references/sdk-token-metadata-kit.md | 263 +++++ .../metaplex/references/sdk-token-metadata.md | 459 ++++++++ .agents/skills/metaplex/references/sdk-umi.md | 181 +++ .vscode/settings.json | 1 - .../src/instructions/mint_doom_index_nft.rs | 9 +- scripts/devnet/mint.test.ts | 37 +- scripts/devnet/mint.ts | 64 +- skills-lock.json | 10 + 20 files changed, 4594 insertions(+), 66 deletions(-) create mode 100644 .agents/skills/metaplex/SKILL.md create mode 100644 .agents/skills/metaplex/references/cli-bubblegum.md create mode 100644 .agents/skills/metaplex/references/cli-candy-machine.md create mode 100644 .agents/skills/metaplex/references/cli-core.md create mode 100644 .agents/skills/metaplex/references/cli-genesis.md create mode 100644 .agents/skills/metaplex/references/cli-token-metadata.md create mode 100644 .agents/skills/metaplex/references/cli.md create mode 100644 .agents/skills/metaplex/references/concepts.md create mode 100644 .agents/skills/metaplex/references/sdk-bubblegum.md create mode 100644 .agents/skills/metaplex/references/sdk-core.md create mode 100644 .agents/skills/metaplex/references/sdk-genesis.md create mode 100644 .agents/skills/metaplex/references/sdk-token-metadata-kit.md create mode 100644 .agents/skills/metaplex/references/sdk-token-metadata.md create mode 100644 .agents/skills/metaplex/references/sdk-umi.md create mode 100644 skills-lock.json diff --git a/.agents/memory/todo.md b/.agents/memory/todo.md index 155f5f4..1aaa7f7 100644 --- a/.agents/memory/todo.md +++ b/.agents/memory/todo.md @@ -1,5 +1,32 @@ # Task Plan: Documentation English Translation (2026-03-12) +# Task Plan: Verify NFT Metadata Reference Path (2026-03-12) + +- [x] Inspect the repository paths that mint, fetch, or validate NFT metadata. +- [x] Compare the implementation's expected metadata source with the provided Metaplex metadata object shape. +- [x] Conclude whether the current flow can resolve NFT metadata correctly and note any mismatches. + +# Review: Verify NFT Metadata Reference Path (2026-03-12) + +- Confirmed the repository is built around Metaplex Core, not Token Metadata account parsing. The program stores `name` and `uri` on the Core asset and resolves richer NFT metadata from the off-chain JSON behind that URI. +- Verified that `mint_doom_index_nft` writes the deterministic URI `{base_metadata_url}/{tokenId}.json` and does not inspect fields such as `data.creators`, `sellerFeeBasisPoints`, `edition`, or `collection.key`. +- Verified that the devnet mint script decodes the Core asset account to get the URI, fetches the JSON document, and currently requires both `image` and `animation_url`. +- Fetched the user-provided URI and confirmed it is reachable over HTTPS and returns JSON, but that sample metadata omits `animation_url`, so it would fail the repository's current DOOM-specific post-mint validation even though generic metadata fetch by URI works. + +# Task Plan: Use Official Metaplex Core Patterns (2026-03-12) + +- [x] Inspect where the repository hand-rolls Metaplex Core account handling instead of using official Core Anchor / JS helpers. +- [x] Replace on-chain Core account handling with official Anchor account types where applicable. +- [x] Replace handwritten Core asset decoding in scripts with official Metaplex JS SDK fetch paths. +- [x] Update tests and run targeted verification for the refactor. + +# Review: Use Official Metaplex Core Patterns (2026-03-12) + +- Replaced the mint instruction's `collection` account from `UncheckedAccount` to `Account`, matching the Metaplex Core Anchor guidance enabled by the crate's `anchor` feature. +- Removed the handwritten Metaplex Core asset-account URI decoder from `scripts/devnet/mint.ts` and switched the script to `@metaplex-foundation/mpl-core` `fetchAsset()` plus `asset.uri`. +- Replaced the old decoder unit test with coverage for the new `fetchAssetUri` helper while keeping the existing URL reachability tests intact. +- Verified with `cargo fmt --all`, `bunx prettier scripts/devnet/mint.ts scripts/devnet/mint.test.ts --write`, `bun test scripts/devnet/mint.test.ts`, `bun run typecheck`, `cargo test -p tests --lib mint_with_valid_reservation_creates_core_asset -- --nocapture`, and `bun run test:contract`. + - [x] Inspect public markdown files and identify Japanese text that should be translated. - [x] Translate `README.md` and `docs/*.md` to English while preserving technical meaning. - [x] Verify that no Japanese text remains in the public documentation set. @@ -194,6 +221,23 @@ - `initialize_collection` (`qVj2Rgi...`) は `1569040` lamports (`0.00156904 SOL`) で、内訳は fee `10000` + collection asset rent `1559040`。`computeUnitsConsumed = 15302`。 - `reserve_token_id` (`5rJadHXM...`) は `1243880` lamports (`0.00124388 SOL`) で、内訳は fee `5000` + reservation rent `1238880`。`computeUnitsConsumed = 10190`。 - `mint_doom_index_nft` (`2cevXVJF...`) は `3208240` lamports (`0.00320824 SOL`) で、内訳は fee `10000` + Core asset rent `3198240`。`computeUnitsConsumed = 25713`。 + +# Task Plan: Install Metaplex Skill (2026-03-12) + +- [x] Load the project memory and the local `skill-installer` workflow. +- [x] Reproduce or independently verify the reported install failure source. +- [x] Check whether the referenced GitHub repository is publicly reachable. +- [x] Check whether the public SkillMD distribution is a valid Codex skill package. +- [x] Record the root cause and the verified next steps. + +# Review: Install Metaplex Skill (2026-03-12) + +- Confirmed the requested source `https://github.com/metaplex-foundation/metaplex-skill.git` is not publicly reachable on March 12, 2026. `git ls-remote` returns `repository not found`, and GitHub responds with HTTP `404`. +- The `npx skills add metaplex-foundation/metaplex-skill` error message is therefore misleading: it surfaces as an authentication problem, but the underlying issue is that the referenced repo path is not available to this environment. +- Found a public SkillMD page for the same skill at `https://skillmd.ai/how-to-build/metaplex/`, with a ZIP download at `https://skillmd.ai/skills/metaplex/download/`. +- Verified that the ZIP download succeeds, but the archive is not a valid Codex skill package as-is: it contains support files under `docs/`, `examples/`, `resources/`, and `templates/`, but no `SKILL.md`. +- Because both verified installation paths are incomplete or invalid, no local skill installation was applied. The actionable next step is to get the correct upstream source from the skill author: either a reachable GitHub repo/path or a fixed ZIP bundle that includes `SKILL.md`. +- Notes: `docs/PRODUCT.md` exists in this repo, but `docs/TECH.md` and `docs/STRUCTURE.md` are currently absent. - localnet で作られた account sizes は `GlobalConfig = 406 bytes`, collection asset `96 bytes`, reservation `50 bytes`, minted Core asset `116 bytes`。対応する rent は `solana rent` の実測と一致した。 # Task Plan: Code Review Against main (2026-03-12) @@ -259,3 +303,16 @@ - Extended Rust and TypeScript coverage for reservation misuse, paused minting, transfer-admin handshakes, keypair permissions, reserve retries, on-chain URI decoding, and HEAD-to-GET asset validation. - Verified and intentionally skipped two stale comments because the current code already addressed them: `scripts/build-test-sbf.sh` no longer reuses a dumped `mpl_core_program.so`, and the redundant `let config = global_config;` alias in `tests/src/lib.rs` was already gone after the earlier test-module split. - Verified with `bun test scripts/devnet/common.test.ts scripts/devnet/mint.test.ts`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`, `bun run check`, and the `bun run check` path’s `./scripts/test-contract-v1.sh` contract suite. + +# Task Plan: Show target In VS Code Explorer (2026-03-12) + +- [x] Inspect workspace-level VS Code settings and confirm what hides `target/`. +- [x] Apply the minimum settings change so `target/` is visible again in Explorer. +- [x] Validate the edited JSON and record the outcome. + +# Review: Show target In VS Code Explorer (2026-03-12) + +- Confirmed the hiding behavior came from workspace-local VS Code settings in `.vscode/settings.json`, where `files.exclude` contained `**/target: true`. +- Removed only the `files.exclude` rule for `target/`, which makes the folder visible again in the Explorer without changing other workspace noise filters. +- Kept `search.exclude` for `**/target` intact, so global search still skips build artifacts unless that setting is changed separately. +- Validated the edited JSON with a local parse check: `node -e "... JSON.parse(...)"` completed successfully. diff --git a/.agents/skills/metaplex/SKILL.md b/.agents/skills/metaplex/SKILL.md new file mode 100644 index 0000000..c987b5f --- /dev/null +++ b/.agents/skills/metaplex/SKILL.md @@ -0,0 +1,119 @@ +--- +name: metaplex +description: Metaplex development on Solana — NFTs, tokens, compressed NFTs, candy machines, token launches. Use when working with Token Metadata, Core, Bubblegum, Candy Machine, Genesis, or the mplx CLI. +license: Apache-2.0 +metadata: + author: metaplex-foundation + version: "0.1.0" + openclaw: {"emoji":"💎","os":["darwin","linux","win32"],"requires":{"bins":["node"]},"homepage":"https://developers.metaplex.com"} +--- + +# Metaplex Development Skill + +## Overview + +Metaplex provides the standard infrastructure for NFTs and tokens on Solana: +- **Core**: Next-gen NFT standard (recommended for new NFT projects) +- **Token Metadata**: Fungible tokens + legacy NFTs/pNFTs +- **Bubblegum**: Compressed NFTs (cNFTs) using Merkle trees — massive scale at minimal cost +- **Candy Machine**: NFT drops with configurable minting rules +- **Genesis**: Token launch protocol with fair distribution + liquidity graduation + +## Tool Selection + +> **Prefer CLI over SDK** for direct execution. Use SDK only when user specifically needs code. + +| Approach | When to Use | +|----------|-------------| +| **CLI (`mplx`)** | Default choice - direct execution, no code needed | +| **Umi SDK** | User needs code — default SDK choice. Covers all programs (TM, Core, Bubblegum, Genesis) | +| **Kit SDK** | User specifically uses @solana/kit, or asks for minimal dependencies. Token Metadata only — no Core/Bubblegum/Genesis support | + +## Task Router + +> **IMPORTANT**: You MUST read the detail file for your task BEFORE executing any command or writing any code. The command syntax, required flags, setup steps, and batching rules are ONLY in the detail files. Do NOT guess commands from memory. + +| Task Type | Read This File | +|-----------|----------------| +| Any CLI operation (shared setup) | `./references/cli.md` | +| CLI: Core NFTs/Collections | `./references/cli.md` + `./references/cli-core.md` | +| CLI: Token Metadata NFTs | `./references/cli.md` + `./references/cli-token-metadata.md` | +| CLI: Compressed NFTs (Bubblegum) | `./references/cli.md` + `./references/cli-bubblegum.md` | +| CLI: Candy Machine (NFT drops) | `./references/cli.md` + `./references/cli-candy-machine.md` | +| CLI: Token launch (Genesis) | `./references/cli.md` + `./references/cli-genesis.md` | +| CLI: Fungible tokens | `./references/cli.md` (toolbox section) | +| SDK setup (Umi) | `./references/sdk-umi.md` | +| SDK: Core NFTs | `./references/sdk-umi.md` + `./references/sdk-core.md` | +| SDK: Token Metadata | `./references/sdk-umi.md` + `./references/sdk-token-metadata.md` | +| SDK: Compressed NFTs (Bubblegum) | `./references/sdk-umi.md` + `./references/sdk-bubblegum.md` | +| SDK: Token Metadata with Kit | `./references/sdk-token-metadata-kit.md` | +| SDK: Token launch (Genesis) | `./references/sdk-umi.md` + `./references/sdk-genesis.md` | +| Account structures, PDAs, concepts | `./references/concepts.md` | + +## CLI Capabilities + +The `mplx` CLI can handle most Metaplex operations directly. **Read `./references/cli.md` for shared setup, then the program-specific file.** + +| Task | CLI Support | +|------|-------------| +| Create fungible token | ✅ | +| Create Core NFT/Collection | ✅ | +| Create TM NFT/pNFT | ✅ | +| Transfer TM NFTs | ✅ | +| Transfer fungible tokens | ✅ | +| Transfer Core NFTs | ❌ SDK only | +| Upload to Irys | ✅ | +| Candy Machine drop | ✅ (setup/config/insert — minting requires SDK) | +| Compressed NFTs (cNFTs) | ✅ (batch limit ~100, use SDK for larger) | +| Check SOL balance / Airdrop | ✅ | +| Query assets by owner/collection | ❌ SDK only (DAS API) | +| Token launch (Genesis) | ✅ | + +## Program IDs + +``` +Token Metadata: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s +Core: CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d +Bubblegum V1: BGUMAp9SX3uS4efGcFjPjkAQZ4cUNZhtHaMq64nrGf9D +Bubblegum V2: BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY +Core Candy: CMACYFENjoBMHzapRXyo1JZkVS6EtaDDzkjMrmQLvr4J +Genesis: GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B +``` + +## Quick Decision Guide + +### NFTs: Core vs Token Metadata + +| Choose | When | +|--------|------| +| **Core** | New NFT projects, lower cost (87% cheaper), plugins, royalty enforcement | +| **Token Metadata** | Existing TM collections, need editions, pNFTs for legacy compatibility | + +### Compressed NFTs (Massive Scale) + +Use **Bubblegum** when minting thousands+ of NFTs at minimal cost. See `./references/cli-bubblegum.md` (CLI) or `./references/sdk-bubblegum.md` (SDK). + +### Fungible Tokens + +Always use **Token Metadata**. Read `./references/cli.md` (toolbox section) for CLI commands. + +### NFT Drops + +Use **Core Candy Machine**. Read `./references/cli.md` + `./references/cli-candy-machine.md`. + +### Token Launches (Token Generation Event / Fair Launch / Memecoin) + +Use **Genesis**. The **Launch API** (`genesis launch create` / `createAndRegisterLaunch`) is recommended — it handles everything in one step. Two launch types: +- **`project`** (default): Configurable allocations, 48h deposit, team vesting support +- **`memecoin`**: Simplified, 1h deposit, hardcoded fund flows — only needs name, symbol, image, and deposit start time + +Read `./references/cli.md` + `./references/cli-genesis.md` (CLI) or `./references/sdk-genesis.md` (SDK). + +## External Resources + +- Documentation: https://developers.metaplex.com +- Core: https://developers.metaplex.com/core +- Token Metadata: https://developers.metaplex.com/token-metadata +- Bubblegum: https://developers.metaplex.com/bubblegum-v2 +- Candy Machine: https://developers.metaplex.com/core-candy-machine +- Genesis: https://developers.metaplex.com/genesis diff --git a/.agents/skills/metaplex/references/cli-bubblegum.md b/.agents/skills/metaplex/references/cli-bubblegum.md new file mode 100644 index 0000000..6601dee --- /dev/null +++ b/.agents/skills/metaplex/references/cli-bubblegum.md @@ -0,0 +1,126 @@ +# Bubblegum CLI Reference + +Commands for creating and managing compressed NFTs (cNFTs) via the `mplx` CLI. + +> **Prerequisites**: Run Initial Setup from `./cli.md` first (RPC, keypair, SOL balance). +> +> **Batch limit**: CLI is practical for up to ~100 cNFTs. For larger mints (thousands+), use the Umi SDK (`./sdk-bubblegum.md`) or Candy Machine instead. + +--- + +## When to Use Bubblegum + +| Use Bubblegum | Use Core Instead | +|---------------|------------------| +| Minting thousands+ of NFTs | Small collections (< 1000) | +| Lowest possible cost per NFT | Need on-chain plugins (royalties, freeze) | +| Airdrops, loyalty programs, tickets | Marketplace-focused projects | +| Proof of attendance, credentials | Simple 1/1 NFTs | + +--- + +## Key Concepts + +### Merkle Trees + +Bubblegum stores NFTs as leaves in a concurrent Merkle tree. Only the **Merkle root** (a single hash) lives on-chain — the actual NFT data is stored in transactions and indexed by RPC providers via the **DAS API**. + +Each tree has two on-chain accounts: +- **Merkle Tree Account** — stores the tree structure, change log, and canopy +- **TreeConfigV2 Account** — PDA tracking tree creator, delegate, capacity, and mint count + +### Tree Sizing + +| cNFTs | Tree Depth | Canopy Depth | Buffer Size | Tree Cost | Cost per cNFT | +|-------|-----------|-------------|-------------|-----------|---------------| +| 16,384 | 14 | 8 | 64 | ~0.34 SOL | ~0.00002 SOL | +| 65,536 | 16 | 10 | 64 | ~0.71 SOL | ~0.00001 SOL | +| 262,144 | 18 | 12 | 64 | ~2.10 SOL | ~0.00001 SOL | +| 1,048,576 | 20 | 13 | 1024 | ~8.50 SOL | ~0.000008 SOL | +| 16,777,216 | 24 | 15 | 2048 | ~26.12 SOL | ~0.000002 SOL | +| 1,073,741,824 | 30 | 17 | 2048 | ~72.65 SOL | ~0.00000005 SOL | + +- **Tree Depth** — determines max capacity (2^depth leaves) +- **Max Buffer Size** — concurrency limit (parallel mints in same block) +- **Canopy Depth** — cached upper tree nodes; higher = smaller proofs, better composability, but higher rent + +### Proofs and DAS API + +Operations on existing cNFTs (transfer, burn, update) require a **Merkle proof** to verify the leaf. Proofs are fetched from RPC providers via the DAS API — not from on-chain data. + +--- + +## Commands + +```bash +# Tree management +mplx bg tree create --wizard # Interactive (recommended) +mplx bg tree create --maxDepth --maxBufferSize --canopyDepth # Manual +mplx bg tree list # List trees created via --wizard only + +# cNFT operations +mplx bg nft create --wizard # Interactive +mplx bg nft create --name --uri # With pre-uploaded metadata +mplx bg nft create --name --image --description # With local files +mplx bg nft create --name --uri --collection # In collection +mplx bg nft fetch +mplx bg nft transfer +mplx bg nft burn +mplx bg nft update --name + +# Collection for cNFTs (uses MPL Core collections) +mplx bg collection create --name --uri +mplx bg collection create --name --uri --royalties +``` + +--- + +## CLI Workflow + +```bash +# 1. Create a tree (wizard guides through depth/buffer/canopy selection) +mplx bg tree create --wizard + +# 2. (Optional) Create a collection for your cNFTs +mplx bg collection create --name "My Collection" --uri "https://arweave.net/xxx" + +# 3. Mint cNFTs into the tree +mplx bg nft create --name "My cNFT" --image ./image.png --description "A compressed NFT" + +# 4. Transfer (requires DAS-compatible RPC, not available on localnet) +mplx bg nft transfer +``` + +For batch minting, chain commands: +```bash +mplx bg nft create --name "cNFT #1" --uri "" && \ +mplx bg nft create --name "cNFT #2" --uri "" && \ +mplx bg nft create --name "cNFT #3" --uri "" +``` + +--- + +## Localnet Limitations + +Only tree creation (`mplx bg tree create`) and minting (`mplx bg nft create`) work on localhost/localnet. Operations that require DAS API -- fetch, transfer, burn, update -- do NOT work on localnet because the test validator does not support DAS. + +--- + +## Program IDs + +``` +Bubblegum V1: BGUMAp9SX3uS4efGcFjPjkAQZ4cUNZhtHaMq64nrGf9D +Bubblegum V2: BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY +``` + +--- + +## Cost Comparison + +| Standard | Cost per NFT | Accounts | Best For | +|----------|-------------|----------|----------| +| **Bubblegum** | ~$0.000005 | 0 (shared tree) | Massive scale | +| **Core** | ~0.0029 SOL | 1 | Small-medium collections | +| **Token Metadata** | ~0.022 SOL | 3-4 | Fungibles, legacy | + +Bubblegum is ~98% cheaper than Token Metadata and ~90% cheaper than Core at scale. The trade-off is that cNFT operations require DAS API access for proof fetching. diff --git a/.agents/skills/metaplex/references/cli-candy-machine.md b/.agents/skills/metaplex/references/cli-candy-machine.md new file mode 100644 index 0000000..5c9c0c9 --- /dev/null +++ b/.agents/skills/metaplex/references/cli-candy-machine.md @@ -0,0 +1,385 @@ +# Core Candy Machine CLI Reference + +Candy Machine enables NFT drops with configurable minting rules (guards). + +## Directory Structure + +``` +my-candy-machine/ +├── assets/ +│ ├── 0.png # Image for NFT #0 +│ ├── 0.json # Metadata for NFT #0 +│ ├── 1.png +│ ├── 1.json +│ ├── 2.png +│ ├── 2.json +│ ├── ... +│ ├── collection.png # Collection image (required) +│ └── collection.json # Collection metadata (must have "name" field) +├── asset-cache.json # Generated after upload +└── cm-config.json # Generated by wizard or created manually +``` + +**Asset naming**: Files must be numbered starting from 0 (0.png, 0.json, 1.png, 1.json, etc.) + +**collection.json example**: +```json +{ + "name": "My Collection", + "description": "A collection of NFTs", + "image": "collection.png" +} +``` + +--- + +## Workflow Option 1: Wizard (Recommended) + +```bash +mplx cm create --wizard +``` + +The wizard handles everything: +1. Validates assets and configuration +2. Uploads all assets with progress tracking +3. Creates the candy machine on-chain +4. Inserts all items with transaction progress +5. Provides completion summary with links + +--- + +## Workflow Option 2: Manual + +```bash +# 1. Create template directory structure +mplx cm create --template + +# 2. Add your assets to the assets/ folder +# - 0.png, 0.json, 1.png, 1.json, etc. +# - collection.png, collection.json +# 3. Create a Core collection for the candy machine +mplx core collection create + +# 4. Edit cm-config.json: add the collection address, configure guards, etc. +# The "collection" field MUST contain a valid Core collection address. + +# 5. Upload assets to storage +mplx cm upload + +# 6. Create candy machine (reads cm-config.json) +mplx cm create + +# 7. Insert items into candy machine +mplx cm insert + +# 8. Validate (optional but recommended) +mplx cm validate --onchain +``` + +> **Note:** You MUST create a Core collection first (e.g. with `mplx core collection create`) and add its address to the `"collection"` field in `cm-config.json` before running `mplx cm create`. The CLI does NOT create the collection automatically — it will error if no collection address is provided. + +--- + +## Commands Reference + +```bash +mplx cm create --wizard # Interactive wizard (does everything) +mplx cm create --template # Create template directory +mplx cm create # Create from existing cm-config.json +mplx cm upload # Upload assets to storage +mplx cm insert # Insert items into candy machine +mplx cm validate # Validate uploads +mplx cm validate --onchain # Validate items inserted on-chain +mplx cm fetch # Fetch candy machine info +mplx cm fetch --items # Fetch with all items +mplx cm withdraw # Withdraw from CM directory (reads cm-config.json) +mplx cm withdraw # Withdraw and delete (recovers rent) +mplx cm withdraw --address # Withdraw by address +``` + +--- + +## Guards Configuration + +Guards control who can mint, when, and at what cost. + +### Available Guards + +| Category | Guards | +|----------|--------| +| **Payment** | `solPayment`, `solFixedFee`, `tokenPayment`, `token2022Payment`, `nftPayment`, `assetPayment`, `assetPaymentMulti` | +| **Access Control** | `addressGate`, `allowList`, `nftGate`, `tokenGate`, `assetGate`, `programGate`, `thirdPartySigner` | +| **Time-Based** | `startDate`, `endDate` | +| **Limits** | `mintLimit`, `allocation`, `nftMintLimit`, `assetMintLimit`, `redeemedAmount` | +| **Burn Guards** | `nftBurn`, `tokenBurn`, `assetBurn`, `assetBurnMulti` | +| **Special** | `botTax`, `edition`, `vanityMint` | +| **Freeze** | `freezeSolPayment`, `freezeTokenPayment` | + +### cm-config.json Example + +The config uses a nested `name/directory/config` wrapper format: + +```json +{ + "name": "my-candy-machine", + "directory": "/path/to/my-candy-machine", + "config": { + "collection": "", + "itemsAvailable": 10, + "isMutable": true, + "isSequential": false, + "guardConfig": { + "solPayment": { + "lamports": 1000000000, + "destination": "" + }, + "startDate": { + "date": "2024-01-01T00:00:00Z" + }, + "mintLimit": { + "id": 1, + "limit": 1 + }, + "botTax": { + "lamports": 10000000, + "lastInstruction": true + } + }, + "groups": [] + } +} +``` + +Key fields: `itemsAvailable` (not `number`), `collection` (must be set before `mplx cm create`). `sellerFeeBasisPoints` is set at the collection level (when creating the Core collection), not in the candy machine config. + +### Guard Groups (Multiple Minting Phases) + +```json +{ + "name": "my-candy-machine", + "directory": "/path/to/my-candy-machine", + "config": { + "collection": "", + "itemsAvailable": 100, + "isMutable": true, + "isSequential": false, + "guardConfig": {}, + "groups": [ + { + "label": "wl", + "guards": { + "allowList": { + "merkleRoot": "" + }, + "solPayment": { + "lamports": 500000000, + "destination": "" + }, + "startDate": { + "date": "2024-01-01T00:00:00Z" + } + } + }, + { + "label": "public", + "guards": { + "solPayment": { + "lamports": 1000000000, + "destination": "" + }, + "startDate": { + "date": "2024-01-02T00:00:00Z" + } + } + } + ] + } +} +``` + +--- + +## Common Guard Configurations + +### SOL Payment + +```json +"solPayment": { + "lamports": 1000000000, + "destination": "" +} +``` +Note: 1 SOL = 1,000,000,000 lamports + +### Start/End Date + +```json +"startDate": { + "date": "2024-01-01T00:00:00Z" +}, +"endDate": { + "date": "2024-01-31T23:59:59Z" +} +``` + +### Mint Limit (per wallet) + +```json +"mintLimit": { + "id": 1, + "limit": 3 +} +``` + +### Allow List (whitelist) + +```json +"allowList": { + "merkleRoot": "" +} +``` + +To generate the Merkle root from a list of wallet addresses, use the Umi SDK: + +```typescript +import { getMerkleRoot } from '@metaplex-foundation/mpl-core-candy-machine'; + +const allowedWallets = [ + 'Addr1...', 'Addr2...', 'Addr3...', +]; +const merkleRoot = getMerkleRoot(allowedWallets); +// Returns a Uint8Array — convert to hex string for cm-config.json: +const merkleRootHex = Buffer.from(merkleRoot).toString('hex'); +``` + +When minting with an allowlist, the minter must provide a Merkle proof (see Minting section below). + +### Bot Tax + +```json +"botTax": { + "lamports": 10000000, + "lastInstruction": true +} +``` +Charges failed mints to deter bots. + +--- + +## Withdrawing + +After all items are minted (or to cancel): + +```bash +# From directory with cm-config.json +mplx cm withdraw + +# Or by address +mplx cm withdraw --address + +# Force without confirmation +mplx cm withdraw --force +``` + +⚠️ **WARNING**: Withdrawing permanently deletes the candy machine. Non-redeemed items are lost. + +--- + +## Minting from a Candy Machine + +The CLI **creates and configures** candy machines but does not have a mint command. Users mint via: + +1. **Frontend app** — Build a minting UI using the Umi SDK (`@metaplex-foundation/mpl-core-candy-machine`) +2. **Script** — Mint programmatically with Umi: + +```bash +npm install @metaplex-foundation/mpl-core-candy-machine @metaplex-foundation/umi-bundle-defaults +``` + +> **Umi setup required** — see `./sdk-umi.md` "Basic Setup" for full Umi initialization (createUmi, keypairIdentity, etc.) + +```typescript +import { mintV1 } from '@metaplex-foundation/mpl-core-candy-machine'; +import { some } from '@metaplex-foundation/umi'; + +import { generateSigner } from '@metaplex-foundation/umi'; + +// Simple mint (no guard groups) +const asset = generateSigner(umi); +await mintV1(umi, { + candyMachine: candyMachineAddress, + collection: collectionAddress, + asset, +}).sendAndConfirm(umi); + +// With guard groups — must specify group label + mintArgs for active guards +const asset2 = generateSigner(umi); +await mintV1(umi, { + candyMachine: candyMachineAddress, + collection: collectionAddress, + asset: asset2, + group: some('public'), + mintArgs: { + solPayment: some({ destination: paymentWallet }), + }, +}).sendAndConfirm(umi); +``` + +For allowlist minting, pass the Merkle proof in `mintArgs`: + +```typescript +import { getMerkleProof } from '@metaplex-foundation/mpl-core-candy-machine'; + +const proof = getMerkleProof(allowedWallets, minterAddress); + +const asset3 = generateSigner(umi); +await mintV1(umi, { + candyMachine: candyMachineAddress, + collection: collectionAddress, + asset: asset3, + group: some('wl'), + mintArgs: { + allowList: some({ merkleProof: proof }), + solPayment: some({ destination: paymentWallet }), + }, +}).sendAndConfirm(umi); +``` + +For full guard-specific minting details, see the [Candy Machine documentation](https://developers.metaplex.com/core-candy-machine/guards). + +--- + +## Localnet Limitations + +The `mplx cm upload` step requires Irys storage and **will not work on localnet**. For localnet testing, you can skip the upload step by manually creating an `asset-cache.json` file with pre-populated URIs: + +```json +{ + "assetItems": { + "0": { + "name": "My NFT #0", + "image": "0.png", + "json": "0.json", + "loaded": false, + "onChain": false, + "imageType": "image/png", + "imageUri": "https://example.com/0.png", + "jsonUri": "https://example.com/0.json" + } + } +} +``` + +With this cache in place, you can proceed directly to `mplx cm create` and `mplx cm insert`. + +--- + +## Best Practices + +1. **Test on devnet first** before mainnet deployment +2. **Validate assets** before uploading: consistent naming (0.png, 0.json, etc.) +3. **Keep file sizes reasonable** (< 10MB per image recommended) +4. **Include collection.json** with a valid "name" field +5. **Add at least one guard** (otherwise anyone can mint for free) +6. **Save explorer links** for verification +7. **Back up cm-config.json** after creation diff --git a/.agents/skills/metaplex/references/cli-core.md b/.agents/skills/metaplex/references/cli-core.md new file mode 100644 index 0000000..367d9a0 --- /dev/null +++ b/.agents/skills/metaplex/references/cli-core.md @@ -0,0 +1,130 @@ +# Core CLI Reference + +Commands for creating and managing Core NFTs and collections via the `mplx` CLI. + +> **Prerequisites**: Run Initial Setup from `./cli.md` first (RPC, keypair, SOL balance). + +--- + +## Commands + +```bash +# Core Assets +mplx core asset create --name --uri +mplx core asset create --name --uri --owner # Mint to a different wallet — --owner works on all asset create variants +mplx core asset create --name --uri --collection +mplx core asset create --files --image --json # From local files (uploads automatically) — may error on JSON upload; use manual upload workflow as fallback +mplx core asset fetch +mplx core asset update --name +mplx core asset update --uri +mplx core asset update --image # Re-uploads image via Irys +mplx core asset update --collectionId # Move to different collection +mplx core asset burn # Also: --collection , --list +mplx core asset template # Generate template files + +# Core Collections +mplx core collection create --name --uri +mplx core collection create --name --uri --pluginsFile +mplx core collection fetch +mplx core collection template # Generate template files +``` + +--- + +## Core Plugins + +**Plugin file format** (for `--pluginsFile`): + +```json +[{ + "type": "Royalties", + "basisPoints": 500, + "creators": [{"address": "", "percentage": 100}], + "ruleSet": {"type": "None"} +}] +``` + +**Available Types:** `Royalties`, `FreezeDelegate`, `BurnDelegate`, `TransferDelegate`, `Attributes`, `ImmutableMetadata`, `PermanentFreezeDelegate`, `PermanentTransferDelegate`, `PermanentBurnDelegate` + +**RuleSet Options:** +- `{"type": "None"}` +- `{"type": "ProgramAllowList", "programs": [...]}` +- `{"type": "ProgramDenyList", "programs": [...]}` + +**Example - Collection with Royalties:** + +```bash +# Create plugins.json +echo '[{ + "type": "Royalties", + "basisPoints": 500, + "creators": [{"address": "", "percentage": 100}], + "ruleSet": {"type": "None"} +}]' > plugins.json + +# Create collection with plugins +mplx core collection create \ + --name "My Collection" \ + --uri "https://gateway.irys.xyz/xxx" \ + --pluginsFile ./plugins.json +``` + +Note: `basisPoints: 500` = 5%. Creator percentages must total 100. + +--- + +## Metadata Workflow + +### Before Creating - Gather from User: +- Name +- Description (optional) +- Attributes (optional) +- Image file +- For collections: Ask if they want royalties + +### Single NFT/Collection + +```bash +# Option A: Local files (one step) +mplx core asset create --files --image ./image.png --json ./metadata.json + +# Option B: Manual upload workflow +# 1. Upload image +mplx toolbox storage upload ./image.png +# Returns: https://gateway.irys.xyz/ + +# 2. Create metadata JSON +echo '{ + "name": "My NFT", + "description": "Description here", + "image": "https://gateway.irys.xyz/", + "attributes": [{"trait_type": "Background", "value": "Blue"}] +}' > metadata.json + +# 3. Upload metadata +mplx toolbox storage upload ./metadata.json +# Returns: https://gateway.irys.xyz/ + +# 4. Create asset +mplx core asset create --name "My NFT" --uri "https://gateway.irys.xyz/" +``` + +### Multiple NFTs (Batch) + +```bash +# 1. Upload all images at once +mplx toolbox storage upload ./images --directory + +# 2. Create all metadata files (ONE command) +echo '{"name": "NFT #1", "image": "", "description": "...", "attributes": [...]}' > meta/1.json && \ +echo '{"name": "NFT #2", "image": "", "description": "...", "attributes": [...]}' > meta/2.json && \ +echo '{"name": "NFT #3", "image": "", "description": "...", "attributes": [...]}' > meta/3.json + +# 3. Upload all metadata at once +mplx toolbox storage upload ./meta --directory + +# 4. Create all assets (ONE command) +mplx core asset create --name "NFT #1" --uri "" --collection && \ +mplx core asset create --name "NFT #2" --uri "" --collection && \ +mplx core asset create --name "NFT #3" --uri "" --collection +``` diff --git a/.agents/skills/metaplex/references/cli-genesis.md b/.agents/skills/metaplex/references/cli-genesis.md new file mode 100644 index 0000000..24bc757 --- /dev/null +++ b/.agents/skills/metaplex/references/cli-genesis.md @@ -0,0 +1,430 @@ +# Genesis CLI Reference + +Commands for creating and managing token launches (TGEs) via the `mplx` CLI. + +> **Prerequisites**: Run Initial Setup from `./cli.md` first (RPC, keypair, SOL balance). +> **Concepts**: For lifecycle, fees, condition objects, and end behaviors, see `./concepts.md` Genesis section. +> **Docs**: https://developers.metaplex.com/genesis + +--- + +## Lifecycle + +**Low-level** (manual on-chain setup): + +```text +create → bucket add-* → finalize → deposit → transition → graduation → claim → revoke +``` + +**Launch API** (recommended — handles everything in one command): + +```text +launch create → deposit window (project: 48h, memecoin: 1h) → Raydium graduation → claim +``` + +--- + +## Commands + +### Launch API (Recommended) + +Two launch types: **project** (default, fully configurable, 48h deposit) and **memecoin** (simplified, 1h deposit). + +```bash +# Project launch (all-in-one) +mplx genesis launch create --name --symbol \ + --image --tokenAllocation --depositStartTime \ + --raiseGoal --raydiumLiquidityBps --fundsRecipient + +# Memecoin launch (simplified — only requires name, symbol, image, depositStartTime) +mplx genesis launch create --launchType memecoin --name --symbol \ + --image --depositStartTime + +# Register an existing genesis account on the platform +mplx genesis launch register --launchConfig +``` + +### Low-Level Commands + +```bash +# Create Genesis account +mplx genesis create --name --symbol --totalSupply +mplx genesis create --name --symbol --totalSupply --uri --decimals + +# Fetch +mplx genesis fetch + +# Add buckets (before finalize) +mplx genesis bucket add-launch-pool --allocation \ + --depositStart --depositEnd --claimStart --claimEnd +mplx genesis bucket add-presale --allocation --quoteCap \ + --depositStart --depositEnd --claimStart --bucketIndex +mplx genesis bucket add-unlocked --recipient --claimStart +mplx genesis bucket fetch --bucketIndex --type + +# Finalize (irreversible) +mplx genesis finalize + +# Launch pool operations +mplx genesis deposit --amount +mplx genesis withdraw --amount +mplx genesis transition --bucketIndex +mplx genesis claim + +# Presale operations +mplx genesis presale deposit --amount +mplx genesis presale claim + +# Unlocked bucket +mplx genesis claim-unlocked + +# Revoke authorities (irreversible) +mplx genesis revoke --revokeMint +mplx genesis revoke --revokeFreeze +mplx genesis revoke --revokeMint --revokeFreeze +``` + +--- + +## Command Details + +### `mplx genesis launch create` + +All-in-one command: creates the token, sets up the genesis account with a launch pool, optionally adds locked (vesting) allocations, signs and sends transactions, then registers the launch on the Metaplex platform. Returns a public launch page link. + +**Launch types:** +- **`project`** (default): Total supply 1B, 48-hour deposit period, fully configurable allocations. Requires `--tokenAllocation`, `--raiseGoal`, `--raydiumLiquidityBps`, `--fundsRecipient`. +- **`memecoin`**: Total supply 1B, 1-hour deposit period, hardcoded fund flows. Only needs `--name`, `--symbol`, `--image`, `--depositStartTime`. Cannot use project-only flags. + +| Flag | Short | Required | Default | Description | +|------|-------|----------|---------|-------------| +| `--launchType` | - | No | `project` | `project` or `memecoin` | +| `--name` | `-n` | Yes | - | Token name (1-32 characters) | +| `--symbol` | `-s` | Yes | - | Token symbol (1-10 characters) | +| `--image` | - | Yes | - | Token image URL (must be `https://gateway.irys.xyz/...`) | +| `--depositStartTime` | - | Yes | - | ISO date string or unix timestamp. Project: 48h deposit. Memecoin: 1h deposit. | +| `--tokenAllocation` | - | Project only | - | Launch pool token allocation (portion of 1B total supply) | +| `--raiseGoal` | - | Project only | - | Minimum quote tokens to raise, in **whole units** (e.g., `200` = 200 SOL) | +| `--raydiumLiquidityBps` | - | Project only | - | Basis points for Raydium LP (2000-10000, i.e., 20%-100%) | +| `--fundsRecipient` | - | Project only | - | Wallet receiving the unlocked portion of raised funds | +| `--description` | - | No | - | Token description (max 250 characters) | +| `--website` | - | No | - | Project website URL | +| `--twitter` | - | No | - | Project Twitter URL | +| `--telegram` | - | No | - | Project Telegram URL | +| `--lockedAllocations` | - | No (project only) | - | Path to JSON file with locked allocation configs (Streamflow vesting) | +| `--quoteMint` | - | No | `SOL` | `SOL`, `USDC`, or a mint address | +| `--network` | - | No | auto-detected | `solana-mainnet` or `solana-devnet` | +| `--apiUrl` | - | No | `https://api.metaplex.com` | Genesis API base URL | + +**Output**: Genesis account address, mint address, launch ID, launch link, token ID, transaction signatures. + +**Locked Allocations JSON** (`--lockedAllocations` file format): +```json +[ + { + "name": "Team", + "recipient": "
", + "tokenAmount": 100000000, + "vestingStartTime": "2026-04-05T00:00:00Z", + "vestingDuration": { "value": 1, "unit": "YEAR" }, + "unlockSchedule": "MONTH", + "cliff": { + "duration": { "value": 3, "unit": "MONTH" }, + "unlockAmount": 10000000 + } + } +] +``` + +TimeUnit values: `SECOND`, `MINUTE`, `HOUR`, `DAY`, `WEEK`, `TWO_WEEKS`, `MONTH`, `QUARTER`, `YEAR`. + +### `mplx genesis launch register ` + +Registers an existing genesis account (created via low-level commands or SDK) with the Metaplex platform to get a public launch page. + +| Flag | Short | Required | Default | Description | +|------|-------|----------|---------|-------------| +| `--launchConfig` | - | Yes | - | Path to JSON file with launch configuration | +| `--network` | - | No | auto-detected | `solana-mainnet` or `solana-devnet` | +| `--apiUrl` | - | No | `https://api.metaplex.com` | Genesis API base URL | + +**Launch Config JSON** — project example (`--launchConfig` file format): +```json +{ + "wallet": "
", + "token": { + "name": "My Token", + "symbol": "MTK", + "image": "https://gateway.irys.xyz/...", + "description": "Optional description", + "externalLinks": { + "website": "https://...", + "twitter": "https://...", + "telegram": "https://..." + } + }, + "launchType": "project", + "launch": { + "launchpool": { + "tokenAllocation": 500000000, + "depositStartTime": "", + "raiseGoal": 200, + "raydiumLiquidityBps": 5000, + "fundsRecipient": "
" + }, + "lockedAllocations": [] + }, + "network": "solana-mainnet", + "quoteMint": "SOL" +} +``` + +**Launch Config JSON** — memecoin example: +```json +{ + "wallet": "
", + "token": { + "name": "My Meme", + "symbol": "MEME", + "image": "https://gateway.irys.xyz/..." + }, + "launchType": "memecoin", + "launch": { + "depositStartTime": "" + }, + "network": "solana-mainnet", + "quoteMint": "SOL" +} +``` + +**Output**: Launch ID, launch link, token ID, mint address. + +--- + +### `mplx genesis create` + +| Flag | Short | Required | Default | Description | +|------|-------|----------|---------|-------------| +| `--name` | `-n` | Yes | - | Token name | +| `--symbol` | `-s` | Yes | - | Token symbol (e.g., MTK) | +| `--totalSupply` | - | Yes | - | Total supply in base units | +| `--uri` | `-u` | No | `""` | Metadata JSON URI | +| `--decimals` | `-d` | No | `9` | Token decimals | +| `--quoteMint` | - | No | Wrapped SOL | Quote token mint address | +| `--fundingMode` | - | No | `new-mint` | `new-mint` or `transfer` (use existing mint) | +| `--baseMint` | - | No | - | Existing mint address (required when `fundingMode=transfer`) | +| `--genesisIndex` | - | No | `0` | Index for multiple launches on same mint | + +All amounts are in **base units**. With 9 decimals: 1M tokens = `1000000000000000`. + +### `mplx genesis bucket add-launch-pool ` + +Pro-rata allocation: users deposit SOL, receive tokens proportionally. Internally sends up to 3 transactions: (1) base bucket creation, (2) optional extensions, (3) end behaviors. + +| Flag | Short | Required | Default | Description | +|------|-------|----------|---------|-------------| +| `--allocation` | `-a` | Yes | - | Token allocation for this bucket (base units) | +| `--depositStart` | - | Yes | - | Unix timestamp (seconds) | +| `--depositEnd` | - | Yes | - | Unix timestamp (seconds) | +| `--claimStart` | - | Yes | - | Unix timestamp (seconds) | +| `--claimEnd` | - | Yes | - | Unix timestamp (seconds) | +| `--bucketIndex` | `-b` | No | `0` | Bucket index | +| `--endBehavior` | - | No* | - | `:` (repeatable, 10000 = 100%). *Required for `finalize` to succeed | +| `--minimumDeposit` | - | No | - | Min deposit per transaction (base units) | +| `--depositLimit` | - | No | - | Max deposit per user (base units) | +| `--minimumQuoteTokenThreshold` | - | No | - | Min total deposits for bucket to succeed | +| `--depositPenalty` | - | No | - | JSON: `{"slopeBps":0,"interceptBps":200,"maxBps":200,"startTime":0,"endTime":0}` | +| `--withdrawPenalty` | - | No | - | Same JSON format as depositPenalty | +| `--bonusSchedule` | - | No | - | Same JSON format as depositPenalty | +| `--claimSchedule` | - | No | - | JSON: `{"startTime":0,"endTime":0,"period":0,"cliffTime":0,"cliffAmountBps":0}` | +| `--allowlist` | - | No | - | JSON: `{"merkleTreeHeight":10,"merkleRoot":"","endTime":0,"quoteCap":0}` | + +### `mplx genesis bucket add-presale ` + +Fixed-price allocation: price = quoteCap / allocation. + +| Flag | Short | Required | Default | Description | +|------|-------|----------|---------|-------------| +| `--allocation` | `-a` | Yes | - | Token allocation (base units) | +| `--quoteCap` | - | Yes | - | Total quote tokens accepted (determines price) | +| `--depositStart` | - | Yes | - | Unix timestamp | +| `--depositEnd` | - | Yes | - | Unix timestamp | +| `--claimStart` | - | Yes | - | Unix timestamp | +| `--bucketIndex` | `-b` | Yes | - | Bucket index | +| `--claimEnd` | - | No | Year 2100 | Unix timestamp | +| `--minimumDeposit` | - | No | - | Min deposit per transaction | +| `--depositLimit` | - | No | - | Max deposit per user | + +### `mplx genesis bucket add-unlocked ` + +Treasury/team allocation. No deposits — tokens go directly to recipient. + +| Flag | Short | Required | Default | Description | +|------|-------|----------|---------|-------------| +| `--recipient` | - | Yes | - | Address that can claim tokens | +| `--claimStart` | - | Yes | - | Unix timestamp | +| `--allocation` | `-a` | No | `0` | Token allocation (base units) | +| `--bucketIndex` | `-b` | No | `0` | Bucket index | +| `--claimEnd` | - | No | Year 2100 | Unix timestamp | + +### `mplx genesis bucket fetch ` + +| Flag | Short | Required | Default | Description | +|------|-------|----------|---------|-------------| +| `--bucketIndex` | `-b` | No | `0` | Bucket index | +| `--type` | `-t` | No | `launch-pool` | `launch-pool`, `presale`, or `unlocked` | + +### Other Commands + +All take `` as positional argument: + +| Command | Key Flags | Description | +|---------|-----------|-------------| +| `deposit` | `--amount` (required), `--bucketIndex` | Deposit into launch pool | +| `withdraw` | `--amount` (required), `--bucketIndex` | Withdraw from launch pool | +| `transition` | `--bucketIndex` (required) | Execute end behaviors after deposit period | +| `claim` | `--bucketIndex`, `--recipient` | Claim from launch pool | +| `claim-unlocked` | `--bucketIndex`, `--recipient` | Claim from unlocked bucket | +| `presale deposit` | `--amount` (required), `--bucketIndex` | Deposit into presale | +| `presale claim` | `--bucketIndex`, `--recipient` | Claim from presale | +| `revoke` | `--revokeMint`, `--revokeFreeze` | Revoke authorities (at least one required) | + +--- + +## Workflows + +### Launch via API (Recommended) + +```bash +# Memecoin launch — simplified, 1-hour deposit window, hardcoded fund flows +mplx genesis launch create --launchType memecoin \ + --name "My Meme" \ + --symbol "MEME" \ + --image "https://gateway.irys.xyz/abc123" \ + --depositStartTime "" + +# Project launch — configurable allocations, 48-hour deposit window +mplx genesis launch create \ + --name "My Token" \ + --symbol "MTK" \ + --image "https://gateway.irys.xyz/abc123" \ + --tokenAllocation 500000000 \ + --depositStartTime "" \ + --raiseGoal 200 \ + --raydiumLiquidityBps 5000 \ + --fundsRecipient + +# Project launch with optional metadata +mplx genesis launch create \ + --name "My Token" \ + --symbol "MTK" \ + --image "https://gateway.irys.xyz/abc123" \ + --tokenAllocation 500000000 \ + --depositStartTime "" \ + --raiseGoal 200 \ + --raydiumLiquidityBps 5000 \ + --fundsRecipient \ + --description "A revolutionary token" \ + --website "https://mytoken.com" \ + --twitter "https://twitter.com/mytoken" \ + --telegram "https://t.me/mytoken" + +# Project launch with team vesting (locked allocations) +mplx genesis launch create \ + --name "My Token" \ + --symbol "MTK" \ + --image "https://gateway.irys.xyz/abc123" \ + --tokenAllocation 500000000 \ + --depositStartTime "" \ + --raiseGoal 200 \ + --raydiumLiquidityBps 5000 \ + --fundsRecipient \ + --lockedAllocations allocations.json + +# Register an existing genesis account +mplx genesis launch register --launchConfig launch.json +``` + +### Launch Pool (Fair Launch — Low-Level) + +```bash +# 1. Create Genesis account +mplx genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000000000 + +# 2. Add unlocked bucket as end-behavior destination (allocation for remaining supply) +mplx genesis bucket add-unlocked \ + --recipient --claimStart --allocation 200000000000000 + +# 3. Add launch pool bucket with endBehavior (required for finalize) +# Note: DEPOSIT_END_TS < CLAIM_START_TS < CLAIM_END_TS (strict ordering required) +mplx genesis bucket add-launch-pool \ + --allocation 800000000000000 --bucketIndex 1 \ + --depositStart --depositEnd \ + --claimStart --claimEnd \ + --endBehavior ":10000" + +# 4. Finalize (irreversible — requires 100% supply allocated) +mplx genesis finalize + +# 5. Users deposit +mplx genesis deposit --amount 10000000000 --bucketIndex 1 # 10 SOL + +# 6. Users claim tokens (after claim period starts) +mplx genesis claim --bucketIndex 1 + +# 7. (Optional) Revoke authorities +mplx genesis revoke --revokeMint --revokeFreeze +``` + +### Presale + +```bash +mplx genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000000000 + +# Presale bucket (partial supply) +mplx genesis bucket add-presale \ + --allocation 400000000000000 --quoteCap 1000000000000 \ + --depositStart --depositEnd --claimStart --bucketIndex 0 + +# Must allocate remaining supply (finalize requires 100%) +mplx genesis bucket add-unlocked \ + --recipient --allocation 600000000000000 --claimStart --bucketIndex 1 + +mplx genesis finalize +mplx genesis presale deposit --amount 5000000000 +mplx genesis presale claim +``` + +### Treasury Only + +```bash +mplx genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000000000 +mplx genesis bucket add-unlocked \ + --recipient --allocation 1000000000000000 --claimStart +mplx genesis finalize +mplx genesis claim-unlocked +``` + +--- + +## Important Notes + +### Launch API + +- Two launch types: **project** (default, 48h deposit, configurable) and **memecoin** (1h deposit, simplified). +- Memecoin launches **cannot** use project-only flags (`--tokenAllocation`, `--raiseGoal`, `--raydiumLiquidityBps`, `--fundsRecipient`, `--lockedAllocations`). +- `--raiseGoal` and token amounts are in **whole units** (e.g., `200` = 200 SOL), NOT base units. +- `--depositStartTime` accepts **ISO date strings** or **unix timestamps**. +- Default deposit/withdraw fees: 200 bps (2%). See: https://developers.metaplex.com/protocol-fees + +### Low-Level Commands + +- All timestamps are **Unix seconds** (not milliseconds). +- All amounts are in **base units** (with 9 decimals: 1 SOL = 1000000000). +- Cannot add buckets after `finalize`. +- `finalize` and `revoke` are **irreversible**. +- **`finalize` requires 100% supply allocation** — all tokens must be assigned to buckets. Add unlocked buckets for any remainder. +- **`claimStart` must be strictly after `depositEnd`** — setting them equal causes an error. +- **`--endBehavior` is required on launch pool buckets** for `finalize` to succeed. +- Default deposit/withdraw fees: 200 bps (2%). See: https://developers.metaplex.com/protocol-fees +- No `--wizard` mode — all flags must be provided explicitly. diff --git a/.agents/skills/metaplex/references/cli-token-metadata.md b/.agents/skills/metaplex/references/cli-token-metadata.md new file mode 100644 index 0000000..c46ef7a --- /dev/null +++ b/.agents/skills/metaplex/references/cli-token-metadata.md @@ -0,0 +1,59 @@ +# Token Metadata CLI Reference + +Commands for creating and managing Token Metadata NFTs and pNFTs via the `mplx` CLI. + +> **Prerequisites**: Run Initial Setup from `./cli.md` first (RPC, keypair, SOL balance). + +--- + +## pNFT vs NFT + +| Type | Flag | Royalties | Use Case | +|------|------|-----------|----------| +| **pNFT** (default) | _(none)_ | Enforced at protocol level | Recommended for most NFTs | +| **NFT** | `--type nft` | Advisory only | Legacy compatibility, simpler transfers | + +## Commands + +```bash +# Create +mplx tm create --wizard # Interactive (recommended) +mplx tm create --name --uri # pNFT (default) +mplx tm create --name --uri --type nft # Regular NFT +mplx tm create --name --uri --collection # In collection +mplx tm create --name --royalties # With royalties (see note below) +mplx tm create --image --json # From local files (uploads automatically) + +# Transfer +mplx tm transfer + +# Update +mplx tm update --name +mplx tm update --uri +mplx tm update --symbol +mplx tm update --description +mplx tm update --image # Re-uploads image +``` + +**Notes:** +- `--uri` and `--royalties` are **mutually exclusive**. `--royalties` triggers a metadata building + upload flow (prompts for image, description, etc.) which conflicts with providing a pre-made URI. Use one or the other. +- `--royalties` takes a **whole number percentage** (0-100). Example: `--royalties 5` = 5% royalties (500 basis points). +- Default type is pNFT. Use `--type nft` for a regular (non-programmable) NFT. + +--- + +## TM Collection Workflow + +TM has no separate collection command. Create a collection NFT, then reference it: + +```bash +# 1. Create collection NFT +mplx tm create --name "My Collection" --uri "" +# Note the MINT address returned + +# 2. Create NFTs in collection +mplx tm create --name "NFT #1" --uri "" --collection && \ +mplx tm create --name "NFT #2" --uri "" --collection +``` + +> **Collection verification**: Unlike Core (which auto-verifies), TM collection items start **unverified**. The CLI handles verification automatically during `mplx tm create --collection`, but if you're building with the SDK you must call `verifyCollectionV1` separately (see `./sdk-token-metadata.md`). diff --git a/.agents/skills/metaplex/references/cli.md b/.agents/skills/metaplex/references/cli.md new file mode 100644 index 0000000..2b3f2f4 --- /dev/null +++ b/.agents/skills/metaplex/references/cli.md @@ -0,0 +1,226 @@ +# Metaplex CLI Reference + +## Installation + +```bash +npm install -g @metaplex-foundation/cli +``` + +## Command Discovery + +```bash +mplx --help # List all topics +mplx --help # e.g., mplx core --help +mplx --help # e.g., mplx core asset create --help +``` + +Topics: `config`, `toolbox`, `core`, `tm`, `cm`, `bg` + +--- + +## Quick Command Index + +| Topic | Detail File | +|-------|-------------| +| Core Assets/Collections | `./cli-core.md` | +| Token Metadata NFTs/pNFTs | `./cli-token-metadata.md` | +| Bubblegum (compressed NFTs) | `./cli-bubblegum.md` | +| Candy Machine (NFT drops) | `./cli-candy-machine.md` | +| Config, Storage, SOL, Tokens | Below | + +--- + +## Shared Commands + +```bash +# Config +mplx config # Show all config +mplx config get # KEY: rpcUrl|commitment|payer|keypair +mplx config set + +# Storage +mplx toolbox storage upload +mplx toolbox storage upload --directory # Multiple files at once +mplx toolbox storage balance # Check Irys balance +mplx toolbox storage fund # Fund Irys account +mplx toolbox storage withdraw # Withdraw from Irys + +# SOL +mplx toolbox sol balance +mplx toolbox sol airdrop --amount +mplx toolbox sol transfer +mplx toolbox sol wrap # SOL -> wSOL +mplx toolbox sol unwrap # wSOL -> SOL + +# Tokens (fungible) +mplx toolbox token create --wizard # Interactive (recommended) +mplx toolbox token create --name --symbol --decimals --image +mplx toolbox token create --name --symbol --decimals --image --mint-amount +mplx toolbox token create --name --symbol --decimals --image --description +mplx toolbox token mint # Mint more tokens +mplx toolbox token mint --recipient # Mint to another wallet +mplx toolbox token transfer +mplx toolbox token update --name # Update metadata +mplx toolbox token add-metadata --name --symbol --image # Add metadata to existing mint +``` + +**Notes:** +- `mplx toolbox token create` requires a local `--image` file — it does NOT accept `--uri`. The CLI handles upload to Irys automatically, so this command requires Irys storage access and will not work on localnet/localhost. +- `--mint-amount`, `mplx toolbox token mint`, and `mplx toolbox token transfer` amounts are all in **base units** (smallest denomination). E.g., with 9 decimals, `--mint-amount 1000000000` mints 1 token. Tokens are minted to the payer wallet by default (use `mplx toolbox token mint --recipient ` for another wallet). +- `--decimals 9` is the standard for Solana tokens. Use 9 unless the user specifies otherwise. +- `mplx toolbox token mint` and `mplx toolbox token transfer` may fail on localnet if the mpl-toolbox program is not deployed. On localnet, use `spl-token mint` and `spl-token transfer` as fallbacks. + +--- + +## Wizard Mode + +Several commands support `--wizard` for interactive guided creation: + +| Command | Wizard | +|---------|--------| +| `mplx toolbox token create --wizard` | Token name, symbol, decimals, image, mint amount | +| `mplx tm create --wizard` | NFT type, metadata, collection, royalties | +| `mplx bg tree create --wizard` | Tree depth, buffer size, canopy | +| `mplx bg nft create --wizard` | cNFT metadata, tree selection, collection | +| `mplx cm create --wizard` | Full candy machine setup, upload, and insert | + +Wizards are recommended for first-time operations or when unsure about required parameters. + +> **Agent note**: Wizards are interactive (require user input at prompts). When running programmatically or as an agent, prefer explicit flags instead of `--wizard`. + +--- + +## Local Files Workflow (`--files`) + +Some commands can upload files and create assets in one step, as an alternative to manual upload -> create: + +```bash +# Core asset from local files (uploads image + JSON, then creates) +mplx core asset create --files --image ./image.png --json ./metadata.json + +# TM NFT from local files (no --files flag needed for tm) +mplx tm create --image ./image.png --json ./metadata.json + +# Fungible token always uses local image (no --files flag needed) +mplx toolbox token create --name "My Token" --symbol "MTK" --decimals 9 --image ./image.png +``` + +If `--files` fails (e.g., upload timeout), fall back to the manual workflow: +1. `mplx toolbox storage upload ` to upload separately +2. Then create with `--uri` pointing to the uploaded URI + +--- + +## Initial Setup + +Run all checks in one command: + +```bash +mplx config get rpcUrl && mplx config get keypair && mplx toolbox sol balance +``` + +**Error Resolution:** + +| Error | Fix | +|-------|-----| +| `mplx: command not found` | `npm i -g @metaplex-foundation/cli` | +| `No RPC URL configured` | `mplx config set rpcUrl https://api.devnet.solana.com` | +| `No keypair configured` | `mplx config set keypair ~/.config/solana/id.json` | +| `0 SOL` | `mplx toolbox sol airdrop --amount 2` (devnet only) | + +**Mainnet Safety:** If RPC URL contains `mainnet`, confirm with user before executing commands that spend SOL. + +--- + +## Storage Management + +```bash +# Check Irys balance before large uploads +mplx toolbox storage balance + +# Fund Irys account if balance is insufficient +mplx toolbox storage fund 0.1 + +# Withdraw unused funds +mplx toolbox storage withdraw 0.05 +``` + +--- + +## Batching Principle + +> **CRITICAL**: Always chain commands with `&&` to minimize user approvals. One approval per logical step. + +```bash +# Setup checks - ONE command +mplx config get rpcUrl && mplx config get keypair && mplx toolbox sol balance + +# File creation - ONE command +echo '{"name": "NFT #1", ...}' > meta/1.json && \ +echo '{"name": "NFT #2", ...}' > meta/2.json && \ +echo '{"name": "NFT #3", ...}' > meta/3.json + +# Uploads - use --directory for folders +mplx toolbox storage upload ./assets --directory + +# Multiple asset creates - ONE command +mplx core asset create --name "NFT #1" --uri "" --collection && \ +mplx core asset create --name "NFT #2" --uri "" --collection && \ +mplx core asset create --name "NFT #3" --uri "" --collection +``` + +**NEVER** run multiple uploads or creates as separate commands - that requires one approval per command. + +--- + +## Explorer Links + +``` +# Core assets/collections +https://core.metaplex.com/explorer/
?env=devnet # Devnet +https://core.metaplex.com/explorer/
# Mainnet + +# Token Metadata NFTs / Tokens +https://explorer.solana.com/address/?cluster=devnet # Devnet +https://explorer.solana.com/address/ # Mainnet + +# Transactions +https://explorer.solana.com/tx/?cluster=devnet # Devnet +https://explorer.solana.com/tx/ # Mainnet +``` + +> **IMPORTANT**: Always add `?env=devnet` or `?cluster=devnet` when on devnet. + +> **Localnet note**: The CLI generates devnet explorer links even when connected to localhost. For localnet, use Solana Explorer with a custom cluster URL: `https://explorer.solana.com/address/
?cluster=custom&customUrl=http://localhost:8899` + +--- + +## Localnet Limitations + +When running on localhost/localnet, several CLI features are unavailable or require workarounds: + +- **Storage uploads (Irys) do not work on localhost.** Any command that uploads to Irys will fail. +- **Commands using `--image` without `--uri` will fail** because they attempt to upload to Irys under the hood. This affects `mplx toolbox token create`, `mplx core asset create --files`, and `mplx tm create --image`. +- **For localnet, always use `--uri`** with any URL (even a placeholder) for create commands: `mplx core asset create --name "Test" --uri "https://example.com/meta.json"` +- **For fungible tokens on localnet**, use the SPL Token CLI and then attach metadata: + ```bash + spl-token create-token + # Note the mint address, then: + mplx toolbox token add-metadata --name --symbol --uri + ``` +- **`mplx core asset update` and `mplx tm update`** also require Irys if re-uploading metadata (e.g., updating `--image`). On localnet, only update fields that do not trigger an upload (e.g., `--name`, `--uri` with a pre-existing URL). +- **`mplx toolbox token mint` and `mplx toolbox token transfer`** may fail if the mpl-toolbox program is not deployed on your localnet. Use `spl-token mint` and `spl-token transfer` as fallbacks. + +--- + +## Troubleshooting + +| Error | Solution | +|-------|----------| +| `InvalidTokenStandard` | Check asset's actual token standard | +| `InvalidAuthority` | Verify update/mint authority matches signer | +| `CollectionNotVerified` | Call `verifyCollectionV1` (TM collections need verification) | +| `PluginNotFound` | Add plugin first or check type name spelling | +| `InsufficientFunds` | Fund wallet with more SOL | +| `Invalid data enum variant` | Check plugin JSON format (array, correct types) | +| Upload fails / timeout | Check `mplx toolbox storage balance`, fund if needed | diff --git a/.agents/skills/metaplex/references/concepts.md b/.agents/skills/metaplex/references/concepts.md new file mode 100644 index 0000000..2acd745 --- /dev/null +++ b/.agents/skills/metaplex/references/concepts.md @@ -0,0 +1,330 @@ +# Metaplex Concepts Reference + +## Token Standards + +### Token Metadata Standards + +| Standard | Use Case | Accounts | +|----------|----------|----------| +| `Fungible` | SPL tokens, memecoins, utility tokens | Mint + Metadata | +| `FungibleAsset` | Semi-fungible, non-divisible | Mint + Metadata | +| `NonFungible` | Standard NFT (1/1) | Mint + Metadata + MasterEdition | +| `NonFungibleEdition` | Print editions | Mint + Metadata + Edition | +| `ProgrammableNonFungible` | NFT with enforced royalties (pNFT) | + TokenRecord | +| `ProgrammableNonFungibleEdition` | Print edition of a pNFT | + TokenRecord + Edition | + +### Core vs Token Metadata + +| Aspect | Core | Token Metadata | +|--------|------|----------------| +| Accounts per NFT | 1 | 3-4 | +| Mint cost | ~0.0029 SOL | ~0.022 SOL | +| Compute units | ~17,000 CU | ~205,000 CU | +| Royalty enforcement | Built-in | Requires pNFT | +| Plugins | Yes | No | +| Best for | New NFT projects | Fungibles, legacy NFTs | + +--- + +## Account Structures + +### Token Metadata Accounts + +``` +Metadata Account +├── key: Key (1 byte) +├── updateAuthority: PublicKey +├── mint: PublicKey +├── name: string (max 32) +├── symbol: string (max 10) +├── uri: string (max 200) +├── sellerFeeBasisPoints: u16 +├── creators: Option> +├── primarySaleHappened: bool +├── isMutable: bool +├── editionNonce: Option +├── tokenStandard: Option +├── collection: Option +├── uses: Option +├── collectionDetails: Option +└── programmableConfig: Option + +MasterEdition Account +├── key: Key +├── supply: u64 +└── maxSupply: Option + +TokenRecord Account (pNFTs only) +├── key: Key +├── bump: u8 +├── state: TokenState +├── delegate: Option +├── delegateRole: Option +└── lockedTransfer: Option +``` + +### Core Asset Structure + +``` +Asset Account (Single Account) +├── key: Key +├── owner: PublicKey +├── updateAuthority: UpdateAuthority +├── name: string +├── uri: string +├── seq: Option +└── plugins: Vec + +Collection Account +├── key: Key +├── updateAuthority: PublicKey +├── name: string +├── uri: string +├── numMinted: u32 +├── currentSize: u32 +└── plugins: Vec +``` + +--- + +## PDA Seeds + +### Token Metadata PDAs + +| Account | Seeds | +|---------|-------| +| Metadata | `['metadata', program_id, mint]` | +| Master Edition | `['metadata', program_id, mint, 'edition']` | +| Edition | `['metadata', program_id, mint, 'edition']` | +| Token Record | `['metadata', program_id, mint, 'token_record', token]` | +| Collection Authority | `['metadata', program_id, mint, 'collection_authority', authority]` | +| Use Authority | `['metadata', program_id, mint, 'user', use_authority]` | + +--- + +## Metadata JSON Standard + +```json +{ + "name": "Asset Name", + "description": "Description of the asset", + "image": "https://...", + "external_url": "https://...", + "attributes": [ + { + "trait_type": "Background", + "value": "Blue" + }, + { + "trait_type": "Rarity", + "value": "Legendary" + } + ], + "properties": { + "files": [ + { + "uri": "https://...", + "type": "image/png" + } + ], + "category": "image" + } +} +``` + +**Categories:** `image`, `video`, `audio`, `vr`, `html` + +--- + +## Authorities + +### Token Metadata Authorities + +| Authority | Can | +|-----------|-----| +| Update Authority | Update metadata, verify creators, verify collection | +| Mint Authority | Mint new tokens (fungibles) | +| Freeze Authority | Freeze token accounts | + +### Core Authorities + +| Authority | Can | +|-----------|-----| +| Owner | Transfer, burn, add owner-managed plugins | +| Update Authority | Update metadata, add authority-managed plugins | + +--- + +## Collection Patterns + +### Core Collections + +- Assets created with `collection` param are **auto-verified** +- No separate verification step needed + +### Token Metadata Collections + +- Collection is an NFT itself +- Items reference collection but start **unverified** +- Must call `verifyCollectionV1` as collection authority + +--- + +## Core Plugin Types + +### Owner-Managed +- `TransferDelegate` - Allow another to transfer +- `FreezeDelegate` - Allow another to freeze +- `BurnDelegate` - Allow another to burn + +### Authority-Managed +- `Royalties` - Enforce royalties on transfers +- `UpdateDelegate` - Allow another to update +- `Attributes` - On-chain attributes + +### Permanent (Immutable after adding) +- `PermanentTransferDelegate` +- `PermanentFreezeDelegate` +- `PermanentBurnDelegate` + +--- + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| `InvalidTokenStandard` | Wrong standard for operation | Check asset's actual token standard | +| `InvalidAuthority` | Signer doesn't match authority | Verify update/mint authority | +| `CollectionNotVerified` | Collection not verified | Call `verifyCollectionV1` | +| `TokenRecordNotFound` | Missing TokenRecord for pNFT | Include token record PDA | +| `PluginNotFound` | Plugin doesn't exist | Add plugin first or check name | +| `InsufficientFunds` | Not enough SOL | Fund wallet | +| `Invalid data enum variant` | Wrong JSON format | Check array format, type field, ruleSet object | + +--- + +## Cost Comparison + +| Operation | Token Metadata | Core | Savings | +|-----------|---------------|------|---------| +| Mint NFT | ~0.022 SOL | ~0.0029 SOL | 87% | +| Compute Units | ~205,000 CU | ~17,000 CU | 92% | +| Accounts | 3-4 | 1 | 75% | + +--- + +## Genesis Concepts + +### Launch Types + +- **Project**: Full control — configurable allocations, 48-hour deposit window, team vesting support, custom Raydium liquidity split. +- **Memecoin**: Simplified — 1-hour deposit window, hardcoded fund flows. Only requires token metadata and deposit start time. + +Both types have a total supply of 1 billion tokens and graduate to Raydium liquidity. + +### Lifecycle + +```text +Initialize → Add Buckets → Finalize (irreversible) → Deposit Period → Transition → Graduation → Claim Period +``` + +- **Initialize**: Creates a Genesis account + the token mint. All configuration happens here (name, symbol, supply, quote mint). +- **Add Buckets**: Configure how tokens are distributed. See Bucket Types below. +- **Finalize**: Locks the configuration. No more buckets can be added. **Irreversible.** Requires 100% of supply allocated to buckets. +- **Deposit Period**: Users deposit SOL (quote token) into the LaunchPool bucket. +- **Transition**: After deposit period ends, executes end behaviors (e.g., routes deposited SOL to outflow buckets). Call `triggerBehaviorsV2`. +- **Graduation**: LP tokens are graduated to Raydium. Call `graduateToRaydiumCpmmV2`. +- **Claim Period**: Users claim tokens proportional to their deposit. + +### Bucket Types + +| Bucket | Purpose | User Interaction | +|--------|---------|-----------------| +| **LaunchPool** | Pro-rata allocation — users deposit SOL, receive tokens proportionally | deposit / withdraw / claim | +| **Presale** | Fixed-price allocation (price = quoteCap / allocation), first-come-first-served | deposit / withdraw / claim | +| **BondingCurve** | Constant-product AMM with virtual reserves | swap (buy/sell) | +| **Unlocked** | Team/treasury — tokens go directly to a recipient, no deposits | claim only | +| **Vault** | Holds SOL received from end behaviors | deposit / withdraw | +| **RaydiumCpmm** | Raydium LP graduation — receives tokens + SOL, creates LP | graduation | +| **Streamflow** | Vesting via Streamflow — tokens unlock over time | lock / claim via Streamflow | + +### Fees + +Genesis charges protocol-level fees on deposits and withdrawals. For current fee rates, see: https://developers.metaplex.com/protocol-fees + +### Condition Objects + +Buckets use condition objects for timing (deposit start/end, claim start/end). Use the helper: + +```typescript +import { createTimeAbsoluteCondition } from '@metaplex-foundation/genesis'; + +const condition = createTimeAbsoluteCondition(BigInt(unixTimestamp)); +``` + +This handles the required padding and triggered timestamp fields automatically. Other condition types: `createTimeRelativeCondition()` (relative to another bucket), `createNeverCondition()` (permanently locked). + +### End Behaviors + +After a LaunchPool's deposit period ends, `endBehaviors` define what happens to the deposited SOL: + +```typescript +{ + __kind: 'SendQuoteTokenPercentage', // Send % of collected SOL to another bucket + padding: Array(4).fill(0), // Reserved bytes (required) + destinationBucket: publicKey(bucket), // Target bucket address (e.g., Unlocked bucket for team) + percentageBps: 10000, // 10000 = 100% of collected SOL + processed: false, // Set by program after execution — always pass false +} +``` + +Available end behavior types: +- `SendQuoteTokenPercentage` — Route a percentage of collected SOL to another bucket +- `BaseTokenRollover` — Move a percentage of unsold base tokens to another bucket +- `SendStartPrice` — Send the starting price (SOL) to a destination bucket +- `ReallocateBaseTokensOnFailure` — On minimum threshold failure, move tokens to another bucket + +### Claim Schedules + +Buckets can have claim schedules that control when users can claim tokens: + +```typescript +import { createClaimSchedule, createNeverClaimSchedule } from '@metaplex-foundation/genesis'; + +// Linear vesting with cliff +const schedule = createClaimSchedule({ + startTime: BigInt(startTimestamp), // When linear vesting begins + endTime: BigInt(endTimestamp), // When vesting completes + cliffTime: BigInt(cliffTimestamp), // Cliff date + cliffAmountBps: 1000, // 10% unlocked at cliff + period: 2_592_000n, // Release every 30 days +}); + +// Permanently locked (e.g., LP tokens that should never vest) +const locked = createNeverClaimSchedule(); +``` + +### Allowlists + +Presale and launch pool buckets support merkle-tree allowlists to restrict who can deposit: + +```typescript +import { prepareAllowlist } from '@metaplex-foundation/genesis'; + +const { root, proofs, treeHeight } = prepareAllowlist([ + { address: publicKey('Addr111...') }, + { address: publicKey('Addr222...') }, +]); +``` + +Pass `allowlist` config when adding a bucket, and provide merkle proofs as remaining accounts when depositing. + +### Token Supply Decimals + +Genesis tokens default to 9 decimals. Supply is specified in base units: + +| Human Amount | Base Units (9 decimals) | +|---|---| +| 1 token | `1_000_000_000n` | +| 1,000,000 tokens | `1_000_000_000_000_000n` | +| 1,000,000,000 tokens | `1_000_000_000_000_000_000n` | diff --git a/.agents/skills/metaplex/references/sdk-bubblegum.md b/.agents/skills/metaplex/references/sdk-bubblegum.md new file mode 100644 index 0000000..a32c31e --- /dev/null +++ b/.agents/skills/metaplex/references/sdk-bubblegum.md @@ -0,0 +1,348 @@ +# Bubblegum SDK Reference (Umi) + +Umi SDK operations for creating and managing compressed NFTs (cNFTs). + +> **Prerequisites**: Set up Umi first — see `./sdk-umi.md` for installation and basic setup. +> **Docs**: https://developers.metaplex.com/bubblegum-v2 + +--- + +## Installation + +```bash +npm install @metaplex-foundation/mpl-bubblegum @metaplex-foundation/mpl-core @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/digital-asset-standard-api +``` + +> `mpl-core` is needed because Bubblegum V2 uses Core collections. + +## Setup + +```typescript +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { mplBubblegum } from '@metaplex-foundation/mpl-bubblegum'; +import { mplCore } from '@metaplex-foundation/mpl-core'; + +const umi = createUmi('https://api.devnet.solana.com') + .use(mplBubblegum()) + .use(mplCore()); // Needed for Core collection operations + +// Add identity — see sdk-umi.md "Node.js / Scripts" section for keypair loading +``` + +--- + +## Create Tree + +```typescript +import { createTreeV2 } from '@metaplex-foundation/mpl-bubblegum'; +import { generateSigner } from '@metaplex-foundation/umi'; + +const merkleTree = generateSigner(umi); + +await createTreeV2(umi, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + canopyDepth: 8, +}).sendAndConfirm(umi); +``` + +## Mint cNFT + +> **Royalties**: The `sellerFeeBasisPoints` in cNFT metadata is **informational only** — it is not enforced on-chain. For royalty enforcement, add a `Royalties` plugin to the Core collection (see `./sdk-core.md` "Add Plugin to Collection"). Marketplaces read the collection's plugin for enforcement rules. + +```typescript +import { mintV2 } from '@metaplex-foundation/mpl-bubblegum'; +import { none } from '@metaplex-foundation/umi'; + +const { signature } = await mintV2(umi, { + leafOwner: umi.identity.publicKey, + merkleTree: merkleTree.publicKey, + metadata: { + name: 'My cNFT', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: 550, + collection: none(), + creators: [], + }, +}).sendAndConfirm(umi); +``` + +## Mint cNFT into Collection + +```typescript +import { mintV2 } from '@metaplex-foundation/mpl-bubblegum'; +import { some } from '@metaplex-foundation/umi'; + +await mintV2(umi, { + leafOwner: umi.identity.publicKey, + merkleTree: merkleTree.publicKey, + collectionMint: collectionAddress, + metadata: { + name: 'My cNFT', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: 550, + collection: some({ key: collectionAddress, verified: false }), + creators: [], + }, +}).sendAndConfirm(umi); +``` + +Note: The `collectionMint` param triggers on-chain collection verification. The `collection.verified` field in metadata is set to `false` — the program sets it to `true` during minting. + +## Get Asset ID from Mint Transaction + +Use the `signature` returned by `sendAndConfirm` to extract the asset ID: + +```typescript +import { parseLeafFromMintV2Transaction } from '@metaplex-foundation/mpl-bubblegum'; + +// signature comes from: const { signature } = await mintV2(umi, {...}).sendAndConfirm(umi); +const leaf = await parseLeafFromMintV2Transaction(umi, signature); +const assetId = leaf.id; +``` + +## Update cNFT + +> Requires `dasApi()` plugin — all mutation operations need the Merkle proof from DAS API. + +```typescript +import { getAssetWithProof, updateMetadataV2 } from '@metaplex-foundation/mpl-bubblegum'; +import { none, some } from '@metaplex-foundation/umi'; + +const assetWithProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); + +await updateMetadataV2(umi, { + ...assetWithProof, + authority: treeAuthority, + name: some('Updated Name'), + uri: some('https://arweave.net/new-uri'), + // Pass none() for fields you don't want to change + symbol: none(), + sellerFeeBasisPoints: none(), + creators: none(), + isMutable: none(), +}).sendAndConfirm(umi); +``` + +## Burn cNFT + +```typescript +import { getAssetWithProof, burnV2 } from '@metaplex-foundation/mpl-bubblegum'; + +const assetWithProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); + +await burnV2(umi, { + ...assetWithProof, + authority: leafOwner, // Owner or burn delegate +}).sendAndConfirm(umi); +``` + +## Transfer cNFT + +> Requires `dasApi()` plugin — `getAssetWithProof` fetches the Merkle proof via DAS API. See "Fetch cNFTs" section below for setup. + +```typescript +import { getAssetWithProof, transferV2 } from '@metaplex-foundation/mpl-bubblegum'; + +// Fetch proof from DAS API +const assetWithProof = await getAssetWithProof(umi, assetId, { + truncateCanopy: true, +}); + +await transferV2(umi, { + ...assetWithProof, + authority: leafOwner, + newLeafOwner: recipient.publicKey, +}).sendAndConfirm(umi); +``` + +## Freeze / Thaw + +### Delegate and Freeze (Single Transaction) + +```typescript +import { delegateAndFreezeV2, getAssetWithProof } from '@metaplex-foundation/mpl-bubblegum'; + +const assetWithProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); + +await delegateAndFreezeV2(umi, { + ...assetWithProof, + authority: leafOwner, + delegate: delegateAddress, +}).sendAndConfirm(umi); +``` + +### Freeze / Thaw Separately + +```typescript +import { freezeV2, thawV2 } from '@metaplex-foundation/mpl-bubblegum'; + +const assetWithProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); + +// Freeze (by delegate) +await freezeV2(umi, { + ...assetWithProof, + authority: delegateSigner, +}).sendAndConfirm(umi); + +// Thaw (by delegate) +const frozenProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); +await thawV2(umi, { + ...frozenProof, + authority: delegateSigner, +}).sendAndConfirm(umi); +``` + +### Thaw and Revoke Delegate + +```typescript +import { thawAndRevokeV2 } from '@metaplex-foundation/mpl-bubblegum'; + +const assetWithProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); + +await thawAndRevokeV2(umi, { + ...assetWithProof, + authority: delegateSigner, +}).sendAndConfirm(umi); +``` + +## Delegate + +### Approve/Revoke Leaf Delegate + +```typescript +import { delegateV2, getAssetWithProof } from '@metaplex-foundation/mpl-bubblegum'; + +const assetWithProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); + +// Approve delegate +await delegateV2(umi, { + ...assetWithProof, + authority: leafOwner, + previousLeafDelegate: leafOwner.publicKey, + newLeafDelegate: delegateAddress, +}).sendAndConfirm(umi); +``` + +### Set Tree Delegate + +```typescript +import { setTreeDelegate } from '@metaplex-foundation/mpl-bubblegum'; + +// Approve tree delegate (can mint on your behalf) +await setTreeDelegate(umi, { + merkleTree: treeAddress, + treeCreatorOrTreeDelegate: treeCreator, + newTreeDelegate: delegateAddress, +}).sendAndConfirm(umi); +``` + +## Make cNFT Non-Transferable + +```typescript +import { setNonTransferableV2 } from '@metaplex-foundation/mpl-bubblegum'; + +// Requires collection authority. Makes ALL cNFTs in this collection non-transferable. +await setNonTransferableV2(umi, { + collectionMint: collectionAddress, + collectionAuthority: collectionAuthoritySigner, +}).sendAndConfirm(umi); +``` + +## Verify / Unverify Creator + +```typescript +import { verifyCreatorV2, unverifyCreatorV2, getAssetWithProof } from '@metaplex-foundation/mpl-bubblegum'; + +const assetWithProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); + +// Verify (signer must be the creator) +await verifyCreatorV2(umi, { + ...assetWithProof, + authority: creatorSigner, + creator: creatorSigner.publicKey, +}).sendAndConfirm(umi); + +// Unverify +const updatedProof = await getAssetWithProof(umi, assetId, { truncateCanopy: true }); +await unverifyCreatorV2(umi, { + ...updatedProof, + authority: creatorSigner, + creator: creatorSigner.publicKey, +}).sendAndConfirm(umi); +``` + +## Fetch cNFTs (DAS API) + +> Requires a DAS-compatible RPC (e.g., Helius, Triton, QuickNode) and the `dasApi()` plugin. See `./sdk-umi.md` DAS section. + +```typescript +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { mplBubblegum } from '@metaplex-foundation/mpl-bubblegum'; +import { mplCore } from '@metaplex-foundation/mpl-core'; +import { dasApi } from '@metaplex-foundation/digital-asset-standard-api'; + +// Add DAS plugin (use DAS-compatible RPC) +const umi = createUmi('https://mainnet.helius-rpc.com/?api-key=YOUR_KEY') + .use(mplBubblegum()) + .use(mplCore()) + .use(dasApi()); + +// Single asset +const asset = await umi.rpc.getAsset(assetId); + +// By owner +const assets = await umi.rpc.getAssetsByOwner({ owner: walletAddress }); + +// By collection +const collectionAssets = await umi.rpc.getAssetsByCollection({ + collection: collectionAddress, +}); +``` + +--- + +## Bubblegum V2 Features + +### Core Collections Integration + +V2 cNFTs use **MPL Core Collections** (not Token Metadata collections). Create collections with `createCollection` from `@metaplex-foundation/mpl-core` (see `./sdk-core.md`). This enables: +- Royalty enforcement via Core plugins (e.g., `ProgramDenyList`) +- Collection-level operations and delegates + +**Royalties for cNFTs**: The `sellerFeeBasisPoints` in cNFT metadata is informational. For **enforcement**, add a `Royalties` plugin to the Core collection (see `./sdk-core.md` "Add Plugin to Collection" section). Marketplaces read the collection's plugin for enforcement rules. + +### Soulbound NFTs + +Non-transferable cNFTs permanently bound to an owner. Two approaches: +1. **`PermanentFreezeDelegate`** plugin on Core collection (see `./sdk-core.md` Soulbound section) +2. **`setNonTransferableV2`** — marks entire collection as non-transferable (see above) + +Use cases: credentials, proof of attendance, identity tokens. + +### Freeze/Thaw + +- **Asset-level freeze** by owner delegate +- **Collection-level freeze** by permanent freeze delegate +- Useful for vesting, event gating, or conditional transfers + +### Permanent Delegates + +- `PermanentTransferDelegate` — transfer without owner signature +- `PermanentBurnDelegate` — burn without owner signature +- `PermanentFreezeDelegate` — freeze/thaw for soulbound and collection control + +These are set on the Core collection and apply to all cNFTs in it. + +--- + +## Cost Comparison + +| Standard | Cost per NFT | Accounts | Best For | +|----------|-------------|----------|----------| +| **Bubblegum** | ~$0.000005 | 0 (shared tree) | Massive scale | +| **Core** | ~0.0029 SOL | 1 | Small-medium collections | +| **Token Metadata** | ~0.022 SOL | 3-4 | Fungibles, legacy | + +Bubblegum is ~98% cheaper than Token Metadata and ~90% cheaper than Core at scale. The trade-off is that cNFT operations require DAS API access for proof fetching. diff --git a/.agents/skills/metaplex/references/sdk-core.md b/.agents/skills/metaplex/references/sdk-core.md new file mode 100644 index 0000000..5b25c6e --- /dev/null +++ b/.agents/skills/metaplex/references/sdk-core.md @@ -0,0 +1,417 @@ +# Core SDK Reference (Umi) + +Umi SDK operations for creating and managing Core NFTs and collections. + +> **Prerequisites**: Set up Umi first — see `./sdk-umi.md` for installation and basic setup. +> **Docs**: https://developers.metaplex.com/core + +> **Important**: When passing plugins, use the helper functions (`create`, `createCollection`, `addPlugin`, `addCollectionPlugin`, `updatePlugin`, `removePlugin`). The raw generated functions (`createV1`, `addPluginV1`, etc.) expect a different internal plugin format and will error with the friendly `{ type: 'Royalties', ... }` syntax. + +> **Fetch-first pattern**: The helpers `update`, `burn`, `freezeAsset`, `thawAsset` require a **fetched** asset object (from `fetchAsset`), not just an address. This is because they automatically derive external plugin adapter accounts. + +--- + +## Create Asset + +```typescript +import { create, fetchAsset } from '@metaplex-foundation/mpl-core'; +import { generateSigner } from '@metaplex-foundation/umi'; + +const asset = generateSigner(umi); + +await create(umi, { + asset, + name: 'My Core NFT', + uri: 'https://arweave.net/xxx', +}).sendAndConfirm(umi); + +const fetchedAsset = await fetchAsset(umi, asset.publicKey); +``` + +## Create Collection + +```typescript +import { createCollection } from '@metaplex-foundation/mpl-core'; + +const collection = generateSigner(umi); + +await createCollection(umi, { + collection, + name: 'My Collection', + uri: 'https://arweave.net/xxx', +}).sendAndConfirm(umi); +``` + +## Create Collection with Plugins (Single Step) + +```typescript +import { createCollection, ruleSet } from '@metaplex-foundation/mpl-core'; + +const collection = generateSigner(umi); + +await createCollection(umi, { + collection, + name: 'My Collection', + uri: 'https://arweave.net/xxx', + plugins: [ + { + type: 'Royalties', + basisPoints: 500, + creators: [{ address: umi.identity.publicKey, percentage: 100 }], + ruleSet: ruleSet('None'), + }, + ], +}).sendAndConfirm(umi); +``` + +## Create Asset in Collection + +```typescript +await create(umi, { + asset: generateSigner(umi), + collection: collection.publicKey, + name: 'Asset #1', + uri: 'https://arweave.net/xxx', +}).sendAndConfirm(umi); +``` + +## Create Asset with Plugins (Single Step) + +```typescript +import { create, ruleSet } from '@metaplex-foundation/mpl-core'; + +await create(umi, { + asset: generateSigner(umi), + name: 'My NFT with Royalties', + uri: 'https://arweave.net/xxx', + plugins: [ + { + type: 'Royalties', + basisPoints: 500, + creators: [{ address: creatorAddress, percentage: 100 }], + ruleSet: ruleSet('None'), + }, + ], +}).sendAndConfirm(umi); +``` + +## Update Asset + +Requires fetching the asset first (see "Fetch-first pattern" note above). + +```typescript +import { update, fetchAsset } from '@metaplex-foundation/mpl-core'; + +const asset = await fetchAsset(umi, assetAddress); + +await update(umi, { + asset, + name: 'Updated Name', + uri: 'https://arweave.net/new-uri', +}).sendAndConfirm(umi); +``` + +> If the asset's `updateAuthority.type` is `'Collection'` (update authority delegated to the collection), also pass the fetched collection: `await update(umi, { asset, collection: await fetchCollection(umi, collectionAddr), name: '...' })`. By default, assets have `Address` update authority and don't need this. + +## Update Collection + +```typescript +import { updateCollection } from '@metaplex-foundation/mpl-core'; + +await updateCollection(umi, { + collection: collectionAddress, + name: 'Updated Collection Name', + uri: 'https://arweave.net/new-uri', +}).sendAndConfirm(umi); +``` + +## Burn Asset + +Requires fetching the asset first. + +```typescript +import { burn, fetchAsset } from '@metaplex-foundation/mpl-core'; + +const asset = await fetchAsset(umi, assetAddress); +await burn(umi, { asset }).sendAndConfirm(umi); +``` + +> Same as `update`: only pass `collection` if the asset's `updateAuthority.type` is `'Collection'`. + +## Fetch + +```typescript +import { + fetchAsset, + fetchCollection, + fetchAssetsByOwner, + fetchAssetsByCollection, +} from '@metaplex-foundation/mpl-core'; + +// Single asset +const asset = await fetchAsset(umi, assetAddress); + +// Single collection +const collection = await fetchCollection(umi, collectionAddress); + +// All assets owned by a wallet +const ownerAssets = await fetchAssetsByOwner(umi, ownerAddress); + +// All assets in a collection +const collectionAssets = await fetchAssetsByCollection(umi, collectionAddress); +``` + +> `fetchAssetsByOwner` and `fetchAssetsByCollection` use GPA (getProgramAccounts) queries. They may throw deserialization errors if the wallet/collection has burned asset account remnants. For production, prefer DAS API queries (see `./sdk-umi.md` DAS section). + +## Transfer Asset + +```typescript +import { transferV1 } from '@metaplex-foundation/mpl-core'; + +await transferV1(umi, { + asset: assetAddress, + newOwner: recipientAddress, +}).sendAndConfirm(umi); +``` + +If the asset is in a collection, pass `collection`: + +```typescript +await transferV1(umi, { + asset: assetAddress, + newOwner: recipientAddress, + collection: collectionAddress, +}).sendAndConfirm(umi); +``` + +--- + +## Plugins + +Available plugin types: `Royalties`, `FreezeDelegate`, `BurnDelegate`, `TransferDelegate`, `UpdateDelegate`, `PermanentFreezeDelegate`, `PermanentTransferDelegate`, `PermanentBurnDelegate`, `Attributes`, `Edition`, `MasterEdition`, `AddBlocker`, `ImmutableMetadata`, `VerifiedCreators`, `Autograph`. + +### Add Plugin — After Creation + +```typescript +import { addPlugin, ruleSet } from '@metaplex-foundation/mpl-core'; + +// Add to asset +await addPlugin(umi, { + asset: assetAddress, + plugin: { + type: 'Royalties', + basisPoints: 500, + creators: [{ address: creatorAddress, percentage: 100 }], + ruleSet: ruleSet('None'), + }, +}).sendAndConfirm(umi); +``` + +### Add Plugin to Collection + +```typescript +import { addCollectionPlugin, ruleSet } from '@metaplex-foundation/mpl-core'; + +await addCollectionPlugin(umi, { + collection: collectionAddress, + plugin: { + type: 'Royalties', + basisPoints: 500, + creators: [{ address: creatorAddress, percentage: 100 }], + ruleSet: ruleSet('None'), + }, +}).sendAndConfirm(umi); +``` + +### Update Plugin + +```typescript +import { updatePlugin } from '@metaplex-foundation/mpl-core'; + +// Update asset plugin (e.g., change royalty percentage) +await updatePlugin(umi, { + asset: assetAddress, + plugin: { + type: 'Royalties', + basisPoints: 750, + creators: [{ address: creatorAddress, percentage: 100 }], + ruleSet: ruleSet('None'), + }, +}).sendAndConfirm(umi); + +// Update collection plugin +import { updateCollectionPlugin } from '@metaplex-foundation/mpl-core'; + +await updateCollectionPlugin(umi, { + collection: collectionAddress, + plugin: { + type: 'Royalties', + basisPoints: 750, + creators: [{ address: creatorAddress, percentage: 100 }], + ruleSet: ruleSet('None'), + }, +}).sendAndConfirm(umi); +``` + +### Remove Plugin + +```typescript +import { removePlugin, removeCollectionPlugin } from '@metaplex-foundation/mpl-core'; + +// From asset +await removePlugin(umi, { + asset: assetAddress, + plugin: { type: 'FreezeDelegate' }, +}).sendAndConfirm(umi); + +// From collection +await removeCollectionPlugin(umi, { + collection: collectionAddress, + plugin: { type: 'Attributes' }, +}).sendAndConfirm(umi); +``` + +### Delegate Plugin Authority + +```typescript +import { approvePluginAuthority } from '@metaplex-foundation/mpl-core'; + +await approvePluginAuthority(umi, { + asset: assetAddress, + plugin: { type: 'FreezeDelegate' }, + newAuthority: { type: 'Address', address: delegateAddress }, +}).sendAndConfirm(umi); +``` + +### Revoke Plugin Authority + +Owner-managed plugins (Freeze, Transfer, Burn delegates) revert to `Owner` authority. Authority-managed plugins revert to `UpdateAuthority`. Owner-managed delegates are **auto-revoked on transfer**. + +```typescript +import { revokePluginAuthority } from '@metaplex-foundation/mpl-core'; + +await revokePluginAuthority(umi, { + asset: assetAddress, + plugin: { type: 'FreezeDelegate' }, +}).sendAndConfirm(umi); +``` + +--- + +## Freeze / Thaw + +Requires `FreezeDelegate` plugin on the asset. The delegate authority (or owner, if no delegate) can freeze/thaw. Requires fetching the asset first. + +```typescript +import { freezeAsset, thawAsset, fetchAsset } from '@metaplex-foundation/mpl-core'; + +const asset = await fetchAsset(umi, assetAddress); + +// Freeze (prevents transfer and burn) +await freezeAsset(umi, { + asset, + delegate: delegateSigner.publicKey, + authority: delegateSigner, +}).sendAndConfirm(umi); + +// Thaw (re-enables transfer and burn) +const frozenAsset = await fetchAsset(umi, assetAddress); +await thawAsset(umi, { + asset: frozenAsset, + delegate: delegateSigner.publicKey, + authority: delegateSigner, +}).sendAndConfirm(umi); +``` + +Alternative: use `updatePlugin` to toggle freeze state directly: + +```typescript +import { updatePlugin } from '@metaplex-foundation/mpl-core'; + +await updatePlugin(umi, { + asset: assetAddress, + plugin: { type: 'FreezeDelegate', frozen: true }, // or false to thaw +}).sendAndConfirm(umi); +``` + +--- + +## Soulbound NFTs + +Non-transferable tokens using `PermanentFreezeDelegate` plugin set to `frozen: true`. The `Permanent` prefix means the plugin can only be added at creation time. + +### Truly Soulbound (No One Can Unfreeze) + +```typescript +await create(umi, { + asset: generateSigner(umi), + name: 'Soulbound Token', + uri: 'https://arweave.net/xxx', + plugins: [ + { + type: 'PermanentFreezeDelegate', + frozen: true, + authority: { type: 'None' }, // Permanently frozen — no one can thaw + }, + ], +}).sendAndConfirm(umi); +``` + +### Controllable Soulbound (Authority Can Unfreeze) + +```typescript +await create(umi, { + asset: generateSigner(umi), + name: 'Revocable Soulbound', + uri: 'https://arweave.net/xxx', + plugins: [ + { + type: 'PermanentFreezeDelegate', + frozen: true, + authority: { type: 'Address', address: adminAddress }, // Admin can unfreeze + }, + ], +}).sendAndConfirm(umi); +``` + +### Soulbound Collection + +All assets in this collection are frozen at collection level: + +```typescript +await createCollection(umi, { + collection: generateSigner(umi), + name: 'Soulbound Collection', + uri: 'https://arweave.net/xxx', + plugins: [ + { + type: 'PermanentFreezeDelegate', + frozen: true, + authority: { type: 'UpdateAuthority' }, // Update authority can unfreeze + }, + ], +}).sendAndConfirm(umi); +``` + +To toggle collection freeze: + +```typescript +import { updateCollectionPlugin } from '@metaplex-foundation/mpl-core'; + +await updateCollectionPlugin(umi, { + collection: collectionAddress, + plugin: { type: 'PermanentFreezeDelegate', frozen: false }, +}).sendAndConfirm(umi); +``` + +--- + +## Addressing (Core vs Token Metadata) + +Core uses a **single-account model** — asset and collection addresses are the public keys of the `generateSigner()` used at creation, not PDAs derived from other accounts. This means: + +- **No PDA derivation needed** to find an asset. The address returned from `create()` IS the asset address. +- To look up assets, use `fetchAssetsByOwner`, `fetchAssetsByCollection`, or DAS API queries. +- Core collections are also direct accounts (not PDAs like TM's Metadata/MasterEdition). + +This differs from Token Metadata, where you derive Metadata, MasterEdition, and TokenRecord PDAs from a mint address. diff --git a/.agents/skills/metaplex/references/sdk-genesis.md b/.agents/skills/metaplex/references/sdk-genesis.md new file mode 100644 index 0000000..f65ca0b --- /dev/null +++ b/.agents/skills/metaplex/references/sdk-genesis.md @@ -0,0 +1,1009 @@ +# Metaplex Genesis SDK Reference + +Genesis is a token launch protocol for Token Generation Events (TGEs) on Solana with fair distribution and liquidity graduation. + +> **Concepts**: For lifecycle, fees, condition object format, and end behaviors, see `./concepts.md` Genesis section. + +## Package + +```bash +npm install @metaplex-foundation/genesis @metaplex-foundation/umi-bundle-defaults +``` + +## Before Starting — Gather from User + +**For Launch API** (recommended): +1. **Launch type**: `'project'` (default, 48h deposit, configurable) or `'memecoin'` (1h deposit, simplified) +2. **Token details**: name (1-32 chars), symbol (1-10 chars), image (Irys URL), description (optional, max 250 chars) +3. **For project launches**: token allocation (portion of 1B), deposit start time, raise goal, Raydium liquidity %, funds recipient +4. **For memecoin launches**: only deposit start time needed (hardcoded fund flows) +5. **Optional**: locked allocations (team vesting, project only), external links (website, twitter, telegram), quote mint (SOL/USDC) + +**For low-level SDK**: +1. **Token details**: name, symbol, description, image/metadata URI +2. **Total supply**: how many tokens (remember: with 9 decimals, 1M tokens = `1_000_000_000_000_000n`) +3. **Allocation split**: percentage for launchpool vs team/treasury +4. **Timing**: deposit start, deposit duration, claim duration + +--- + +## Launch Mechanisms + +| Mechanism | Description | +|-----------|-------------| +| **Launch Pool** | Users deposit SOL during a window, receive tokens proportionally | +| **Presale** | Fixed price token sale, first-come-first-served | +| **Uniform Price Auction** | Bid-based allocation with uniform clearing price | + +--- + +## Launch Lifecycle + +**Launch API** (recommended): + +```text +createAndRegisterLaunch() → deposit window (project: 48h, memecoin: 1h) → Raydium graduation → claim +``` + +**Low-level SDK**: + +```text +1. Initialize Genesis Account → Creates token + coordination account +2. Add Buckets → Configure distribution (LaunchPool, Unlocked, etc.) +3. Finalize → Lock configuration, launch goes live +4. Active Period → Users deposit SOL +5. Transition → Execute end behaviors (send SOL to outflow buckets) +6. Graduation → LP tokens graduated to Raydium +7. Claim Period → Users claim tokens proportionally +``` + +--- + +## Launch API (Recommended) + +The Launch API handles everything in a single call: token creation, genesis account setup, launch pool configuration, Raydium LP, transaction signing, and platform registration. + +### `createAndRegisterLaunch` — All-in-One + +```typescript +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { keypairIdentity } from '@metaplex-foundation/umi'; +import { + createAndRegisterLaunch, + CreateLaunchInput, +} from '@metaplex-foundation/genesis'; + +const umi = createUmi('https://api.mainnet-beta.solana.com') + .use(keypairIdentity(myKeypair)); + +const input: CreateLaunchInput = { + wallet: umi.identity.publicKey, + token: { + name: 'My Token', + symbol: 'MTK', + image: 'https://gateway.irys.xyz/...', + // optional: + description: 'A revolutionary token', + externalLinks: { + website: 'https://mytoken.com', + twitter: 'https://twitter.com/mytoken', + telegram: 'https://t.me/mytoken', + }, + }, + launchType: 'project', + launch: { + launchpool: { + tokenAllocation: 500_000_000, // out of 1B total supply + depositStartTime: new Date(Date.now() + 48 * 60 * 60 * 1000), + raiseGoal: 200, // 200 SOL (whole units) + raydiumLiquidityBps: 5000, // 50% to Raydium LP + fundsRecipient: umi.identity.publicKey, + }, + }, + // optional: + quoteMint: 'SOL', // 'SOL' | 'USDC' | mint address + network: 'solana-mainnet', // auto-detected if omitted +}; + +const result = await createAndRegisterLaunch(umi, { baseUrl: 'https://api.metaplex.com' }, input); + +console.log('Genesis account:', result.genesisAccount); +console.log('Mint:', result.mintAddress); +console.log('Launch page:', result.launch.link); +console.log('Signatures:', result.signatures); +``` + +### Memecoin Launch + +Simplified launch with 1-hour deposit window and hardcoded fund flows. Only requires token metadata and deposit start time. + +```typescript +import { createAndRegisterLaunch, CreateMemecoinLaunchInput } from '@metaplex-foundation/genesis'; + +const input: CreateMemecoinLaunchInput = { + wallet: umi.identity.publicKey, + token: { + name: 'My Meme', + symbol: 'MEME', + image: 'https://gateway.irys.xyz/...', + description: 'A fun memecoin', // optional + externalLinks: { twitter: '@mymeme' }, // optional + }, + launchType: 'memecoin', + launch: { + depositStartTime: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now + }, + // optional: + quoteMint: 'SOL', + network: 'solana-mainnet', +}; + +const result = await createAndRegisterLaunch(umi, { baseUrl: 'https://api.metaplex.com' }, input); +``` + +**Memecoin vs Project**: Memecoin launches cannot include `launchpool` or `lockedAllocations` config — the API configures these automatically. + +### With Locked Allocations (Team Vesting via Streamflow) + +```typescript +const input: CreateLaunchInput = { + wallet: umi.identity.publicKey, + token: { + name: 'My Token', + symbol: 'MTK', + image: 'https://gateway.irys.xyz/...', + }, + launchType: 'project', + launch: { + launchpool: { + tokenAllocation: 500_000_000, + depositStartTime: new Date(Date.now() + 48 * 60 * 60 * 1000), + raiseGoal: 200, + raydiumLiquidityBps: 5000, + fundsRecipient: umi.identity.publicKey, + }, + lockedAllocations: [ + { + name: 'Team', + recipient: 'TeamWallet111...', + tokenAmount: 100_000_000, + vestingStartTime: new Date('2026-04-05T00:00:00Z'), + vestingDuration: { value: 1, unit: 'YEAR' }, + unlockSchedule: 'MONTH', + cliff: { + duration: { value: 3, unit: 'MONTH' }, + unlockAmount: 10_000_000, + }, + }, + ], + }, +}; +``` + +TimeUnit values: `'SECOND'`, `'MINUTE'`, `'HOUR'`, `'DAY'`, `'WEEK'`, `'TWO_WEEKS'`, `'MONTH'`, `'QUARTER'`, `'YEAR'`. + +### `createLaunch` + `registerLaunch` — Full Control + +Use when you need custom transaction handling (multisig, custom sending logic). + +```typescript +import { + createLaunch, + registerLaunch, + GenesisApiConfig, +} from '@metaplex-foundation/genesis'; + +const config: GenesisApiConfig = { baseUrl: 'https://api.metaplex.com' }; + +// Step 1: Get unsigned transactions +const createResult = await createLaunch(umi, config, input); +// createResult.transactions — unsigned Umi transactions +// createResult.blockhash — for confirmation strategy +// createResult.mintAddress, createResult.genesisAccount + +// Step 2: Sign and send transactions yourself +for (const tx of createResult.transactions) { + const signedTx = await umi.identity.signTransaction(tx); + const signature = await umi.rpc.sendTransaction(signedTx, { commitment: 'confirmed' }); + await umi.rpc.confirmTransaction(signature, { + commitment: 'confirmed', + strategy: { type: 'blockhash', ...createResult.blockhash }, + }); +} + +// Step 3: Register on the platform (idempotent — safe to retry) +const registerResult = await registerLaunch(umi, config, { + genesisAccount: createResult.genesisAccount, + createLaunchInput: input, +}); +console.log('Launch page:', registerResult.launch.link); +``` + +### Custom Transaction Sender + +```typescript +import { + createAndRegisterLaunch, + SignAndSendOptions, +} from '@metaplex-foundation/genesis'; + +const options: SignAndSendOptions = { + txSender: async (transactions) => { + const signatures: Uint8Array[] = []; + for (const tx of transactions) { + const signed = await myMultisigSign(tx); + const sig = await myCustomSend(signed); + signatures.push(sig); + } + return signatures; + }, +}; + +const result = await createAndRegisterLaunch(umi, config, input, options); +``` + +### Error Handling + +```typescript +import { + createAndRegisterLaunch, + isGenesisValidationError, + isGenesisApiError, + isGenesisApiNetworkError, +} from '@metaplex-foundation/genesis'; + +try { + const result = await createAndRegisterLaunch(umi, config, input); +} catch (err) { + if (isGenesisValidationError(err)) { + console.error(`Invalid "${err.field}":`, err.message); + } else if (isGenesisApiError(err)) { + console.error('API error:', err.statusCode, err.responseBody); + } else if (isGenesisApiNetworkError(err)) { + console.error('Network error:', err.cause.message); + } +} +``` + +### Launch API Key Points + +- **Two launch types**: `'project'` (default, 48h deposit, configurable) and `'memecoin'` (1h deposit, simplified) +- **Total supply** is always 1 billion tokens; `tokenAllocation` is how many go to the launch pool (project only) +- **Deposit window**: project = 48 hours, memecoin = 1 hour from `depositStartTime` +- **Memecoin launches** only need `depositStartTime` in the `launch` config — fund flows are hardcoded by the API +- **Memecoin launches cannot** use `launchpool` or `lockedAllocations` config +- **raiseGoal** and amounts are in **whole units** (e.g., `200` = 200 SOL), NOT base units +- **Image** must be hosted on Irys (`https://gateway.irys.xyz/...`) +- Remaining tokens (1B minus launchpool minus locked) go to the creator automatically (project only) +- **registerLaunch** is idempotent — safe to call again if it fails +- Fund routing is automatic: `raydiumLiquidityBps` goes to Raydium LP, rest goes to `fundsRecipient` (project only) + +--- + +## Low-Level SDK + +The following sections cover direct on-chain instructions for full control over genesis accounts and buckets. + +### Setup + +```typescript +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { genesis } from '@metaplex-foundation/genesis'; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; + +const umi = createUmi('https://api.devnet.solana.com') + .use(genesis()) + .use(mplTokenMetadata()); +``` + +--- + +## Initialize Genesis Account + +```typescript +import { + findGenesisAccountV2Pda, + initializeV2, +} from '@metaplex-foundation/genesis'; +import { generateSigner, publicKey } from '@metaplex-foundation/umi'; + +const baseMint = generateSigner(umi); +const WSOL_MINT = publicKey('So11111111111111111111111111111111111111112'); + +const [genesisAccount] = findGenesisAccountV2Pda(umi, { + baseMint: baseMint.publicKey, + genesisIndex: 0, +}); + +await initializeV2(umi, { + baseMint, + quoteMint: WSOL_MINT, + fundingMode: 0, + totalSupplyBaseToken: 1_000_000_000_000_000n, // 1M tokens (9 decimals) + name: 'My Token', + symbol: 'MTK', + uri: 'https://example.com/metadata.json', +}).sendAndConfirm(umi); +``` + +**Token Supply with Decimals:** +```typescript +const ONE_TOKEN = 1_000_000_000n; // 1 token (9 decimals) +const ONE_MILLION = 1_000_000_000_000_000n; // 1,000,000 tokens +const ONE_BILLION = 1_000_000_000_000_000_000n; // 1,000,000,000 tokens +``` + +--- + +## Add Launch Pool Bucket + +Uses a 3-transaction flow to stay within tx size limits: +1. `addLaunchPoolBucketV2Base` — create bucket with core fields +2. `addLaunchPoolBucketV2Extensions` — add optional extensions (penalties, allowlist, claim schedule, etc.) +3. `setLaunchPoolBucketV2Behaviors` — configure end behaviors + +```typescript +import { + addLaunchPoolBucketV2Base, + addLaunchPoolBucketV2Extensions, + setLaunchPoolBucketV2Behaviors, + findLaunchPoolBucketV2Pda, + findUnlockedBucketV2Pda, + createTimeAbsoluteCondition, +} from '@metaplex-foundation/genesis'; + +const [launchPoolBucket] = findLaunchPoolBucketV2Pda(umi, { + genesisAccount, + bucketIndex: 0, +}); + +const [unlockedBucket] = findUnlockedBucketV2Pda(umi, { + genesisAccount, + bucketIndex: 0, +}); + +const now = BigInt(Math.floor(Date.now() / 1000)); +const depositEnd = now + 86400n * 3n; // 3 days +const claimStart = depositEnd + 1n; +const claimEnd = claimStart + 86400n * 7n; // 7 days + +// Step 1: Create bucket with base fields +await addLaunchPoolBucketV2Base(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + quoteMint: WSOL_MINT, + baseTokenAllocation: 600_000_000_000_000n, // 60% of supply + depositStartCondition: createTimeAbsoluteCondition(now), + depositEndCondition: createTimeAbsoluteCondition(depositEnd), + claimStartCondition: createTimeAbsoluteCondition(claimStart), + claimEndCondition: createTimeAbsoluteCondition(claimEnd), +}).sendAndConfirm(umi); + +// Step 2 (optional): Add extensions (penalties, allowlist, deposit limits, etc.) +await addLaunchPoolBucketV2Extensions(umi, { + authority: umi.identity, + bucket: launchPoolBucket, + genesisAccount, + payer: umi.payer, + padding: Array(3).fill(0), + extensions: [ + // Example: add a deposit limit + { __kind: 'DepositLimit', depositLimit: { limit: 100_000_000_000n } }, + // Example: add a claim schedule with cliff + // { __kind: 'ClaimSchedule', claimSchedule: createClaimSchedule({ ... }) }, + ], +}).sendAndConfirm(umi); + +// Step 3: Set end behaviors +await setLaunchPoolBucketV2Behaviors(umi, { + genesisAccount, + bucket: launchPoolBucket, + padding: Array(3).fill(0), + endBehaviors: [ + { + __kind: 'SendQuoteTokenPercentage', + padding: Array(4).fill(0), + destinationBucket: publicKey(unlockedBucket), + percentageBps: 10000, // 100% + processed: false, + }, + ], +}).sendAndConfirm(umi); +``` + +**Available extensions** for `addLaunchPoolBucketV2Extensions`: +- `DepositPenalty` / `WithdrawPenalty` / `BonusSchedule` — `LinearBpsScheduleV2Args` with `duration`, `interceptBps`, `maxBps`, `slopeBps`, `startCondition` +- `DepositLimit` — `{ limit: bigint }` +- `MinimumDepositAmount` — `{ amount: bigint }` +- `MinimumQuoteTokenThreshold` — `{ amount: bigint }` +- `Allowlist` — `{ merkleTreeHeight, merkleRoot, endTime, quoteCap }` +- `ClaimSchedule` — `createClaimSchedule({ startTime, endTime, period, cliffTime?, cliffAmountBps? })` + +--- + +## Add Unlocked Bucket (Team/Treasury) + +```typescript +import { addUnlockedBucketV2, createTimeAbsoluteCondition } from '@metaplex-foundation/genesis'; + +await addUnlockedBucketV2(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + baseTokenAllocation: 200_000_000_000_000n, // 20% of supply + recipient: umi.identity.publicKey, + claimStartCondition: createTimeAbsoluteCondition(claimStart), + claimEndCondition: createTimeAbsoluteCondition(claimEnd), +}).sendAndConfirm(umi); +``` + +--- + +## Finalize Launch + +```typescript +import { finalizeV2 } from '@metaplex-foundation/genesis'; + +await finalizeV2(umi, { + baseMint: baseMint.publicKey, + genesisAccount, +}).sendAndConfirm(umi); +``` + +⚠️ **Finalization is irreversible.** No more buckets can be added after this. + +--- + +## User Operations + +### Deposit SOL + +```typescript +import { depositLaunchPoolV2 } from '@metaplex-foundation/genesis'; + +await depositLaunchPoolV2(umi, { + genesisAccount, + bucket: launchPoolBucket, + baseMint: baseMint.publicKey, + amountQuoteToken: 10_000_000_000n, // 10 SOL +}).sendAndConfirm(umi); +``` + +### Withdraw SOL + +```typescript +import { withdrawLaunchPoolV2 } from '@metaplex-foundation/genesis'; + +await withdrawLaunchPoolV2(umi, { + genesisAccount, + bucket: launchPoolBucket, + baseMint: baseMint.publicKey, + amountQuoteToken: 3_000_000_000n, // 3 SOL +}).sendAndConfirm(umi); +``` + +### Claim Tokens + +```typescript +import { claimLaunchPoolV2 } from '@metaplex-foundation/genesis'; + +await claimLaunchPoolV2(umi, { + genesisAccount, + bucket: launchPoolBucket, + baseMint: baseMint.publicKey, + recipient: umi.identity.publicKey, +}).sendAndConfirm(umi); +``` + +--- + +## Execute Transition + +After deposit period ends, execute transition to process end behaviors: + +```typescript +import { triggerBehaviorsV2, WRAPPED_SOL_MINT } from '@metaplex-foundation/genesis'; +import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox'; + +const unlockedBucketQuoteTokenAccount = findAssociatedTokenPda(umi, { + owner: unlockedBucket, + mint: WRAPPED_SOL_MINT, +}); + +await triggerBehaviorsV2(umi, { + genesisAccount, + primaryBucket: launchPoolBucket, + baseMint: baseMint.publicKey, +}) + .addRemainingAccounts([ + { pubkey: unlockedBucket, isSigner: false, isWritable: true }, + { pubkey: publicKey(unlockedBucketQuoteTokenAccount), isSigner: false, isWritable: true }, + ]) + .sendAndConfirm(umi); +``` + +--- + +## Revoke Authorities (Post-Launch) + +```typescript +import { revokeV2 } from '@metaplex-foundation/genesis'; +import { publicKey } from '@metaplex-foundation/umi'; + +const TOKEN_PROGRAM_ID = publicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + +// Revoke both mint and freeze authority in one call +await revokeV2(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + baseTokenProgram: TOKEN_PROGRAM_ID, + revokeMintAuthority: true, + revokeFreezeAuthority: true, + padding: Array(5).fill(0), +}).sendAndConfirm(umi); +``` + +⚠️ **Authority revocation is irreversible.** + +--- + +## Fetching State + +```typescript +import { + fetchLaunchPoolBucketV2, + fetchLaunchPoolDepositV2, + findLaunchPoolDepositV2Pda, +} from '@metaplex-foundation/genesis'; + +// Bucket state +const bucket = await fetchLaunchPoolBucketV2(umi, launchPoolBucket); +console.log('Total deposits:', bucket.quoteTokenDepositTotal); +console.log('Token allocation:', bucket.bucket.baseTokenAllocation); + +// User deposit state +const [depositPda] = findLaunchPoolDepositV2Pda(umi, { + bucket: launchPoolBucket, + recipient: umi.identity.publicKey, +}); +const deposit = await fetchLaunchPoolDepositV2(umi, depositPda); +console.log('User deposit:', deposit.amountQuoteToken); +``` + +--- + +## Add Presale Bucket + +Fixed-price allocation: price = quoteCap / allocation. First-come-first-served. + +**Setup for the following examples** (Presale, Bonding Curve, Streamflow): +```typescript +const now = BigInt(Math.floor(Date.now() / 1000)); +const depositStart = now; +const depositEnd = now + 86400n; // 1 day +const claimStart = now + 86400n * 3n; // 3 days +const claimEnd = now + 86400n * 7n; // 7 days +const startTime = now; +const endTime = now + 86400n; +const graduationTime = now + 86400n * 2n; // 2 days +const teamWallet = umi.identity.publicKey; +const lockStart = now; +const lockEnd = now + 86400n * 30n; // 30 days +const cliffDuration = 86400n * 7n; // 7 days +``` + +```typescript +import { + addPresaleBucketV2, + findPresaleBucketV2Pda, + createTimeAbsoluteCondition, +} from '@metaplex-foundation/genesis'; + +const [presaleBucket] = findPresaleBucketV2Pda(umi, { + genesisAccount, + bucketIndex: 0, +}); + +await addPresaleBucketV2(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + baseTokenAllocation: 100_000_000_000_000n, // 10% of supply + allocationQuoteTokenCap: 50_000_000_000n, // 50 SOL cap → price = 0.5 SOL per 1M tokens + minimumDepositAmount: null, // no minimum + depositStartCondition: createTimeAbsoluteCondition(depositStart), + depositEndCondition: createTimeAbsoluteCondition(depositEnd), + claimStartCondition: createTimeAbsoluteCondition(claimStart), + claimEndCondition: createTimeAbsoluteCondition(claimEnd), + endBehaviors: [], +}).sendAndConfirm(umi); +``` + +### Presale User Operations + +```typescript +import { depositPresaleV2, claimPresaleV2 } from '@metaplex-foundation/genesis'; + +// Deposit +await depositPresaleV2(umi, { + genesisAccount, + bucket: presaleBucket, + baseMint: baseMint.publicKey, + amountQuoteToken: 5_000_000_000n, // 5 SOL +}).sendAndConfirm(umi); + +// Claim tokens (after claim period starts) +await claimPresaleV2(umi, { + genesisAccount, + bucket: presaleBucket, + baseMint: baseMint.publicKey, +}).sendAndConfirm(umi); +``` + +--- + +## Add Bonding Curve Bucket + +Constant-product AMM with virtual reserves. Users can swap SOL for tokens and vice versa. + +```typescript +import { + addConstantProductBondingCurveBucketV2, + findBondingCurveBucketV2Pda, + createTimeAbsoluteCondition, +} from '@metaplex-foundation/genesis'; + +const [bondingCurveBucket] = findBondingCurveBucketV2Pda(umi, { + genesisAccount, + bucketIndex: 0, +}); + +await addConstantProductBondingCurveBucketV2(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + baseTokenAllocation: 300_000_000_000_000n, + paused: false, + swapStartCondition: createTimeAbsoluteCondition(startTime), + swapEndCondition: createTimeAbsoluteCondition(endTime), + virtualSol: 30_000_000_000n, // 30 SOL virtual reserve + virtualTokens: 300_000_000_000n, // Virtual token reserve + endBehaviors: [], +}).sendAndConfirm(umi); +``` + +### Bonding Curve Swaps + +```typescript +import { swapBondingCurveV2 } from '@metaplex-foundation/genesis'; +import { SwapDirection } from '@metaplex-foundation/genesis'; + +// Buy tokens with SOL +await swapBondingCurveV2(umi, { + genesisAccount, + bucket: bondingCurveBucket, + baseMint: baseMint.publicKey, + amount: 1_000_000_000n, // 1 SOL + minAmountOut: 0n, // Set slippage tolerance + swapDirection: SwapDirection.Buy, +}).sendAndConfirm(umi); + +// Sell tokens for SOL +await swapBondingCurveV2(umi, { + genesisAccount, + bucket: bondingCurveBucket, + baseMint: baseMint.publicKey, + amount: 100_000_000_000n, // 100 tokens + minAmountOut: 0n, + swapDirection: SwapDirection.Sell, +}).sendAndConfirm(umi); +``` + +--- + +## Raydium CPMM Graduation + +Graduate a launch pool to a Raydium CPMM liquidity pool. + +```typescript +import { + addRaydiumCpmmBucketV2, + graduateToRaydiumCpmmV2, + findRaydiumCpmmBucketV2Pda, + deriveRaydiumPDAsV2, + createNeverClaimSchedule, + createTimeAbsoluteCondition, +} from '@metaplex-foundation/genesis'; + +// Step 1: Add Raydium CPMM bucket +const [raydiumBucket] = findRaydiumCpmmBucketV2Pda(umi, { + genesisAccount, + bucketIndex: 0, +}); + +await addRaydiumCpmmBucketV2(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + baseTokenAllocation: 200_000_000_000_000n, + startCondition: createTimeAbsoluteCondition(graduationTime), + lpLockSchedule: createNeverClaimSchedule(), // Lock LP tokens forever + endBehaviors: [], +}).sendAndConfirm(umi); + +// Step 2: Graduate (after deposit period + transition) +const raydiumAccounts = deriveRaydiumPDAsV2(umi, baseMint.publicKey, { + env: 'devnet', // or 'mainnet' +}); + +await graduateToRaydiumCpmmV2(umi, { + genesisAccount, + bucket: raydiumBucket, + baseMint: baseMint.publicKey, + ...raydiumAccounts, +}).sendAndConfirm(umi); +``` + +--- + +## Streamflow Vesting (Low-Level) + +Lock tokens in a Streamflow vesting stream. The low-level Streamflow instruction requires a `StreamflowConfigArgs` with many fields — for most use cases, prefer the **Launch API's `lockedAllocations`** which handles this automatically. + +```typescript +import { + addStreamflowBucketV2, + findStreamflowBucketV2Pda, + createTimeAbsoluteCondition, +} from '@metaplex-foundation/genesis'; + +const [streamflowBucket] = findStreamflowBucketV2Pda(umi, { + genesisAccount, + bucketIndex: 0, +}); + +await addStreamflowBucketV2(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + baseTokenAllocation: 100_000_000_000_000n, + recipient: teamWallet, + lockStartCondition: createTimeAbsoluteCondition(lockStart), + lockEndCondition: createTimeAbsoluteCondition(lockEnd), + config: { + startTime: lockStart, + period: 2_592_000n, // Monthly (30 days in seconds) + amountPerPeriod: 8_333_333_000_000n, // Tokens released per period + cliff: cliffDuration, // Cliff duration in seconds + cliffAmount: 10_000_000_000_000n, // Tokens unlocked at cliff + streamName: new Uint8Array(64), // UTF-8 encoded stream name (padded to 64 bytes) + withdrawFrequency: 2_592_000n, // How often recipient can withdraw + cancelableBySender: false, + cancelableByRecipient: false, + automaticWithdrawal: false, + transferableBySender: false, + transferableByRecipient: false, + canTopup: false, + pausable: false, + canUpdateRate: false, + }, +}).sendAndConfirm(umi); +``` + +> **Tip**: For team vesting, the Launch API's `lockedAllocations` is much simpler — it converts high-level parameters (duration, unlock schedule, cliff) into the `StreamflowConfigArgs` automatically. + +--- + +## Allowlist (Whitelist) + +Restrict deposits to a merkle-tree allowlist. + +```typescript +import { prepareAllowlist } from '@metaplex-foundation/genesis'; +import { publicKey } from '@metaplex-foundation/umi'; + +const allowlistMembers = [ + { address: publicKey('Addr111...') }, + { address: publicKey('Addr222...') }, + { address: publicKey('Addr333...') }, +]; + +const { root, proofs, treeHeight } = prepareAllowlist(allowlistMembers); + +// Pass allowlist config when adding a presale or launch pool bucket: +await addPresaleBucketV2(umi, { + // ...other params + allowlist: { + merkleTreeHeight: treeHeight, + merkleRoot: Array.from(root), + endTime: allowlistEndTimestamp, // When allowlist restriction expires (open to all after) + quoteCap: 0n, // Per-address SOL cap (0 = no per-address cap) + }, +}).sendAndConfirm(umi); + +// When depositing, provide the user's merkle proof: +await depositPresaleV2(umi, { + // ...other params +}).addRemainingAccounts( + proofs[userIndex].map((proof) => ({ + pubkey: publicKey(proof), + isSigner: false, + isWritable: false, + })) +).sendAndConfirm(umi); +``` + +--- + +## Helper Utilities + +### Bonding Curve Helpers + +```typescript +import { + getSwapResult, + getSwapAmountOutForIn, + getSwapAmountInForOut, + getCurrentPrice, + fetchBondingCurveBucketV1, +} from '@metaplex-foundation/genesis'; +import { SwapDirection } from '@metaplex-foundation/genesis'; + +const bucket = await fetchBondingCurveBucketV1(umi, bondingCurveBucket); + +// Get swap result including fees +const result = getSwapResult(bucket, 1_000_000_000n, SwapDirection.Buy); +console.log('Amount in (incl. fee):', result.amountIn); +console.log('Fee:', result.fee); +console.log('Amount out:', result.amountOut); + +// Get output amount (without fees) +const tokensOut = getSwapAmountOutForIn(bucket, 1_000_000_000n, SwapDirection.Buy); + +// Get required input for desired output (without fees) +const solNeeded = getSwapAmountInForOut(bucket, 100_000_000_000n, SwapDirection.Buy); + +// Get current token price +const price = getCurrentPrice(bucket); +``` + +### Schedule Helpers + +```typescript +import { + createClaimSchedule, + createNeverClaimSchedule, + createLinearBpsScheduleV2WithAbsoluteStart, + createLinearBpsScheduleV2WithRelativeStart, +} from '@metaplex-foundation/genesis'; + +// Vesting schedule with cliff +const schedule = createClaimSchedule({ + startTime: BigInt(startTimestamp), + endTime: BigInt(endTimestamp), + cliffTime: BigInt(cliffTimestamp), + cliffAmountBps: 1000, // 10% at cliff + period: 2_592_000n, // Monthly release (30 days) +}); + +// Permanently locked (e.g., LP tokens) +const lockedForever = createNeverClaimSchedule(); + +// Linear schedule with absolute start (e.g., for deposit penalties) +const penalty = createLinearBpsScheduleV2WithAbsoluteStart({ + startTime: BigInt(penaltyStart), + duration: 86400n * 7n, // 7 days + point1: { timeBps: 0n, bps: 500n }, // 5% at start + point2: { timeBps: 10000n, bps: 0n }, // 0% at end + maxBps: 500, +}); +``` + +### Condition Helpers + +```typescript +import { + createTimeAbsoluteCondition, + createTimeRelativeCondition, + createNeverCondition, + isConditionArgs, +} from '@metaplex-foundation/genesis'; +import { BucketTimes } from '@metaplex-foundation/genesis'; + +// Trigger at specific Unix timestamp +const condition = createTimeAbsoluteCondition(BigInt(unixTimestamp)); + +// Trigger relative to another bucket's deposit end time (+ optional offset) +const relativeCondition = createTimeRelativeCondition( + launchPoolBucket, // reference bucket + BucketTimes.DepositEnd, // relative to its deposit end time + 60n, // 60 seconds after (optional, default 0) +); + +// BucketTimes values: DepositStart, DepositEnd, ClaimStart, ClaimEnd, +// SwapStart, SwapEnd, LockStart, LockEnd, GraduateStart, Graduate + +// Never triggers (permanently locked) +const never = createNeverCondition(); + +// Type guard +if (isConditionArgs('TimeAbsolute', condition)) { + console.log('Triggers at:', condition.time); +} +``` + +### Fee Calculation + +```typescript +import { + calculateFee, + DEFAULT_LAUNCHPOOL_DEPOSIT_FEE, // 200n (2% in bps) + DEFAULT_LAUNCHPOOL_WITHDRAW_FEE, // 200n + DEFAULT_PRESALE_DEPOSIT_FEE, // 200n + DEFAULT_PRESALE_WITHDRAW_FEE, // 200n + DEFAULT_BONDING_CURVE_DEPOSIT_FEE, // 200n + DEFAULT_BONDING_CURVE_WITHDRAW_FEE, // 200n + DEFAULT_UNLOCKED_CLAIM_FEE, // 500n (5% in bps) +} from '@metaplex-foundation/genesis'; +import { FeeDiscriminants } from '@metaplex-foundation/genesis'; + +const fee = calculateFee( + 10_000_000_000n, // 10 SOL deposit + FeeDiscriminants.BasisPoints, + 200n, // 2% +); +// fee = 200_000_000n (0.2 SOL) +``` + +### Raydium PDA Derivation + +```typescript +import { deriveRaydiumPDAsV2 } from '@metaplex-foundation/genesis'; + +// Returns all accounts needed for Raydium graduation +const raydiumAccounts = deriveRaydiumPDAsV2(umi, baseMint.publicKey, { + env: 'mainnet', // or 'devnet' +}); + +// raydiumAccounts contains: +// poolState, poolAuthority, lpMint, baseVault, quoteVault, +// observationState, ammConfig, raydiumProgram, createPoolFee, +// token0Mint, token1Mint, isProjectMintToken0, raydiumSigner, permission +``` + +--- + +## Key Constants + +```typescript +import { + WRAPPED_SOL_MINT, // So11111111111111111111111111111111111111112 + SPL_TOKEN_2022_PROGRAM_ID, // TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + FEE_WALLET, // 9kFjQsxtpBsaw8s7aUyiY3wazYDNgFP4Lj5rsBVVF8tb + BACKEND_SIGNER, // BESN8h2HKyvjzksY2Ka86eLPdjraNBW1jheqHGSw7NZn +} from '@metaplex-foundation/genesis'; +``` + +--- + +## Fees + +Genesis charges protocol-level fees on deposits and withdrawals. Default fees: + +| Operation | Fee Type | Default | +|-----------|----------|---------| +| Launch Pool deposit/withdraw | BasisPoints | 200 (2%) | +| Presale deposit/withdraw | BasisPoints | 200 (2%) | +| Bonding Curve deposit/withdraw | BasisPoints | 200 (2%) | +| Vault deposit/withdraw | BasisPoints | 200 (2%) | +| Unlocked claim | BasisPoints | 500 (5%) | +| Auction bid | Flat | 1,000,000 (0.001 SOL) | + +For current rates, see: https://developers.metaplex.com/protocol-fees + +--- + +## Program ID + +``` +Genesis: GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B +``` + +## Documentation + +Full documentation: https://developers.metaplex.com/genesis diff --git a/.agents/skills/metaplex/references/sdk-token-metadata-kit.md b/.agents/skills/metaplex/references/sdk-token-metadata-kit.md new file mode 100644 index 0000000..f80e826 --- /dev/null +++ b/.agents/skills/metaplex/references/sdk-token-metadata-kit.md @@ -0,0 +1,263 @@ +# Token Metadata Kit SDK Reference + +Native @solana/kit integration for Token Metadata - direct SDK control with minimal dependencies. + +## Package + +```bash +npm install @metaplex-foundation/mpl-token-metadata-kit @solana/kit +``` + +## When to Use Kit vs Umi + +| Use Kit When | Use Umi When | +|--------------|--------------| +| Integrating into existing @solana/kit codebase | Rapid development, multiple Metaplex programs | +| Minimal dependencies needed | Want transaction builder patterns | +| Direct SDK control preferred | Using Umi plugins (uploaders, etc.) | + +--- + +## Basic Setup + +```typescript +import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit'; +import { generateKeyPairSigner, createKeyPairSignerFromBytes } from '@solana/signers'; +import fs from 'fs'; + +const rpc = createSolanaRpc('https://api.devnet.solana.com'); +const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com'); + +// Generate a new random keypair +const newKeypair = await generateKeyPairSigner(); + +// Or load from file (Node.js scripts) +const secretKey = new Uint8Array(JSON.parse(fs.readFileSync('/path/to/keypair.json', 'utf-8'))); +const authority = await createKeyPairSignerFromBytes(secretKey); +``` + +--- + +## Creating NFTs + +### Convenience Helpers (Recommended) + +```typescript +import { createNft, createProgrammableNft, createFungible } from '@metaplex-foundation/mpl-token-metadata-kit'; +import { generateKeyPairSigner } from '@solana/signers'; + +const mint = await generateKeyPairSigner(); +const authority = await generateKeyPairSigner(); + +// Regular NFT — returns [createIx, mintIx] +const [createIx, mintIx] = await createNft({ + mint, + authority, + payer: authority, + name: 'My NFT', + uri: 'https://example.com/nft.json', + sellerFeeBasisPoints: 550, // 5.5% +}); + +// pNFT — returns [createIx, mintIx] +const [createIx, mintIx] = await createProgrammableNft({ + mint, + authority, + payer: authority, + name: 'My pNFT', + uri: 'https://example.com/pnft.json', + sellerFeeBasisPoints: 500, +}); + +// Fungible — returns createIx only +const createIx = await createFungible({ + mint, + payer: authority, + name: 'My Token', + symbol: 'MTK', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: 0, + decimals: 9, +}); +``` + +Also available: `createFungibleAsset()` for semi-fungible tokens. + +### Low-Level: Async Version (Auto-resolves PDAs) + +```typescript +import { getCreateV1InstructionAsync, TokenStandard } from '@metaplex-foundation/mpl-token-metadata-kit'; + +const createIx = await getCreateV1InstructionAsync({ + mint, + authority, + payer: authority, + name: 'My NFT', + uri: 'https://example.com/nft.json', + sellerFeeBasisPoints: 550, // 5.5% + tokenStandard: TokenStandard.NonFungible, +}); +``` + +### Low-Level: Sync Version (Provide all addresses) + +```typescript +import { getCreateV1Instruction, findMetadataPda, findMasterEditionPda } from '@metaplex-foundation/mpl-token-metadata-kit'; + +const metadataPda = await findMetadataPda({ mint: mint.address }); +const editionPda = await findMasterEditionPda({ mint: mint.address }); + +const createIx = getCreateV1Instruction({ + metadata: metadataPda, + masterEdition: editionPda, + mint, + authority, + payer: authority, + name: 'My NFT', + uri: 'https://example.com/nft.json', + sellerFeeBasisPoints: 550, + tokenStandard: TokenStandard.NonFungible, +}); +``` + +--- + +## Transfers + +```typescript +import { getTransferV1InstructionAsync, TokenStandard } from '@metaplex-foundation/mpl-token-metadata-kit'; + +// Regular NFT +const transferIx = await getTransferV1InstructionAsync({ + mint: mintAddress, + authority: owner, + payer: owner, + tokenOwner: owner.address, + destinationOwner: recipientAddress, + tokenStandard: TokenStandard.NonFungible, +}); + +// pNFT (handles TokenRecord automatically) +const transferIx = await getTransferV1InstructionAsync({ + mint: mintAddress, + authority: owner, + payer: owner, + tokenOwner: owner.address, + destinationOwner: recipientAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}); +``` + +> **Fungible token transfers** are not handled by Token Metadata — use SPL Token's `transfer` instruction from `@solana-program/token` or the Umi SDK's `transferTokens` from `mpl-toolbox`. + +--- + +## Transaction Flow + +```typescript +import { pipe } from '@solana/functional'; +import { + createTransactionMessage, + setTransactionMessageFeePayer, + setTransactionMessageLifetimeUsingBlockhash, + appendTransactionMessageInstructions, +} from '@solana/transaction-messages'; +import { compileTransaction, signTransaction, assertIsTransactionWithBlockhashLifetime } from '@solana/transactions'; +import { sendAndConfirmTransactionFactory } from '@solana/kit'; + +// Create send function +const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + +// Get blockhash +const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); + +// Build transaction +const transactionMessage = pipe( + createTransactionMessage({ version: 0 }), + (tx) => setTransactionMessageFeePayer(authority.address, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), + (tx) => appendTransactionMessageInstructions([createIx, mintIx], tx), +); + +// Compile, sign, and send (both mint and authority must sign for NFT creation) +const transaction = compileTransaction(transactionMessage); +assertIsTransactionWithBlockhashLifetime(transaction); +const signedTx = await signTransaction([mint.keyPair, authority.keyPair], transaction); +await sendAndConfirm(signedTx, { commitment: 'confirmed' }); +``` + +--- + +## PDAs + +```typescript +import { findMetadataPda, findMasterEditionPda, findTokenRecordPda } from '@metaplex-foundation/mpl-token-metadata-kit'; + +// PDA derivation (async — returns Promise) +const metadataPda = await findMetadataPda({ mint: mintAddress }); +const editionPda = await findMasterEditionPda({ mint: mintAddress }); +const tokenRecordPda = await findTokenRecordPda({ mint: mintAddress, token: tokenAddress }); +``` + +Note: Kit PDAs are async (unlike Umi's sync PDAs). The async instruction variants (`getCreateV1InstructionAsync`, etc.) resolve PDAs automatically. + +--- + +## Fetching Metadata + +```typescript +import { fetchMetadata, fetchMasterEdition } from '@metaplex-foundation/mpl-token-metadata-kit'; + +const metadata = await fetchMetadata(rpc, metadataPda); +console.log(metadata.data.name, metadata.data.uri); + +const edition = await fetchMasterEdition(rpc, editionPda); +console.log(edition.data.supply, edition.data.maxSupply); +``` + +--- + +## Fetching Digital Assets + +```typescript +import { fetchDigitalAsset } from '@metaplex-foundation/mpl-token-metadata-kit'; + +// Fetches mint, metadata, and edition in one call +const asset = await fetchDigitalAsset(rpc, mintAddress); +console.log(asset.metadata.name); // Metadata account +console.log(asset.mint); // Mint account +console.log(asset.edition); // MasterEdition or Edition (if NFT) +``` + +Also available: `fetchDigitalAssetByMetadata()`, `fetchAllDigitalAsset()`. + +--- + +## Key Differences from Umi + +| Aspect | Kit Client | Umi Client | +|--------|------------|------------| +| Setup | RPC + RpcSubscriptions | Umi context | +| Instructions | `getCreateV1InstructionAsync()` | `createV1(umi, {})` | +| Signers | `generateKeyPairSigner()` | `generateSigner(umi)` | +| Transactions | Manual with Kit utilities | `.sendAndConfirm(umi)` | +| PDA Resolution | Async functions | Built into instruction | +| Dependencies | Only @solana/* packages | Umi + plugins | + +--- + +## Interop with Umi + +If you need to mix Kit and Umi: + +```typescript +import { fromKitAddress, toKitAddress, toKitInstruction, fromKitInstruction } from '@metaplex-foundation/umi-kit-adapters'; + +// Convert addresses +const umiPublicKey = fromKitAddress(kitAddress); +const kitAddress = toKitAddress(umiPublicKey); + +// Convert instructions +const kitIx = toKitInstruction(umiInstruction); +const umiIx = fromKitInstruction(kitInstruction); +``` diff --git a/.agents/skills/metaplex/references/sdk-token-metadata.md b/.agents/skills/metaplex/references/sdk-token-metadata.md new file mode 100644 index 0000000..0014859 --- /dev/null +++ b/.agents/skills/metaplex/references/sdk-token-metadata.md @@ -0,0 +1,459 @@ +# Token Metadata SDK Reference (Umi) + +Umi SDK operations for creating and managing Token Metadata NFTs, pNFTs, and fungible tokens. + +> **Prerequisites**: Set up Umi first — see `./sdk-umi.md` for installation and basic setup. +> **Docs**: https://developers.metaplex.com/token-metadata + +--- + +## Create Fungible Token + +```typescript +import { createFungible } from '@metaplex-foundation/mpl-token-metadata'; +import { generateSigner, percentAmount } from '@metaplex-foundation/umi'; + +const mint = generateSigner(umi); + +await createFungible(umi, { + mint, + name: 'My Token', + symbol: 'MTK', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: percentAmount(0), + decimals: 9, +}).sendAndConfirm(umi); +``` + +## Create NFT + +```typescript +import { createNft } from '@metaplex-foundation/mpl-token-metadata'; +import { generateSigner, percentAmount } from '@metaplex-foundation/umi'; + +const mint = generateSigner(umi); + +await createNft(umi, { + mint, + name: 'My NFT', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: percentAmount(5.5), + creators: [{ address: umi.identity.publicKey, share: 100, verified: false }], +}).sendAndConfirm(umi); +``` + +## Create pNFT (Programmable) + +```typescript +import { createProgrammableNft } from '@metaplex-foundation/mpl-token-metadata'; +import { generateSigner, percentAmount } from '@metaplex-foundation/umi'; + +const mint = generateSigner(umi); + +await createProgrammableNft(umi, { + mint, + name: 'My pNFT', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: percentAmount(5), + creators: [{ address: umi.identity.publicKey, share: 100, verified: false }], +}).sendAndConfirm(umi); +``` + +pNFTs enforce royalties at the protocol level — the Token Metadata program controls all transfers and enforces `sellerFeeBasisPoints`. No additional plugin is needed (unlike Core's `Royalties` plugin). + +## Create Collection NFT (Token Metadata) + +In Token Metadata, a collection is itself an NFT. Use `isCollection: true`: + +```typescript +import { createNft } from '@metaplex-foundation/mpl-token-metadata'; +import { generateSigner, percentAmount } from '@metaplex-foundation/umi'; + +const collectionMint = generateSigner(umi); + +await createNft(umi, { + mint: collectionMint, + name: 'My Collection', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: percentAmount(5), + isCollection: true, +}).sendAndConfirm(umi); +``` + +## Create NFT/pNFT in a Collection + +Pass the `collection` field when creating. Items start **unverified** — call `verifyCollectionV1` afterward: + +```typescript +import { createProgrammableNft, verifyCollectionV1, findMetadataPda } from '@metaplex-foundation/mpl-token-metadata'; +import { generateSigner, percentAmount } from '@metaplex-foundation/umi'; + +const mint = generateSigner(umi); + +await createProgrammableNft(umi, { + mint, + name: 'My pNFT #1', + uri: 'https://arweave.net/xxx', + sellerFeeBasisPoints: percentAmount(5), + creators: [{ address: umi.identity.publicKey, share: 100, verified: false }], + collection: { key: collectionMint.publicKey, verified: false }, +}).sendAndConfirm(umi); + +// Then verify (requires collection update authority) +await verifyCollectionV1(umi, { + metadata: findMetadataPda(umi, { mint: mint.publicKey }), + collectionMint: collectionMint.publicKey, + authority: umi.identity, +}).sendAndConfirm(umi); +``` + +## Update Metadata + +```typescript +import { updateV1, fetchDigitalAsset, TokenStandard } from '@metaplex-foundation/mpl-token-metadata'; + +const da = await fetchDigitalAsset(umi, mintAddress); + +await updateV1(umi, { + mint: mintAddress, + authority: updateAuthority, + data: { + ...da.metadata, // spread existing fields + name: 'Updated Name', // override what you want + }, + primarySaleHappened: true, +}).sendAndConfirm(umi); +``` + +To update URI only: + +```typescript +await updateV1(umi, { + mint: mintAddress, + data: { ...da.metadata, uri: 'https://arweave.net/new-uri' }, +}).sendAndConfirm(umi); +``` + +## Burn + +```typescript +import { burnV1, TokenStandard } from '@metaplex-foundation/mpl-token-metadata'; + +// NFT +await burnV1(umi, { + mint: mintAddress, + authority: owner, + tokenOwner: owner.publicKey, + tokenStandard: TokenStandard.NonFungible, +}).sendAndConfirm(umi); + +// pNFT +await burnV1(umi, { + mint: mintAddress, + authority: owner, + tokenOwner: owner.publicKey, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); + +// Fungible (partial burn) +await burnV1(umi, { + mint: mintAddress, + authority: owner, + tokenOwner: owner.publicKey, + tokenStandard: TokenStandard.Fungible, + amount: 1000n, +}).sendAndConfirm(umi); +``` + +## Lock / Unlock (pNFTs) + +pNFTs can be locked by a delegate to prevent transfers. Used for staking, escrowless listings, etc. + +```typescript +import { lockV1, unlockV1, TokenStandard } from '@metaplex-foundation/mpl-token-metadata'; + +// Lock (requires Utility, Staking, or Standard delegate) +await lockV1(umi, { + mint: mintAddress, + authority: delegateSigner, + tokenOwner: ownerAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); + +// Unlock +await unlockV1(umi, { + mint: mintAddress, + authority: delegateSigner, + tokenOwner: ownerAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); +``` + +## Transfer NFT + +```typescript +import { transferV1, TokenStandard } from '@metaplex-foundation/mpl-token-metadata'; + +// Regular NFT +await transferV1(umi, { + mint: mintAddress, + authority: owner, + tokenOwner: owner.publicKey, + destinationOwner: recipientAddress, + tokenStandard: TokenStandard.NonFungible, +}).sendAndConfirm(umi); + +// pNFT (handles TokenRecord automatically) +await transferV1(umi, { + mint: mintAddress, + authority: owner, + tokenOwner: owner.publicKey, + destinationOwner: recipientAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); +``` + +## Fetch + +```typescript +import { + fetchDigitalAsset, + fetchDigitalAssetWithAssociatedToken, + fetchAllDigitalAssetByOwner, + fetchAllDigitalAssetByCreator, + fetchAllDigitalAssetByVerifiedCollection, +} from '@metaplex-foundation/mpl-token-metadata'; + +// Single asset (metadata + mint + edition) +const da = await fetchDigitalAsset(umi, mintAddress); +// da.metadata, da.mint, da.edition + +// Asset with token account +const daWithToken = await fetchDigitalAssetWithAssociatedToken(umi, mintAddress, ownerAddress); +// daWithToken.token, daWithToken.tokenRecord (for pNFTs) + +// By owner +const owned = await fetchAllDigitalAssetByOwner(umi, ownerAddress); + +// By creator (first verified creator) +const created = await fetchAllDigitalAssetByCreator(umi, creatorAddress); + +// By verified collection +const inCollection = await fetchAllDigitalAssetByVerifiedCollection(umi, collectionMintAddress); +``` + +## Delegates + +Token Metadata supports multiple delegate types for pNFTs. Each delegate type grants specific permissions: + +- **Standard** — Basic approval. Grants no transfer/burn/lock rights, but the delegate address is recorded on-chain. Used for custom program integrations that check delegate status. +- **Transfer** — Can transfer the NFT on behalf of the owner. +- **Sale** — Can transfer + lock. Designed for marketplace listings (lock prevents owner from moving the NFT while listed). +- **Utility** — Can burn + lock. Used for staking-like flows where the asset may be consumed. +- **Staking** — Can lock only. Prevents transfers while staked, but cannot burn or transfer. +- **LockedTransfer** — Can transfer + lock, but only to a **specific pre-set address** (passed as `lockedAddress` when delegating). Used for escrow flows. + +### Approve Delegate + +```typescript +import { + delegateStandardV1, + delegateTransferV1, + delegateSaleV1, + delegateUtilityV1, + delegateStakingV1, + delegateLockedTransferV1, + TokenStandard, +} from '@metaplex-foundation/mpl-token-metadata'; + +// Standard delegate (basic approval, no special permissions) +await delegateStandardV1(umi, { + mint: mintAddress, + tokenOwner: ownerAddress, + authority: owner, + delegate: delegateAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); + +// Transfer delegate (can transfer) +await delegateTransferV1(umi, { + mint: mintAddress, + tokenOwner: ownerAddress, + authority: owner, + delegate: delegateAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); + +// Sale delegate (can transfer + lock — for marketplace listings) +await delegateSaleV1(umi, { + mint: mintAddress, + tokenOwner: ownerAddress, + authority: owner, + delegate: delegateAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); + +// Utility delegate (can burn + lock) +await delegateUtilityV1(umi, { + mint: mintAddress, + tokenOwner: ownerAddress, + authority: owner, + delegate: delegateAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); + +// Staking delegate (can lock) +await delegateStakingV1(umi, { + mint: mintAddress, + tokenOwner: ownerAddress, + authority: owner, + delegate: delegateAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); + +// Locked transfer delegate (transfer to specific address only) +await delegateLockedTransferV1(umi, { + mint: mintAddress, + tokenOwner: ownerAddress, + authority: owner, + delegate: delegateAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, + lockedAddress: destinationAddress, +}).sendAndConfirm(umi); +``` + +### Revoke Delegate + +Each delegate type has a matching revoke function: + +```typescript +import { + revokeStandardV1, + revokeTransferV1, + revokeSaleV1, + revokeUtilityV1, + revokeStakingV1, + revokeLockedTransferV1, + TokenStandard, +} from '@metaplex-foundation/mpl-token-metadata'; + +await revokeTransferV1(umi, { + mint: mintAddress, + tokenOwner: ownerAddress, + authority: owner, + delegate: delegateAddress, + tokenStandard: TokenStandard.ProgrammableNonFungible, +}).sendAndConfirm(umi); +``` + +## Print Editions + +Create numbered prints from a Master Edition NFT: + +```typescript +import { printV1, TokenStandard } from '@metaplex-foundation/mpl-token-metadata'; +import { generateSigner } from '@metaplex-foundation/umi'; + +const editionMint = generateSigner(umi); + +await printV1(umi, { + masterTokenAccountOwner: masterOwner, + masterEditionMint: masterMintAddress, + editionMint, + editionTokenAccountOwner: recipientAddress, + editionNumber: 1, + tokenStandard: TokenStandard.NonFungible, +}).sendAndConfirm(umi); +``` + +## Verify / Unverify + +### Creator Verification + +```typescript +import { verifyCreatorV1, unverifyCreatorV1, findMetadataPda } from '@metaplex-foundation/mpl-token-metadata'; + +// Verify (signer must be the creator being verified) +await verifyCreatorV1(umi, { + metadata: findMetadataPda(umi, { mint: mintAddress }), + authority: creatorSigner, +}).sendAndConfirm(umi); + +// Unverify +await unverifyCreatorV1(umi, { + metadata: findMetadataPda(umi, { mint: mintAddress }), + authority: creatorSigner, +}).sendAndConfirm(umi); +``` + +### Collection Verification + +```typescript +import { verifyCollectionV1, unverifyCollectionV1, findMetadataPda } from '@metaplex-foundation/mpl-token-metadata'; + +// Verify (signer must be collection update authority) +await verifyCollectionV1(umi, { + metadata: findMetadataPda(umi, { mint: assetMintAddress }), + collectionMint: collectionMintAddress, + authority: collectionAuthority, +}).sendAndConfirm(umi); + +// Unverify +await unverifyCollectionV1(umi, { + metadata: findMetadataPda(umi, { mint: assetMintAddress }), + collectionMint: collectionMintAddress, + authority: collectionAuthority, +}).sendAndConfirm(umi); +``` + +--- + +## Mint Fungible Tokens + +After creating a fungible token with `createFungible`, mint supply using `mintTokensTo` from `mpl-toolbox`: + +```typescript +import { mintTokensTo, findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox'; + +const tokenAccount = findAssociatedTokenPda(umi, { mint: mint.publicKey, owner: umi.identity.publicKey }); + +await mintTokensTo(umi, { + mint: mint.publicKey, + token: tokenAccount, + amount: 1_000_000_000n, // In base units (this = 1 token with 9 decimals) +}).sendAndConfirm(umi); +``` + +## Transfer Fungible Token + +```typescript +import { transferTokens } from '@metaplex-foundation/mpl-toolbox'; + +// Use mpl-toolbox for fungible transfers, NOT Token Metadata +await transferTokens(umi, { + source: sourceTokenAccount, + destination: destinationTokenAccount, + amount: 1000000000n, +}).sendAndConfirm(umi); +``` + +--- + +## PDAs + +```typescript +import { + findMetadataPda, + findMasterEditionPda, + findTokenRecordPda +} from '@metaplex-foundation/mpl-token-metadata'; + +// Metadata PDA +const [metadataPda] = findMetadataPda(umi, { mint }); + +// Master Edition PDA +const [editionPda] = findMasterEditionPda(umi, { mint }); + +// Token Record PDA (pNFTs) +const [tokenRecordPda] = findTokenRecordPda(umi, { mint, token }); +``` diff --git a/.agents/skills/metaplex/references/sdk-umi.md b/.agents/skills/metaplex/references/sdk-umi.md new file mode 100644 index 0000000..e48a403 --- /dev/null +++ b/.agents/skills/metaplex/references/sdk-umi.md @@ -0,0 +1,181 @@ +# Metaplex Umi SDK Reference + +Umi is Metaplex's modular JavaScript framework for Solana program clients. + +## Packages + +| Package | Purpose | +|---------|---------| +| `@metaplex-foundation/umi-bundle-defaults` | Base Umi setup | +| `@metaplex-foundation/mpl-token-metadata` | Token Metadata (NFTs, fungibles) | +| `@metaplex-foundation/mpl-core` | Core NFT standard | +| `@metaplex-foundation/mpl-bubblegum` | Compressed NFTs (Bubblegum) | +| `@metaplex-foundation/umi-uploader-irys` | Upload to Arweave via Irys | +| `@metaplex-foundation/umi-signer-wallet-adapters` | Wallet adapter integration | +| `@metaplex-foundation/mpl-toolbox` | SPL token helpers (transfers, compute budget) | +| `@metaplex-foundation/digital-asset-standard-api` | DAS API (asset queries) | + +## Installation + +```bash +npm install @metaplex-foundation/umi-bundle-defaults \ + @metaplex-foundation/mpl-core \ + @metaplex-foundation/mpl-token-metadata \ + @metaplex-foundation/mpl-toolbox \ + @metaplex-foundation/digital-asset-standard-api +# Optional — add if needed: +# @metaplex-foundation/umi-uploader-irys (for uploading files) +# @metaplex-foundation/umi-signer-wallet-adapters (for browser wallet integration) +``` + +--- + +## Basic Setup + +### Browser / Wallet Adapter + +```typescript +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { mplCore } from '@metaplex-foundation/mpl-core'; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; +import { walletAdapterIdentity } from '@metaplex-foundation/umi-signer-wallet-adapters'; +import { irysUploader } from '@metaplex-foundation/umi-uploader-irys'; + +const umi = createUmi('https://api.devnet.solana.com') + .use(mplCore()) + .use(mplTokenMetadata()) + .use(walletAdapterIdentity(wallet)) + .use(irysUploader()); +``` + +### Node.js / Scripts (Keypair from File) + +```typescript +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { keypairIdentity } from '@metaplex-foundation/umi'; +import { mplCore } from '@metaplex-foundation/mpl-core'; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; +import fs from 'fs'; + +// Create Umi first, then load keypair using its eddsa helper +const umi = createUmi('https://api.devnet.solana.com') + .use(mplCore()) + .use(mplTokenMetadata()); + +const secretKey = JSON.parse(fs.readFileSync('/path/to/keypair.json', 'utf-8')); +const keypair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(secretKey)); +umi.use(keypairIdentity(keypair)); +``` + +--- + +## Program-Specific SDK Guides + +| Program | Detail File | +|---------|-------------| +| Core NFTs | `./sdk-core.md` | +| Token Metadata | `./sdk-token-metadata.md` | +| Bubblegum (compressed NFTs) | `./sdk-bubblegum.md` | +| Genesis (token launches) | `./sdk-genesis.md` | +| Token Metadata with Kit | `./sdk-token-metadata-kit.md` | + +--- + +## Transaction Patterns + +### Chaining Instructions + +```typescript +await createV1(umi, { ...args }) + .add(anotherInstruction(umi, { ...args })) + .sendAndConfirm(umi); +``` + +### Compute Budget + +```typescript +import { setComputeUnitLimit, setComputeUnitPrice } from '@metaplex-foundation/mpl-toolbox'; + +await createV1(umi, { ...args }) + .prepend(setComputeUnitLimit(umi, { units: 200_000 })) + .prepend(setComputeUnitPrice(umi, { microLamports: 5000 })) + .sendAndConfirm(umi); +``` + +### Safe Fetch (returns null if not found) + +```typescript +import { safeFetchMetadata } from '@metaplex-foundation/mpl-token-metadata'; + +const metadata = await safeFetchMetadata(umi, metadataPda); +if (metadata) { + // exists +} +``` + +Use `safeFetch*` variants when the account may not exist (e.g., checking if a mint has metadata). Regular `fetch*` throws if the account is missing. + +--- + +## Uploading + +```typescript +import { irysUploader } from '@metaplex-foundation/umi-uploader-irys'; + +const umi = createUmi(rpcEndpoint).use(irysUploader()); + +// Upload file +const [imageUri] = await umi.uploader.upload([imageFile]); + +// Upload JSON +const metadataUri = await umi.uploader.uploadJson({ + name: 'My NFT', + description: 'Description', + image: imageUri, + attributes: [{ trait_type: 'Background', value: 'Blue' }], +}); +``` + +--- + +## DAS API (Asset Queries) + +> **Important**: DAS API requires a DAS-compatible RPC provider (e.g., Helius, Triton, QuickNode). The default public Solana RPC does **not** support DAS methods. + +```typescript +import { dasApi } from '@metaplex-foundation/digital-asset-standard-api'; + +const umi = createUmi('https://mainnet.helius-rpc.com/?api-key=YOUR_KEY').use(dasApi()); + +// Single asset by ID +const asset = await umi.rpc.getAsset(assetId); + +// By owner +const assets = await umi.rpc.getAssetsByOwner({ owner: walletAddress }); + +// By collection +const collectionAssets = await umi.rpc.getAssetsByCollection({ + collection: collectionAddress +}); + +// Search +const results = await umi.rpc.searchAssets({ + owner: walletAddress, + burnt: false, +}); +``` + +--- + +## Error Handling + +```typescript +try { + await instruction.sendAndConfirm(umi); +} catch (error) { + if (error.name === 'TokenMetadataError') { + console.error('Token Metadata Error:', error.message); + } + throw error; +} +``` diff --git a/.vscode/settings.json b/.vscode/settings.json index 9872962..be52fea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -92,7 +92,6 @@ "files.exclude": { "**/.git": true, "**/.DS_Store": true, - "**/target": true, "**/node_modules": true, "**/.anchor": true, "**/test-ledger": true, diff --git a/programs/doom-nft-program/src/instructions/mint_doom_index_nft.rs b/programs/doom-nft-program/src/instructions/mint_doom_index_nft.rs index ab295e1..5a0fdb7 100644 --- a/programs/doom-nft-program/src/instructions/mint_doom_index_nft.rs +++ b/programs/doom-nft-program/src/instructions/mint_doom_index_nft.rs @@ -1,5 +1,8 @@ use anchor_lang::prelude::*; -use mpl_core::{instructions::CreateV2CpiBuilder, types::DataState, ID as MPL_CORE_ID}; +use mpl_core::{ + accounts::BaseCollectionV1, instructions::CreateV2CpiBuilder, types::DataState, + ID as MPL_CORE_ID, +}; use crate::{ constants::{COLLECTION_AUTHORITY_SEED, GLOBAL_CONFIG_SEED, RESERVATION_SEED}, @@ -40,12 +43,12 @@ pub struct MintDoomIndexNft<'info> { )] pub collection_update_authority: UncheckedAccount<'info>, - /// CHECK: Existing Core collection account. Address checked against config. + /// Existing Core collection account. #[account( mut, address = global_config.collection @ DoomNftProgramError::CollectionMismatch )] - pub collection: UncheckedAccount<'info>, + pub collection: Account<'info, BaseCollectionV1>, /// CHECK: Verified against the canonical Metaplex Core program id. #[account(address = MPL_CORE_ID)] diff --git a/scripts/devnet/mint.test.ts b/scripts/devnet/mint.test.ts index b4befe2..c10309c 100644 --- a/scripts/devnet/mint.test.ts +++ b/scripts/devnet/mint.test.ts @@ -1,22 +1,22 @@ import assert from "node:assert/strict"; import { describe, test } from "node:test"; -import { assertUrlReachable, decodeAssetUri } from "./mint"; - -describe("decodeAssetUri", () => { - test("returns the URI stored in the on-chain asset data", () => { - const accountData = Buffer.concat([ - Buffer.from([1]), - Buffer.alloc(32, 7), - Buffer.from([2]), - Buffer.alloc(32, 8), - encodeBorshString("DOOM INDEX #1"), - encodeBorshString("https://example.com/base/1.json"), - Buffer.from([0]), - Buffer.from([9, 9, 9]), - ]); +import { assertUrlReachable, fetchAssetUri } from "./mint"; + +describe("fetchAssetUri", () => { + test("returns the URI from the official asset fetcher", async () => { + const metadataUri = await fetchAssetUri("Asset111111111111111111111111111111111111111", async () => { + return "https://example.com/base/1.json"; + }); + + assert.equal(metadataUri, "https://example.com/base/1.json"); + }); - assert.equal(decodeAssetUri(accountData), "https://example.com/base/1.json"); + test("throws when the asset does not contain a URI", async () => { + await assert.rejects( + () => fetchAssetUri("Asset111111111111111111111111111111111111111", async () => ""), + /does not contain a metadata URI/, + ); }); }); @@ -52,10 +52,3 @@ describe("assertUrlReachable", () => { ); }); }); - -function encodeBorshString(value: string): Buffer { - const text = Buffer.from(value, "utf8"); - const length = Buffer.alloc(4); - length.writeUInt32LE(text.length, 0); - return Buffer.concat([length, text]); -} diff --git a/scripts/devnet/mint.ts b/scripts/devnet/mint.ts index 42e7fd5..a2bb846 100644 --- a/scripts/devnet/mint.ts +++ b/scripts/devnet/mint.ts @@ -1,45 +1,24 @@ +import { fetchAsset } from "@metaplex-foundation/mpl-core"; +import { publicKey } from "@metaplex-foundation/umi"; + import { fetchGlobalConfig, getConnection, Keypair, - MPL_CORE_PROGRAM_ID, loadWallet, mintDoomIndexNftInstruction, readJson, sendInstructions, writeJson, } from "./common"; +import { createMetaplexClient } from "../metaplex/common"; type ReservationOutput = { tokenId: string; }; type FetchLike = typeof fetch; - -export function decodeAssetUri(accountData: Buffer | Uint8Array): string { - const data = Buffer.from(accountData); - let offset = 0; - - const key = data.readUInt8(offset); - offset += 1; - if (key !== 1) { - throw new Error(`Expected Metaplex Core AssetV1 account data, got key ${key}`); - } - - offset += 32; - - const updateAuthorityKind = data.readUInt8(offset); - offset += 1; - if (updateAuthorityKind === 1 || updateAuthorityKind === 2) { - offset += 32; - } else if (updateAuthorityKind !== 0) { - throw new Error(`Unknown Metaplex Core update authority kind ${updateAuthorityKind}`); - } - - const [, afterName] = readBorshString(data, offset); - const [uri] = readBorshString(data, afterName); - return uri; -} +type AssetUriFetcher = (assetAddress: string) => Promise; export async function assertUrlReachable(url: string, label: string, fetchImpl: FetchLike = fetch): Promise { const headResponse = await fetchImpl(url, { method: "HEAD" }); @@ -53,6 +32,18 @@ export async function assertUrlReachable(url: string, label: string, fetchImpl: } } +export async function fetchAssetUri( + assetAddress: string, + fetchAssetUriImpl: AssetUriFetcher = createAssetUriFetcher(), +): Promise { + const uri = await fetchAssetUriImpl(assetAddress); + if (!uri) { + throw new Error(`Asset ${assetAddress} does not contain a metadata URI`); + } + + return uri; +} + async function main(): Promise { const connection = getConnection(); const payer = loadWallet(); @@ -72,15 +63,7 @@ async function main(): Promise { ); const signature = await sendInstructions(connection, payer, [mintInstruction], [asset]); - const assetAccount = await connection.getAccountInfo(asset.publicKey, "confirmed"); - if (!assetAccount) { - throw new Error(`Asset account not found at ${asset.publicKey.toBase58()}`); - } - if (!assetAccount.owner.equals(MPL_CORE_PROGRAM_ID)) { - throw new Error(`Asset account ${asset.publicKey.toBase58()} is not owned by Metaplex Core`); - } - - const metadataUri = decodeAssetUri(assetAccount.data); + const metadataUri = await fetchAssetUri(asset.publicKey.toBase58()); const metadataResponse = await fetch(metadataUri); if (!metadataResponse.ok) { throw new Error(`Metadata fetch failed: ${metadataResponse.status} ${metadataResponse.statusText}`); @@ -109,11 +92,12 @@ async function main(): Promise { console.log(JSON.stringify(output, null, 2)); } -function readBorshString(data: Buffer, offset: number): [string, number] { - const length = data.readUInt32LE(offset); - const start = offset + 4; - const end = start + length; - return [data.subarray(start, end).toString("utf8"), end]; +function createAssetUriFetcher(): AssetUriFetcher { + const { umi } = createMetaplexClient(); + return async (assetAddress: string) => { + const asset = await fetchAsset(umi, publicKey(assetAddress)); + return asset.uri; + }; } if (require.main === module) { diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..87a9d79 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "metaplex": { + "source": "metaplex-foundation/skill", + "sourceType": "github", + "computedHash": "fd3f4ebfb0f30964cb4661744b1ee8250b5495dc4fb9da83b5b063a563a80948" + } + } +}