diff --git a/.mintignore b/.mintignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/.mintignore @@ -0,0 +1 @@ +node_modules diff --git a/.mintlifyignore b/.mintlifyignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/.mintlifyignore @@ -0,0 +1 @@ +node_modules/ diff --git a/client-library/client-guide.mdx b/client-library/client-guide.mdx index 89bd8cc7..deb82b16 100644 --- a/client-library/client-guide.mdx +++ b/client-library/client-guide.mdx @@ -127,8 +127,8 @@ Use the [API documentation](https://lightprotocol.github.io/light-protocol/) to ```bash npm install --save \ - @lightprotocol/stateless.js@beta \ - @lightprotocol/compressed-token@beta \ + @lightprotocol/stateless.js \ + @lightprotocol/compressed-token \ @solana/web3.js ``` @@ -138,8 +138,8 @@ npm install --save \ ```bash yarn add \ - @lightprotocol/stateless.js@beta \ - @lightprotocol/compressed-token@beta \ + @lightprotocol/stateless.js \ + @lightprotocol/compressed-token \ @solana/web3.js ``` @@ -149,8 +149,8 @@ yarn add \ ```bash pnpm add \ - @lightprotocol/stateless.js@beta \ - @lightprotocol/compressed-token@beta \ + @lightprotocol/stateless.js \ + @lightprotocol/compressed-token \ @solana/web3.js ``` @@ -182,7 +182,7 @@ const rpc = createRpc('https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY'); 1. Install the CLI ```bash -npm i -g @lightprotocol/zk-compression-cli@beta +npm i -g @lightprotocol/zk-compression-cli ``` 2. Start a local Solana test validator, photon indexer, and prover server on default ports 8899, 8784, and 3001. @@ -253,7 +253,7 @@ client.payer = read_keypair_file("~/.config/solana/id.json")?; 1. Install the CLI ```bash -npm i -g @lightprotocol/zk-compression-cli@beta +npm i -g @lightprotocol/zk-compression-cli ``` 2. Start a single-node Solana cluster, an RPC node, and a prover node at ports 8899, 8784, and 3001. diff --git a/cspell.json b/cspell.json index 447b8aac..4b56b634 100644 --- a/cspell.json +++ b/cspell.json @@ -192,7 +192,9 @@ "offramps", "zkcompression", "Tasktool", - "clippy" + "clippy", + "Raydium", + "raydium" ], "ignorePaths": [ "node_modules", diff --git a/docs.json b/docs.json index 1086a92d..15f1b562 100644 --- a/docs.json +++ b/docs.json @@ -55,24 +55,39 @@ "pages": [ "light-token/toolkits/overview", "light-token/toolkits/for-payments", - "light-token/toolkits/for-wallets" + "light-token/toolkits/for-wallets", + "light-token/toolkits/for-streaming-mints", + "light-token/toolkits/for-streaming-tokens" ] }, { "group": "Cookbook", "pages": [ "light-token/cookbook/overview", + { + "group": "Examples", + "expanded": true, + "pages": [ + "light-token/examples/client" + ] + }, { "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/transfer-checked", + "light-token/cookbook/approve-revoke", + "light-token/cookbook/freeze-thaw", "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" + ] } ] @@ -254,7 +269,6 @@ "references/whitepaper", "references/node-operators", "references/terminology", - "references/light-token-terminology", "references/migration-v1-to-v2", "support", "references/security" diff --git a/init-light.skill b/init-light.skill new file mode 100644 index 00000000..1701001b Binary files /dev/null and b/init-light.skill differ diff --git a/light-token/cookbook/approve-revoke.mdx b/light-token/cookbook/approve-revoke.mdx new file mode 100644 index 00000000..26fa1b1c --- /dev/null +++ b/light-token/cookbook/approve-revoke.mdx @@ -0,0 +1,174 @@ +--- +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 FullSetup from "/snippets/setup/full-setup.mdx"; +import { CodeCompare } from "/snippets/jsx/code-compare.jsx"; +import { + splApproveCode, + lightApproveCode, + splRevokeCode, + lightRevokeCode, + splApproveRustCode, + lightApproveRustCode, + splRevokeRustCode, + lightRevokeRustCode, +} from "/snippets/code-samples/code-compare-snippets.jsx"; +import TsApproveActionCode from "/snippets/code-snippets/light-token/approve-revoke/approve-action.mdx"; +import TsRevokeActionCode from "/snippets/code-snippets/light-token/approve-revoke/revoke-action.mdx"; +import ApproveActionCode from "/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-action.mdx"; +import ApproveInstructionCode from "/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-instruction.mdx"; +import RevokeActionCode from "/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-action.mdx"; +import RevokeInstructionCode from "/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-instruction.mdx"; +import ApproveAnchorProgramCode from "/snippets/code-snippets/light-token/approve/anchor-program/full-example.mdx"; +import RevokeAnchorProgramCode from "/snippets/code-snippets/light-token/revoke/anchor-program/full-example.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. + + + + + + + + +`approve` grants a delegate permission to transfer up to a specified amount of tokens. + + + + + + +`revoke` removes all delegate permissions from a Light Token account. + + + + + + + + Find the source code + [here](https://github.com/Lightprotocol/examples-light-token/tree/main/typescript-client/actions). + + + + +### Approve a delegate + + + + + + + + +### Revoke a delegate + + + + + + + + + + + + + + + + + + + + + + + + + +### Prerequisites + + + + + + +### Approve or revoke delegates + + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# Next Steps + + diff --git a/light-token/cookbook/burn.mdx b/light-token/cookbook/burn.mdx new file mode 100644 index 00000000..ca6b79ca --- /dev/null +++ b/light-token/cookbook/burn.mdx @@ -0,0 +1,73 @@ +--- +title: Burn Light Tokens +sidebarTitle: Burn +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 RustInstructionCode from "/snippets/code-snippets/light-token/burn/rust-client/instruction.mdx"; +import BurnCheckedRustInstructionCode from "/snippets/code-snippets/light-token/burn-checked/rust-client/instruction.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 + + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + +# Next Steps + + diff --git a/light-token/cookbook/close-token-account.mdx b/light-token/cookbook/close-token-account.mdx index bcc10901..7e294438 100644 --- a/light-token/cookbook/close-token-account.mdx +++ b/light-token/cookbook/close-token-account.mdx @@ -1,39 +1,44 @@ --- title: Close Light Token Account sidebarTitle: Close Token Account -description: Program guide to close light-token accounts via CPI. Includes step-by-step implementation and full code examples. +description: Program guide to close Light Token accounts via CPI. Includes step-by-step implementation and full code examples. 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 RustInstructionCode from "/snippets/code-snippets/light-token/close-token-account/rust-client/instruction.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). +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 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: + + @@ -44,335 +49,21 @@ 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) -} - -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[..])?) -} -``` - - - - - - +### Close Light Token Account - -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. + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + - - -```rust -use light_token_sdk::token::CloseTokenAccountCpi; - -CloseTokenAccountCpi { - token_program: token_program.clone(), - account: account.clone(), - destination: destination.clone(), - owner: owner.clone(), - rent_sponsor: Some(rent_sponsor.clone()), -} -.invoke()?; -``` - - - - -```rust -use light_token_sdk::token::CloseTokenAccountCpi; - -let close_account_cpi = CloseTokenAccountCpi { - token_program: token_program.clone(), - account: account.clone(), - destination: destination.clone(), - owner: owner.clone(), - rent_sponsor: Some(rent_sponsor.clone()), -}; - -let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; -close_account_cpi.invoke_signed(&[signer_seeds])?; -``` - + + - - - - - -# Full Code Example - - - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/close.rs). - - -```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,7 +73,7 @@ pub fn process_close_account_invoke_signed(accounts: &[AccountInfo]) -> Result<( {" "} 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 - -The `createAtaInterface` function creates an associated light-token account in a single call. +The `createAtaInterface` function creates an associated Light Token account in a single call. Compare to SPL: @@ -75,57 +74,21 @@ Compare to SPL: -## Custom Rent Config - - -The SDK uses the protocol's rent sponsor and config PDA by default. To pass a custom config explicitly: - -```typescript -import { createAssociatedTokenAccountInterfaceInstruction } from "@lightprotocol/compressed-token"; - -const ix = createAssociatedTokenAccountInterfaceInstruction( - payer.publicKey, - associatedToken, - owner.publicKey, - mint, - undefined, // token program id, defaults to light-token - undefined, // associated token program id, defaults to light-token - { - compressibleConfig: { - tokenAccountVersion: 3, - rentPayment: 16, - compressionOnly: 1, - writeTopUp: 766, - compressToAccountPubkey: null, - }, - configAccount: new PublicKey("ACXg8a7VaqecBWrSbdu73W4Pg9gsqXJ3EXAqkHyhvVXg"), // light token default config pda - rentPayerPda: new PublicKey("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"), // light token default rent sponsor pda - }, -); -``` - - - -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: - -```rust -use light_token_sdk::token::CreateAssociatedTokenAccount; +`CreateAssociatedTokenAccount` creates an on-chain ATA to store token balances of light, SPL, or Token 2022 mints. -let instruction = CreateAssociatedTokenAccount::new( - payer.pubkey(), - owner, - mint, -) -.instruction()?; -``` +Compare to SPL: -4. Send transaction & verify light-ATA creation with `get_account`. + @@ -140,352 +103,22 @@ 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) -} - -fn load_keypair(path: &str) -> Result> { - let path = if path.starts_with("~") { - path.replace("~", &env::var("HOME").unwrap_or_default()) - } else { - path.to_string() - }; - let file = fs::read_to_string(&path)?; - let bytes: Vec = serde_json::from_str(&file)?; - Ok(Keypair::try_from(&bytes[..])?) -} -``` - - - - - - - - -Find [a full code example at the end](#full-code-example). - - - - -### Define Rent Config Accounts - - - - - - - -### Build Account Infos and CPI the Light Token Program - -1. Pass the required accounts that include the rent config. -2. Use `invoke` or `invoke_signed`, when a CPI requires a PDA signer. - - The light-ATA address is derived from `[owner, light_token_program_id, mint]`. - Unlike light-token accounts, owner and mint are passed as accounts, not in - instruction data. - + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + - - -```rust -use light_token_sdk::token::CreateAssociatedTokenAccountCpi; - -CreateAssociatedTokenAccountCpi { - owner: owner.clone(), - mint: mint.clone(), - payer: payer.clone(), - associated_token_account: associated_token_account.clone(), - system_program: system_program.clone(), - bump: data.bump, - compressible: Some(compressible_params), - idempotent: false, -} -.invoke()?; -``` - + + - - -```rust -use light_token_sdk::token::CreateAssociatedTokenAccountCpi; - -let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; -CreateAssociatedTokenAccountCpi { - owner: owner.clone(), - mint: mint.clone(), - payer: payer.clone(), - associated_token_account: associated_token_account.clone(), - system_program: system_program.clone(), - bump: data.bump, - compressible: Some(compressible_params), - idempotent: false, -} -.invoke_signed(&[signer_seeds])?; -``` - + + - -# Full Code Example - - - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/create_ata.rs). - - -```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(()) -} -``` @@ -495,7 +128,7 @@ pub fn process_create_ata2_invoke_signed( {" "} + + + @@ -36,17 +48,17 @@ import TokenPoolInstructionCode from "/snippets/code-snippets/light-token/create `createMintInterface` is a unified interface that dispatches to different mint creation paths based on programId: - `TOKEN_PROGRAM_ID` or `TOKEN_2022_PROGRAM_ID` → delegates to SPL or T22 `createMint` -- Otherwise it defaults to `LIGHT_TOKEN_PROGRAM_ID` → creates a light-token mint +- Otherwise it defaults to `LIGHT_TOKEN_PROGRAM_ID` → creates a Light Token mint You can use the same interface regardless of mint type. Compare to SPL: @@ -78,56 +90,22 @@ Compare to SPL: -## Create SPL Mint with Token Pool - -Pass `TOKEN_PROGRAM_ID` to create a standard SPL mint with a token pool in one transaction. This enables compression for the mint. - - - - - - - - - - -## Enable Existing SPL Mints with Light ATAs - -Register a token pool for an existing SPL mint to enable compression. No mint authority required. - - - - - - - - - - -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 also writes a compressed mint address to the address Merkle tree, which preserves the mint state when the on-chain account is compressed. -3. Configure mint and your token metadata (name, symbol, URI, additional metadata) -4. Build the instruction with `CreateCMint::new()` and send the transaction. +Compare to SPL: -```rust -use light_token_sdk::token::CreateCMint; - -let create_cmint = CreateCMint::new( - params, - mint_signer.pubkey(), - payer.pubkey(), - address_tree.tree, - output_queue, -); -let instruction = create_cmint.instruction()?; -``` + @@ -140,618 +118,22 @@ 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[..])?) -} -``` - - - - - - - - -Find [a full code example at the end](#full-code-example). - - - - -### Configure Token Metadata - -```rust -use light_token_interface::{ - instructions::extensions::{ - token_metadata::TokenMetadataInstructionData, - ExtensionInstructionData, - }, - state::AdditionalMetadata, -}; - -let token_metadata = ExtensionInstructionData::TokenMetadata( - TokenMetadataInstructionData { - update_authority: Some(authority.to_bytes().into()), - name: b"My Token".to_vec(), - symbol: b"MTK".to_vec(), - uri: b"https://example.com/metadata.json".to_vec(), - additional_metadata: Some(vec![ - AdditionalMetadata { - key: b"category".to_vec(), - value: b"utility".to_vec(), - }, - ]), - }, -); -``` - - - **Fields must be set at light-mint creation.** Standard fields (`name`, - `symbol`, `uri`) can be updated by `update_authority`. For - `additional_metadata`, only existing keys can be modified or removed. New keys - cannot be added after creation. - - - - - -### Configure Mint - -Set `decimals`, `mint_authority`, `freeze_authority`, and pass the `token_metadata` from the previous step. - -```rust -use light_token_sdk::token::CreateCMintParams; - -let params = CreateCMintParams { - decimals: data.decimals, - address_merkle_tree_root_index: data.address_merkle_tree_root_index, - mint_authority: data.mint_authority, - proof: data.proof, - compression_address: data.compression_address, - mint: data.mint, - freeze_authority: data.freeze_authority, - extensions: data.extensions, -}; -``` - - The client passes a validity proof that proves the light-mint address does not - exist in the address tree where it will be stored. You can safely ignore - `compression_address` and `address_merkle_tree_root_index`. The client passes - these for proof verification. + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). - - - -### System Accounts - -Include system accounts such as the Light System Program required to interact with compressed state. -The client includes them in the instruction. - - - - - -```rust -use light_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 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(), - system_accounts, - cpi_context: None, - cpi_context_account: None, - params, -} -.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(), - 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])?; -``` - + + - - - -# 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). - - -```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(()) -} -``` - @@ -759,7 +141,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,347 +54,26 @@ 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) -} - -fn load_keypair(path: &str) -> Result> { - let path = if path.starts_with("~") { - path.replace("~", &env::var("HOME").unwrap_or_default()) - } else { - path.to_string() - }; - let file = fs::read_to_string(&path)?; - let bytes: Vec = serde_json::from_str(&file)?; - Ok(Keypair::try_from(&bytes[..])?) -} -``` - - - - - - - - - -Find [a full code example at the end](#full-code-example). - - - - -### Configure Rent - - - - - - -### Build Account Infos and CPI - -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. + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + - - -```rust -use light_token_sdk::token::CreateTokenAccountCpi; - -CreateTokenAccountCpi { - payer: payer.clone(), - account: account.clone(), - mint: mint.clone(), - owner: data.owner, - compressible: Some(compressible_params), -} -.invoke()?; -``` - - - - - - -```rust -use light_token_sdk::token::CreateTokenAccountCpi; - -let account_cpi = CreateTokenAccountCpi { - payer: payer.clone(), - account: account.clone(), - mint: mint.clone(), - owner: data.owner, - compressible: Some(compressible_params), -}; - -let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; -account_cpi.invoke_signed(&[signer_seeds])?; -``` - + + -# Full Code Example - - - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/create_token_account.rs). - - -```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 + + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + + + + + + + + + + + +
+ +# Next Steps + + diff --git a/light-token/cookbook/load-ata.mdx b/light-token/cookbook/load-ata.mdx index 17eced75..0f20c3e7 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. +title: Load Token Balances to Light Associated Token Account +sidebarTitle: Load Associated Token Account +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 19ac18f3..822d5c8a 100644 --- a/light-token/cookbook/mint-to.mdx +++ b/light-token/cookbook/mint-to.mdx @@ -1,5 +1,6 @@ --- title: Mint to Light Token Accounts +sidebarTitle: Mint To description: Client and program guide to mint tokens to a account. Includes step-by-step implementation and full code examples. keywords: ["mint tokens on solana", "spl mint to for developers", "token minting for protocols"] --- @@ -15,14 +16,17 @@ 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 RustActionCode from "/snippets/code-snippets/light-token/mint-to/rust-client/action.mdx"; +import RustInstructionCode from "/snippets/code-snippets/light-token/mint-to/rust-client/instruction.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 +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. @@ -30,15 +34,15 @@ import InstructionCode from "/snippets/code-snippets/light-token/mint-to/instruc `mintToInterface` mints tokens to token accounts in a single call. -The function auto-detects the token program (SPL, Token-2022, or Light) from the mint address. +The function auto-detects the token program (SPL, Token 2022, or Light) from the mint address. Compare to SPL: @@ -68,26 +72,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. - -```rust -use light_token_sdk::token::MintTo; +Compare to SPL: -let instruction = MintTo::new( - params, - payer.pubkey(), - state_tree, - output_queue, - input_queue, - vec![recipient_account.pubkey()], -) -.instruction()?; -``` + @@ -100,573 +95,30 @@ 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) -} - -fn load_keypair(path: &str) -> Result> { - let path = if path.starts_with("~") { - path.replace("~", &env::var("HOME").unwrap_or_default()) - } else { - path.to_string() - }; - let file = fs::read_to_string(&path)?; - let bytes: Vec = serde_json::from_str(&file)?; - Ok(Keypair::try_from(&bytes[..])?) -} -``` - - - - - - - -Find [a full code example at the end](#full-code-example). - - - - -### Configure Mint Parameters - -Include your mint, the amount of tokens to be minted and the pubkey of the mint authority. -The client passes a validity proof that proves the light-mint exists. - -```rust -use light_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. + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + - - -```rust -use light_token_sdk::token::MintToCpi; - -MintToCpi { - 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, -} -.invoke()?; -``` - + + - - -```rust -use light_token_sdk::token::MintToCpi; - -let account_infos = MintToCpi { - 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])?; -``` - + + - - - - - -# 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). - - -```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(()) -} -``` - + # Next Steps + + + + + + + + diff --git a/light-token/cookbook/transfer-checked.mdx b/light-token/cookbook/transfer-checked.mdx new file mode 100644 index 00000000..06b747f9 --- /dev/null +++ b/light-token/cookbook/transfer-checked.mdx @@ -0,0 +1,62 @@ +--- +title: Transfer Checked +sidebarTitle: Transfer Checked +description: Rust client guide to transfer Light Tokens with decimal validation. Includes step-by-step implementation and full code examples. +keywords: ["transfer tokens solana", "checked transfer", "decimal validation"] +--- + +--- + +import TokenClientPrerequisites from "/snippets/light-token-guides/light-token-client-prerequisites.mdx"; +import RustActionCode from "/snippets/code-snippets/light-token/transfer-checked/rust-client/action.mdx"; +import RustInstructionCode from "/snippets/code-snippets/light-token/transfer-checked/rust-client/instruction.mdx"; + +1. TransferChecked validates that the decimals parameter matches the mint's decimals. +2. Use for Light→Light transfers when you need decimal verification. +3. For transfers involving SPL or Token 2022 accounts, use [Transfer Interface](/light-token/cookbook/transfer-interface) instead. + + + + + + +### Prerequisites + + + + + + +### Transfer with Decimal Check + + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + + + + + +# Next Steps + +{" "} + + diff --git a/light-token/cookbook/transfer-interface.mdx b/light-token/cookbook/transfer-interface.mdx index 8ddce1f0..d4083af9 100644 --- a/light-token/cookbook/transfer-interface.mdx +++ b/light-token/cookbook/transfer-interface.mdx @@ -1,13 +1,11 @@ --- title: Transfer Interface -description: Program and client guide for transfers between light-token and SPL token accounts. The interface detects account types and invokes the right programs. +description: Program and client guide for transfers between Light Token and SPL token accounts. The interface detects account types and invokes the right programs. keywords: ["transfer tokens on solana", "token transfer on solana", "interface methods on solana", "spl transfer for developers"] --- --- -import TransferInterfaceAccountsListCtoken1 from "/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx"; -import TransferInterfaceAccountsListSpl2 from "/snippets/accounts-list/transfer-interface-accounts-list-spl-2.mdx"; import TransferInterfaceIntro from "/snippets/light-token-guides/transfer-interface-intro.mdx"; import TokenClientPrerequisites from "/snippets/light-token-guides/light-token-client-prerequisites.mdx"; import TokenTsClientPrerequisites from "/snippets/light-token-guides/light-token-ts-client-prerequisites.mdx"; @@ -17,58 +15,59 @@ import FullSetup from "/snippets/setup/full-setup.mdx"; import { splTransferCode, lightTransferCode, + splTransferRustCode, + lightTransferRustCode, } from "/snippets/code-samples/code-compare-snippets.jsx"; import ActionCode from "/snippets/code-snippets/light-token/transfer-interface/action.mdx"; import InstructionCode from "/snippets/code-snippets/light-token/transfer-interface/instruction.mdx"; - +import RustActionCode from "/snippets/code-snippets/light-token/transfer-interface/rust-client/action.mdx"; +import RustInstructionCode from "/snippets/code-snippets/light-token/transfer-interface/rust-client/instruction.mdx"; - + - + - +
**light-token -> light-token Account****Light Token -> Light Token Account**
    -
  • Transfers tokens between light-token accounts
  • +
  • Transfers tokens between Light Token accounts
**SPL token -> light-token Account****SPL token -> Light Token Account**
    -
  • Transfers SPL tokens to light-token accounts
  • +
  • Transfers SPL tokens to Light Token accounts
  • SPL tokens are locked in interface PDA
  • -
  • Tokens are minted to light-token account
  • +
  • Tokens are minted to Light Token account
**light-token -> SPL Account****Light Token -> SPL Account**
  • Releases SPL tokens from interface PDA to SPL account
  • -
  • Burns tokens in source light-token account
  • +
  • Burns tokens in source Light Token account
-## Get Started - -The `transferInterface` function transfers tokens between token accounts (SPL, Token-2022, or Light) in a single call. +The `transferInterface` function transfers tokens between token accounts (SPL, Token 2022, or Light) in a single call. Compare to SPL: @@ -99,11 +98,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,457 +117,30 @@ The example transfers SPL token -> light-token and light-token -> light-token: -### Transfer Interface - - - -```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 - -- 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(), - authority.clone(), - payer.clone(), - compressed_token_program_authority.clone(), -); -``` - - - - - - - - - -### SPL Transfer Interface (Optional) - -The SPL transfer interface is only needed for SPL ↔ light-token transfers. - -```rust -transfer = transfer.with_spl_interface( - Some(mint.clone()), - Some(spl_token_program.clone()), - Some(spl_interface_pda.clone()), - data.spl_interface_pda_bump, -)?; -``` - -SPL ↔ light-token transfers require a `spl_interface_pda`: - -- **SPL → light-token**: SPL tokens are locked by the light token program in the PDA, light-tokens are minted to light-token accounts -- **light-token → SPL**: light-tokens are burned, SPL tokens transferred to SPL token accounts - -The interface PDA is derived from the `mint` pubkey and pool seed. - - - - - - - +### Transfer Interface -### CPI +The example transfers +1. SPL token -> Light Token, +2. Light Token -> Light Token, and +3. Light Token -> SPL token. -CPI the Light Token program to execute the transfer. -Use `invoke()`, or `invoke_signed()` when a CPI requires a PDA signer. + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + - - -```rust -transfer.invoke()?; -``` - - - - -```rust -let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]]; -transfer.invoke_signed(&[authority_seeds])?; -``` - - + + + + + + -# Full Code Example - - - Find the source code - [here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-tests/sdk-light-token-test/src/transfer_interface.rs). - - -```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,9 +150,9 @@ pub fn process_transfer_interface_invoke_signed( {" "} diff --git a/light-token/cookbook/update-metadata.mdx b/light-token/cookbook/update-metadata.mdx deleted file mode 100644 index e69de29b..00000000 diff --git a/light-token/cookbook/wrap-unwrap.mdx b/light-token/cookbook/wrap-unwrap.mdx index 59a50eeb..72728b2a 100644 --- a/light-token/cookbook/wrap-unwrap.mdx +++ b/light-token/cookbook/wrap-unwrap.mdx @@ -1,7 +1,7 @@ --- -title: Wrap & Unwrap SPL/Token-22 <> Light Token +title: Wrap & Unwrap SPL/Token 2022 <> Light Token sidebarTitle: Wrap & Unwrap SPL -description: Move tokens between SPL/Token-22 token and light-token accounts. Use to interact with applications that only support SPL/Token-22. +description: Move tokens between SPL/Token 2022 token and Light Token accounts. Use to interact with applications that only support SPL/Token 2022. keywords: ["wrap tokens on solana", "unwrap tokens for developers", "spl to light token conversion"] --- @@ -12,9 +12,12 @@ import WrapActionCode from "/snippets/code-snippets/light-token/wrap/action.mdx" import WrapInstructionCode from "/snippets/code-snippets/light-token/wrap/instruction.mdx"; import UnwrapActionCode from "/snippets/code-snippets/light-token/unwrap/action.mdx"; import UnwrapInstructionCode from "/snippets/code-snippets/light-token/unwrap/instruction.mdx"; +import WrapRustActionCode from "/snippets/code-snippets/light-token/wrap/rust-client/action.mdx"; +import UnwrapRustActionCode from "/snippets/code-snippets/light-token/unwrap/rust-client/action.mdx"; +import TokenClientPrerequisites from "/snippets/light-token-guides/light-token-client-prerequisites.mdx"; -- **Wrap**: Move tokens from SPL/T22 account → light-token ATA (hot balance) -- **Unwrap**: Move tokens from light-token ATA (hot balance) → SPL/T22 account +- **Wrap**: Move tokens from SPL/T22 account → Light Token ATA (hot balance) +- **Unwrap**: Move tokens from Light Token ATA (hot balance) → SPL/T22 account Find the source code: @@ -23,7 +26,8 @@ import UnwrapInstructionCode from "/snippets/code-snippets/light-token/unwrap/in [unwrap.ts](https://github.com/Lightprotocol/light-protocol/blob/0c4e2417b2df2d564721b89e18d1aad3665120e7/js/compressed-token/src/v3/actions/unwrap.ts) -## Get Started + + @@ -68,3 +72,66 @@ import UnwrapInstructionCode from "/snippets/code-snippets/light-token/unwrap/in + + + + + + + + + +### Prerequisites + + + + + + +### Wrap SPL tokens to Light Token ATA + + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + + + +### Prerequisites + + + + + + +### Unwrap Light Tokens to SPL account + + + Find the full example including shared test utilities in the + [examples-light-token](https://github.com/Lightprotocol/examples-light-token/tree/main/rust-client). + + + + + + + + + + + + + + + diff --git a/light-token/examples/client.mdx b/light-token/examples/client.mdx new file mode 100644 index 00000000..a0c89492 --- /dev/null +++ b/light-token/examples/client.mdx @@ -0,0 +1,11 @@ +--- +title: "Client examples" +sidebarTitle: "Client" +description: "TypeScript and Rust client examples for light-token SDK." +--- + +import ClientExamplesTable from "/snippets/overview-tables/light-token-client-examples-table.mdx"; + +Find all examples on Github: [examples-light-token](https://github.com/Lightprotocol/examples-light-token) + + 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 7d96a5c1..7046300c 100644 --- a/light-token/toolkits/for-streaming-mints.mdx +++ b/light-token/toolkits/for-streaming-mints.mdx @@ -17,29 +17,42 @@ Find devnet examples [here](https://github.com/Lightprotocol/examples-light-toke ## Setup +Light mints are Solana accounts owned by the Light Token Program. Subscribe to account updates to detect new mints and changes. + ```toml Cargo.toml [dependencies] helius-laserstream = "0.1" tokio = { version = "1", features = ["full"] } futures = "0.3" -light-event = "0.3" -light-compressed-account = "0.8" -light-token-interface = "0.2" -borsh = "0.10" +anyhow = "1" +dotenvy = "0.15" bs58 = "0.5" +borsh = "0.10" + +light-token-interface = "0.3.0" +light-compressed-account = { version = "0.9.0", features = ["std"] } + +# Pin blake3 to avoid constant_time_eq edition2024 issue +blake3 = "=1.5.5" ``` ```rust +use borsh::BorshDeserialize; use futures::StreamExt; +use helius_laserstream::grpc::subscribe_request_filter_accounts_filter::Filter; +use helius_laserstream::grpc::subscribe_request_filter_accounts_filter_memcmp::Data; +use helius_laserstream::grpc::{ + SubscribeRequestFilterAccounts, SubscribeRequestFilterAccountsFilter, + SubscribeRequestFilterAccountsFilterMemcmp, +}; use helius_laserstream::{subscribe, LaserstreamConfig}; -use light_event::parse::event_from_light_transaction; -use light_token_interface::state::mint::Mint; -use light_token_interface::state::extensions::ExtensionStruct; -use borsh::BorshDeserialize; +use light_token_interface::state::{ExtensionStruct, Mint}; -const LIGHT_SYSTEM_PROGRAM: &str = "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"; const LIGHT_TOKEN_PROGRAM_ID: &str = "cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"; -const COMPRESSED_MINT_DISCRIMINATOR: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 1]; + +/// Byte offset of account_type in the Mint account data. +/// BaseMint (82) + MintMetadata (67) + reserved (16) = 165 +const ACCOUNT_TYPE_OFFSET: u64 = 165; ``` @@ -71,13 +84,20 @@ let config = LaserstreamConfig::new( ```rust +// Subscribe to Solana accounts owned by the Light Token Program +// with account_type = 1 (Mint) at byte offset 165 let request = helius_laserstream::grpc::SubscribeRequest { - transactions: [( - "light".to_string(), - helius_laserstream::grpc::SubscribeRequestFilterTransactions { - vote: Some(false), - failed: Some(false), - account_include: vec![LIGHT_SYSTEM_PROGRAM.to_string()], + accounts: [( + "light_mints".to_string(), + SubscribeRequestFilterAccounts { + owner: vec![LIGHT_TOKEN_PROGRAM_ID.to_string()], + filters: vec![SubscribeRequestFilterAccountsFilter { + filter: Some(Filter::Memcmp(SubscribeRequestFilterAccountsFilterMemcmp { + offset: ACCOUNT_TYPE_OFFSET, + data: Some(Data::Bytes(vec![1])), + })), + }], + nonempty_txn_signature: Some(true), ..Default::default() }, )] @@ -89,73 +109,53 @@ let (stream, _handle) = subscribe(config, request); tokio::pin!(stream); while let Some(update) = stream.next().await { - // Process transactions... + // Process account updates... } ```
-### Parse Events +### Deserialize mint accounts ```rust -fn process_transaction(msg: &Message, meta: Option<&TransactionStatusMeta>) { - let account_keys = extract_account_keys(msg, meta); - - let (program_ids, instruction_data, accounts_per_ix) = - extract_instructions(msg, meta, &account_keys); - - match event_from_light_transaction(&program_ids, &instruction_data, accounts_per_ix) { - Ok(Some(batches)) => { - for batch in batches { - println!( - "Event: {} inputs, {} outputs", - batch.event.input_compressed_account_hashes.len(), - batch.event.output_compressed_accounts.len() - ); +if let Some(helius_laserstream::grpc::subscribe_update::UpdateOneof::Account( + account_update, +)) = msg.update_oneof +{ + if let Some(account_info) = account_update.account { + let pubkey = bs58::encode(&account_info.pubkey).into_string(); + let tx_sig = account_info + .txn_signature + .as_ref() + .map(|s| bs58::encode(s).into_string()) + .unwrap_or_default(); + + match Mint::deserialize(&mut account_info.data.as_slice()) { + Ok(mint) => { + println!("Mint: {}", pubkey); + println!(" tx: {}", tx_sig); + println!(" decimals: {}", mint.base.decimals); + println!(" supply: {}", mint.base.supply); + + if let Some(authority) = &mint.base.mint_authority { + println!( + " mint_authority: {}", + bs58::encode(authority.to_bytes()).into_string() + ); + } + + if let Some(authority) = &mint.base.freeze_authority { + println!( + " freeze_authority: {}", + bs58::encode(authority.to_bytes()).into_string() + ); + } + } + Err(e) => { + eprintln!("Failed to deserialize mint {}: {}", pubkey, e); } } - Ok(None) => {} // No compressed account events - Err(e) => eprintln!("Parse error: {:?}", e), - } -} -``` - - - - -### Extract Mints - -```rust -for output in event.output_compressed_accounts.iter() { - let owner = output.compressed_account.owner; - - // Check if owned by light token program - let light_token_program_id = bs58::decode(LIGHT_TOKEN_PROGRAM_ID).into_vec().unwrap(); - if owner != light_token_program_id.as_slice() { - continue; - } - - // Check discriminator - let data = match &output.compressed_account.data { - Some(d) if d.discriminator == COMPRESSED_MINT_DISCRIMINATOR => &d.data, - _ => continue, - }; - - // Deserialize - let mint = Mint::try_from_slice(data)?; - - // Check if new (address not in inputs) - let is_new = output - .compressed_account - .address - .map(|addr| { - !event.input_compressed_account_hashes.iter().any(|h| *h == addr) - }) - .unwrap_or(true); - - if is_new { - println!("New mint: {}", bs58::encode(mint.metadata.mint).into_string()); } } ``` @@ -163,7 +163,7 @@ for output in event.output_compressed_accounts.iter() { -### Extract Token Metadata from Mint Extensions +### Extract Token Metadata from mint extensions ```rust fn extract_metadata(mint: &Mint) -> Option<(String, String, String)> { @@ -202,16 +202,13 @@ if let Some((name, symbol, uri)) = extract_metadata(&mint) { pub struct Mint { pub base: BaseMint, pub metadata: MintMetadata, - /// Reserved bytes (16 bytes) for T22 layout compatibility. pub reserved: [u8; 16], - /// Account type discriminator at byte offset 165 (1 = Mint, 2 = Account) pub account_type: u8, - /// Compression info embedded directly in the mint 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, @@ -221,19 +218,13 @@ pub struct BaseMint { pub freeze_authority: Option, } -/// Light Protocol-specific metadata for compressed mints. -/// Total size: 67 bytes +/// Light Protocol metadata for mints (67 bytes) #[repr(C)] pub struct MintMetadata { - /// Version for upgradability pub version: u8, - /// Whether the compressed mint has been decompressed to a Mint Solana account. pub mint_decompressed: bool, - /// PDA derived from mint_signer pub mint: Pubkey, - /// Signer pubkey used to derive the mint PDA pub mint_signer: [u8; 32], - /// Bump seed from mint PDA derivation pub bump: u8, } ``` diff --git a/light-token/toolkits/for-streaming-tokens.mdx b/light-token/toolkits/for-streaming-tokens.mdx index 8b181e66..d1e78f7b 100644 --- a/light-token/toolkits/for-streaming-tokens.mdx +++ b/light-token/toolkits/for-streaming-tokens.mdx @@ -5,153 +5,41 @@ description: "Light token accounts follow the same layout as SPL-token accounts, keywords: ["streaming tokens for solana apps", "scalable token distribution on solana", "token streaming for developers"] --- -import ToolkitsSetup from "/snippets/setup/toolkits-setup.mdx"; +import ActionCode from "/snippets/code-snippets/light-token/load-ata/action.mdx"; +import InstructionCode from "/snippets/code-snippets/light-token/load-ata/instruction.mdx"; -When a market becomes inactive, its token accounts and related PDAs will be compressed - their state is committed and effectively frozen until a client decompresses it. +When a market becomes inactive, its token accounts and related PDAs will +be compressed automatically (cold storage). +The state is cryptographically preserved on the Solana ledger. While compressed, pure on-chain lookups will return uninitialized. -Your indexer should keep tracking, quoting, and routing markets even if the on-chain account shows `is_initialized: false`, `is_compressed: true`. To trade a cold market, the first client must prepend an idempotent decompress "warm up" instruction. +Your indexer should keep tracking, quoting, and routing markets even if the +on-chain account shows `is_initialized: false`, `is_compressed: true`. +To trade a cold market, the first client must prepend an +idempotent decompress "warm up" instruction. Find the source code [here](https://github.com/Lightprotocol/light-protocol/blob/main/js/compressed-token/tests/e2e/load-ata-standard.test.ts). -## Setup - - - -### Load Compressed Tokens to Hot Balance - - -```typescript Action -import { Keypair } from "@solana/web3.js"; -import { createRpc, bn } from "@lightprotocol/stateless.js"; -import { - createMint, - mintTo, - loadAta, - getAssociatedTokenAddressInterface, -} from "@lightprotocol/compressed-token"; - -async function main() { - const rpc = createRpc(); - const payer = Keypair.generate(); - await rpc.requestAirdrop(payer.publicKey, 10e9); - - const owner = Keypair.generate(); - await rpc.requestAirdrop(owner.publicKey, 1e9); - - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const { mint } = await createMint( - rpc, - payer, - mintAuthority.publicKey, - 9, - mintKeypair, - ); - - await mintTo(rpc, payer, mint, owner.publicKey, mintAuthority, bn(1000)); - - // Get light-token ATA address - const tokenAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); - - // Load compressed tokens to hot balance - // Creates ATA if needed, returns null if nothing to load - const signature = await loadAta(rpc, tokenAta, owner, mint, payer); - - if (signature) { - console.log("Loaded tokens to hot balance"); - console.log("Transaction:", signature); - } else { - console.log("Nothing to load"); - } -} - -main().catch(console.error); -``` - -```typescript Instruction -import { Keypair, ComputeBudgetProgram } from "@solana/web3.js"; -import { - createRpc, - bn, - buildAndSignTx, - sendAndConfirmTx, - dedupeSigner, -} from "@lightprotocol/stateless.js"; -import { - createMint, - mintTo, - createLoadAtaInstructions, - getAssociatedTokenAddressInterface, -} from "@lightprotocol/compressed-token"; - -async function main() { - const rpc = createRpc(); - const payer = Keypair.generate(); - await rpc.requestAirdrop(payer.publicKey, 10e9); - - const owner = Keypair.generate(); - await rpc.requestAirdrop(owner.publicKey, 1e9); - - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const { mint } = await createMint( - rpc, - payer, - mintAuthority.publicKey, - 9, - mintKeypair - ); - - await mintTo(rpc, payer, mint, owner.publicKey, mintAuthority, bn(1000)); - - // Get light-token ATA address - const tokenAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); - - // Create load instructions - const ixs = await createLoadAtaInstructions( - rpc, - tokenAta, - owner.publicKey, - mint, - payer.publicKey - ); - - if (ixs.length === 0) { - console.log("Nothing to load"); - return; - } - - // Build, sign, and send transaction - const { blockhash } = await rpc.getLatestBlockhash(); - const additionalSigners = dedupeSigner(payer, [owner]); - - const tx = buildAndSignTx( - [ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ...ixs], - payer, - blockhash, - additionalSigners - ); - - const signature = await sendAndConfirmTx(rpc, tx); - console.log("Loaded tokens to hot balance"); - console.log("Transaction:", signature); -} - -main().catch(console.error); -``` +### Load compressed tokens (cold storage) to Associated Token Account (hot balance) - + + + + + + + + -# Stream Light-Mint Accounts +# Stream light-mint accounts +/> \ No newline at end of file diff --git a/light-token/toolkits/overview.mdx b/light-token/toolkits/overview.mdx index 485e7df9..30e36eb4 100644 --- a/light-token/toolkits/overview.mdx +++ b/light-token/toolkits/overview.mdx @@ -25,7 +25,7 @@ keywords: ["token sdk for solana developers", "token infrastructure for solana a Allow users to display and swap light-tokens. -{/* Stream token events from the network in real-time. - */} +
diff --git a/references/light-token-terminology.mdx b/references/light-token-terminology.mdx deleted file mode 100644 index 9be0bda4..00000000 --- a/references/light-token-terminology.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: "Light Token terminology" -description: "Terminology for Light Token concepts and operations" ---- - -For ZK Compression terms, see [Terminology](/references/terminology). - -## Claim - -An instruction to recover rent from expired compressible accounts. When a Light Token account's prepaid rent period expires, any user can claim the rent-exemption lamports stored in the account. - -## Compressibility - -The state when a Light Token account can be compressed permissionlessly. An account becomes compressible when its prepaid rent period expires. The `is_compressible()` function checks the current slot against the account's rent configuration. - -## Compressible extension - -An extension on decompressed Light Token accounts that tracks compression configuration and rent data. Fields include `decimals`, `compression_only`, `is_ata`, and embedded `CompressionInfo`. Accounts with this extension become eligible for permissionless compression when their rent expires. - -Discriminator: 32 - -## CompressibleConfig - -A registry account that stores compression policy for a set of Light Token accounts. Controls rent configuration, compression authority, and address space allocation. - -- **PDA seeds**: `["compressible_config", version_bytes]` -- **States**: Inactive (0), Active (1), Deprecated (2) - -## Compress - -An operation that moves tokens from a decompressed Light Token account into a compressed account stored in a state tree. The token data becomes a leaf in the Merkle tree, and the on-chain Solana account can be closed. - -## CompressAndClose - -An instruction that atomically compresses a Light Token account's balance and closes the Solana account. Rent-exemption lamports return to the rent sponsor. Requires the Compressible extension. - -## Compressed (cold) state - -A Light Token state where token data exists as a leaf in a state Merkle tree. No on-chain Solana account exists. Requires a validity proof to read or modify. Uses ZK Compression state trees. - -Internal identifier: `ctoken-cold` - -## Compressed Mint - -A mint account for Light Tokens stored in a state tree. Contains SPL-compatible fields (`mint_authority`, `supply`, `decimals`, `freeze_authority`) plus Light Protocol metadata. Can be decompressed to a Solana account. - -Discriminator: 1 - -Internal type: `CMint` - - - **Source**: https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token - - -## CompressedOnly extension - -An extension on compressed token accounts indicating they can only be decompressed, not transferred. Created when a Light Token account with this extension is compressed-and-closed. Fields include `delegated_amount`, `withheld_transfer_fee`, and `is_ata`. - -Discriminator: 31 - -## CompressionInfo - -A structure embedded in the Compressible extension that tracks rent and compression configuration. Fields include `config_account_version`, `compress_to_pubkey`, `lamports_per_write`, `compression_authority`, `rent_sponsor`, `last_claimed_slot`, `rent_exemption_paid`, and `rent_config`. - -## Decompress - -An operation that moves tokens from a compressed account in a state tree to a decompressed Light Token account on-chain. Creates or updates a Solana account with the token data. - -## Decompressed (hot) state - -A Light Token state where token data exists in an on-chain Solana account with the Compressible extension. Supports direct read/write access without validity proofs. Standard Solana rent-exemption applies and is reclaimable on compression. - -Internal identifier: `ctoken-hot` - -## Light Token - -A token system built on ZK Compression with sponsored rent-exemption. Token data can exist in Merkle trees (compressed state) or on-chain Solana accounts (decompressed state). Uses ZK Compression state trees and validity proofs. - -**Canonical name**: Light Token - -**Codebase aliases**: `light-token` (Rust crate), `c-token` (JS/TS package), `CToken` (Rust types), `compressed-token` (npm package) - - - **Source**: https://github.com/Lightprotocol/light-protocol/tree/main/programs/compressed-token - - -## Light Token account - -A decompressed token account on-chain with SPL Token layout plus the Compressible extension. Stores mint, owner, amount, delegate, and state fields. - -Discriminator: 2 (matches SPL Token) - -Program owner: `light-compressed-token` - -Internal type: `CToken` - -## MintAction - -A batch instruction for compressed mint operations. Supports `CreateMint`, `MintTo`, `MintToCompressed`, `UpdateMintAuthority`, `UpdateFreezeAuthority`, `UpdateMetadataField`, `UpdateMetadataAuthority`, `RemoveMetadataKey`, `DecompressMint`, and `CompressAndCloseMint`. - -Discriminator: 103 - -## Non-unified path - -An SDK import path (`@lightprotocol/compressed-token`) that decompresses compressed tokens without wrapping SPL or Token-2022 balances. Uses `wrap=false` by default. Output can be SPL, Token-2022, or Light Token. Suited for DeFi integrations where downstream applications may not support Light Token. - -## Sponsored rent-exemption - -The rent model for Light Tokens. Compressed tokens require no lamports because data lives in state Merkle trees. Decompressed tokens pay standard Solana rent-exemption, which the owner reclaims when compressing the account. - -**Canonical term**: Sponsored rent-exemption by the light-token program - -**Avoid**: "rent-free token", "rent-free account" - -## Token pool - -An SPL token account that holds SPL tokens corresponding to compressed tokens in circulation. Tokens deposit during compression and withdraw during decompression. - -- **PDA seeds**: `["pool", mint_pubkey]` or `["pool", mint_pubkey, "restricted"]` for mints with restricted extensions -- **Max pools**: 5 per mint (indices 0-4) - -## TokenData - -The data structure for a compressed Light Token stored in a state tree leaf. Contains `mint`, `owner`, `amount`, `delegate`, and `state` fields. State values: Initialized (0), Frozen (1). Supports optional TLV extensions. - -## TokenMetadata extension - -A metadata extension for compressed mints storing name, symbol, uri, and additional key-value pairs. - -Discriminator: 19 - -Fields: `update_authority`, `mint`, `name`, `symbol`, `uri`, `additional_metadata` array - -## Transfer2 - -A batch instruction for compression operations supporting multiple modes: - -- **Compress** (0): Decompressed account to compressed account in state tree -- **Decompress** (1): State tree to decompressed account -- **CompressAndClose** (2): Compress and close atomically, rent to sponsor - -Discriminator: 101 - -## Unified path - -An SDK import path (`@lightprotocol/compressed-token/unified`) that aggregates balances from hot Light Token, cold Light Token, SPL, and Token-2022 into a single canonical ATA. Enforces `wrap=true`. Output is a single Light Token ATA derived from `CTOKEN_PROGRAM_ID`. Suited for closed-loop payment systems where you control the complete transaction flow. diff --git a/resources/cli-installation.mdx b/resources/cli-installation.mdx index 9ce7e4fa..b29b6b37 100644 --- a/resources/cli-installation.mdx +++ b/resources/cli-installation.mdx @@ -33,7 +33,7 @@ Ensure you have Node >= v20.9.0 installed on your machine. Windows users do not Run this single command to install the ZK Compression CLI. ```bash -npm i -g @lightprotocol/zk-compression-cli@beta +npm i -g @lightprotocol/zk-compression-cli ``` diff --git a/scripts/copy-light-token-snippets.sh b/scripts/copy-light-token-snippets.sh index 862a7069..c24d40c1 100755 --- a/scripts/copy-light-token-snippets.sh +++ b/scripts/copy-light-token-snippets.sh @@ -1,18 +1,19 @@ #!/bin/bash -# Script to copy TypeScript code from examples-light-token to docs/snippets/code-snippets/light-token +# Script to copy TypeScript code from streaming-tokens to docs/snippets/code-snippets/light-token # Wraps each file in typescript markdown code blocks -EXAMPLES="/home/tilo/Workspace/examples-light-token/cookbook" +EXAMPLES="/home/tilo/Workspace/streaming-tokens/typescript-client" SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/light-token" -# Recipes to process (matching directory names) +# Recipes to process (matching directory and file names) RECIPES=("create-mint" "create-ata" "mint-to" "transfer-interface" "load-ata" "wrap" "unwrap") # Function to wrap TypeScript code in markdown wrap_typescript() { local input_file="$1" local output_file="$2" + mkdir -p "$(dirname "$output_file")" echo '```typescript' > "$output_file" cat "$input_file" >> "$output_file" echo '```' >> "$output_file" @@ -40,6 +41,23 @@ for recipe in "${RECIPES[@]}"; do fi done +# Approve/revoke: non-standard filenames, action-only +echo "Processing: approve-revoke" + +approve_file="$EXAMPLES/actions/delegate-approve.ts" +if [ -f "$approve_file" ]; then + wrap_typescript "$approve_file" "$SNIPPETS_DIR/approve-revoke/approve-action.mdx" +else + echo " WARNING: Not found - $approve_file" +fi + +revoke_file="$EXAMPLES/actions/delegate-revoke.ts" +if [ -f "$revoke_file" ]; then + wrap_typescript "$revoke_file" "$SNIPPETS_DIR/approve-revoke/revoke-action.mdx" +else + echo " WARNING: Not found - $revoke_file" +fi + echo "" echo "Done! Created snippets in: $SNIPPETS_DIR" echo "" diff --git a/scripts/copy-rust-snippets.sh b/scripts/copy-rust-snippets.sh new file mode 100755 index 00000000..4cad3dc9 --- /dev/null +++ b/scripts/copy-rust-snippets.sh @@ -0,0 +1,194 @@ +#!/bin/bash + +# Script to copy Rust client code from examples-light-token-rust-client to docs snippets +# Creates action.mdx and instruction.mdx files wrapped in rust code blocks + +EXAMPLES_DIR="/home/tilo/Workspace/examples-light-token/rust-client" +SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/light-token" + +# Full recipes (action + instruction in same directory) +FULL_RECIPES=("create-mint" "mint-to" "transfer-interface" "transfer-checked") + +# Full recipes with name mapping (target-dir:source-filename) +FULL_RECIPES_MAPPED=( + "create-ata:create_associated_token_account" +) + +# Action-only recipes (action file only) +ACTION_ONLY=("wrap" "unwrap") + +# Instruction-only recipes with name mapping (target:source) +# Format: "output-dir:source-filename" +INSTRUCTION_ONLY=( + "burn:burn" + "close-token-account:close" + "create-token-account:create_token_account" +) + +# 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" +} + +# Convert kebab-case to snake_case +kebab_to_snake() { + echo "$1" | tr '-' '_' +} + +echo "=== Processing Rust client files ===" +echo "" + +# Process full recipes (action + instruction) +echo "--- Full recipes (action + instruction) ---" +for recipe in "${FULL_RECIPES[@]}"; do + rust_name=$(kebab_to_snake "$recipe") + echo "Processing: $recipe (source: $rust_name.rs)" + + output_dir="$SNIPPETS_DIR/$recipe/rust-client" + mkdir -p "$output_dir" + + # Action file + action_file="$EXAMPLES_DIR/actions/$rust_name.rs" + if [ -f "$action_file" ]; then + wrap_rust "$action_file" "$output_dir/action.mdx" + else + echo " WARNING: Not found - $action_file" + fi + + # Instruction file + instruction_file="$EXAMPLES_DIR/instructions/$rust_name.rs" + if [ -f "$instruction_file" ]; then + wrap_rust "$instruction_file" "$output_dir/instruction.mdx" + else + echo " WARNING: Not found - $instruction_file" + fi +done + +echo "" +echo "--- Full recipes (mapped names) ---" +for mapping in "${FULL_RECIPES_MAPPED[@]}"; do + output_name="${mapping%%:*}" + source_name="${mapping##*:}" + echo "Processing: $output_name (source: $source_name.rs)" + + output_dir="$SNIPPETS_DIR/$output_name/rust-client" + mkdir -p "$output_dir" + + action_file="$EXAMPLES_DIR/actions/$source_name.rs" + if [ -f "$action_file" ]; then + wrap_rust "$action_file" "$output_dir/action.mdx" + else + echo " WARNING: Not found - $action_file" + fi + + instruction_file="$EXAMPLES_DIR/instructions/$source_name.rs" + if [ -f "$instruction_file" ]; then + wrap_rust "$instruction_file" "$output_dir/instruction.mdx" + else + echo " WARNING: Not found - $instruction_file" + fi +done + +echo "" +echo "--- Action-only recipes ---" +for recipe in "${ACTION_ONLY[@]}"; do + rust_name=$(kebab_to_snake "$recipe") + echo "Processing: $recipe (source: $rust_name.rs)" + + output_dir="$SNIPPETS_DIR/$recipe/rust-client" + mkdir -p "$output_dir" + + # Action file only + action_file="$EXAMPLES_DIR/actions/$rust_name.rs" + if [ -f "$action_file" ]; then + wrap_rust "$action_file" "$output_dir/action.mdx" + else + echo " WARNING: Not found - $action_file" + fi +done + +echo "" +echo "--- Instruction-only recipes ---" +for mapping in "${INSTRUCTION_ONLY[@]}"; do + output_name="${mapping%%:*}" + source_name="${mapping##*:}" + echo "Processing: $output_name (source: $source_name.rs)" + + output_dir="$SNIPPETS_DIR/$output_name/rust-client" + mkdir -p "$output_dir" + + # Instruction file only + instruction_file="$EXAMPLES_DIR/instructions/$source_name.rs" + if [ -f "$instruction_file" ]; then + wrap_rust "$instruction_file" "$output_dir/instruction.mdx" + else + echo " WARNING: Not found - $instruction_file" + fi +done + +echo "" +echo "--- Freeze/Thaw recipes ---" +output_dir="$SNIPPETS_DIR/freeze-thaw/rust-client" +mkdir -p "$output_dir" + +freeze_file="$EXAMPLES_DIR/instructions/freeze.rs" +if [ -f "$freeze_file" ]; then + wrap_rust "$freeze_file" "$output_dir/freeze-instruction.mdx" +else + echo " WARNING: Not found - $freeze_file" +fi + +thaw_file="$EXAMPLES_DIR/instructions/thaw.rs" +if [ -f "$thaw_file" ]; then + wrap_rust "$thaw_file" "$output_dir/thaw-instruction.mdx" +else + echo " WARNING: Not found - $thaw_file" +fi + +echo "" +echo "--- Approve/Revoke recipes ---" +output_dir="$SNIPPETS_DIR/approve-revoke/rust-client" +mkdir -p "$output_dir" + +# Approve action +approve_action="$EXAMPLES_DIR/actions/approve.rs" +if [ -f "$approve_action" ]; then + wrap_rust "$approve_action" "$output_dir/approve-action.mdx" +else + echo " WARNING: Not found - $approve_action" +fi + +# Approve instruction +approve_instruction="$EXAMPLES_DIR/instructions/approve.rs" +if [ -f "$approve_instruction" ]; then + wrap_rust "$approve_instruction" "$output_dir/approve-instruction.mdx" +else + echo " WARNING: Not found - $approve_instruction" +fi + +# Revoke action +revoke_action="$EXAMPLES_DIR/actions/revoke.rs" +if [ -f "$revoke_action" ]; then + wrap_rust "$revoke_action" "$output_dir/revoke-action.mdx" +else + echo " WARNING: Not found - $revoke_action" +fi + +# Revoke instruction +revoke_instruction="$EXAMPLES_DIR/instructions/revoke.rs" +if [ -f "$revoke_instruction" ]; then + wrap_rust "$revoke_instruction" "$output_dir/revoke-instruction.mdx" +else + echo " WARNING: Not found - $revoke_instruction" +fi + +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/accounts-list/light-token-create-accounts-list-client.mdx b/snippets/accounts-list/light-token-create-accounts-list-client.mdx index ee97c25f..c85d9193 100644 --- a/snippets/accounts-list/light-token-create-accounts-list-client.mdx +++ b/snippets/accounts-list/light-token-create-accounts-list-client.mdx @@ -89,7 +89,7 @@ mutable - - light token program PDA that fronts rent exemption at creation. + - light token program PDA that pays rent exemption at creation.
- Claims rent when account compresses. diff --git a/snippets/accounts-list/light-token-create-accounts-list.mdx b/snippets/accounts-list/light-token-create-accounts-list.mdx index 024f2a0c..611fcf8b 100644 --- a/snippets/accounts-list/light-token-create-accounts-list.mdx +++ b/snippets/accounts-list/light-token-create-accounts-list.mdx @@ -16,7 +16,7 @@ signer, mutable - Pays initial rent per epoch, transaction fee and compression incentive.
- - Does NOT pay rent exemption (fronted by `rent_sponsor`). + - Does NOT pay rent exemption (paid by the light token program, `rent_sponsor`). diff --git a/snippets/accounts-list/light-token-create-ata-accounts-list.mdx b/snippets/accounts-list/light-token-create-ata-accounts-list.mdx index 46c8e291..d29eea47 100644 --- a/snippets/accounts-list/light-token-create-ata-accounts-list.mdx +++ b/snippets/accounts-list/light-token-create-ata-accounts-list.mdx @@ -32,7 +32,7 @@ signer, mutable - Pays initial rent per epoch, transaction fee and compression incentive.
- - Does NOT pay rent exemption (fronted by `rent_sponsor`). + - Does NOT pay rent exemption (paid by the light token program, `rent_sponsor`). diff --git a/snippets/code-samples/code-compare-snippets.jsx b/snippets/code-samples/code-compare-snippets.jsx index 9fc0e28e..07c4e082 100644 --- a/snippets/code-samples/code-compare-snippets.jsx +++ b/snippets/code-samples/code-compare-snippets.jsx @@ -3,7 +3,6 @@ // === CREATE MINT === export const splCreateMintCode = [ - "// SPL createMint", 'import { createMint } from "@solana/spl-token";', "", "const mint = await createMint(", @@ -16,7 +15,6 @@ export const splCreateMintCode = [ ].join("\n"); export const lightCreateMintCode = [ - "// light-token createMint", 'import { createMintInterface } from "@lightprotocol/compressed-token";', "", "const { mint } = await createMintInterface(", @@ -31,7 +29,6 @@ export const lightCreateMintCode = [ // === MINT TO === export const splMintToCode = [ - "// SPL mintTo", 'import { mintTo } from "@solana/spl-token";', "", "const tx = await mintTo(", @@ -45,7 +42,6 @@ export const splMintToCode = [ ].join("\n"); export const lightMintToCode = [ - "// light-token mintTo", 'import { mintToInterface } from "@lightprotocol/compressed-token";', "", "const tx = await mintToInterface(", @@ -60,7 +56,6 @@ export const lightMintToCode = [ // === CREATE ATA === export const splCreateAtaCode = [ - "// SPL create ATA", 'import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";', "", "const ata = await getOrCreateAssociatedTokenAccount(", @@ -72,7 +67,6 @@ export const splCreateAtaCode = [ ].join("\n"); export const lightCreateAtaCode = [ - "// light-token create ATA", 'import { createAtaInterface } from "@lightprotocol/compressed-token";', "", "const ata = await createAtaInterface(", @@ -85,7 +79,6 @@ export const lightCreateAtaCode = [ // === TRANSFER === export const splTransferCode = [ - "// SPL transfer", 'import { transfer } from "@solana/spl-token";', "", "const tx = await transfer(", @@ -99,7 +92,6 @@ export const splTransferCode = [ ].join("\n"); export const lightTransferCode = [ - "// light-token transfer", 'import { transferInterface } from "@lightprotocol/compressed-token";', "", "const tx = await transferInterface(", @@ -113,9 +105,165 @@ export const lightTransferCode = [ ");", ].join("\n"); +// === TRANSFER (RUST) === +export const splTransferRustCode = [ + "use spl_token::instruction::transfer;", + "", + "let ix = transfer(", + " &spl_token::id(),", + " &source,", + " &destination,", + " &authority,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightTransferRustCode = [ + "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 = [ + "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 = [ + "use light_token_sdk::token::CreateAssociatedTokenAccount;", + "", + "let ix = CreateAssociatedTokenAccount::new(", + " payer.pubkey(),", + " owner.pubkey(),", + " mint,", + ")", + ".instruction()?;", +].join("\n"); + +// === CREATE MINT (RUST) === +export const splCreateMintRustCode = [ + "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 = [ + "use light_token_sdk::token::CreateMint;", + "", + "let ix = CreateMint::new(", + " params,", + " mint_seed.pubkey(),", + " payer.pubkey(),", + " address_tree.tree,", + " output_queue,", + ")", + ".instruction()?;", +].join("\n"); + +// === MINT TO (RUST) === +export const splMintToRustCode = [ + "use spl_token::instruction::mint_to;", + "", + "let ix = mint_to(", + " &spl_token::id(),", + " &mint,", + " &destination,", + " &mint_authority,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightMintToRustCode = [ + "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 = [ + "use spl_token::instruction::initialize_account;", + "", + "let ix = initialize_account(", + " &spl_token::id(),", + " &account,", + " &mint,", + " &owner,", + ")?;", +].join("\n"); + +export const lightCreateTokenAccountRustCode = [ + "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 = [ + "use spl_token::instruction::close_account;", + "", + "let ix = close_account(", + " &spl_token::id(),", + " &account,", + " &destination,", + " &owner,", + " &[],", + ")?;", +].join("\n"); + +export const lightCloseAccountRustCode = [ + "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", "const ix = createAssociatedTokenAccountInstruction(", " payer,", " ata,", @@ -125,7 +273,6 @@ export const blogSplCreateAtaCode = [ ].join("\n"); export const blogLightCreateAtaCode = [ - "// Create light-token account", "const ix = CreateAssociatedTokenAccount.new(", " payer,", " account,", @@ -133,3 +280,434 @@ export const blogLightCreateAtaCode = [ " mint", ");", ].join("\n"); + +// === BURN (RUST) === +export const splBurnRustCode = [ + "use spl_token::instruction::burn;", + "", + "let ix = burn(", + " &spl_token::id(),", + " &source,", + " &mint,", + " &authority,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightBurnRustCode = [ + "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 = [ + "use spl_token::instruction::freeze_account;", + "", + "let ix = freeze_account(", + " &spl_token::id(),", + " &account,", + " &mint,", + " &freeze_authority,", + " &[],", + ")?;", +].join("\n"); + +export const lightFreezeRustCode = [ + "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 = [ + "use spl_token::instruction::thaw_account;", + "", + "let ix = thaw_account(", + " &spl_token::id(),", + " &account,", + " &mint,", + " &freeze_authority,", + " &[],", + ")?;", +].join("\n"); + +export const lightThawRustCode = [ + "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 = [ + "use spl_token::instruction::approve;", + "", + "let ix = approve(", + " &spl_token::id(),", + " &source,", + " &delegate,", + " &owner,", + " &[],", + " amount,", + ")?;", +].join("\n"); + +export const lightApproveRustCode = [ + "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 = [ + "use spl_token::instruction::revoke;", + "", + "let ix = revoke(", + " &spl_token::id(),", + " &source,", + " &owner,", + " &[],", + ")?;", +].join("\n"); + +export const lightRevokeRustCode = [ + "use light_token_sdk::token::Revoke;", + "", + "let ix = Revoke {", + " token_account: ata,", + " owner: payer.pubkey(),", + "}", + ".instruction()?;", +].join("\n"); + +// === APPROVE (TYPESCRIPT) === +export const splApproveCode = [ + 'import { approve } from "@solana/spl-token";', + "", + "const tx = await approve(", + " connection,", + " payer,", + " source,", + " delegate,", + " owner,", + " amount", + ");", +].join("\n"); + +export const lightApproveCode = [ + 'import { approve } from "@lightprotocol/compressed-token";', + "", + "const tx = await approve(", + " rpc,", + " payer,", + " mint,", + " amount,", + " owner,", + " delegate", + ");", +].join("\n"); + +// === REVOKE (TYPESCRIPT) === +export const splRevokeCode = [ + 'import { revoke } from "@solana/spl-token";', + "", + "const tx = await revoke(", + " connection,", + " payer,", + " source,", + " owner", + ");", +].join("\n"); + +export const lightRevokeCode = [ + 'import { revoke } from "@lightprotocol/compressed-token";', + "", + "const tx = await revoke(", + " rpc,", + " payer,", + " delegatedAccounts,", + " owner", + ");", +].join("\n"); + +// === CREATE MINT MACRO (ANCHOR) === +export const splCreateMintMacroCode = [ + "#[account(", + " init,", + " payer = fee_payer,", + " mint::decimals = 9,", + " mint::authority = fee_payer,", + ")]", + "pub mint: InterfaceAccount<'info, Mint>,", +].join("\n"); + +export const lightCreateMintMacroCode = [ + "#[light_account(init,", + " mint::signer = mint_signer,", + " mint::authority = fee_payer,", + " mint::decimals = 9,", + " mint::seeds = &[MINT_SIGNER_SEED, authority.key().as_ref()],", + " mint::bump = params.mint_signer_bump", + ")]", + "pub mint: UncheckedAccount<'info>,", +].join("\n"); + +// === CREATE MINT WITH METADATA MACRO (ANCHOR) === +export const splCreateMintMetadataMacroCode = [ + "#[account(", + " init,", + " payer = fee_payer,", + " mint::decimals = 9,", + " mint::authority = fee_payer,", + " extensions::metadata_pointer::authority = fee_payer,", + " extensions::metadata_pointer::metadata_address = mint_account,", + ")]", + "pub mint_account: InterfaceAccount<'info, Mint>,", + "", + "// Metadata requires a separate CPI:", + "token_metadata_initialize(", + " cpi_ctx,", + " params.name,", + " params.symbol,", + " params.uri,", + ")?;", +].join("\n"); + +export const lightCreateMintMetadataMacroCode = [ + "#[light_account(", + " init,", + " mint,", + " mint_signer = mint_signer,", + " authority = fee_payer,", + " decimals = 9,", + " mint_seeds = &[MINT_SIGNER_SEED, authority.key().as_ref(), &[params.mint_signer_bump]],", + " name = params.name.clone(),", + " symbol = params.symbol.clone(),", + " uri = params.uri.clone(),", + " update_authority = authority,", + " additional_metadata = params.additional_metadata.clone()", + ")]", + "pub mint: UncheckedAccount<'info>,", +].join("\n"); + +// === CREATE ATA MACRO (ANCHOR) === +export const splCreateAtaMacroCode = [ + "#[account(", + " init_if_needed,", + " payer = fee_payer,", + " associated_token::mint = mint,", + " associated_token::authority = owner,", + ")]", + "pub ata: Account<'info, TokenAccount>,", +].join("\n"); + +export const lightCreateAtaMacroCode = [ + "#[light_account(init,", + " associated_token::authority = ata_owner,", + " associated_token::mint = ata_mint,", + " associated_token::bump = params.ata_bump", + ")]", + "pub ata: UncheckedAccount<'info>,", +].join("\n"); + +// === CREATE TOKEN ACCOUNT MACRO (ANCHOR) === +export const splCreateTokenAccountMacroCode = [ + "#[account(", + " init,", + " payer = fee_payer,", + " token::mint = mint,", + " token::authority = authority,", + ")]", + "pub vault: Account<'info, TokenAccount>,", +].join("\n"); + +export const lightCreateTokenAccountMacroCode = [ + "#[account(", + " mut,", + " seeds = [VAULT_SEED, mint.key().as_ref()],", + " bump,", + ")]", + "#[light_account(init,", + " token::authority = [VAULT_SEED, self.mint.key()],", + " token::mint = mint,", + " token::owner = vault_authority,", + " token::bump = params.vault_bump", + ")]", + "pub vault: UncheckedAccount<'info>,", +].join("\n"); + +// === CREATE ATA CPI (RUST) === +export const splCreateAtaCpiCode = [ + "use spl_associated_token_account::instruction::create_associated_token_account;", + "", + "let ix = create_associated_token_account(", + " &payer.pubkey(),", + " &owner.pubkey(),", + " &mint,", + " &spl_token::id(),", + ");", + "", + "invoke(&ix, &[payer, owner, mint])?;", +].join("\n"); + +export const lightCreateAtaCpiCode = [ + "use light_token::instruction::CreateAssociatedAccountCpi;", + "", + "CreateAssociatedAccountCpi {", + " payer: payer.clone(),", + " owner: owner.clone(),", + " mint: mint.clone(),", + " ata: associated_token_account.clone(),", + " bump,", + "}", + ".rent_free(", + " compressible_config.clone(),", + " rent_sponsor.clone(),", + " system_program.clone(),", + ")", + ".invoke()?", +].join("\n"); + +// === CREATE TOKEN ACCOUNT CPI (RUST) === +export const splCreateTokenAccountCpiCode = [ + "use spl_token::instruction::initialize_account;", + "", + "let ix = initialize_account(", + " &spl_token::id(),", + " &account,", + " &mint,", + " &owner,", + ")?;", + "", + "invoke(&ix, &[account, mint, owner])?;", +].join("\n"); + +export const lightCreateTokenAccountCpiCode = [ + "use light_token::instruction::CreateTokenAccountCpi;", + "", + "CreateTokenAccountCpi {", + " payer: payer.clone(),", + " account: account.clone(),", + " mint: mint.clone(),", + " owner,", + "}", + ".rent_free(", + " compressible_config.clone(),", + " rent_sponsor.clone(),", + " system_program.clone(),", + " token_program.key,", + ")", + ".invoke()?", +].join("\n"); + +// === CREATE MINT CPI (RUST) === +export const splCreateMintCpiCode = [ + "use spl_token::instruction::initialize_mint;", + "", + "let ix = initialize_mint(", + " &spl_token::id(),", + " &mint.pubkey(),", + " &mint_authority,", + " Some(&freeze_authority),", + " decimals,", + ")?;", + "", + "invoke(&ix, &[mint, rent_sysvar])?;", +].join("\n"); + +export const lightCreateMintCpiCode = [ + "use light_token::instruction::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,", + " params,", + ")", + ".invoke()?", +].join("\n"); + +// === CREATE MINT WITH METADATA CPI (RUST) === +export const splCreateMintMetadataCpiCode = [ + "use spl_token_2022::instruction::initialize_mint;", + "use spl_token_metadata_interface::instruction::initialize as init_metadata;", + "", + "let ix_mint = initialize_mint(", + " &spl_token_2022::id(),", + " &mint.pubkey(),", + " &mint_authority,", + " Some(&freeze_authority),", + " decimals,", + ")?;", + "invoke(&ix_mint, &[mint.clone(), rent_sysvar.clone()])?;", + "", + "let ix_meta = init_metadata(", + " &spl_token_2022::id(),", + " &mint.pubkey(),", + " &update_authority,", + " &mint.pubkey(),", + " &mint_authority,", + " name, symbol, uri,", + ")?;", + "invoke(&ix_meta, &[mint, update_auth])?;", +].join("\n"); + +export const lightCreateMintMetadataCpiCode = [ + "use light_token::instruction::CreateMintCpi;", + "", + "let extensions = Some(vec![", + " ExtensionInstructionData::TokenMetadata(", + " TokenMetadataInstructionData {", + " update_authority: Some(authority.key.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: None,", + " },", + " ),", + "]);", + "", + "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, // includes extensions", + ")", + ".invoke()?", +].join("\n"); diff --git a/snippets/code-snippets/light-token/approve-revoke/approve-action.mdx b/snippets/code-snippets/light-token/approve-revoke/approve-action.mdx new file mode 100644 index 00000000..e358f1f3 --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/approve-action.mdx @@ -0,0 +1,41 @@ +```typescript +import "dotenv/config"; +import { Keypair } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + mintToCompressed, + approve, +} from "@lightprotocol/compressed-token"; +import { homedir } from "os"; +import { readFileSync } from "fs"; + +// devnet: +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); +// localnet: +const rpc = createRpc(); + +const payer = Keypair.fromSecretKey( + new Uint8Array( + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")), + ), +); + +(async function () { + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await mintToCompressed(rpc, payer, mint, payer, [{ recipient: payer.publicKey, amount: 1000n }]); + + const delegate = Keypair.generate(); + const tx = await approve( + rpc, + payer, + mint, + 500, + payer, + delegate.publicKey, + ); + + console.log("Tx:", tx); +})(); +``` diff --git a/snippets/code-snippets/light-token/approve-revoke/revoke-action.mdx b/snippets/code-snippets/light-token/approve-revoke/revoke-action.mdx new file mode 100644 index 00000000..cd3d7cc2 --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/revoke-action.mdx @@ -0,0 +1,41 @@ +```typescript +import "dotenv/config"; +import { Keypair } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + mintToCompressed, + approve, + revoke, +} from "@lightprotocol/compressed-token"; +import { homedir } from "os"; +import { readFileSync } from "fs"; + +// devnet: +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); +// localnet: +const rpc = createRpc(); + +const payer = Keypair.fromSecretKey( + new Uint8Array( + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")), + ), +); + +(async function () { + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await mintToCompressed(rpc, payer, mint, payer, [{ recipient: payer.publicKey, amount: 1000n }]); + + const delegate = Keypair.generate(); + await approve(rpc, payer, mint, 500, payer, delegate.publicKey); + + const delegatedAccounts = await rpc.getCompressedTokenAccountsByDelegate( + delegate.publicKey, + { mint }, + ); + const tx = await revoke(rpc, payer, delegatedAccounts.items, payer); + + console.log("Tx:", tx); +})(); +``` diff --git a/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-action.mdx b/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-action.mdx new file mode 100644 index 00000000..26156d6c --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-action.mdx @@ -0,0 +1,35 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_client::actions::Approve; +use rust_client::{setup, SetupContext}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and associated token account with tokens + let SetupContext { + mut rpc, + payer, + associated_token_account, + .. + } = setup().await; + + let delegate = Keypair::new(); + + let sig = Approve { + token_account: associated_token_account, + delegate: delegate.pubkey(), + amount: 500_000, + owner: Some(payer.pubkey()), + } + .execute(&mut rpc, &payer) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Delegate: {:?} Tx: {sig}", token.delegate); + + Ok(()) +} +``` 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 00000000..e157cb8e --- /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/approve-instruction.mdx b/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-instruction.mdx new file mode 100644 index 00000000..3a60f85c --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/rust-client/approve-instruction.mdx @@ -0,0 +1,39 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::Approve; +use rust_client::{setup, SetupContext}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and associated token account with tokens + let SetupContext { + mut rpc, + payer, + associated_token_account, + .. + } = setup().await; + + let delegate = Keypair::new(); + let delegate_amount = 500_000u64; + + let approve_instruction = Approve { + token_account: associated_token_account, + delegate: delegate.pubkey(), + owner: payer.pubkey(), + amount: delegate_amount, + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[approve_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Delegate: {:?} Tx: {sig}", token.delegate); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-action.mdx b/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-action.mdx new file mode 100644 index 00000000..f1fe2d58 --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-action.mdx @@ -0,0 +1,31 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_client::actions::Revoke; +use rust_client::{setup, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and associated token account with approved delegate + let SetupContext { + mut rpc, + payer, + associated_token_account, + .. + } = setup().await; + + let sig = Revoke { + token_account: associated_token_account, + owner: Some(payer.pubkey()), + } + .execute(&mut rpc, &payer) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Delegate: {:?} Tx: {sig}", token.delegate); + + Ok(()) +} +``` 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 00000000..5a15cc0f --- /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/approve-revoke/rust-client/revoke-instruction.mdx b/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-instruction.mdx new file mode 100644 index 00000000..d18bc441 --- /dev/null +++ b/snippets/code-snippets/light-token/approve-revoke/rust-client/revoke-instruction.mdx @@ -0,0 +1,34 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::Revoke; +use rust_client::{setup, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint, associated token account with tokens, and approves delegate + let SetupContext { + mut rpc, + payer, + associated_token_account, + .. + } = setup().await; + + let revoke_instruction = Revoke { + token_account: associated_token_account, + owner: payer.pubkey(), + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[revoke_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Delegate: {:?} Tx: {sig}", token.delegate); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/burn-checked/rust-client/instruction.mdx b/snippets/code-snippets/light-token/burn-checked/rust-client/instruction.mdx new file mode 100644 index 00000000..9801f7e5 --- /dev/null +++ b/snippets/code-snippets/light-token/burn-checked/rust-client/instruction.mdx @@ -0,0 +1,44 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::BurnChecked; +use rust_client::{setup, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and ATA with tokens + let SetupContext { + mut rpc, + payer, + mint, + ata, + decimals, + .. + } = setup().await; + + let burn_amount = 400_000u64; + + // BurnChecked validates that decimals match the mint + let burn_ix = BurnChecked { + source: ata, + mint, + amount: burn_amount, + decimals, + authority: payer.pubkey(), + max_top_up: None, + fee_payer: None, + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[burn_ix], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc.get_account(ata).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/burn/rust-client/instruction.mdx b/snippets/code-snippets/light-token/burn/rust-client/instruction.mdx new file mode 100644 index 00000000..a43ab642 --- /dev/null +++ b/snippets/code-snippets/light-token/burn/rust-client/instruction.mdx @@ -0,0 +1,41 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::Burn; +use rust_client::{setup, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and associated token account with tokens + let SetupContext { + mut rpc, + payer, + mint, + associated_token_account, + .. + } = setup().await; + + let burn_amount = 400_000u64; + + let burn_instruction = Burn { + source: associated_token_account, + mint, + amount: burn_amount, + authority: payer.pubkey(), + max_top_up: None, + fee_payer: None, + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[burn_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/close-token-account/rust-client/instruction.mdx b/snippets/code-snippets/light-token/close-token-account/rust-client/instruction.mdx new file mode 100644 index 00000000..8a0f3e36 --- /dev/null +++ b/snippets/code-snippets/light-token/close-token-account/rust-client/instruction.mdx @@ -0,0 +1,28 @@ +```rust +use light_client::rpc::Rpc; +use light_token::instruction::{CloseAccount, LIGHT_TOKEN_PROGRAM_ID}; +use rust_client::{setup_empty_associated_token_account, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and empty associated token account (must be empty to close). + let SetupContext { + mut rpc, + payer, + associated_token_account, + .. + } = setup_empty_associated_token_account().await; + let close_instruction = CloseAccount::new(LIGHT_TOKEN_PROGRAM_ID, associated_token_account, payer.pubkey(), payer.pubkey()) + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[close_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let account = rpc.get_account(associated_token_account).await?; + println!("Closed: {} Tx: {sig}", account.is_none()); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/create-ata/anchor-macro/full-example.mdx b/snippets/code-snippets/light-token/create-ata/anchor-macro/full-example.mdx new file mode 100644 index 00000000..b0770779 --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/anchor-macro/full-example.mdx @@ -0,0 +1,237 @@ + +```rust lib.rs +#![allow(unexpected_cfgs, deprecated)] + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk::derive_light_cpi_signer; +use light_sdk_macros::{light_program, LightAccounts}; +use light_sdk_types::{CpiSigner, LIGHT_TOKEN_PROGRAM_ID}; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR}; + +declare_id!("CLsn9MTFv97oMTsujRoQAw1u2rSm2HnKtGuWUbbc8Jfn"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("CLsn9MTFv97oMTsujRoQAw1u2rSm2HnKtGuWUbbc8Jfn"); + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateAtaParams { + pub create_accounts_proof: CreateAccountsProof, + pub ata_bump: u8, +} + +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateAtaParams)] +pub struct CreateAta<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Validated by light-token CPI + // You can use Light, SPL, or Token-2022 mints to create a Light associated token account. + pub ata_mint: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + pub ata_owner: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + #[account(mut)] + #[light_account(init, associated_token::authority = ata_owner, associated_token::mint = ata_mint, associated_token::bump = params.ata_bump)] + pub ata: UncheckedAccount<'info>, + + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub light_token_compressible_config: AccountInfo<'info>, + + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: Light token program for CPI + #[account(address = LIGHT_TOKEN_PROGRAM_ID.into())] + pub light_token_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +#[light_program] +#[program] +pub mod light_token_macro_create_ata { + use super::*; + + #[allow(unused_variables)] + pub fn create_ata<'info>( + ctx: Context<'_, '_, '_, 'info, CreateAta<'info>>, + params: CreateAtaParams, + ) -> Result<()> { + Ok(()) + } +} +``` + +```rust test.rs +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_client::interface::{get_create_accounts_proof, InitializeRentFreeConfig}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Creates a compressed mint. Returns (mint_pda, mint_seed_keypair). +async fn setup_create_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, Keypair) { + use light_token::instruction::{CreateMint, CreateMintParams}; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + (mint, mint_seed) +} + +/// Creates a Light Protocol ATA via the macro. +#[tokio::test] +async fn test_create_ata() { + use light_token_macro_create_ata::CreateAtaParams; + + let program_id = light_token_macro_create_ata::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("light_token_macro_create_ata", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (init_config_ix, _config_pda) = InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + RENT_SPONSOR, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + let (mint, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), // mint_authority + 9, // decimals + ) + .await; + + let ata_owner = payer.pubkey(); + let (ata, ata_bump) = light_token::instruction::derive_token_ata(&ata_owner, &mint); + + // No PDA accounts needed for ATA-only instruction + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + let accounts = light_token_macro_create_ata::accounts::CreateAta { + fee_payer: payer.pubkey(), + ata_mint: mint, + ata_owner, + ata, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + light_token_rent_sponsor: RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = light_token_macro_create_ata::instruction::CreateAta { + params: CreateAtaParams { + create_accounts_proof: proof_result.create_accounts_proof, + ata_bump, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .expect("CreateAta instruction should succeed"); + + let ata_account = rpc + .get_account(ata) + .await + .unwrap() + .expect("ATA should exist on-chain"); + + use light_token_interface::state::Token; + let token: Token = borsh::BorshDeserialize::deserialize(&mut &ata_account.data[..]) + .expect("Failed to deserialize Token"); + + assert_eq!(token.owner, ata_owner.to_bytes(), "ATA owner should match"); + assert_eq!(token.mint, mint.to_bytes(), "ATA mint should match"); + assert_eq!(token.amount, 0, "ATA amount should be 0 initially"); +} +``` + diff --git a/snippets/code-snippets/light-token/create-ata/instruction.mdx b/snippets/code-snippets/light-token/create-ata/instruction.mdx index c1219adc..ec0a5285 100644 --- a/snippets/code-snippets/light-token/create-ata/instruction.mdx +++ b/snippets/code-snippets/light-token/create-ata/instruction.mdx @@ -15,15 +15,15 @@ import { homedir } from "os"; import { readFileSync } from "fs"; // devnet: -const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; -const rpc = createRpc(RPC_URL); +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const rpc = createRpc(); +const rpc = createRpc(); const payer = Keypair.fromSecretKey( new Uint8Array( - JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) - ) + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")), + ), ); (async function () { @@ -32,7 +32,7 @@ const payer = Keypair.fromSecretKey( const owner = Keypair.generate(); const associatedToken = getAssociatedTokenAddressInterface( mint, - owner.publicKey + owner.publicKey, ); const ix = createAssociatedTokenAccountInterfaceInstruction( @@ -40,7 +40,7 @@ const payer = Keypair.fromSecretKey( associatedToken, owner.publicKey, mint, - CTOKEN_PROGRAM_ID + CTOKEN_PROGRAM_ID, ); const tx = new Transaction().add(ix); diff --git a/snippets/code-snippets/light-token/create-ata/rust-client/action.mdx b/snippets/code-snippets/light-token/create-ata/rust-client/action.mdx new file mode 100644 index 00000000..f78de568 --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/rust-client/action.mdx @@ -0,0 +1,33 @@ +```rust +use light_token_client::actions::{CreateAta, CreateMint}; +use rust_client::setup_rpc_and_payer; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let (mut rpc, payer) = setup_rpc_and_payer().await; + + // Create mint + let (_signature, mint) = CreateMint { + decimals: 9, + freeze_authority: None, + token_metadata: None, + seed: None, + } + .execute(&mut rpc, &payer, &payer) + .await?; + + // Create associated token account + let (_signature, associated_token_account) = CreateAta { + mint, + owner: payer.pubkey(), + idempotent: true, + } + .execute(&mut rpc, &payer) + .await?; + + println!("Associated token account: {associated_token_account}"); + + 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 00000000..5f4fa5fd --- /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-ata/rust-client/instruction.mdx b/snippets/code-snippets/light-token/create-ata/rust-client/instruction.mdx new file mode 100644 index 00000000..0b8a1fcd --- /dev/null +++ b/snippets/code-snippets/light-token/create-ata/rust-client/instruction.mdx @@ -0,0 +1,31 @@ +```rust +use light_client::rpc::Rpc; +use light_token::instruction::{get_associated_token_address, CreateAssociatedTokenAccount}; +use rust_client::{setup_spl_mint_context, SplMintContext}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // You can use Light, SPL, or Token-2022 mints to create a Light associated token account. + let SplMintContext { + mut rpc, + payer, + mint, + } = setup_spl_mint_context().await; + + let owner = Keypair::new(); + + let create_associated_token_account_instruction = + CreateAssociatedTokenAccount::new(payer.pubkey(), owner.pubkey(), mint).instruction()?; + + let sig = rpc + .create_and_send_transaction(&[create_associated_token_account_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let associated_token_account = get_associated_token_address(&owner.pubkey(), &mint); + let data = rpc.get_account(associated_token_account).await?; + println!("Associated token account: {associated_token_account} exists: {} Tx: {sig}", data.is_some()); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/create-mint/anchor-macro/full-example.mdx b/snippets/code-snippets/light-token/create-mint/anchor-macro/full-example.mdx new file mode 100644 index 00000000..812571c7 --- /dev/null +++ b/snippets/code-snippets/light-token/create-mint/anchor-macro/full-example.mdx @@ -0,0 +1,371 @@ + +```rust lib.rs +#![allow(unexpected_cfgs, deprecated)] + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk::derive_light_cpi_signer; +use light_sdk_macros::{light_program, LightAccounts}; +use light_sdk_types::CpiSigner; +use light_token::AdditionalMetadata; + +declare_id!("HVmVqSJyMejBeUigePMSfX4aENJzCGHNxAJuT2PDMPRx"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("HVmVqSJyMejBeUigePMSfX4aENJzCGHNxAJuT2PDMPRx"); + +pub const MINT_SIGNER_SEED: &[u8] = b"mint_signer"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateMintParams { + // We must create a compressed address at creation to ensure the mint does not exist yet. + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump: u8, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateMintWithMetadataParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump: u8, + pub name: Vec, + pub symbol: Vec, + pub uri: Vec, + pub additional_metadata: Option>, +} + +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateMintParams)] +pub struct CreateMint<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: Validated by light-token CPI + #[account( + seeds = [MINT_SIGNER_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, + + /// CHECK: Validated by light-token CPI + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer, + mint::authority = fee_payer, + mint::decimals = 9, + mint::seeds = &[MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump + )] + pub mint: UncheckedAccount<'info>, + + /// CHECK: Validated by light-token CPI + pub compression_config: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + pub light_token_compressible_config: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + pub light_token_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateMintWithMetadataParams)] +pub struct CreateMintWithMetadata<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: Validated by light-token CPI + #[account( + seeds = [MINT_SIGNER_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, + + /// CHECK: Validated by light-token CPI + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer, + mint::authority = fee_payer, + mint::decimals = 9, + mint::seeds = &[MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump, + mint::name = params.name.clone(), + mint::symbol = params.symbol.clone(), + mint::uri = params.uri.clone(), + mint::update_authority = authority, + mint::additional_metadata = params.additional_metadata.clone() + )] + pub mint: UncheckedAccount<'info>, + + /// CHECK: Validated by light-token CPI + pub compression_config: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + pub light_token_compressible_config: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + pub light_token_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +#[light_program] +#[program] +pub mod light_token_macro_create_mint { + use super::*; + + #[allow(unused_variables)] + pub fn create_mint<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMint<'info>>, + params: CreateMintParams, + ) -> Result<()> { + Ok(()) + } + + #[allow(unused_variables)] + pub fn create_mint_with_metadata<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMintWithMetadata<'info>>, + params: CreateMintWithMetadataParams, + ) -> Result<()> { + Ok(()) + } +} +``` + +```rust test.rs +use anchor_lang::{prelude::borsh, InstructionData, ToAccountMetas}; +use light_client::interface::{ + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, +}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{find_mint_address, COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Creates a mint via #[light_account(init, mint, ...)]. +#[tokio::test] +async fn test_create_mint() { + use light_token_macro_create_mint::{CreateMintParams, MINT_SIGNER_SEED}; + + let program_id = light_token_macro_create_mint::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("light_token_macro_create_mint", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (init_config_ix, config_pda) = InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + RENT_SPONSOR, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + let authority = Keypair::new(); + + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + let (mint_pda, _) = find_mint_address(&mint_signer_pda); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::mint(mint_signer_pda)], + ) + .await + .unwrap(); + + let accounts = light_token_macro_create_mint::accounts::CreateMint { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer: mint_signer_pda, + mint: mint_pda, + compression_config: config_pda, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + rent_sponsor: RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = light_token_macro_create_mint::instruction::CreateMint { + params: CreateMintParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateMint should succeed"); + + let mint_account = rpc + .get_account(mint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain"); + + use light_token_interface::state::Mint; + let mint: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_account.data[..]) + .expect("Failed to deserialize Mint"); + + assert_eq!(mint.base.decimals, 9, "Mint should have 9 decimals"); + + assert_eq!( + mint.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be fee_payer" + ); +} + +/// Creates a mint with metadata via #[light_account(init, mint, ...)]. +#[tokio::test] +async fn test_create_mint_with_metadata() { + use light_token_macro_create_mint::{CreateMintWithMetadataParams, MINT_SIGNER_SEED}; + + let program_id = light_token_macro_create_mint::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("light_token_macro_create_mint", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (init_config_ix, config_pda) = InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + RENT_SPONSOR, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + let authority = Keypair::new(); + + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + let (mint_pda, _) = find_mint_address(&mint_signer_pda); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::mint(mint_signer_pda)], + ) + .await + .unwrap(); + + let accounts = light_token_macro_create_mint::accounts::CreateMintWithMetadata { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer: mint_signer_pda, + mint: mint_pda, + compression_config: config_pda, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + rent_sponsor: RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = + light_token_macro_create_mint::instruction::CreateMintWithMetadata { + params: CreateMintWithMetadataParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + name: b"Example Token".to_vec(), + symbol: b"EXT".to_vec(), + uri: b"https://example.com/metadata.json".to_vec(), + additional_metadata: None, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateMintWithMetadata should succeed"); + + let mint_account = rpc + .get_account(mint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain"); + + use light_token_interface::state::Mint; + let mint: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_account.data[..]) + .expect("Failed to deserialize Mint"); + + assert_eq!(mint.base.decimals, 9, "Mint should have 9 decimals"); + + assert_eq!( + mint.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be fee_payer" + ); +} +``` + diff --git a/snippets/code-snippets/light-token/create-mint/instruction.mdx b/snippets/code-snippets/light-token/create-mint/instruction.mdx index 753db086..9789b974 100644 --- a/snippets/code-snippets/light-token/create-mint/instruction.mdx +++ b/snippets/code-snippets/light-token/create-mint/instruction.mdx @@ -25,20 +25,20 @@ const COMPRESSED_MINT_SEED = Buffer.from("compressed_mint"); function findMintAddress(mintSigner: PublicKey): [PublicKey, number] { return PublicKey.findProgramAddressSync( [COMPRESSED_MINT_SEED, mintSigner.toBuffer()], - CTOKEN_PROGRAM_ID + CTOKEN_PROGRAM_ID, ); } // devnet: -const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; -const rpc = createRpc(RPC_URL); +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const rpc = createRpc(); +const rpc = createRpc(); const payer = Keypair.fromSecretKey( new Uint8Array( - JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) - ) + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")), + ), ); (async function () { @@ -49,7 +49,7 @@ const payer = Keypair.fromSecretKey( const validityProof = await rpc.getValidityProofV2( [], - [{ address: mintPda.toBytes(), treeInfo: addressTreeInfo }] + [{ address: mintPda.toBytes(), treeInfo: addressTreeInfo }], ); const ix = createMintInstruction( @@ -64,13 +64,13 @@ const payer = Keypair.fromSecretKey( createTokenMetadata( "Example Token", "EXT", - "https://example.com/metadata.json" - ) + "https://example.com/metadata.json", + ), ); const tx = new Transaction().add( ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), - ix + ix, ); const signature = await sendAndConfirmTransaction(rpc, tx, [ payer, diff --git a/snippets/code-snippets/light-token/create-mint/rust-client/action.mdx b/snippets/code-snippets/light-token/create-mint/rust-client/action.mdx new file mode 100644 index 00000000..5432b8eb --- /dev/null +++ b/snippets/code-snippets/light-token/create-mint/rust-client/action.mdx @@ -0,0 +1,31 @@ +```rust +use light_client::rpc::Rpc; +use light_token_client::actions::{CreateMint, TokenMetadata}; +use rust_client::setup_rpc_and_payer; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let (mut rpc, payer) = setup_rpc_and_payer().await; + + let (signature, mint) = CreateMint { + decimals: 9, + freeze_authority: None, + token_metadata: Some(TokenMetadata { + name: "Example Token".to_string(), + symbol: "EXT".to_string(), + uri: "https://example.com/metadata.json".to_string(), + update_authority: Some(payer.pubkey()), + additional_metadata: Some(vec![("type".to_string(), "example".to_string())]), + }), + seed: None, + } + .execute(&mut rpc, &payer, &payer) + .await?; + + let data = rpc.get_account(mint).await?; + println!("Mint: {mint} exists: {} Tx: {signature}", data.is_some()); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/create-mint/rust-client/instruction.mdx b/snippets/code-snippets/light-token/create-mint/rust-client/instruction.mdx new file mode 100644 index 00000000..f56e0885 --- /dev/null +++ b/snippets/code-snippets/light-token/create-mint/rust-client/instruction.mdx @@ -0,0 +1,97 @@ +```rust +use light_client::{ + indexer::{AddressWithTree, Indexer}, + rpc::Rpc, +}; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::instruction::{ + derive_mint_compressed_address, find_mint_address, CreateMint, CreateMintParams, + DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, +}; +use light_token_interface::{ + instructions::extensions::{ + token_metadata::TokenMetadataInstructionData, ExtensionInstructionData, + }, + state::AdditionalMetadata, +}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None)).await?; + + 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: DEFAULT_RENT_PAYMENT, // 24h of rent + write_top_up: DEFAULT_WRITE_TOP_UP, // 3h of rent + }; + + // Build and send instruction (mint_seed must sign) + let create_mint_instruction = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ) + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[create_mint_instruction], &payer.pubkey(), &[&payer, &mint_seed]) + .await?; + + let data = rpc.get_account(mint).await?; + println!("Mint: {} exists: {} Tx: {sig}", mint, data.is_some()); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/create-token-account/anchor-macro/full-example.mdx b/snippets/code-snippets/light-token/create-token-account/anchor-macro/full-example.mdx new file mode 100644 index 00000000..85abf4af --- /dev/null +++ b/snippets/code-snippets/light-token/create-token-account/anchor-macro/full-example.mdx @@ -0,0 +1,231 @@ + +```rust lib.rs +#![allow(unexpected_cfgs, deprecated)] + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk::derive_light_cpi_signer; +use light_sdk_macros::{light_program, LightAccounts}; +use light_sdk_types::CpiSigner; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR}; + +declare_id!("9p5BUDtVmRRJqp2sN73ZUZDbaYtYvEWuxzrHH3A2ni9y"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("9p5BUDtVmRRJqp2sN73ZUZDbaYtYvEWuxzrHH3A2ni9y"); + +pub const VAULT_AUTH_SEED: &[u8] = b"vault_auth"; +pub const VAULT_SEED: &[u8] = b"vault"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateTokenAccountParams { + pub create_accounts_proof: CreateAccountsProof, + pub vault_bump: u8, +} + +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateTokenAccountParams)] +pub struct CreateTokenAccount<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Validated by light-token CPI + // You can use Light, SPL, or Token-2022 mints to create a light token account. + pub mint: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + #[account( + seeds = [VAULT_AUTH_SEED], + bump, + )] + pub vault_authority: UncheckedAccount<'info>, + + /// CHECK: Validated by light-token CPI + #[account( + mut, + seeds = [VAULT_SEED, mint.key().as_ref()], + bump, + )] + #[light_account( + init, + token::authority = [VAULT_SEED, self.mint.key()], + token::mint = mint, + token::owner = vault_authority, + token::bump = params.vault_bump + )] + pub vault: UncheckedAccount<'info>, + + /// CHECK: Validated by light-token CPI + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub light_token_compressible_config: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: Validated by light-token CPI + pub light_token_cpi_authority: AccountInfo<'info>, + + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +#[light_program] +#[program] +pub mod light_token_macro_create_token_account { + use super::*; + + #[allow(unused_variables)] + pub fn create_token_account<'info>( + ctx: Context<'_, '_, '_, 'info, CreateTokenAccount<'info>>, + params: CreateTokenAccountParams, + ) -> Result<()> { + Ok(()) + } +} +``` + +```rust test.rs +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_client::{ + indexer::AddressWithTree, + interface::get_create_accounts_proof, +}; +use light_program_test::{Indexer, LightProgramTest, ProgramTestConfig, Rpc}; +use light_token::instruction::{ + CreateMint, CreateMintParams, LIGHT_TOKEN_PROGRAM_ID, derive_mint_compressed_address, + find_mint_address, +}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Create a compressed mint for testing. +async fn setup_create_mint( + rpc: &mut LightProgramTest, + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, Keypair) { + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compression_address = + derive_mint_compressed_address(&mint_seed.pubkey(), &address_tree.tree); + + let (mint, bump) = find_mint_address(&mint_seed.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + (mint, mint_seed) +} + +/// Creates a token vault via #[light_account(init, token, ...)]. +#[tokio::test] +async fn test_create_token_account() { + use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR}; + use light_token_macro_create_token_account::{ + CreateTokenAccountParams, VAULT_AUTH_SEED, VAULT_SEED, + }; + use light_token_types::CPI_AUTHORITY_PDA; + + let program_id = light_token_macro_create_token_account::ID; + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("light_token_macro_create_token_account", program_id)]), + ); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let (mint, _mint_seed) = setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + + let (vault_authority, _auth_bump) = + Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, mint.as_ref()], &program_id); + + // Empty inputs: no PDA accounts for token-only instruction + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + let accounts = light_token_macro_create_token_account::accounts::CreateTokenAccount { + fee_payer: payer.pubkey(), + mint, + vault_authority, + vault, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + light_token_rent_sponsor: RENT_SPONSOR, + light_token_cpi_authority: CPI_AUTHORITY_PDA.into(), + light_token_program: LIGHT_TOKEN_PROGRAM_ID, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = light_token_macro_create_token_account::instruction::CreateTokenAccount { + params: CreateTokenAccountParams { + create_accounts_proof: proof_result.create_accounts_proof, + vault_bump, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + + println!("Transaction result: {:?}", result); +} +``` + diff --git a/snippets/code-snippets/light-token/create-token-account/rust-client/instruction.mdx b/snippets/code-snippets/light-token/create-token-account/rust-client/instruction.mdx new file mode 100644 index 00000000..2795ac32 --- /dev/null +++ b/snippets/code-snippets/light-token/create-token-account/rust-client/instruction.mdx @@ -0,0 +1,36 @@ +```rust +use light_client::rpc::Rpc; +use light_token::instruction::CreateTokenAccount; +use rust_client::{setup_spl_mint_context, SplMintContext}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint + // You can use Light, SPL, or Token-2022 mints to create a light token account. + let SplMintContext { + mut rpc, + payer, + mint, + } = setup_spl_mint_context().await; + + let account = Keypair::new(); + + let create_token_account_instruction = + CreateTokenAccount::new(payer.pubkey(), account.pubkey(), mint, payer.pubkey()) + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[create_token_account_instruction], &payer.pubkey(), &[&payer, &account]) + .await?; + + let data = rpc.get_account(account.pubkey()).await?; + println!( + "Account: {} exists: {} Tx: {sig}", + account.pubkey(), + data.is_some() + ); + + Ok(()) +} +``` 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 00000000..40ede2f4 --- /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/freeze-instruction.mdx b/snippets/code-snippets/light-token/freeze-thaw/rust-client/freeze-instruction.mdx new file mode 100644 index 00000000..4b17db45 --- /dev/null +++ b/snippets/code-snippets/light-token/freeze-thaw/rust-client/freeze-instruction.mdx @@ -0,0 +1,37 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::Freeze; +use rust_client::{setup, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint, associated token account with tokens, and approves delegate + let SetupContext { + mut rpc, + payer, + mint, + associated_token_account, + .. + } = setup().await; + + // freeze_authority must match what was set during mint creation. + let freeze_instruction = Freeze { + token_account: associated_token_account, + mint, + freeze_authority: payer.pubkey(), + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[freeze_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("State: {:?} Tx: {sig}", token.state); + + Ok(()) +} +``` 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 00000000..60864340 --- /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/freeze-thaw/rust-client/thaw-instruction.mdx b/snippets/code-snippets/light-token/freeze-thaw/rust-client/thaw-instruction.mdx new file mode 100644 index 00000000..7d7eb8f9 --- /dev/null +++ b/snippets/code-snippets/light-token/freeze-thaw/rust-client/thaw-instruction.mdx @@ -0,0 +1,36 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::Thaw; +use rust_client::{setup_frozen, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint, associated token account with tokens, and freezes account + let SetupContext { + mut rpc, + payer, + mint, + associated_token_account, + .. + } = setup_frozen().await; + + let thaw_instruction = Thaw { + token_account: associated_token_account, + mint, + freeze_authority: payer.pubkey(), + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[thaw_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("State: {:?} Tx: {sig}", token.state); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/load-ata/instruction.mdx b/snippets/code-snippets/light-token/load-ata/instruction.mdx index 8a8b05fc..4cf75957 100644 --- a/snippets/code-snippets/light-token/load-ata/instruction.mdx +++ b/snippets/code-snippets/light-token/load-ata/instruction.mdx @@ -3,13 +3,12 @@ import "dotenv/config"; import { Keypair } from "@solana/web3.js"; import { createRpc, - bn, buildAndSignTx, sendAndConfirmTx, } from "@lightprotocol/stateless.js"; import { - createMint, - mintTo, + createMintInterface, + mintToCompressed, createLoadAtaInstructions, getAssociatedTokenAddressInterface, } from "@lightprotocol/compressed-token"; @@ -17,31 +16,36 @@ import { homedir } from "os"; import { readFileSync } from "fs"; // devnet: -const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; -const rpc = createRpc(RPC_URL); +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const rpc = createRpc(); +const rpc = createRpc(); const payer = Keypair.fromSecretKey( new Uint8Array( - JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) - ) + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")), + ), ); (async function () { - // Setup: mint directly to cold state - const { mint } = await createMint(rpc, payer, payer.publicKey, 9); - await mintTo(rpc, payer, mint, payer.publicKey, payer, bn(1000)); + // Inactive Light Tokens are cryptographically preserved on the Solana ledger + // as compressed tokens (cold storage) + // Setup: Get compressed tokens in light-token associated token account + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await mintToCompressed(rpc, payer, mint, payer, [{ recipient: payer.publicKey, amount: 1000n }]); - const lightTokenAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); + const lightTokenAta = getAssociatedTokenAddressInterface( + mint, + payer.publicKey, + ); - // load from cold to hot state + // Load compressed tokens to light associated token account (hot balance) const ixs = await createLoadAtaInstructions( rpc, lightTokenAta, payer.publicKey, mint, - payer.publicKey + payer.publicKey, ); if (ixs.length === 0) return console.log("Nothing to load"); diff --git a/snippets/code-snippets/light-token/mint-to/instruction.mdx b/snippets/code-snippets/light-token/mint-to/instruction.mdx index c2efd0bb..fcaf80d0 100644 --- a/snippets/code-snippets/light-token/mint-to/instruction.mdx +++ b/snippets/code-snippets/light-token/mint-to/instruction.mdx @@ -1,6 +1,11 @@ ```typescript import "dotenv/config"; -import { Keypair, ComputeBudgetProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; +import { + Keypair, + ComputeBudgetProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; import { createRpc, bn, DerivationMode } from "@lightprotocol/stateless.js"; import { createMintInterface, @@ -13,26 +18,25 @@ import { homedir } from "os"; import { readFileSync } from "fs"; // devnet: -const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; -const rpc = createRpc(RPC_URL); +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const rpc = createRpc(); +const rpc = createRpc(); const payer = Keypair.fromSecretKey( new Uint8Array( - JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) - ) + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")), + ), ); (async function () { - const { mint } = await createMintInterface(rpc, payer, payer, null, 9); const recipient = Keypair.generate(); await createAtaInterface(rpc, payer, mint, recipient.publicKey); const destination = getAssociatedTokenAddressInterface( mint, - recipient.publicKey + recipient.publicKey, ); const mintInterface = await getMintInterface(rpc, mint); @@ -49,7 +53,7 @@ const payer = Keypair.fromSecretKey( }, ], [], - DerivationMode.compressible + DerivationMode.compressible, ); } @@ -59,12 +63,12 @@ const payer = Keypair.fromSecretKey( payer.publicKey, payer.publicKey, 1_000_000_000, - validityProof + validityProof, ); const tx = new Transaction().add( ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), - ix + ix, ); const signature = await sendAndConfirmTransaction(rpc, tx, [payer]); diff --git a/snippets/code-snippets/light-token/mint-to/rust-client/action.mdx b/snippets/code-snippets/light-token/mint-to/rust-client/action.mdx new file mode 100644 index 00000000..3a741321 --- /dev/null +++ b/snippets/code-snippets/light-token/mint-to/rust-client/action.mdx @@ -0,0 +1,46 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_client::actions::{CreateAta, CreateMint, MintTo}; +use rust_client::setup_rpc_and_payer; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let (mut rpc, payer) = setup_rpc_and_payer().await; + + // Create mint (payer is also mint authority) + let (_signature, mint) = CreateMint { + decimals: 9, + freeze_authority: None, + token_metadata: None, + seed: None, + } + .execute(&mut rpc, &payer, &payer) + .await?; + + // Create associated token account + let (_signature, associated_token_account) = CreateAta { + mint, + owner: payer.pubkey(), + idempotent: true, + } + .execute(&mut rpc, &payer) + .await?; + + // Mint tokens (payer is mint authority) + let sig = MintTo { + mint, + destination: associated_token_account, + amount: 1_000_000, + } + .execute(&mut rpc, &payer, &payer) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/mint-to/rust-client/instruction.mdx b/snippets/code-snippets/light-token/mint-to/rust-client/instruction.mdx new file mode 100644 index 00000000..cd9f6d90 --- /dev/null +++ b/snippets/code-snippets/light-token/mint-to/rust-client/instruction.mdx @@ -0,0 +1,41 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::MintTo; +use rust_client::{setup_empty_associated_token_account, SetupContext}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and empty associated token account + let SetupContext { + mut rpc, + payer, + mint, + associated_token_account, + .. + } = setup_empty_associated_token_account().await; + + let mint_amount = 1_000_000_000u64; + + let mint_to_instruction = MintTo { + mint, + destination: associated_token_account, + amount: mint_amount, + authority: payer.pubkey(), + max_top_up: None, + fee_payer: None, + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[mint_to_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc.get_account(associated_token_account).await?.ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/transfer-checked/rust-client/action.mdx b/snippets/code-snippets/light-token/transfer-checked/rust-client/action.mdx new file mode 100644 index 00000000..165e0989 --- /dev/null +++ b/snippets/code-snippets/light-token/transfer-checked/rust-client/action.mdx @@ -0,0 +1,51 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_client::actions::{CreateAta, TransferChecked}; +use rust_client::{setup, SetupContext}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let SetupContext { + mut rpc, + payer, + mint, + associated_token_account, + decimals, + .. + } = setup().await; + + // Create recipient associated token account + let recipient = Keypair::new(); + let (_signature, recipient_associated_token_account) = CreateAta { + mint, + owner: recipient.pubkey(), + idempotent: true, + } + .execute(&mut rpc, &payer) + .await?; + + // TransferChecked validates decimals match the mint's decimals. + // Only use for Light->Light transfers. + // Use TransferInterface for all other transfers (Light, SPL or Token-2022). + let sig = TransferChecked { + source: associated_token_account, + mint, + destination: recipient_associated_token_account, + amount: 1000, + decimals, + } + .execute(&mut rpc, &payer, &payer) + .await?; + + let data = rpc + .get_account(recipient_associated_token_account) + .await? + .ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/transfer-checked/rust-client/instruction.mdx b/snippets/code-snippets/light-token/transfer-checked/rust-client/instruction.mdx new file mode 100644 index 00000000..621f93de --- /dev/null +++ b/snippets/code-snippets/light-token/transfer-checked/rust-client/instruction.mdx @@ -0,0 +1,62 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token::instruction::{ + get_associated_token_address, CreateAssociatedTokenAccount, TransferChecked, +}; +use rust_client::{setup, SetupContext}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates mint and associated token account with tokens + let SetupContext { + mut rpc, + payer, + mint, + associated_token_account, + decimals, + .. + } = setup().await; + + let transfer_amount = 400_000u64; + + // Create recipient associated token account + let recipient = Keypair::new(); + let recipient_associated_token_account = get_associated_token_address(&recipient.pubkey(), &mint); + + let create_associated_token_account_instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction()?; + + rpc.create_and_send_transaction(&[create_associated_token_account_instruction], &payer.pubkey(), &[&payer]) + .await?; + + // TransferChecked validates decimals match the mint's decimals + // Only use for Light->Light transfers. + // Use TransferInterface for all other transfers (Light, SPL or Token-2022). + let transfer_instruction = TransferChecked { + source: associated_token_account, + mint, + destination: recipient_associated_token_account, + amount: transfer_amount, + decimals, + authority: payer.pubkey(), + max_top_up: None, + fee_payer: None, + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[transfer_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc + .get_account(recipient_associated_token_account) + .await? + .ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/transfer-interface/instruction.mdx b/snippets/code-snippets/light-token/transfer-interface/instruction.mdx index 2ae13150..20d076fb 100644 --- a/snippets/code-snippets/light-token/transfer-interface/instruction.mdx +++ b/snippets/code-snippets/light-token/transfer-interface/instruction.mdx @@ -18,15 +18,15 @@ import { homedir } from "os"; import { readFileSync } from "fs"; // devnet: -const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; -const rpc = createRpc(RPC_URL); +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const rpc = createRpc(); +const rpc = createRpc(); const payer = Keypair.fromSecretKey( new Uint8Array( - JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) - ) + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")), + ), ); (async function () { @@ -36,7 +36,7 @@ const payer = Keypair.fromSecretKey( await createAtaInterface(rpc, payer, mint, sender.publicKey); const senderAta = getAssociatedTokenAddressInterface( mint, - sender.publicKey + sender.publicKey, ); await mintToInterface(rpc, payer, mint, senderAta, payer, 1_000_000_000); @@ -44,14 +44,15 @@ const payer = Keypair.fromSecretKey( await createAtaInterface(rpc, payer, mint, recipient.publicKey); const recipientAta = getAssociatedTokenAddressInterface( mint, - recipient.publicKey + recipient.publicKey, ); + // Transfer tokens between light-token associate token accounts const ix = createTransferInterfaceInstruction( senderAta, recipientAta, sender.publicKey, - 500_000_000 + 500_000_000, ); const tx = new Transaction().add(ix); diff --git a/snippets/code-snippets/light-token/transfer-interface/rust-client/action.mdx b/snippets/code-snippets/light-token/transfer-interface/rust-client/action.mdx new file mode 100644 index 00000000..61f2c9c5 --- /dev/null +++ b/snippets/code-snippets/light-token/transfer-interface/rust-client/action.mdx @@ -0,0 +1,50 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_client::actions::{CreateAta, TransferInterface}; +use rust_client::{setup, SetupContext}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let SetupContext { + mut rpc, + payer, + mint, + associated_token_account, + decimals, + .. + } = setup().await; + + // Create recipient associated token account + let recipient = Keypair::new(); + let (_signature, recipient_associated_token_account) = CreateAta { + mint, + owner: recipient.pubkey(), + idempotent: true, + } + .execute(&mut rpc, &payer) + .await?; + + // Transfers tokens between token accounts (SPL, Token-2022, or Light) in a single call. + let sig = TransferInterface { + source: associated_token_account, + mint, + destination: recipient_associated_token_account, + amount: 1000, + decimals, + ..Default::default() + } + .execute(&mut rpc, &payer, &payer) + .await?; + + let data = rpc + .get_account(recipient_associated_token_account) + .await? + .ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/transfer-interface/rust-client/instruction.mdx b/snippets/code-snippets/light-token/transfer-interface/rust-client/instruction.mdx new file mode 100644 index 00000000..bc4ebd49 --- /dev/null +++ b/snippets/code-snippets/light-token/transfer-interface/rust-client/instruction.mdx @@ -0,0 +1,74 @@ +```rust +use anchor_spl::token::spl_token; +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::{ + instruction::{ + get_associated_token_address, CreateAssociatedTokenAccount, SplInterface, + TransferInterface, LIGHT_TOKEN_PROGRAM_ID, + }, + spl_interface::find_spl_interface_pda_with_index, +}; +use rust_client::{setup_spl_associated_token_account, setup_spl_mint}; +use solana_sdk::signer::Signer; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(true, None)).await?; + + let payer = rpc.get_payer().insecure_clone(); + let decimals = 2u8; + let amount = 10_000u64; + + // Setup creates mint, mints tokens and creates SPL associated token account + let mint = setup_spl_mint(&mut rpc, &payer, decimals).await; + let spl_associated_token_account = setup_spl_associated_token_account(&mut rpc, &payer, &mint, &payer.pubkey(), amount).await; + let (interface_pda, interface_bump) = find_spl_interface_pda_with_index(&mint, 0, false); + + // Create Light associated token account + let light_associated_token_account = get_associated_token_address(&payer.pubkey(), &mint); + + let create_associated_token_account_instruction = + CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint).instruction()?; + + rpc.create_and_send_transaction(&[create_associated_token_account_instruction], &payer.pubkey(), &[&payer]) + .await?; + + // SPL interface PDA for Mint (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, + }; + + // Transfers tokens between token accounts (SPL, Token-2022, or Light) in a single call. + let transfer_instruction = TransferInterface { + source: spl_associated_token_account, + destination: light_associated_token_account, + amount, + decimals, + authority: payer.pubkey(), + payer: payer.pubkey(), + spl_interface: Some(spl_interface), + max_top_up: None, + source_owner: spl_token::ID, + destination_owner: LIGHT_TOKEN_PROGRAM_ID, + } + .instruction()?; + + let sig = rpc + .create_and_send_transaction(&[transfer_instruction], &payer.pubkey(), &[&payer]) + .await?; + + let data = rpc + .get_account(light_associated_token_account) + .await? + .ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/unwrap/instruction.mdx b/snippets/code-snippets/light-token/unwrap/instruction.mdx index ec4ee0da..38689df2 100644 --- a/snippets/code-snippets/light-token/unwrap/instruction.mdx +++ b/snippets/code-snippets/light-token/unwrap/instruction.mdx @@ -15,7 +15,7 @@ import { homedir } from "os"; import { readFileSync } from "fs"; // devnet: -const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; const rpc = createRpc(RPC_URL); // localnet: // const rpc = createRpc(); diff --git a/snippets/code-snippets/light-token/unwrap/rust-client/action.mdx b/snippets/code-snippets/light-token/unwrap/rust-client/action.mdx new file mode 100644 index 00000000..79f3bf78 --- /dev/null +++ b/snippets/code-snippets/light-token/unwrap/rust-client/action.mdx @@ -0,0 +1,40 @@ +```rust +use anchor_spl::token::spl_token::state::Account as SplAccount; +use light_client::rpc::Rpc; +use light_token_client::actions::Unwrap; +use rust_client::{setup_for_unwrap, UnwrapContext}; +use solana_sdk::program_pack::Pack; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates Light associated token account with tokens and empty SPL associated token account + let UnwrapContext { + mut rpc, + payer, + mint, + destination_associated_token_account, + light_associated_token_account, + decimals, + } = setup_for_unwrap().await; + + // Unwrap tokens from Light Token associated token account to SPL associated token account + let sig = Unwrap { + source: light_associated_token_account, + destination_spl_ata: destination_associated_token_account, + mint, + amount: 500_000, + decimals, + } + .execute(&mut rpc, &payer, &payer) + .await?; + + let data = rpc + .get_account(destination_associated_token_account) + .await? + .ok_or("Account not found")?; + let token = SplAccount::unpack(&data.data)?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/code-snippets/light-token/wrap/instruction.mdx b/snippets/code-snippets/light-token/wrap/instruction.mdx index 5be18dbd..e360f5d2 100644 --- a/snippets/code-snippets/light-token/wrap/instruction.mdx +++ b/snippets/code-snippets/light-token/wrap/instruction.mdx @@ -16,7 +16,7 @@ import { homedir } from "os"; import { readFileSync } from "fs"; // devnet: -const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; const rpc = createRpc(RPC_URL); // localnet: // const rpc = createRpc(); diff --git a/snippets/code-snippets/light-token/wrap/rust-client/action.mdx b/snippets/code-snippets/light-token/wrap/rust-client/action.mdx new file mode 100644 index 00000000..5b235784 --- /dev/null +++ b/snippets/code-snippets/light-token/wrap/rust-client/action.mdx @@ -0,0 +1,39 @@ +```rust +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_token_client::actions::Wrap; +use rust_client::{setup_for_wrap, WrapContext}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup creates SPL associated token account with tokens and empty Light associated token account + let WrapContext { + mut rpc, + payer, + mint, + source_associated_token_account, + light_associated_token_account, + decimals, + } = setup_for_wrap().await; + + // Wrap tokens from SPL associated token account to Light Token associated token account + let sig = Wrap { + source_spl_ata: source_associated_token_account, + destination: light_associated_token_account, + mint, + amount: 500_000, + decimals, + } + .execute(&mut rpc, &payer, &payer) + .await?; + + let data = rpc + .get_account(light_associated_token_account) + .await? + .ok_or("Account not found")?; + let token = light_token_interface::state::Token::deserialize(&mut &data.data[..])?; + println!("Balance: {} Tx: {sig}", token.amount); + + Ok(()) +} +``` diff --git a/snippets/jsx/code-compare.jsx b/snippets/jsx/code-compare.jsx index 86ec2b1f..7cce628a 100644 --- a/snippets/jsx/code-compare.jsx +++ b/snippets/jsx/code-compare.jsx @@ -3,18 +3,52 @@ 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 firstPreRef = useRef(null); + const secondPreRef = useRef(null); + const [containerHeight, setContainerHeight] = useState(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 +94,7 @@ export const CodeCompare = ({ }; const handleToggle = () => { - animateTo(isLightMode ? 0 : 100); + animateTo(showingFirst ? 0 : 100); }; const handleMouseDown = (e) => { @@ -123,74 +157,111 @@ export const CodeCompare = ({ }; }, []); + // Measure and animate height when toggling + useEffect(() => { + const activeRef = showingFirst ? firstPreRef : secondPreRef; + if (activeRef.current) { + setContainerHeight(activeRef.current.scrollHeight); + } + }, [showingFirst]); + return ( <>
{/* Header with toggle */} -
- - {isLightMode ? secondLabel : firstLabel} +
+ + {showingFirst ? firstLabel : secondLabel} - {/* Neumorphic Toggle Switch */} -
- {/* Toggle button */} +
+ {/* Copy button */} + + + {/* Neumorphic Toggle Switch */}
- {/* LED */} + {/* Toggle button */}
+ > + {/* LED */} +
+
@@ -209,27 +280,43 @@ 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,7 +343,7 @@ 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%)", }} diff --git a/snippets/overview-tables/cookbook-guides-table.mdx b/snippets/overview-tables/cookbook-guides-table.mdx index bcafa882..decb9cf0 100644 --- a/snippets/overview-tables/cookbook-guides-table.mdx +++ b/snippets/overview-tables/cookbook-guides-table.mdx @@ -46,11 +46,41 @@ Transfer between light-token and SPL accounts + + + Transfer Checked + + Transfer with decimals verification + + + + Burn + + Burn tokens from light-token accounts + + + + Freeze & Thaw + + Freeze and thaw light-token accounts + + + + Approve & Revoke + + Delegate and revoke token authority + Wrap & Unwrap Convert between SPL/T22 and light-token + + + Load ATA + + Load cold light-token accounts to hot balance for transfers in one instruction + diff --git a/snippets/overview-tables/light-token-client-examples-table.mdx b/snippets/overview-tables/light-token-client-examples-table.mdx new file mode 100644 index 00000000..0dede07a --- /dev/null +++ b/snippets/overview-tables/light-token-client-examples-table.mdx @@ -0,0 +1,35 @@ +### TypeScript + +| | | | +|---------|--------|-------------| +| **approve** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/delegate-approve.ts) | — | +| **create-ata** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/create-ata.ts) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/instructions/create-ata.ts) | +| **create-mint** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/create-mint.ts) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/instructions/create-mint.ts) | +| **load-ata** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/load-ata.ts) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/instructions/load-ata.ts) | +| **mint-to** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/mint-to.ts) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/instructions/mint-to.ts) | +| **revoke** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/delegate-revoke.ts) | — | +| **transfer-interface** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/transfer-interface.ts) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/instructions/transfer-interface.ts) | +| **unwrap** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/unwrap.ts) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/instructions/unwrap.ts) | +| **wrap** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/wrap.ts) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/instructions/wrap.ts) | + +### Rust + +| | | | +|---------|--------|-------------| +| **approve** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/approve.rs) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/approve.rs) | +| **burn** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/burn.rs) | +| **burn-checked** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/burn_checked.rs) | +| **close** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/close.rs) | +| **create-ata** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/create_associated_token_account.rs) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/create_associated_token_account.rs) | +| **create-mint** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/create_mint.rs) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/create_mint.rs) | +| **create-token-account** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/create_token_account.rs) | +| **freeze** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/freeze.rs) | +| **mint-to** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/mint_to.rs) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/mint_to.rs) | +| **mint-to-checked** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/mint_to_checked.rs) | +| **revoke** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/revoke.rs) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/revoke.rs) | +| **spl-to-light** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/spl_to_light_transfer.rs) | +| **thaw** | — | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/thaw.rs) | +| **transfer-checked** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/transfer_checked.rs) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/transfer_checked.rs) | +| **transfer-interface** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/transfer_interface.rs) | [Instruction](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/instructions/transfer_interface.rs) | +| **unwrap** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/unwrap.rs) | — | +| **wrap** | [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/wrap.rs) | — | diff --git a/snippets/overview-tables/light-token-program-examples-table.mdx b/snippets/overview-tables/light-token-program-examples-table.mdx new file mode 100644 index 00000000..6801290e --- /dev/null +++ b/snippets/overview-tables/light-token-program-examples-table.mdx @@ -0,0 +1,35 @@ +### Instructions + +The instructions use pure CPI calls which you can combine with existing and / or light macros. +For existing programs, you can replace spl_token with light_token instructions as you need. The API is a superset of SPL-token so switching is straightforward. + +| | Description | +|---------|-------------| +| [approve](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/approve/src/lib.rs) | Approve delegate via CPI | +| [burn](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/burn/src/lib.rs) | Burn tokens via CPI | +| [close](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/close/src/lib.rs) | Close token account via CPI | +| [create-associated-token-account](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/create-ata/src/lib.rs) | Create associated light-token account via CPI | +| [create-mint](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/create-mint/src/lib.rs) | Create light-token mint via CPI | +| [create-token-account](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/create-token-account/src/lib.rs) | Create light-token account via CPI | +| [freeze](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/freeze/src/lib.rs) | Freeze token account via CPI | +| [mint-to](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/mint-to/src/lib.rs) | Mint tokens via CPI | +| [revoke](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/revoke/src/lib.rs) | Revoke delegate via CPI | +| [thaw](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/thaw/src/lib.rs) | Thaw token account via CPI | +| [transfer-checked](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/transfer-checked/src/lib.rs) | Transfer with mint validation via CPI | +| [transfer-interface](https://github.com/Lightprotocol/examples-light-token/blob/main/programs/anchor/basic-instructions/transfer-interface/src/lib.rs) | Transfer between light-token, T22, and SPL accounts via CPI | + +### Macros + +| | Description | +|---------|-------------| +| [counter](https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/basic-macros/counter) | Create PDA with sponsored rent-exemption | +| [create-associated-token-account](https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/basic-macros/create-ata) | Create associated light-token account | +| [create-mint](https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/basic-macros/create-mint) | Create light-token mint | +| [create-token-account](https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/basic-macros/create-token-account) | Create light-token account | + +### Examples + +| | Description | +|---------|-------------| +| [cp-swap-reference](https://github.com/Lightprotocol/cp-swap-reference) | Fork of Raydium AMM that creates markets without paying rent-exemption | +| [create-and-transfer](https://github.com/Lightprotocol/examples-light-token/tree/main/programs/anchor/create-and-transfer) | Create account via macro and transfer via CPI | diff --git a/snippets/setup/compressed-pdas-program-setup.mdx b/snippets/setup/compressed-pdas-program-setup.mdx index 74801922..0e3426fb 100644 --- a/snippets/setup/compressed-pdas-program-setup.mdx +++ b/snippets/setup/compressed-pdas-program-setup.mdx @@ -5,13 +5,13 @@ Add dependencies to your program. ```toml Anchor [dependencies] -light-sdk = "0.16.0" +light-sdk = "0.19.0" anchor_lang = "0.31.1" ``` ```toml Native Rust [dependencies] -light-sdk = "0.16.0" +light-sdk = "0.19.0" borsh = "0.10.0" solana-program = "2.2" ``` diff --git a/snippets/setup/rust-install-dependencies.mdx b/snippets/setup/rust-install-dependencies.mdx index cf39be3a..c1de57df 100644 --- a/snippets/setup/rust-install-dependencies.mdx +++ b/snippets/setup/rust-install-dependencies.mdx @@ -1,12 +1,8 @@ ```toml Cargo.toml [dependencies] -light-compressed-token-sdk = "0.1" -light-client = "0.1" -light-token-types = "0.1" -solana-sdk = "2.2" -borsh = "0.10" -tokio = { version = "1.36", features = ["full"] } - -[dev-dependencies] -light-program-test = "0.1" # For in-memory tests with LiteSVM +light-token = "0.4.0" +light-client = { version = "0.19.0", features = ["v2"] } +solana-sdk = "2" +borsh = "0.10.4" +tokio = { version = "1", features = ["full"] } ``` diff --git a/snippets/versions/rust-deps-0.16.0.mdx b/snippets/versions/rust-deps-0.16.0.mdx deleted file mode 100644 index c14d002d..00000000 --- a/snippets/versions/rust-deps-0.16.0.mdx +++ /dev/null @@ -1,5 +0,0 @@ -```toml -[dependencies] -light-client = "0.16.0" -light-sdk = "0.16.0" -``` diff --git a/snippets/versions/rust-deps-0.19.0.mdx b/snippets/versions/rust-deps-0.19.0.mdx new file mode 100644 index 00000000..ddb245ee --- /dev/null +++ b/snippets/versions/rust-deps-0.19.0.mdx @@ -0,0 +1,5 @@ +```toml +[dependencies] +light-client = "0.19.0" +light-sdk = "0.19.0" +``` diff --git a/snippets/versions/rust-deps-token-sdk-0.16.0.mdx b/snippets/versions/rust-deps-token-sdk-0.16.0.mdx deleted file mode 100644 index a15a3903..00000000 --- a/snippets/versions/rust-deps-token-sdk-0.16.0.mdx +++ /dev/null @@ -1,8 +0,0 @@ -```toml -[dependencies] -light-sdk = "0.16.0" -light-compressed-token-sdk = "0.1.0" - -[dev-dependencies] -light-program-test = "1.2.1" -``` diff --git a/snippets/versions/rust-deps-token-sdk-0.19.0.mdx b/snippets/versions/rust-deps-token-sdk-0.19.0.mdx new file mode 100644 index 00000000..e51ec9cc --- /dev/null +++ b/snippets/versions/rust-deps-token-sdk-0.19.0.mdx @@ -0,0 +1,5 @@ +```toml +[dependencies] +light-sdk = "0.19.0" +light-token = "0.4.0" +```