From 38bf1ed9764438f036c35216ac57081baf844aa3 Mon Sep 17 00:00:00 2001 From: Kevin Arifin Date: Tue, 21 Apr 2026 14:19:25 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20add=20metaplex=20skill=20=E2=80=94=20So?= =?UTF-8?q?lana=20NFTs,=20tokens,=20compressed=20NFTs,=20candy=20machines,?= =?UTF-8?q?=20token=20launches,=20agent=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked from #42 (by tonyagents) with marketplace.json conflict resolved. Co-Authored-By: tonyagents --- .claude-plugin/marketplace.json | 9 + skills/metaplex-development/SKILL.md | 152 +++ .../references/cli-agent.md | 138 +++ .../references/cli-bubblegum.md | 126 ++ .../references/cli-candy-machine.md | 345 ++++++ .../references/cli-config.md | 39 + .../references/cli-core.md | 190 ++++ .../references/cli-genesis.md | 419 +++++++ .../references/cli-initial-setup.md | 66 ++ .../references/cli-token-metadata.md | 59 + .../references/cli-toolbox.md | 68 ++ .../references/cli-troubleshooting.md | 29 + skills/metaplex-development/references/cli.md | 137 +++ .../references/concepts.md | 327 ++++++ .../references/metadata-json.md | 55 + .../references/sdk-agent.md | 349 ++++++ .../references/sdk-bubblegum.md | 349 ++++++ .../references/sdk-core.md | 508 +++++++++ .../references/sdk-genesis.md | 1010 +++++++++++++++++ .../references/sdk-token-metadata-kit.md | 264 +++++ .../references/sdk-token-metadata.md | 460 ++++++++ .../references/sdk-umi.md | 188 +++ 22 files changed, 5287 insertions(+) create mode 100644 skills/metaplex-development/SKILL.md create mode 100644 skills/metaplex-development/references/cli-agent.md create mode 100644 skills/metaplex-development/references/cli-bubblegum.md create mode 100644 skills/metaplex-development/references/cli-candy-machine.md create mode 100644 skills/metaplex-development/references/cli-config.md create mode 100644 skills/metaplex-development/references/cli-core.md create mode 100644 skills/metaplex-development/references/cli-genesis.md create mode 100644 skills/metaplex-development/references/cli-initial-setup.md create mode 100644 skills/metaplex-development/references/cli-token-metadata.md create mode 100644 skills/metaplex-development/references/cli-toolbox.md create mode 100644 skills/metaplex-development/references/cli-troubleshooting.md create mode 100644 skills/metaplex-development/references/cli.md create mode 100644 skills/metaplex-development/references/concepts.md create mode 100644 skills/metaplex-development/references/metadata-json.md create mode 100644 skills/metaplex-development/references/sdk-agent.md create mode 100644 skills/metaplex-development/references/sdk-bubblegum.md create mode 100644 skills/metaplex-development/references/sdk-core.md create mode 100644 skills/metaplex-development/references/sdk-genesis.md create mode 100644 skills/metaplex-development/references/sdk-token-metadata-kit.md create mode 100644 skills/metaplex-development/references/sdk-token-metadata.md create mode 100644 skills/metaplex-development/references/sdk-umi.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 24a04f1..2c6809a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -154,6 +154,15 @@ "skills": [ "./skills/nansen-dca-tracker" ] + }, + { + "name": "metaplex-skills", + "description": "Metaplex development on Solana — NFTs, tokens, compressed NFTs, candy machines, token launches, and autonomous agent identity via the mplx CLI and Umi SDK.", + "source": "./", + "strict": false, + "skills": [ + "./skills/metaplex-development" + ] } ] } diff --git a/skills/metaplex-development/SKILL.md b/skills/metaplex-development/SKILL.md new file mode 100644 index 0000000..db890a8 --- /dev/null +++ b/skills/metaplex-development/SKILL.md @@ -0,0 +1,152 @@ +--- +name: metaplex-development +description: Metaplex development on Solana — NFTs, tokens, compressed NFTs, candy machines, token launches, autonomous agents. Use when working with Token Metadata, Core, Bubblegum, Candy Machine, Genesis, Agent Registry, or the mplx CLI. +tags: [solana, nft, token-metadata, compressed-nfts, candy-machine, token-launch, agent-identity, bubblegum] +license: Apache-2.0 +metadata: + author: metaplex-foundation + version: "0.2.0" + openclaw: {"emoji":"💎","os":["darwin","linux","win32"],"requires":{"bins":["node"]},"homepage":"https://metaplex.com/docs"} +--- + +# Metaplex Development Skill + +## Overview + +Metaplex provides the standard infrastructure for NFTs and tokens on Solana: +- **Agent Registry**: On-chain agent identity, wallets, and execution delegation for MPL Core assets +- **Genesis**: Token launch protocol with fair distribution + liquidity graduation +- **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 + +## Prerequisites + +- Node.js installed +- Metaplex CLI installed: `npm install -g @metaplex-foundation/cli` +- CLI configured (RPC endpoint, keypair, funded wallet) — see `./references/cli-initial-setup.md` +- For Bubblegum/compressed NFTs: a DAS-compatible RPC (Helius, Triton, or QuickNode) + +## 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 (agent guidelines, batching, explorer links) | `./references/cli.md` | +| CLI: Agent Registry (identity, delegation) | `./references/cli.md` + `./references/cli-agent.md` | +| CLI: Core NFTs/Collections | `./references/cli.md` + `./references/cli-core.md` + `./references/metadata-json.md` | +| CLI: Token Metadata NFTs | `./references/cli.md` + `./references/cli-token-metadata.md` + `./references/metadata-json.md` | +| CLI: Compressed NFTs (Bubblegum) | `./references/cli.md` + `./references/cli-bubblegum.md` + `./references/metadata-json.md` | +| CLI: Candy Machine (NFT drops) | `./references/cli.md` + `./references/cli-candy-machine.md` + `./references/metadata-json.md` | +| CLI: Token launch (Genesis) | `./references/cli.md` + `./references/cli-genesis.md` | +| CLI: Execute / asset-signer wallets / agent vault | `./references/cli.md` + `./references/cli-core.md` (execute section) | +| SDK: Execute / asset-signer PDA / agent vault | `./references/sdk-umi.md` + `./references/sdk-core.md` (execute section) | +| CLI: Fungible tokens | `./references/cli.md` + `./references/cli-toolbox.md` | +| SDK setup (Umi) | `./references/sdk-umi.md` | +| SDK: Core NFTs | `./references/sdk-umi.md` + `./references/sdk-core.md` + `./references/metadata-json.md` | +| SDK: Token Metadata | `./references/sdk-umi.md` + `./references/sdk-token-metadata.md` + `./references/metadata-json.md` | +| SDK: Compressed NFTs (Bubblegum) | `./references/sdk-umi.md` + `./references/sdk-bubblegum.md` + `./references/metadata-json.md` | +| SDK: Token Metadata with Kit | `./references/sdk-token-metadata-kit.md` + `./references/metadata-json.md` | +| SDK: Agent Registry (identity, wallets, delegation) | `./references/sdk-umi.md` + `./references/sdk-agent.md` | +| SDK: Token launch (Genesis) | `./references/sdk-umi.md` + `./references/sdk-genesis.md` | +| Off-chain metadata JSON format/schema (NFT or token) | `./references/metadata-json.md` | +| Account structures, PDAs, concepts | `./references/concepts.md` | +| CLI errors, localnet issues | `./references/cli-troubleshooting.md` | + +## CLI Capabilities + +The `mplx` CLI can handle most Metaplex operations directly. **Read `./references/cli.md` for agent guidelines (batching, JSON output, explorer links), then the program-specific file.** + +> **CLI v0.1.0 breaking changes** (for agents/scripts migrating from older versions): +> - `--json ` (used to pass an offchain metadata file path) is now `--offchain `. `--json` is now the standard OCLIF flag for machine-readable output. +> - All commands now return structured JSON when `--json` is passed — use this for programmatic/agent use. + +| Task | CLI Support | +|------|-------------| +| Register agent identity | ✅ | +| Fetch agent data | ✅ | +| Create fungible token | ✅ | +| Create Core NFT/Collection | ✅ | +| Create TM NFT/pNFT | ✅ | +| Transfer TM NFTs | ✅ | +| Transfer fungible tokens | ✅ | +| Transfer Core NFTs | ✅ | +| Upload to Irys | ✅ | +| Candy Machine drop | ✅ (setup/config/insert — minting requires SDK) | +| Compressed NFTs (cNFTs) | ✅ (batch limit ~100, use SDK for larger) | +| Execute (asset-signer wallets) | ✅ | +| Check SOL balance / Airdrop | ✅ | +| Query assets by owner/collection | ❌ SDK only (DAS API) | +| Token launch (Genesis) | ✅ | + +## Program IDs + +``` +Agent Identity: 1DREGFgysWYxLnRnKQnwrxnJQeSMk2HmGaC6whw2B2p +Agent Tools: TLREGni9ZEyGC3vnPZtqUh95xQ8oPqJSvNjvB7FGK8S +Genesis: GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B +Core: CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d +Token Metadata: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s +Bubblegum V1: BGUMAp9SX3uS4efGcFjPjkAQZ4cUNZhtHaMq64nrGf9D +Bubblegum V2: BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY +Core Candy: CMACYFENjoBMHzapRXyo1JZkVS6EtaDDzkjMrmQLvr4J +``` + +## Quick Decision Guide + +### Autonomous Agents + +Use **Agent Registry** to register on-chain identity and execution delegation for MPL Core assets. Any Core asset already has a built-in wallet (Asset Signer PDA) via Core's Execute hook — the registry adds discoverable identity records and lets owners delegate an off-chain executive to operate the agent. Read `./references/cli-agent.md` (CLI) or `./references/sdk-umi.md` + `./references/sdk-agent.md` (SDK). + +### 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). + +### 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-toolbox.md` for CLI commands. + +### NFT Drops + +Use **Core Candy Machine**. Read `./references/cli.md` + `./references/cli-candy-machine.md`. + +### Asset as Agent / Vault / Wallet (Execute) + +Use **Core Execute** when an asset (NFT, agent, vault) needs to hold SOL/tokens, transfer funds, sign transactions, or own other assets. Every Core asset has a signer PDA that can act as an autonomous wallet. Read `./references/cli-core.md` (CLI) or `./references/sdk-core.md` (SDK), execute section. + +## External Resources + +- Documentation: https://metaplex.com/docs +- Agent Registry: https://metaplex.com/docs/agents +- Genesis: https://metaplex.com/docs/smart-contracts/genesis +- Core: https://metaplex.com/docs/smart-contracts/core +- Token Metadata: https://metaplex.com/docs/smart-contracts/token-metadata +- Bubblegum: https://metaplex.com/docs/smart-contracts/bubblegum-v2 +- Candy Machine: https://metaplex.com/docs/smart-contracts/core-candy-machine diff --git a/skills/metaplex-development/references/cli-agent.md b/skills/metaplex-development/references/cli-agent.md new file mode 100644 index 0000000..bc09c6e --- /dev/null +++ b/skills/metaplex-development/references/cli-agent.md @@ -0,0 +1,138 @@ +# Agent Registry CLI Reference + +Commands for registering agent identities and delegating execution via the `mplx` CLI. + +> **Prerequisites**: CLI must be configured (RPC, keypair, funded wallet). If not yet verified this session, see `./cli-initial-setup.md`. +> **Docs**: https://metaplex.com/docs/agents + +--- + +## Commands + +### Register Agent Identity + +Binds an on-chain identity record to an MPL Core asset. Creates a PDA, attaches an `AgentIdentity` plugin with lifecycle hooks for Transfer, Update, and Execute. + +```bash +# Interactive wizard (creates new asset + walks through registration document) +mplx agents register --new --wizard + +# New asset with inline flags +mplx agents register --new --name "My Agent" --description "An autonomous agent" \ + --image "./avatar.png" + +# New asset with services and trust models +mplx agents register --new --name "My Agent" --description "An autonomous agent" \ + --image "./avatar.png" \ + --services '[{"name":"MCP","endpoint":"https://example.com/mcp","version":"2025-06-18"}]' \ + --supported-trust '["reputation","crypto-economic"]' + +# Existing asset — build document from flags +mplx agents register --name "My Agent" --description "An autonomous agent" \ + --image "./avatar.png" + +# Existing asset — upload a local registration document +mplx agents register --from-file "./agent-doc.json" + +# Existing asset with collection authority +mplx agents register --collection --name "My Agent" \ + --description "An autonomous agent" --image "./avatar.png" + +# Save the generated document locally (in addition to uploading) +mplx agents register --new --name "My Agent" --description "..." --image "./avatar.png" \ + --save-document "./agent-doc.json" +``` + +| Flag | Description | +|------|-------------| +| `--new` | Create a new Core asset and register it | +| `--owner` | Owner public key for the new asset (defaults to signer, only with `--new`) | +| `--collection` | Collection address the asset belongs to | +| `--wizard` | Interactive wizard to build the registration document | +| `--from-file ` | Path to a local agent registration JSON file to upload | +| `--name` | Agent name for building the registration document | +| `--description` | Agent description | +| `--image` | Agent image file path (uploaded) or existing URI | +| `--active` | Set agent as active (default: true) | +| `--services` | Service endpoints as JSON array (e.g. `'[{"name":"MCP","endpoint":"https://..."}]'`) | +| `--supported-trust` | Trust models as JSON array (e.g. `'["reputation","tee-attestation"]'`) | +| `--save-document` | Save the generated document JSON to a local file | + +> `--wizard`, `--from-file`, and `--name` are mutually exclusive — use one approach to provide the registration document. + +### Fetch Agent Identity + +Reads the on-chain agent identity PDA and displays registration info, lifecycle hooks, and the agent's wallet. + +```bash +mplx agents fetch +``` + +Returns: `registered`, `asset`, `owner`, `identityPda`, `wallet` (Asset Signer PDA), `registrationUri`, `lifecycleChecks`. + +### Register Executive Profile + +Creates a one-time on-chain executive profile for the current wallet. Required before any delegation. + +```bash +mplx agents executive register +``` + +> Each wallet can only have one executive profile. Calling this again will fail. + +### Delegate Execution + +Links a registered agent to an executive profile, allowing the executive to sign transactions on behalf of the agent. + +```bash +mplx agents executive delegate --executive +``` + +| Flag | Description | +|------|-------------| +| `--executive` | The executive's wallet address (profile PDA derived automatically) | + +> Only the asset owner can delegate. The agent must be registered and the executive must have a profile. + +--- + +## Typical Workflows + +### Register a New Agent (Quick) + +```bash +# One command — creates asset, uploads image + doc, registers identity +mplx agents register --new --name "My Agent" \ + --description "An autonomous trading agent on Solana" \ + --image "./avatar.png" +``` + +### Register + Delegate Execution + +```bash +# 1. Register agent +mplx agents register --new --name "My Agent" \ + --description "An autonomous agent" --image "./avatar.png" +# Note the asset address from output + +# 2. Register executive profile (one-time, on the executive's machine) +mplx agents executive register + +# 3. Delegate execution (on the owner's machine) +mplx agents executive delegate --executive +``` + +### Verify an Agent + +```bash +mplx agents fetch +``` + +--- + +## Agent Registration Document + +The `--name`/`--from-file`/`--wizard` flags produce an [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) JSON document uploaded to Arweave. See `./sdk-agent.md` for the full field reference. + +Service types: `web`, `A2A`, `MCP`, `OASF`, `DID`, `email`, or custom. +Trust models: `reputation`, `crypto-economic`, `tee-attestation`. diff --git a/skills/metaplex-development/references/cli-bubblegum.md b/skills/metaplex-development/references/cli-bubblegum.md new file mode 100644 index 0000000..8450c6e --- /dev/null +++ b/skills/metaplex-development/references/cli-bubblegum.md @@ -0,0 +1,126 @@ +# Bubblegum CLI Reference + +Commands for creating and managing compressed NFTs (cNFTs) via the `mplx` CLI. + +> **Prerequisites**: CLI must be configured (RPC, keypair, funded wallet). If not yet verified this session, see `./cli-initial-setup.md`. +> +> **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/skills/metaplex-development/references/cli-candy-machine.md b/skills/metaplex-development/references/cli-candy-machine.md new file mode 100644 index 0000000..7034e61 --- /dev/null +++ b/skills/metaplex-development/references/cli-candy-machine.md @@ -0,0 +1,345 @@ +# Core Candy Machine CLI Reference + +Candy Machine enables NFT drops with configurable minting rules (guards). + +> **Prerequisites**: CLI must be configured (RPC, keypair, funded wallet). If not yet verified this session, see `./cli-initial-setup.md`. + +--- + +## 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** (follows the NFT metadata standard — see `metadata-json.md`; use relative image path, CM CLI replaces with uploaded URI): +```json +{ + "name": "My Collection", + "description": "A collection of NFTs", + "image": "collection.png", + "external_url": "https://yourproject.com", + "attributes": [], + "properties": { + "files": [{ "uri": "collection.png", "type": "image/png" }], + "category": "image" + } +} +``` + +**Individual asset JSON example** (0.json, 1.json, etc. — same standard, see `metadata-json.md`): +```json +{ + "name": "NFT #0", + "description": "Description here", + "image": "0.png", + "external_url": "https://yourproject.com", + "attributes": [ + { "trait_type": "Background", "value": "Blue" } + ], + "properties": { + "files": [{ "uri": "0.png", "type": "image/png" }], + "category": "image" + } +} +``` + +--- + +## 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) + +Use `groups` instead of `guardConfig` to create separate minting phases (e.g., whitelist then public). Set `guardConfig: {}` when using groups. + +```json +"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 merkleRoot = getMerkleRoot(['Addr1...', 'Addr2...']); +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. Minting requires the Umi SDK (`@metaplex-foundation/mpl-core-candy-machine`). + +- **Frontend app** — Build a minting UI with the Umi SDK +- **Script** — Mint programmatically with `mintV1` from the SDK + +See `./sdk-umi.md` for Umi setup and the [Candy Machine documentation](https://metaplex.com/docs/smart-contracts/core-candy-machine/guards) for minting code examples and guard-specific mint args. + +--- + +## 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/skills/metaplex-development/references/cli-config.md b/skills/metaplex-development/references/cli-config.md new file mode 100644 index 0000000..5beb7e1 --- /dev/null +++ b/skills/metaplex-development/references/cli-config.md @@ -0,0 +1,39 @@ +# Metaplex CLI — Config + +## General + +```bash +mplx config # Show all config +mplx config get # KEY: rpcUrl|commitment|payer|keypair +mplx config set +``` + +## RPC Management + +```bash +mplx config rpcs add # Add a named RPC +mplx config rpcs set # Set active RPC by name +mplx config set rpcUrl # Set active RPC by URL +``` + +## Wallets + +```bash +mplx config wallets # List configured wallets +mplx config wallets new --hidden # Create a new CLI-managed wallet +mplx config wallets add # Add existing keypair (e.g., ~/.config/solana/id.json) +mplx config wallets add --asset # Add an asset-signer wallet +mplx config wallets set # Switch active wallet +``` + +## Storage + +```bash +mplx config storage # Set active storage provider from available providers +``` + +## Explorer + +```bash +mplx config explorer # Set preferred blockchain explorer +``` diff --git a/skills/metaplex-development/references/cli-core.md b/skills/metaplex-development/references/cli-core.md new file mode 100644 index 0000000..4170ef4 --- /dev/null +++ b/skills/metaplex-development/references/cli-core.md @@ -0,0 +1,190 @@ +# Core CLI Reference + +Commands for creating and managing Core NFTs and collections via the `mplx` CLI. + +> **Prerequisites**: CLI must be configured (RPC, keypair, funded wallet). If not yet verified this session, see `./cli-initial-setup.md`. + +--- + +## 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 --offchain # 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 transfer # Collection is auto-detected from the asset +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 + +# Execute (Asset-Signer Wallets) +mplx core asset execute info # Show signer PDA address and SOL balance +``` + +--- + +## 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) - recommended +mplx core asset create --files --image ./image.png --offchain ./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 — follow the NFT schema in metadata-json.md, set image URI to above + +# 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 metadata files — one per NFT, each following the NFT schema in metadata-json.md +# Set each "image" + properties.files[0].uri to the corresponding uploaded image URI + +# 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 +``` + +--- + +## Execute (Asset-Signer Wallets) + +Every MPL Core asset has a deterministic **signer PDA** that can hold SOL, tokens, and own other assets. Asset-signer wallets let you use this PDA as your active wallet — all CLI commands automatically operate through the PDA. + +> **How it works**: When an asset-signer wallet is active, `umi.identity` and `umi.payer` are set to the PDA. At send time, the transaction is wrapped in MPL Core's `execute` instruction, which signs on behalf of the PDA on-chain. The real wallet (asset owner) signs the outer transaction. + +### Setup + +```bash +# 1. Check the PDA info for any asset +mplx core asset execute info + +# 2. Fund the PDA +mplx toolbox sol transfer 0.1 + +# 3. Register as a wallet +mplx config wallets add vault --asset + +# 4. Switch to the asset-signer wallet +mplx config wallets set vault + +# 5. Use any command as the PDA +mplx toolbox sol balance +mplx toolbox sol transfer 0.01 +mplx core asset create --name "PDA Created NFT" --uri "https://example.com/nft" + +# Switch back to normal wallet +mplx config wallets set my-wallet +``` + +> Pass `-k /path/to/wallet.json` to bypass the asset-signer wallet for a single command. +> Use `-p /path/to/fee-payer.json` to have a different wallet pay transaction fees. + +### Supported Commands + +Most CLI commands work with asset-signer wallets. The transaction wrapping is transparent. + +- **Core**: `asset create`, `asset transfer`, `asset burn`, `asset update`, `collection create` +- **Toolbox SOL**: `balance`, `transfer` (`wrap`/`unwrap` may fail; see CPI limitations) +- **Toolbox Token**: `transfer`, `create`, `mint` +- **Toolbox Raw**: `raw --instruction ` +- **Token Metadata**: `transfer`, `create`, `update` +- **Bubblegum**: `nft create` (public trees), `nft transfer`, `nft burn`, `collection create` +- **Genesis**: `create`, `bucket add-*`, `deposit`, `withdraw`, `claim`, `finalize`, `revoke` + +### Raw Instructions + +```bash +# Execute arbitrary base64-encoded instructions as the PDA +mplx toolbox raw --instruction +mplx toolbox raw --instruction --instruction +echo "" | mplx toolbox raw --stdin +``` + +### CPI Limitations + +Some operations cannot be wrapped in `execute()` due to Solana CPI constraints: +- **Large account creation** — Merkle trees, candy machines (exceed CPI account allocation limits) +- **Native SOL wrapping** — `transferSol` to a token account fails in CPI context + +For these, use a normal wallet to create the infrastructure first, then switch to the asset-signer wallet for subsequent operations. diff --git a/skills/metaplex-development/references/cli-genesis.md b/skills/metaplex-development/references/cli-genesis.md new file mode 100644 index 0000000..53bf7aa --- /dev/null +++ b/skills/metaplex-development/references/cli-genesis.md @@ -0,0 +1,419 @@ +# Genesis CLI Reference + +Commands for creating and managing token launches (TGEs) via the `mplx` CLI. + +> **Prerequisites**: CLI must be configured (RPC, keypair, funded wallet). If not yet verified this session, see `./cli-initial-setup.md`. +> **Concepts**: For lifecycle, fees, condition objects, and end behaviors, see `./concepts.md` Genesis section. +> **Docs**: https://metaplex.com/docs/smart-contracts/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 + +- Default deposit/withdraw fees: 200 bps (2%). See: https://metaplex.com/docs/protocol-fees +- Launch API `--raiseGoal` is in **whole units** (e.g., `200` = 200 SOL), NOT base units. +- Low-level commands use **base units** (1 SOL = 1000000000) and **Unix seconds** (not milliseconds). +- Cannot add buckets after `finalize`. `finalize` and `revoke` are **irreversible**. +- **`finalize` requires 100% supply allocation** — 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. +- Low-level commands have no `--wizard` mode — all flags must be provided explicitly. diff --git a/skills/metaplex-development/references/cli-initial-setup.md b/skills/metaplex-development/references/cli-initial-setup.md new file mode 100644 index 0000000..a865d02 --- /dev/null +++ b/skills/metaplex-development/references/cli-initial-setup.md @@ -0,0 +1,66 @@ +# Metaplex CLI Initial Setup + +> **Check first**: Run `mplx config get rpcUrl && mplx config get keypair && mplx toolbox sol balance` before starting setup. If all three return valid values, setup is already complete — skip this file. + +### 1. Configure RPC URLs + +Ask the user whether they have their own RPC provider (e.g., Helius, Triton, QuickNode) or want to use the default public RPCs. Public RPCs are rate-limited and may fail under load. Recommend adding RPCs for both devnet and mainnet so they can easily switch later. + +**Add RPC URLs to the CLI:** + +```bash +# Add custom RPCs (recommended) +mplx config rpcs add mainnet +mplx config rpcs add devnet + +# Or add default public RPCs (rate-limited) +mplx config rpcs add mainnet https://api.mainnet-beta.solana.com +mplx config rpcs add devnet https://api.devnet.solana.com +mplx config rpcs add localnet http://localhost:8899 +``` + +**Then set the active RPC by name or URL:** + +```bash +mplx config rpcs set # e.g., mplx config rpcs set devnet +mplx config set rpcUrl # or set directly by URL +``` + +### 2. Configure Keypair + +Ask the user whether they want to create a new CLI-managed wallet or use an existing Solana CLI keypair. + +**Option A — New CLI wallet:** + +```bash +mplx config wallets new --hidden +``` + +**Option B — Existing Solana CLI keypair:** + +```bash +mplx config wallets add ~/.config/solana/id.json +``` + +### 3. Fund Your Wallet (devnet) + +```bash +mplx toolbox sol airdrop --amount 2 +``` + +### 4. Verify Setup + +Run all checks in one command: + +```bash +mplx config get rpcUrl && mplx config get keypair && mplx toolbox sol balance +``` + +### Setup Error Resolution + +- **`mplx: command not found`** — Install the CLI: `npm i -g @metaplex-foundation/cli` +- **`No RPC URL configured`** — Ask the user for an RPC address, or default to devnet: `mplx config set rpcUrl https://api.devnet.solana.com` +- **`No keypair configured`** — Ask the user if they want to create a new CLI wallet (`mplx config wallets new --hidden`) or add an existing Solana keypair (`mplx config wallets add ~/.config/solana/id.json`) +- **`0 SOL`** — Airdrop on devnet: `mplx toolbox sol airdrop --amount 2` (devnet only) + +> **Mainnet Safety:** If RPC URL contains `mainnet`, confirm with user before executing commands that spend SOL. diff --git a/skills/metaplex-development/references/cli-token-metadata.md b/skills/metaplex-development/references/cli-token-metadata.md new file mode 100644 index 0000000..7b3506d --- /dev/null +++ b/skills/metaplex-development/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**: CLI must be configured (RPC, keypair, funded wallet). If not yet verified this session, see `./cli-initial-setup.md`. + +--- + +## 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 --offchain # 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/skills/metaplex-development/references/cli-toolbox.md b/skills/metaplex-development/references/cli-toolbox.md new file mode 100644 index 0000000..5b58150 --- /dev/null +++ b/skills/metaplex-development/references/cli-toolbox.md @@ -0,0 +1,68 @@ +# Metaplex CLI — Toolbox (Storage, SOL, Tokens, Rent, Templates, Raw) + +## Rent + +```bash +mplx toolbox rent # Get rent cost for a given number of bytes +``` + +## Raw Transaction Sender + +Send arbitrary base64-encoded instructions as raw transactions. Useful for executing custom program instructions or interacting with programs not directly supported by the CLI. + +```bash +mplx toolbox raw --instruction # Execute a base64-encoded instruction +mplx toolbox raw --instruction --instruction # Multiple instructions +echo "" | mplx toolbox raw --stdin # From stdin +``` + +## Templates + +Templates download to the current directory the CLI is run from. + +```bash +mplx toolbox template program # Download a MPLX program template +mplx toolbox template website # Download a MPLX website template +``` + +## Storage (Irys) + +```bash +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 +``` + +> Check `mplx toolbox storage balance` before large uploads. Fund with `mplx toolbox storage fund ` if insufficient. + +## SOL + +```bash +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) + +```bash +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. diff --git a/skills/metaplex-development/references/cli-troubleshooting.md b/skills/metaplex-development/references/cli-troubleshooting.md new file mode 100644 index 0000000..171765a --- /dev/null +++ b/skills/metaplex-development/references/cli-troubleshooting.md @@ -0,0 +1,29 @@ +# Metaplex CLI — Localnet Limitations & Troubleshooting + +## 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/skills/metaplex-development/references/cli.md b/skills/metaplex-development/references/cli.md new file mode 100644 index 0000000..21910a0 --- /dev/null +++ b/skills/metaplex-development/references/cli.md @@ -0,0 +1,137 @@ +# 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`, `agents`, `genesis` + +--- + +## Quick Command Index + +| Topic | Detail File | +|-------|-------------| +| Initial Setup | `./cli-initial-setup.md` | +| 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` | +| Genesis (Token Launches) | `./cli-genesis.md` | +| Agent Registry (Identity, Delegation) | `./cli-agent.md` | +| Config & RPC Management | `./cli-config.md` | +| Toolbox (Storage, SOL, Tokens) | `./cli-toolbox.md` | +| Localnet & Troubleshooting | `./cli-troubleshooting.md` | + +--- + +## JSON Output (Agent Use) + +All commands support `--json` for structured machine-readable output: + +```bash +mplx core asset create --name "My NFT" --uri "https://..." --json +mplx genesis launch create --name "My Token" ... --json +mplx toolbox sol balance --json +``` + +Returns a JSON object instead of formatted text. Use this when running the CLI programmatically or from an agent — the output shape is consistent and parseable. + +> **Note**: `--offchain ` is the flag for passing a local offchain metadata JSON file to upload. This is separate from `--json` (which controls output format). + +--- + +## 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) +# Prepare metadata.json first — follow the NFT schema in metadata-json.md +mplx core asset create --files --image ./image.png --offchain ./metadata.json + +# TM NFT from local files (no --files flag needed for tm) +mplx tm create --image ./image.png --offchain ./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 + +--- + +## 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 (each file must follow the NFT JSON schema — see metadata-json.md) +# Create one .json file per NFT with the correct schema before running uploads + +# 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` + diff --git a/skills/metaplex-development/references/concepts.md b/skills/metaplex-development/references/concepts.md new file mode 100644 index 0000000..d88ffb0 --- /dev/null +++ b/skills/metaplex-development/references/concepts.md @@ -0,0 +1,327 @@ +# 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]` | + +### Agent Registry Accounts + +``` +AgentIdentityV1 (40 bytes) +├── key: u8 (discriminator) +├── bump: u8 +├── _padding: [u8; 6] +└── asset: Pubkey (the MPL Core asset) + +ExecutiveProfileV1 (40 bytes) +├── key: u8 (discriminator) +├── _padding: [u8; 7] +└── authority: Pubkey + +ExecutionDelegateRecordV1 (104 bytes) +├── key: u8 (discriminator) +├── bump: u8 +├── _padding: [u8; 6] +├── executiveProfile: Pubkey +├── authority: Pubkey +└── agentAsset: Pubkey +``` + +### Agent Registry PDAs + +| Account | Seeds | +|---------|-------| +| Agent Identity | `['agent_identity', asset]` | +| Executive Profile | `['executive_profile', authority]` | +| Execution Delegate Record | `['execution_delegate_record', executive_profile, agent_asset]` | + +--- + +## 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://metaplex.com/docs/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/skills/metaplex-development/references/metadata-json.md b/skills/metaplex-development/references/metadata-json.md new file mode 100644 index 0000000..6be828e --- /dev/null +++ b/skills/metaplex-development/references/metadata-json.md @@ -0,0 +1,55 @@ +# Off-Chain Metadata JSON Standards + +## NFT Metadata JSON (Core & Token Metadata NFTs) + +```json +{ + "name": "Asset Name", + "description": "Description of the asset", + "image": "https://...", + "external_url": "https://yourproject.com", + "animation_url": "https://...", + "attributes": [ + { + "trait_type": "Background", + "value": "Blue" + }, + { + "trait_type": "Rarity", + "value": "Legendary" + } + ], + "properties": { + "files": [ + { + "uri": "https://...", + "type": "image/png" + } + ], + "category": "image" + } +} +``` + +**Required:** `name`, `image`, `properties` +**Optional:** `description`, `external_url`, `animation_url`, `attributes` + +> **Note**: While `external_url` and `attributes` are optional, including them is strongly recommended — wallets and marketplaces rely on them for display and indexing. + +**Categories:** `image`, `video`, `audio`, `vr`, `html` + +**`properties.files` ordering convention:** +- **Index 0** — always the image file, matching the top-level `image` field +- **Index 1** — the `animation_url` file, if present — can be any rich media type: video, audio, HTML, 3D model, etc. +- **Index 2+** — any additional files + +## Fungible Token Metadata JSON + +```json +{ + "name": "TOKEN_NAME", + "symbol": "TOKEN_SYMBOL", + "description": "TOKEN_DESC", + "image": "TOKEN_IMAGE_URL" +} +``` diff --git a/skills/metaplex-development/references/sdk-agent.md b/skills/metaplex-development/references/sdk-agent.md new file mode 100644 index 0000000..a44ca4f --- /dev/null +++ b/skills/metaplex-development/references/sdk-agent.md @@ -0,0 +1,349 @@ + +# Agent Registry SDK Reference (Umi) + +Umi SDK operations for registering agent identities, reading agent data, and delegating execution. + +> **Prerequisites**: Set up Umi first — see `./sdk-umi.md` for installation and basic setup. +> **Docs**: https://metaplex.com/docs/agents + +> **Agents are MPL Core assets.** Before registering an agent, you need an MPL Core asset. See `./sdk-core.md` for creating assets and collections. + +--- + +## Installation + +```shell +npm install @metaplex-foundation/mpl-agent-registry +``` + +## Setup + +```typescript +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'; +import { mplCore } from '@metaplex-foundation/mpl-core'; +import { mplAgentIdentity, mplAgentTools } from '@metaplex-foundation/mpl-agent-registry'; + +const umi = createUmi('https://api.mainnet-beta.solana.com') + .use(mplCore()) + .use(mplAgentIdentity()) + .use(mplAgentTools()); +``` + +--- + +## Register Agent Identity + +Creates a PDA derived from the asset's public key and attaches an `AgentIdentity` plugin with lifecycle hooks for Transfer, Update, and Execute. + +```typescript +import { registerIdentityV1 } from '@metaplex-foundation/mpl-agent-registry'; + +await registerIdentityV1(umi, { + asset: assetPublicKey, + collection: collectionPublicKey, // optional but recommended + agentRegistrationUri: 'https://arweave.net/agent-registration.json', +}).sendAndConfirm(umi); +``` + +| Parameter | Description | +|-----------|-------------| +| `asset` | The MPL Core asset to register | +| `collection` | The asset's collection (optional) | +| `agentRegistrationUri` | URI pointing to off-chain agent registration metadata (ERC-8004 format) | +| `payer` | Pays for rent and fees (defaults to `umi.payer`) | +| `authority` | Collection authority (defaults to `payer`) | + +> Registration is a one-time operation per asset. Calling `registerIdentityV1` on an already-registered asset will fail. + +## Agent Registration Document (ERC-8004) + +The `agentRegistrationUri` points to a JSON document hosted on permanent storage (e.g. Arweave). Format follows [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004): + +```json +{ + "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", + "name": "Plexpert", + "description": "An informational agent providing help related to Metaplex protocols and tools.", + "image": "https://arweave.net/agent-avatar-tx-hash", + "services": [ + { + "name": "web", + "endpoint": "https://example.com/agent/" + }, + { + "name": "A2A", + "endpoint": "https://example.com/agent//agent-card.json", + "version": "0.3.0" + }, + { + "name": "MCP", + "endpoint": "https://example.com/agent//mcp", + "version": "2025-06-18" + } + ], + "active": true, + "registrations": [ + { + "agentId": "", + "agentRegistry": "solana:101:metaplex" + } + ], + "supportedTrust": ["reputation", "crypto-economic"] +} +``` + +### Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `type` | Yes | Schema identifier — use `https://eips.ethereum.org/EIPS/eip-8004#registration-v1` | +| `name` | Yes | Human-readable agent name | +| `description` | Yes | What the agent does, how it works, and how to interact with it | +| `image` | Yes | Avatar or logo URI | +| `services` | No | Array of service endpoints (each has `name`, `endpoint`, optional `version`/`skills`/`domains`) | +| `active` | No | Whether the agent is currently active (`true`/`false`) | +| `registrations` | No | Array of on-chain registrations (`agentId` = mint address, `agentRegistry` = `solana:101:metaplex`) | +| `supportedTrust` | No | Trust models: `reputation`, `crypto-economic`, `tee-attestation` | + +--- + +## Check Registration + +```typescript +import { safeFetchAgentIdentityV1, findAgentIdentityV1Pda } from '@metaplex-foundation/mpl-agent-registry'; + +const pda = findAgentIdentityV1Pda(umi, { asset: assetPublicKey }); +const identity = await safeFetchAgentIdentityV1(umi, pda); + +console.log('Registered:', identity !== null); +``` + +> `safeFetchAgentIdentityV1` returns `null` for unregistered assets instead of throwing. + +## Fetch Identity from Seeds + +```typescript +import { fetchAgentIdentityV1FromSeeds } from '@metaplex-foundation/mpl-agent-registry'; + +const identity = await fetchAgentIdentityV1FromSeeds(umi, { + asset: assetPublicKey, +}); +``` + +## Verify AgentIdentity Plugin on Asset + +```typescript +import { fetchAsset } from '@metaplex-foundation/mpl-core'; + +const assetData = await fetchAsset(umi, assetPublicKey); + +const agentIdentity = assetData.agentIdentities?.[0]; +console.log(agentIdentity?.uri); // registration URI +console.log(agentIdentity?.lifecycleChecks?.transfer); // truthy +console.log(agentIdentity?.lifecycleChecks?.update); // truthy +console.log(agentIdentity?.lifecycleChecks?.execute); // truthy +``` + +## Read Registration Document + +```typescript +const assetData = await fetchAsset(umi, assetPublicKey); +const agentIdentity = assetData.agentIdentities?.[0]; + +if (agentIdentity?.uri) { + const response = await fetch(agentIdentity.uri); + const registration = await response.json(); + + console.log(registration.name); + console.log(registration.description); + console.log(registration.active); + + for (const service of registration.services) { + console.log(service.name, service.endpoint); + } +} +``` + +## Fetch Agent Wallet (Asset Signer) + +Every Core asset has a built-in PDA wallet — no private key exists, so it can't be stolen. Only the asset itself can sign for it through Core's Execute lifecycle hook. + +```typescript +import { findAssetSignerPda } from '@metaplex-foundation/mpl-core'; + +const assetSignerPda = findAssetSignerPda(umi, { asset: assetPublicKey }); + +const balance = await umi.rpc.getBalance(assetSignerPda); +console.log('Agent wallet:', assetSignerPda); +console.log('Balance:', balance.basisPoints.toString(), 'lamports'); +``` + +--- + +## Register Executive Profile + +Before an executive can run any agent, it needs a profile. This is a one-time setup per wallet. + +```typescript +import { registerExecutiveV1 } from '@metaplex-foundation/mpl-agent-registry'; + +await registerExecutiveV1(umi, { + payer: umi.payer, +}).sendAndConfirm(umi); +``` + +| Parameter | Description | +|-----------|-------------| +| `payer` | Pays for rent and fees (also used as the authority) | +| `authority` | The wallet that owns this executive profile (defaults to `payer`) | + +> Each wallet can only have one executive profile. PDA: `["executive_profile", ]`. + +## Delegate Execution + +The agent asset owner delegates execution to an executive profile. Creates a delegation record on-chain. + +```typescript +import { delegateExecutionV1, findAgentIdentityV1Pda, findExecutiveProfileV1Pda } from '@metaplex-foundation/mpl-agent-registry'; + +const agentIdentity = findAgentIdentityV1Pda(umi, { asset: agentAssetPublicKey }); +const executiveProfile = findExecutiveProfileV1Pda(umi, { authority: executiveAuthorityPublicKey }); + +await delegateExecutionV1(umi, { + agentAsset: agentAssetPublicKey, + agentIdentity, + executiveProfile, +}).sendAndConfirm(umi); +``` + +| Parameter | Description | +|-----------|-------------| +| `agentAsset` | The registered agent's MPL Core asset | +| `agentIdentity` | The agent identity PDA for the asset | +| `executiveProfile` | The executive profile PDA to delegate to | +| `payer` | Pays for rent and fees (defaults to `umi.payer`) | +| `authority` | Must be the asset owner (defaults to `payer`) | + +> Only the asset owner can delegate execution. Delegation is per-asset. + +## Verify Delegation + +```typescript +import { findExecutiveProfileV1Pda, findExecutionDelegateRecordV1Pda } from '@metaplex-foundation/mpl-agent-registry'; + +const executiveProfile = findExecutiveProfileV1Pda(umi, { + authority: executiveAuthorityPublicKey, +}); + +const delegateRecord = findExecutionDelegateRecordV1Pda(umi, { + executiveProfile, + agentAsset: agentAssetPublicKey, +}); + +const account = await umi.rpc.getAccount(delegateRecord); +console.log('Delegated:', account.exists); +``` + +--- + +## Full Example: Register + Delegate + +```typescript +import { generateSigner } from '@metaplex-foundation/umi'; +import { create, createCollection } from '@metaplex-foundation/mpl-core'; +import { mplAgentIdentity, mplAgentTools } from '@metaplex-foundation/mpl-agent-registry'; +import { + registerIdentityV1, + registerExecutiveV1, + delegateExecutionV1, + findAgentIdentityV1Pda, + findExecutiveProfileV1Pda, +} from '@metaplex-foundation/mpl-agent-registry'; + +// 1. Create a collection +const collection = generateSigner(umi); +await createCollection(umi, { + collection, + name: 'Agent Collection', + uri: 'https://example.com/collection.json', +}).sendAndConfirm(umi); + +// 2. Create an asset +const asset = generateSigner(umi); +await create(umi, { + asset, + name: 'My Agent', + uri: 'https://example.com/agent.json', + collection, +}).sendAndConfirm(umi); + +// 3. Register identity +await registerIdentityV1(umi, { + asset: asset.publicKey, + collection: collection.publicKey, + agentRegistrationUri: 'https://example.com/agent-registration.json', +}).sendAndConfirm(umi); + +// 4. Register executive profile (one-time per wallet) +await registerExecutiveV1(umi, { + payer: umi.payer, +}).sendAndConfirm(umi); + +// 5. Delegate execution +const agentIdentity = findAgentIdentityV1Pda(umi, { asset: asset.publicKey }); +const executiveProfile = findExecutiveProfileV1Pda(umi, { authority: umi.payer.publicKey }); + +await delegateExecutionV1(umi, { + agentAsset: asset.publicKey, + agentIdentity, + executiveProfile, +}).sendAndConfirm(umi); +``` + +--- + +## PDA Reference + +| Account | Seeds | Size | +|---------|-------|------| +| `AgentIdentityV1` | `["agent_identity", ]` | 40 bytes | +| `ExecutiveProfileV1` | `["executive_profile", ]` | 40 bytes | +| `ExecutionDelegateRecordV1` | `["execution_delegate_record", , ]` | 104 bytes | + +## Program IDs + +| Program | Address | +|---------|---------| +| Agent Identity | `1DREGFgysWYxLnRnKQnwrxnJQeSMk2HmGaC6whw2B2p` | +| Agent Tools | `TLREGni9ZEyGC3vnPZtqUh95xQ8oPqJSvNjvB7FGK8S` | + +Same addresses on Mainnet and Devnet. + +## Errors + +### Agent Identity + +| Code | Name | Description | +|------|------|-------------| +| 0 | `InvalidSystemProgram` | System program account is incorrect | +| 1 | `InvalidInstructionData` | Instruction data is malformed | +| 2 | `InvalidAccountData` | PDA derivation does not match the asset | +| 3 | `InvalidMplCoreProgram` | MPL Core program account is incorrect | +| 4 | `InvalidCoreAsset` | Asset is not a valid MPL Core asset | + +### Agent Tools + +| Code | Name | Description | +|------|------|-------------| +| 0 | `InvalidSystemProgram` | System program account is incorrect | +| 1 | `InvalidInstructionData` | Instruction data is malformed | +| 2 | `InvalidAccountData` | Invalid account data | +| 3 | `InvalidMplCoreProgram` | MPL Core program account is incorrect | +| 4 | `InvalidCoreAsset` | Asset is not a valid MPL Core asset | +| 5 | `ExecutiveProfileMustBeUninitialized` | Executive profile already exists | +| 6 | `InvalidExecutionDelegateRecordDerivation` | Delegation record PDA derivation mismatch | +| 7 | `ExecutionDelegateRecordMustBeUninitialized` | Delegation record already exists | +| 8 | `InvalidAgentIdentity` | Agent identity account is invalid | +| 9 | `AgentIdentityNotRegistered` | Asset does not have a registered identity | +| 10 | `AssetOwnerMustBeTheOneToDelegateExecution` | Only the asset owner can delegate execution | +| 11 | `InvalidExecutiveProfileDerivation` | Executive profile PDA derivation mismatch | diff --git a/skills/metaplex-development/references/sdk-bubblegum.md b/skills/metaplex-development/references/sdk-bubblegum.md new file mode 100644 index 0000000..76dc937 --- /dev/null +++ b/skills/metaplex-development/references/sdk-bubblegum.md @@ -0,0 +1,349 @@ + +# 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://metaplex.com/docs/smart-contracts/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/skills/metaplex-development/references/sdk-core.md b/skills/metaplex-development/references/sdk-core.md new file mode 100644 index 0000000..874cb99 --- /dev/null +++ b/skills/metaplex-development/references/sdk-core.md @@ -0,0 +1,508 @@ + +# 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://metaplex.com/docs/smart-contracts/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. + +> **Off-chain metadata**: Before creating an asset, upload a metadata JSON to Arweave/IPFS. See `./metadata-json.md` for the canonical schema. The resulting URI is passed as the `uri` parameter. + +--- + +## 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, // pass the fetched collection object, not just the publicKey + name: 'Asset #1', + uri: 'https://arweave.net/xxx', +}).sendAndConfirm(umi); +``` + +> The `create` helper requires a collection **object** (from `fetchCollection` or `createCollection`'s signer), not a bare public key. Passing `collection.publicKey` silently creates the asset without a collection association. + +## 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); +``` + +--- + +## Execute (Asset-Signer PDA) + +Every MPL Core asset has a deterministic **signer PDA** that can hold SOL, tokens, and own other assets. The `execute` function wraps arbitrary instructions so the PDA signs them on-chain via CPI. + +> **Permission model**: Only the asset **owner** can call `execute`. Update authority cannot execute. +> **Collection assets**: Pass the `collection` parameter only when `asset.updateAuthority.type === 'Collection'`. Omitting it causes `MissingCollection`; passing it when the asset has `Address`-type update authority causes `InvalidCollection`. + +### Single Instruction + +```typescript +import { execute, findAssetSignerPda, fetchAsset } from '@metaplex-foundation/mpl-core'; +import { transferSol } from '@metaplex-foundation/mpl-toolbox'; +import { createNoopSigner, publicKey, sol } from '@metaplex-foundation/umi'; + +const asset = await fetchAsset(umi, assetAddress); +const assetSigner = findAssetSignerPda(umi, { asset: asset.publicKey }); + +await execute(umi, { + asset, + instructions: transferSol(umi, { + source: createNoopSigner(publicKey(assetSigner)), + destination: recipientAddress, + amount: sol(0.5), + }), +}).sendAndConfirm(umi); +``` + +### Multiple Instructions + +Chain instructions using `.add()` on a `TransactionBuilder`: + +```typescript +await execute(umi, { + asset, + instructions: transferSol(umi, { + source: createNoopSigner(publicKey(assetSigner)), + destination: recipientAddress, + amount: sol(0.25), + }).add( + transferSol(umi, { + source: createNoopSigner(publicKey(assetSigner)), + destination: recipientAddress, + amount: sol(0.25), + }) + ), +}).sendAndConfirm(umi); +``` + +### With Raw Instruction Array + +Extract instructions from a builder and pass as `Instruction[]`. When using raw instructions, provide explicit `signers`: + +```typescript +const instructions = transferSol(umi, { + source: createNoopSigner(publicKey(assetSigner)), + destination: recipientAddress, + amount: sol(0.5), +}).getInstructions(); + +await execute(umi, { + asset, + instructions, + signers: [createNoopSigner(publicKey(assetSigner))], +}).sendAndConfirm(umi); +``` + +### With Collection + +```typescript +const { asset, collection } = /* fetched asset and collection */; + +await execute(umi, { + asset, + collection, + instructions: transferSol(umi, { + source: createNoopSigner(publicKey(assetSigner)), + destination: recipientAddress, + amount: sol(0.5), + }), +}).sendAndConfirm(umi); +``` + +> **CPI limitations**: Large account creation (Merkle trees, candy machines) and native SOL wrapping may fail inside `execute()` due to Solana CPI constraints. + +--- + +## 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/skills/metaplex-development/references/sdk-genesis.md b/skills/metaplex-development/references/sdk-genesis.md new file mode 100644 index 0000000..a2528f5 --- /dev/null +++ b/skills/metaplex-development/references/sdk-genesis.md @@ -0,0 +1,1010 @@ + +# 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://metaplex.com/docs/protocol-fees + +--- + +## Program ID + +``` +Genesis: GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B +``` + +## Documentation + +Full documentation: https://metaplex.com/docs/smart-contracts/genesis diff --git a/skills/metaplex-development/references/sdk-token-metadata-kit.md b/skills/metaplex-development/references/sdk-token-metadata-kit.md new file mode 100644 index 0000000..09c0c32 --- /dev/null +++ b/skills/metaplex-development/references/sdk-token-metadata-kit.md @@ -0,0 +1,264 @@ + +# 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/skills/metaplex-development/references/sdk-token-metadata.md b/skills/metaplex-development/references/sdk-token-metadata.md new file mode 100644 index 0000000..19bb676 --- /dev/null +++ b/skills/metaplex-development/references/sdk-token-metadata.md @@ -0,0 +1,460 @@ + +# 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://metaplex.com/docs/smart-contracts/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/skills/metaplex-development/references/sdk-umi.md b/skills/metaplex-development/references/sdk-umi.md new file mode 100644 index 0000000..6a37d8d --- /dev/null +++ b/skills/metaplex-development/references/sdk-umi.md @@ -0,0 +1,188 @@ + +# 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 — follow the full NFT metadata schema (see metadata-json.md) +const metadataUri = await umi.uploader.uploadJson({ + name: 'My NFT', + description: 'Description', // optional + image: imageUri, + external_url: 'https://yourproject.com', // optional but recommended + animation_url: animationUri, // optional, omit if not applicable + attributes: [{ trait_type: 'Background', value: 'Blue' }], // optional but recommended + properties: { + files: [{ uri: imageUri, type: 'image/png' }], + category: 'image', + }, +}); +``` + +--- + +## 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; +} +```