diff --git a/.github/workflows/sync-handlers.yml b/.github/workflows/sync-handlers.yml new file mode 100644 index 00000000..a0aeffb0 --- /dev/null +++ b/.github/workflows/sync-handlers.yml @@ -0,0 +1,33 @@ +name: Sync Handler Code + +on: + repository_dispatch: + types: [sync-handlers] + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout docs repo + uses: actions/checkout@v4 + + - name: Checkout light-protocol + uses: actions/checkout@v4 + with: + repository: Lightprotocol/light-protocol + path: light-protocol + ref: ${{ github.event.client_payload.commit }} + + - name: Sync handlers to docs + run: | + chmod +x ./scripts/sync-docs.sh + ./scripts/sync-docs.sh + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + branch: sync-handlers-${{ github.event.client_payload.commit }} + title: "Sync handler code from light-protocol release" + body: | + Auto-generated from light-protocol commit: ${{ github.event.client_payload.commit }} + commit-message: "sync: update Full Code Examples from light-protocol" diff --git a/.windsurf/rules/mint.md b/.windsurf/rules/mint.md deleted file mode 100644 index ed84568a..00000000 --- a/.windsurf/rules/mint.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -trigger: always_on ---- - ---- -name: light-protocol-documentation-writer -description: Configures Claude as a pragmatic technical writer following strict style guides, frontmatter requirements, and Git workflow rules for MDX documentation. Use PROACTIVELY when working on technical documentation, MDX files, content strategy, or documentation structure tasks. -allowed-tools: [read, edit, glob, grep, mcp__deepwiki__read_wiki_structure, mcp__deepwiki__read_wiki_contents, mcp__deepwiki__ask_question] ---- - -You are an experienced, pragmatic technical writer with robust content strategy and content design experience. You elegantly create just enough docs to solve users' needs and get them back to the product quickly. - -Rule #1: If you want an exception to ANY rule, YOU MUST STOP and get explicit permission from Ethan first. BREAKING THE LETTER OR SPIRIT OF THE RULES IS FAILURE. - -## Working relationship - -- We're colleagues working together your name is "Windie" -- You can push back on ideas-this can lead to better documentation. Cite sources and explain your reasoning when you do so -- ALWAYS ask for clarification rather than making assumptions -- NEVER lie, guess, or make up information -- You are much better read than I am. I have more nuanced understanding about our users. We work together to solve problems with our combined strengths. -- When you disagree with my approach, YOU MUST push back, citing specific reasons if you have them. -- YOU MUST call out bad ideas, unreasonable expectations, and mistakes. -- NEVER be agreeable just to be nice - I need your honest technical judgment. -- NEVER tell me I'm "absolutely right" or anything like that. You ARE NOT a sycophant. -- We can be humorous and playful, but not when it gets in the way of the task at hand. Save it for when a project is finished or we need levity during a tough project. -- YOU MUST ALWAYS ask for clarification rather than making assumptions. -- If you're having trouble, YOU MUST STOP and ask for help, especially for tasks where human input would be valuable. -- If you are making an inferrance, stop and ask me for confirmation or say that you need more information - -## Project context -- Format: MDX files with YAML frontmatter -- Config: docs.json for navigation, theme, settings - - See the docs.json schema: https://mintlify.com/docs.json -- Components reference: - - Quick reference: mintlify-docs/quick-reference/ - - All components: mintlify-docs/docs/components/ -- Directory structure: - - client-library/ - Client library documentation (TypeScript and Rust) - - c-token/ - Compressed token documentation - - mintlify-docs/ - Local Mintlify documentation (gitignored) - - images/ - Image assets - - logo/ - Logo files - -## Content strategy -- We document just enough so that users are successful. Too much content makes it hard to find what people are looking for. Too little makes it too challenging to accomplish users' goals. -- Prioritize accuracy and usability of information -- Make content evergreen when possible -- Search for existing information before adding new content. Avoid duplication unless it is done for a strategic reason -- Check existing patterns for consistency -- Start by making the smallest reasonable changes - -## Frontmatter requirements for pages -- title: Clear, descriptive page title -- description: Concise summary for SEO/navigation - -## Writing standards -- Second-person voice ("you") -- Prerequisites at start of procedural content -- Test all code examples before publishing -- Match style and formatting of existing pages -- Include both basic and advanced use cases -- Language tags on all code blocks -- Alt text on all images -- Relative paths for internal links -- Use broadly applicable examples rather than overly specific business cases -- Lead with context when helpful - explain what something is before diving into implementation details -- Use sentence case for all headings ("Getting started", not "Getting Started") -- Use sentence case for code block titles ("Expandable example", not "Expandable Example") -- Prefer active voice and direct language -- Remove unnecessary words while maintaining clarity -- Break complex instructions into clear numbered steps -- Make language more precise and contextual -- Use [Lucide](https://lucide.dev) icon library - -### Language and tone standards -- **Avoid promotional language**: Never use phrases like "rich heritage," "breathtaking," "captivates," "stands as a testament," "plays a vital role,""enables","comprehensive" or similar marketing language in technical documentation -- **Reduce conjunction overuse**: Limit use of "moreover," "furthermore," "additionally," "on the other hand" - favor direct, clear statements -- **Avoid editorializing**: Remove phrases like "it's important to note," "this article will," "in conclusion," or personal interpretations -- **No undue emphasis**: Avoid overstating importance or significance of routine technical concepts - -### Technical accuracy standards -- **Verify all links**: Every external reference must be tested and functional before publication -- **Use precise citations**: Replace vague references with specific documentation, version numbers, and accurate sources -- **Maintain consistency**: Use consistent terminology, formatting, and language variety throughout all documentation -- **Valid technical references**: Ensure all code examples, API references, and technical specifications are current and accurate - -### Formatting discipline - -- **Purposeful formatting**: Use bold, italics, and emphasis only when it serves the user's understanding, not for visual appeal -- **Clean structure**: Avoid excessive formatting, emoji, or decorative elements that don't add functional value -- **Standard heading case**: Use sentence case for headings unless project style guide specifies otherwise -- **Minimal markup**: Keep formatting clean and functional, avoiding unnecessary markdown or styling - -### Component introductions -- Start with action-oriented language: "Use [component] to..." rather than "The [component] component..." -- Be specific about what components can contain or do -- Make introductions practical and user-focused - -### Property descriptions -- End all property descriptions with periods for consistency -- Be specific and helpful rather than generic -- Add scope clarification where needed (e.g., "For Font Awesome icons only:") -- Use proper technical terminology ("boolean" not "bool") - -### Code examples -- Keep examples simple and practical -- Use consistent formatting and naming -- Provide clear, actionable examples rather than showing multiple options when one will do - -## Content organization -- Structure content in the order users need it -- Combine related information to reduce redundancy -- Use specific links (direct to relevant pages rather than generic dashboards) -- Put most commonly needed information first - -## Git workflow -- NEVER use --no-verify when committing -- Ask how to handle uncommitted changes before starting -- Create a new branch when no clear branch exists for changes -- Commit frequently throughout development -- NEVER skip or disable pre-commit hooks - -## Do not -- Skip frontmatter on any MDX file -- Use absolute URLs for internal links -- Include untested code examples -- Make assumptions - always ask for clarification diff --git a/.windsurf/workflows/ask-deepwiki.md b/.windsurf/workflows/ask-deepwiki.md index 012cc54c..889bdb23 100644 --- a/.windsurf/workflows/ask-deepwiki.md +++ b/.windsurf/workflows/ask-deepwiki.md @@ -15,7 +15,7 @@ Query the Light Protocol repository via DeepWiki MCP with precise technical answ **MANDATORY STEPS BEFORE EXECUTION:** 1. Read this complete file -2. Read [Global CLAUDE.md](/home/tilo/.claude/CLAUDE.md) +2. Read Global CLAUDE.md 3. Read terminology reference for precision rules **VERIFICATION CHECKLIST:** @@ -23,7 +23,7 @@ Query the Light Protocol repository via DeepWiki MCP with precise technical answ - [ ] Are you familiar with precision rules (avoid vague verbs)? - [ ] Will you provide file:line references? -**Navigation**: [Global README](/home/tilo/.claude/README.md) | [Commands](/home/tilo/.claude/commands/) | [Global CLAUDE.md](/home/tilo/.claude/CLAUDE.md) +**Navigation**: Global README | Commands | Global CLAUDE.md ## Command Process diff --git a/blog/README.mdx b/blog/README.mdx new file mode 100644 index 00000000..59db2268 --- /dev/null +++ b/blog/README.mdx @@ -0,0 +1,3 @@ +--- +title: "Blog" +--- diff --git a/blog/c-token.mdx b/blog/c-token.mdx new file mode 100644 index 00000000..d3ae9fe4 --- /dev/null +++ b/blog/c-token.mdx @@ -0,0 +1,133 @@ +--- +title: "Announcing rent-free Tokens on Solana: the Light-Token Standard" +description: " Today we bring to you the light-token standard. A token standard with mint and token accounts equivalent to SPL, but without rent-exemption." + +--- +import { CodeCompare } from '/snippets/jsx/code-compare.jsx'; +import CompressibleRentExplained from '/snippets/compressible-rent-explained.mdx'; +import { RentLifecycleVisualizer } from '/snippets/jsx/rent-lifecycle-visualizer.jsx'; + +Today we bring to you the light-token standard. A token standard with mint and token accounts equivalent to SPL, but without rent-exemption. +Users receive and send the same tokens. +From now on, you can create mints and tokens +1. at a fraction of the cost, +2. with improved performance, and +3. with similar developer experience. + + +At the moment around XXX token accounts over 20k mint accounts are created on a daily basis. +Many are created and rarely accessed. +Still lock up SOL due to rent-exemption. +There have been many discussions around how to solve rent on Solana (SIMD references). + +To truly scale Solana to 1 billion+ users the cost to store data must be near zero. + +That's why we built light-token, which is like SPL token just stored more efficiently to reduce account creation cost. +#### Mints and Tokens without Rent-Exemption + +| Creation Cost | SPL | Light | +|:---------------------|:------------------|:-------------------| +| **Mint Account** | ~1,500,000 lamports | **15,000** lamports | +| **Token Account** | ~2,000,000 lamports | ~**17,000** lamports | + +#### Interoperability with Existing SPL Tokens + +A key feature to highlight is that light-token accounts can hold tokens from light, SPL, or Token 2022 mints. +This means you can convert existing SPL tokens to light-tokens and back. +Existing SPL tokens can hereby benefit from the rent config light-token introduces, to which we get in a minute. + +This is implemented by the `light-token-sdk`. +It follows similar patterns to SPL to maximize developer experience +and puts the barrier low to integrate light-token in your protocol, app, or payment flow. + +Example to create SPL vs light-token account: + + +## Light-Token Program Features + +### Mint Accounts with Token Metadata + +Mints uniquely represent a token on Solana and store its global metadata, +similar to SPL mint accounts with few core differences: +- Light-token mint accounts are compressed accounts and rent-free. +- Tokens created from light-token mints are light-tokens. + +You can view the source code here. + +### Token Accounts with Pay-as-you-go Rent + +Light-tokens are like SPL tokens and hold token balances of light, SPL, or Token 2022 mints. + +Light-token accounts are Solana accounts, but don't require the creator to pay rent-exemption. +Instead, rent is paid over time when accounts are accessed: + + + + + +The token accounts is initalized and funded with initial rent. +When rent runs low, transactions top-up the account's rent balance. +Transactions only top-up the account's rent balance, when it is low. +When rent runs out, the account is automatically compressed. +Your tokens are preserved as a compressed token account and +are automatically decompressed when you interact with it again. + +You can view the source code here. + +### Extensions + +Extensions are under development and are currently implemented. +Additional extensions can be requested. + +Coming soon: +- MetadataPointer +- TokenMetadata +- InterestBearingConfig +- GroupPointer +- GroupMemberPointer +- TokenGroup +- TokenGroupMember +- MintCloseAuthority +- TransferFeeConfig +- DefaultAccountState +- PermanentDelegate +- TransferHook +- Pausable +- ConfidentialTransferMint +- ConfidentialTransferFeeConfig +- ConfidentialMintBurn + +## Integrate with Your Application + +We have dedicated toolkits for specific use cases: + +- [Payments](/light-token/toolkits/for-payments) +- [Wallets](/light-token/toolkits/for-wallets) +- [DEXs](/light-token/toolkits/for-dexs) +- [Launchpads](/light-token/toolkits/for-launchpads) +- [Trading Apps](/light-token/toolkits/for-trading-apps) + +Get in touch on telegram for help! + +## What's next? + +For now, light-token is live on Devnet. Mainnet is anticipated for end of Q1 2026. + +Get started and send us your feedback! diff --git a/client-library/client-guide.mdx b/client-library/client-guide.mdx index 30705dec..e60d35d2 100644 --- a/client-library/client-guide.mdx +++ b/client-library/client-guide.mdx @@ -5,7 +5,7 @@ description: >- code examples. --- -import SystemAccountsList from '/snippets/compressed-pdas-system-accounts-list.mdx'; +import SystemAccountsList from '/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx'; ZK Compression provides Rust and Typescript clients to interact with compressed accounts and tokens on Solana. diff --git a/compressed-pdas/create-a-program-with-compressed-pdas.mdx b/compressed-pdas/create-a-program-with-compressed-pdas.mdx index 9a68059f..7a046f86 100644 --- a/compressed-pdas/create-a-program-with-compressed-pdas.mdx +++ b/compressed-pdas/create-a-program-with-compressed-pdas.mdx @@ -1,16 +1,16 @@ --- -title: Create a Program with Compressed PDAs -description: Overview to compressed PDA core features and guide for program development +title: Overview & Program Template +description: Compressed PDAs provide full functionality of accounts at PDAs, without rent-exemption per account. + --- -import ProgramExamplesTable from '/snippets/program-examples-table.mdx'; -import DevelopmentEnvironmentSetup from '/snippets/development-environment-setup.mdx'; +import ProgramExamplesTable from '/snippets/overview-tables/program-examples-table.mdx'; +import DevelopmentEnvironmentSetup from '/snippets/setup/development-environment-setup.mdx'; -Compressed PDAs provide full functionality of accounts at PDAs, without per-account rent cost. -| Creation | Regular PDA Account | Compressed PDA | Cost Reduction | -| :------------- | :--------------------- | :---------------------- | :------------------ | -| 100-byte PDA | ~ 0.0016 SOL | **~ 0.00001 SOL** | ***160x*** | +| Creation | Regular PDA Account | Compressed PDA | +| :------------- | :--------------------- | :---------------------- | +| 100-byte PDA | ~1,600,000 lamports | 15,000 lamports | Compressed PDAs are derived using a specific program address and seed, like regular PDAs. Custom programs invoke the Light System program to create and update accounts, instead of the System program. diff --git a/compressed-pdas/guides.mdx b/compressed-pdas/guides.mdx index c25c1694..da612261 100644 --- a/compressed-pdas/guides.mdx +++ b/compressed-pdas/guides.mdx @@ -4,7 +4,7 @@ description: Overview to guides for Solana programs to create, update, close, re sidebarTitle: "Overview" --- -import GuidesTable from '/snippets/compressed-pdas-guides-table.mdx'; +import GuidesTable from '/snippets/overview-tables/compressed-pdas-guides-table.mdx'; diff --git a/compressed-pdas/guides/how-to-burn-compressed-accounts.mdx b/compressed-pdas/guides/how-to-burn-compressed-accounts.mdx index 42adf795..a1b25a4a 100644 --- a/compressed-pdas/guides/how-to-burn-compressed-accounts.mdx +++ b/compressed-pdas/guides/how-to-burn-compressed-accounts.mdx @@ -3,9 +3,9 @@ title: Burn Compressed Accounts description: Guide to burn compressed accounts in Solana programs with full code examples. --- -import CompressedPdasProgramSetup from '/snippets/compressed-pdas-program-setup.mdx'; -import CompressedPdasSystemAccountsList from '/snippets/compressed-pdas-system-accounts-list.mdx'; -import DevelopmentEnvironmentSetup from '/snippets/development-environment-setup.mdx'; +import CompressedPdasProgramSetup from '/snippets/setup/compressed-pdas-program-setup.mdx'; +import CompressedPdasSystemAccountsList from '/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx'; +import DevelopmentEnvironmentSetup from '/snippets/setup/development-environment-setup.mdx'; Compressed accounts are permanently burned via CPI to the Light System Program. diff --git a/compressed-pdas/guides/how-to-close-compressed-accounts.mdx b/compressed-pdas/guides/how-to-close-compressed-accounts.mdx index 0441ba82..611ecfe9 100644 --- a/compressed-pdas/guides/how-to-close-compressed-accounts.mdx +++ b/compressed-pdas/guides/how-to-close-compressed-accounts.mdx @@ -3,9 +3,9 @@ title: Close Compressed Accounts description: Guide to close compressed accounts in Solana programs with full code examples. --- -import CompressedPdasProgramSetup from '/snippets/compressed-pdas-program-setup.mdx'; -import CompressedPdasSystemAccountsList from '/snippets/compressed-pdas-system-accounts-list.mdx'; -import DevelopmentEnvironmentSetup from '/snippets/development-environment-setup.mdx'; +import CompressedPdasProgramSetup from '/snippets/setup/compressed-pdas-program-setup.mdx'; +import CompressedPdasSystemAccountsList from '/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx'; +import DevelopmentEnvironmentSetup from '/snippets/setup/development-environment-setup.mdx'; Compressed accounts are closed via CPI to the Light System Program. diff --git a/compressed-pdas/guides/how-to-create-compressed-accounts.mdx b/compressed-pdas/guides/how-to-create-compressed-accounts.mdx index f7057995..aa02342a 100644 --- a/compressed-pdas/guides/how-to-create-compressed-accounts.mdx +++ b/compressed-pdas/guides/how-to-create-compressed-accounts.mdx @@ -3,8 +3,8 @@ title: Create Compressed Accounts description: Guide to create compressed accounts in Solana programs with full code examples. --- -import CompressedPdasSystemAccountsList from '/snippets/compressed-pdas-system-accounts-list.mdx'; -import DevelopmentEnvironmentSetup from '/snippets/development-environment-setup.mdx'; +import CompressedPdasSystemAccountsList from '/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx'; +import DevelopmentEnvironmentSetup from '/snippets/setup/development-environment-setup.mdx'; Compressed accounts and addresses are created via CPI to the Light System Program. diff --git a/compressed-pdas/guides/how-to-reinitialize-compressed-accounts.mdx b/compressed-pdas/guides/how-to-reinitialize-compressed-accounts.mdx index dbc26544..913171c8 100644 --- a/compressed-pdas/guides/how-to-reinitialize-compressed-accounts.mdx +++ b/compressed-pdas/guides/how-to-reinitialize-compressed-accounts.mdx @@ -3,9 +3,9 @@ title: Reinitialize Compressed Accounts description: Guide to reinitialize compressed accounts in Solana programs with full code examples. --- -import CompressedPdasProgramSetup from '/snippets/compressed-pdas-program-setup.mdx'; -import CompressedPdasSystemAccountsList from '/snippets/compressed-pdas-system-accounts-list.mdx'; -import DevelopmentEnvironmentSetup from '/snippets/development-environment-setup.mdx'; +import CompressedPdasProgramSetup from '/snippets/setup/compressed-pdas-program-setup.mdx'; +import CompressedPdasSystemAccountsList from '/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx'; +import DevelopmentEnvironmentSetup from '/snippets/setup/development-environment-setup.mdx'; Compressed accounts are reinitialized via CPI to the Light System Program. diff --git a/compressed-pdas/guides/how-to-update-compressed-accounts.mdx b/compressed-pdas/guides/how-to-update-compressed-accounts.mdx index 1993c533..8c836723 100644 --- a/compressed-pdas/guides/how-to-update-compressed-accounts.mdx +++ b/compressed-pdas/guides/how-to-update-compressed-accounts.mdx @@ -3,9 +3,9 @@ title: Update Compressed Accounts description: Guide to update compressed accounts in Solana programs with full code examples. --- -import CompressedPdasProgramSetup from '/snippets/compressed-pdas-program-setup.mdx'; -import CompressedPdasSystemAccountsList from '/snippets/compressed-pdas-system-accounts-list.mdx'; -import DevelopmentEnvironmentSetup from '/snippets/development-environment-setup.mdx'; +import CompressedPdasProgramSetup from '/snippets/setup/compressed-pdas-program-setup.mdx'; +import CompressedPdasSystemAccountsList from '/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx'; +import DevelopmentEnvironmentSetup from '/snippets/setup/development-environment-setup.mdx'; Compressed accounts are updated via CPI to the Light System Program. diff --git a/compressed-token-program/stash.mdx b/compressed-token-program/stash.mdx deleted file mode 100644 index 283fc4ea..00000000 --- a/compressed-token-program/stash.mdx +++ /dev/null @@ -1,6 +0,0 @@ - -**SPL mints and tokens** owned by the [Token Program](https://github.com/solana-program/token) or [Token-2022](https://github.com/solana-program/token-2022) **require rent** by default.
-You can **transfer SPL token accounts to cToken accounts** for **sponsored rent-exemption** while keeping the **same interoparability**. -
- -Ask DeepWiki \ No newline at end of file diff --git a/compressed-tokens/README.mdx b/compressed-tokens/README.mdx new file mode 100644 index 00000000..1db8f2c9 --- /dev/null +++ b/compressed-tokens/README.mdx @@ -0,0 +1,88 @@ +--- +title: Overview +description: "Overview to compressed tokens and guides with full code examples. Use for token distribution or storage of inactive token accounts." +--- + +import GuidesTable from '/snippets/overview-tables/compressed-tokens-guides-table.mdx'; +import AdvancedGuidesTable from '/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import { TokenAccountCompressedVsSpl } from '/snippets/jsx/token-account-compressed-vs-spl.jsx'; + +| Creation | Solana | Compressed | +| :------------- | :--------------------- | :---------------------- | +| **Token Account** | ~2,000,000 lamports | **5,000** lamports | + +1. Compressed token accounts store token balance, owner, and other information of tokens like SPL and light-tokens. +2. Compressed token accounts are rent-free. +3. Any light-token or SPL token can be compressed/decompressed at will. + +Wallets like Phantom and Backpack support compressed tokens. The UI does not distinguish between SPL and compressed tokens. + +## Recommended Usage of Compressed Tokens + + + ### [Token Distribution](#advanced-guides) + * Distribute tokens without paying up front rent per recipient. + + + + + + ### Storage of Inactive Token Accounts + * Most (associated) token accounts are not frequently written to. + * **Store token accounts rent-free** when inactive + * [Light-tokens](/light-token/README) are automatically compressed/decompressed, when active/inactive and include **sponsored rent-exemption**. + + + + + +# Get Started + + + + + +### Install dependencies + + + + + + +### Set up your developer environment + + + + + + + + + + + + +### Basic Guides + + + + +### Advanced Guides + + + + \ No newline at end of file diff --git a/compressed-tokens/advanced-guides.mdx b/compressed-tokens/advanced-guides.mdx index 34b0cd5e..94066669 100644 --- a/compressed-tokens/advanced-guides.mdx +++ b/compressed-tokens/advanced-guides.mdx @@ -4,6 +4,6 @@ description: "Reference table to advanced guides. Includes guides for airdrops, sidebarTitle: "Overview" --- -import AdvancedGuidesTable from '/snippets/compressed-tokens-advanced-guides-table.mdx'; +import AdvancedGuidesTable from '/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx'; diff --git a/compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens.mdx b/compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens.mdx index 75f03f49..feb5eff2 100644 --- a/compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens.mdx +++ b/compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens.mdx @@ -3,8 +3,8 @@ title: Add Wallet Support for Compressed Tokens description: Guide to add Compressed Token Support to Your Wallet Application --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; Leading Solana Wallets like Phantom and Backpack already support compressed tokens. diff --git a/compressed-tokens/advanced-guides/create-an-airdrop.mdx b/compressed-tokens/advanced-guides/create-an-airdrop.mdx index 42a0d3d3..8c8f5fbd 100644 --- a/compressed-tokens/advanced-guides/create-an-airdrop.mdx +++ b/compressed-tokens/advanced-guides/create-an-airdrop.mdx @@ -3,9 +3,9 @@ title: Create an Airdrop without Claim description: Complete guide to create an airdrop – with or without code. Access to cost calculation and best practices. ZK compression is the most efficient way to distribute SPL tokens. --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; -import { TokenAccountCompressedVsSpl } from '/snippets/token-account-compressed-vs-spl.jsx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; +import { TokenAccountCompressedVsSpl } from '/snippets/jsx/token-account-compressed-vs-spl.jsx'; ## Guides diff --git a/compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction.mdx b/compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction.mdx index a633fd20..0b4909d6 100644 --- a/compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction.mdx +++ b/compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction.mdx @@ -3,8 +3,8 @@ title: Combine Instructions in One Transaction description: Guide to combine multiple instructions in a single transaction. Full code example for token pool creation and for first-time compression of existing SPL tokens. --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; The SDK provides instruction-level APIs that return instructions without sending transactions. Combine these instructions to build custom transactions with multiple instructions. diff --git a/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx b/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx index d66daf7e..0029a174 100644 --- a/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx +++ b/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx @@ -3,8 +3,8 @@ title: Use Token-2022 with Compression description: Complete guide to mint, compress and transfer tokens with Token-2022 Metadata with ZK Compression. --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; # What you will do diff --git a/compressed-tokens/guides.mdx b/compressed-tokens/guides.mdx index 4d0a1c6f..16a44ebd 100644 --- a/compressed-tokens/guides.mdx +++ b/compressed-tokens/guides.mdx @@ -4,6 +4,6 @@ description: "Overview of guides to compressed token operations and reference ta sidebarTitle: "Overview" --- -import GuidesTable from '/snippets/compressed-tokens-guides-table.mdx'; +import GuidesTable from '/snippets/overview-tables/compressed-tokens-guides-table.mdx'; diff --git a/compressed-tokens/guides/how-to-approve-and-revoke-delegate-authority.mdx b/compressed-tokens/guides/how-to-approve-and-revoke-delegate-authority.mdx index d1bb1d67..5523d75b 100644 --- a/compressed-tokens/guides/how-to-approve-and-revoke-delegate-authority.mdx +++ b/compressed-tokens/guides/how-to-approve-and-revoke-delegate-authority.mdx @@ -3,8 +3,8 @@ title: Approve and Revoke Delegate Authority description: "Complete guide to manage delegate authority for compressed tokens with `approve()` and `revoke()`, troubleshooting and advanced configurations." --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; The `approve()` and `revoke()` functions grant and remove delegate spending authority for compressed tokens. Only the token owner can perform these instructions. diff --git a/compressed-tokens/guides/how-to-compress-and-decompress-spl-tokens.mdx b/compressed-tokens/guides/how-to-compress-and-decompress-spl-tokens.mdx index f381642f..632ea228 100644 --- a/compressed-tokens/guides/how-to-compress-and-decompress-spl-tokens.mdx +++ b/compressed-tokens/guides/how-to-compress-and-decompress-spl-tokens.mdx @@ -3,9 +3,9 @@ title: Compress and Decompress SPL Tokens description: "Guide to compress and decompress SPL tokens, troubleshooting and advanced configurations." --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; -import MintPrereq from '/snippets/compressed-tokens-mint-prereq.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; +import MintPrereq from '/snippets/setup/compressed-tokens-mint-prereq.mdx'; The `compress()` and `decompress()` functions convert SPL tokens between compressed and regular format. diff --git a/compressed-tokens/guides/how-to-compress-complete-spl-token-accounts.mdx b/compressed-tokens/guides/how-to-compress-complete-spl-token-accounts.mdx index 10c534fd..f3343ad9 100644 --- a/compressed-tokens/guides/how-to-compress-complete-spl-token-accounts.mdx +++ b/compressed-tokens/guides/how-to-compress-complete-spl-token-accounts.mdx @@ -3,9 +3,9 @@ title: Compress Complete SPL Token Accounts description: "Guide to compress complete SPL Token Accounts with compressSplTokenAccount() for account migration and to reclaim rent afterwards." --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; -import MintPrereq from '/snippets/compressed-tokens-mint-prereq.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; +import MintPrereq from '/snippets/setup/compressed-tokens-mint-prereq.mdx'; The `compressSplTokenAccount` function compresses the balance of an SPL token account, with an optional `remainingAmount` parameter to leave tokens in the original account. diff --git a/compressed-tokens/guides/how-to-create-and-register-a-mint-account-for-compression.mdx b/compressed-tokens/guides/how-to-create-and-register-a-mint-account-for-compression.mdx index 8a616c1b..350c10d3 100644 --- a/compressed-tokens/guides/how-to-create-and-register-a-mint-account-for-compression.mdx +++ b/compressed-tokens/guides/how-to-create-and-register-a-mint-account-for-compression.mdx @@ -3,8 +3,8 @@ title: Create a Mint Account with Token Pool for Compression description: "Create an SPL token mint account with token pool for compression with createMint(), troubleshooting and advanced configurations." --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; The mint account itself requires rent (like regular SPL mints), but individual compressed token accounts are rent-free. diff --git a/compressed-tokens/guides/how-to-create-compressed-token-pools-for-mint-accounts.mdx b/compressed-tokens/guides/how-to-create-compressed-token-pools-for-mint-accounts.mdx index 873e68f2..c9950a5f 100644 --- a/compressed-tokens/guides/how-to-create-compressed-token-pools-for-mint-accounts.mdx +++ b/compressed-tokens/guides/how-to-create-compressed-token-pools-for-mint-accounts.mdx @@ -3,8 +3,8 @@ title: Create Token Pools for Compression to Existing Mints description: Guide to create token pools for compressed tokens for SPL mints with `createTokenPool()`, troubleshooting and advanced configurations. --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; Create a token for compression fo an existing SPL mint. `createTokenPool()` requires only `fee_payer` and has no mint authority constraint. diff --git a/compressed-tokens/guides/how-to-merge-compressed-token-accounts.mdx b/compressed-tokens/guides/how-to-merge-compressed-token-accounts.mdx index 4e63e04b..dc72f3e0 100644 --- a/compressed-tokens/guides/how-to-merge-compressed-token-accounts.mdx +++ b/compressed-tokens/guides/how-to-merge-compressed-token-accounts.mdx @@ -3,8 +3,8 @@ title: Merge Compressed Token Accounts description: "Complete guide to merge multiple compressed token accounts into a single account with mergeTokenAccounts(), troubleshooting and advanced configurations." --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; The `mergeTokenAccounts()` function consolidates multiple compressed accounts of the same mint into a single compressed account. diff --git a/compressed-tokens/guides/how-to-mint-compressed-tokens.mdx b/compressed-tokens/guides/how-to-mint-compressed-tokens.mdx index 0062d482..3f4027f8 100644 --- a/compressed-tokens/guides/how-to-mint-compressed-tokens.mdx +++ b/compressed-tokens/guides/how-to-mint-compressed-tokens.mdx @@ -3,9 +3,9 @@ title: Mint Compressed Tokens description: "Guide to mint compressed tokens with mintTo(), troubleshooting, and advanced configurations." --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; -import MintPrereq from '/snippets/compressed-tokens-mint-prereq.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; +import MintPrereq from '/snippets/setup/compressed-tokens-mint-prereq.mdx'; The `mintTo()` function creates compressed token accounts for recipients and increases the mint's token supply. Only the mint authority can perform this operation. diff --git a/compressed-tokens/guides/how-to-transfer-compressed-token.mdx b/compressed-tokens/guides/how-to-transfer-compressed-token.mdx index b3295b29..9f70547a 100644 --- a/compressed-tokens/guides/how-to-transfer-compressed-token.mdx +++ b/compressed-tokens/guides/how-to-transfer-compressed-token.mdx @@ -3,9 +3,9 @@ title: Transfer Compressed Tokens description: Guide to transfer compressed SPL tokens between compressed or regular accounts with `transfer()`, troubleshooting and advanced configurations. --- -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; -import MintPrereq from '/snippets/compressed-tokens-mint-prereq.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; +import MintPrereq from '/snippets/setup/compressed-tokens-mint-prereq.mdx'; The `transfer()` function moves compressed tokens between accounts. * SPL token transfers that update the existing account diff --git a/compressed-tokens/overview.mdx b/compressed-tokens/overview.mdx deleted file mode 100644 index 4f706041..00000000 --- a/compressed-tokens/overview.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Overview -description: Compressed tokens provide full SPL token functionality without per-account rent cost. ---- - -import GuidesTable from '/snippets/compressed-tokens-guides-table.mdx'; -import AdvancedGuidesTable from '/snippets/compressed-tokens-advanced-guides-table.mdx'; -import SetupEnvironment from '/snippets/setup-environment-tabs.mdx'; -import InstallDependencies from '/snippets/install-dependencies-codegroup.mdx'; - -| Creation | Regular SPL Token | Compressed Token | Cost Reduction | -|:---------------------|:------------------|:----------------------|:---------------| -| 100 Token Accounts | ~ 0.2 SOL | **~ 0.00004 SOL** | **5000x** | - -Compressed token accounts store information about an individual's ownership of a specific token (mint). Different from regular token accounts, they don't require an Associated Token Account (ATA) per token holder. - -For example, this simplifies [token distribution](/compressed-tokens/advanced-guides/create-an-airdrop), since you don't need to allocate a token account per recipient. - -#### Compressed Tokens at a Glance - - - - Create token accounts without upfront rent exempt balance. - - - Compatible with SPL tokens and Solana programs. - - - Supported by leading wallets including Phantom and Backpack. - - - -# Start building - -Developing with compressed tokens works similar SPL tokens and involves minimal setup: - - - - -### Install dependencies - - - - - - -### Set up your developer environment - - - - - - - -### Get started - - - -## Guides - - - - -## Advanced Guides - - - - -# Next Steps - - \ No newline at end of file diff --git a/docs.json b/docs.json index 7eb392ea..c924b63a 100644 --- a/docs.json +++ b/docs.json @@ -11,13 +11,46 @@ "appearance": { "default": "light" }, + "banner": { + "content": "The Light-Token Standard is live on Devnet. Start testing and create mints or tokens!", + "dismissible": false + }, "navigation": { "tabs": [ { - "tab": "Docs", - "pages": [ + "tab": "Documentation", + "groups": [ + { + "group": "Light-Token Standard (Beta)", + "pages": [ + "light-token/README", + "light-token/faq", + { + "group": "Cookbook", + "pages": [ + "light-token/cookbook/create-mint", + "light-token/cookbook/create-ata", + "light-token/cookbook/create-token-account", + "light-token/cookbook/mint-tokens", + "light-token/cookbook/transfer-interface", + "light-token/cookbook/close-token-account" + ] + }, + { + "group": "Toolkits", + "expanded": true, + "pages": [ + "light-token/toolkits/README", + "light-token/toolkits/for-payments", + "light-token/toolkits/for-wallets", + "light-token/toolkits/for-streaming-mints", + "light-token/toolkits/for-streaming-tokens" + ] + } + ] + }, { - "group": "Get Started", + "group": "Get Started with ZK Compression", "pages": [ "welcome", "quickstart", @@ -27,11 +60,10 @@ { "group": "Compressed Tokens", "pages": [ - "compressed-tokens/overview", + "compressed-tokens/README", { - "group": "Guides", + "group": "Cookbook", "pages": [ - "compressed-tokens/guides", "compressed-tokens/guides/how-to-create-compressed-token-accounts", "compressed-tokens/guides/how-to-mint-compressed-tokens", "compressed-tokens/guides/how-to-transfer-compressed-token", @@ -44,16 +76,27 @@ ] }, { - "group": "Advanced Guides", + "group": "Implementation Guides", "pages": [ - "compressed-tokens/advanced-guides", - "compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction", - "compressed-tokens/advanced-guides/create-an-airdrop", - "compressed-tokens/advanced-guides/create-an-airdrop-with-claim", - "compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens", - "compressed-tokens/advanced-guides/use-token-2022-with-compression", - "compressed-tokens/advanced-guides/example-web-client", - "compressed-tokens/advanced-guides/example-node-js" + { + "group": "Integration", + "expanded": true, + "pages": [ + "compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction", + "compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens", + "compressed-tokens/advanced-guides/use-token-2022-with-compression" + ] + }, + { + "group": "Examples", + "expanded": true, + "pages": [ + "compressed-tokens/advanced-guides/create-an-airdrop", + "compressed-tokens/advanced-guides/create-an-airdrop-with-claim", + "compressed-tokens/advanced-guides/example-web-client", + "compressed-tokens/advanced-guides/example-node-js" + ] + } ] } ] @@ -360,7 +403,8 @@ "learn/core-concepts/transaction-lifecycle", "learn/core-concepts/considerations" ] - } + }, + "learn/light-token-standard" ] }, { @@ -405,99 +449,6 @@ ] } ] - }, - { - "tab": "Compressed Tokens", - "pages": [ - "compressed-tokens/overview", - { - "group": "Guides", - "pages": [ - "compressed-tokens/guides/how-to-create-compressed-token-accounts", - "compressed-tokens/guides/how-to-mint-compressed-tokens", - "compressed-tokens/guides/how-to-transfer-compressed-token", - "compressed-tokens/guides/how-to-compress-and-decompress-spl-tokens", - "compressed-tokens/guides/how-to-compress-complete-spl-token-accounts", - "compressed-tokens/guides/how-to-create-and-register-a-mint-account-for-compression", - "compressed-tokens/guides/how-to-create-compressed-token-pools-for-mint-accounts", - "compressed-tokens/guides/how-to-merge-compressed-token-accounts", - "compressed-tokens/guides/how-to-approve-and-revoke-delegate-authority" - ] - }, - { - "group": "Advanced Guides", - "pages": [ - "compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction", - "compressed-tokens/advanced-guides/create-an-airdrop", - "compressed-tokens/advanced-guides/create-an-airdrop-with-claim", - "compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens", - "compressed-tokens/advanced-guides/use-token-2022-with-compression", - "compressed-tokens/advanced-guides/example-web-client", - "compressed-tokens/advanced-guides/example-node-js" - ] - } - ] - }, - { - "tab": "Compressed PDAs", - "pages": [ - "compressed-pdas/create-a-program-with-compressed-pdas", - { - "group": "Guides", - "pages": [ - "compressed-pdas/guides/how-to-create-compressed-accounts", - "compressed-pdas/guides/how-to-update-compressed-accounts", - "compressed-pdas/guides/how-to-close-compressed-accounts", - "compressed-pdas/guides/how-to-reinitialize-compressed-accounts", - "compressed-pdas/guides/how-to-burn-compressed-accounts" - ] - }, - "compressed-pdas/program-examples" - ] - }, - { - "tab": "API Reference", - "pages": [ - { - "group": "JSON RPC Methods", - "pages": [ - "api-reference/json-rpc-methods/overview", - { - "group": "Methods", - "pages": [ - "api-reference/json-rpc-methods/methods", - "api-reference/json-rpc-methods/getcompressedaccount", - "api-reference/json-rpc-methods/getcompressedaccountsbyowner", - "api-reference/json-rpc-methods/getcompressedbalancebyowner", - "api-reference/json-rpc-methods/getcompressedbalance", - "api-reference/json-rpc-methods/getcompressedminttokenholders", - "api-reference/json-rpc-methods/getcompressedtokenaccountbalance", - "api-reference/json-rpc-methods/getcompressedtokenaccountbydelegate", - "api-reference/json-rpc-methods/getcompressedtokenaccountsbyowner", - "api-reference/json-rpc-methods/getcompressedtokenbalancesbyowner", - "api-reference/json-rpc-methods/getcompressionsignaturesforaccount", - "api-reference/json-rpc-methods/getcompressionsignaturesforaddress", - "api-reference/json-rpc-methods/getcompressionsignaturesforowner", - "api-reference/json-rpc-methods/getcompressionsignaturesfortokenowner", - "api-reference/json-rpc-methods/getindexerhealth", - "api-reference/json-rpc-methods/getindexerslot", - "api-reference/json-rpc-methods/getlatestcompressionsignatures", - "api-reference/json-rpc-methods/getlatestnonvotingsignatures", - "api-reference/json-rpc-methods/getmultiplecompressedaccounts", - "api-reference/json-rpc-methods/getmultiplenewaddressproofs", - "api-reference/json-rpc-methods/gettransactionwithcompressioninfo", - "api-reference/json-rpc-methods/getvalidityproof" - ] - } - ] - } - ] - }, - { - "tab": "Releases", - "pages": [ - "changelog" - ] } ], "global": { diff --git a/learn/ai-tools-guide.mdx b/learn/ai-tools-guide.mdx index 1484f08d..4463958a 100644 --- a/learn/ai-tools-guide.mdx +++ b/learn/ai-tools-guide.mdx @@ -185,7 +185,7 @@ Start testing your AI tools with compressed tokens or PDAs. title="Compressed Tokens" icon="chevron-right" color="#0066ff" - href="/compressed-tokens/overview" + href="/compressed-tokens" /> + + + + Account Type + Key Features + + + + + **[Light-Mint](#light-mint-accounts)** + Compressed account + +
    +
  • **Rent-free mint accounts** (similar to SPL mints)
  • +
  • Custom [Token Metadata](#token-metadata) **extension**
  • +
+ + + + **[Light-Token](#light-token-account)** + Solana account + +
    +
  • **Store tokens** of **light-mints, SPL mints, or Token 2022 mints**
  • +
  • **Sponsored rent** exemption via [compressible extension](#compressible)
  • +
  • Use for **active token accounts** with frequent writes (trading, etc.)
  • +
+ + + + **[Compressed Token](#compressed-token-account)** + Compressed account + +
    +
  • **Compressed account** with `TokenData` field
  • +
  • **Rent-free** and SPL-compatible
  • +
  • Use for **storage of inactive tokens** and **token distribution**
  • +
  • Light-token accounts are automatically compressed/decompressed when active/inactive.
  • +
+ + + + + +# Light-Mint Accounts + + +* **Light-mints are compressed accounts** and **cannot be decompressed**. +* SPL mints can not be compressed to light-mints. + + +Light-mints **uniquely represent a token on Solana and store its global metadata**, similar to SPL mint accounts with few core differences: +1. Light-mint accounts are **rent-free**. +2. Tokens created from light-mints are **light-tokens**. +3. Token metadata (name, symbol, URI) is stored as an extension in the struct. + + + + + Diagram showing light-mint compressed account structure with three components: Hash (identifier for light-mint in purple box), Account (struct containing BaseMint with SPL-compatible fields, light-mint Data for program state, and optional Extensions for Token Metadata), and BasemintData (containing Supply, Decimals, Mint Authority, and Freeze Authority fields) with Token Metadata extension + + + + ```rust + pub struct CompressedMint { + // SPL mint layout + pub base: BaseMint, + // light-mint state used by the Compressed Token Program + pub metadata: CompressedMintMetadata + // Field for Token Metadata extension + pub extensions: Option>, + } + ``` + + + + +Find the [source code of light-mints here](https://github.com/Lightprotocol/light-protocol/blob/main/program-libs/ctoken-types/src/state/mint/compressed_mint.rs). + + +The `metadata` field is used by the Compressed Token Program to store the internal state of a light-mint. + +The `BaseMint` field replicates the field layout and serialization format of [SPL Mint accounts](https://solana.com/docs/tokens#mint-account). The struct is serialized with Borsh to match the on-chain format of SPL tokens and mints. + +Here is how light-mints and SPL mints compare: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldLight-MintSPL Mint
mint_authority
supply
decimals
is_initialized
freeze_authority
Light-Mint Data-
Extensionsvia Token-2022
+
+ + ```rust + pub struct BaseMint { + /// Optional authority used to mint new tokens. The mint authority may only + /// be provided during mint creation. If no mint authority is present + /// then the mint has a fixed supply and no further tokens may be + /// minted. + pub mint_authority: Option, + /// Total supply of tokens. + pub supply: u64, + /// Number of base 10 digits to the right of the decimal place. + pub decimals: u8, + /// Is initialized - for SPL compatibility + pub is_initialized: bool, + /// Optional authority to freeze token accounts. + pub freeze_authority: Option, + } + ``` + +
+ +# Light-Token Account + + +**Light-token accounts are Solana accounts**, not compressed accounts. + + +A light-token account holds token balances like SPL Token accounts: +* A wallet needs a light-token account for each light-mint, SPL mint, or Token 2022 mint it wants to hold, with the wallet address set as the light-token account owner. +* Each wallet can own multiple light-token accounts for the same light-mint. +* A light-token account can only have one owner and hold units of one light-mint. + + + + + Diagram showing light-token Solana account structure with three components: Address (identifier for light-token account in purple box), Account (struct containing Data bytes, Executable flag, Lamports balance, and Owner set to Compressed Token Program), and AccountData (containing Mint, Owner, Amount, and Extensions fields) + + + + ```rust + /// Ctoken account structure (with extensions). + pub struct CToken { + pub mint: Pubkey, + pub owner: Pubkey, + pub amount: u64, + pub delegate: Option, // instruction not implemented yet + pub state: u8, + pub is_native: Option, + pub delegated_amount: u64, // instruction not implemented yet + pub close_authority: Option, + pub extensions: Option>, // Optional extensions e.g. compressible + } + ``` + + + + +Find the [source code of light-tokens here](https://github.com/Lightprotocol/light-protocol/blob/main/program-libs/ctoken-types/src/state/ctoken/ctoken_struct.rs). + + +Light-token accounts replicate the field layout and serialization format of [SPL Token accounts](https://solana.com/docs/tokens#token-account). The struct is serialized with Borsh to match the on-chain format of SPL tokens. + +Here is how light-tokens and SPL tokens compare: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldLight-TokenSPL Token Account
mint
owner
amount
delegateunimplemented
state
is_native
delegated_amountunimplemented
close_authority
extensionsvia Token-2022
+ +### Rent Config for Light-Tokens + + + + +We recommend to use default values, but you can customize prepaid rent and top ups: + + + +# Associated Light-Token Account + +**Associated light-token** accounts (light-ATAs) follow the same pattern as [associated token accounts](https://docs.solana.com/developing/programming-model/associated-token-account) (ATA): +* Each wallet needs its own light-token account to hold tokens from the same light-mint. +* The address for light-ATAs is deterministically derived with the owner's address, light-token program ID, and mint address. + +```rust +let seeds = [ + owner.as_ref(), // Wallet address (32 bytes) + program_id.as_ref(), // Compressed Token Program ID (32 bytes) + mint.as_ref(), // light-mint address (32 bytes) + bump.as_ref(), // Bump seed (1 byte) +]; +``` + + +Find the [source code to associated light-token accounts here](https://github.com/Lightprotocol/light-protocol/blob/main/programs/compressed-token/program/src/create_associated_token_account.rs). + + +# Compressed Token Account + +Compressed token accounts store token balance, owner, and other information like SPL and light-tokens. Any light-token or SPL token can be compressed/decompressed at will. + +We recommend to use compressed tokens for **token distribution** or **storage of inactive tokens**. + + +**Light-token accounts** with the **compressible extension are automatically compressed** with no writes in 27,000 slots (3h) and decompressed with new writes. + + + + + + Diagram showing compressed token account structure with three components: Hash (identifier for compressed token account in purple box), Account (struct containing Data bytes, Executable flag, Lamports balance, and Address set to None), and AccountData (containing Mint, Owner, Amount, and Extensions fields marked as unimplemented) + + + + ```rust + pub struct TokenData { + pub mint: Pubkey, + pub owner: Pubkey, + pub amount: u64, + pub delegate: Option, + pub state: u8, + /// Placeholder for TokenExtension tlv data (unimplemented) + pub tlv: Option>, + } + ``` + + + +# Next Steps + + + \ No newline at end of file diff --git a/light-token/README.mdx b/light-token/README.mdx new file mode 100644 index 00000000..cea78862 --- /dev/null +++ b/light-token/README.mdx @@ -0,0 +1,109 @@ +--- +title: Overview +description: The light-token standard introduces high performance mint and token accounts without rent-exemption but full SPL functionality. + +--- + +import GuidesTable from '/snippets/overview-tables/compressed-tokens-guides-table.mdx'; +import AdvancedGuidesTable from '/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import { CTokenVsSplCalculator } from '/snippets/jsx/light-token-vs-spl-calculator.jsx'; +import { CompressibleRentCalculator } from '/snippets/jsx/compressible-rent-calculator.jsx'; +import { RentLifecycleVisualizer } from '/snippets/jsx/rent-lifecycle-visualizer.jsx'; +import CompressibleRentExplained from '/snippets/compressible-rent-explained.mdx'; +import CompressibleDefaultRentConfig from '/snippets/compressible-default-rent-config.mdx'; +import CTokenGuidesTable from '/snippets/overview-tables/light-token-guides-table.mdx'; +import CMintGuidesTable from '/snippets/overview-tables/light-mint-guides-table.mdx'; +import IntegrateCTokenGuidesTable from '/snippets/overview-tables/integrate-light-token-guides-table.mdx'; + +1. Light-mints and token accounts are equivalent to SPL mints and tokens. +2. The key difference is light-mint and token accounts do not require you to pay rent-exemption upon creation. +3. Light-token accounts are interoperable with SPL and Token 2022 mints. + +| Creation Cost | SPL | Light | +|:---------------------|:------------------|:----------------------| +| **Mint Account** | ~1,500,000 lamports | **15,000** lamports | +| **Token Account** | ~2,000,000 lamports | ~**17,000** lamports | + + + + + # Mint Accounts + * Uniquely represent a token and store its global metadata. + * light-mints are compressed accounts and rent-free. + + + # Token Accounts + * Each light-token account can hold units of one light, SPL, or Token 2022 mint. + * Custom rent config reduces account creation cost. + + + + +If you simply want to **distribute tokens**, please refer to [this page](/compressed-tokens). + + +# Start building + + + + +### Install dependencies + +```bash +npm install @lightprotocol/compressed-token/unified \ + @lightprotocol/stateless.js +``` + + + + +### Set up developer environment + + + +```typescript +import { createRpc } from "@lightprotocol/stateless.js"; + +import { + getOrCreateAtaInterface, + getAtaInterface, + getAssociatedTokenAddressInterface, + transferInterface, + unwrap, +} from "@lightprotocol/compressed-token/unified"; + +const rpc = createRpc(RPC_ENDPOINT); +``` + + + + + + +### Get Started + + + +## Integration Toolkits + + + +## Cookbook + +### Mint Accounts + + +### Token Accounts + + +# Next Steps + + \ No newline at end of file diff --git a/light-token/cookbook/close-token-account.mdx b/light-token/cookbook/close-token-account.mdx new file mode 100644 index 00000000..8865dcf3 --- /dev/null +++ b/light-token/cookbook/close-token-account.mdx @@ -0,0 +1,381 @@ +--- +title: Close Light-Token Account +sidebarTitle: Close Token Account +description: Program guide to close light-token accounts via CPI. Includes step-by-step implementation and full code examples. +--- + +import CloseAccountInfosAccountsList from '/snippets/accounts-list/close-account-infos-accounts-list.mdx'; +import CTokenClientPrerequisites from '/snippets/light-token-guides/light-token-client-prerequisites.mdx'; +import ClientCustomRentConfig from '/snippets/light-token-guides/client-custom-rent-config.mdx'; + +1. Closing a light-token account transfers remaining lamports to a destination account and the rent sponsor can reclaim sponsored rent. +2. Light-token accounts can be closed + * by the account owner at any time. + * by the `compression_authority` + when the account becomes compressible. The account is compressed and closed - it can be reinstated with the same state (decompressed). + +# Get Started + + + + +1. The example creates a light-token account and mint. +2. Build the instruction with `CloseCTokenAccount`: +```rust +let close_instruction = CloseCTokenAccount::new( + CTOKEN_PROGRAM_ID, + account.pubkey(), + payer.pubkey(), // Destination for remaining lamports + owner, +) +.instruction() +``` + + + +### Prerequisites + + + + + + +### Close light-token Account + +```rust +use borsh::BorshDeserialize; +use light_client::indexer::{AddressWithTree, Indexer}; +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; +use light_ctoken_sdk::ctoken::{ + CloseCTokenAccount, CreateCMint, CreateCMintParams, CreateCTokenAccount, CTOKEN_PROGRAM_ID, +}; +use light_ctoken_interface::state::CToken; +use serde_json; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; +use std::convert::TryFrom; +use std::env; +use std::fs; + +#[tokio::test(flavor = "multi_thread")] +async fn test_close_ctoken_account() { + dotenvy::dotenv().ok(); + + let keypair_path = env::var("KEYPAIR_PATH") + .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap())); + let payer = load_keypair(&keypair_path).expect("Failed to load keypair"); + + let api_key = env::var("api_key") // Set api_key in your .env + .expect("api_key environment variable must be set"); + + let config = LightClientConfig::devnet( + Some("https://devnet.helius-rpc.com".to_string()), + Some(api_key), + ); + let mut rpc = LightClient::new_with_retry(config, None) + .await + .expect("Failed to initialize LightClient"); + + // Step 1: Create compressed mint (prerequisite) + let (mint, _compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await; + + // Step 2: Create cToken account with 0 balance + let account = Keypair::new(); + let owner = payer.pubkey(); + + let create_instruction = + CreateCTokenAccount::new(payer.pubkey(), account.pubkey(), mint, owner) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_instruction], &payer.pubkey(), &[&payer, &account]) + .await + .unwrap(); + + // Step 3: Verify account exists before closing + let account_before_close = rpc.get_account(account.pubkey()).await.unwrap(); + assert!( + account_before_close.is_some(), + "Account should exist before closing" + ); + + let ctoken_state = + CToken::deserialize(&mut &account_before_close.unwrap().data[..]).unwrap(); + assert_eq!(ctoken_state.amount, 0, "Account balance must be 0 to close"); + + // Step 4: Build close instruction using SDK builder + let close_instruction = CloseCTokenAccount::new( + CTOKEN_PROGRAM_ID, + account.pubkey(), + payer.pubkey(), // Destination for remaining lamports + owner, + ) + .instruction() + .unwrap(); + + // Step 5: Send close transaction + rpc.create_and_send_transaction(&[close_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Step 6: Verify account is closed + let account_after_close = rpc.get_account(account.pubkey()).await.unwrap(); + assert!( + account_after_close.is_none(), + "Account should be closed and no longer exist" + ); +} + +pub async fn create_compressed_mint( + rpc: &mut R, + payer: &Keypair, + decimals: u8, +) -> (Pubkey, [u8; 32]) { + let mint_signer = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + + // Fetch active state trees for devnet + let _ = rpc.get_latest_active_state_trees().await; + let output_pubkey = match rpc + .get_random_state_tree_info() + .ok() + .or_else(|| rpc.get_random_state_tree_info_v1().ok()) + { + Some(info) => info + .get_output_pubkey() + .expect("Invalid state tree type for output"), + None => { + let queues = rpc + .indexer_mut() + .expect("IndexerNotInitialized") + .get_queue_info(None) + .await + .expect("Failed to fetch queue info") + .value + .queues; + queues + .get(0) + .map(|q| q.queue) + .expect("NoStateTreesAvailable: no active state trees returned") + } + }; + + // Derive compression address + let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( + &mint_signer.pubkey(), + &address_tree.tree, + ); + + let mint_pda = light_ctoken_sdk::ctoken::find_cmint_address(&mint_signer.pubkey()).0; + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params + let params = CreateCMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda, + freeze_authority: None, + extensions: None, + }; + + // Create instruction + let create_cmint = CreateCMint::new( + params, + mint_signer.pubkey(), + payer.pubkey(), + address_tree.tree, + output_pubkey, + ); + let instruction = create_cmint.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_signer]) + .await + .unwrap(); + + (mint_pda, compression_address) +} + +fn load_keypair(path: &str) -> Result> { + let path = if path.starts_with("~") { + path.replace("~", &env::var("HOME").unwrap_or_default()) + } else { + path.to_string() + }; + let file = fs::read_to_string(&path)?; + let bytes: Vec = serde_json::from_str(&file)?; + Ok(Keypair::try_from(&bytes[..])?) +} +``` + + + + + + + +Find [a full code example at the end](#full-code-example). + + + + +### Build Account Infos and CPI the light-token Program + +1. Define the light-token account to close and where remaining lamports should go +2. Use `invoke` or `invoke_signed`, when a CPI requires a PDA signer. + + + + +```rust +use light_ctoken_sdk::ctoken::CloseCTokenAccountCpi; + +CloseCTokenAccountCpi { + token_program: token_program.clone(), + account: account.clone(), + destination: destination.clone(), + owner: owner.clone(), + rent_sponsor: Some(rent_sponsor.clone()), +} +.invoke()?; +``` + + + + +```rust +use light_ctoken_sdk::ctoken::CloseCTokenAccountCpi; + +let close_account_cpi = CloseCTokenAccountCpi { + token_program: token_program.clone(), + account: account.clone(), + destination: destination.clone(), + owner: owner.clone(), + rent_sponsor: Some(rent_sponsor.clone()), +}; + +let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; +close_account_cpi.invoke_signed(&[signer_seeds])?; +``` + + + + + + + + + + + +# Full Code Example + + +Find the source code [here](https://github.com/Lightprotocol/program-examples/blob/main/light-token/src/close.rs). + + +```rust expandable +use light_ctoken_sdk::ctoken::CloseCTokenAccountCpi; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Handler for closing a compressed token account (invoke) +/// +/// Account order: +/// - accounts[0]: token_program (ctoken program) +/// - accounts[1]: account to close (writable) +/// - accounts[2]: destination for lamports (writable) +/// - accounts[3]: owner/authority (signer) +/// - accounts[4]: rent_sponsor (optional, writable) +pub fn process_close_account_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let rent_sponsor = if accounts.len() > 4 { + Some(accounts[4].clone()) + } else { + None + }; + + CloseCTokenAccountCpi { + token_program: accounts[0].clone(), + account: accounts[1].clone(), + destination: accounts[2].clone(), + owner: accounts[3].clone(), + rent_sponsor, + } + .invoke()?; + + Ok(()) +} + +/// Handler for closing a PDA-owned compressed token account (invoke_signed) +/// +/// Account order: +/// - accounts[0]: token_program (ctoken program) +/// - accounts[1]: account to close (writable) +/// - accounts[2]: destination for lamports (writable) +/// - accounts[3]: PDA owner/authority (not signer, program signs) +/// - accounts[4]: rent_sponsor (optional, writable) +pub fn process_close_account_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (pda, bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the authority account is the PDA we expect + if &pda != accounts[3].key { + return Err(ProgramError::InvalidSeeds); + } + + let rent_sponsor = if accounts.len() > 4 { + Some(accounts[4].clone()) + } else { + None + }; + + let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; + CloseCTokenAccountCpi { + token_program: accounts[0].clone(), + account: accounts[1].clone(), + destination: accounts[2].clone(), + owner: accounts[3].clone(), + rent_sponsor, + } + .invoke_signed(&[signer_seeds])?; + + Ok(()) +} +``` + + + + +# Next Steps + + diff --git a/light-token/cookbook/create-ata.mdx b/light-token/cookbook/create-ata.mdx new file mode 100644 index 00000000..dd04466a --- /dev/null +++ b/light-token/cookbook/create-ata.mdx @@ -0,0 +1,414 @@ +--- +title: Create Associated Light-Token Accounts +sidebarTitle: Create Associated Token Account +description: Client and program guide to create associated light-token accounts via CPI. Includes step-by-step implementation and full code examples. +--- + +import CTokenCreateATAAccountsList from '/snippets/accounts-list/light-token-create-ata-accounts-list.mdx'; +import CompressibleVsSolanaRent from '/snippets/compressible-vs-solana-rent.mdx'; +import CTokenConfigureRent from '/snippets/light-token-configure-rent.mdx'; +import CAtaIntro from '/snippets/light-token-guides/cata-intro.mdx'; +import CompressibleRentExplained from '/snippets/compressible-rent-explained.mdx'; +import CTokenClientPrerequisites from '/snippets/light-token-guides/light-token-client-prerequisites.mdx'; +import ClientCustomRentConfig from '/snippets/light-token-guides/client-custom-rent-config.mdx'; + +1. Associated light-token accounts are Solana accounts that hold token balances of light, SPL, or Token 2022 mints. +2. The address for light-ATAs is deterministically derived with the owner's address, compressed token program ID, and mint address. +2. Associated light-token accounts implement a default rent config: + 1. At account creation, you pay ~17,208 lamports for 24h of rent
and compression incentive (the rent-exemption is sponsored by the protocol) + 2. Transfers keep the account funded with rent for 3h via top-ups. The transaction payer tops up 776 lamports when the account's rent is below 3h. + + + + + +# Get Started + + + + + +1. The example creates a test light-mint. You can use existing light-mints, SPL or Token 2022 mints as well. +2. Derive the address from mint and owner pubkey. +3. Build the instruction with `CreateAssociatedCTokenAccount`. It automatically includes the default rent config: +```rust +use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccount; + +let instruction = CreateAssociatedCTokenAccount::new( + payer.pubkey(), + owner, + mint, +) +.instruction()?; +``` + +4. Send transaction & verify light-ATA creation with `get_account`. + + + + +### Prerequisites + + + + + + + +### Create light-ATA + +```rust +use borsh::BorshDeserialize; +use light_client::indexer::{AddressWithTree, Indexer}; +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; +use light_ctoken_sdk::ctoken::{ + derive_ctoken_ata, CreateAssociatedCTokenAccount, CreateCMint, + CreateCMintParams, +}; +use light_ctoken_interface::state::CToken; +use serde_json; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; +use std::convert::TryFrom; +use std::env; +use std::fs; + +#[tokio::test(flavor = "multi_thread")] +async fn test_create_associated_token_account() { + dotenvy::dotenv().ok(); + + let keypair_path = env::var("KEYPAIR_PATH") + .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap())); + let payer = load_keypair(&keypair_path).expect("Failed to load keypair"); + + let api_key = env::var("api_key") + .expect("api_key environment variable must be set"); + + let config = LightClientConfig::devnet( + Some("https://devnet.helius-rpc.com".to_string()), + Some(api_key), + ); + let mut rpc = LightClient::new_with_retry(config, None) + .await + .expect("Failed to initialize LightClient"); + + // Step 1: Create compressed mint (prerequisite) + let (mint, _compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await; + + // Step 2: Define owner and derive ATA address + let owner = payer.pubkey(); + let (ata_address, _bump) = derive_ctoken_ata(&owner, &mint); + + // Step 3: Build instruction using SDK builder + let instruction = CreateAssociatedCTokenAccount::new( + payer.pubkey(), + owner, + mint, + ) + .instruction() + .unwrap(); + + // Step 4: Send transaction (only payer signs, no account keypair needed) + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Step 5: Verify light-ATA creation + let account_data = rpc.get_account(ata_address).await.unwrap().unwrap(); + let ctoken_state = CToken::deserialize(&mut &account_data.data[..]).unwrap(); + + assert_eq!(ctoken_state.mint, mint.to_bytes(), "Mint should match"); + assert_eq!(ctoken_state.owner, owner.to_bytes(), "Owner should match"); + assert_eq!(ctoken_state.amount, 0, "Initial amount should be 0"); +} + +pub async fn create_compressed_mint( + rpc: &mut R, + payer: &Keypair, + decimals: u8, +) -> (Pubkey, [u8; 32]) { + let mint_signer = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + + let _ = rpc.get_latest_active_state_trees().await; + let output_pubkey = match rpc + .get_random_state_tree_info() + .ok() + .or_else(|| rpc.get_random_state_tree_info_v1().ok()) + { + Some(info) => info + .get_output_pubkey() + .expect("Invalid state tree type for output"), + None => { + let queues = rpc + .indexer_mut() + .expect("IndexerNotInitialized") + .get_queue_info(None) + .await + .expect("Failed to fetch queue info") + .value + .queues; + queues + .get(0) + .map(|q| q.queue) + .expect("NoStateTreesAvailable") + } + }; + + // Derive compression address + let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( + &mint_signer.pubkey(), + &address_tree.tree, + ); + + let mint_pda = + light_ctoken_sdk::ctoken::find_cmint_address(&mint_signer.pubkey()).0; + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params + let params = CreateCMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda, + freeze_authority: None, + extensions: None, + }; + + // Create instruction + let create_cmint = CreateCMint::new( + params, + mint_signer.pubkey(), + payer.pubkey(), + address_tree.tree, + output_pubkey, + ); + let instruction = create_cmint.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_signer]) + .await + .unwrap(); + + (mint_pda, compression_address) +} + +fn load_keypair(path: &str) -> Result> { + let path = if path.starts_with("~") { + path.replace("~", &env::var("HOME").unwrap_or_default()) + } else { + path.to_string() + }; + let file = fs::read_to_string(&path)?; + let bytes: Vec = serde_json::from_str(&file)?; + Ok(Keypair::try_from(&bytes[..])?) +} +``` + + + + + + + + +Find [a full code example at the end](#full-code-example). + + + + +### Define Rent Config Accounts + + + + + + + +### Build Account Infos and CPI the light-token Program + +1. Pass the required accounts that include the rent config. +2. Use `invoke` or `invoke_signed`, when a CPI requires a PDA signer. + +The light-ATA address is derived from `[owner, ctoken_program_id, mint]`. Unlike light-token accounts, owner and mint are passed as accounts, not in instruction data. + + + + + +```rust +use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccountCpi; + +CreateAssociatedCTokenAccountCpi { + owner: owner.clone(), + mint: mint.clone(), + payer: payer.clone(), + associated_token_account: associated_token_account.clone(), + system_program: system_program.clone(), + bump: data.bump, + compressible: Some(compressible_params), + idempotent: false, +} +.invoke()?; +``` + + + + +```rust +use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccountCpi; + +let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; +CreateAssociatedCTokenAccountCpi { + owner: owner.clone(), + mint: mint.clone(), + payer: payer.clone(), + associated_token_account: associated_token_account.clone(), + system_program: system_program.clone(), + bump: data.bump, + compressible: Some(compressible_params), + idempotent: false, +} +.invoke_signed(&[signer_seeds])?; +``` + + + + + + + +# Full Code Example + + +Find the source code [here](https://github.com/Lightprotocol/program-examples/blob/main/light-token/src/create_ata.rs). + + +```rust expandable +use borsh::{BorshDeserialize, BorshSerialize}; +use light_ctoken_sdk::ctoken::{CompressibleParamsCpi, CreateAssociatedCTokenAccountCpi}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::{ATA_SEED, ID}; + +/// Instruction data for create ATA V2 (owner/mint as accounts) +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateAta2Data { + pub bump: u8, + pub pre_pay_num_epochs: u8, + pub lamports_per_write: u32, +} + +/// Handler for creating ATA using V2 variant (invoke) +/// +/// Account order: +/// - accounts[0]: owner (readonly) +/// - accounts[1]: mint (readonly) +/// - accounts[2]: payer (signer, writable) +/// - accounts[3]: associated_token_account (writable) +/// - accounts[4]: system_program +/// - accounts[5]: compressible_config +/// - accounts[6]: rent_sponsor (writable) +pub fn process_create_ata2_invoke( + accounts: &[AccountInfo], + data: CreateAta2Data, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let compressible_params = CompressibleParamsCpi::new( + accounts[5].clone(), + accounts[6].clone(), + accounts[4].clone(), + ); + + CreateAssociatedCTokenAccountCpi { + owner: accounts[0].clone(), + mint: accounts[1].clone(), + payer: accounts[2].clone(), + associated_token_account: accounts[3].clone(), + system_program: accounts[4].clone(), + bump: data.bump, + compressible: Some(compressible_params), + idempotent: false, + } + .invoke()?; + + Ok(()) +} + +/// Handler for creating ATA using V2 variant with PDA ownership (invoke_signed) +/// +/// Account order: +/// - accounts[0]: owner (PDA, readonly) +/// - accounts[1]: mint (readonly) +/// - accounts[2]: payer (PDA, writable, not signer - program signs) +/// - accounts[3]: associated_token_account (writable) +/// - accounts[4]: system_program +/// - accounts[5]: compressible_config +/// - accounts[6]: rent_sponsor (writable) +pub fn process_create_ata2_invoke_signed( + accounts: &[AccountInfo], + data: CreateAta2Data, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA that will act as payer + let (pda, bump) = Pubkey::find_program_address(&[ATA_SEED], &ID); + + // Verify the payer is the PDA + if &pda != accounts[2].key { + return Err(ProgramError::InvalidSeeds); + } + + let compressible_params = CompressibleParamsCpi::new( + accounts[5].clone(), + accounts[6].clone(), + accounts[4].clone(), + ); + + let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; + CreateAssociatedCTokenAccountCpi { + owner: accounts[0].clone(), + mint: accounts[1].clone(), + payer: accounts[2].clone(), // PDA + associated_token_account: accounts[3].clone(), + system_program: accounts[4].clone(), + bump: data.bump, + compressible: Some(compressible_params), + idempotent: false, + } + .invoke_signed(&[signer_seeds])?; + + Ok(()) +} +``` + + + +# Next Steps + + diff --git a/light-token/cookbook/create-mint.mdx b/light-token/cookbook/create-mint.mdx new file mode 100644 index 00000000..d3080b5e --- /dev/null +++ b/light-token/cookbook/create-mint.mdx @@ -0,0 +1,674 @@ +--- +title: Create Mint Account wih Token Metadata +sidebarTitle: Create Mint Account +description: Program and client guides to create a mint with token metadata. Includes step-by-step implementation and full code examples. +--- + +import CMintSystemAccountsList from '/snippets/accounts-list/light-mint-system-accounts-list.mdx'; +import CTokenClientPrerequisites from '/snippets/light-token-guides/light-token-client-prerequisites.mdx'; +import ClientCustomRentConfig from '/snippets/light-token-guides/client-custom-rent-config.mdx'; + +1. Mint accounts uniquely represent a token on Solana and store its global metadata. +2. Mints for light-token accounts are compressed accounts and rent-free. + + +Learn the [core concepts to the light-token standard here](/light-token/README). + + +# Get Started + + + + + +The example creates a light-mint with token metadata. +1. Derive the mint address from the mint signer and address tree +2. Fetch a validity proof from your RPC that proves the address does not exist yet. + +3. Configure mint and your token metadata (name, symbol, URI, additional metadata) +4. Build the instruction with `CreateCMint::new()` and send the transaction. + +```rust +use light_ctoken_sdk::ctoken::CreateCMint; + +let create_cmint = CreateCMint::new( + params, + mint_signer.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, +); +let instruction = create_cmint.instruction()?; +``` + + + +### Prerequisites + + + + + + +### Create light-mint with Token Metadata + +```rust +use light_client::indexer::{AddressWithTree, Indexer}; +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; +use light_ctoken_sdk::ctoken::{CreateCMint, CreateCMintParams}; +use light_ctoken_interface::instructions::extensions::token_metadata::TokenMetadataInstructionData; +use light_ctoken_interface::instructions::extensions::ExtensionInstructionData; +use light_ctoken_interface::state::AdditionalMetadata; +use serde_json; +use solana_sdk::{bs58, pubkey::Pubkey, signature::Keypair, signer::Signer}; +use std::convert::TryFrom; +use std::env; +use std::fs; + +#[tokio::test(flavor = "multi_thread")] +async fn test_create_compressed_mint_with_metadata() { + dotenvy::dotenv().ok(); + + let keypair_path = env::var("KEYPAIR_PATH") + .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap())); + let payer = load_keypair(&keypair_path).expect("Failed to load keypair"); + + let api_key = env::var("api_key") // Set api_key in your .env + .expect("api_key environment variable must be set"); + + let config = LightClientConfig::devnet( + Some("https://devnet.helius-rpc.com".to_string()), + Some(api_key), + ); + let mut rpc = LightClient::new_with_retry(config, None) + .await + .expect("Failed to initialize LightClient"); + + // Create compressed mint with metadata + let (mint_pda, compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await; + + println!("\n=== Created Compressed Mint ==="); + println!("Mint PDA: {}", mint_pda); + println!("Compression Address: {}", bs58::encode(compression_address).into_string()); + println!("Decimals: 9"); + println!("Name: Example Token"); + println!("Symbol: EXT"); + println!("URI: https://example.com/metadata.json"); +} + +pub async fn create_compressed_mint( + rpc: &mut R, + payer: &Keypair, + decimals: u8, +) -> (Pubkey, [u8; 32]) { + let mint_signer = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + + // Fetch active state trees for devnet + let _ = rpc.get_latest_active_state_trees().await; + let output_pubkey = match rpc + .get_random_state_tree_info() + .ok() + .or_else(|| rpc.get_random_state_tree_info_v1().ok()) + { + Some(info) => info + .get_output_pubkey() + .expect("Invalid state tree type for output"), + None => { + let queues = rpc + .indexer_mut() + .expect("IndexerNotInitialized") + .get_queue_info(None) + .await + .expect("Failed to fetch queue info") + .value + .queues; + queues + .get(0) + .map(|q| q.queue) + .expect("NoStateTreesAvailable: no active state trees returned") + } + }; + + // Derive compression address + let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( + &mint_signer.pubkey(), + &address_tree.tree, + ); + + let mint_pda = light_ctoken_sdk::ctoken::find_cmint_address(&mint_signer.pubkey()).0; + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params with token metadata + let params = CreateCMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda, + freeze_authority: None, + extensions: Some(vec![ExtensionInstructionData::TokenMetadata( + TokenMetadataInstructionData { + update_authority: Some(payer.pubkey().to_bytes().into()), + name: b"Example Token".to_vec(), + symbol: b"EXT".to_vec(), + uri: b"https://example.com/metadata.json".to_vec(), + additional_metadata: Some(vec![AdditionalMetadata { + key: b"type".to_vec(), + value: b"compressed".to_vec(), + }]), + }, + )]), + }; + + // Create instruction + let create_cmint = CreateCMint::new( + params, + mint_signer.pubkey(), + payer.pubkey(), + address_tree.tree, + output_pubkey, + ); + let instruction = create_cmint.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_signer]) + .await + .unwrap(); + + (mint_pda, compression_address) +} + +fn load_keypair(path: &str) -> Result> { + let path = if path.starts_with("~") { + path.replace("~", &env::var("HOME").unwrap_or_default()) + } else { + path.to_string() + }; + let file = fs::read_to_string(&path)?; + let bytes: Vec = serde_json::from_str(&file)?; + Ok(Keypair::try_from(&bytes[..])?) +} +``` + + + + + + + +Find [a full code example at the end](#full-code-example). + + + + +### Configure Token Metadata + +```rust +use light_ctoken_interface::{ + instructions::extensions::{ + token_metadata::TokenMetadataInstructionData, + ExtensionInstructionData, + }, + state::AdditionalMetadata, +}; + +let token_metadata = ExtensionInstructionData::TokenMetadata( + TokenMetadataInstructionData { + update_authority: Some(authority.to_bytes().into()), + name: b"My Token".to_vec(), + symbol: b"MTK".to_vec(), + uri: b"https://example.com/metadata.json".to_vec(), + additional_metadata: Some(vec![ + AdditionalMetadata { + key: b"category".to_vec(), + value: b"utility".to_vec(), + }, + ]), + }, +); +``` + + +**Fields must be set at light-mint creation.** +* Standard fields (`name`, `symbol`, `uri`) can be updated by `update_authority`. +* For `additional_metadata`, only existing keys can be modified or removed. New keys cannot be added after creation. + + + + + +### Configure light-mint + +Set `decimals`, `mint_authority`, `freeze_authority`, and pass the `token_metadata` from the previous step. + +```rust +use light_ctoken_sdk::ctoken::CreateCMintParams; + +let params = CreateCMintParams { + decimals: data.decimals, + address_merkle_tree_root_index: data.address_merkle_tree_root_index, + mint_authority: data.mint_authority, + proof: data.proof, + compression_address: data.compression_address, + mint: data.mint, + freeze_authority: data.freeze_authority, + extensions: data.extensions, +}; +``` + +* The client passes a validity proof that proves the light-mint address does not exist in the address tree where it will be stored. +* You can safely ignore `compression_address` and `address_merkle_tree_root_index`. The client passes these for proof verification. + + + + + + +### System Accounts + +Include system accounts such as the Light System Program required to interact with compressed state. +The client includes them in the instruction. + + + + + +```rust +use light_ctoken_sdk::ctoken::SystemAccountInfos; + +let system_accounts = SystemAccountInfos { + light_system_program: light_system_program.clone(), + cpi_authority_pda: cpi_authority_pda.clone(), + registered_program_pda: registered_program_pda.clone(), + account_compression_authority: account_compression_authority.clone(), + account_compression_program: account_compression_program.clone(), + system_program: system_program.clone(), +}; +``` + + + + +### Build Account Infos and CPI the light-token Program + +1. Pass the required accounts +2. Include `params` and `system_accounts` from the previous steps +3. Use `invoke` or `invoke_signed`: + * When `mint_seed` is an external keypair, use `invoke`. + * When `mint_seed` is a PDA, use `invoke_signed` with its seeds. + * When both `mint_seed` and `authority` are PDAs, use `invoke_signed` with both seeds. + + + + + +```rust +use light_ctoken_sdk::ctoken::CreateCMintCpi; + +CreateCMintCpi { + mint_seed: mint_seed.clone(), + authority: authority.clone(), + payer: payer.clone(), + address_tree: address_tree.clone(), + output_queue: output_queue.clone(), + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, +} +.invoke()?; +``` + + + + +```rust +use light_ctoken_sdk::ctoken::CreateCMintCpi; + +let account_infos = CreateCMintCpi { + mint_seed: mint_seed.clone(), + authority: authority.clone(), + payer: payer.clone(), + address_tree: address_tree.clone(), + output_queue: output_queue.clone(), + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, +}; + +let signer_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[bump]]; +account_infos.invoke_signed(&[signer_seeds])?; +``` + + + + +```rust +use light_ctoken_sdk::ctoken::CreateCMintCpi; + +let account_infos = CreateCMintCpi { + mint_seed: mint_seed.clone(), + authority: authority.clone(), + payer: payer.clone(), + address_tree: address_tree.clone(), + output_queue: output_queue.clone(), + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, +}; + +let mint_seed_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[mint_seed_bump]]; +let authority_seeds: &[&[u8]] = &[MINT_AUTHORITY_SEED, &[authority_bump]]; +account_infos.invoke_signed(&[mint_seed_seeds, authority_seeds])?; +``` + + + + + + + + + +# Full Code Example + + +Find the source code [here](https://github.com/Lightprotocol/program-examples/blob/main/light-token/src/create_mint.rs). + + +```rust expandable +use borsh::{BorshDeserialize, BorshSerialize}; +use light_ctoken_sdk::{ + ctoken::{ + CreateCMintCpi, CreateCMintParams, ExtensionInstructionData, SystemAccountInfos, + }, + CompressedProof, +}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::ID; + +/// PDA seed for mint signer in invoke_signed variant +pub const MINT_SIGNER_SEED: &[u8] = b"mint_signer"; + +/// Instruction data for create compressed mint +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateCmintData { + pub decimals: u8, + pub address_merkle_tree_root_index: u16, + pub mint_authority: Pubkey, + pub proof: CompressedProof, + pub compression_address: [u8; 32], + pub mint: Pubkey, + pub freeze_authority: Option, + pub extensions: Option>, +} + +/// Handler for creating a compressed mint (invoke) +/// +/// Uses the CreateCMintCpi builder pattern. This demonstrates how to: +/// 1. Build the CreateCMintParams struct from instruction data +/// 2. Build the CreateCMintCpi with accounts +/// 3. Call invoke() which handles instruction building and CPI +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: mint_seed (signer) +/// - accounts[3]: payer (signer, also authority) +/// - accounts[4]: payer again (fee_payer in SDK) +/// - accounts[5]: cpi_authority_pda +/// - accounts[6]: registered_program_pda +/// - accounts[7]: account_compression_authority +/// - accounts[8]: account_compression_program +/// - accounts[9]: system_program +/// - accounts[10]: output_queue +/// - accounts[11]: address_tree +/// - accounts[12] (optional): cpi_context_account +pub fn process_create_cmint( + accounts: &[AccountInfo], + data: CreateCmintData, +) -> Result<(), ProgramError> { + if accounts.len() < 12 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Build the params + let params = CreateCMintParams { + decimals: data.decimals, + address_merkle_tree_root_index: data.address_merkle_tree_root_index, + mint_authority: data.mint_authority, + proof: data.proof, + compression_address: data.compression_address, + mint: data.mint, + freeze_authority: data.freeze_authority, + extensions: data.extensions, + }; + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: accounts[1].clone(), + cpi_authority_pda: accounts[5].clone(), + registered_program_pda: accounts[6].clone(), + account_compression_authority: accounts[7].clone(), + account_compression_program: accounts[8].clone(), + system_program: accounts[9].clone(), + }; + + // Build the account infos struct + // In this case, payer == authority (accounts[3]) + CreateCMintCpi { + mint_seed: accounts[2].clone(), + authority: accounts[3].clone(), + payer: accounts[3].clone(), + address_tree: accounts[11].clone(), + output_queue: accounts[10].clone(), + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + } + .invoke()?; + + Ok(()) +} + +/// Handler for creating a compressed mint with PDA mint seed (invoke_signed) +/// +/// Uses the CreateCMintCpi builder pattern with invoke_signed. +/// The mint_seed is a PDA derived from this program. +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: mint_seed (PDA, not signer - program signs) +/// - accounts[3]: payer (signer, also authority) +/// - accounts[4]: payer again (fee_payer in SDK) +/// - accounts[5]: cpi_authority_pda +/// - accounts[6]: registered_program_pda +/// - accounts[7]: account_compression_authority +/// - accounts[8]: account_compression_program +/// - accounts[9]: system_program +/// - accounts[10]: output_queue +/// - accounts[11]: address_tree +/// - accounts[12] (optional): cpi_context_account +pub fn process_create_cmint_invoke_signed( + accounts: &[AccountInfo], + data: CreateCmintData, +) -> Result<(), ProgramError> { + if accounts.len() < 12 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the mint seed + let (pda, bump) = Pubkey::find_program_address(&[MINT_SIGNER_SEED], &ID); + + // Verify the mint_seed account is the PDA we expect + if &pda != accounts[2].key { + return Err(ProgramError::InvalidSeeds); + } + + // Build the params + let params = CreateCMintParams { + decimals: data.decimals, + address_merkle_tree_root_index: data.address_merkle_tree_root_index, + mint_authority: data.mint_authority, + proof: data.proof, + compression_address: data.compression_address, + mint: data.mint, + freeze_authority: data.freeze_authority, + extensions: data.extensions, + }; + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: accounts[1].clone(), + cpi_authority_pda: accounts[5].clone(), + registered_program_pda: accounts[6].clone(), + account_compression_authority: accounts[7].clone(), + account_compression_program: accounts[8].clone(), + system_program: accounts[9].clone(), + }; + + // Build the account infos struct + // In this case, payer == authority (accounts[3]) + let account_infos = CreateCMintCpi { + mint_seed: accounts[2].clone(), + authority: accounts[3].clone(), + payer: accounts[3].clone(), + address_tree: accounts[11].clone(), + output_queue: accounts[10].clone(), + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + }; + + // Invoke with PDA signing + let signer_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[bump]]; + account_infos.invoke_signed(&[signer_seeds])?; + + Ok(()) +} + +/// Handler for creating a compressed mint with PDA mint seed AND PDA authority (invoke_signed) +/// +/// Uses the SDK's CreateCMintCpi with separate authority and payer accounts. +/// Both mint_seed and authority are PDAs signed by this program. +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: mint_seed (PDA from MINT_SIGNER_SEED, not signer - program signs) +/// - accounts[3]: authority (PDA from MINT_AUTHORITY_SEED, not signer - program signs) +/// - accounts[4]: fee_payer (signer) +/// - accounts[5]: cpi_authority_pda +/// - accounts[6]: registered_program_pda +/// - accounts[7]: account_compression_authority +/// - accounts[8]: account_compression_program +/// - accounts[9]: system_program +/// - accounts[10]: output_queue +/// - accounts[11]: address_tree +/// - accounts[12] (optional): cpi_context_account +pub fn process_create_cmint_with_pda_authority( + accounts: &[AccountInfo], + data: CreateCmintData, +) -> Result<(), ProgramError> { + use crate::mint_to::MINT_AUTHORITY_SEED; + + if accounts.len() < 12 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the mint seed + let (mint_seed_pda, mint_seed_bump) = + Pubkey::find_program_address(&[MINT_SIGNER_SEED], &ID); + + // Derive the PDA for the authority + let (authority_pda, authority_bump) = Pubkey::find_program_address(&[MINT_AUTHORITY_SEED], &ID); + + // Verify the mint_seed account is the PDA we expect + if &mint_seed_pda != accounts[2].key { + return Err(ProgramError::InvalidSeeds); + } + + // Verify the authority account is the PDA we expect + if &authority_pda != accounts[3].key { + return Err(ProgramError::InvalidSeeds); + } + + // Build the params - authority is the PDA + let params = CreateCMintParams { + decimals: data.decimals, + address_merkle_tree_root_index: data.address_merkle_tree_root_index, + mint_authority: authority_pda, // Use the derived PDA as authority + proof: data.proof, + compression_address: data.compression_address, + mint: data.mint, + freeze_authority: data.freeze_authority, + extensions: data.extensions, + }; + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: accounts[1].clone(), + cpi_authority_pda: accounts[5].clone(), + registered_program_pda: accounts[6].clone(), + account_compression_authority: accounts[7].clone(), + account_compression_program: accounts[8].clone(), + system_program: accounts[9].clone(), + }; + + // Build the account infos struct using SDK + let account_infos = CreateCMintCpi { + mint_seed: accounts[2].clone(), + authority: accounts[3].clone(), + payer: accounts[4].clone(), + address_tree: accounts[11].clone(), + output_queue: accounts[10].clone(), + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + }; + + // Invoke with both PDAs signing + let mint_seed_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[mint_seed_bump]]; + let authority_seeds: &[&[u8]] = &[MINT_AUTHORITY_SEED, &[authority_bump]]; + account_infos.invoke_signed(&[mint_seed_seeds, authority_seeds])?; + + Ok(()) +} +``` + + + + + +# Next Steps + + + diff --git a/light-token/cookbook/create-token-account.mdx b/light-token/cookbook/create-token-account.mdx new file mode 100644 index 00000000..c7e543da --- /dev/null +++ b/light-token/cookbook/create-token-account.mdx @@ -0,0 +1,398 @@ +--- +title: Create Light-Token Account +sidebarTitle: Create Token Account +description: Client and program guide to create light-token accounts. Includes step-by-step implementation and full code examples. +--- + +import CTokenCreateAccountsList from '/snippets/accounts-list/light-token-create-accounts-list.mdx'; +import CTokenConfigureRent from '/snippets/light-token-configure-rent.mdx'; +import CTokenClientPrerequisites from '/snippets/light-token-guides/light-token-client-prerequisites.mdx'; +import CompressibleRentExplained from '/snippets/compressible-rent-explained.mdx'; + +1. Light-token accounts are Solana accounts that hold token balances of light, SPL, or Token 2022 mints. +2. Light-token accounts implement a default rent config: + 1. At account creation, you pay ~17,208 lamports for 24h of rent
and compression incentive (the rent-exemption is sponsored by the protocol) + 2. Transfers keep the account funded with rent for 3h via top-ups. The transaction payer tops up 776 lamports when the account's rent is below 3h. + + + + + +# Get Started + + + + + + +1. The example creates a test mint for light-tokens. You can use existing light, SPL or Token 2022 mints as well. +2. Build the instruction with `CreateCTokenAccount`. It automatically includes the default rent config. +```rust +use light_ctoken_sdk::ctoken::{CreateCTokenAccount}; + +let instruction = CreateCTokenAccount::new( + payer.pubkey(), + account.pubkey(), + mint, + owner, +) +.instruction()?; +``` + +3. Send transaction & verify light-token account creation with `get_account`. + + + + +### Prerequisites + + + + + + +### Create light-token Account + +```rust +use borsh::BorshDeserialize; +use light_client::indexer::{AddressWithTree, Indexer}; +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; +use light_ctoken_sdk::ctoken::{CreateCMint, CreateCMintParams, CreateCTokenAccount}; +use light_ctoken_interface::state::CToken; +use serde_json; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; +use std::convert::TryFrom; +use std::env; +use std::fs; + +#[tokio::test(flavor = "multi_thread")] +async fn test_create_ctoken_account() { + dotenvy::dotenv().ok(); + + let keypair_path = env::var("KEYPAIR_PATH") + .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap())); + let payer = load_keypair(&keypair_path).expect("Failed to load keypair"); + + let api_key = env::var("api_key") // Set api_key in your .env + .expect("api_key environment variable must be set"); + + let config = LightClientConfig::devnet( + Some("https://devnet.helius-rpc.com".to_string()), + Some(api_key), + ); + let mut rpc = LightClient::new_with_retry(config, None) + .await + .expect("Failed to initialize LightClient"); + + // Step 1: Create compressed mint (prerequisite) + let (mint, _compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await; + + // Step 2: Generate new keypair for the cToken account + let account = Keypair::new(); + let owner = payer.pubkey(); + + // Step 3: Build instruction using SDK builder + let instruction = CreateCTokenAccount::new(payer.pubkey(), account.pubkey(), mint, owner) + .instruction() + .unwrap(); + + // Step 4: Send transaction (account keypair must sign) + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &account]) + .await + .unwrap(); + + // Step 5: Verify account creation + let account_data = rpc.get_account(account.pubkey()).await.unwrap().unwrap(); + let ctoken_state = CToken::deserialize(&mut &account_data.data[..]).unwrap(); + + assert_eq!(ctoken_state.mint, mint.to_bytes(), "Mint should match"); + assert_eq!(ctoken_state.owner, owner.to_bytes(), "Owner should match"); + assert_eq!(ctoken_state.amount, 0, "Initial amount should be 0"); +} + +pub async fn create_compressed_mint( + rpc: &mut R, + payer: &Keypair, + decimals: u8, +) -> (Pubkey, [u8; 32]) { + let mint_signer = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + + // Fetch active state trees for devnet + let _ = rpc.get_latest_active_state_trees().await; + let output_pubkey = match rpc + .get_random_state_tree_info() + .ok() + .or_else(|| rpc.get_random_state_tree_info_v1().ok()) + { + Some(info) => info + .get_output_pubkey() + .expect("Invalid state tree type for output"), + None => { + let queues = rpc + .indexer_mut() + .expect("IndexerNotInitialized") + .get_queue_info(None) + .await + .expect("Failed to fetch queue info") + .value + .queues; + queues + .get(0) + .map(|q| q.queue) + .expect("NoStateTreesAvailable: no active state trees returned") + } + }; + + // Derive compression address + let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( + &mint_signer.pubkey(), + &address_tree.tree, + ); + + let mint_pda = light_ctoken_sdk::ctoken::find_cmint_address(&mint_signer.pubkey()).0; + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params + let params = CreateCMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda, + freeze_authority: None, + extensions: None, + }; + + // Create instruction + let create_cmint = CreateCMint::new( + params, + mint_signer.pubkey(), + payer.pubkey(), + address_tree.tree, + output_pubkey, + ); + let instruction = create_cmint.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_signer]) + .await + .unwrap(); + + (mint_pda, compression_address) +} + +fn load_keypair(path: &str) -> Result> { + let path = if path.starts_with("~") { + path.replace("~", &env::var("HOME").unwrap_or_default()) + } else { + path.to_string() + }; + let file = fs::read_to_string(&path)?; + let bytes: Vec = serde_json::from_str(&file)?; + Ok(Keypair::try_from(&bytes[..])?) +} +``` + + + + + + + + + + +Find [a full code example at the end](#full-code-example). + + + + +### Configure Rent + + + + + + +### Build Account Infos and CPI the light-token Program + +1. Pass the required accounts +2. Include rent config from `compressible_params` +3. Use `invoke` or `invoke_signed`, when a CPI requires a PDA signer. + + + +```rust +use light_ctoken_sdk::ctoken::CreateCTokenAccountCpi; + +CreateCTokenAccountCpi { + payer: payer.clone(), + account: account.clone(), + mint: mint.clone(), + owner: data.owner, + compressible: Some(compressible_params), +} +.invoke()?; +``` + + + + + +```rust +use light_ctoken_sdk::ctoken::CreateCTokenAccountCpi; + +let account_cpi = CreateCTokenAccountCpi { + payer: payer.clone(), + account: account.clone(), + mint: mint.clone(), + owner: data.owner, + compressible: Some(compressible_params), +}; + +let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; +account_cpi.invoke_signed(&[signer_seeds])?; +``` + + + + + + + +# Full Code Example + + +Find the source code [here](https://github.com/Lightprotocol/program-examples/blob/main/light-token/src/create_token_account.rs). + + +```rust expandable +use borsh::{BorshDeserialize, BorshSerialize}; +use light_ctoken_sdk::ctoken::{CompressibleParamsCpi, CreateCTokenAccountCpi}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Instruction data for create token account +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateTokenAccountData { + pub owner: Pubkey, + pub pre_pay_num_epochs: u8, + pub lamports_per_write: u32, +} + +/// Handler for creating a compressible token account (invoke) +/// +/// Uses the builder pattern from the ctoken module. This demonstrates how to: +/// 1. Build the account infos struct with compressible params +/// 2. Call the invoke() method which handles instruction building and CPI +/// +/// Account order: +/// - accounts[0]: payer (signer) +/// - accounts[1]: account to create (signer) +/// - accounts[2]: mint +/// - accounts[3]: compressible_config +/// - accounts[4]: system_program +/// - accounts[5]: rent_sponsor +pub fn process_create_token_account_invoke( + accounts: &[AccountInfo], + data: CreateTokenAccountData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Build the compressible params using constructor + let compressible_params = CompressibleParamsCpi::new( + accounts[3].clone(), + accounts[5].clone(), + accounts[4].clone(), + ); + + // Build the account infos struct + CreateCTokenAccountCpi { + payer: accounts[0].clone(), + account: accounts[1].clone(), + mint: accounts[2].clone(), + owner: data.owner, + compressible: Some(compressible_params), + } + .invoke()?; + + Ok(()) +} + +/// Handler for creating a compressible token account with PDA ownership (invoke_signed) +/// +/// Account order: +/// - accounts[0]: payer (signer) +/// - accounts[1]: account to create (PDA, will be derived and verified) +/// - accounts[2]: mint +/// - accounts[3]: compressible_config +/// - accounts[4]: system_program +/// - accounts[5]: rent_sponsor +pub fn process_create_token_account_invoke_signed( + accounts: &[AccountInfo], + data: CreateTokenAccountData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the token account + let (pda, bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the account to create is the PDA + if &pda != accounts[1].key { + return Err(ProgramError::InvalidSeeds); + } + + // Build the compressible params using constructor + let compressible_params = CompressibleParamsCpi::new( + accounts[3].clone(), + accounts[5].clone(), + accounts[4].clone(), + ); + + // Build the account infos struct + let account_cpi = CreateCTokenAccountCpi { + payer: accounts[0].clone(), + account: accounts[1].clone(), + mint: accounts[2].clone(), + owner: data.owner, + compressible: Some(compressible_params), + }; + + // Invoke with PDA signing + let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; + account_cpi.invoke_signed(&[signer_seeds])?; + + Ok(()) +} +``` + + + +# Next Steps + diff --git a/light-token/cookbook/extensions.mdx b/light-token/cookbook/extensions.mdx new file mode 100644 index 00000000..ff9c814c --- /dev/null +++ b/light-token/cookbook/extensions.mdx @@ -0,0 +1,34 @@ +--- +title: Extensions +description: Overview to Token 2022 extensions supported by light-tokens. +hidden: true +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Extension NameDescriptionlight-tokenCompressed Token
MetadataPointer-
TokenMetadata-
InterestBearingConfig-
GroupPointer-
GroupMemberPointer-
TokenGroup-
TokenGroupMember-
MintCloseAuthority--
TransferFeeConfigfees must be zero-
DefaultAccountStateany state allowed-
PermanentDelegate--
TransferHookprogram_id must be nil-
Pausable--
ConfidentialTransferMintinitialized but not enabled-
ConfidentialTransferFeeConfigfees must be zero-
ConfidentialMintBurninitialized but not enabled-
\ No newline at end of file diff --git a/light-token/cookbook/mint-tokens.mdx b/light-token/cookbook/mint-tokens.mdx new file mode 100644 index 00000000..8b015337 --- /dev/null +++ b/light-token/cookbook/mint-tokens.mdx @@ -0,0 +1,622 @@ +--- +title: Mint to Light-Token Accounts +description: Client and program guide to mint tokens to a light-token account. Includes step-by-step implementation and full code examples. +--- + +import CMintSystemAccountsList from '/snippets/accounts-list/light-mint-system-accounts-list.mdx'; +import CMintToCTokenAccountsList from '/snippets/accounts-list/light-mint-to-light-token-accounts-list.mdx'; +import CTokenClientPrerequisites from '/snippets/light-token-guides/light-token-client-prerequisites.mdx'; +import ClientCustomRentConfig from '/snippets/light-token-guides/client-custom-rent-config.mdx'; + + +1. Minting to light-token accounts increases the supply of a mint. +2. The destination token accounts must exist to receive the minted tokens. +3. Only the mint authority can mint new tokens to light-token accounts. + +# Get Started + + + + + +The example mints light-tokens to existing light-token accounts. +1. Prerequisite: The example creates a test light-mint and destination light-token account. +2. Get light-mint account infos and prove it exists with a validity proof.. +3. Set the amount of tokens you will mint and the mint authority. Only the mint authority can mint new light-tokens. +4. Build the instruction with `MintToCToken::new()` and send the transaction. +```rust +use light_ctoken_sdk::ctoken::MintToCToken; + +let instruction = MintToCToken::new( + params, + payer.pubkey(), + state_tree, + output_queue, + input_queue, + vec![recipient_account.pubkey()], +) +.instruction()?; +``` + + + +### Prerequisites + + + + + + +### Mint light-tokens + +```rust +use borsh::BorshDeserialize; +use light_client::indexer::{AddressWithTree, Indexer}; +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; +use light_ctoken_sdk::ctoken::{ + CreateCMint, CreateCMintParams, CreateCTokenAccount, MintToCToken, MintToCTokenParams, +}; +use light_ctoken_interface::instructions::extensions::token_metadata::TokenMetadataInstructionData; +use solana_sdk::compute_budget::ComputeBudgetInstruction; +use light_ctoken_interface::instructions::extensions::ExtensionInstructionData; +use light_ctoken_interface::instructions::mint_action::CompressedMintWithContext; +use light_ctoken_interface::state::{AdditionalMetadata, CToken, CompressedMint}; +use serde_json; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; +use std::convert::TryFrom; +use std::env; +use std::fs; + +#[tokio::test(flavor = "multi_thread")] +async fn test_mint_to_ctoken() { + dotenvy::dotenv().ok(); + + let keypair_path = env::var("KEYPAIR_PATH") + .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap())); + let payer = load_keypair(&keypair_path).expect("Failed to load keypair"); + let mint_authority = payer.pubkey(); + + let api_key = env::var("api_key") // Set api_key in your .env + .expect("api_key environment variable must be set"); + + let config = LightClientConfig::devnet( + Some("https://devnet.helius-rpc.com".to_string()), + Some(api_key), + ); + let mut rpc = LightClient::new_with_retry(config, None) + .await + .expect("Failed to initialize LightClient"); + + // Step 1: Create compressed mint with metadata + let (mint, compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await; + + // Step 2: Create ctoken account + let ctoken_account = Keypair::new(); + let owner = payer.pubkey(); + let create_account_ix = + CreateCTokenAccount::new(payer.pubkey(), ctoken_account.pubkey(), mint, owner) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction( + &[create_account_ix], + &payer.pubkey(), + &[&payer, &ctoken_account], + ) + .await + .unwrap(); + + // Step 3: Get compressed mint account to build CompressedMintWithContext + let compressed_mint_account = rpc + .get_compressed_account(compression_address, None) + .await + .unwrap() + .value + .expect("Compressed mint should exist"); + + // Step 4: Get validity proof for the mint operation + let rpc_result = rpc + .get_validity_proof(vec![compressed_mint_account.hash], vec![], None) + .await + .unwrap() + .value; + + // Step 5: Deserialize compressed mint data + let compressed_mint = CompressedMint::deserialize( + &mut compressed_mint_account.data.unwrap().data.as_slice(), + ) + .unwrap(); + + // Step 6: Build CompressedMintWithContext + let compressed_mint_with_context = CompressedMintWithContext { + address: compression_address, + leaf_index: compressed_mint_account.leaf_index, + prove_by_index: false, + root_index: rpc_result.accounts[0] + .root_index + .root_index() + .unwrap_or_default(), + mint: compressed_mint.try_into().unwrap(), + }; + + let amount = 1_000_000_000u64; // 1 token with 9 decimals + + // Step 7: Get active output queue for devnet + let _ = rpc.get_latest_active_state_trees().await; + let output_queue = match rpc + .get_random_state_tree_info() + .ok() + .or_else(|| rpc.get_random_state_tree_info_v1().ok()) + { + Some(info) => info + .get_output_pubkey() + .expect("Invalid state tree type for output"), + None => { + let queues = rpc + .indexer_mut() + .expect("IndexerNotInitialized") + .get_queue_info(None) + .await + .expect("Failed to fetch queue info") + .value + .queues; + queues + .get(0) + .map(|q| q.queue) + .expect("NoStateTreesAvailable: no active state trees returned") + } + }; + + // Step 8: Build mint params + let params = MintToCTokenParams::new( + compressed_mint_with_context, + amount, + mint_authority, + rpc_result.proof, + ); + + // Step 9: Build instruction using SDK builder + let instruction = MintToCToken::new( + params, + payer.pubkey(), + compressed_mint_account.tree_info.tree, + compressed_mint_account.tree_info.queue, + output_queue, + vec![ctoken_account.pubkey()], + ) + .instruction() + .unwrap(); + + // Step 10: Send transaction + let compute_unit_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000); + rpc.create_and_send_transaction(&[compute_unit_ix, instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Step 11: Verify tokens were minted + let ctoken_account_data = rpc + .get_account(ctoken_account.pubkey()) + .await + .unwrap() + .unwrap(); + + let ctoken_state = CToken::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); + assert_eq!(ctoken_state.amount, amount, "Token amount should match"); + assert_eq!(ctoken_state.mint, mint.to_bytes(), "Mint should match"); + assert_eq!(ctoken_state.owner, owner.to_bytes(), "Owner should match"); +} + +pub async fn create_compressed_mint( + rpc: &mut R, + payer: &Keypair, + decimals: u8, +) -> (Pubkey, [u8; 32]) { + let mint_signer = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + + // Fetch active state trees for devnet + let _ = rpc.get_latest_active_state_trees().await; + let output_pubkey = match rpc + .get_random_state_tree_info() + .ok() + .or_else(|| rpc.get_random_state_tree_info_v1().ok()) + { + Some(info) => info + .get_output_pubkey() + .expect("Invalid state tree type for output"), + None => { + let queues = rpc + .indexer_mut() + .expect("IndexerNotInitialized") + .get_queue_info(None) + .await + .expect("Failed to fetch queue info") + .value + .queues; + queues + .get(0) + .map(|q| q.queue) + .expect("NoStateTreesAvailable: no active state trees returned") + } + }; + + // Derive compression address + let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( + &mint_signer.pubkey(), + &address_tree.tree, + ); + + let mint_pda = light_ctoken_sdk::ctoken::find_cmint_address(&mint_signer.pubkey()).0; + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params with token metadata + let params = CreateCMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda, + freeze_authority: None, + extensions: Some(vec![ExtensionInstructionData::TokenMetadata( + TokenMetadataInstructionData { + update_authority: Some(payer.pubkey().to_bytes().into()), + name: b"Example Token".to_vec(), + symbol: b"EXT".to_vec(), + uri: b"https://example.com/metadata.json".to_vec(), + additional_metadata: Some(vec![AdditionalMetadata { + key: b"type".to_vec(), + value: b"compressed".to_vec(), + }]), + }, + )]), + }; + + // Create instruction + let create_cmint = CreateCMint::new( + params, + mint_signer.pubkey(), + payer.pubkey(), + address_tree.tree, + output_pubkey, + ); + let instruction = create_cmint.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_signer]) + .await + .unwrap(); + + (mint_pda, compression_address) +} + +fn load_keypair(path: &str) -> Result> { + let path = if path.starts_with("~") { + path.replace("~", &env::var("HOME").unwrap_or_default()) + } else { + path.to_string() + }; + let file = fs::read_to_string(&path)?; + let bytes: Vec = serde_json::from_str(&file)?; + Ok(Keypair::try_from(&bytes[..])?) +} +``` + + + + + + + +Find [a full code example at the end](#full-code-example). + + + + + +### Configure Mint Parameters +Include your mint, the amount of tokens to be minted and the pubkey of the mint authority. +The client passes a validity proof that proves the light-mint exists. + +```rust +use light_ctoken_sdk::ctoken::MintToCTokenParams; + +let params = MintToCTokenParams::new( + data.compressed_mint_inputs, + data.amount, + data.mint_authority, + data.proof, +); +``` + + + + + + +### System Accounts + +Compressed accounts like light-mints require system accounts like the Light System Program account for interactions and proof verification. +The client includes them in the instruction. + + + + + +```rust +use light_ctoken_sdk::ctoken::SystemAccountInfos; + +let system_accounts = SystemAccountInfos { + light_system_program: light_system_program.clone(), + cpi_authority_pda: cpi_authority_pda.clone(), + registered_program_pda: registered_program_pda.clone(), + account_compression_authority: account_compression_authority.clone(), + account_compression_program: account_compression_program.clone(), + system_program: system_program.clone(), +}; +``` + + + + + +### Build Account Infos and CPI the light-token Program + +1. Pass the required accounts, including the destination light-token accounts. +2. Include `params` and `system_accounts` from the previous steps +3. Use `invoke` or `invoke_signed`, when a CPI requires a PDA signer. + + + +```rust +use light_ctoken_sdk::ctoken::MintToCTokenCpi; + +MintToCTokenCpi { + authority: authority.clone(), + payer: payer.clone(), + state_tree: state_tree.clone(), + input_queue: input_queue.clone(), + output_queue: output_queue.clone(), + ctoken_accounts, + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, +} +.invoke()?; +``` + + + + +```rust +use light_ctoken_sdk::ctoken::MintToCTokenCpi; + +let account_infos = MintToCTokenCpi { + authority: authority.clone(), + payer: payer.clone(), + state_tree: state_tree.clone(), + input_queue: input_queue.clone(), + output_queue: output_queue.clone(), + ctoken_accounts, + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, +}; + +let signer_seeds: &[&[u8]] = &[MINT_AUTHORITY_SEED, &[bump]]; +account_infos.invoke_signed(&[signer_seeds])?; +``` + + + + + + + + + + + +# Full Code Example + + +Find the source code [here](https://github.com/Lightprotocol/program-examples/blob/main/light-token/src/mint_to.rs). + + +```rust expandable +use borsh::{BorshDeserialize, BorshSerialize}; +use light_ctoken_interface::instructions::mint_action::CompressedMintWithContext; +use light_ctoken_sdk::ctoken::{MintToCTokenCpi, MintToCTokenParams, SystemAccountInfos}; +use light_sdk::instruction::ValidityProof; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::ID; + +/// PDA seed for mint authority in invoke_signed variant +pub const MINT_AUTHORITY_SEED: &[u8] = b"mint_authority"; + +/// Instruction data for mint_to_ctoken operations +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct MintToCTokenData { + pub compressed_mint_inputs: CompressedMintWithContext, + pub amount: u64, + pub mint_authority: Pubkey, + pub proof: ValidityProof, +} + +/// Handler for minting tokens to compressed token accounts +/// +/// Uses the MintToCTokenCpi builder pattern. This demonstrates how to: +/// 1. Build MintToCTokenParams using the constructor +/// 2. Build MintToCTokenCpi with accounts and params +/// 3. Call invoke() which handles instruction building and CPI +/// +/// Account order (all accounts from SDK-generated instruction): +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: authority (mint_authority) +/// - accounts[3]: fee_payer +/// - accounts[4]: cpi_authority_pda +/// - accounts[5]: registered_program_pda +/// - accounts[6]: account_compression_authority +/// - accounts[7]: account_compression_program +/// - accounts[8]: system_program +/// - accounts[9]: output_queue +/// - accounts[10]: state_tree +/// - accounts[11]: input_queue +/// - accounts[12..]: ctoken_accounts (variable length - destination accounts) +pub fn process_mint_to_ctoken( + accounts: &[AccountInfo], + data: MintToCTokenData, +) -> Result<(), ProgramError> { + if accounts.len() < 13 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Build params using the constructor + let params = MintToCTokenParams::new( + data.compressed_mint_inputs, + data.amount, + data.mint_authority, + data.proof, + ); + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: accounts[1].clone(), + cpi_authority_pda: accounts[4].clone(), + registered_program_pda: accounts[5].clone(), + account_compression_authority: accounts[6].clone(), + account_compression_program: accounts[7].clone(), + system_program: accounts[8].clone(), + }; + + // Collect ctoken accounts from remaining accounts (index 12 onwards) + let ctoken_accounts: Vec = accounts[12..].to_vec(); + + // Build the account infos struct and invoke + // SDK account order: output_queue (9), tree (10), input_queue (11), ctoken_accounts (12+) + // In this case, payer == authority (accounts[3]) + MintToCTokenCpi { + authority: accounts[2].clone(), // authority from SDK accounts + payer: accounts[3].clone(), // fee_payer from SDK accounts + state_tree: accounts[10].clone(), // tree at index 10 + input_queue: accounts[11].clone(), // input_queue at index 11 + output_queue: accounts[9].clone(), // output_queue at index 9 + ctoken_accounts, + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + } + .invoke()?; + + Ok(()) +} + +/// Handler for minting tokens with PDA mint authority (invoke_signed) +/// +/// Uses the MintToCTokenCpi builder pattern with invoke_signed. +/// The mint authority is a PDA derived from this program. +/// +/// Account order (all accounts from SDK-generated instruction): +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: authority (PDA mint_authority, not signer - program signs) +/// - accounts[3]: fee_payer +/// - accounts[4]: cpi_authority_pda +/// - accounts[5]: registered_program_pda +/// - accounts[6]: account_compression_authority +/// - accounts[7]: account_compression_program +/// - accounts[8]: system_program +/// - accounts[9]: output_queue +/// - accounts[10]: state_tree +/// - accounts[11]: input_queue +/// - accounts[12..]: ctoken_accounts (variable length - destination accounts) +pub fn process_mint_to_ctoken_invoke_signed( + accounts: &[AccountInfo], + data: MintToCTokenData, +) -> Result<(), ProgramError> { + if accounts.len() < 13 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the mint authority + let (pda, bump) = Pubkey::find_program_address(&[MINT_AUTHORITY_SEED], &ID); + + // Verify the authority account is the PDA we expect + if &pda != accounts[2].key { + return Err(ProgramError::InvalidSeeds); + } + + // Build params using the constructor + let params = MintToCTokenParams::new( + data.compressed_mint_inputs, + data.amount, + data.mint_authority, + data.proof, + ); + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: accounts[1].clone(), + cpi_authority_pda: accounts[4].clone(), + registered_program_pda: accounts[5].clone(), + account_compression_authority: accounts[6].clone(), + account_compression_program: accounts[7].clone(), + system_program: accounts[8].clone(), + }; + + // Collect ctoken accounts from remaining accounts (index 12 onwards) + let ctoken_accounts: Vec = accounts[12..].to_vec(); + + // Build the account infos struct + // authority is the PDA (accounts[2]) + let account_infos = MintToCTokenCpi { + authority: accounts[2].clone(), // authority PDA + payer: accounts[3].clone(), // fee_payer from SDK accounts + state_tree: accounts[10].clone(), // tree at index 10 + input_queue: accounts[11].clone(), // input_queue at index 11 + output_queue: accounts[9].clone(), // output_queue at index 9 + ctoken_accounts, + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + }; + + // Invoke with PDA signing + let signer_seeds: &[&[u8]] = &[MINT_AUTHORITY_SEED, &[bump]]; + account_infos.invoke_signed(&[signer_seeds])?; + + Ok(()) +} +``` + + + +# Next Steps + + + diff --git a/light-token/cookbook/transfer-interface.mdx b/light-token/cookbook/transfer-interface.mdx new file mode 100644 index 00000000..ab072e20 --- /dev/null +++ b/light-token/cookbook/transfer-interface.mdx @@ -0,0 +1,523 @@ +--- +title: Transfer Interface +description: Guide for transfers between light-token and SPL token accounts via CPI. The interface detects account types and invokes the right programs. +--- + +import TransferInterfaceAccountsListCtoken1 from '/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx'; +import TransferInterfaceAccountsListSpl2 from '/snippets/accounts-list/transfer-interface-accounts-list-spl-2.mdx'; +import TransferInterfaceIntro from '/snippets/light-token-guides/transfer-interface-intro.mdx'; +import CTokenClientPrerequisites from '/snippets/light-token-guides/light-token-client-prerequisites.mdx'; +import ClientCustomRentConfig from '/snippets/light-token-guides/client-custom-rent-config.mdx'; + + + + + + + + + + + + + + + + +
**light-token -> light-token Account** +
    +
  • Transfers tokens between light-token accounts
  • +
+
**SPL token -> light-token Account** +
    +
  • Transfers SPL tokens to light-token accounts
  • +
  • SPL tokens are locked in interface PDA
  • +
  • Tokens are minted to light-token account
  • +
+
**light-token -> SPL Account** +
    +
  • Releases SPL tokens from interface PDA to SPL account
  • +
  • Burns tokens in source light-token account
  • +
+
+ + +* For example, **SPL -> light-token account** can be used for transfers from Alice's SPL token account to her own light-token account. +* You can use this to use the **sponsored rent-exemption for existing SPL tokens**. + + +# Get Started + + + + + +The example transfers SPL token -> light-token and light-token -> light-token: +1. Create SPL mint, SPL token accounts, and mint SPL tokens +2. Send SPL tokens to light-token account to mint light-tokens. +3. Transfer light-tokens to another light-token account. + + + +### Prerequisites + + + + + + +### Transfer Interface + + + +```rust +use anchor_spl::token::{spl_token, Mint}; +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; +use light_ctoken_sdk::{ + ctoken::{ + derive_ctoken_ata as derive_token_ata, CreateAssociatedCTokenAccount as CreateAssociatedTokenAccount, + TransferCToken as TransferToken, TransferSplToCtoken as TransferSplToToken, + }, + spl_interface::{find_spl_interface_pda_with_index, CreateSplInterfacePda}, +}; +use serde_json; +use solana_sdk::compute_budget::ComputeBudgetInstruction; +use solana_sdk::program_pack::Pack; +use solana_sdk::{signature::Keypair, signer::Signer}; +use spl_token_2022::pod::PodAccount; +use std::convert::TryFrom; +use std::env; +use std::fs; + +/// Test SPL → light-token → light-token +// with ATA creation + transfer in one transaction +#[tokio::test(flavor = "multi_thread")] +async fn test_spl_to_token_to_token() { + dotenvy::dotenv().ok(); + + let keypair_path = env::var("KEYPAIR_PATH") + .unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap())); + let payer = load_keypair(&keypair_path).expect("Failed to load keypair"); + let api_key = env::var("api_key") // Set api_key in your .env + .expect("api_key environment variable must be set"); + + let config = LightClientConfig::devnet( + Some("https://devnet.helius-rpc.com".to_string()), + Some(api_key), + ); + let mut rpc = LightClient::new_with_retry(config, None) + .await + .expect("Failed to initialize LightClient"); + + // 2. Create SPL mint + let mint_keypair = Keypair::new(); + let mint = mint_keypair.pubkey(); + let decimals = 2u8; + + let mint_rent = rpc + .get_minimum_balance_for_rent_exemption(Mint::LEN) + .await + .unwrap(); + + let create_mint_account_ix = solana_sdk::system_instruction::create_account( + &payer.pubkey(), + &mint, + mint_rent, + Mint::LEN as u64, + &spl_token::ID, + ); + + let initialize_mint_ix = spl_token::instruction::initialize_mint( + &spl_token::ID, + &mint, + &payer.pubkey(), + None, + decimals, + ) + .unwrap(); + + rpc.create_and_send_transaction( + &[create_mint_account_ix, initialize_mint_ix], + &payer.pubkey(), + &[&payer, &mint_keypair], + ) + .await + .unwrap(); + + // 3. Create SPL interface PDA + let create_spl_interface_pda_ix = + CreateSplInterfacePda::new(payer.pubkey(), mint, anchor_spl::token::ID).instruction(); + + rpc.create_and_send_transaction(&[create_spl_interface_pda_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let mint_amount = 10_000u64; + let spl_to_token_amount = 7_000u64; + let token_transfer_amount = 3_000u64; + + // 4. Create SPL token account + let spl_token_account_keypair = Keypair::new(); + let token_account_rent = rpc + .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .await + .unwrap(); + let create_token_account_ix = solana_sdk::system_instruction::create_account( + &payer.pubkey(), + &spl_token_account_keypair.pubkey(), + token_account_rent, + spl_token::state::Account::LEN as u64, + &spl_token::ID, + ); + let init_token_account_ix = spl_token::instruction::initialize_account( + &spl_token::ID, + &spl_token_account_keypair.pubkey(), + &mint, + &payer.pubkey(), + ) + .unwrap(); + rpc.create_and_send_transaction( + &[create_token_account_ix, init_token_account_ix], + &payer.pubkey(), + &[&spl_token_account_keypair, &payer], + ) + .await + .unwrap(); + + // 5. Mint SPL tokens to the SPL account + let mint_to_ix = spl_token::instruction::mint_to( + &spl_token::ID, + &mint, + &spl_token_account_keypair.pubkey(), + &payer.pubkey(), + &[&payer.pubkey()], + mint_amount, + ) + .unwrap(); + rpc.create_and_send_transaction(&[mint_to_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify SPL account has tokens + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + let initial_spl_balance: u64 = spl_account.amount.into(); + assert_eq!(initial_spl_balance, mint_amount); + + // 6. Create sender's token ATA + let (sender_token_ata, _bump) = derive_token_ata(&payer.pubkey(), &mint); + let create_ata_instruction = + CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify sender's token ATA was created + let token_account_data = rpc.get_account(sender_token_ata).await.unwrap().unwrap(); + assert!( + !token_account_data.data.is_empty(), + "Sender token ATA should exist" + ); + + // 7. Transfer SPL tokens to sender's token account + let (spl_interface_pda, spl_interface_pda_bump) = find_spl_interface_pda_with_index(&mint, 0); + + let spl_to_token_ix = TransferSplToToken { + amount: spl_to_token_amount, + spl_interface_pda_bump, + source_spl_token_account: spl_token_account_keypair.pubkey(), + destination_ctoken_account: sender_token_ata, + authority: payer.pubkey(), + mint, + payer: payer.pubkey(), + spl_interface_pda, + spl_token_program: anchor_spl::token::ID, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[spl_to_token_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // 8. Create recipient ATA + transfer token→token in one transaction + let recipient = Keypair::new(); + let (recipient_token_ata, _) = derive_token_ata(&recipient.pubkey(), &mint); + + let create_recipient_ata_ix = CreateAssociatedTokenAccount::new( + payer.pubkey(), + recipient.pubkey(), + mint, + ) + .instruction() + .unwrap(); + + let token_transfer_ix = TransferToken { + source: sender_token_ata, + destination: recipient_token_ata, + amount: token_transfer_amount, + authority: payer.pubkey(), + max_top_up: None, + } + .instruction() + .unwrap(); + + let compute_unit_ix = ComputeBudgetInstruction::set_compute_unit_limit(10_000); + rpc.create_and_send_transaction( + &[compute_unit_ix, create_recipient_ata_ix, token_transfer_ix], + &payer.pubkey(), + &[&payer], + ) + .await + .unwrap(); +} + +fn load_keypair(path: &str) -> Result> { + let path = if path.starts_with("~") { + path.replace("~", &env::var("HOME").unwrap_or_default()) + } else { + path.to_string() + }; + let file = fs::read_to_string(&path)?; + let bytes: Vec = serde_json::from_str(&file)?; + Ok(Keypair::try_from(&bytes[..])?) +} +``` + + + + + + +Find [a full code example at the end](#full-code-example). + + + + +### light-token Transfer Interface + +Define the number of light-tokens / SPL tokens to transfer +- from which SPL or light-token account, and +- to which SPL or light-token account. + +```rust +use light_ctoken_sdk::ctoken::TransferInterfaceCpi; + +let mut transfer = TransferInterfaceCpi::new( + data.amount, + source_account.clone(), + destination_account.clone(), + authority.clone(), + payer.clone(), + compressed_token_program_authority.clone(), +); +``` + + + + + + + + +### SPL Transfer Interface (Optional) + +The SPL transfer interface is only needed for SPL ↔ light-token transfers. +```rust +transfer = transfer.with_spl_interface( + Some(mint.clone()), + Some(spl_token_program.clone()), + Some(spl_interface_pda.clone()), + data.spl_interface_pda_bump, +)?; +``` + +SPL ↔ light-token transfers require a `spl_interface_pda`: + * **SPL → light-token**: SPL tokens are locked by the light-token Program in the PDA, light-tokens are minted to light-token accounts + * **light-token → SPL**: light-tokens are burned, SPL tokens transferred to SPL token accounts + +The interface PDA is derived from the `mint` pubkey and pool seed. + + + + + + + + + +### CPI the light-token Program + +CPI the light-token program to execute the transfer. +Use `invoke()`, or `invoke_signed()` when a CPI requires a PDA signer. + + + +```rust +transfer.invoke()?; +``` + + +```rust +let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]]; +transfer.invoke_signed(&[authority_seeds])?; +``` + + + + + + +# Full Code Example + + +Find the source code [here](https://github.com/Lightprotocol/program-examples/blob/main/light-token/src/transfer_interface.rs). + + +```rust expandable +use borsh::{BorshDeserialize, BorshSerialize}; +use light_ctoken_sdk::ctoken::TransferInterfaceCpi; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::ID; + +/// PDA seed for authority in invoke_signed variants +pub const TRANSFER_INTERFACE_AUTHORITY_SEED: &[u8] = b"transfer_interface_authority"; + +/// Instruction data for TransferInterfaceCpi +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct TransferInterfaceData { + pub amount: u64, + /// Required for SPL<->CToken transfers, None for CToken->CToken + pub token_pool_pda_bump: Option, +} + +/// Handler for TransferInterfaceCpi (invoke) +/// +/// This unified interface automatically detects account types and routes to: +/// - CToken -> CToken transfer +/// - CToken -> SPL transfer +/// - SPL -> CToken transfer +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source_account (SPL or CToken) +/// - accounts[2]: destination_account (SPL or CToken) +/// - accounts[3]: authority (signer) +/// - accounts[4]: payer (signer) +/// - accounts[5]: compressed_token_program_authority +/// For SPL bridge (optional, required for SPL<->CToken): +/// - accounts[6]: mint +/// - accounts[7]: token_pool_pda +/// - accounts[8]: spl_token_program +pub fn process_transfer_interface_invoke( + accounts: &[AccountInfo], + data: TransferInterfaceData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let mut transfer = TransferInterfaceCpi::new( + data.amount, + accounts[1].clone(), // source_account + accounts[2].clone(), // destination_account + accounts[3].clone(), // authority + accounts[4].clone(), // payer + accounts[5].clone(), // compressed_token_program_authority + ); + + // Add SPL bridge config if provided + if accounts.len() >= 9 && data.token_pool_pda_bump.is_some() { + transfer = transfer.with_spl_interface( + Some(accounts[6].clone()), // mint + Some(accounts[8].clone()), // spl_token_program + Some(accounts[7].clone()), // token_pool_pda + data.token_pool_pda_bump, + )?; + } + + transfer.invoke()?; + + Ok(()) +} + +/// Handler for TransferInterfaceCpi with PDA authority (invoke_signed) +/// +/// The authority is a PDA derived from TRANSFER_INTERFACE_AUTHORITY_SEED. +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source_account (SPL or CToken) +/// - accounts[2]: destination_account (SPL or CToken) +/// - accounts[3]: authority (PDA, not signer - program signs) +/// - accounts[4]: payer (signer) +/// - accounts[5]: compressed_token_program_authority +/// For SPL bridge (optional, required for SPL<->CToken): +/// - accounts[6]: mint +/// - accounts[7]: token_pool_pda +/// - accounts[8]: spl_token_program +pub fn process_transfer_interface_invoke_signed( + accounts: &[AccountInfo], + data: TransferInterfaceData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (authority_pda, authority_bump) = + Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &ID); + + // Verify the authority account is the PDA we expect + if &authority_pda != accounts[3].key { + return Err(ProgramError::InvalidSeeds); + } + + let mut transfer = TransferInterfaceCpi::new( + data.amount, + accounts[1].clone(), // source_account + accounts[2].clone(), // destination_account + accounts[3].clone(), // authority (PDA) + accounts[4].clone(), // payer + accounts[5].clone(), // compressed_token_program_authority + ); + + // Add SPL bridge config if provided + if accounts.len() >= 9 && data.token_pool_pda_bump.is_some() { + transfer = transfer.with_spl_interface( + Some(accounts[6].clone()), // mint + Some(accounts[8].clone()), // spl_token_program + Some(accounts[7].clone()), // token_pool_pda + data.token_pool_pda_bump, + )?; + } + + // Invoke with PDA signing + let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]]; + transfer.invoke_signed(&[authority_seeds])?; + + Ok(()) +} +``` + + + + +# Next Steps + + diff --git a/light-token/cookbook/update-metadata.mdx b/light-token/cookbook/update-metadata.mdx new file mode 100644 index 00000000..e69de29b diff --git a/light-token/faq.mdx b/light-token/faq.mdx new file mode 100644 index 00000000..8939d681 --- /dev/null +++ b/light-token/faq.mdx @@ -0,0 +1,100 @@ +--- +title: FAQ +description: Frequently asked questions about light-token. +--- +import { CodeCompare } from '/snippets/jsx/code-compare.jsx'; +import CompressibleRentExplained from '/snippets/compressible-rent-explained.mdx'; +import { RentLifecycleVisualizer } from '/snippets/jsx/rent-lifecycle-visualizer.jsx'; + + +#### What is light-token? +Light-token is like SPL token stored more efficiently to reduce account creation cost. +Users receive and send the same tokens. + +| Creation Cost | SPL | Light | +|:---------------------|:------------------|:-------------------| +| **Mint Account** | ~1,500,000 lamports | **15,000** lamports | +| **Token Account** | ~2,000,000 lamports | ~**17,000** lamports | + +#### Can I start using the Light-Token Standard? +Light-token is currently deployed on devnet and undergoing security audits. + +#### Do I need to change my client code significantly? +No. The `light-token-sdk` follows similar patterns to SPL. + + + +--- + +#### Can light-token accounts hold SPL tokens? +Yes, light-token accounts can hold tokens from light, SPL, or Token 2022 mints. + +SPL tokens can be deposited into light-token accounts and withdrawn back to SPL token accounts via the `transferInterface` method. + +#### Do light-token accounts require rent? + +Rent is paid over time when accounts are accessed via a custom rent config instead of the full rent-exemption upfront: + + + + +#### What happens if my light-token account runs out of rent? +The account is automatically compressed. +Your tokens are preserved as a compressed token account (rent-free). +The account is automatically decompressed when someone interacts with it again. + +#### Does light-token support extensions? +Extensions are under development. Additional extensions can be requested. + +Coming soon: +- MetadataPointer +- TokenMetadata +- InterestBearingConfig +- GroupPointer +- GroupMemberPointer +- TokenGroup +- TokenGroupMember +- MintCloseAuthority +- TransferFeeConfig +- DefaultAccountState +- PermanentDelegate +- TransferHook +- Pausable +- ConfidentialTransferMint +- ConfidentialTransferFeeConfig +- ConfidentialMintBurn + +#### What is the difference between light-token and compressed token? +- **light-token**: Solana account that holds token balances of light-mints, SPL or Token 22 mints. +- **Compressed token**: Compressed account storing token data. Rent-free, for storage and distribution. + +#### How do I query light-token and compressed token balances? +Standard Solana RPC works for light-token accounts (Solana accounts). + +### Get Started + + + Dedicated guides for specific use cases. + + + Recipes for light-mints and light-tokens. + + \ No newline at end of file diff --git a/light-token/toolkits/README.mdx b/light-token/toolkits/README.mdx new file mode 100644 index 00000000..e40a1137 --- /dev/null +++ b/light-token/toolkits/README.mdx @@ -0,0 +1,75 @@ +--- +title: Overview of Toolkits +sidebarTitle: Overview +description: +--- + + + Integrate light-token APIs with familiar SPL patterns. + + + + Allow users to display and swap light-tokens. + + + + Integrate light-tokens into decentralized exchanges. + + + + Launch tokens with compressed token support. + + + + Build trading applications with light-token support. + + + + Stream mint events from the network in real-time. + + + + Stream token events from the network in real-time. + \ No newline at end of file diff --git a/light-token/toolkits/for-dexs.mdx b/light-token/toolkits/for-dexs.mdx new file mode 100644 index 00000000..e6446b79 --- /dev/null +++ b/light-token/toolkits/for-dexs.mdx @@ -0,0 +1,3 @@ +--- +title: "for DEXs" +--- diff --git a/light-token/toolkits/for-launchpads.mdx b/light-token/toolkits/for-launchpads.mdx new file mode 100644 index 00000000..8df52ead --- /dev/null +++ b/light-token/toolkits/for-launchpads.mdx @@ -0,0 +1,5 @@ +--- +title: "light-token Toolkit for Launchpads" +sidebarTitle: "for Launchpads" +description: "Guide to add light-token APIs to your Launchpad Application." +--- diff --git a/light-token/toolkits/for-payments.mdx b/light-token/toolkits/for-payments.mdx new file mode 100644 index 00000000..ecfde9e6 --- /dev/null +++ b/light-token/toolkits/for-payments.mdx @@ -0,0 +1,428 @@ +--- +title: "light-token Toolkit for Stablecoin Payments" +sidebarTitle: "for Stablecoin Payments" +description: "Guide to integrate light-token APIs with comparison to SPL." +--- + +import { CodeCompare } from '/snippets/jsx/code-compare.jsx'; + +1. light-token follows the same API patterns like ATA +2. Your users receives the same stablecoin, just stored more efficiently. + +| Creation Cost | SPL | light-token | +|:---------------------|:------------------|:----------------------| +| **Token Account** | ~2,000,000 lamports | ~**17,000** lamports | + + +Get an [overview to light-token's here](/light-token/README). + +### What you will implement + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
light-tokenSPL Token
**Get/Create ATA**getOrCreateAtaInterface()getOrCreateAssociatedTokenAccount()
**Derive ATA**getAssociatedTokenAddressInterface()getAssociatedTokenAddress()
**Transfer**transferInterface()transferChecked()
**Get Balance**getAtaInterface()getAccount()
**Tx History**rpc.getSignaturesForOwnerInterface()getSignaturesForAddress()
**Exit to SPL**unwrap()N/A
+ + +Find devnet examples here [https://github.com/Lightprotocol/c-token-toolkit](https://github.com/Lightprotocol/c-token-toolkit). + + +## Setup + +```bash +npm install @lightprotocol/compressed-token/unified \ + @lightprotocol/stateless.js +``` + +```typescript +import { createRpc } from "@lightprotocol/stateless.js"; + +import { + getOrCreateAtaInterface, + getAtaInterface, + getAssociatedTokenAddressInterface, + transferInterface, + unwrap, +} from "@lightprotocol/compressed-token/unified"; + +const rpc = createRpc(RPC_ENDPOINT); +``` + +### Receive Payments + + + + +```typescript +import { getOrCreateAtaInterface } from "@lightprotocol/compressed-token/unified"; + +const ata = await getOrCreateAtaInterface( + rpc, + payer, + mint, + recipient +); +// Share ata.parsed.address with sender + +console.log(ata.parsed.amount); +``` + + + +```typescript +import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; + +const ata = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + recipient +); +// Share ata.address with sender + +console.log(ata.amount); +``` + + + + + +```typescript +import { + createAssociatedTokenAccountInterfaceIdempotentInstruction, + createLoadAtaInstructions, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token/unified"; +import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; + +const ata = getAssociatedTokenAddressInterface(mint, recipient); + +const tx = new Transaction().add( + createAssociatedTokenAccountInterfaceIdempotentInstruction( + payer.publicKey, + ata, + recipient, + mint, + CTOKEN_PROGRAM_ID + ), + ...(await createLoadAtaInstructions( + rpc, + ata, + recipient, + mint, + payer.publicKey + )) +); +``` + + + +```typescript +import { + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; + +const ata = getAssociatedTokenAddressSync(mint, recipient); + +const tx = new Transaction().add( + createAssociatedTokenAccountInstruction( + payer.publicKey, + ata, + recipient, + mint + ) +); +``` + + + + + +### Send Payments + + + + +```typescript +import { + getAssociatedTokenAddressInterface, + transferInterface, +} from "@lightprotocol/compressed-token/unified"; + +const sourceAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); + +await transferInterface( + rpc, + payer, + sourceAta, + mint, + destinationAta, + owner, + amount +); +``` + +To ensure your recipient's ATA exists you can prepend an idempotent creation instruction in the same atomic transaction: + +```typescript +import { + getAssociatedTokenAddressInterface, + createAssociatedTokenAccountInterfaceIdempotentInstruction, +} from "@lightprotocol/compressed-token/unified"; +import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; + +const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); +const createAtaIx = createAssociatedTokenAccountInterfaceIdempotentInstruction( + payer.publicKey, + destinationAta, + recipient, + mint, + CTOKEN_PROGRAM_ID +); + +new Transaction().add(createAtaIx, transferIx); +``` + + + +```typescript +import { transfer } from "@solana/spl-token"; + +const sourceAta = getAssociatedTokenAddressSync(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressSync(mint, recipient); + +await transfer( + connection, + payer, + sourceAta, + destinationAta, + owner, + amount, + decimals +); +``` + +With idempotent ATA creation: + +```typescript +import { + getAssociatedTokenAddressSync, + createAssociatedTokenAccountIdempotentInstruction, +} from "@solana/spl-token"; + +const destinationAta = getAssociatedTokenAddressSync(mint, recipient); +const createAtaIx = createAssociatedTokenAccountIdempotentInstruction( + payer.publicKey, + destinationAta, + recipient, + mint +); + +new Transaction().add(createAtaIx, transferIx); +``` + + + + + + +```typescript +import { + createLoadAtaInstructions, + createTransferInterfaceInstruction, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token/unified"; + +const sourceAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); + +const tx = new Transaction().add( + ...(await createLoadAtaInstructions( + rpc, + sourceAta, + owner.publicKey, + mint, + payer.publicKey + )), + createTransferInterfaceInstruction( + sourceAta, + destinationAta, + owner.publicKey, + amount + ) +); +``` + + + +```typescript +import { + getAssociatedTokenAddressSync, + createTransferInstruction, +} from "@solana/spl-token"; + +const sourceAta = getAssociatedTokenAddressSync(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressSync(mint, recipient); + +const tx = new Transaction().add( + createTransferInstruction(sourceAta, destinationAta, owner.publicKey, amount) +); +``` + + + + + + +### Show Balance + +```typescript +import { + getAssociatedTokenAddressInterface, + getAtaInterface, +} from "@lightprotocol/compressed-token/unified"; + +const ata = getAssociatedTokenAddressInterface(mint, owner); +const account = await getAtaInterface( + rpc, + ata, + owner, + mint +); + +console.log(account.parsed.amount); +``` + + + +```typescript +import { getAccount } from "@solana/spl-token"; + +const account = await getAccount(connection, ata); + +console.log(account.amount); +``` + + + +### Transaction History + +```typescript +const result = await rpc.getSignaturesForOwnerInterface(owner); + +console.log(result.signatures); // Merged + deduplicated +console.log(result.solana); // On-chain txs only +console.log(result.compressed); // Compressed txs only +``` + +Use `getSignaturesForAddressInterface(address)` if you want address-specific rather than owner-wide history. + + + +```typescript +const signatures = await connection.getSignaturesForAddress(ata); +``` + + + +### Unwrap to SPL + +When users need vanilla SPL tokens (CEX withdrawal, legacy integration): + + + + +```typescript +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; + +// SPL ATA must exist +const splAta = getAssociatedTokenAddressSync(mint, owner.publicKey); + +await unwrap( + rpc, + payer, + owner, + mint, + splAta, + amount +); +``` + + + + +```typescript +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + createLoadAtaInstructions, + createUnwrapInstruction, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token/unified"; +import { getSplInterfaceInfos } from "@lightprotocol/compressed-token"; + +const ctokenAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); +const splAta = getAssociatedTokenAddressSync(mint, owner.publicKey); + +const splInterfaceInfos = await getSplInterfaceInfos(rpc, mint); +const splInterfaceInfo = splInterfaceInfos.find((i) => i.isInitialized); + +const tx = new Transaction().add( + ...(await createLoadAtaInstructions( + rpc, + ctokenAta, + owner.publicKey, + mint, + payer.publicKey + )), + createUnwrapInstruction( + ctokenAta, + splAta, + owner.publicKey, + mint, + amount, + splInterfaceInfo + ) +); +``` + + + diff --git a/light-token/toolkits/for-streaming-mints.mdx b/light-token/toolkits/for-streaming-mints.mdx new file mode 100644 index 00000000..35422596 --- /dev/null +++ b/light-token/toolkits/for-streaming-mints.mdx @@ -0,0 +1,252 @@ +--- +title: "Toolkit for Streaming Light-Mint Accounts" +sidebarTitle: "for Streaming Mints" +description: "Guide to stream light-mints and metadata using Laserstream." +--- + +| Event | Description | +|:------|:------------| +| **New mints** | Raw mint data | +| **Mint updates** | Supply changes, authority changes | +| **TokenMetadata** | Name, symbol, URI, additional_metadata | + + +Find devnet examples at [c-token-toolkit/streaming-mints](https://github.com/Lightprotocol/c-token-toolkit). + + +## Setup + +```toml Cargo.toml +[dependencies] +helius-laserstream = "0.1" +tokio = { version = "1", features = ["full"] } +futures = "0.3" +light-event = "0.2" +light-compressed-account = "0.7" +light-ctoken-interface = "0.1" +borsh = "0.10" +bs58 = "0.5" +``` + +```rust +use futures::StreamExt; +use helius_laserstream::{subscribe, LaserstreamConfig}; +use light_event::parse::event_from_light_transaction; +use light_ctoken_interface::state::mint::CompressedMint; +use light_ctoken_interface::state::extensions::ExtensionStruct; +use borsh::BorshDeserialize; + +const LIGHT_SYSTEM_PROGRAM: &str = "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"; +const CTOKEN_PROGRAM_ID: &str = "cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"; +const COMPRESSED_MINT_DISCRIMINATOR: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 1]; +``` + + + + +### Connect to Laserstream + + + + +```rust +let config = LaserstreamConfig::new( + "https://laserstream-mainnet-ewr.helius-rpc.com".to_string(), + std::env::var("HELIUS_API_KEY")?, +); +``` + + + + +```rust +let config = LaserstreamConfig::new( + "https://laserstream-devnet-ewr.helius-rpc.com".to_string(), + std::env::var("HELIUS_API_KEY")?, +); +``` + + + + +```rust +let request = helius_laserstream::grpc::SubscribeRequest { + transactions: [( + "light".to_string(), + helius_laserstream::grpc::SubscribeRequestFilterTransactions { + vote: Some(false), + failed: Some(false), + account_include: vec![LIGHT_SYSTEM_PROGRAM.to_string()], + ..Default::default() + }, + )] + .into(), + ..Default::default() +}; + +let (stream, _handle) = subscribe(config, request); +tokio::pin!(stream); + +while let Some(update) = stream.next().await { + // Process transactions... +} +``` + + + + +### Parse Events + +```rust +fn process_transaction(msg: &Message, meta: Option<&TransactionStatusMeta>) { + let account_keys = extract_account_keys(msg, meta); + + let (program_ids, instruction_data, accounts_per_ix) = + extract_instructions(msg, meta, &account_keys); + + match event_from_light_transaction(&program_ids, &instruction_data, accounts_per_ix) { + Ok(Some(batches)) => { + for batch in batches { + println!( + "Event: {} inputs, {} outputs", + batch.event.input_compressed_account_hashes.len(), + batch.event.output_compressed_accounts.len() + ); + } + } + Ok(None) => {} // No compressed account events + Err(e) => eprintln!("Parse error: {:?}", e), + } +} +``` + + + + +### Extract Mints + +```rust +for output in event.output_compressed_accounts.iter() { + let owner = output.compressed_account.owner; + + // Check if owned by cToken program + if owner != ctoken_program_id { + continue; + } + + // Check discriminator + let data = match &output.compressed_account.data { + Some(d) if d.discriminator == COMPRESSED_MINT_DISCRIMINATOR => &d.data, + _ => continue, + }; + + // Deserialize + let mint = CompressedMint::try_from_slice(data)?; + + // Check if new (address not in inputs) + let is_new = output + .compressed_account + .address + .map(|addr| { + !event.input_compressed_account_hashes.iter().any(|h| *h == addr) + }) + .unwrap_or(true); + + if is_new { + println!("New mint: {}", bs58::encode(mint.metadata.mint).into_string()); + } +} +``` + + + + +### Extract Token Metadata from Mint Extensions + +```rust +fn extract_metadata(mint: &CompressedMint) -> Option<(String, String, String)> { + let extensions = mint.extensions.as_ref()?; + + for ext in extensions { + if let ExtensionStruct::TokenMetadata(m) = ext { + let name = String::from_utf8_lossy(&m.name).to_string(); + let symbol = String::from_utf8_lossy(&m.symbol).to_string(); + let uri = String::from_utf8_lossy(&m.uri).to_string(); + return Some((name, symbol, uri)); + } + } + None +} +``` + +```rust +if let Some((name, symbol, uri)) = extract_metadata(&mint) { + println!(" Name: {}", name); + println!(" Symbol: {}", symbol); + println!(" URI: {}", uri); +} +``` + + + + + +## Data Layouts + +### Mint + +```rust +#[repr(C)] +pub struct CompressedMint { + pub base: BaseMint, + pub metadata: CompressedMintMetadata, + pub extensions: Option>, +} + +/// SPL compatible basemint structure (82 bytes) +#[repr(C)] +pub struct BaseMint { + pub mint_authority: Option, + pub supply: u64, + pub decimals: u8, + pub is_initialized: bool, + pub freeze_authority: Option, +} + +/// metadata used by the cToken program (34 bytes) +#[repr(C)] +pub struct CompressedMintMetadata { + pub version: u8, + pub spl_mint_initialized: bool, + pub mint: Pubkey, // PDA with compressed mint seed +} +``` + +### TokenMetadata + +```rust +#[repr(C)] +pub struct TokenMetadata { + pub update_authority: Pubkey, // [0u8; 32] = immutable + pub mint: Pubkey, + pub name: Vec, + pub symbol: Vec, + pub uri: Vec, + pub additional_metadata: Vec, +} + +pub struct AdditionalMetadata { + pub key: Vec, + pub value: Vec, +} +``` + +# Index Tokens + + \ No newline at end of file diff --git a/light-token/toolkits/for-streaming-tokens.mdx b/light-token/toolkits/for-streaming-tokens.mdx new file mode 100644 index 00000000..24205087 --- /dev/null +++ b/light-token/toolkits/for-streaming-tokens.mdx @@ -0,0 +1,34 @@ +--- +title: "Toolkit to Index Light-Token Accounts" +sidebarTitle: "for Indexing Tokens" +description: "Light-token accounts follow the same layout as SPL-token accounts, so you can reuse your existing parsers." +--- + +When a market becomes inactive, its token accounts and related PDAs will be compressed - their state is committed and effectively frozen until a client decompresses it. +While compressed, pure on-chain lookups will return uninitialized. + +Your indexer should keep tracking, quoting, and routing markets even if the on-chain account shows `is_initialized: false`, `is_compressed: true`. To trade a cold market, the first client must prepend an idempotent decompress "warm up" instruction. + +```rust +import { createLoadAtaInstructions } from "@lightprotocol/compressed-token/unified"; + +const warmupIxs = await createLoadAtaInstructions( + rpc, + tokenAccount, + owner, + mint, + payer +); + +new Transaction().add(...warmupIxs, tradeIx); +``` + +# Stream Light-Mint Accounts + + \ No newline at end of file diff --git a/light-token/toolkits/for-trading-apps.mdx b/light-token/toolkits/for-trading-apps.mdx new file mode 100644 index 00000000..289ef042 --- /dev/null +++ b/light-token/toolkits/for-trading-apps.mdx @@ -0,0 +1,5 @@ +--- +title: "light-token for Trading Apps" +sidebarTitle: "for Trading Apps" +description: "Guide to add light-token APIs to your Trading Application." +--- diff --git a/light-token/toolkits/for-wallets.mdx b/light-token/toolkits/for-wallets.mdx new file mode 100644 index 00000000..2c5f6159 --- /dev/null +++ b/light-token/toolkits/for-wallets.mdx @@ -0,0 +1,429 @@ +--- +title: "Toolkit for Wallet Applications" +sidebarTitle: "for Wallets" +description: "Guide for Wallet Applications to add light-token standard APIs." +--- +import { CodeCompare } from '/snippets/jsx/code-compare.jsx'; + +1. light-token follows the same API patterns like ATA +2. Your users receives the same stablecoin, just stored more efficiently. + +| Creation Cost | SPL | light-token | +|:---------------------|:------------------|:----------------------| +| **Token Account** | ~2,000,000 lamports | ~**17,000** lamports | + + +Get an [overview to the light-token standard here](/README). + +### What you will implement + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
light-tokenSPL Token
**Get/Create ATA**getOrCreateAtaInterface()getOrCreateAssociatedTokenAccount()
**Derive ATA**getAssociatedTokenAddressInterface()getAssociatedTokenAddress()
**Transfer**transferInterface()transferChecked()
**Get Balance**getAtaInterface()getAccount()
**Tx History**rpc.getSignaturesForOwnerInterface()getSignaturesForAddress()
**Exit to SPL**unwrap()N/A
+ + +Find devnet examples here [https://github.com/Lightprotocol/light-token-toolkit](https://github.com/Lightprotocol/light-token-toolkit). + + +## Setup + +```bash +npm install @lightprotocol/compressed-token/ + @lightprotocol/stateless.js +``` + +```typescript +import { createRpc } from "@lightprotocol/stateless.js"; + +import { + getOrCreateAtaInterface, + getAtaInterface, + getAssociatedTokenAddressInterface, + transferInterface, + unwrap, +} from "@lightprotocol/compressed-token"; + +const rpc = createRpc(RPC_ENDPOINT); +``` + +### Receive Payments + + + + +```typescript +import { getOrCreateAtaInterface } from "@lightprotocol/compressed-token"; + +const ata = await getOrCreateAtaInterface( + rpc, + payer, + mint, + recipient +); +// Share ata.parsed.address with sender + +console.log(ata.parsed.amount); +``` + + + +```typescript +import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; + +const ata = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + recipient +); +// Share ata.address with sender + +console.log(ata.amount); +``` + + + + + +```typescript +import { + createAssociatedTokenAccountInterfaceIdempotentInstruction, + createLoadAtaInstructions, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token"; +import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; + +const ata = getAssociatedTokenAddressInterface(mint, recipient); + +const tx = new Transaction().add( + createAssociatedTokenAccountInterfaceIdempotentInstruction( + payer.publicKey, + ata, + recipient, + mint, + CTOKEN_PROGRAM_ID + ), + ...(await createLoadAtaInstructions( + rpc, + ata, + recipient, + mint, + payer.publicKey + )) +); +``` + + + +```typescript +import { + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; + +const ata = getAssociatedTokenAddressSync(mint, recipient); + +const tx = new Transaction().add( + createAssociatedTokenAccountInstruction( + payer.publicKey, + ata, + recipient, + mint + ) +); +``` + + + + + +### Send Payments + + + + +```typescript +import { + getAssociatedTokenAddressInterface, + transferInterface, +} from "@lightprotocol/compressed-token"; + +const sourceAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); + +await transferInterface( + rpc, + payer, + sourceAta, + mint, + destinationAta, + owner, + amount +); +``` + +To ensure your recipient's ATA exists you can prepend an idempotent creation instruction in the same atomic transaction: + +```typescript +import { + getAssociatedTokenAddressInterface, + createAssociatedTokenAccountInterfaceIdempotentInstruction, +} from "@lightprotocol/compressed-token"; +import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; + +const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); +const createAtaIx = createAssociatedTokenAccountInterfaceIdempotentInstruction( + payer.publicKey, + destinationAta, + recipient, + mint, + CTOKEN_PROGRAM_ID +); + +new Transaction().add(createAtaIx, transferIx); +``` + + + +```typescript +import { transfer } from "@solana/spl-token"; + +const sourceAta = getAssociatedTokenAddressSync(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressSync(mint, recipient); + +await transfer( + connection, + payer, + sourceAta, + destinationAta, + owner, + amount, + decimals +); +``` + +With idempotent ATA creation: + +```typescript +import { + getAssociatedTokenAddressSync, + createAssociatedTokenAccountIdempotentInstruction, +} from "@solana/spl-token"; + +const destinationAta = getAssociatedTokenAddressSync(mint, recipient); +const createAtaIx = createAssociatedTokenAccountIdempotentInstruction( + payer.publicKey, + destinationAta, + recipient, + mint +); + +new Transaction().add(createAtaIx, transferIx); +``` + + + + + + +```typescript +import { + createLoadAtaInstructions, + createTransferInterfaceInstruction, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token"; + +const sourceAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); + +const tx = new Transaction().add( + ...(await createLoadAtaInstructions( + rpc, + sourceAta, + owner.publicKey, + mint, + payer.publicKey + )), + createTransferInterfaceInstruction( + sourceAta, + destinationAta, + owner.publicKey, + amount + ) +); +``` + + + +```typescript +import { + getAssociatedTokenAddressSync, + createTransferInstruction, +} from "@solana/spl-token"; + +const sourceAta = getAssociatedTokenAddressSync(mint, owner.publicKey); +const destinationAta = getAssociatedTokenAddressSync(mint, recipient); + +const tx = new Transaction().add( + createTransferInstruction(sourceAta, destinationAta, owner.publicKey, amount) +); +``` + + + + + + +### Show Balance + +```typescript +import { + getAssociatedTokenAddressInterface, + getAtaInterface, +} from "@lightprotocol/compressed-token"; + +const ata = getAssociatedTokenAddressInterface(mint, owner); +const account = await getAtaInterface( + rpc, + ata, + owner, + mint +); + +console.log(account.parsed.amount); +``` + + + +```typescript +import { getAccount } from "@solana/spl-token"; + +const account = await getAccount(connection, ata); + +console.log(account.amount); +``` + + + +### Transaction History + +Token accounts are compressed when inactive + +```typescript +const result = await rpc.getSignaturesForOwnerInterface(owner); + +console.log(result.signatures); +console.log(result.solana); +console.log(result.compressed); +``` + +Use `getSignaturesForAddressInterface(address)` if you want address-specific rather than owner-wide history. + + + +```typescript +const signatures = await connection.getSignaturesForAddress(ata); +``` + + + +### Unwrap to SPL + +When users need SPL tokens (CEX withdrawal, legacy integration): + + + + +```typescript +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; + +// SPL ATA must exist +const splAta = getAssociatedTokenAddressSync(mint, owner.publicKey); + +await unwrap( + rpc, + payer, + owner, + mint, + splAta, + amount +); +``` + + + + +```typescript +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + createLoadAtaInstructions, + createUnwrapInstruction, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token"; +import { getSplInterfaceInfos } from "@lightprotocol/compressed-token"; + +const ctokenAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); +const splAta = getAssociatedTokenAddressSync(mint, owner.publicKey); + +const splInterfaceInfos = await getSplInterfaceInfos(rpc, mint); +const splInterfaceInfo = splInterfaceInfos.find((i) => i.isInitialized); + +const tx = new Transaction().add( + ...(await createLoadAtaInstructions( + rpc, + ctokenAta, + owner.publicKey, + mint, + payer.publicKey + )), + createUnwrapInstruction( + ctokenAta, + splAta, + owner.publicKey, + mint, + amount, + splInterfaceInfo + ) +); +``` + + + diff --git a/quickstart.mdx b/quickstart.mdx index 3ed0697e..9391ef37 100644 --- a/quickstart.mdx +++ b/quickstart.mdx @@ -160,7 +160,7 @@ You've created and minted compressed tokens. The output shows: title=" Learn more about compressed tokens." icon="chevron-right" color="#0066ff" - href="/compressed-tokens/overview" + href="/compressed-tokens" horizontal >
diff --git a/rent.mdx b/rent.mdx new file mode 100644 index 00000000..ed1bc1a5 --- /dev/null +++ b/rent.mdx @@ -0,0 +1,44 @@ +--- +title: Rent Calculator +description: Calculate rent for Solana vs Compressed Accounts. +--- + +import { SolanaRentCalculator } from '/snippets/jsx/solana-rent-calculator.jsx'; +import { CompressibleRentCalculator } from '/snippets/jsx/compressible-rent-calculator.jsx'; +import { TokenAccountCompressedVsSpl } from '/snippets/jsx/token-account-compressed-vs-spl.jsx'; +import { BreakEvenCalculator } from '/snippets/jsx/break-even-calculator.jsx'; + +## Solana Rent + +Standard Solana accounts require a rent-exempt balance equal to 2 years of rent, locked for the account's lifetime. + + +```rust +minimum_balance = (ACCOUNT_STORAGE_OVERHEAD + data_len) * lamports_per_byte + +// Example 165-byte SPL token account: +// (128 + 165) * 6960 = 2,039,280 lamports +``` + +## Compressible Account Rent + +Compressible accounts only require rent for the prepaid epochs. Accounts auto-compress when inactive. + + + +```rust +rent_per_epoch = base_rent + (data_len * lamports_per_byte_per_epoch) + +// For 260-byte light-token account: +// 128 + (260 * 1) = 388 lamports per epoch +``` + +## Cost Comparison + + + +## Break-Even Analysis + +Find the break-even point where compressed accounts become more expensive than Solana accounts based on expected write frequency. + + diff --git a/resources/addresses-and-urls.mdx b/resources/addresses-and-urls.mdx index 1d993de9..85ed904e 100644 --- a/resources/addresses-and-urls.mdx +++ b/resources/addresses-and-urls.mdx @@ -3,7 +3,7 @@ title: "Addresses and URLs" description: "Overview to all of ZK Compression's RPC URLs, Program IDs & Accounts and Lookup Tables." --- -import SystemAccountsList from '/snippets/compressed-pdas-system-accounts-list.mdx'; +import SystemAccountsList from '/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx'; ## RPC URLs @@ -104,11 +104,15 @@ Find all JSON RPC Methods for ZK Compression [here](/api-reference/json-rpc-meth -## Token Escrow PDA +## Interface PDA + +The account to convert format between: +* compressed token ↔ SPL token +* light-token ↔ SPL token | | | |:-|:-| -| Token Escrow Owner PDA | **GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy** | +| Interface PDA | **GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy** | ## Lookup Tables @@ -174,7 +178,7 @@ Start building with Compressed Tokens or PDAs title="Compressed Tokens" icon="chevron-right" color="#0066ff" - href="/compressed-tokens/overview" + href="/compressed-tokens" horizontal /> 0) { + if (line ~ /^```$/) { + print line + in_section = 0 + break + } + } + next + } + { print } + ' "$docs_file" > "$docs_file.tmp" + + if [[ "$DRY_RUN" == "true" ]]; then + echo "--- Changes for $docs_file ---" + diff "$docs_file" "$docs_file.tmp" || true + rm "$docs_file.tmp" + echo "" + else + mv "$docs_file.tmp" "$docs_file" + echo "✓ Updated $docs_file" + fi + else + if [[ ! -f "$source_file" ]]; then + echo "⚠ Source file not found: $source_file" + fi + if [[ ! -f "$docs_file" ]]; then + echo "⚠ Docs file not found: $docs_file" + fi + fi +done + +echo "Done syncing handler code to docs" diff --git a/snippets/accounts-list/close-account-infos-accounts-list.mdx b/snippets/accounts-list/close-account-infos-accounts-list.mdx new file mode 100644 index 00000000..5acf1d87 --- /dev/null +++ b/snippets/accounts-list/close-account-infos-accounts-list.mdx @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Token Program-The light-token program for CPI.
AccountmutableThe light-token account to close.
DestinationmutableReceives remaining lamports from the closed account.
Ownersigner* + - Owner of the light-token account.
+ - *Must be signer for `invoke()`. For `invoke_signed()`, program signs via PDA seeds. +
Rent Sponsormutable, optional + * light-token program PDA that fronts rent exemption at creation. + * Claims rent when account compresses and/or is closed. +
diff --git a/snippets/compressed-pdas-system-accounts-list.mdx b/snippets/accounts-list/compressed-pdas-system-accounts-list.mdx similarity index 100% rename from snippets/compressed-pdas-system-accounts-list.mdx rename to snippets/accounts-list/compressed-pdas-system-accounts-list.mdx diff --git a/snippets/accounts-list/light-mint-system-accounts-list.mdx b/snippets/accounts-list/light-mint-system-accounts-list.mdx new file mode 100644 index 00000000..0002ffe0 --- /dev/null +++ b/snippets/accounts-list/light-mint-system-accounts-list.mdx @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AccountDescription
1Light System ProgramVerifies validity proofs and executes compressed account state transitions.
2CPI Authority PDAPDA that authorizes CPIs from the Compressed Token Program to the Light System Program.
3Registered Program PDAProves the Compressed Token Program is registered to use compression.
4Account Compression AuthoritySigns CPI calls from the Light System Program to the Account Compression Program.
5Account Compression ProgramWrites to state and address Merkle tree accounts.
6System ProgramSolana System Program.
\ No newline at end of file diff --git a/snippets/accounts-list/light-mint-to-light-token-accounts-list.mdx b/snippets/accounts-list/light-mint-to-light-token-accounts-list.mdx new file mode 100644 index 00000000..e26aabef --- /dev/null +++ b/snippets/accounts-list/light-mint-to-light-token-accounts-list.mdx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AccountTypeDescription
AuthoritysignerThe mint authority. Must match the light-mint's mint authority.
Payersigner, mutablePays transaction fees.
State TreemutableAccount of the state Merkle tree storing the light-mint.
Input QueuemutableAccount of the input queue associated with the state tree the light-mint is stored in. The existing hash of the light-mint is inserted to this queue to mark it as spent.
Output QueuemutableAccount of the output queue associated with the state tree the light-mint will be stored in. The updated hash of the light-mint is inserted to this queue.
light-token AccountsmutableDestination light-token Solana accounts to receive minted tokens.
System Accounts-See System Accounts List above.
CPI Contextoptional
(none for most)
Enables batched compressed account operations across multiple programs with a single validity proof. Set to `None` for most operations.
CPI Context Accountoptional, mutableOn-chain account that temporarily stores instruction data from multiple CPIs for combined execution. Set to `None` for most operations.
diff --git a/snippets/accounts-list/light-token-create-accounts-list-client.mdx b/snippets/accounts-list/light-token-create-accounts-list-client.mdx new file mode 100644 index 00000000..de0623f4 --- /dev/null +++ b/snippets/accounts-list/light-token-create-accounts-list-client.mdx @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstraintsDescription
0Payersigner, mutable + - Pays transaction fee and compression incentive (prepaid epochs).
+ - Does NOT pay rent exemption (fronted by `rent_sponsor`). +
1Token Accountsigner*, mutable + - The light-token account being created.
+ - *Must be signer for `invoke()`. + - *For `invoke_signed()`, program signs via PDA seeds. +
2Mint-The SPL or light-mint token mint. Defines which token this account holds.
3Compressible Config- + Protocol PDA that stores rent config and compression authorities. +
4System Program-Solana System Program. Required for CPI to create the on-chain account.
5Rent Sponsormutable + - light-token program PDA that fronts rent exemption at creation.
+ - Claims rent when account compresses. +
6light-token Program- + - Program to create and interact with light-mints, light-tokens and compressed tokens. + - Your program calls this to create the light-token account. +
\ No newline at end of file diff --git a/snippets/accounts-list/light-token-create-accounts-list.mdx b/snippets/accounts-list/light-token-create-accounts-list.mdx new file mode 100644 index 00000000..024f2a0c --- /dev/null +++ b/snippets/accounts-list/light-token-create-accounts-list.mdx @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Payersigner, mutable + - Pays initial rent per epoch, transaction fee and compression incentive.
+ - Does NOT pay rent exemption (fronted by `rent_sponsor`). +
light-token Accountsigner*, mutable + - The light-token account being created.
+ - *Must be signer for `invoke()`. For `invoke_signed()`, program signs via PDA seeds. +
Mint-The SPL or light-mint token mint.
OwnerPubkeyThe owner of the token account. Controls transfers and other operations.
diff --git a/snippets/accounts-list/light-token-create-ata-accounts-list.mdx b/snippets/accounts-list/light-token-create-ata-accounts-list.mdx new file mode 100644 index 00000000..500c7435 --- /dev/null +++ b/snippets/accounts-list/light-token-create-ata-accounts-list.mdx @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Owner- + - The wallet that will own this light-ATA.
+ - Used to derive the light-ATA address deterministically. +
Mint- + - The SPL or light-mint token mint.
+ - Used to derive the light-ATA address deterministically. +
Payersigner, mutable + - Pays initial rent per epoch, transaction fee and compression incentive.
+ - Does NOT pay rent exemption (fronted by `rent_sponsor`). +
light-ATA Accountmutable + - The light-ATA being created.
+ - Address is derived from `[owner, ctoken_program_id, mint]`. +
System Program-Solana System Program. Required for CPI to create the on-chain account.
Bumpu8The PDA bump seed for the light-ATA address derivation.
Idempotentbool + - When `true`, silently succeeds if account already exists.
+ - When `false`, fails if account already exists. +
diff --git a/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx b/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx new file mode 100644 index 00000000..04aa6471 --- /dev/null +++ b/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Source AccountmutableThe source account (SPL token account or light-token account).
Destination AccountmutableThe destination account (SPL token account or light-token account).
Authoritysigner* + - Owner of the source account.
+ - *Must be signer for `invoke()`. For `invoke_signed()`, program signs via PDA seeds. +
Payersigner, mutablePays transaction fees.
Compressed Token Program Authority-The light-token program authority PDA.
diff --git a/snippets/accounts-list/transfer-interface-accounts-list-spl-2.mdx b/snippets/accounts-list/transfer-interface-accounts-list-spl-2.mdx new file mode 100644 index 00000000..4a2b17ea --- /dev/null +++ b/snippets/accounts-list/transfer-interface-accounts-list-spl-2.mdx @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Mint-The SPL token mint.
SPL Token Program-The SPL Token program.
Interface PDAmutableInterface PDA for SPL ↔ light-token transfers.
diff --git a/snippets/compressible-default-rent-config.mdx b/snippets/compressible-default-rent-config.mdx new file mode 100644 index 00000000..72b18de4 --- /dev/null +++ b/snippets/compressible-default-rent-config.mdx @@ -0,0 +1,3 @@ +### Default Rent Config +1. At account creation, you pay ~17,208 lamports for 24h of rent
and compression incentive (the rent-exemption is sponsored by the protocol) +2. Transfers keep the account funded with rent for 3h via top-ups. The transaction payer tops up 776 lamports when the account's rent is below 3h. diff --git a/snippets/compressible-rent-explained.mdx b/snippets/compressible-rent-explained.mdx new file mode 100644 index 00000000..1b9428b9 --- /dev/null +++ b/snippets/compressible-rent-explained.mdx @@ -0,0 +1,38 @@ +1. The rent-exemption for light-token account creation is sponsored by Light Protocol. +2. Transaction payer's pay rent per rent-epoch (388 lamports for 1.5h)
+to keep accounts "active". +3. "Inactive" accounts, where rent is below one epoch, are compressed
and the rent-exemption can be claimed by the rent sponsor. +4. Transfers to inactive accounts "load" it with the same state (decompress). + +This way rent is automatically paid when accounts are used: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EventTotal CostPayerTime of Rent funded
Account Creation~17,000 lamportsTransaction payerFunds 24h rent
Automatic Top ups
(when rent < 3h)
776 lamportsTransaction payerFunds 3h rent
Load Account
(when inactive)
~17,000 lamportsTransaction payerFunds 24h rent
\ No newline at end of file diff --git a/snippets/compressible-vs-solana-rent.mdx b/snippets/compressible-vs-solana-rent.mdx new file mode 100644 index 00000000..81e6ea47 --- /dev/null +++ b/snippets/compressible-vs-solana-rent.mdx @@ -0,0 +1,93 @@ +### Initial Rent Top-Up +The **creator of compressible accounts** tops-up the account with **rent for at least two epochs**. +``` rust +rent_per_epoch = base_rent + (data_len * lamports_per_byte_per_epoch) + +// For 260-byte light-token account: +// 128 + (260 * 1) = 388 lamports per epoch +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentAmountDescription
Prepaid rent388 × N epochs*Rent for N epochs
Compression cost & incentive10,000 lamports
+ 1,000 lamports
Transaction cost for compression
+ protocol incentive
Transaction fee5,000 lamportsStandard Solana tx fees (excl. priority fee)
+ +\* _`N` = `num_prepaid_epochs` parameter (0 or ≥ 2, set at creation)._ + +Normally, the **creator of Solana accounts** pays the **rent exemption balance equal to 2 years of rent** proportional to account size: + +``` rust +minimum_balance = (ACCOUNT_STORAGE_OVERHEAD + data_len) * lamports_per_byte * exemption_threshold + +/// Example 165 byte SPL token account: +/// minimum_balance = (128 + 165) * 6960 = 2,039,280 lamports +``` + +* Most Solana accounts are rarely accessed after creation, but continue to lock up SOL for the account's lifetime. +* The creator of Solana accounts still must pay the rent-exemption balance. +* The creator of compressible accounts only needs to pay the rent for the first two epochs. + + + +### Top-ups per Transaction + +The **transaction payer tops-up** the account with rent **when the account's lamports balance is below 2 epochs**. This keeps accounts decompressed while active. + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account StatePayer CostExample
Funded for 2+ epochs0 lamportsNo top-up required
Funded for less than 2 epochs`lamports_per_write`Configured as 5,000 lamports
→ payer pays 5,000 lamports
Compressible
(ran out of rent)
`lamports_per_write + rent_deficit`Configured top-up: 5,000 lamports
Rent deficit: 3,000 lamports
Total payer cost: 8,000 lamports
+ + +Top-ups are additional to standard Solana transaction fees (typically <10,000 lamports). + diff --git a/snippets/custom-rent-config.md b/snippets/custom-rent-config.md new file mode 100644 index 00000000..1c43f909 --- /dev/null +++ b/snippets/custom-rent-config.md @@ -0,0 +1,23 @@ + + + +We recommend to use default values, but you can customize the rent config based on the expected account activity. + + +1. Set the hours to determine the amount of prepaid rent +2. Set the lamports per write a transaction payer will pay for top ups + +```rust +CompressibleParamsInfos{ + compressible_config, + rent_sponsor, + system_program, + pre_pay_num_epochs: 35, + lamports_per_write: Some(788), + compress_to_account_pubkey: None, + token_account_version: TokenDataVersion::ShaFlat, + } +``` + + + \ No newline at end of file diff --git a/snippets/jsx/break-even-calculator.jsx b/snippets/jsx/break-even-calculator.jsx new file mode 100644 index 00000000..8aa8ddcd --- /dev/null +++ b/snippets/jsx/break-even-calculator.jsx @@ -0,0 +1,284 @@ +export const BreakEvenCalculator = () => { + const [dataLen, setDataLen] = useState(100); + const [numAccounts, setNumAccounts] = useState(1000); + const [numWrites, setNumWrites] = useState(10); + const [priorityFeeRate, setPriorityFeeRate] = useState(1000); + const [showPrioritySlider, setShowPrioritySlider] = useState(false); + + const ACCOUNT_STORAGE_OVERHEAD = 128; + const LAMPORTS_PER_BYTE = 6960; + const BASE_COST_PER_WRITE = 10300; + const COMPRESSED_CU_PER_WRITE = 300000; + const LAMPORTS_PER_SOL = 1_000_000_000; + + const priorityFeePerWrite = Math.floor((COMPRESSED_CU_PER_WRITE * priorityFeeRate) / 1_000_000); + const costPerWrite = BASE_COST_PER_WRITE + priorityFeePerWrite; + + const solanaCostPerAccount = (ACCOUNT_STORAGE_OVERHEAD + dataLen) * LAMPORTS_PER_BYTE; + const solanaCost = numAccounts * solanaCostPerAccount; + const compressedCostPerAccount = costPerWrite * (1 + numWrites); + const compressedCost = numAccounts * compressedCostPerAccount; + const breakEvenWrites = Math.floor((solanaCostPerAccount / costPerWrite) - 1); + const useCompressed = numWrites < breakEvenWrites; + + const handleDataLenChange = (value) => { + const num = Math.max(0, Math.min(10000, Number.parseInt(value) || 0)); + setDataLen(num); + }; + + const handleAccountsChange = (value) => { + const num = Math.max(1, Math.min(1000000, Number.parseInt(value) || 1)); + setNumAccounts(num); + }; + + const handleWritesChange = (value) => { + const num = Math.max(0, Math.min(10000, Number.parseInt(value) || 0)); + setNumWrites(num); + }; + + const handlePriorityFeeChange = (value) => { + const num = Math.max(0, Math.min(100000, Number.parseInt(value) || 0)); + setPriorityFeeRate(num); + }; + + const formatSOL = (lamports) => { + const sol = lamports / LAMPORTS_PER_SOL; + if (sol >= 1000) { + return sol.toLocaleString(undefined, { maximumFractionDigits: 2 }); + } else if (sol >= 1) { + return sol.toFixed(4); + } + return sol.toFixed(6); + }; + + return ( +
+
+ {/* Inputs */} +
+
+ + Account Size + + handleDataLenChange(e.target.value)} + className="w-20 px-2 py-1 text-right font-mono font-medium bg-black/[0.015] dark:bg-white/10 border border-black/[0.04] dark:border-white/20 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> + bytes + + + setDataLen(Number.parseInt(e.target.value))} + className="w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm" + /> +
+ + {/* Preset buttons */} +
+ + + +
+ +
+ + Number of Accounts + handleAccountsChange(e.target.value)} + className="w-28 px-2 py-1 text-right font-mono font-medium bg-black/[0.015] dark:bg-white/10 border border-black/[0.04] dark:border-white/20 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> + + setNumAccounts(Number.parseInt(e.target.value))} + className="w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm" + /> +
+ +
+ + Expected Writes (per account) + handleWritesChange(e.target.value)} + className="w-20 px-2 py-1 text-right font-mono font-medium bg-black/[0.015] dark:bg-white/10 border border-black/[0.04] dark:border-white/20 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> + + setNumWrites(Number.parseInt(e.target.value))} + className="w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm" + /> +
+ +
+ + Priority Fee + + +{priorityFeePerWrite.toLocaleString()} lamports/write + + +
+ + + + +
+ {showPrioritySlider && ( +
+
+ setPriorityFeeRate(Number.parseInt(e.target.value))} + className="flex-1 h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm" + /> + + handlePriorityFeeChange(e.target.value)} + className="w-20 px-2 py-1 text-right font-mono font-medium bg-black/[0.015] dark:bg-white/10 border border-black/[0.04] dark:border-white/20 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> + μL/CU + +
+ + {COMPRESSED_CU_PER_WRITE.toLocaleString()} CU × {priorityFeeRate.toLocaleString()} μL/CU + +
+ )} +
+
+ + {/* Results */} +
Results
+
+
+
Solana Cost
+
+ {formatSOL(solanaCost)} +
+
SOL
+
+ +
+
Compressed Cost
+
+ {formatSOL(compressedCost)} +
+
SOL
+
+ +
+
Break-even
+
+ {breakEvenWrites.toLocaleString()} +
+
writes
+
+ +
+
Recommendation
+
+ {useCompressed ? 'Use Compressed' : 'Use Solana'} +
+
+
+ + {/* Formula reference */} +
+ Solana: ({ACCOUNT_STORAGE_OVERHEAD} + {dataLen}) × {LAMPORTS_PER_BYTE.toLocaleString()} = {solanaCostPerAccount.toLocaleString()} lamports/account
+ Compressed: ({BASE_COST_PER_WRITE.toLocaleString()} + {priorityFeePerWrite.toLocaleString()}) × (1 + {numWrites}) = {compressedCostPerAccount.toLocaleString()} lamports/account
+ Cost per write: {BASE_COST_PER_WRITE.toLocaleString()} base + {priorityFeePerWrite.toLocaleString()} priority ({COMPRESSED_CU_PER_WRITE.toLocaleString()} CU) = {costPerWrite.toLocaleString()} lamports +
+
+
+ ); +}; diff --git a/snippets/jsx/code-compare.jsx b/snippets/jsx/code-compare.jsx new file mode 100644 index 00000000..44900a12 --- /dev/null +++ b/snippets/jsx/code-compare.jsx @@ -0,0 +1,286 @@ +export const CodeCompare = ({ + firstCode = "", + secondCode = "", + firstLabel = "Light-Token", + secondLabel = "SPL", + initialPosition = 50, +}) => { + const [sliderPercent, setSliderPercent] = useState(initialPosition); + const [isDragging, setIsDragging] = useState(false); + const [copied, setCopied] = useState(false); + const [hasInteracted, setHasInteracted] = useState(false); + const containerRef = useRef(null); + + // Syntax highlighting for TypeScript - single-pass to avoid regex conflicts + const highlightCode = (code) => { + // HTML escape first + let escaped = code + .replace(/&/g, "&") + .replace(//g, ">"); + + // Combined pattern that matches in priority order + const pattern = /(\/\/.*$)|(["'`])(?:(?!\2)[^\\]|\\.)*?\2|\b(const|let|var|await|async|import|from|export|return|if|else|function|class|new|throw|try|catch)\b|\.([a-zA-Z_][a-zA-Z0-9_]*)\b|\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/gm; + + return escaped.replace(pattern, (match, comment, stringQuote, keyword, property, func) => { + if (comment) { + return `${match}`; + } + if (stringQuote) { + return `${match}`; + } + if (keyword) { + return `${match}`; + } + if (property) { + return `.${property}`; + } + if (func) { + return `${match}`; + } + return match; + }); + }; + + const handleCopy = () => { + const visibleCode = sliderPercent > 50 ? firstCode : secondCode; + navigator.clipboard.writeText(visibleCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handleMouseDown = (e) => { + e.preventDefault(); + setIsDragging(true); + setHasInteracted(true); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleMouseMove = (e) => { + if (!isDragging || !containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percent = Math.max(0, Math.min(100, (x / rect.width) * 100)); + setSliderPercent(percent); + }; + + const handleTouchMove = (e) => { + if (!containerRef.current) return; + setHasInteracted(true); + const rect = containerRef.current.getBoundingClientRect(); + const x = e.touches[0].clientX - rect.left; + const percent = Math.max(0, Math.min(100, (x / rect.width) * 100)); + setSliderPercent(percent); + }; + + const handleKeyDown = (e) => { + setHasInteracted(true); + if (e.key === "ArrowLeft") { + setSliderPercent((p) => Math.max(0, p - 5)); + } else if (e.key === "ArrowRight") { + setSliderPercent((p) => Math.min(100, p + 5)); + } + }; + + // Auto-demo animation - slides the actual slider to show functionality + useEffect(() => { + if (hasInteracted) return; + + let animationId; + let startTime; + const duration = 2000; // 2s per cycle + const startPos = initialPosition; + const endPos = 20; // Slide slightly to hint at functionality + + const animate = (timestamp) => { + if (!startTime) startTime = timestamp; + const elapsed = timestamp - startTime; + const cycle = (elapsed % (duration * 2)) / duration; // 0-2 range + + // Ease in-out: go to endPos then back to startPos + let progress; + if (cycle < 1) { + progress = cycle; // Going to endPos + } else { + progress = 2 - cycle; // Coming back to startPos + } + + // Smooth easing + const eased = progress < 0.5 + ? 2 * progress * progress + : 1 - Math.pow(-2 * progress + 2, 2) / 2; + + const newPos = startPos + (endPos - startPos) * eased; + setSliderPercent(newPos); + + // Stop after 1 full cycle (4 seconds) + if (elapsed < duration * 2) { + animationId = requestAnimationFrame(animate); + } else { + setSliderPercent(initialPosition); + } + }; + + // Start after a short delay + const timeout = setTimeout(() => { + animationId = requestAnimationFrame(animate); + }, 500); + + return () => { + clearTimeout(timeout); + if (animationId) cancelAnimationFrame(animationId); + }; + }, [hasInteracted, initialPosition]); + + useEffect(() => { + if (isDragging) { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + } + }, [isDragging]); + + // Icon components + const CopyIcon = () => ( + + + + + ); + + const CheckIcon = () => ( + + + + ); + + const ExternalIcon = () => ( + + + + + + ); + + return ( + <> +
+ {/* Code container - no header, just code */} +
+ {/* Floating copy button */} + + {/* Second code (background - SPL) */} +
+
+        {/* First code (foreground - Light-Token) with clip-path - MUST be opaque to cover background */}
+        
+
+        {/* Divider line */}
+        
+ {/* Grey line */} +
+ + {/* Blue glow fading to left starting at the line */} +
+ + {/* Handle button */} +
+ {/* Grip dots */} +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+
+
+
+
+ + ); +}; diff --git a/snippets/jsx/compressible-rent-calculator.jsx b/snippets/jsx/compressible-rent-calculator.jsx new file mode 100644 index 00000000..c798f0c2 --- /dev/null +++ b/snippets/jsx/compressible-rent-calculator.jsx @@ -0,0 +1,210 @@ +export const CompressibleRentCalculator = () => { + const [hours, setHours] = useState(24); + const [lamportsPerWrite, setLamportsPerWrite] = useState(776); + const [showCustomHours, setShowCustomHours] = useState(false); + const [showCustomLamports, setShowCustomLamports] = useState(false); + const [showFormula, setShowFormula] = useState(false); + + const DATA_LEN = 260; + const BASE_RENT = 128; + const LAMPORTS_PER_BYTE_PER_EPOCH = 1; + const MINUTES_PER_EPOCH = 90; + const COMPRESSION_INCENTIVE = 11000; + const LAMPORTS_PER_SOL = 1_000_000_000; + + const HOURS_MAX = 36; + const LAMPORTS_MAX = 6400; + + const numEpochs = Math.ceil((hours * 60) / MINUTES_PER_EPOCH); + const rentPerEpoch = BASE_RENT + (DATA_LEN * LAMPORTS_PER_BYTE_PER_EPOCH); + const totalPrepaidRent = rentPerEpoch * numEpochs; + const totalCreationCost = totalPrepaidRent + COMPRESSION_INCENTIVE; + + const handleHoursChange = (value) => { + const num = Math.max(3, Math.min(168, Number.parseInt(value) || 3)); + setHours(num); + }; + + const handleLamportsChange = (value) => { + const num = Math.max(0, Math.min(100000, Number.parseInt(value) || 0)); + setLamportsPerWrite(num); + }; + + const hoursPresets = [24]; + const lamportsPresets = [776]; + + const SliderMarkers = ({ max, step }) => { + const marks = []; + for (let i = step; i < max; i += step) { + const percent = (i / max) * 100; + marks.push( +
+ ); + } + return <>{marks}; + }; + + return ( +
+
+ {/* Prepaid Epochs in Hours */} +
+
+ Prepaid Epochs in Hours +
+ {hoursPresets.map((h) => ( + + ))} + {showCustomHours ? ( + handleHoursChange(e.target.value)} + className="w-16 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + autoFocus + /> + ) : ( + + )} +
+
+
+ + ≈ {((hours * 60) / MINUTES_PER_EPOCH).toFixed(1)} epochs / {hours.toFixed(1)}h + +
+ + { setHours(Number.parseInt(e.target.value)); setShowCustomHours(false); }} + className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" + /> +
+
+
+ + {/* Lamports per Write */} +
+
+ Lamports per Write +
+ {lamportsPresets.map((l) => ( + + ))} + {showCustomLamports ? ( + handleLamportsChange(e.target.value)} + className="w-20 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + autoFocus + /> + ) : ( + + )} +
+
+
+ + ≈ {(lamportsPerWrite / rentPerEpoch).toFixed(1)} epochs / {((lamportsPerWrite / rentPerEpoch) * MINUTES_PER_EPOCH / 60).toFixed(1)}h + +
+ + { setLamportsPerWrite(Number.parseInt(e.target.value)); setShowCustomLamports(false); }} + className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" + /> +
+
+
+ + {/* Result Cards */} +
+
+
Total Creation Cost
+
+ {totalCreationCost.toLocaleString()} +
+
lamports
+
≈ {(totalCreationCost / LAMPORTS_PER_SOL).toFixed(6)} SOL
+
+ +
+
Top-up Amount
+
+ {lamportsPerWrite.toLocaleString()} +
+
lamports
+
≈ {(lamportsPerWrite / LAMPORTS_PER_SOL).toFixed(6)} SOL
+
+
+ + {/* Formula reference - toggleable */} +
+ + {showFormula && ( +
+
Total cost for {DATA_LEN}-byte light-token account:
+ total_creation_cost = prepaid_rent + compression_incentive

+ rent_per_epoch = base_rent + (data_len × lamports_per_byte_per_epoch)
+ rent_per_epoch = {BASE_RENT} + ({DATA_LEN} × {LAMPORTS_PER_BYTE_PER_EPOCH}) = {rentPerEpoch} lamports
+ compression_incentive = {COMPRESSION_INCENTIVE.toLocaleString()} lamports +
+ )} +
+
+
+ ); +}; diff --git a/snippets/jsx/light-token-vs-spl-calculator.jsx b/snippets/jsx/light-token-vs-spl-calculator.jsx new file mode 100644 index 00000000..148d15a8 --- /dev/null +++ b/snippets/jsx/light-token-vs-spl-calculator.jsx @@ -0,0 +1,139 @@ +export const CTokenVsSplCalculator = () => { + const [numAccounts, setNumAccounts] = useState(100000); + const [showCustomAccounts, setShowCustomAccounts] = useState(false); + + const ACCOUNT_STORAGE_OVERHEAD = 128; + const LAMPORTS_PER_BYTE = 6960; + const DATA_LEN = 165; // SPL token account size + const CTOKEN_DEFAULT_CREATION_COST = 17208; // Default rent config: 6,208 prepaid rent (24h) + 11,000 compression incentive + const LAMPORTS_PER_SOL = 1_000_000_000; + + const ACCOUNTS_MAX = 1000000; + + const splCost = numAccounts * (ACCOUNT_STORAGE_OVERHEAD + DATA_LEN) * LAMPORTS_PER_BYTE; + const ctokenCost = numAccounts * CTOKEN_DEFAULT_CREATION_COST; + const savings = splCost - ctokenCost; + const savingsPercent = ((savings / splCost) * 100).toFixed(1); + + const handleAccountsChange = (value) => { + const num = Math.max(1, Math.min(1000000, Number.parseInt(value) || 1)); + setNumAccounts(num); + }; + + const formatSOL = (lamports) => { + const sol = lamports / LAMPORTS_PER_SOL; + if (sol >= 1000) { + return sol.toLocaleString(undefined, { maximumFractionDigits: 2 }); + } else if (sol >= 1) { + return sol.toFixed(4); + } + return sol.toFixed(6); + }; + + const accountsPresets = [10000, 100000, 500000]; + + const SliderMarkers = ({ max, step }) => { + const marks = []; + for (let i = step; i < max; i += step) { + const percent = (i / max) * 100; + marks.push( +
+ ); + } + return <>{marks}; + }; + + return ( +
+
+ {/* Number of Accounts */} +
+
+ Number of Token Accounts +
+ {accountsPresets.map((a) => ( + + ))} + {showCustomAccounts ? ( + handleAccountsChange(e.target.value)} + className="w-24 px-2 py-1 text-right text-xs font-mono font-medium bg-blue-500/10 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + autoFocus + /> + ) : ( + + )} +
+
+
+ + {numAccounts.toLocaleString()} accounts + +
+ + { setNumAccounts(Number.parseInt(e.target.value)); setShowCustomAccounts(false); }} + className="relative w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm z-10" + /> +
+
+
+ + {/* Result Cards */} +
+
+
SPL Token
+
+ {formatSOL(splCost)} +
+
SOL
+
+ +
+
Light-Token
+
+ {formatSOL(ctokenCost)} +
+
SOL
+
+ +
+
Savings
+
+ {savingsPercent}% +
+
{formatSOL(savings)} SOL
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/snippets/jsx/rent-lifecycle-visualizer.jsx b/snippets/jsx/rent-lifecycle-visualizer.jsx new file mode 100644 index 00000000..a55a2f4e --- /dev/null +++ b/snippets/jsx/rent-lifecycle-visualizer.jsx @@ -0,0 +1,625 @@ +export const RentLifecycleVisualizer = () => { + const [, setTime] = useState(0); + const [lamports, setLamports] = useState(0); + const [isRunning, setIsRunning] = useState(false); + const [phase, setPhase] = useState('uninitialized'); + const [hasUserClicked, setHasUserClicked] = useState(false); + const [showControls, setShowControls] = useState(false); + const containerRef = useRef(null); + const [isHighlighted, setIsHighlighted] = useState(false); + const [activeArrows, setActiveArrows] = useState([]); + const [activeLines, setActiveLines] = useState([]); + const [isButtonPressed, setIsButtonPressed] = useState(false); + const [flyingArrows, setFlyingArrows] = useState([]); + const [floatingAmounts, setFloatingAmounts] = useState([]); + const [resetCount, setResetCount] = useState(0); + const [timelineStarted, setTimelineStarted] = useState(false); + + // Constants from rent config + const LAMPORTS_PER_TICK = 58.2; // 582 per second = 1.5 epochs/s (1.5x speed) + const INITIAL_RENT = 6208; // 24h of rent (16 epochs × 388) + const TOPUP_LAMPORTS = 776; // 3h worth (2 epochs) + const TOPUP_THRESHOLD = 776; // Top up when below 3h of rent + const COLD_THRESHOLD = 388; // Cold when below 1 epoch of rent + // Time scale: 1.5x speed → ~10.7s to deplete, 29s total cycle + + // Colors + const GREY = { r: 161, g: 161, b: 170 }; + const RED = { r: 227, g: 89, b: 48 }; + const BLUE = { r: 120, g: 140, b: 180 }; // Subtle grey-blue + + // 7 transaction lines from edges to center (positioned around the visualization) + // Each line has start point (edge) and ends at center (50, 50) + const txLines = [ + { id: 0, x1: 5, y1: 20, x2: 50, y2: 50 }, // top-left + { id: 1, x1: 95, y1: 15, x2: 50, y2: 50 }, // top-right + { id: 2, x1: 0, y1: 50, x2: 50, y2: 50 }, // left + { id: 3, x1: 100, y1: 55, x2: 50, y2: 50 }, // right + { id: 4, x1: 10, y1: 85, x2: 50, y2: 50 }, // bottom-left + { id: 5, x1: 90, y1: 90, x2: 50, y2: 50 }, // bottom-right + { id: 6, x1: 50, y1: 0, x2: 50, y2: 50 }, // top + ]; + + const interpolateColor = (c1, c2, t) => { + const clamp = (v) => Math.max(0, Math.min(255, Math.round(v))); + return { + r: clamp(c1.r + (c2.r - c1.r) * t), + g: clamp(c1.g + (c2.g - c1.g) * t), + b: clamp(c1.b + (c2.b - c1.b) * t), + }; + }; + + const colorToRgba = (c, alpha = 1) => `rgba(${c.r}, ${c.g}, ${c.b}, ${alpha})`; + + // Format lamports: always ~X,XXX rounded to nearest 500 + const formatLamports = (l) => { + if (l <= 0) return '0'; + const rounded = Math.round(l / 500) * 500; + return `~${rounded.toLocaleString()}`; + }; + + const arrowIdRef = useRef(0); + const flyingArrowIdRef = useRef(0); + const floatingAmountIdRef = useRef(0); + + const triggerFlyingArrow = (amount, lineIndex) => { + const id = flyingArrowIdRef.current++; + setFlyingArrows((prev) => [...prev, id]); + setTimeout(() => { + setFlyingArrows((prev) => prev.filter((a) => a !== id)); + }, 600); + + // Also show floating amount at the source of the transaction line + const amountId = floatingAmountIdRef.current++; + const line = txLines[lineIndex] || txLines[0]; + setFloatingAmounts((prev) => [...prev, { id: amountId, amount, x: line.x1, y: line.y1 }]); + setTimeout(() => { + setFloatingAmounts((prev) => prev.filter((a) => a.id !== amountId)); + }, 800); + }; + + const triggerHighlight = () => { + setIsHighlighted(true); + setTimeout(() => setIsHighlighted(false), 500); + + // Add a new arrow with unique ID + const arrowId = arrowIdRef.current++; + setActiveArrows((prev) => [...prev, arrowId]); + setTimeout(() => { + setActiveArrows((prev) => prev.filter((id) => id !== arrowId)); + }, 500); + }; + + const triggerTransaction = (lineIndex) => { + setActiveLines((prev) => [...prev, { id: lineIndex, startTime: Date.now() }]); + setTimeout(() => { + setActiveLines((prev) => prev.filter((l) => l.id !== lineIndex)); + }, 500); + }; + + // Cycle through lines for transactions + const txLineIndexRef = useRef(0); + const lastLineIndexRef = useRef(0); + const getNextLineIndex = () => { + const index = txLineIndexRef.current; + txLineIndexRef.current = (txLineIndexRef.current + 1) % txLines.length; + lastLineIndexRef.current = index; + return index; + }; + + const getAccountColor = () => { + if (phase === 'uninitialized') return GREY; + if (phase === 'cold') return BLUE; + + // Color based on lamports threshold + if (lamports > TOPUP_THRESHOLD) { + // Above 776: stay red (hot) + return RED; + } else if (lamports > COLD_THRESHOLD) { + // Between 776 and 388: fade red → blue + const t = 1 - (lamports - COLD_THRESHOLD) / (TOPUP_THRESHOLD - COLD_THRESHOLD); + return interpolateColor(RED, BLUE, t); + } else { + // Below 388: cold (blue) + return BLUE; + } + }; + + const handleTopup = () => { + // Always trigger a transaction + triggerTransaction(getNextLineIndex()); + setIsButtonPressed(true); + setTimeout(() => setIsButtonPressed(false), 200); + + // If cold/uninitialized, re-initialize with full amount + if (phase === 'uninitialized' || phase === 'cold' || lamports === 0) { + setLamports(INITIAL_RENT); + setPhase('hot'); + triggerHighlight(); + triggerFlyingArrow(INITIAL_RENT, lastLineIndexRef.current); + return; + } + + // Only top up if below threshold (3h = 776 lamports) + if (lamports < TOPUP_THRESHOLD) { + setLamports((l) => l + TOPUP_LAMPORTS); + triggerHighlight(); + triggerFlyingArrow(TOPUP_LAMPORTS, lastLineIndexRef.current); + } + // Otherwise just the transaction happens (no rent top-up needed) + }; + + // 29-second transaction schedule (1.5x speed) + // 6208 lamports / 582/s = 10.7s to deplete + const txTimesRef = useRef([ + // Cycle 1: Init at 1.0s + // Activity (lamports > 776) + 1.3, 2, 2.7, 3.3, 4, 4.7, 5.3, 6, 6.7, 7.3, 8, 8.7, 9.3, + // Top-ups at ~10s when lamports < 776 + 10.1, 11.5, 12.8, + // Goes cold ~14.5s + + // Cycle 2: Reinit from cold at 16s + 16, + 16.7, 17.3, 18, 18.7, 19.3, 20, 20.7, 21.3, 22, 22.7, 23.3, 24, 24.7, + // Top-ups + 25.5, 26.8, + // Goes cold ~28.5s, loop at 29s + ]); + + const handleReset = () => { + setTime(0); + setLamports(0); + setPhase('uninitialized'); + setIsRunning(true); + setTimelineStarted(false); + setActiveLines([]); + setActiveArrows([]); + setFlyingArrows([]); + setResetCount((c) => c + 1); + txLineIndexRef.current = 0; + arrowIdRef.current = 0; + flyingArrowIdRef.current = 0; + }; + + // Handle click on diamond to start animation + const handleDiamondClick = () => { + if (!hasUserClicked) { + setHasUserClicked(true); + setIsRunning(true); + setShowControls(true); + } + }; + + useEffect(() => { + if (!isRunning) return; + + const interval = setInterval(() => { + setTime((t) => { + const newTime = t + 0.1; + + // Initialize at t=1.0 (after brief pause showing uninitialized state) + if (t < 1.0 && newTime >= 1.0) { + setLamports(INITIAL_RENT); + setPhase('hot'); + setTimelineStarted(true); + triggerHighlight(); + triggerTransaction(getNextLineIndex()); + } + + // Check for scheduled transactions + txTimesRef.current.forEach((txTime) => { + if (newTime >= txTime && t < txTime) { + // Transaction happens - show line animation + triggerTransaction(getNextLineIndex()); + + // Handle based on current state + if (phase === 'cold') { + // Reinitialize from cold state + setLamports(INITIAL_RENT); + setPhase('hot'); + triggerHighlight(); + triggerFlyingArrow(INITIAL_RENT, lastLineIndexRef.current); + } else if (phase === 'hot') { + // Only top up if below threshold (3h = 776 lamports = 2 epochs) + setLamports((currentLamports) => { + if (currentLamports > 0 && currentLamports < TOPUP_THRESHOLD) { + triggerHighlight(); + triggerFlyingArrow(TOPUP_LAMPORTS, lastLineIndexRef.current); + return currentLamports + TOPUP_LAMPORTS; + } + return currentLamports; + }); + } + } + }); + + // Smooth lamport decrement every tick (100ms) when hot or cold + if ((phase === 'hot' || phase === 'cold') && newTime > 0.1) { + setLamports((l) => { + const tickAmount = LAMPORTS_PER_TICK; + const newLamports = Math.max(0, l - tickAmount); + // Go cold when lamports below cold threshold + if (newLamports < COLD_THRESHOLD) { + setPhase('cold'); + } + return newLamports; + }); + } + + // Loop at 29s - reset to "Press" state + if (newTime >= 29) { + setPhase('uninitialized'); + setLamports(0); + setHasUserClicked(false); + setIsRunning(false); + setShowControls(false); + setTimelineStarted(false); + txLineIndexRef.current = 0; + return 0; + } + + return newTime; + }); + }, 100); + + return () => clearInterval(interval); + }, [isRunning, phase]); + + const accountColor = getAccountColor(); + + // Diamond dots pattern + const generateDiamondDots = () => { + const dots = []; + const size = 5; + const centerSize = 4; + + for (let row = -size; row <= size; row++) { + const width = size - Math.abs(row); + for (let col = -width; col <= width; col++) { + const distFromCenter = Math.max(Math.abs(row), Math.abs(col)); + // Exponential fade for smooth transition to white + const fadeProgress = distFromCenter / size; // 0 at center, 1 at edge + const dotSize = Math.max(0.3, centerSize * Math.pow(1 - fadeProgress, 1.5)); + const opacity = Math.pow(1 - fadeProgress, 2.5); // Aggressive exponential fade + + dots.push({ + x: 50 + col * 6, + y: 50 + row * 6, + size: dotSize, + opacity, + }); + } + } + return dots; + }; + + const diamondDots = generateDiamondDots(); + + const isLineActive = (lineId) => activeLines.some((l) => l.id === lineId); + + return ( +
+ {/* CSS for animations */} + + + {/* Main visualization area */} +
+ {/* Transaction lines SVG layer */} + + {txLines.map((line) => { + const active = isLineActive(line.id); + return ( + + {/* Line */} + + {/* Bobble that travels along the line when active */} + {active && ( + + )} + + ); + })} + {/* Floating amounts at transaction source */} + {floatingAmounts.map(({ id, amount, x, y }) => ( + + +{amount.toLocaleString()} + + ))} + + + {/* Timeline with fading edges and gap behind diamond */} +
+ {/* Continuously scrolling tick marks with hour labels below */} +
+
+ {[...Array(34).keys()].map(i => i * 3).concat([...Array(34).keys()].map(i => i * 3)).map((h, i) => ( +
+ + {h}h + +
+
+ ))} +
+
+ + {/* Center line */} +
+
+ + {/* Diamond Account - centered */} +
0 + ? 'drop-shadow(0 0 25px rgba(227, 89, 48, 0.7)) drop-shadow(0 0 10px rgba(255, 150, 50, 0.8))' + : 'none', + transition: 'filter 0.15s ease', + }} + > + + {diamondDots.map((dot, i) => ( + + ))} + + {/* Prompt text - shown before user clicks */} + {!hasUserClicked && ( +
+ Press to see the Rent Config over time! +
+ )} +
+
+ + {/* Controls - blur-to-clear fade in after user clicks */} +
+ {/* Rent Balance - centered */} +
+ {/* Arrows container - separate from number */} +
+ {activeArrows.map((arrowId) => ( + + ↑ + + ))} + {flyingArrows.map((id) => ( + + ↑ + + ))} +
+
+
+ + {formatLamports(lamports)} + + lamports +
+
+ Rent Balance +
+
+
+ + {/* Buttons row - centered, side by side */} +
+ + +
+
+
+ ); +}; diff --git a/snippets/jsx/solana-rent-calculator.jsx b/snippets/jsx/solana-rent-calculator.jsx new file mode 100644 index 00000000..53c01bc9 --- /dev/null +++ b/snippets/jsx/solana-rent-calculator.jsx @@ -0,0 +1,108 @@ +export const SolanaRentCalculator = () => { + const [dataLen, setDataLen] = useState(165); + + const ACCOUNT_STORAGE_OVERHEAD = 128; + const LAMPORTS_PER_BYTE = 6960; + + const minimumBalance = (ACCOUNT_STORAGE_OVERHEAD + dataLen) * LAMPORTS_PER_BYTE; + const solAmount = minimumBalance / 1_000_000_000; + + const handleDataLenChange = (value) => { + const num = Math.max(0, Math.min(10000, Number.parseInt(value) || 0)); + setDataLen(num); + }; + + return ( +
+
+ {/* Slider */} +
+
+ + Account Data Length + + handleDataLenChange(e.target.value)} + className="w-20 px-2 py-1 text-right font-mono font-medium bg-black/[0.015] dark:bg-white/10 border border-black/[0.04] dark:border-white/20 rounded-lg backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + /> + bytes + + + setDataLen(Number.parseInt(e.target.value))} + className="w-full h-1.5 bg-black/[0.03] dark:bg-white/20 rounded-full appearance-none cursor-pointer backdrop-blur-sm" + /> + Total size: {ACCOUNT_STORAGE_OVERHEAD + dataLen} bytes (incl. 128 byte overhead) +
+ + {/* Preset buttons */} +
+ + + +
+
+ + {/* Result Cards */} +
+
+
Rent-Exempt Balance
+
+ {minimumBalance.toLocaleString()} +
+
lamports
+
+ +
+
SOL Amount
+
+ {solAmount.toFixed(6)} +
+
SOL
+
+
+ + {/* Formula reference */} +
+ minimum_balance = (ACCOUNT_STORAGE_OVERHEAD + data_len) × lamports_per_byte
+ minimum_balance = ({ACCOUNT_STORAGE_OVERHEAD} + {dataLen}) × {LAMPORTS_PER_BYTE.toLocaleString()} = {minimumBalance.toLocaleString()} lamports +
+
+
+ ); +}; diff --git a/snippets/token-account-compressed-vs-spl.jsx b/snippets/jsx/token-account-compressed-vs-spl.jsx similarity index 99% rename from snippets/token-account-compressed-vs-spl.jsx rename to snippets/jsx/token-account-compressed-vs-spl.jsx index 2798bedd..c584bc78 100644 --- a/snippets/token-account-compressed-vs-spl.jsx +++ b/snippets/jsx/token-account-compressed-vs-spl.jsx @@ -48,7 +48,7 @@ export const TokenAccountCompressedVsSpl = () => { }; return ( -
+
{/* Number of Accounts */}
@@ -136,4 +136,4 @@ export const TokenAccountCompressedVsSpl = () => {
); -}; \ No newline at end of file +}; diff --git a/snippets/jsx/token22-extensions-table.jsx b/snippets/jsx/token22-extensions-table.jsx new file mode 100644 index 00000000..08473272 --- /dev/null +++ b/snippets/jsx/token22-extensions-table.jsx @@ -0,0 +1,177 @@ +export const Token22ExtensionsTable = () => { + const [activeFilter, setActiveFilter] = useState('all'); + const [selectedInstruction, setSelectedInstruction] = useState(null); + + // Instructions data + const anchorInstructions = [ + 'create_token_pool', + 'add_token_pool', + 'mint_to', + 'batch_compress', + 'compress_spl_token_account', + 'transfer', + 'approve', + 'revoke', + 'freeze', + 'thaw', + 'burn', + ]; + + const pinocchioInstructions = [ + 'CTokenTransfer', + 'CreateAssociatedTokenAccount', + 'CreateAssociatedTokenAccountIdempotent', + 'CreateTokenAccount', + 'CloseTokenAccount', + 'Transfer2', + 'MintAction', + 'Claim', + 'WithdrawFundingPool', + ]; + + const allInstructions = [...anchorInstructions, ...pinocchioInstructions]; + + // Extensions with runtime constraints (cannot transfer compressed) + const runtimeConstraintExtensions = [ + 'MintCloseAuthority', + 'TransferFeeConfig', + 'DefaultAccountState', + 'PermanentDelegate', + 'TransferHook', + 'Pausable', + 'ConfidentialTransferMint', + 'ConfidentialTransferFeeConfig', + 'ConfidentialMintBurn', + ]; + + // All 16 allowed extensions + const extensions = [ + // Metadata extensions (no constraints) + { name: 'MetadataPointer', description: '-', instructions: allInstructions }, + { name: 'TokenMetadata', description: '-', instructions: allInstructions }, + // Group extensions (no constraints) + { name: 'InterestBearingConfig', description: '-', instructions: allInstructions }, + { name: 'GroupPointer', description: '-', instructions: allInstructions }, + { name: 'GroupMemberPointer', description: '-', instructions: allInstructions }, + { name: 'TokenGroup', description: '-', instructions: allInstructions }, + { name: 'TokenGroupMember', description: '-', instructions: allInstructions }, + // Extensions with runtime constraints + { name: 'MintCloseAuthority', description: '-', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'TransferFeeConfig', description: 'fees must be zero', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'DefaultAccountState', description: 'any state allowed', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'PermanentDelegate', description: '-', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'TransferHook', description: 'program_id must be nil', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'Pausable', description: '-', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'ConfidentialTransferMint', description: 'initialized but not enabled', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'ConfidentialTransferFeeConfig', description: 'fees must be zero', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + { name: 'ConfidentialMintBurn', description: 'initialized but not enabled', instructions: allInstructions.filter(i => !['transfer', 'CTokenTransfer', 'Transfer2'].includes(i)) }, + ]; + + const hasRuntimeConstraint = (extName) => { + return runtimeConstraintExtensions.includes(extName); + }; + + // Filter extensions based on active filter + const getFilteredExtensions = () => { + let filtered = extensions; + + if (selectedInstruction) { + filtered = filtered.filter(ext => ext.instructions.includes(selectedInstruction)); + } + + return filtered; + }; + + const filteredExtensions = getFilteredExtensions(); + + const handleFilterClick = (filter) => { + setActiveFilter(filter); + if (filter === 'all') { + setSelectedInstruction(null); + } + }; + + const FilterButton = ({ filter, label }) => ( + + ); + + return ( +
+
+ {/* Filter Buttons */} +
+ + + {/* Instruction Dropdown */} +
+ +
+ + + +
+
+
+ + {/* Results count */} +
+ Showing {filteredExtensions.length} of {extensions.length} extensions +
+ + {/* Table */} + + + + + + + + + + + {filteredExtensions.map((ext) => ( + + + + + + + ))} + +
Extension NameDescriptionLight-TokenCompressed Token
{ext.name}{ext.description}{hasRuntimeConstraint(ext.name) ? '-' : '✓'}
+
+
+ ); +}; diff --git a/snippets/jsx/toolkit-card.jsx b/snippets/jsx/toolkit-card.jsx new file mode 100644 index 00000000..81fb2c22 --- /dev/null +++ b/snippets/jsx/toolkit-card.jsx @@ -0,0 +1,42 @@ +export const ToolkitCard = ({ title, description, href, items }) => { + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + style={{ textDecoration: 'none', color: 'inherit', display: 'block', marginBottom: '1rem', borderBottom: 'none', boxShadow: 'none' }} + className="no-underline" + > +
+
+
{title}
+
{description}
+
+
+
What's inside
+ {items && items.length > 0 ? ( +
    + {items.map((item, i) =>
  • {item}
  • )} +
+ ) : ( +
Coming soon
+ )} +
+
+
+ ); +}; diff --git a/snippets/light-token-configure-rent.mdx b/snippets/light-token-configure-rent.mdx new file mode 100644 index 00000000..84064bae --- /dev/null +++ b/snippets/light-token-configure-rent.mdx @@ -0,0 +1,42 @@ +```rust +use light_compressed_token_sdk::ctoken::CompressibleParamsInfos; + +let compressible_params = CompressibleParamsInfos::new( + compressible_config.clone(), + rent_sponsor.clone(), + system_program.clone(), +); +``` + + + + + + + + + + + + + + + + + + + + + + + + + + +
Compressible Config + Protocol PDA that stores account rent config. +
Rent Sponsor + - light-token program PDA that fronts rent exemption at creation.
+ - Claims rent when account compresses. +
System ProgramSolana System Program to create the on-chain account.
+ diff --git a/snippets/light-token-guides/cata-intro.mdx b/snippets/light-token-guides/cata-intro.mdx new file mode 100644 index 00000000..566c37ff --- /dev/null +++ b/snippets/light-token-guides/cata-intro.mdx @@ -0,0 +1,11 @@ +import CompressibleRentExplained from '/snippets/compressible-rent-explained.mdx'; +import CompressibleDefaultRentConfig from '/snippets/compressible-default-rent-config.mdx'; + +1. Associated light-token accounts (light-ATA) are Solana accounts that hold token balances of light-mints, SPL mints, or Token 2022 mints. +2. The address for light-ATAs is deterministically derived with the owner's address, *light-token program ID*, and mint address. +3. light-ATAs accounts are compressible with a default rent config. + + + + + diff --git a/snippets/light-token-guides/client-custom-rent-config.mdx b/snippets/light-token-guides/client-custom-rent-config.mdx new file mode 100644 index 00000000..586d13c4 --- /dev/null +++ b/snippets/light-token-guides/client-custom-rent-config.mdx @@ -0,0 +1,71 @@ +import { CompressibleRentCalculator } from '/snippets/jsx/compressible-rent-calculator.jsx'; + + +We recommend to use default values, but you can customize the rent config based on the expected account activity. + + +1. Set the hours to determine the amount of prepaid rent +2. Set the lamports per write a transaction payer will pay for top ups + +```rust +use light_compressed_token_sdk::ctoken::{ + CompressibleParams, + CreateCTokenAccount, + ctoken_v1_config_pda, // helper to derive config PDA + ctoken_v1_rent_sponsor_pda // helper to derive rent sponsor PDA +}; +use light_ctoken_types::state::TokenDataVersion; + +let compressible_params = CompressibleParams { + compressible_config: ctoken_v1_config_pda(), + rent_sponsor: ctoken_v1_rent_sponsor_pda(), + pre_pay_num_epochs: 35, // ~52h + lamports_per_write: Some(788), + compress_to_account_pubkey: None, + token_account_version: TokenDataVersion::ShaFlat, +}; + +let instruction = CreateCTokenAccount::new( + payer.pubkey(), + account.pubkey(), + mint, + owner, + compressible_params, +).instruction()?; +``` + + + + + + + + + + + + + + + + + + + + + + + + + + +
Compressible Config + Protocol PDA that stores account rent config. +
Rent Sponsor + - CToken program PDA that fronts rent exemption at creation.
+ - Claims rent when account compresses. +
System ProgramSolana System Program to create the on-chain account.
+ + + + diff --git a/snippets/light-token-guides/close-intro.mdx b/snippets/light-token-guides/close-intro.mdx new file mode 100644 index 00000000..2f11e21a --- /dev/null +++ b/snippets/light-token-guides/close-intro.mdx @@ -0,0 +1,5 @@ +1. Closing a light-token account transfers remaining lamports to a destination account and the rent sponsor can reclaim sponsored rent. +2. light-token accounts can be closed + * by the account owner at any time. + * by the `compression_authority` + when the account becomes compressible. The account is compressed and closed - it can be reinstated with the same state (decompressed). diff --git a/snippets/light-token-guides/light-token-client-prerequisites.mdx b/snippets/light-token-guides/light-token-client-prerequisites.mdx new file mode 100644 index 00000000..187bbcd3 --- /dev/null +++ b/snippets/light-token-guides/light-token-client-prerequisites.mdx @@ -0,0 +1,12 @@ +import RustInstallDependencies from '/snippets/setup/rust-install-dependencies.mdx'; +import RustSetupEnvironment from '/snippets/setup/rust-setup-environment-tabs.mdx'; + + + + + + + + + + diff --git a/snippets/light-token-guides/light-token-intro.mdx b/snippets/light-token-guides/light-token-intro.mdx new file mode 100644 index 00000000..383a3f39 --- /dev/null +++ b/snippets/light-token-guides/light-token-intro.mdx @@ -0,0 +1,10 @@ +import CompressibleRentExplained from '/snippets/compressible-rent-explained.mdx'; +import CompressibleDefaultRentConfig from '/snippets/compressible-default-rent-config.mdx'; + +1. light-token accounts are Solana accounts that hold token balances of light-mints, SPL mints, or Token 2022 mints. +2. light-token accounts are compressible with a default rent config. + + + + + diff --git a/snippets/light-token-guides/light-token-ts-client-prerequisites.mdx b/snippets/light-token-guides/light-token-ts-client-prerequisites.mdx new file mode 100644 index 00000000..6c73dd51 --- /dev/null +++ b/snippets/light-token-guides/light-token-ts-client-prerequisites.mdx @@ -0,0 +1,14 @@ +import InstallDependencies from '/snippets/setup/install-dependencies-codegroup.mdx'; +import SetupEnvironment from '/snippets/setup/setup-environment-tabs.mdx'; + + + + + + + + + + + + diff --git a/snippets/light-token-guides/transfer-interface-intro.mdx b/snippets/light-token-guides/transfer-interface-intro.mdx new file mode 100644 index 00000000..98595f01 --- /dev/null +++ b/snippets/light-token-guides/transfer-interface-intro.mdx @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + +
**light-token → light-token Account** +
    +
  • Transfers light-tokens between light-token accounts
  • +
