diff --git a/docs.json b/docs.json index 3e3db01..bf01fa9 100644 --- a/docs.json +++ b/docs.json @@ -57,15 +57,19 @@ "light-token/cookbook/overview", { "group": "Recipes", + "expanded": true, "pages": [ "light-token/cookbook/create-mint", "light-token/cookbook/create-ata", "light-token/cookbook/create-token-account", "light-token/cookbook/mint-to", - "light-token/cookbook/close-token-account", "light-token/cookbook/transfer-interface", "light-token/cookbook/wrap-unwrap", - "light-token/cookbook/load-ata" + "light-token/cookbook/load-ata", + "light-token/cookbook/close-token-account", + "light-token/cookbook/burn", + "light-token/cookbook/freeze-thaw", + "light-token/cookbook/approve-revoke" ] } ] diff --git a/light-token/cookbook/approve-revoke.mdx b/light-token/cookbook/approve-revoke.mdx new file mode 100644 index 0000000..2a7d47c --- /dev/null +++ b/light-token/cookbook/approve-revoke.mdx @@ -0,0 +1,202 @@ +--- +title: Approve and Revoke Delegates +sidebarTitle: Approve / Revoke +description: Rust client guide to approve and revoke delegates for light-token accounts. Includes step-by-step implementation and full code examples. +keywords: ["approve delegate solana", "revoke delegate solana", "token delegation"] +--- + +--- + +import TokenClientPrerequisites from "/snippets/light-token-guides/light-token-client-prerequisites.mdx"; +import { CodeCompare } from "/snippets/jsx/code-compare.jsx"; +import { + splApproveRustCode, + lightApproveRustCode, + splRevokeRustCode, + lightRevokeRustCode, +} from "/snippets/code-samples/code-compare-snippets.jsx"; +import ApproveFull from "/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-full.mdx"; +import RevokeFull from "/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-full.mdx"; +import NativeProgram from "/snippets/code-snippets/light-token/approve-revoke/program/native.mdx"; +import AnchorProgram from "/snippets/code-snippets/light-token/approve-revoke/program/anchor.mdx"; + +1. Approve grants a delegate permission to transfer up to a specified amount of tokens from your account. + * Each token account can have only one delegate at a time. + * Any new approval overwrites the previous one. +2. Revoke removes all delegate permissions from a light-token account. +3. Only the token account owner can approve or revoke delegates. + + + + + + + + + + + + + + + + + + + +### Prerequisites + + + + + + +### Approve or revoke delegates + + + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/sdk-libs/token-sdk/src/token) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + + + + + + + + + + + + +### Approve + + + + +```rust +use light_token_sdk::token::ApproveCpi; + +ApproveCpi { + token_account: token_account.clone(), + delegate: delegate.clone(), + owner: owner.clone(), + system_program: system_program.clone(), + amount, +} +.invoke() +``` + + + + +```rust +use light_token_sdk::token::ApproveCpi; + +let signer_seeds = authority_seeds!(bump); + +ApproveCpi { + token_account: token_account.clone(), + delegate: delegate.clone(), + owner: owner.clone(), + system_program: system_program.clone(), + amount, +} +.invoke_signed(&[signer_seeds]) +``` + + + + + + + + +### Revoke + + + + +```rust +use light_token_sdk::token::RevokeCpi; + +RevokeCpi { + token_account: token_account.clone(), + owner: owner.clone(), + system_program: system_program.clone(), +} +.invoke() +``` + + + + +```rust +use light_token_sdk::token::RevokeCpi; + +let signer_seeds = authority_seeds!(bump); + +RevokeCpi { + token_account: token_account.clone(), + owner: owner.clone(), + system_program: system_program.clone(), +} +.invoke_signed(&[signer_seeds]) +``` + + + + + + + +# Full Code Example + + + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/approve_revoke.rs) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). + + + + + + + + + + + + + + + +# Next Steps + + diff --git a/light-token/cookbook/burn.mdx b/light-token/cookbook/burn.mdx new file mode 100644 index 0000000..d1ae308 --- /dev/null +++ b/light-token/cookbook/burn.mdx @@ -0,0 +1,131 @@ +--- +title: Burn Light Tokens +sidebarTitle: Burn Light Tokens +description: Rust client guide to burn light-tokens. Includes step-by-step implementation and full code examples. +keywords: ["burn tokens on solana", "destroy tokens solana", "reduce token supply"] +--- + +--- + +import TokenClientPrerequisites from "/snippets/light-token-guides/light-token-client-prerequisites.mdx"; +import { CodeCompare } from "/snippets/jsx/code-compare.jsx"; +import { + splBurnRustCode, + lightBurnRustCode, +} from "/snippets/code-samples/code-compare-snippets.jsx"; +import RustFullCode from "/snippets/code-snippets/light-token/burn/rust-client/full.mdx"; +import NativeProgram from "/snippets/code-snippets/light-token/burn/program/native.mdx"; +import AnchorProgram from "/snippets/code-snippets/light-token/burn/program/anchor.mdx"; + +1. Burn permanently destroys tokens by reducing the balance in a token account. +2. Burned tokens are removed from circulation and decreases the supply tracked on the mint account. +3. Only the token account owner (or approved delegate) can burn tokens. + + + + +Compare to SPL: + + + + + +### Prerequisites + + + + + + +### Burn light-tokens + + + View [Source Code](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/token-sdk/src/token/burn.rs) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + +### Build Account Infos and CPI + + + + +```rust +use light_token_sdk::token::BurnCpi; + +BurnCpi { + source: source.clone(), + mint: mint.clone(), + amount, + authority: authority.clone(), + max_top_up: None, +} +.invoke() +``` + + + + +```rust +use light_token_sdk::token::BurnCpi; + +let signer_seeds = authority_seeds!(bump); + +BurnCpi { + source: source.clone(), + mint: mint.clone(), + amount, + authority: authority.clone(), + max_top_up: None, +} +.invoke_signed(&[signer_seeds]) +``` + + + + + + + +# Full Code Example + + + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/burn.rs) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). + + + + + + + + + + + + + + + +# Next Steps + + diff --git a/light-token/cookbook/close-token-account.mdx b/light-token/cookbook/close-token-account.mdx index b5970ed..8b49454 100644 --- a/light-token/cookbook/close-token-account.mdx +++ b/light-token/cookbook/close-token-account.mdx @@ -10,30 +10,38 @@ keywords: ["close token account on solana", "reclaim rent on solana"] import CloseAccountInfosAccountsList from "/snippets/accounts-list/close-account-infos-accounts-list.mdx"; import TokenClientPrerequisites from "/snippets/light-token-guides/light-token-client-prerequisites.mdx"; import ClientCustomRentConfig from "/snippets/light-token-guides/client-custom-rent-config.mdx"; +import { CodeCompare } from "/snippets/jsx/code-compare.jsx"; +import { + splCloseAccountRustCode, + lightCloseAccountRustCode, +} from "/snippets/code-samples/code-compare-snippets.jsx"; +import RustFullCode from "/snippets/code-snippets/light-token/close-token-account/rust-client/full.mdx"; +import NativeProgram from "/snippets/code-snippets/light-token/close-token-account/program/native.mdx"; +import AnchorProgram from "/snippets/code-snippets/light-token/close-token-account/program/anchor.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). +2. Light token accounts can be closed by the owner. -## Get Started + +The `compression_authority` +closes the account and preserves the balance as compressed token account when the account becomes compressible. +The account is reinstated in flight with the same state the next time it is accessed. + -1. The example creates a light-token account and mint. -2. Build the instruction with `CloseTokenAccount`: +Use `CloseTokenAccount` to close an empty light-token account. -```rust -let close_instruction = CloseTokenAccount::new( - LIGHT_TOKEN_PROGRAM_ID, - account.pubkey(), - payer.pubkey(), // Destination for remaining lamports - owner, -) -.instruction() -``` +Compare to SPL: + + @@ -46,186 +54,11 @@ let close_instruction = CloseTokenAccount::new( ### Close light-token Account -```rust -use borsh::BorshDeserialize; -use light_client::indexer::{AddressWithTree, Indexer}; -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; -use light_token_sdk::token::{ - CloseTokenAccount, CreateCMint, CreateCMintParams, CreateTokenAccount, LIGHT_TOKEN_PROGRAM_ID, -}; -use light_token_interface::state::Token; -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_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") // 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 token account with 0 balance - let account = Keypair::new(); - let owner = payer.pubkey(); - - let create_instruction = - CreateTokenAccount::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 token_state = - Token::deserialize(&mut &account_before_close.unwrap().data[..]).unwrap(); - assert_eq!(token_state.amount, 0, "Account balance must be 0 to close"); - - // Step 4: Build close instruction using SDK builder - let close_instruction = CloseTokenAccount::new( - LIGHT_TOKEN_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_token_sdk::token::derive_cmint_compressed_address( - &mint_signer.pubkey(), - &address_tree.tree, - ); - - let mint_pda = light_token_sdk::token::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) -} + + View [Source Code](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/token-sdk/src/token/close.rs) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + -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[..])?) -} -``` + @@ -233,49 +66,43 @@ fn load_keypair(path: &str) -> Result> { - -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_token_sdk::token::CloseTokenAccountCpi; +use light_token_sdk::token::CloseAccountCpi; -CloseTokenAccountCpi { +CloseAccountCpi { token_program: token_program.clone(), account: account.clone(), destination: destination.clone(), owner: owner.clone(), - rent_sponsor: Some(rent_sponsor.clone()), + rent_sponsor: rent_sponsor.clone(), } -.invoke()?; +.invoke() ``` ```rust -use light_token_sdk::token::CloseTokenAccountCpi; +use light_token_sdk::token::CloseAccountCpi; -let close_account_cpi = CloseTokenAccountCpi { +let signer_seeds = authority_seeds!(bump); + +CloseAccountCpi { 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])?; + rent_sponsor: rent_sponsor.clone(), +} +.invoke_signed(&[signer_seeds]) ``` @@ -291,87 +118,17 @@ close_account_cpi.invoke_signed(&[signer_seeds])?; # Full Code Example - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/close.rs). + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/close/) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). -```rust expandable -use light_token_sdk::token::CloseTokenAccountCpi; -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 (light token 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 - }; - - CloseTokenAccountCpi { - 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 (light token 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]]; - CloseTokenAccountCpi { - token_program: accounts[0].clone(), - account: accounts[1].clone(), - destination: accounts[2].clone(), - owner: accounts[3].clone(), - rent_sponsor, - } - .invoke_signed(&[signer_seeds])?; - - Ok(()) -} -``` + + + + + + + + @@ -382,9 +139,9 @@ pub fn process_close_account_invoke_signed(accounts: &[AccountInfo]) -> Result<( {" "} diff --git a/light-token/cookbook/create-ata.mdx b/light-token/cookbook/create-ata.mdx index 26f46dc..dc58507 100644 --- a/light-token/cookbook/create-ata.mdx +++ b/light-token/cookbook/create-ata.mdx @@ -8,34 +8,30 @@ keywords: ["associated token account on solana", "create ata for solana tokens", --- import TokenCreateATAAccountsList from "/snippets/accounts-list/light-token-create-ata-accounts-list.mdx"; -import CompressibleVsSolanaRent from "/snippets/compressible-vs-solana-rent.mdx"; import TokenConfigureRent from "/snippets/light-token-configure-rent.mdx"; -import AtaIntro from "/snippets/light-token-guides/cata-intro.mdx"; import CompressibleRentExplained from "/snippets/compressible-rent-explained.mdx"; import TokenClientPrerequisites from "/snippets/light-token-guides/light-token-client-prerequisites.mdx"; -import TokenTsClientPrerequisites from "/snippets/light-token-guides/light-token-ts-client-prerequisites.mdx"; -import ClientCustomRentConfig from "/snippets/light-token-guides/client-custom-rent-config.mdx"; import { CodeCompare } from "/snippets/jsx/code-compare.jsx"; import FullSetup from "/snippets/setup/full-setup.mdx"; import { splCreateAtaCode, lightCreateAtaCode, + splCreateAtaRustCode, + lightCreateAtaRustCode, } from "/snippets/code-samples/code-compare-snippets.jsx"; import ActionCode from "/snippets/code-snippets/light-token/create-ata/action.mdx"; import InstructionCode from "/snippets/code-snippets/light-token/create-ata/instruction.mdx"; +import RustFullCode from "/snippets/code-snippets/light-token/create-ata/rust-client/full.mdx"; +import NativeProgram from "/snippets/code-snippets/light-token/create-ata/program/native.mdx"; +import AnchorProgram from "/snippets/code-snippets/light-token/create-ata/program/anchor.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. -3. 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. +1. Associated light-token accounts can hold token balances of light, SPL, or Token 2022 mints. +2. Light-ATAs are on-chain accounts like SPL ATA's, but the light token program sponsors the rent-exemption cost for you. -## Get Started - @@ -45,10 +41,10 @@ The `createAtaInterface` function creates an associated light-token account in a Compare to SPL: @@ -79,22 +75,17 @@ Compare to SPL: -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 `CreateAssociatedTokenAccount`. It automatically includes the default rent config: +`CreateAssociatedTokenAccount` creates an on-chain ATA to store token balances of light, SPL, or Token 2022 mints. -```rust -use light_token_sdk::token::CreateAssociatedTokenAccount; - -let instruction = CreateAssociatedTokenAccount::new( - payer.pubkey(), - owner, - mint, -) -.instruction()?; -``` +Compare to SPL: -4. Send transaction & verify light-ATA creation with `get_account`. + @@ -109,167 +100,11 @@ let instruction = CreateAssociatedTokenAccount::new( ### Create ATA -```rust -use borsh::BorshDeserialize; -use light_client::indexer::{AddressWithTree, Indexer}; -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; -use light_token_sdk::token::{ - derive_token_ata, CreateAssociatedTokenAccount, CreateCMint, - CreateCMintParams, -}; -use light_token_interface::state::Token; -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_token_ata(&owner, &mint); - - // Step 3: Build instruction using SDK builder - let instruction = CreateAssociatedTokenAccount::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 token_state = Token::deserialize(&mut &account_data.data[..]).unwrap(); - - assert_eq!(token_state.mint, mint.to_bytes(), "Mint should match"); - assert_eq!(token_state.owner, owner.to_bytes(), "Owner should match"); - assert_eq!(token_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_token_sdk::token::derive_cmint_compressed_address( - &mint_signer.pubkey(), - &address_tree.tree, - ); - - let mint_pda = - light_token_sdk::token::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) -} + + View [Source Code](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/token-sdk/src/token/create_ata.rs) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + -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[..])?) -} -``` + @@ -277,9 +112,6 @@ fn load_keypair(path: &str) -> Result> { - -Find [a full code example at the end](#full-code-example). - @@ -293,51 +125,61 @@ Find [a full code example at the end](#full-code-example). ### Build Account Infos and CPI the Compressed 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, light_token_program_id, mint]`. - Unlike light-token accounts, owner and mint are passed as accounts, not in - instruction data. - + + The light-ATA address is derived from `[owner, light_token_program_id, mint]`. + Unlike light-token accounts, owner and mint are passed as accounts, not in + instruction data. + ```rust -use light_token_sdk::token::CreateAssociatedTokenAccountCpi; +use light_token_sdk::token::{CompressibleParamsCpi, CreateAssociatedAccountCpi}; -CreateAssociatedTokenAccountCpi { +let compressible = CompressibleParamsCpi::new_ata( + compressible_config.clone(), + rent_sponsor.clone(), + system_program.clone(), +); + +CreateAssociatedAccountCpi { 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, + bump, + compressible, + idempotent, } -.invoke()?; +.invoke() ``` ```rust -use light_token_sdk::token::CreateAssociatedTokenAccountCpi; +use light_token_sdk::token::{CompressibleParamsCpi, CreateAssociatedAccountCpi}; + +let compressible = CompressibleParamsCpi::new_ata( + compressible_config.clone(), + rent_sponsor.clone(), + system_program.clone(), +); -let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; -CreateAssociatedTokenAccountCpi { +let signer_seeds = authority_seeds!(authority_bump); +CreateAssociatedAccountCpi { 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, + bump, + compressible, + idempotent, } -.invoke_signed(&[signer_seeds])?; +.invoke_signed(&[signer_seeds]) ``` @@ -349,112 +191,17 @@ CreateAssociatedTokenAccountCpi { # Full Code Example - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/create_ata.rs). + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/create_ata.rs) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). -```rust expandable -use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_sdk::token::{CompressibleParamsCpi, CreateAssociatedTokenAccountCpi}; -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(), - ); - - CreateAssociatedTokenAccountCpi { - 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]]; - CreateAssociatedTokenAccountCpi { - 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(()) -} -``` + + + + + + + + @@ -464,7 +211,7 @@ pub fn process_create_ata2_invoke_signed( {" "} + + + @@ -39,10 +48,10 @@ You can use the same interface regardless of mint type. Compare to SPL: @@ -78,26 +87,18 @@ Compare to SPL: -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. +`CreateMint` creates an on-chain mint account that can optionally include token metadata. +The instruction creates under the hood a compressed mint address for when the mint is inactive. -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_token_sdk::token::CreateCMint; +Compare to SPL: -let create_cmint = CreateCMint::new( - params, - mint_signer.pubkey(), - payer.pubkey(), - address_tree.tree, - output_queue, -); -let instruction = create_cmint.instruction()?; -``` + @@ -110,158 +111,11 @@ let instruction = create_cmint.instruction()?; ### Create Mint with Token Metadata -```rust -use light_client::indexer::{AddressWithTree, Indexer}; -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; -use light_token_sdk::token::{CreateCMint, CreateCMintParams}; -use light_token_interface::instructions::extensions::token_metadata::TokenMetadataInstructionData; -use light_token_interface::instructions::extensions::ExtensionInstructionData; -use light_token_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_token_sdk::token::derive_cmint_compressed_address( - &mint_signer.pubkey(), - &address_tree.tree, - ); - - let mint_pda = light_token_sdk::token::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[..])?) -} -``` + + View [Source Code](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/token-sdk/src/token/create_mint.rs) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + @@ -269,9 +123,6 @@ fn load_keypair(path: &str) -> Result> { - -Find [a full code example at the end](#full-code-example). - @@ -368,78 +219,49 @@ let system_accounts = SystemAccountInfos { ### 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_token_sdk::token::CreateCMintCpi; - -CreateCMintCpi { - mint_seed: mint_seed.clone(), - authority: authority.clone(), - payer: payer.clone(), - address_tree: address_tree.clone(), - output_queue: output_queue.clone(), +use light_token_sdk::token::CreateMintCpi; + +CreateMintCpi::new( + mint_seed.clone(), + authority.clone(), + payer.clone(), + address_tree.clone(), + output_queue.clone(), + compressible_config.clone(), + mint.clone(), + rent_sponsor.clone(), system_accounts, - cpi_context: None, - cpi_context_account: None, params, -} -.invoke()?; +) +.invoke() ``` - + ```rust -use light_token_sdk::token::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(), +use light_token_sdk::token::CreateMintCpi; + +let signer_seeds: &[&[u8]] = &[b"authority", &[authority_bump]]; + +CreateMintCpi::new( + mint_seed.clone(), + authority.clone(), + payer.clone(), + address_tree.clone(), + output_queue.clone(), + compressible_config.clone(), + mint.clone(), + rent_sponsor.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_token_sdk::token::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])?; +) +.invoke_signed(&[signer_seeds]) ``` @@ -453,274 +275,17 @@ account_infos.invoke_signed(&[mint_seed_seeds, authority_seeds])?; # Full Code Example - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/create_cmint.rs). + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/compressed_token/mint_action/) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). -```rust expandable -use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_sdk::{ - token::{ - 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(()) -} -``` + + + + + + + + @@ -729,7 +294,7 @@ pub fn process_create_cmint_with_pda_authority( # Next Steps 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. +2. Light token accounts are on-chain accounts like SPL ATA’s, but the light token program sponsors the rent-exemption cost for you. -## 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 `CreateTokenAccount`. It automatically includes the default rent config. - -```rust -use light_token_sdk::token::{CreateTokenAccount}; - -let instruction = CreateTokenAccount::new( - payer.pubkey(), - account.pubkey(), - mint, - owner, -) -.instruction()?; -``` +`CreateTokenAccount` creates an on-chain token account to store token balances of light, SPL, or Token 2022 mints. -3. Send transaction & verify light-token account creation with `get_account`. +Compare to SPL: + @@ -55,160 +53,11 @@ let instruction = CreateTokenAccount::new( ### Create Token Account -```rust -use borsh::BorshDeserialize; -use light_client::indexer::{AddressWithTree, Indexer}; -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; -use light_token_sdk::token::{CreateCMint, CreateCMintParams, CreateTokenAccount}; -use light_token_interface::state::Token; -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_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") // 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 = CreateTokenAccount::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 token_state = Token::deserialize(&mut &account_data.data[..]).unwrap(); - - assert_eq!(token_state.mint, mint.to_bytes(), "Mint should match"); - assert_eq!(token_state.owner, owner.to_bytes(), "Owner should match"); - assert_eq!(token_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_token_sdk::token::derive_cmint_compressed_address( - &mint_signer.pubkey(), - &address_tree.tree, - ); - - let mint_pda = light_token_sdk::token::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) -} + + View [Source Code](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/token-sdk/src/token/create.rs) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + -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[..])?) -} -``` + @@ -217,9 +66,6 @@ fn load_keypair(path: &str) -> Result> { - -Find [a full code example at the end](#full-code-example). - @@ -232,10 +78,6 @@ Find [a full code example at the end](#full-code-example). ### Build Account Infos and CPI -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. - @@ -281,121 +123,24 @@ account_cpi.invoke_signed(&[signer_seeds])?; # Full Code Example - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/create_token_account.rs). + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/create.rs) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). -```rust expandable -use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_sdk::token::{CompressibleParamsCpi, CreateTokenAccountCpi}; -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 token 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 - CreateTokenAccountCpi { - 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 = CreateTokenAccountCpi { - 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 + + + + + + + + + + + + + + + + + +### Prerequisites + + + + + + +### Freeze or thaw light-token accounts + + + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/sdk-libs/token-sdk/src/token) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + + + + + + + + + + + + +### Freeze + + + + +```rust +use light_token_sdk::token::FreezeCpi; + +FreezeCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), +} +.invoke() +``` + + + + +```rust +use light_token_sdk::token::FreezeCpi; + +let signer_seeds = authority_seeds!(bump); + +FreezeCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), +} +.invoke_signed(&[signer_seeds]) +``` + + + + + + + + +### Thaw + + + + +```rust +use light_token_sdk::token::ThawCpi; + +ThawCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), +} +.invoke() +``` + + + + +```rust +use light_token_sdk::token::ThawCpi; + +let signer_seeds = authority_seeds!(bump); + +ThawCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), +} +.invoke_signed(&[signer_seeds]) +``` + + + + + + + +# Full Code Example + + + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/freeze_thaw.rs) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). + + + + + + + + + + + + + + + +# Next Steps + + diff --git a/light-token/cookbook/load-ata.mdx b/light-token/cookbook/load-ata.mdx index 17eced7..042e1c2 100644 --- a/light-token/cookbook/load-ata.mdx +++ b/light-token/cookbook/load-ata.mdx @@ -1,7 +1,7 @@ --- title: Load Token Balances to Light ATA sidebarTitle: Load ATA -description: Unify token balances from compressed tokens (cold), SPL, and Token-2022 to one light ATA. +description: Unify token balances from compressed tokens (cold light-tokens), SPL, and Token-2022 to one light ATA. keywords: ["load ata on solana", "get token balance for wallets"] --- @@ -12,7 +12,7 @@ import ActionCode from "/snippets/code-snippets/light-token/load-ata/action.mdx" import InstructionCode from "/snippets/code-snippets/light-token/load-ata/instruction.mdx"; 1. `loadAta` unifies tokens from multiple sources to a single ATA: - * Compressed tokens (cold) -> Decompresses -> light ATA + * Compressed tokens (cold light-tokens) -> Decompresses -> light ATA * SPL balance (if wrap=true) -> Wraps -> light ATA * T22 balance (if wrap=true) -> Wraps -> light ATA @@ -24,11 +24,9 @@ import InstructionCode from "/snippets/code-snippets/light-token/load-ata/instru Find the source code [here](https://github.com/Lightprotocol/light-protocol/blob/0c4e2417b2df2d564721b89e18d1aad3665120e7/js/compressed-token/src/v3/actions/load-ata.ts).
-## Get Started - -### Load Compressed Tokens to Hot Balance +### Unify Tokens to Light-ATA Balance diff --git a/light-token/cookbook/mint-to.mdx b/light-token/cookbook/mint-to.mdx index 19ac18f..111eb5b 100644 --- a/light-token/cookbook/mint-to.mdx +++ b/light-token/cookbook/mint-to.mdx @@ -15,15 +15,18 @@ import FullSetup from "/snippets/setup/full-setup.mdx"; import { splMintToCode, lightMintToCode, + splMintToRustCode, + lightMintToRustCode, } from "/snippets/code-samples/code-compare-snippets.jsx"; import ActionCode from "/snippets/code-snippets/light-token/mint-to/action.mdx"; import InstructionCode from "/snippets/code-snippets/light-token/mint-to/instruction.mdx"; +import RustFullCode from "/snippets/code-snippets/light-token/mint-to/rust-client/full.mdx"; +import NativeProgram from "/snippets/code-snippets/light-token/mint-to/program/native.mdx"; +import AnchorProgram from "/snippets/code-snippets/light-token/mint-to/program/anchor.mdx"; 1. Mint To creates new tokens of a mint and deposits them to a specified token account. 2. Before we can mint any tokens, we need an initialized mint account (SPL, Token-2022 or Light) for which we hold the mint authority. -## Get Started - @@ -35,10 +38,10 @@ The function auto-detects the token program (SPL, Token-2022, or Light) from the Compare to SPL: @@ -68,26 +71,17 @@ Compare to SPL: -The example mints light-tokens to existing light-token accounts. +Use `MintTo` to mint tokens to a light-token account. -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 `MintTo::new()` and send the transaction. +Compare to SPL: -```rust -use light_token_sdk::token::MintTo; - -let instruction = MintTo::new( - params, - payer.pubkey(), - state_tree, - output_queue, - input_queue, - vec![recipient_account.pubkey()], -) -.instruction()?; -``` + @@ -100,271 +94,11 @@ let instruction = MintTo::new( ### Mint to Light Token Accounts -```rust -use borsh::BorshDeserialize; -use light_client::indexer::{AddressWithTree, Indexer}; -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; -use light_token_sdk::token::{ - CreateCMint, CreateCMintParams, CreateTokenAccount, MintTo, MintToParams, -}; -use light_token_interface::instructions::extensions::token_metadata::TokenMetadataInstructionData; -use solana_sdk::compute_budget::ComputeBudgetInstruction; -use light_token_interface::instructions::extensions::ExtensionInstructionData; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; -use light_token_interface::state::{AdditionalMetadata, Token, 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_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 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 token account - let token_account = Keypair::new(); - let owner = payer.pubkey(); - let create_account_ix = - CreateTokenAccount::new(payer.pubkey(), token_account.pubkey(), mint, owner) - .instruction() - .unwrap(); - - rpc.create_and_send_transaction( - &[create_account_ix], - &payer.pubkey(), - &[&payer, &token_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 = MintToParams::new( - compressed_mint_with_context, - amount, - mint_authority, - rpc_result.proof, - ); - - // Step 9: Build instruction using SDK builder - let instruction = MintTo::new( - params, - payer.pubkey(), - compressed_mint_account.tree_info.tree, - compressed_mint_account.tree_info.queue, - output_queue, - vec![token_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 token_account_data = rpc - .get_account(token_account.pubkey()) - .await - .unwrap() - .unwrap(); - - let token_state = Token::deserialize(&mut &token_account_data.data[..]).unwrap(); - assert_eq!(token_state.amount, amount, "Token amount should match"); - assert_eq!(token_state.mint, mint.to_bytes(), "Mint should match"); - assert_eq!(token_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_token_sdk::token::derive_cmint_compressed_address( - &mint_signer.pubkey(), - &address_tree.tree, - ); - - let mint_pda = light_token_sdk::token::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) -} + + View [Source Code](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/token-sdk/src/token/mint_to.rs) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + -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[..])?) -} -``` + @@ -372,63 +106,11 @@ fn load_keypair(path: &str) -> Result> { -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_token_sdk::token::MintToParams; - -let params = MintToParams::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_token_sdk::token::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 -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. - @@ -436,18 +118,14 @@ let system_accounts = SystemAccountInfos { use light_token_sdk::token::MintToCpi; MintToCpi { + mint: mint.clone(), + destination: destination.clone(), + amount, authority: authority.clone(), - payer: payer.clone(), - state_tree: state_tree.clone(), - input_queue: input_queue.clone(), - output_queue: output_queue.clone(), - token_accounts, - system_accounts, - cpi_context: None, - cpi_context_account: None, - params, + system_program: system_program.clone(), + max_top_up: None, } -.invoke()?; +.invoke() ``` @@ -456,21 +134,17 @@ MintToCpi { ```rust use light_token_sdk::token::MintToCpi; -let account_infos = MintToCpi { +let signer_seeds = authority_seeds!(bump); + +MintToCpi { + mint: mint.clone(), + destination: destination.clone(), + amount, authority: authority.clone(), - payer: payer.clone(), - state_tree: state_tree.clone(), - input_queue: input_queue.clone(), - output_queue: output_queue.clone(), - token_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])?; + system_program: system_program.clone(), + max_top_up: None, +} +.invoke_signed(&[signer_seeds]) ``` @@ -486,179 +160,17 @@ account_infos.invoke_signed(&[signer_seeds])?; # Full Code Example - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/ctoken_mint_to.rs). + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/mint_to.rs) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). -```rust expandable -use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; -use light_token_sdk::token::{MintToCpi, MintToParams, 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 -#[derive(BorshSerialize, BorshDeserialize, Debug)] -pub struct MintToData { - 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 MintToCpi builder pattern. This demonstrates how to: -/// 1. Build MintToParams using the constructor -/// 2. Build MintToCpi 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..]: token_accounts (variable length - destination accounts) -pub fn process_mint_to( - accounts: &[AccountInfo], - data: MintToData, -) -> Result<(), ProgramError> { - if accounts.len() < 13 { - return Err(ProgramError::NotEnoughAccountKeys); - } - - // Build params using the constructor - let params = MintToParams::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 token accounts from remaining accounts (index 12 onwards) - let token_accounts: Vec = accounts[12..].to_vec(); - - // Build the account infos struct and invoke - // SDK account order: output_queue (9), tree (10), input_queue (11), token_accounts (12+) - // In this case, payer == authority (accounts[3]) - MintToCpi { - 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 - token_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 MintToCpi 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..]: token_accounts (variable length - destination accounts) -pub fn process_mint_to_invoke_signed( - accounts: &[AccountInfo], - data: MintToData, -) -> 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 = MintToParams::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 token accounts from remaining accounts (index 12 onwards) - let token_accounts: Vec = accounts[12..].to_vec(); - - // Build the account infos struct - // authority is the PDA (accounts[2]) - let account_infos = MintToCpi { - 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 - token_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(()) -} -``` + + + + + + + + @@ -666,7 +178,7 @@ pub fn process_mint_to_invoke_signed( # Next Steps @@ -54,8 +59,6 @@ import InstructionCode from "/snippets/code-snippets/light-token/transfer-interf -## Get Started - @@ -65,10 +68,10 @@ The `transferInterface` function transfers tokens between token accounts (SPL, T Compare to SPL: @@ -99,11 +102,15 @@ Compare to SPL: -The example transfers SPL token -> light-token and light-token -> light-token: +Use the unified `TransferInterface` to transfer tokens between token accounts (SPL, Token-2022, or Light) in a single call. -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. + @@ -114,456 +121,95 @@ The example transfers SPL token -> light-token and light-token -> light-token: -### Transfer Interface +### Transfer Interface + +The example transfers +1. SPL token -> light-token, +2. light-token -> light-token, and +3. light-token -> SPL token. + + + View [Source Code](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/token-sdk/src/token/transfer_interface.rs) or find full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + -```rust -use anchor_spl::token::{spl_token, Mint}; -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; -use light_token_sdk::{ - token::{ - derive_token_ata, CreateAssociatedTokenAccount, - Transfer, TransferFromSpl, - }, - 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 = TransferFromSpl { - amount: spl_to_token_amount, - spl_interface_pda_bump, - source_spl_token_account: spl_token_account_keypair.pubkey(), - destination_token_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 = Transfer { - 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 +### Build Account Infos and CPI -- from which SPL or light-token account, and -- to which SPL or light-token account. + + ```rust use light_token_sdk::token::TransferInterfaceCpi; -let mut transfer = TransferInterfaceCpi::new( - data.amount, - source_account.clone(), - destination_account.clone(), +TransferInterfaceCpi::new( + amount, + decimals, + source.clone(), + destination.clone(), authority.clone(), payer.clone(), - compressed_token_program_authority.clone(), -); + ctoken_authority.clone(), + system_program.clone(), +) +.invoke() ``` - - - - - - - - -### 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 - -CPI the Light Token program to execute the transfer. -Use `invoke()`, or `invoke_signed()` when a CPI requires a PDA signer. - - - + + ```rust -transfer.invoke()?; -``` +use light_token_sdk::token::TransferInterfaceCpi; - - +let signer_seeds = authority_seeds!(bump); -```rust -let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]]; -transfer.invoke_signed(&[authority_seeds])?; +TransferInterfaceCpi::new( + amount, + decimals, + source.clone(), + destination.clone(), + authority.clone(), + payer.clone(), + ctoken_authority.clone(), + system_program.clone(), +) +.invoke_signed(&[signer_seeds]) ``` - + + + + + # Full Code Example - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/transfer_interface.rs). + View [Source Code](https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token/program/src/ctoken/transfer/) or full examples with tests: [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/program-examples). -```rust expandable -use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_sdk::token::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<->Token transfers, None for Token->Token - pub token_pool_pda_bump: Option, -} - -/// Handler for TransferInterfaceCpi (invoke) -/// -/// This unified interface automatically detects account types and routes to: -/// - Token -> Token transfer -/// - Token -> SPL transfer -/// - SPL -> Token transfer -/// -/// Account order: -/// - accounts[0]: compressed_token_program (for CPI) -/// - accounts[1]: source_account (SPL or light-token) -/// - accounts[2]: destination_account (SPL or light-token) -/// - accounts[3]: authority (signer) -/// - accounts[4]: payer (signer) -/// - accounts[5]: compressed_token_program_authority -/// For SPL bridge (optional, required for SPL<->Token): -/// - 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 light-token) -/// - accounts[2]: destination_account (SPL or light-token) -/// - 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<->Token): -/// - 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(()) -} -``` + + + + + + + +
@@ -574,7 +220,7 @@ pub fn process_transfer_interface_invoke_signed( {" "} -## Get Started - diff --git a/light-token/cookbook/extensions.mdx b/light-token/extensions.mdx similarity index 100% rename from light-token/cookbook/extensions.mdx rename to light-token/extensions.mdx diff --git a/light-token/toolkits/for-streaming-mints.mdx b/light-token/toolkits/for-streaming-mints.mdx index 7d87c79..f9a7b1d 100644 --- a/light-token/toolkits/for-streaming-mints.mdx +++ b/light-token/toolkits/for-streaming-mints.mdx @@ -33,7 +33,7 @@ bs58 = "0.5" use futures::StreamExt; use helius_laserstream::{subscribe, LaserstreamConfig}; use light_event::parse::event_from_light_transaction; -use light_token_interface::state::mint::CompressedMint; +use light_token_interface::state::Mint; use light_token_interface::state::extensions::ExtensionStruct; use borsh::BorshDeserialize; @@ -143,7 +143,7 @@ for output in event.output_compressed_accounts.iter() { }; // Deserialize - let mint = CompressedMint::try_from_slice(data)?; + let mint = Mint::try_from_slice(data)?; // Check if new (address not in inputs) let is_new = output @@ -166,7 +166,7 @@ for output in event.output_compressed_accounts.iter() { ### Extract Token Metadata from Mint Extensions ```rust -fn extract_metadata(mint: &CompressedMint) -> Option<(String, String, String)> { +fn extract_metadata(mint: &Mint) -> Option<(String, String, String)> { let extensions = mint.extensions.as_ref()?; for ext in extensions { @@ -199,13 +199,16 @@ if let Some((name, symbol, uri)) = extract_metadata(&mint) { ```rust #[repr(C)] -pub struct CompressedMint { +pub struct Mint { pub base: BaseMint, - pub metadata: CompressedMintMetadata, + pub metadata: MintMetadata, + pub reserved: [u8; 16], + pub account_type: u8, + pub compression: CompressionInfo, pub extensions: Option>, } -/// SPL compatible basemint structure (82 bytes) +/// SPL-compatible base mint structure #[repr(C)] pub struct BaseMint { pub mint_authority: Option, @@ -215,12 +218,14 @@ pub struct BaseMint { pub freeze_authority: Option, } -/// metadata used by the light token program (34 bytes) +/// Light Protocol metadata for compressed mints (67 bytes) #[repr(C)] -pub struct CompressedMintMetadata { +pub struct MintMetadata { pub version: u8, - pub spl_mint_initialized: bool, - pub mint: Pubkey, // PDA with compressed mint seed + pub mint_decompressed: bool, + pub mint: Pubkey, + pub mint_signer: [u8; 32], + pub bump: u8, } ``` diff --git a/scripts/copy-program-snippets.sh b/scripts/copy-program-snippets.sh new file mode 100755 index 0000000..5af10e0 --- /dev/null +++ b/scripts/copy-program-snippets.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Script to copy program example code from examples-light-token to docs/snippets/code-snippets/light-token +# Wraps each file in rust markdown code blocks + +NATIVE_DIR="/home/tilo/Workspace/examples-light-token/program-examples/native/program/src/instructions" +ANCHOR_DIR="/home/tilo/Workspace/examples-light-token/program-examples/anchor/programs" +SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/light-token" + +# Mapping: target-dir -> "native-file:anchor-program-dir" +# For combined operations (approve-revoke, freeze-thaw), use comma-separated native files and anchor dirs +declare -A OPERATION_MAP=( + ["create-ata"]="create_ata.rs:create-ata" + ["create-mint"]="create_mint.rs:create-mint" + ["mint-to"]="mint_to.rs:mint-to" + ["transfer-interface"]="transfer_interface.rs:transfer-interface" + ["close-token-account"]="close.rs:close" + ["burn"]="burn.rs:burn" + ["approve-revoke"]="approve.rs,revoke.rs:approve,revoke" + ["freeze-thaw"]="freeze.rs,thaw.rs:freeze,thaw" +) + +# Function to wrap Rust code in markdown +wrap_rust() { + local input_file="$1" + local output_file="$2" + echo '```rust' > "$output_file" + cat "$input_file" >> "$output_file" + echo '```' >> "$output_file" + echo "Created: $output_file" +} + +# Function to combine multiple Rust files into one markdown file +wrap_rust_multi() { + local output_file="$1" + shift + local input_files=("$@") + + echo '```rust' > "$output_file" + local first=true + for input_file in "${input_files[@]}"; do + if [ -f "$input_file" ]; then + if [ "$first" = true ]; then + first=false + else + echo "" >> "$output_file" + echo "// ---" >> "$output_file" + echo "" >> "$output_file" + fi + cat "$input_file" >> "$output_file" + fi + done + echo '```' >> "$output_file" + echo "Created: $output_file" +} + +# Process each operation +for target_dir in "${!OPERATION_MAP[@]}"; do + mapping="${OPERATION_MAP[$target_dir]}" + native_part="${mapping%%:*}" + anchor_part="${mapping##*:}" + + echo "Processing: $target_dir" + + output_dir="$SNIPPETS_DIR/$target_dir/program" + mkdir -p "$output_dir" + + # Handle native files (may be comma-separated for combined operations) + IFS=',' read -ra native_files <<< "$native_part" + if [ ${#native_files[@]} -eq 1 ]; then + # Single file + native_file="$NATIVE_DIR/${native_files[0]}" + if [ -f "$native_file" ]; then + wrap_rust "$native_file" "$output_dir/native.mdx" + else + echo " WARNING: Not found - $native_file" + fi + else + # Multiple files - combine them + native_paths=() + for nf in "${native_files[@]}"; do + native_paths+=("$NATIVE_DIR/$nf") + done + wrap_rust_multi "$output_dir/native.mdx" "${native_paths[@]}" + fi + + # Handle anchor files (may be comma-separated for combined operations) + IFS=',' read -ra anchor_dirs <<< "$anchor_part" + if [ ${#anchor_dirs[@]} -eq 1 ]; then + # Single anchor program + anchor_file="$ANCHOR_DIR/${anchor_dirs[0]}/src/lib.rs" + if [ -f "$anchor_file" ]; then + wrap_rust "$anchor_file" "$output_dir/anchor.mdx" + else + echo " WARNING: Not found - $anchor_file" + fi + else + # Multiple anchor programs - combine them + anchor_paths=() + for ad in "${anchor_dirs[@]}"; do + anchor_paths+=("$ANCHOR_DIR/$ad/src/lib.rs") + done + wrap_rust_multi "$output_dir/anchor.mdx" "${anchor_paths[@]}" + fi +done + +echo "" +echo "Done! Created program snippets in: $SNIPPETS_DIR" +echo "" +echo "Files created:" +find "$SNIPPETS_DIR" -path "*/program/*.mdx" -type f | sort diff --git a/scripts/copy-rust-snippets.sh b/scripts/copy-rust-snippets.sh new file mode 100755 index 0000000..1e66f17 --- /dev/null +++ b/scripts/copy-rust-snippets.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Script to copy Rust client code from examples-light-token to docs/snippets/code-snippets/light-token +# Wraps each file in rust markdown code blocks + +EXAMPLES_DIR="/home/tilo/Workspace/examples-light-token/rust-client/tests" +SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/light-token" + +# Mapping: source file -> target directory/output-name +# Format: "target-dir:output-name" (output-name without .mdx extension) +declare -A FILE_MAP=( + ["create_ata.rs"]="create-ata:full" + ["create_token_account.rs"]="create-token-account:full" + ["create_mint.rs"]="create-mint:full" + ["mint_to.rs"]="mint-to:full" + ["close.rs"]="close-token-account:full" + ["transfer.rs"]="transfer-interface:full" + ["burn.rs"]="burn:full" + ["approve.rs"]="approve-revoke:approve-full" + ["revoke.rs"]="approve-revoke:revoke-full" + ["freeze.rs"]="freeze-thaw:freeze-full" + ["thaw.rs"]="freeze-thaw:thaw-full" +) + +# Function to wrap Rust code in markdown +wrap_rust() { + local input_file="$1" + local output_file="$2" + echo '```rust' > "$output_file" + cat "$input_file" >> "$output_file" + echo '```' >> "$output_file" + echo "Created: $output_file" +} + +# Process each mapped file +for source_file in "${!FILE_MAP[@]}"; do + mapping="${FILE_MAP[$source_file]}" + target_dir="${mapping%%:*}" + output_name="${mapping##*:}" + echo "Processing: $source_file -> $target_dir/$output_name.mdx" + + input_file="$EXAMPLES_DIR/$source_file" + output_dir="$SNIPPETS_DIR/$target_dir/rust-client" + + if [ -f "$input_file" ]; then + mkdir -p "$output_dir" + wrap_rust "$input_file" "$output_dir/$output_name.mdx" + else + echo " WARNING: Not found - $input_file" + fi +done + +echo "" +echo "Done! Created Rust snippets in: $SNIPPETS_DIR" +echo "" +echo "Files created:" +find "$SNIPPETS_DIR" -path "*/rust-client/*.mdx" -type f | sort diff --git a/snippets/code-samples/code-compare-snippets.jsx b/snippets/code-samples/code-compare-snippets.jsx index 9fc0e28..e000ecb 100644 --- a/snippets/code-samples/code-compare-snippets.jsx +++ b/snippets/code-samples/code-compare-snippets.jsx @@ -113,6 +113,176 @@ export const lightTransferCode = [ ");", ].join("\n"); +// === TRANSFER (RUST) === +export const splTransferRustCode = [ + "// SPL transfer", + "use spl_token::instruction::transfer;", + "", + "let ix = transfer(", + " &spl_token::id(),", + " &source,", + " &destination,", + " &authority,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightTransferRustCode = [ + "// light-token transfer", + "use light_token_sdk::token::TransferInterface;", + "", + "let ix = TransferInterface {", + " source,", + " destination,", + " amount,", + " decimals,", + " authority: payer.pubkey(),", + " payer: payer.pubkey(),", + " spl_interface: None,", + " max_top_up: None,", + " source_owner: LIGHT_TOKEN_PROGRAM_ID,", + " destination_owner: LIGHT_TOKEN_PROGRAM_ID,", + "}", + ".instruction()?;", +].join("\n"); + +// === CREATE ATA (RUST) === +export const splCreateAtaRustCode = [ + "// SPL create ATA", + "use spl_associated_token_account::instruction::create_associated_token_account;", + "", + "let ix = create_associated_token_account(", + " &payer.pubkey(),", + " &owner.pubkey(),", + " &mint,", + " &spl_token::id(),", + ");", +].join("\n"); + +export const lightCreateAtaRustCode = [ + "// light-token create ATA", + "use light_token_sdk::token::CreateAssociatedTokenAccount;", + "", + "let ix = CreateAssociatedTokenAccount::new(", + " payer.pubkey(),", + " owner.pubkey(),", + " mint,", + ")", + ".instruction()?;", +].join("\n"); + +// === CREATE MINT (RUST) === +export const splCreateMintRustCode = [ + "// SPL create mint", + "use spl_token::instruction::initialize_mint;", + "", + "let ix = initialize_mint(", + " &spl_token::id(),", + " &mint.pubkey(),", + " &mint_authority,", + " Some(&freeze_authority),", + " decimals,", + ")?;", +].join("\n"); + +export const lightCreateMintRustCode = [ + "// light-token create mint", + "use light_token_sdk::token::CreateMint;", + "", + "let ix = CreateMint::new(", + " // includes decimals, mint_authority, freeze_authority, extensions, rent config", + " params,", + " mint_seed.pubkey(),", + " payer.pubkey(),", + " address_tree.tree,", + " output_queue,", + ")", + ".instruction()?;", +].join("\n"); + +// === MINT TO (RUST) === +export const splMintToRustCode = [ + "// SPL mint to", + "use spl_token::instruction::mint_to;", + "", + "let ix = mint_to(", + " &spl_token::id(),", + " &mint,", + " &destination,", + " &mint_authority,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightMintToRustCode = [ + "// light-token mint to", + "use light_token_sdk::token::MintTo;", + "", + "let ix = MintTo {", + " mint,", + " destination,", + " amount,", + " authority: payer.pubkey(),", + " max_top_up: None,", + "}", + ".instruction()?;", +].join("\n"); + +// === CREATE TOKEN ACCOUNT (RUST) === +export const splCreateTokenAccountRustCode = [ + "// SPL create token account", + "use spl_token::instruction::initialize_account;", + "", + "let ix = initialize_account(", + " &spl_token::id(),", + " &account,", + " &mint,", + " &owner,", + ")?;", +].join("\n"); + +export const lightCreateTokenAccountRustCode = [ + "// light-token create token account", + "use light_token_sdk::token::CreateTokenAccount;", + "", + "let ix = CreateTokenAccount::new(", + " payer.pubkey(),", + " account.pubkey(),", + " mint,", + " owner,", + ")", + ".instruction()?;", +].join("\n"); + +// === CLOSE TOKEN ACCOUNT (RUST) === +export const splCloseAccountRustCode = [ + "// SPL close account", + "use spl_token::instruction::close_account;", + "", + "let ix = close_account(", + " &spl_token::id(),", + " &account,", + " &destination,", + " &owner,", + " &[],", + ")?;", +].join("\n"); + +export const lightCloseAccountRustCode = [ + "// light-token close account", + "use light_token_sdk::token::{CloseAccount, LIGHT_TOKEN_PROGRAM_ID};", + "", + "let ix = CloseAccount::new(", + " LIGHT_TOKEN_PROGRAM_ID,", + " account,", + " destination,", + " owner,", + ")", + ".instruction()?;", +].join("\n"); + // === BLOG - CREATE ATA (different comments) === export const blogSplCreateAtaCode = [ "// Create SPL token account", @@ -133,3 +303,136 @@ export const blogLightCreateAtaCode = [ " mint", ");", ].join("\n"); + +// === BURN (RUST) === +export const splBurnRustCode = [ + "// SPL burn", + "use spl_token::instruction::burn;", + "", + "let ix = burn(", + " &spl_token::id(),", + " &source,", + " &mint,", + " &authority,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightBurnRustCode = [ + "// light-token burn", + "use light_token_sdk::token::Burn;", + "", + "let ix = Burn {", + " source,", + " mint,", + " amount,", + " authority: payer.pubkey(),", + " max_top_up: None,", + "}", + ".instruction()?;", +].join("\n"); + +// === FREEZE (RUST) === +export const splFreezeRustCode = [ + "// SPL freeze", + "use spl_token::instruction::freeze_account;", + "", + "let ix = freeze_account(", + " &spl_token::id(),", + " &account,", + " &mint,", + " &freeze_authority,", + " &[],", + ")?;", +].join("\n"); + +export const lightFreezeRustCode = [ + "// light-token freeze", + "use light_token_sdk::token::Freeze;", + "", + "let ix = Freeze {", + " token_account: ata,", + " mint,", + " freeze_authority: payer.pubkey(),", + "}", + ".instruction()?;", +].join("\n"); + +// === THAW (RUST) === +export const splThawRustCode = [ + "// SPL thaw", + "use spl_token::instruction::thaw_account;", + "", + "let ix = thaw_account(", + " &spl_token::id(),", + " &account,", + " &mint,", + " &freeze_authority,", + " &[],", + ")?;", +].join("\n"); + +export const lightThawRustCode = [ + "// light-token thaw", + "use light_token_sdk::token::Thaw;", + "", + "let ix = Thaw {", + " token_account: ata,", + " mint,", + " freeze_authority: payer.pubkey(),", + "}", + ".instruction()?;", +].join("\n"); + +// === APPROVE (RUST) === +export const splApproveRustCode = [ + "// SPL approve", + "use spl_token::instruction::approve;", + "", + "let ix = approve(", + " &spl_token::id(),", + " &source,", + " &delegate,", + " &owner,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightApproveRustCode = [ + "// light-token approve", + "use light_token_sdk::token::Approve;", + "", + "let ix = Approve {", + " token_account: ata,", + " delegate: delegate.pubkey(),", + " owner: payer.pubkey(),", + " amount,", + "}", + ".instruction()?;", +].join("\n"); + +// === REVOKE (RUST) === +export const splRevokeRustCode = [ + "// SPL revoke", + "use spl_token::instruction::revoke;", + "", + "let ix = revoke(", + " &spl_token::id(),", + " &source,", + " &owner,", + " &[],", + ")?;", +].join("\n"); + +export const lightRevokeRustCode = [ + "// light-token revoke", + "use light_token_sdk::token::Revoke;", + "", + "let ix = Revoke {", + " token_account: ata,", + " owner: payer.pubkey(),", + "}", + ".instruction()?;", +].join("\n"); diff --git a/snippets/code-snippets/light-token/approve-revoke/program/anchor.mdx b/snippets/code-snippets/light-token/approve-revoke/program/anchor.mdx new file mode 100644 index 0000000..25d322f --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/program/anchor.mdx @@ -0,0 +1,2 @@ +```rust +``` diff --git a/snippets/code-snippets/light-token/approve-revoke/program/native.mdx b/snippets/code-snippets/light-token/approve-revoke/program/native.mdx new file mode 100644 index 0000000..851284f --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/program/native.mdx @@ -0,0 +1,99 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::ApproveCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn approve_invoke(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [token_account, delegate, owner, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let amount = u64::from_le_bytes( + data.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ); + + ApproveCpi { + token_account: token_account.clone(), + delegate: delegate.clone(), + owner: owner.clone(), + system_program: system_program.clone(), + amount, + } + .invoke() +} + +pub fn approve_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [token_account, delegate, owner, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 9 { + return Err(ProgramError::InvalidInstructionData); + } + + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let bump = data[8]; + let signer_seeds = authority_seeds!(bump); + + ApproveCpi { + token_account: token_account.clone(), + delegate: delegate.clone(), + owner: owner.clone(), + system_program: system_program.clone(), + amount, + } + .invoke_signed(&[signer_seeds]) +} + +// --- + +use super::authority_seeds; +use light_token_sdk::token::RevokeCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn revoke_invoke(accounts: &[AccountInfo], _data: &[u8]) -> ProgramResult { + let [token_account, owner, system_program, _token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + RevokeCpi { + token_account: token_account.clone(), + owner: owner.clone(), + system_program: system_program.clone(), + } + .invoke() +} + +pub fn revoke_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [token_account, owner, system_program, _token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let bump = data[0]; + let signer_seeds = authority_seeds!(bump); + + RevokeCpi { + token_account: token_account.clone(), + owner: owner.clone(), + system_program: system_program.clone(), + } + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-full.mdx b/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-full.mdx new file mode 100644 index 0000000..e157cb8 --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-full.mdx @@ -0,0 +1,35 @@ +```rust +mod shared; + +use light_client::rpc::Rpc; +use light_token_sdk::token::Approve; +use shared::SetupContext; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::test(flavor = "multi_thread")] +async fn approve_delegate() { + // Setup creates mint and ATA with tokens + let SetupContext { + mut rpc, + payer, + ata, + .. + } = shared::setup().await; + + let delegate = Keypair::new(); + let delegate_amount = 500_000u64; + + let approve_ix = Approve { + token_account: ata, + delegate: delegate.pubkey(), + owner: payer.pubkey(), + amount: delegate_amount, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[approve_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); +} +``` diff --git a/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-full.mdx b/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-full.mdx new file mode 100644 index 0000000..5a15cc0 --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-full.mdx @@ -0,0 +1,30 @@ +```rust +mod shared; + +use light_client::rpc::Rpc; +use light_token_sdk::token::Revoke; +use shared::SetupContext; +use solana_sdk::signer::Signer; + +#[tokio::test(flavor = "multi_thread")] +async fn revoke_delegation() { + // Setup creates mint, ATA with tokens, and approves delegate + let SetupContext { + mut rpc, + payer, + ata, + .. + } = shared::setup().await; + + let revoke_ix = Revoke { + token_account: ata, + owner: payer.pubkey(), + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[revoke_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); +} +``` diff --git a/snippets/code-snippets/light-token/burn/program/anchor.mdx b/snippets/code-snippets/light-token/burn/program/anchor.mdx new file mode 100644 index 0000000..7c3ca78 --- /dev/null +++ b/snippets/code-snippets/light-token/burn/program/anchor.mdx @@ -0,0 +1,41 @@ +```rust +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; +use light_token_sdk::token::BurnCpi; + +declare_id!("BHTGZDjDw9Gpz8oYm7CRMg2WtKwW65YAYHXXMKv4dpr6"); + +#[program] +pub mod light_token_anchor_burn { + use super::*; + + pub fn burn<'info>( + ctx: Context<'_, '_, '_, 'info, BurnAccounts<'info>>, + amount: u64, + ) -> Result<()> { + BurnCpi { + source: ctx.accounts.source.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + amount, + authority: ctx.accounts.authority.to_account_info(), + max_top_up: None, + } + .invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct BurnAccounts<'info> { + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub source: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub mint: AccountInfo<'info>, + pub authority: Signer<'info>, + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, +} +``` diff --git a/snippets/code-snippets/light-token/burn/program/native.mdx b/snippets/code-snippets/light-token/burn/program/native.mdx new file mode 100644 index 0000000..cfce0d1 --- /dev/null +++ b/snippets/code-snippets/light-token/burn/program/native.mdx @@ -0,0 +1,51 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::BurnCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn burn_invoke(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [source, mint, authority, _token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let amount = u64::from_le_bytes( + data.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ); + + BurnCpi { + source: source.clone(), + mint: mint.clone(), + amount, + authority: authority.clone(), + max_top_up: None, + } + .invoke() +} + +pub fn burn_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [source, mint, authority, _token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 9 { + return Err(ProgramError::InvalidInstructionData); + } + + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let bump = data[8]; + let signer_seeds = authority_seeds!(bump); + + BurnCpi { + source: source.clone(), + mint: mint.clone(), + amount, + authority: authority.clone(), + max_top_up: None, + } + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/burn/rust-client/basic.mdx b/snippets/code-snippets/light-token/burn/rust-client/basic.mdx new file mode 100644 index 0000000..eb2a449 --- /dev/null +++ b/snippets/code-snippets/light-token/burn/rust-client/basic.mdx @@ -0,0 +1,12 @@ +```rust +use light_token_sdk::token::Burn; + +let burn_ix = Burn { + source: ata, + cmint: mint, + amount: burn_amount, + authority: payer.pubkey(), + max_top_up: None, +} +.instruction()?; +``` diff --git a/snippets/code-snippets/light-token/burn/rust-client/full.mdx b/snippets/code-snippets/light-token/burn/rust-client/full.mdx new file mode 100644 index 0000000..21ef3b4 --- /dev/null +++ b/snippets/code-snippets/light-token/burn/rust-client/full.mdx @@ -0,0 +1,42 @@ +```rust +mod shared; + +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_sdk::token::Burn; +use shared::SetupContext; +use solana_sdk::signer::Signer; + +#[tokio::test(flavor = "multi_thread")] +async fn burn() { + // Setup creates mint and ATA with tokens + let SetupContext { + mut rpc, + payer, + mint, + ata, + .. + } = shared::setup().await; + + let initial_amount = 1_000_000u64; + let burn_amount = 400_000u64; + + let burn_ix = Burn { + source: ata, + mint, + amount: burn_amount, + authority: payer.pubkey(), + max_top_up: None, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[burn_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let ata_data = rpc.get_account(ata).await.unwrap().unwrap(); + let token = light_token_interface::state::Token::deserialize(&mut &ata_data.data[..]).unwrap(); + assert_eq!(token.amount, initial_amount - burn_amount); +} +``` diff --git a/snippets/code-snippets/light-token/close-token-account/program/anchor.mdx b/snippets/code-snippets/light-token/close-token-account/program/anchor.mdx new file mode 100644 index 0000000..2a487e9 --- /dev/null +++ b/snippets/code-snippets/light-token/close-token-account/program/anchor.mdx @@ -0,0 +1,43 @@ +```rust +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; +use light_token_sdk::token::CloseAccountCpi; + +declare_id!("4fi27siKEvKXJYN5WCzWuHdAw1rLed6Tprv9ZARv3Gxu"); + +#[program] +pub mod light_token_anchor_close { + use super::*; + + pub fn close_account<'info>( + ctx: Context<'_, '_, '_, 'info, CloseAccountAccounts<'info>>, + ) -> Result<()> { + CloseAccountCpi { + token_program: ctx.accounts.token_program.to_account_info(), + account: ctx.accounts.account.to_account_info(), + destination: ctx.accounts.destination.to_account_info(), + owner: ctx.accounts.owner.to_account_info(), + rent_sponsor: ctx.accounts.rent_sponsor.to_account_info(), + } + .invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CloseAccountAccounts<'info> { + /// CHECK: Validated by light-token CPI + pub token_program: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub account: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub destination: AccountInfo<'info>, + pub owner: Signer<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} +``` diff --git a/snippets/code-snippets/light-token/close-token-account/program/native.mdx b/snippets/code-snippets/light-token/close-token-account/program/native.mdx new file mode 100644 index 0000000..15652f6 --- /dev/null +++ b/snippets/code-snippets/light-token/close-token-account/program/native.mdx @@ -0,0 +1,47 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::CloseAccountCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn close_invoke(accounts: &[AccountInfo], _data: &[u8]) -> ProgramResult { + let [token_program, account, destination, owner, rent_sponsor] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + CloseAccountCpi { + token_program: token_program.clone(), + account: account.clone(), + destination: destination.clone(), + owner: owner.clone(), + rent_sponsor: rent_sponsor.clone(), + } + .invoke() +} + +pub fn close_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [token_program, account, destination, owner, rent_sponsor] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let bump = data[0]; + let signer_seeds = authority_seeds!(bump); + + CloseAccountCpi { + token_program: token_program.clone(), + account: account.clone(), + destination: destination.clone(), + owner: owner.clone(), + rent_sponsor: rent_sponsor.clone(), + } + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/close-token-account/rust-client/basic.mdx b/snippets/code-snippets/light-token/close-token-account/rust-client/basic.mdx new file mode 100644 index 0000000..054d06b --- /dev/null +++ b/snippets/code-snippets/light-token/close-token-account/rust-client/basic.mdx @@ -0,0 +1,11 @@ +```rust +use light_token_sdk::token::CloseTokenAccount; + +let close_instruction = CloseTokenAccount::new( + LIGHT_TOKEN_PROGRAM_ID, + account.pubkey(), + payer.pubkey(), // Destination for remaining lamports + owner, +) +.instruction()?; +``` diff --git a/snippets/code-snippets/light-token/close-token-account/rust-client/full.mdx b/snippets/code-snippets/light-token/close-token-account/rust-client/full.mdx new file mode 100644 index 0000000..c9871d5 --- /dev/null +++ b/snippets/code-snippets/light-token/close-token-account/rust-client/full.mdx @@ -0,0 +1,34 @@ +```rust +mod shared; + +use light_client::rpc::Rpc; +use light_token_sdk::token::{CloseAccount, LIGHT_TOKEN_PROGRAM_ID}; +use shared::SetupContext; +use solana_sdk::signer::Signer; + +#[tokio::test(flavor = "multi_thread")] +async fn close_account() { + // Setup creates mint and empty ATA (must be empty to close). + let SetupContext { + mut rpc, + payer, + ata, + .. + } = shared::setup_empty_ata().await; + let close_ix = CloseAccount::new( + LIGHT_TOKEN_PROGRAM_ID, + ata, + payer.pubkey(), + payer.pubkey(), + ) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[close_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let account_after = rpc.get_account(ata).await.unwrap(); + assert!(account_after.is_none()); +} +``` diff --git a/snippets/code-snippets/light-token/create-ata/program/anchor.mdx b/snippets/code-snippets/light-token/create-ata/program/anchor.mdx new file mode 100644 index 0000000..f46c8a5 --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/program/anchor.mdx @@ -0,0 +1,57 @@ +```rust +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; +use light_token_sdk::token::{CreateAssociatedAccountCpi, CompressibleParamsCpi}; + +declare_id!("77bt3j6A3g9s1WtwYnRFTGP9y8H1nigW7mLtywGKPmMi"); + +#[program] +pub mod light_token_anchor_create_ata { + use super::*; + + pub fn create_ata<'info>( + ctx: Context<'_, '_, '_, 'info, CreateAtaAccounts<'info>>, + bump: u8, + idempotent: bool, + ) -> Result<()> { + let compressible = CompressibleParamsCpi::new_ata( + ctx.accounts.compressible_config.to_account_info(), + ctx.accounts.rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ); + + CreateAssociatedAccountCpi { + owner: ctx.accounts.owner.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + payer: ctx.accounts.payer.to_account_info(), + associated_token_account: ctx.accounts.associated_token_account.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + bump, + compressible, + idempotent, + } + .invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateAtaAccounts<'info> { + /// CHECK: Validated by light-token CPI + pub owner: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + pub mint: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub associated_token_account: AccountInfo<'info>, + pub system_program: Program<'info, System>, + /// CHECK: Validated by light-token CPI + pub compressible_config: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} +``` diff --git a/snippets/code-snippets/light-token/create-ata/program/native.mdx b/snippets/code-snippets/light-token/create-ata/program/native.mdx new file mode 100644 index 0000000..32dfd1e --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/program/native.mdx @@ -0,0 +1,78 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::{ + CompressibleParamsCpi, CreateAssociatedAccountCpi, +}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn create_ata_invoke(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [owner, mint, payer, associated_token_account, system_program, compressible_config, rent_sponsor, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let bump = data[0]; + let idempotent = data.get(1).copied().unwrap_or(0) != 0; + + let compressible = CompressibleParamsCpi::new_ata( + compressible_config.clone(), + rent_sponsor.clone(), + system_program.clone(), + ); + + CreateAssociatedAccountCpi { + owner: owner.clone(), + mint: mint.clone(), + payer: payer.clone(), + associated_token_account: associated_token_account.clone(), + system_program: system_program.clone(), + bump, + compressible, + idempotent, + } + .invoke() +} + +pub fn create_ata_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [owner, mint, payer, associated_token_account, system_program, compressible_config, rent_sponsor, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 3 { + return Err(ProgramError::InvalidInstructionData); + } + + let bump = data[0]; + let idempotent = data[1] != 0; + let authority_bump = data[2]; + let signer_seeds = authority_seeds!(authority_bump); + + let compressible = CompressibleParamsCpi::new_ata( + compressible_config.clone(), + rent_sponsor.clone(), + system_program.clone(), + ); + + CreateAssociatedAccountCpi { + owner: owner.clone(), + mint: mint.clone(), + payer: payer.clone(), + associated_token_account: associated_token_account.clone(), + system_program: system_program.clone(), + bump, + compressible, + idempotent, + } + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/create-ata/rust-client/basic.mdx b/snippets/code-snippets/light-token/create-ata/rust-client/basic.mdx new file mode 100644 index 0000000..6e2a6af --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/rust-client/basic.mdx @@ -0,0 +1,10 @@ +```rust +use light_token_sdk::token::CreateAssociatedTokenAccount; + +let create_ata_ix = CreateAssociatedTokenAccount::new( + payer.pubkey(), + owner.pubkey(), + mint, +) +.instruction()?; +``` diff --git a/snippets/code-snippets/light-token/create-ata/rust-client/full.mdx b/snippets/code-snippets/light-token/create-ata/rust-client/full.mdx new file mode 100644 index 0000000..4576792 --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/rust-client/full.mdx @@ -0,0 +1,64 @@ +```rust +mod shared; + +use light_client::rpc::Rpc; +use light_token_sdk::token::{get_associated_token_address, CreateAssociatedTokenAccount}; +use shared::SplMintContext; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::test(flavor = "multi_thread")] +async fn create_ata() { + // You can use light, spl, t22 mints to create a light token ATA. + let SplMintContext { + mut rpc, + payer, + mint, + } = shared::setup_spl_mint_context().await; + + let owner = Keypair::new(); + + let create_ata_ix = CreateAssociatedTokenAccount::new( + payer.pubkey(), + owner.pubkey(), + mint, + ) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let expected_ata = get_associated_token_address(&owner.pubkey(), &mint); + let ata_account = rpc.get_account(expected_ata).await.unwrap(); + assert!(ata_account.is_some()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn create_ata_idempotent() { + let SplMintContext { + mut rpc, + payer, + mint, + } = shared::setup_spl_mint_context().await; + + let create_ata_ix = CreateAssociatedTokenAccount::new( + payer.pubkey(), + payer.pubkey(), + mint, + ) + .idempotent() + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_ix.clone()], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await; + + assert!(result.is_ok()); +} +``` diff --git a/snippets/code-snippets/light-token/create-ata/rust-client/idempotent.mdx b/snippets/code-snippets/light-token/create-ata/rust-client/idempotent.mdx new file mode 100644 index 0000000..5f4fa5f --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/rust-client/idempotent.mdx @@ -0,0 +1,11 @@ +```rust +use light_token_sdk::token::CreateAssociatedTokenAccount; + +let create_ata_ix = CreateAssociatedTokenAccount::new( + payer.pubkey(), + payer.pubkey(), + mint, +) +.idempotent() +.instruction()?; +``` diff --git a/snippets/code-snippets/light-token/create-mint/program/anchor.mdx b/snippets/code-snippets/light-token/create-mint/program/anchor.mdx new file mode 100644 index 0000000..fcce23f --- /dev/null +++ b/snippets/code-snippets/light-token/create-mint/program/anchor.mdx @@ -0,0 +1,102 @@ +```rust +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; +use light_token_sdk::token::{CreateMintCpi, CreateMintParams, SystemAccountInfos}; +use light_token_sdk::CompressedProof; + +declare_id!("Ev7tKaozVxbZLVGcKcVcz8A9yKZjUf5ATqoNSe5BDkjj"); + +#[program] +pub mod light_token_anchor_create_mint { + use super::*; + + pub fn create_mint<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMintAccounts<'info>>, + decimals: u8, + address_merkle_tree_root_index: u16, + compression_address: [u8; 32], + proof: CompressedProof, + freeze_authority: Option, + bump: u8, + rent_payment: Option, + write_top_up: Option, + ) -> Result<()> { + let mint = light_token_sdk::token::find_mint_address(ctx.accounts.mint_seed.key).0; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index, + mint_authority: *ctx.accounts.authority.key, + proof, + compression_address, + mint, + bump, + freeze_authority, + extensions: None, + rent_payment: rent_payment.unwrap_or(16), // Default: ~24 hours + write_top_up: write_top_up.unwrap_or(766), // Default: ~3 hours per write + }; + + let system_accounts = SystemAccountInfos { + light_system_program: ctx.accounts.light_system_program.to_account_info(), + cpi_authority_pda: ctx.accounts.cpi_authority_pda.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + }; + + CreateMintCpi { + mint_seed: ctx.accounts.mint_seed.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + payer: ctx.accounts.payer.to_account_info(), + address_tree: ctx.accounts.address_tree.to_account_info(), + output_queue: ctx.accounts.output_queue.to_account_info(), + compressible_config: ctx.accounts.compressible_config.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + rent_sponsor: ctx.accounts.rent_sponsor.to_account_info(), + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + } + .invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateMintAccounts<'info> { + pub mint_seed: Signer<'info>, + /// CHECK: Validated by light-token CPI + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub address_tree: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub output_queue: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + pub light_system_program: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + pub cpi_authority_pda: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + pub account_compression_program: AccountInfo<'info>, + pub system_program: Program<'info, System>, + /// CHECK: Validated by light-token CPI - use light_token_sdk::token::config_pda() + pub compressible_config: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI - derived from find_mint_address(mint_seed) + #[account(mut)] + pub mint: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI - use light_token_sdk::token::rent_sponsor_pda() + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} +``` diff --git a/snippets/code-snippets/light-token/create-mint/program/native.mdx b/snippets/code-snippets/light-token/create-mint/program/native.mdx new file mode 100644 index 0000000..081edac --- /dev/null +++ b/snippets/code-snippets/light-token/create-mint/program/native.mdx @@ -0,0 +1,194 @@ +```rust +use borsh::BorshDeserialize; +use light_token_interface::instructions::extensions::{ + token_metadata::TokenMetadataInstructionData, ExtensionInstructionData, +}; +use light_token_sdk::{ + token::{CreateMintCpi, CreateMintParams, SystemAccountInfos}, + CompressedProof, +}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, pubkey::Pubkey, +}; + +#[derive(BorshDeserialize)] +struct CreateMintData { + decimals: u8, + address_merkle_tree_root_index: u16, + mint_authority: Pubkey, + proof_a: [u8; 32], + proof_b: [u8; 64], + proof_c: [u8; 32], + compression_address: [u8; 32], + mint: Pubkey, + bump: u8, + freeze_authority: Option, + rent_payment: u8, + write_top_up: u32, + metadata_name: Option>, + metadata_symbol: Option>, + metadata_uri: Option>, +} + +pub fn create_mint_invoke(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [mint_seed, authority, payer, address_tree, output_queue, compressible_config, mint, rent_sponsor, light_system_program, cpi_authority_pda, registered_program_pda, account_compression_authority, account_compression_program, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let ix_data = CreateMintData::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + // Build token metadata extension if metadata fields are provided + let extensions = match ( + &ix_data.metadata_name, + &ix_data.metadata_symbol, + &ix_data.metadata_uri, + ) { + (Some(name), Some(symbol), Some(uri)) => Some(vec![ExtensionInstructionData::TokenMetadata( + TokenMetadataInstructionData { + update_authority: Some(ix_data.mint_authority.to_bytes().into()), + name: name.clone(), + symbol: symbol.clone(), + uri: uri.clone(), + additional_metadata: None, + }, + )]), + _ => None, + }; + + let params = CreateMintParams { + decimals: ix_data.decimals, + address_merkle_tree_root_index: ix_data.address_merkle_tree_root_index, + mint_authority: ix_data.mint_authority, + proof: CompressedProof { + a: ix_data.proof_a, + b: ix_data.proof_b, + c: ix_data.proof_c, + }, + compression_address: ix_data.compression_address, + mint: ix_data.mint, + bump: ix_data.bump, + freeze_authority: ix_data.freeze_authority, + extensions, + rent_payment: ix_data.rent_payment, + write_top_up: ix_data.write_top_up, + }; + + 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(), + }; + + CreateMintCpi::new( + mint_seed.clone(), + authority.clone(), + payer.clone(), + address_tree.clone(), + output_queue.clone(), + compressible_config.clone(), + mint.clone(), + rent_sponsor.clone(), + system_accounts, + params, + ) + .invoke() +} + +#[derive(BorshDeserialize)] +struct CreateMintSignedData { + decimals: u8, + address_merkle_tree_root_index: u16, + proof_a: [u8; 32], + proof_b: [u8; 64], + proof_c: [u8; 32], + compression_address: [u8; 32], + mint: Pubkey, + bump: u8, + freeze_authority: Option, + rent_payment: u8, + write_top_up: u32, + authority_bump: u8, + metadata_name: Option>, + metadata_symbol: Option>, + metadata_uri: Option>, +} + +pub fn create_mint_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [mint_seed, authority, payer, address_tree, output_queue, compressible_config, mint, rent_sponsor, light_system_program, cpi_authority_pda, registered_program_pda, account_compression_authority, account_compression_program, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let ix_data = CreateMintSignedData::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let signer_seeds: &[&[u8]] = &[b"authority", &[ix_data.authority_bump]]; + + // Build token metadata extension if metadata fields are provided + let extensions = match ( + &ix_data.metadata_name, + &ix_data.metadata_symbol, + &ix_data.metadata_uri, + ) { + (Some(name), Some(symbol), Some(uri)) => Some(vec![ExtensionInstructionData::TokenMetadata( + TokenMetadataInstructionData { + update_authority: Some(authority.key.to_bytes().into()), + name: name.clone(), + symbol: symbol.clone(), + uri: uri.clone(), + additional_metadata: None, + }, + )]), + _ => None, + }; + + let params = CreateMintParams { + decimals: ix_data.decimals, + address_merkle_tree_root_index: ix_data.address_merkle_tree_root_index, + mint_authority: *authority.key, + proof: CompressedProof { + a: ix_data.proof_a, + b: ix_data.proof_b, + c: ix_data.proof_c, + }, + compression_address: ix_data.compression_address, + mint: ix_data.mint, + bump: ix_data.bump, + freeze_authority: ix_data.freeze_authority, + extensions, + rent_payment: ix_data.rent_payment, + write_top_up: ix_data.write_top_up, + }; + + 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(), + }; + + CreateMintCpi::new( + mint_seed.clone(), + authority.clone(), + payer.clone(), + address_tree.clone(), + output_queue.clone(), + compressible_config.clone(), + mint.clone(), + rent_sponsor.clone(), + system_accounts, + params, + ) + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/create-mint/rust-client/basic.mdx b/snippets/code-snippets/light-token/create-mint/rust-client/basic.mdx new file mode 100644 index 0000000..e8c1445 --- /dev/null +++ b/snippets/code-snippets/light-token/create-mint/rust-client/basic.mdx @@ -0,0 +1,15 @@ +```rust +let params = CreateMintParams { + 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, + bump: find_mint_address(&mint_seed.pubkey()).1, + freeze_authority: None, + extensions: Some(vec![ExtensionInstructionData::TokenMetadata(...)]), + rent_payment: 16, // ~24 hours rent + write_top_up: 766, // ~3 hours per write +}; +``` diff --git a/snippets/code-snippets/light-token/create-mint/rust-client/full.mdx b/snippets/code-snippets/light-token/create-mint/rust-client/full.mdx new file mode 100644 index 0000000..4d12247 --- /dev/null +++ b/snippets/code-snippets/light-token/create-mint/rust-client/full.mdx @@ -0,0 +1,101 @@ +```rust +use light_client::{ + indexer::{AddressWithTree, Indexer}, + rpc::Rpc, +}; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token_sdk::token::{ + derive_mint_compressed_address, find_mint_address, CreateMint, CreateMintParams, +}; +use light_token_interface::{ + instructions::extensions::{ + token_metadata::TokenMetadataInstructionData, ExtensionInstructionData, + }, + state::AdditionalMetadata, +}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::test(flavor = "multi_thread")] +async fn create_mint() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None)) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_seed = Keypair::new(); + let decimals = 9u8; + + // Get address tree to store compressed address for when mint turns inactive + // We must create a compressed address at creation to ensure the mint does not exist yet + let address_tree = rpc.get_address_tree_v2(); + // Get state tree to store mint when inactive + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive mint addresses + let compression_address = + derive_mint_compressed_address(&mint_seed.pubkey(), &address_tree.tree); + let mint = find_mint_address(&mint_seed.pubkey()).0; // on-chain Mint PDA + + // Fetch validity proof to proof address does not exist yet + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build CreateMintParams with token metadata extension + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, // stores mint compressed address + mint_authority: payer.pubkey(), + proof: rpc_result.proof.0.unwrap(), + compression_address, // address for compression when mint turns inactive + mint, + bump: find_mint_address(&mint_seed.pubkey()).1, + 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"example".to_vec(), + }]), + }, + )]), + rent_payment: 16, // ~24 hours rent + write_top_up: 766, // ~3 hours rent per write + }; + + // Build and send instruction (mint_seed must sign) + let instruction = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ) + .instruction() + .unwrap(); + + let sig = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &mint_seed]) + .await + .unwrap(); + + println!("Mint: {}", mint); + println!("Tx: {}", sig); + + let mint_account = rpc.get_account(mint).await.unwrap(); + assert!(mint_account.is_some(), "Solana mint account should exist"); +} +``` diff --git a/snippets/code-snippets/light-token/create-token-account/program/anchor.mdx b/snippets/code-snippets/light-token/create-token-account/program/anchor.mdx new file mode 100644 index 0000000..4c7aae0 --- /dev/null +++ b/snippets/code-snippets/light-token/create-token-account/program/anchor.mdx @@ -0,0 +1,51 @@ +```rust +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; +use light_token_sdk::token::{CompressibleParamsCpi, CreateTokenAccountCpi}; + +declare_id!("4fi27siKEvKXJYN5WCzWuHdAw1rLed6Tprv9ZARv3Gxu"); + +#[program] +pub mod light_token_anchor_create_token_account { + use super::*; + + pub fn create_token_account<'info>( + ctx: Context<'_, '_, '_, 'info, CreateTokenAccountAccounts<'info>>, + owner: Pubkey, + ) -> Result<()> { + let compressible_params = CompressibleParamsCpi::new( + ctx.accounts.compressible_config.to_account_info(), + ctx.accounts.rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ); + + CreateTokenAccountCpi { + payer: ctx.accounts.payer.to_account_info(), + account: ctx.accounts.account.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + owner, + compressible: Some(compressible_params), + } + .invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateTokenAccountAccounts<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub account: Signer<'info>, + /// CHECK: Validated by light-token CPI + pub mint: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + pub compressible_config: AccountInfo<'info>, + pub system_program: Program<'info, System>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} +``` diff --git a/snippets/code-snippets/light-token/create-token-account/program/native.mdx b/snippets/code-snippets/light-token/create-token-account/program/native.mdx new file mode 100644 index 0000000..7c08412 --- /dev/null +++ b/snippets/code-snippets/light-token/create-token-account/program/native.mdx @@ -0,0 +1,66 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::{CompressibleParamsCpi, CreateTokenAccountCpi}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, pubkey::Pubkey, +}; + +pub fn create_token_account_invoke(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [payer, account, mint, compressible_config, system_program, rent_sponsor] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 32 { + return Err(ProgramError::InvalidInstructionData); + } + + let owner = Pubkey::try_from(&data[..32]).map_err(|_| ProgramError::InvalidInstructionData)?; + + let compressible_params = CompressibleParamsCpi::new( + compressible_config.clone(), + rent_sponsor.clone(), + system_program.clone(), + ); + + CreateTokenAccountCpi { + payer: payer.clone(), + account: account.clone(), + mint: mint.clone(), + owner, + compressible: Some(compressible_params), + } + .invoke() +} + +pub fn create_token_account_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [payer, account, mint, compressible_config, system_program, rent_sponsor] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 33 { + return Err(ProgramError::InvalidInstructionData); + } + + let owner = Pubkey::try_from(&data[..32]).map_err(|_| ProgramError::InvalidInstructionData)?; + let bump = data[32]; + let signer_seeds = authority_seeds!(bump); + + let compressible_params = CompressibleParamsCpi::new( + compressible_config.clone(), + rent_sponsor.clone(), + system_program.clone(), + ); + + CreateTokenAccountCpi { + payer: payer.clone(), + account: account.clone(), + mint: mint.clone(), + owner, + compressible: Some(compressible_params), + } + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/create-token-account/rust-client/basic.mdx b/snippets/code-snippets/light-token/create-token-account/rust-client/basic.mdx new file mode 100644 index 0000000..cf6f8f8 --- /dev/null +++ b/snippets/code-snippets/light-token/create-token-account/rust-client/basic.mdx @@ -0,0 +1,11 @@ +```rust +use light_token_sdk::token::CreateTokenAccount; + +let instruction = CreateTokenAccount::new( + payer.pubkey(), + account.pubkey(), + mint, + owner, +) +.instruction()?; +``` diff --git a/snippets/code-snippets/light-token/create-token-account/rust-client/full.mdx b/snippets/code-snippets/light-token/create-token-account/rust-client/full.mdx new file mode 100644 index 0000000..ddcf657 --- /dev/null +++ b/snippets/code-snippets/light-token/create-token-account/rust-client/full.mdx @@ -0,0 +1,37 @@ +```rust +mod shared; + +use light_client::rpc::Rpc; +use light_token_sdk::token::CreateTokenAccount; +use shared::SplMintContext; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::test(flavor = "multi_thread")] +async fn create_token_account() { + // Setup creates mint + // You can use light, spl, t22 mints to create a light token account. + let SplMintContext { + mut rpc, + payer, + mint, + } = shared::setup_spl_mint_context().await; + + let account = Keypair::new(); + + let create_account_ix = CreateTokenAccount::new( + payer.pubkey(), + account.pubkey(), + mint, + payer.pubkey(), + ) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_account_ix], &payer.pubkey(), &[&payer, &account]) + .await + .unwrap(); + + let account_data = rpc.get_account(account.pubkey()).await.unwrap(); + assert!(account_data.is_some()); +} +``` diff --git a/snippets/code-snippets/light-token/freeze-thaw/program/anchor.mdx b/snippets/code-snippets/light-token/freeze-thaw/program/anchor.mdx new file mode 100644 index 0000000..25d322f --- /dev/null +++ b/snippets/code-snippets/light-token/freeze-thaw/program/anchor.mdx @@ -0,0 +1,2 @@ +```rust +``` diff --git a/snippets/code-snippets/light-token/freeze-thaw/program/native.mdx b/snippets/code-snippets/light-token/freeze-thaw/program/native.mdx new file mode 100644 index 0000000..6ae4c75 --- /dev/null +++ b/snippets/code-snippets/light-token/freeze-thaw/program/native.mdx @@ -0,0 +1,87 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::FreezeCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn freeze_invoke(accounts: &[AccountInfo], _data: &[u8]) -> ProgramResult { + let [token_account, mint, freeze_authority, _token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + FreezeCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), + } + .invoke() +} + +pub fn freeze_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [token_account, mint, freeze_authority, _token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let bump = data[0]; + let signer_seeds = authority_seeds!(bump); + + FreezeCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), + } + .invoke_signed(&[signer_seeds]) +} + +// --- + +use super::authority_seeds; +use light_token_sdk::token::ThawCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn thaw_invoke(accounts: &[AccountInfo], _data: &[u8]) -> ProgramResult { + let [token_account, mint, freeze_authority, _token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + ThawCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), + } + .invoke() +} + +pub fn thaw_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [token_account, mint, freeze_authority, _token_program] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let bump = data[0]; + let signer_seeds = authority_seeds!(bump); + + ThawCpi { + token_account: token_account.clone(), + mint: mint.clone(), + freeze_authority: freeze_authority.clone(), + } + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/freeze-thaw/rust-client/freeze-full.mdx b/snippets/code-snippets/light-token/freeze-thaw/rust-client/freeze-full.mdx new file mode 100644 index 0000000..40ede2f --- /dev/null +++ b/snippets/code-snippets/light-token/freeze-thaw/rust-client/freeze-full.mdx @@ -0,0 +1,32 @@ +```rust +mod shared; + +use light_client::rpc::Rpc; +use light_token_sdk::token::Freeze; +use shared::SetupContext; +use solana_sdk::signer::Signer; + +#[tokio::test(flavor = "multi_thread")] +async fn test_freeze() { + // Setup creates mint, ATA with tokens, and approves delegate + let SetupContext { + mut rpc, + payer, + mint, + ata, + .. + } = shared::setup().await; + + let freeze_ix = Freeze { + token_account: ata, + mint, + freeze_authority: payer.pubkey(), + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[freeze_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); +} +``` diff --git a/snippets/code-snippets/light-token/freeze-thaw/rust-client/thaw-full.mdx b/snippets/code-snippets/light-token/freeze-thaw/rust-client/thaw-full.mdx new file mode 100644 index 0000000..6086434 --- /dev/null +++ b/snippets/code-snippets/light-token/freeze-thaw/rust-client/thaw-full.mdx @@ -0,0 +1,32 @@ +```rust +mod shared; + +use light_client::rpc::Rpc; +use light_token_sdk::token::Thaw; +use shared::SetupContext; +use solana_sdk::signer::Signer; + +#[tokio::test(flavor = "multi_thread")] +async fn thaw() { + // Setup creates mint, ATA with tokens, and freezes account + let SetupContext { + mut rpc, + payer, + mint, + ata, + .. + } = shared::setup_frozen().await; + + let thaw_ix = Thaw { + token_account: ata, + mint, + freeze_authority: payer.pubkey(), + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[thaw_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); +} +``` diff --git a/snippets/code-snippets/light-token/mint-to/program/anchor.mdx b/snippets/code-snippets/light-token/mint-to/program/anchor.mdx new file mode 100644 index 0000000..c661399 --- /dev/null +++ b/snippets/code-snippets/light-token/mint-to/program/anchor.mdx @@ -0,0 +1,43 @@ +```rust +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; +use light_token_sdk::token::MintToCpi; + +declare_id!("7SUgjNZYC1h89MuPVYkgEP5A4uYx5GFSjC7mzqMbN8U2"); + +#[program] +pub mod light_token_anchor_mint_to { + use super::*; + + pub fn mint_to<'info>( + ctx: Context<'_, '_, '_, 'info, MintToAccounts<'info>>, + amount: u64, + ) -> Result<()> { + MintToCpi { + mint: ctx.accounts.mint.to_account_info(), + destination: ctx.accounts.destination.to_account_info(), + amount, + authority: ctx.accounts.authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct MintToAccounts<'info> { + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub mint: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub destination: AccountInfo<'info>, + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, +} +``` diff --git a/snippets/code-snippets/light-token/mint-to/program/native.mdx b/snippets/code-snippets/light-token/mint-to/program/native.mdx new file mode 100644 index 0000000..79f50dc --- /dev/null +++ b/snippets/code-snippets/light-token/mint-to/program/native.mdx @@ -0,0 +1,57 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::MintToCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn mint_to_invoke(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [mint, destination, authority, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let amount = u64::from_le_bytes( + data.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ); + + MintToCpi { + mint: mint.clone(), + destination: destination.clone(), + amount, + authority: authority.clone(), + system_program: system_program.clone(), + max_top_up: None, + } + .invoke() +} + +pub fn mint_to_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [mint, destination, authority, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 9 { + return Err(ProgramError::InvalidInstructionData); + } + + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let bump = data[8]; + let signer_seeds = authority_seeds!(bump); + + MintToCpi { + mint: mint.clone(), + destination: destination.clone(), + amount, + authority: authority.clone(), + system_program: system_program.clone(), + max_top_up: None, + } + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/mint-to/rust-client/basic.mdx b/snippets/code-snippets/light-token/mint-to/rust-client/basic.mdx new file mode 100644 index 0000000..374fda5 --- /dev/null +++ b/snippets/code-snippets/light-token/mint-to/rust-client/basic.mdx @@ -0,0 +1,13 @@ +```rust +use light_token_sdk::token::MintTo; + +let instruction = MintTo::new( + params, + payer.pubkey(), + state_tree, + output_queue, + input_queue, + vec![recipient_account.pubkey()], +) +.instruction()?; +``` diff --git a/snippets/code-snippets/light-token/mint-to/rust-client/full.mdx b/snippets/code-snippets/light-token/mint-to/rust-client/full.mdx new file mode 100644 index 0000000..3441109 --- /dev/null +++ b/snippets/code-snippets/light-token/mint-to/rust-client/full.mdx @@ -0,0 +1,41 @@ +```rust +mod shared; + +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_sdk::token::MintTo; +use shared::SetupContext; +use solana_sdk::signer::Signer; + +#[tokio::test(flavor = "multi_thread")] +async fn mint_to() { + // Setup creates mint and empty ATA + let SetupContext { + mut rpc, + payer, + mint, + ata, + .. + } = shared::setup_empty_ata().await; + + let mint_amount = 1_000_000_000u64; + + let mint_to_ix = MintTo { + mint, + destination: ata, + amount: mint_amount, + authority: payer.pubkey(), + max_top_up: None, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[mint_to_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let ata_data = rpc.get_account(ata).await.unwrap().unwrap(); + let token = light_token_interface::state::Token::deserialize(&mut &ata_data.data[..]).unwrap(); + assert_eq!(token.amount, mint_amount); +} +``` diff --git a/snippets/code-snippets/light-token/transfer-interface/program/anchor.mdx b/snippets/code-snippets/light-token/transfer-interface/program/anchor.mdx new file mode 100644 index 0000000..da473a0 --- /dev/null +++ b/snippets/code-snippets/light-token/transfer-interface/program/anchor.mdx @@ -0,0 +1,50 @@ +```rust +#![allow(unexpected_cfgs)] + +use anchor_lang::prelude::*; +use light_token_sdk::token::TransferInterfaceCpi; + +declare_id!("ChkDqFsvNNT5CGrV2YCkmK4DiVSATnXc98mNozPbhC6u"); + +#[program] +pub mod light_token_anchor_transfer_interface { + use super::*; + + pub fn transfer<'info>( + ctx: Context<'_, '_, '_, 'info, TransferAccounts<'info>>, + amount: u64, + decimals: u8, + ) -> Result<()> { + TransferInterfaceCpi::new( + amount, + decimals, + ctx.accounts.source.to_account_info(), + ctx.accounts.destination.to_account_info(), + ctx.accounts.authority.to_account_info(), + ctx.accounts.payer.to_account_info(), + ctx.accounts.cpi_authority.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ) + .invoke()?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct TransferAccounts<'info> { + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub source: AccountInfo<'info>, + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub destination: AccountInfo<'info>, + pub authority: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Validated by light-token CPI + pub cpi_authority: AccountInfo<'info>, + pub system_program: Program<'info, System>, + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, +} +``` diff --git a/snippets/code-snippets/light-token/transfer-interface/program/native.mdx b/snippets/code-snippets/light-token/transfer-interface/program/native.mdx new file mode 100644 index 0000000..05c8293 --- /dev/null +++ b/snippets/code-snippets/light-token/transfer-interface/program/native.mdx @@ -0,0 +1,64 @@ +```rust +use super::authority_seeds; +use light_token_sdk::token::TransferInterfaceCpi; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, + program_error::ProgramError, +}; + +pub fn transfer_invoke(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [source, destination, authority, payer, ctoken_authority, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 9 { + return Err(ProgramError::InvalidInstructionData); + } + + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let decimals = data[8]; + + TransferInterfaceCpi::new( + amount, + decimals, + source.clone(), + destination.clone(), + authority.clone(), + payer.clone(), + ctoken_authority.clone(), + system_program.clone(), + ) + .invoke() +} + +pub fn transfer_invoke_signed(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [source, destination, authority, payer, ctoken_authority, system_program, _token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if data.len() < 10 { + return Err(ProgramError::InvalidInstructionData); + } + + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let decimals = data[8]; + let bump = data[9]; + let signer_seeds = authority_seeds!(bump); + + TransferInterfaceCpi::new( + amount, + decimals, + source.clone(), + destination.clone(), + authority.clone(), + payer.clone(), + ctoken_authority.clone(), + system_program.clone(), + ) + .invoke_signed(&[signer_seeds]) +} +``` diff --git a/snippets/code-snippets/light-token/transfer-interface/rust-client/full.mdx b/snippets/code-snippets/light-token/transfer-interface/rust-client/full.mdx new file mode 100644 index 0000000..9ab1fce --- /dev/null +++ b/snippets/code-snippets/light-token/transfer-interface/rust-client/full.mdx @@ -0,0 +1,123 @@ +```rust +mod shared; + +use anchor_spl::token::spl_token; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token_sdk::{ + spl_interface::find_spl_interface_pda_with_index, + token::{ + get_associated_token_address, CreateAssociatedTokenAccount, SplInterface, + TransferInterface, LIGHT_TOKEN_PROGRAM_ID, + }, +}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::test(flavor = "multi_thread")] +async fn transfer_interface() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(true, None)) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let recipient = Keypair::new(); + let decimals = 2u8; + let amount = 10_000u64; + + // Setup creates mint, mints tokens and creates SPL ATA + let mint = shared::setup_spl_mint(&mut rpc, &payer, decimals).await; + let spl_ata = shared::setup_spl_ata(&mut rpc, &payer, &mint, &payer.pubkey(), amount).await; + let (interface_pda, interface_bump) = find_spl_interface_pda_with_index(&mint, 0, false); + + // Create Light ATA + let light_ata_a = get_associated_token_address(&payer.pubkey(), &mint); + + let create_ata_a_ix = CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_a_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Create SPL interface PDA (holds SPL tokens when transferred to Light Token) + let spl_interface = SplInterface { + mint, + spl_token_program: spl_token::ID, + spl_interface_pda: interface_pda, + spl_interface_pda_bump: interface_bump, + }; + + // 1. Transfer SPL tokens to Light ATA + let spl_to_light_ix = TransferInterface { + source: spl_ata, + destination: light_ata_a, + amount, + decimals, + authority: payer.pubkey(), + payer: payer.pubkey(), + spl_interface: Some(spl_interface.clone()), + max_top_up: None, + source_owner: spl_token::ID, + destination_owner: LIGHT_TOKEN_PROGRAM_ID, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[spl_to_light_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Create Second Light ATA + let light_ata_b = get_associated_token_address(&recipient.pubkey(), &mint); + + let create_ata_b_ix = + CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_b_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // 2. Transfer Light-Tokens to Light ATA + let light_to_light_ix = TransferInterface { + source: light_ata_a, + destination: light_ata_b, + amount: amount / 2, + decimals, + authority: payer.pubkey(), + payer: payer.pubkey(), + spl_interface: None, + max_top_up: None, + source_owner: LIGHT_TOKEN_PROGRAM_ID, + destination_owner: LIGHT_TOKEN_PROGRAM_ID, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[light_to_light_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // 3. Transfer Light-Tokens from Light ATA to SPL + let light_to_spl_ix = TransferInterface { + source: light_ata_b, + destination: spl_ata, + amount: amount / 4, + decimals, + authority: recipient.pubkey(), + payer: payer.pubkey(), + spl_interface: Some(spl_interface), + max_top_up: None, + source_owner: LIGHT_TOKEN_PROGRAM_ID, + destination_owner: spl_token::ID, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[light_to_spl_ix], &payer.pubkey(), &[&payer, &recipient]) + .await + .unwrap(); +} +``` diff --git a/snippets/compressible-rent-explained.mdx b/snippets/compressible-rent-explained.mdx index 86d0dde..62b46d1 100644 --- a/snippets/compressible-rent-explained.mdx +++ b/snippets/compressible-rent-explained.mdx @@ -1,7 +1,11 @@ 1. The rent-exemption for light account creation is sponsored by the Light Token Program. 2. Transaction payer's pay rent per rent-epoch (388 lamports for 1.5h)
- to keep accounts "active". + to keep accounts "active" 3. "Inactive" accounts (rent below one epoch) get automatically compressed. 4. The account's state is cryptographically preserved and will be loaded into hot account state in-flight, when the account is used again.
-The hot state fee is paid for by the transaction payer when writing to the respective account. +The hot state fee is paid for by the transaction payer when writing to the respective account: + +* At account creation ~17,208 lamports for 24h of rent
+and compression incentive. +* When the account's rent is below 3h, the transaction payer tops up 776 lamports. diff --git a/snippets/jsx/code-compare.jsx b/snippets/jsx/code-compare.jsx index 86ec2b1..8d251cd 100644 --- a/snippets/jsx/code-compare.jsx +++ b/snippets/jsx/code-compare.jsx @@ -3,18 +3,46 @@ export const CodeCompare = ({ secondCode = "", firstLabel = "Light Token", secondLabel = "SPL", + language = "javascript", }) => { - const [sliderPercent, setSliderPercent] = useState(0); + const [sliderPercent, setSliderPercent] = useState(100); const [isDragging, setIsDragging] = useState(false); const [isAnimating, setIsAnimating] = useState(false); + const [copied, setCopied] = useState(false); const containerRef = useRef(null); const animationRef = useRef(null); - const isLightMode = sliderPercent > 50; + // When slider is on the right (100%), show first code; on left (0%), show second code + const showingFirst = sliderPercent > 50; + + const handleCopy = async () => { + const codeToCopy = showingFirst ? firstCode : secondCode; + await navigator.clipboard.writeText(codeToCopy); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; const highlightCode = (code) => { let escaped = code.replace(/&/g, "&").replace(//g, ">"); + if (language === "rust") { + // Rust syntax highlighting + const rustPattern = + /(\/\/.*$)|(["'])(?:(?!\2)[^\\]|\\.)*?\2|\b(use|let|mut|pub|fn|struct|impl|enum|mod|const|static|trait|type|where|for|in|if|else|match|loop|while|return|self|Self|true|false|Some|None|Ok|Err|Result|Option|vec!)\b|::([a-zA-Z_][a-zA-Z0-9_]*)|&([a-zA-Z_][a-zA-Z0-9_]*)|\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()|(\?)/gm; + + return escaped.replace(rustPattern, (match, comment, stringQuote, keyword, pathSegment, reference, func, questionMark) => { + if (comment) return `${match}`; + if (stringQuote) return `${match}`; + if (keyword) return `${match}`; + if (pathSegment) return `::${pathSegment}`; + if (reference) return `&${reference}`; + if (func) return `${match}`; + if (questionMark) return `?`; + return match; + }); + } + + // JavaScript/TypeScript syntax highlighting (default) 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; @@ -60,7 +88,7 @@ export const CodeCompare = ({ }; const handleToggle = () => { - animateTo(isLightMode ? 0 : 100); + animateTo(showingFirst ? 0 : 100); }; const handleMouseDown = (e) => { @@ -126,54 +154,64 @@ export const CodeCompare = ({ return ( <>
{/* Header with toggle */}
- {isLightMode ? secondLabel : firstLabel} + {showingFirst ? firstLabel : secondLabel} - {/* Neumorphic Toggle Switch */} -
+
+ {/* Copy button */} + + + {/* Neumorphic Toggle Switch */} +
{/* Toggle button */}
+
{/* Code container */} @@ -209,9 +248,9 @@ export const CodeCompare = ({ aria-valuemax={100} aria-label="Code comparison slider" > -
+
- {/* First code (background) */} + {/* Second code (background) - shown when slider is on left */}
 
-              {/* Second code (foreground) with clip-path */}
+              {/* First code (foreground) with clip-path - revealed when slider moves right */}
               
             
@@ -256,9 +295,9 @@ export const CodeCompare = ({ className="absolute top-0 bottom-0" style={{ right: "50%", - width: "80px", + width: "60px", background: - "linear-gradient(to left, rgba(0, 102, 255, 0.15) 0%, transparent 100%)", + "linear-gradient(to left, rgba(0, 102, 255, 0.08) 0%, transparent 100%)", }} /> diff --git a/snippets/light-token-configure-rent.mdx b/snippets/light-token-configure-rent.mdx index 5b4f701..cedea8b 100644 --- a/snippets/light-token-configure-rent.mdx +++ b/snippets/light-token-configure-rent.mdx @@ -1,7 +1,7 @@ ```rust -use light_token_sdk::token::CompressibleParamsInfos; +use light_token_sdk::token::CompressibleParamsCpi; -let compressible_params = CompressibleParamsInfos::new( +let compressible = CompressibleParamsCpi::new_ata( compressible_config.clone(), rent_sponsor.clone(), system_program.clone(), @@ -22,38 +22,29 @@ let compressible_params = CompressibleParamsInfos::new( - - - Compressible Config - - + + 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. + + Rent Sponsor + + Light token program PDA that pays rent exemption at creation and claims rent when account compresses. - - - System Program - - + + System Program + Solana System Program to create the on-chain account.