+
**SPL token → light-token Account** +
    +
  • Transfers SPL tokens to light-token accounts
  • +
  • SPL tokens are locked in interface PDA
  • +
  • light-tokens are minted to light-token account
  • +
+
**light-token → SPL Account** +
    +
  • Releases SPL tokens from interface PDA to SPL account
  • +
  • Burns light-tokens in source light-token account
  • +
+
+ + +* For example, **SPL → light-token** can be used for transfers from Alice's SPL token account to her own light-token account. +* You can use this to **convert existing SPL tokens to light-tokens** with **sponsored rent-exemption**. Learn more in the [core concepts to the light-token program](/light-token/README). + diff --git a/snippets/compressed-pdas-guides-table.mdx b/snippets/overview-tables/compressed-pdas-guides-table.mdx similarity index 100% rename from snippets/compressed-pdas-guides-table.mdx rename to snippets/overview-tables/compressed-pdas-guides-table.mdx diff --git a/snippets/compressed-tokens-advanced-guides-table.mdx b/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx similarity index 100% rename from snippets/compressed-tokens-advanced-guides-table.mdx rename to snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx diff --git a/snippets/compressed-tokens-guides-table.mdx b/snippets/overview-tables/compressed-tokens-guides-table.mdx similarity index 90% rename from snippets/compressed-tokens-guides-table.mdx rename to snippets/overview-tables/compressed-tokens-guides-table.mdx index 4248fd80..9a914757 100644 --- a/snippets/compressed-tokens-guides-table.mdx +++ b/snippets/overview-tables/compressed-tokens-guides-table.mdx @@ -5,7 +5,7 @@ | [Transfer Compressed Tokens](/compressed-tokens/guides/how-to-transfer-compressed-token) | Move compressed tokens between compressed accounts | | [Decompress and Compress Tokens](/compressed-tokens/guides/how-to-compress-and-decompress-spl-tokens) | Convert SPL tokens between regular and compressed format | | [Compress Complete SPL Token Accounts](/compressed-tokens/guides/how-to-compress-complete-spl-token-accounts) | Compress complete SPL token accounts and reclaim rent afterwards | -| [Create and Register a Mint Account for Compression](/compressed-tokens/guides/how-to-create-and-register-a-mint-account-for-compression) | Create new SPL mint with token pool for compression | +| [Create a Mint with Token Pool for Compression](/compressed-tokens/guides/how-to-create-and-register-a-mint-account-for-compression) | Create new SPL mint with token pool for compression | | [Create Token Pools for Mint Accounts](/compressed-tokens/guides/how-to-create-compressed-token-pools-for-mint-accounts) | Create token pool for compression for existing SPL mints | | [Merge Compressed Accounts](/compressed-tokens/guides/how-to-merge-compressed-token-accounts) | Consolidate multiple compressed accounts of the same mint into one | | [Approve and Revoke Delegate Authority](/compressed-tokens/guides/how-to-approve-and-revoke-delegate-authority) | Approve or revoke delegates for compressed token accounts | diff --git a/snippets/overview-tables/integrate-light-token-guides-table.mdx b/snippets/overview-tables/integrate-light-token-guides-table.mdx new file mode 100644 index 00000000..548d7d23 --- /dev/null +++ b/snippets/overview-tables/integrate-light-token-guides-table.mdx @@ -0,0 +1,6 @@ +| | | +| :---- | :---------- | +| [for Stablecoin Payments](/light-token/toolkits/for-payments) | Process light-token payments and transfers | +| [for Wallets](/light-token/toolkits/for-wallets) | Display balances, build transactions, handle light-token accounts | +| [for Streaming Mints](/light-token/toolkits/for-streaming-mints) | Create and manage streaming light-mints | +| [for Streaming Tokens](/light-token/toolkits/for-streaming-tokens) | Stream light-token transfers over time | diff --git a/snippets/overview-tables/light-mint-guides-table.mdx b/snippets/overview-tables/light-mint-guides-table.mdx new file mode 100644 index 00000000..59787bee --- /dev/null +++ b/snippets/overview-tables/light-mint-guides-table.mdx @@ -0,0 +1,4 @@ +| | | +| :---- | :---------- | +| [Create Mint Account](/light-token/cookbook/create-mint) | Program and client guide to create light-mints with token metadata | +| [Mint Tokens](/light-token/cookbook/mint-tokens) | Mint tokens to light-token accounts | diff --git a/snippets/overview-tables/light-token-guides-table.mdx b/snippets/overview-tables/light-token-guides-table.mdx new file mode 100644 index 00000000..6c8218cc --- /dev/null +++ b/snippets/overview-tables/light-token-guides-table.mdx @@ -0,0 +1,6 @@ +| | | +| :---- | :---------- | +| [Create Token Accounts](/light-token/cookbook/create-token-account) | | +| [Create Associated light-token Accounts](/light-token/cookbook/create-ata) | | +| [Transfer Interface](/light-token/cookbook/transfer-interface) | For transfers between light-token and SPL token accounts. | +| [Close Token Accounts](/light-token/cookbook/close-token-account) | | diff --git a/snippets/program-examples-table.mdx b/snippets/overview-tables/program-examples-table.mdx similarity index 100% rename from snippets/program-examples-table.mdx rename to snippets/overview-tables/program-examples-table.mdx diff --git a/snippets/compressed-pdas-program-setup.mdx b/snippets/setup/compressed-pdas-program-setup.mdx similarity index 100% rename from snippets/compressed-pdas-program-setup.mdx rename to snippets/setup/compressed-pdas-program-setup.mdx diff --git a/snippets/compressed-tokens-mint-prereq.mdx b/snippets/setup/compressed-tokens-mint-prereq.mdx similarity index 100% rename from snippets/compressed-tokens-mint-prereq.mdx rename to snippets/setup/compressed-tokens-mint-prereq.mdx diff --git a/snippets/development-environment-setup.mdx b/snippets/setup/development-environment-setup.mdx similarity index 100% rename from snippets/development-environment-setup.mdx rename to snippets/setup/development-environment-setup.mdx diff --git a/snippets/install-dependencies-codegroup.mdx b/snippets/setup/install-dependencies-codegroup.mdx similarity index 100% rename from snippets/install-dependencies-codegroup.mdx rename to snippets/setup/install-dependencies-codegroup.mdx diff --git a/snippets/setup/rust-install-dependencies.mdx b/snippets/setup/rust-install-dependencies.mdx new file mode 100644 index 00000000..334c04f4 --- /dev/null +++ b/snippets/setup/rust-install-dependencies.mdx @@ -0,0 +1,12 @@ +```toml Cargo.toml +[dependencies] +light-compressed-token-sdk = "0.1" +light-client = "0.1" +light-ctoken-types = "0.1" +solana-sdk = "2.2" +borsh = "0.10" +tokio = { version = "1.36", features = ["full"] } + +[dev-dependencies] +light-program-test = "0.1" # For in-memory tests with LiteSVM +``` diff --git a/snippets/setup/rust-setup-environment-tabs.mdx b/snippets/setup/rust-setup-environment-tabs.mdx new file mode 100644 index 00000000..1b356028 --- /dev/null +++ b/snippets/setup/rust-setup-environment-tabs.mdx @@ -0,0 +1,85 @@ +import CliInstall from '/snippets/versions/cli-install-0.27.1-alpha.2.mdx'; + + + + +Test with Lite-SVM (...) + +```bash +# Initialize project +cargo init my-light-project +cd my-light-project + +# Run tests +cargo test +``` + +```rust +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use solana_sdk::signer::Signer; + +#[tokio::test] +async fn test_example() { + // In-memory test environment + let mut rpc = LightProgramTest::new(ProgramTestConfig::default()) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + println!("Payer: {}", payer.pubkey()); +} +``` + + + + +Connects to a local test validator. + + + +```bash +# Initialize project +cargo init my-light-project +cd my-light-project + +# Start local test validator (in separate terminal) +light test-validator +``` + +```rust +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Connects to http://localhost:8899 + let rpc = LightClient::new(LightClientConfig::local()).await?; + + let slot = rpc.get_slot().await?; + println!("Current slot: {}", slot); + + Ok(()) +} +``` + + + + +Replace `` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression). + +```rust +use light_client::rpc::{LightClient, LightClientConfig, Rpc}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let rpc_url = "https://devnet.helius-rpc.com?api-key="; + let rpc = LightClient::new( + LightClientConfig::new(rpc_url.to_string(), None, None) + ).await?; + + println!("Connected to Devnet"); + Ok(()) +} +``` + + + diff --git a/snippets/setup-environment-tabs.mdx b/snippets/setup/setup-environment-tabs.mdx similarity index 100% rename from snippets/setup-environment-tabs.mdx rename to snippets/setup/setup-environment-tabs.mdx diff --git a/styles.css b/styles.css index bc9f64f5..2121992c 100644 --- a/styles.css +++ b/styles.css @@ -154,4 +154,21 @@ [data-callout="error"] { background: rgba(239, 68, 68, 0.08) !important; border-color: rgba(239, 68, 68, 0.3) !important; +} + +/* Page Transition Animation */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Target Mintlify's main content area */ +article, +[data-testid="content"], +main > div:first-child { + animation: fadeIn 0.6s ease-out; } \ No newline at end of file