diff --git a/.gitignore b/.gitignore index bd44d776..f6f2388f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ mintlify-docs/ # Dependencies node_modules/ -stash \ No newline at end of file +stash +docs-audit.md +docs-audit.txt \ No newline at end of file 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/api-reference/libraries/light-token.mdx b/api-reference/libraries/light-token.mdx new file mode 100644 index 00000000..e755a7b4 --- /dev/null +++ b/api-reference/libraries/light-token.mdx @@ -0,0 +1,7 @@ +--- +title: "light-token" +description: "Light Token program crate for rent-free token accounts and mints" +url: "https://docs.rs/light-token/latest/light_token/" +--- + +View the full reference: [light-token](https://docs.rs/light-token/latest/light_token/) diff --git a/blog/light-token.mdx b/blog/light-token.mdx index 28c33d6e..a0535383 100644 --- a/blog/light-token.mdx +++ b/blog/light-token.mdx @@ -113,8 +113,7 @@ We have dedicated toolkits for specific use cases: - [Payments](/light-token/toolkits/for-payments) - [Wallets](/light-token/toolkits/for-wallets) -- [Streaming mints](/light-token/toolkits/for-streaming-mints) -- [Streaming tokens](/light-token/toolkits/for-streaming-tokens) + Get in touch on telegram for help! diff --git a/changelog.mdx b/changelog.mdx index c718c909..97a030a7 100644 --- a/changelog.mdx +++ b/changelog.mdx @@ -5,24 +5,24 @@ description: "Latest Releases" rss: true --- - + ### ZK Compression CLI ```bash - npm i -g @lightprotocol/zk-compression-cli@alpha + npm i -g @lightprotocol/zk-compression-cli@beta ``` ```bash - yarn global add @lightprotocol/zk-compression-cli@0.27.1-alpha.2 + yarn global add @lightprotocol/zk-compression-cli@beta ``` ```bash - pnpm add -g @lightprotocol/zk-compression-cli@alpha + pnpm add -g @lightprotocol/zk-compression-cli@beta ``` @@ -32,20 +32,20 @@ rss: true ```bash - npm install @lightprotocol/stateless.js@0.22.1-alpha.1 - npm install @lightprotocol/compressed-token + npm install @lightprotocol/stateless.js@beta + npm install @lightprotocol/compressed-token@beta ``` ```bash - yarn add @lightprotocol/stateless.js@0.22.1-alpha.1 - yarn add @lightprotocol/compressed-token + yarn add @lightprotocol/stateless.js@beta + yarn add @lightprotocol/compressed-token@beta ``` ```bash - pnpm add @lightprotocol/stateless.js@0.22.1-alpha.1 - pnpm add @lightprotocol/compressed-token + pnpm add @lightprotocol/stateless.js@beta + pnpm add @lightprotocol/compressed-token@beta ``` diff --git a/client-library/client-guide.mdx b/client-library/client-guide.mdx index 3843dfba..89bd8cc7 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@0.22.1-alpha.1 \ - @lightprotocol/compressed-token@0.22.1-alpha.1 \ + @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta \ @solana/web3.js ``` @@ -138,8 +138,8 @@ npm install --save \ ```bash yarn add \ - @lightprotocol/stateless.js@0.22.1-alpha.1 \ - @lightprotocol/compressed-token@0.22.1-alpha.1 \ + @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta \ @solana/web3.js ``` @@ -149,8 +149,8 @@ yarn add \ ```bash pnpm add \ - @lightprotocol/stateless.js@0.22.1-alpha.1 \ - @lightprotocol/compressed-token@0.22.1-alpha.1 \ + @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta \ @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@alpha +npm i -g @lightprotocol/zk-compression-cli@beta ``` 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@alpha +npm i -g @lightprotocol/zk-compression-cli@beta ``` 2. Start a single-node Solana cluster, an RPC node, and a prover node at ports 8899, 8784, and 3001. diff --git a/compressed-tokens/advanced-guides/example-node-js.mdx b/compressed-tokens/advanced-guides/example-node-js.mdx deleted file mode 100644 index 4f1065ae..00000000 --- a/compressed-tokens/advanced-guides/example-node-js.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Example Node.js Client" -description: "Script to execute basic compression/decompression/transfers" -url: "https://github.com/Lightprotocol/example-nodejs-client" -keywords: ["compressed tokens nodejs", "solana nodejs example"] ---- - -View the full example on GitHub: [example-nodejs-client](https://github.com/Lightprotocol/example-nodejs-client) diff --git a/compressed-tokens/advanced-guides/example-web-client.mdx b/compressed-tokens/advanced-guides/example-web-client.mdx deleted file mode 100644 index 8c1ec3e9..00000000 --- a/compressed-tokens/advanced-guides/example-web-client.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Example Web Client" -description: "Demonstrates how to use @lightprotocol/stateless.js in a browser environment" -url: "https://github.com/Lightprotocol/example-web-client" -keywords: ["compressed tokens web client", "solana web app example"] ---- - -View the full example on GitHub: [example-web-client](https://github.com/Lightprotocol/example-web-client) diff --git a/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx b/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx index 12da5bd5..0afaf0a1 100644 --- a/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx +++ b/compressed-tokens/advanced-guides/use-token-2022-with-compression.mdx @@ -178,12 +178,12 @@ const connection = createRpc(); // defaults to localhost:8899 console.log(`Token-2022 mint created with address: ${mint.publicKey.toString()}`); // Step 3: Call createTokenPool() to initialize omnibus account - // and register Token-2022 mint with Compressed Token Program + // and register Token-2022 mint with Light Token Program // Create PDA that holds SPL tokens for compressed tokens const registerTxId = await createTokenPool( connection, payer, - mint.publicKey, // Token-2022 mint to register with compressed token program + mint.publicKey, // Token-2022 mint to register with Light Token Program undefined, TOKEN_2022_PROGRAM_ID ); diff --git a/compressed-tokens/for-privy.mdx b/compressed-tokens/for-privy.mdx new file mode 100644 index 00000000..a7200aa6 --- /dev/null +++ b/compressed-tokens/for-privy.mdx @@ -0,0 +1,207 @@ +--- +title: "Rent-Free SPL Accounts with Privy" +sidebarTitle: "Privy Guide" +description: "Integrate compressed tokens with Privy embedded wallets for rent-free SPL token accounts and transfers." +keywords: ["privy solana", "privy compressed tokens", "embedded wallet compression", "rent-free tokens privy"] +--- +import Toolkits2Setup from "/snippets/setup/toolkits2-setup.mdx"; +import TransferNodejs from "/snippets/code-snippets/privy/transfer/nodejs.mdx"; +import TransferReact from "/snippets/code-snippets/privy/transfer/react.mdx"; +import CompressNodejs from "/snippets/code-snippets/privy/compress/nodejs.mdx"; +import CompressReact from "/snippets/code-snippets/privy/compress/react.mdx"; +import DecompressNodejs from "/snippets/code-snippets/privy/decompress/nodejs.mdx"; +import DecompressReact from "/snippets/code-snippets/privy/decompress/react.mdx"; +import BalancesNodejs from "/snippets/code-snippets/privy/balances/nodejs.mdx"; +import BalancesReact from "/snippets/code-snippets/privy/balances/react.mdx"; +import TransactionHistoryNodejs from "/snippets/code-snippets/privy/transaction-history/nodejs.mdx"; +import TransactionHistoryReact from "/snippets/code-snippets/privy/transaction-history/react.mdx"; +import ActionCode from '/snippets/code-snippets/compressed-token/create-mint/action.mdx'; +import AiPromptNodejs from "/snippets/ai-prompts/privy-nodejs-compressed.mdx"; +import AiPromptReact from "/snippets/ai-prompts/privy-react-compressed.mdx"; + +| Creation Cost | SPL Token Account | Compressed Token | +| :------------ | :---------------- | :--------------- | +| **Per account** | ~2,000,000 lamports | ~**5,000** lamports | + +Privy handles user authentication and wallet management. You build transactions with tokens (SOL, SPL or compressed) and Privy signs them client-side: + +1. Authenticate with Privy +2. Build unsigned transaction +4. Sign transaction using Privy's wallet provider +5. Send signed transaction to RPC + +## What you will implement + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SPLCompressed
[**Transfer**](#full-code-examples)transferChecked()transfer()
[**Compress**](#full-code-examples)N/Acompress()
[**Decompress**](#full-code-examples)N/Adecompress()
[**Get Balance**](#get-balances)getAccount()getCompressedTokenAccountsByOwner()
[**Transaction History**](#get-transaction-history)getSignaturesForAddress()getCompressionSignaturesForOwner()
+ + + + + +### Prerequisites + + + +Connect to an RPC endpoint that supports ZK Compression (Helius, Triton) +```typescript +import { createRpc } from "@lightprotocol/stateless.js"; + +import { + transfer, + compress, + decompress, + selectMinCompressedTokenAccountsForTransfer, +} from "@lightprotocol/compressed-token"; + +const rpc = createRpc(RPC_ENDPOINT); +``` + + + + + +Before we call compress or decompresss, we need: + +* An SPL mint with a interface PDA for compression. This PDA can be created for new SPL mints via [`createMint()`](/compressed-tokens/guides/create-mint-with-token-pool) or added to existing SPL mints via [`createTokenPool()`](/compressed-tokens/guides/add-token-pools-to-mint-accounts). +* For `compress()` SPL tokens in an Associated Token Account, or +* For `decompress()` compressed token accounts with sufficient balance. + + + + + + + +## Full Code Examples + + +Find complete examples on Github: [nodejs](https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/nodejs-privy-compressed) and [react](https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/react-privy-compressed). + + + + + +Send compressed tokens to another recipient, similar to SPL token transfers: + +1. Fetch compressed token accounts with `getCompressedTokenAccountsByOwner` +2. Select sender accounts with `selectMinCompressedTokenAccountsForTransfer` +3. Fetch a validity proof from your RPC provider to prove the account's state correctness +4. Build the transfer instruction and transaction +5. Sign with Privy and send transaction + + + + + + + + + + + + + + +Convert SPL to compressed tokens and send to a recipient in one instruction. + + + + + + + + + + + + + + +Convert compressed tokens back to SPL tokens. + + + + + + + + + + + + + + + +## Get balances + +Fetch SPL and compressed token balances. + + + + + + + + + + +## Get transaction history + +Fetch compressed token transaction history for an owner, with optional detailed compression information. + + + + + + + + + + + + + + + + + + + + + + diff --git a/cspell.json b/cspell.json index 57f1a852..447b8aac 100644 --- a/cspell.json +++ b/cspell.json @@ -186,7 +186,13 @@ "Jotaro", "Yano", "jito", - "Jito" + "Jito", + "Offramp", + "offramp", + "offramps", + "zkcompression", + "Tasktool", + "clippy" ], "ignorePaths": [ "node_modules", diff --git a/docs.json b/docs.json index 3e3db011..efa855d9 100644 --- a/docs.json +++ b/docs.json @@ -1,7 +1,8 @@ { "$schema": "https://mintlify.com/docs.json", "theme": "aspen", - "name": "Lightprotocol", + "exclude": ["node_modules/**"], + "name": "Light Protocol", "colors": { "primary": "#0066FF", "light": "#0066FF", @@ -16,7 +17,7 @@ }, "seo": { "metatags": { - "keywords": "solana tokens, spl token, token 2022, light protocol, zk compression, rent free accounts on solana, scalable solana infrastructure" + "keywords": "solana tokens, rent, spl token, token 2022, light protocol, zk compression, rent-free accounts on solana, scalable solana infrastructure" } }, "navigation": { @@ -26,7 +27,9 @@ "groups": [ { "group": "Overview", - "pages": ["home"] + "pages": [ + "home" + ] } ] }, @@ -41,6 +44,13 @@ "light-token/faq" ] }, + { + "group": "DeFi", + "pages": [ + "light-token/defi/routers", + "light-token/defi/programs" + ] + }, { "group": "Toolkits", "pages": [ @@ -55,8 +65,16 @@ "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", @@ -64,8 +82,12 @@ "light-token/cookbook/mint-to", "light-token/cookbook/close-token-account", "light-token/cookbook/transfer-interface", + "light-token/cookbook/transfer-checked", "light-token/cookbook/wrap-unwrap", - "light-token/cookbook/load-ata" + "light-token/cookbook/load-ata", + "light-token/cookbook/burn", + "light-token/cookbook/freeze-thaw", + "light-token/cookbook/approve-revoke" ] } ] @@ -80,11 +102,12 @@ { "group": "SDK Reference", "pages": [ - "api-reference/libraries/stateless-js", - "api-reference/libraries/compressed-token", - "api-reference/libraries/light-client", "api-reference/libraries/light-sdk", - "api-reference/libraries/light-program-test" + "api-reference/libraries/light-token", + "api-reference/libraries/light-program-test", + "api-reference/libraries/light-client", + "api-reference/libraries/stateless-js", + "api-reference/libraries/compressed-token" ] } ] @@ -130,6 +153,7 @@ "group": "Compressed Tokens", "pages": [ "compressed-tokens/overview", + "compressed-tokens/for-privy", "compressed-tokens/advanced-guides/airdrop", { "group": "Cookbook", @@ -161,8 +185,6 @@ "group": "Examples", "expanded": true, "pages": [ - "compressed-tokens/advanced-guides/example-web-client", - "compressed-tokens/advanced-guides/example-node-js", "compressed-tokens/advanced-guides/example-token-distribution" ] } @@ -246,6 +268,7 @@ "references/whitepaper", "references/node-operators", "references/terminology", + "references/light-token-terminology", "references/migration-v1-to-v2", "support", "references/security" @@ -270,7 +293,11 @@ "display": "interactive" }, "examples": { - "languages": ["curl", "javascript", "python"], + "languages": [ + "curl", + "javascript", + "python" + ], "defaults": "all", "prefill": true } @@ -303,10 +330,12 @@ "icon": "/images/deepwiki-logo.svg", "href": { "base": "https://deepwiki.com/Lightprotocol/light-protocol", - "query": [{ - "key": "context", - "value": "$page" - }] + "query": [ + { + "key": "context", + "value": "$page" + } + ] } }, "chatgpt", @@ -325,8 +354,10 @@ "discord": "https://discord.com/invite/CYvjBgzRFP" } }, - "redirects": [{ - "source": "/landing", - "destination": "/welcome" - }] -} + "redirects": [ + { + "source": "/landing", + "destination": "/welcome" + } + ] +} \ No newline at end of file diff --git a/home.mdx b/home.mdx index 423d88a8..41a44a9f 100644 --- a/home.mdx +++ b/home.mdx @@ -58,6 +58,17 @@ import WelcomePageInstall from "/snippets/setup/welcome-page-install.mdx"; +## DeFi Integration + + + + Build rent-free AMMs and DeFi programs with minimal code changes. + + + Add support for rent-free AMMs to your aggregator or router. + + + ## Toolkits @@ -82,7 +93,7 @@ import WelcomePageInstall from "/snippets/setup/welcome-page-install.mdx"; Allow users to display and swap light-tokens. - Stream mint events from the network in real-time. - + */} - Stream token events from the network in real-time. - + */} @@ -113,7 +124,15 @@ import WelcomePageInstall from "/snippets/setup/welcome-page-install.mdx"; > ZK Compression is the most efficient way to distribute SPL tokens. - + + Add rent-free SPL token accounts and transfers to your Privy embedded wallet. + - - - Browser-based example application. - - - - Server-side Node.js implementation. -
  • Represents a unique mint and optionally can store token-metadata.
  • Functionally equivalent to SPL mints.
  • -
  • Sponsored rent exemption via custom rent-config
  • +
  • Pays the rent-exemption cost
  • @@ -36,7 +36,7 @@ import CompressibleRentExplained from "/snippets/compressible-rent-explained.mdx
    • Stores token balances of mints (SPL, Token-2022, or Light)
    • -
    • Sponsored rent exemption via custom rent-config
    • +
    • Pays the rent-exemption cost
    @@ -96,7 +96,7 @@ Light-mints **uniquely represent a token on Solana and store its global metadata here](https://github.com/Lightprotocol/light-protocol/blob/bda52c305fad28b1b72093911f878d37fb190656/program-libs/token-interface/src/state/mint/compressed_mint.rs#L19). -The `metadata` field is used by the Compressed Token Program to store the internal state of a light-mint. +The `metadata` field is used by the Light Token Program to store the internal state of a light-mint. The `BaseMint` field replicates the field layout and serialization format of [SPL Mint accounts](https://solana.com/docs/tokens#mint-account). The struct is serialized with Borsh to match the on-chain format of SPL tokens and mints. @@ -202,7 +202,7 @@ Additionally Light Token is more compute-efficient than SPL on hot paths: Diagram showing light-token Solana account structure with three components: Address (identifier for light-token account in purple box), Account (struct containing Data bytes, Executable flag, Lamports balance, and Owner set to Compressed Token Program), and AccountData (containing Mint, Owner, Amount, and Extensions fields)
    @@ -301,7 +301,7 @@ Here is how light-tokens and SPL tokens compare: ```rust let seeds = [ owner.as_ref(), // Wallet address (32 bytes) - program_id.as_ref(), // Compressed Token Program ID (32 bytes) + program_id.as_ref(), // Light Token Program ID (32 bytes) mint.as_ref(), // light-mint address (32 bytes) bump.as_ref(), // Bump seed (1 byte) ]; 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 b5970ed4..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: @@ -79,22 +78,17 @@ Compare to SPL: -1. The example creates a test light-mint. You can use existing light-mints, SPL or Token 2022 mints as well. -2. Derive the address from mint and owner pubkey. -3. Build the instruction with `CreateAssociatedTokenAccount`. It automatically includes the default rent config: - -```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`. + @@ -109,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 Compressed Token Program - -1. Pass the required accounts that include the rent config. -2. Use `invoke` or `invoke_signed`, when a CPI requires a PDA signer. - - The light-ATA address is derived from `[owner, light_token_program_id, mint]`. - Unlike light-token accounts, owner and mint are passed as accounts, not in - instruction data. - + + 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(()) -} -``` @@ -464,7 +128,7 @@ pub fn process_create_ata2_invoke_signed( {" "} + + + @@ -32,17 +48,17 @@ import InstructionCode from "/snippets/code-snippets/light-token/create-mint/ins `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,26 +94,18 @@ Compare to SPL: -The example creates a light-mint with token metadata. - -1. Derive the mint address from the mint signer and address tree -2. Fetch a validity proof from your RPC that proves the address does not exist yet. +`CreateMint` creates an on-chain mint account that can optionally include token metadata. +The instruction 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()?; -``` + @@ -110,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(()) -} -``` - @@ -729,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 + + + + +### 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/defi/programs.mdx b/light-token/defi/programs.mdx new file mode 100644 index 00000000..37821e22 --- /dev/null +++ b/light-token/defi/programs.mdx @@ -0,0 +1,551 @@ +--- +title: "Program Integration" +description: "Build high-performance DeFi programs with rent-free accounts" +--- + +The Light-SDK pays rent-exemption for your PDAs, token accounts, and mints (98% +cost savings). Your program logic stays the same. + +## What Changes + + +| Area | Change | +|------|--------| +| State struct | Derive `LightAccount` and add a `compression_info: Option` field | +| Accounts | Derive `LightAccounts` and add `#[light_account]` on init accounts | +| Program module | Add `#[light_program]` on top of `#[program]` | +| Instructions (swap, deposit, withdraw, ...) | No changes | + +Audit overhead is minimal as your program logic is mostly untouched. The rest is +macro-generated. + +If you don't use Anchor, [let us know](https://discord.com/invite/7cJ8BhAXhu). +References for native solana-program integration coming soon. + +--- + +You can find a complete rent-free AMM reference implementation [here](https://github.com/Lightprotocol/cp-swap-reference). + + +## Step 1: Dependencies + +```toml +[dependencies] + +light-sdk = { version = "0.18.0", features = ["anchor", "v2", "cpi-context"] } +light-sdk-macros = { version = "0.18.0" } +light-token = { version = "0.3.0", features = ["anchor"] } + +light-anchor-spl = { version = "0.31" } # TokenInterface uses light_token::ID +anchor-lang = "0.31" +``` + + +## Step 2: State Struct + +Add `compression_info` field and derive `LightAccount`: + +```rust +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::LightAccount; + +#[derive(Default, Debug, InitSpace, LightAccount)] +#[account] +pub struct PoolState { + /// Add this: + pub compression_info: Option, + + /// Your existing fields + /// ... +} +``` + +## Step 3: Program + +Add `#[light_program]` above `#[program]`: + +```rust +use light_sdk_macros::light_program; + +#[light_program] +#[program] +pub mod my_amm { + use super::*; + + pub fn initialize_pool(ctx: Context, params: InitializeParams) -> Result<()> { + process_initialize_pool(ctx, params) + } + + // These don't change + pub fn swap(ctx: Context, amount_in: u64, min_out: u64) -> Result<()> { + process_swap(ctx, amount_in, min_out) + } +} +``` + +## Step 4: Accounts Struct + +Derive `LightAccounts` on your `Accounts` struct and add `#[light_account(...)]` next to `#[account(...)]`. + + + +```rust +#[account( + init, + seeds = [...], + bump, + payer = creator, + space = 8 + PoolState::INIT_SPACE +)] +#[light_account(init)] +pub pool_state: Box>, +``` + + +```rust +#[account( + mut, + seeds = [b"vault", pool_state.key().as_ref(), mint.key().as_ref()], + bump, + payer = creator, + space = 8 + TokenAccount::INIT_SPACE +)] +#[light_account( + init, + token, + authority = [AUTH_SEED.as_bytes()], + mint = token_mint, + owner = authority, +)] +pub token_vault: UncheckedAccount<'info>, +``` + + + + +```rust +#[account( + mut, + seeds = [b"vault", pool_state.key().as_ref(), mint.key().as_ref()], + bump, + payer = creator, + space = 8 + TokenAccount::INIT_SPACE +)] +#[light_account( + token, + authority = [AUTH_SEED.as_bytes()], + mint = token_mint, + owner = authority, +)] +pub token_vault: UncheckedAccount<'info>, +``` +Use without `init` to create manually in the instruction handler via +`CreateTokenAccountCpi`. + + + + + +```rust +#[light_account( + init, + associated_token, + owner = creator, mint = lp_mint, + compressible = Some(compressible_params) +)] +pub creator_lp_token: UncheckedAccount<'info>, +``` + + + +```rust +#[account(mut)] +#[light_account( + init, + mint, + mint_signer = lp_mint_signer, + authority = authority, + decimals = 9, + mint_seeds = &[LP_MINT_SIGNER_SEED, pool_state.key().as_ref(), &[params.mint_signer_bump]], + authority_seeds = &[AUTH_SEED.as_bytes(), &[params.authority_bump]] +)] +pub lp_mint: UncheckedAccount<'info>, +``` + + + +```rust +#[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(), + // Optional + update_authority = authority, + additional_metadata = params.additional_metadata.clone() +)] +pub mint: UncheckedAccount<'info>, +``` + + + + + +We also need to add `light_token_interface_config`, `rent_sponsor`, and `light_token_cpi_authority`. + +```rust +use light_sdk::interface::CreateAccountsProof; +use light_sdk_macros::LightAccounts; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct InitializeParams { + pub create_accounts_proof: CreateAccountsProof, + pub lp_mint_signer_bump: u8, + pub creator_lp_token_bump: u8, + pub authority_bump: u8, +} + +#[derive(Accounts, LightAccounts)] +#[instruction(params: InitializeParams)] +pub struct InitializePool<'info> { + #[account(mut)] + pub creator: Signer<'info>, + + #[account(mut, seeds = [AUTH_SEED.as_bytes()], bump)] + pub authority: UncheckedAccount<'info>, + + #[account( + init, + seeds = [POOL_SEED.as_bytes(), token_0_mint.key().as_ref(), token_1_mint.key().as_ref()], + bump, + payer = creator, + space = 8 + PoolState::INIT_SPACE + )] + #[light_account(init)] + pub pool_state: Box>, + + pub token_0_mint: Box>, + pub token_1_mint: Box>, + + #[account(seeds = [POOL_LP_MINT_SIGNER_SEED, pool_state.key().as_ref()], bump)] + pub lp_mint_signer: UncheckedAccount<'info>, + + #[account(mut)] + #[light_account(init, mint, + mint_signer = lp_mint_signer, + authority = authority, + decimals = 9, + mint_seeds = &[POOL_LP_MINT_SIGNER_SEED, self.pool_state.to_account_info().key.as_ref(), &[params.lp_mint_signer_bump]], + authority_seeds = &[AUTH_SEED.as_bytes(), &[params.authority_bump]] + )] + pub lp_mint: UncheckedAccount<'info>, + + #[account(mut, seeds = [POOL_VAULT_SEED.as_bytes(), pool_state.key().as_ref(), token_0_mint.key().as_ref()], bump)] + #[light_account(token, authority = [AUTH_SEED.as_bytes()])] + pub token_0_vault: UncheckedAccount<'info>, + + #[account(mut, seeds = [POOL_VAULT_SEED.as_bytes(), pool_state.key().as_ref(), token_1_mint.key().as_ref()], bump)] + #[light_account(token, authority = [AUTH_SEED.as_bytes()])] + pub token_1_vault: UncheckedAccount<'info>, + + #[account(mut)] + pub creator_lp_token: UncheckedAccount<'info>, + + + pub light_interface_config: AccountInfo<'info>, + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub light_token_interface_config: AccountInfo<'info>, + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub rent_sponsor: AccountInfo<'info>, + pub light_token_program: AccountInfo<'info>, + pub light_token_cpi_authority: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} +``` + + +## Step 5: Instructions +Replace `spl_token` with `light_token` instructions as you need. The API is a superset of SPL-token so switching is straightforward. + +Examples include: `MintToCpi`, `TransferCpi`, `TransferInterfaceCpi`, +`CreateTokenAccountCpi`, and `CreateTokenAtaCpi`. + + + +```rust +use light_token::instruction::{CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi}; + +pub fn process_initialize_pool(ctx: Context, params: InitializeParams) -> Result<()> { + let pool_key = ctx.accounts.pool_state.key(); + + // Create rent-free token vault + CreateTokenAccountCpi { + payer: ctx.accounts.creator.to_account_info(), + account: ctx.accounts.token_0_vault.to_account_info(), + mint: ctx.accounts.token_0_mint.to_account_info(), + owner: ctx.accounts.authority.key(), + } + .rent_free( + ctx.accounts.light_token_interface_config.to_account_info(), + ctx.accounts.rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + &crate::ID, + ) + .invoke_signed(&[ + POOL_VAULT_SEED.as_bytes(), + pool_key.as_ref(), + ctx.accounts.token_0_mint.key().as_ref(), + &[ctx.bumps.token_0_vault], + ])?; + + // Create rent-free ATA for LP tokens + CreateTokenAtaCpi { + payer: ctx.accounts.creator.to_account_info(), + owner: ctx.accounts.creator.to_account_info(), + mint: ctx.accounts.lp_mint.to_account_info(), + ata: ctx.accounts.creator_lp_token.to_account_info(), + bump: params.creator_lp_token_bump, + } + .idempotent() + .rent_free( + ctx.accounts.light_token_interface_config.to_account_info(), + ctx.accounts.rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ) + .invoke()?; + + // Mint LP tokens (standard CPI, no changes) + MintToCpi { + mint: ctx.accounts.lp_mint.to_account_info(), + destination: ctx.accounts.creator_lp_token.to_account_info(), + amount: 1000, + authority: ctx.accounts.authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]])?; + + // Populate pool state (unchanged) + let pool = &mut ctx.accounts.pool_state; + pool.token_0_vault = ctx.accounts.token_0_vault.key(); + pool.lp_mint = ctx.accounts.lp_mint.key(); + // ... + + Ok(()) +} +``` + + + + +--- + +## Client SDK + +To make it easy for clients to integrate with your program, implement the +`LightProgramInterface` trait in your program's SDK crate. + +For a detailed example of how clients use this trait, check out the [Router Integration](./routers.mdx) page. + + + +```rust +pub struct AmmSdk { + pool_state_pubkey: Option, + token_0_vault: Option, + token_1_vault: Option, + // ... other fields + program_owned_specs: HashMap>, +} + +pub enum AmmInstruction { + Swap, + Deposit, + Withdraw, +} + +impl LightProgramInterface for AmmSdk { + type Variant = LightAccountVariant; + type Instruction = AmmInstruction; + type Error = AmmSdkError; + + fn program_id(&self) -> Pubkey { + PROGRAM_ID + } + + fn from_keyed_accounts(accounts: &[AccountInterface]) -> Result { + let mut sdk = Self::new(); + for account in accounts { + sdk.parse_account(account)?; + } + Ok(sdk) + } + + fn get_accounts_to_update(&self, ix: &Self::Instruction) -> Vec { + match ix { + AmmInstruction::Swap => vec![ + AccountToFetch::pda(self.pool_state_pubkey.unwrap(), PROGRAM_ID), + AccountToFetch::token(self.token_0_vault.unwrap()), + AccountToFetch::token(self.token_1_vault.unwrap()), + ], + // ... + } + } + + fn update(&mut self, accounts: &[AccountInterface]) -> Result<(), Self::Error> { + for account in accounts { + self.parse_account(account)?; + } + Ok(()) + } + + fn get_specs_for_instruction(&self, ix: &Self::Instruction) -> Vec> { + // Return specs for accounts needed by this instruction + // Specs include the variant (seeds) needed for loading cold accounts back onchain. + self.program_owned_specs + .values() + .cloned() + .map(AccountSpec::Pda) + .collect() + } +} +``` + + + +| Resource | Link | +|----------|------| +| Trait Implementation Example | [CpSwapSdk](https://github.com/Lightprotocol/cp-swap-reference/blob/main/programs/cp-swap/tests/program.rs#L409) | + +--- + +## Testing + + + +```rust +use light_program_test::{LightProgramTest, ProgramTestConfig, Rpc}; +use light_sdk::interface::rent::SLOTS_PER_EPOCH; +use light_client::interface::{create_load_instructions, LightProgramInterface, AccountSpec}; + +#[tokio::test] +async fn test_pool_lifecycle() { + let config = ProgramTestConfig::new_v2(true, Some(vec![("my_amm", MY_AMM_ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + + // 1. Init pool (rent-free) + // ... build and send init instruction ... + assert!(rpc.get_account_interface(&pool_address, &program_id).await.unwrap().is_some()); + + // 2. Swap (hot path - works normally) + // ... build and send swap instruction ... + + // 3. Trigger compression (advance time) + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + let pool_interface = rpc + .get_account_interface(&pool_address, &program_id) + .await + .unwrap(); + assert!(pool_interface.is_cold()); // get_account would return None + + // 5. get load instructions + let mut sdk = AmmSdk::from_keyed_accounts(&[pool_interface]).unwrap(); + let accounts_to_fetch = sdk.get_accounts_to_update(&AmmInstruction::Deposit); + let keyed_accounts = rpc.get_multiple_account_interfaces(&accounts_to_fetch).await.unwrap(); + sdk.update(&keyed_accounts).unwrap(); + + let specs = sdk.get_specs_for_instruction(&AmmInstruction::Deposit); + let load_ixs = create_load_instructions( + &specs, + payer.pubkey(), + config_pda, + payer.pubkey(), + &rpc, + ).await.unwrap(); + + // 6. send transaction + rpc.create_and_send_transaction(&load_ixs, &payer.pubkey(), &[&payer]).await.unwrap(); + assert!(rpc.get_account_interface(&pool_address, &program_id).await.unwrap().is_hot()); +} +``` + + + +| Resource | Link | +|----------|------| +| Test example | [program.rs](https://github.com/Lightprotocol/cp-swap-reference/blob/main/programs/cp-swap/tests/program.rs) | + +--- +## How it works + +The SDK pays the rent-exemption cost. After extended inactivity (e.g., multiple +epochs without any writes), cold accounts auto-compress. Your program only ever +interacts with hot accounts. Clients can load cold accounts back into the +onchain Solana account space when needed via `create_load_instructions`. + +Under the hood, clients use `AccountInterface` - a superset of Solana's +`Account` that unifies hot and cold state. See [Router Integration](./routers) +for details. + +| | Hot (active) | Cold (inactive) | +|---|---|---| +| Storage | On-chain | Compressed | +| Latency/CU | No change | +load instruction | +| Your program code | No change | No change | + +## Existing programs + +If you have an existing program that you would like to migrate to rent-free +accounts, [join our tech Discord](https://discord.com/invite/7cJ8BhAXhu) for +migration support. + + +## FAQ + + When creating an +account for the first time, the SDK provides a proof that the account doesn't +exist in the cold address space. The SVM already verifies this for the onchain +space. Both address spaces are checked before creation, preventing re-init +attacks, even if the account is currently cold. + + Miners automatically compress when +virtual rent is below threshold (e.g., multiple epochs without any writes). In +practice, cold markets should be rare. The common path (hot) has no extra +overhead and does not increase CU or txn size. + + +No. Any write bumps the virtual rent balance. Active accounts do not get compressed. + + + No. Helius and Triton run +the Interface RPC endpoints, self-hosting the Photon indexer is optional. Helius +Labs maintains the open-source Photon indexer implementation. + + +Hot markets work all the same as long as Solana is up. Cold accounts cannot be loaded into hot state until your indexer or RPC provider recovers. +Note that compression is cryptographically verifiable, so integrity and safety are not dependent on the indexer or any other external service beyond the onchain protocol. + + + +When accounts compress after extended inactivity, the on-chain rent-exemption is released back +to the rent sponsor. This creates a revolving fund: active "hot" accounts hold a +rent exempt balance temporarily, inactive "cold" accounts release it back. The +rent sponsor must be derived from the program owner. For all mint, ATA, and +token accounts, the Light Token Program is the rent sponsor. For your +program-owned PDAs, the SDK derives a rent sponsor address automatically. + + +--- + + +API is in Beta and subject to change. + +Questions or need hands-on support? [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu) + diff --git a/light-token/defi/routers.mdx b/light-token/defi/routers.mdx new file mode 100644 index 00000000..a7103338 --- /dev/null +++ b/light-token/defi/routers.mdx @@ -0,0 +1,285 @@ +--- +title: "Router Integration" +description: "Add support for rent-free AMMs on Solana." +--- + +1. Use `get_account_interface` instead of `get_account` to store `AccountInterface`. +2. Use the AMM's LightProgramInterface trait to dynamically prepend load + instructions if the market is cold. + + +## Step 1: Use `get_account_interface` + +`get_account_interface` is a new RPC endpoint that returns a superset of +`get_account`. `AccountInterface` stores additional info `Option` +that you will need later. + +
    +
    +

    Account

    +```rust +pub struct Account { + pub lamports: u64, + pub data: Vec, + pub owner: Pubkey, + pub executable: bool, + pub rent_epoch: Epoch, +} +``` +
    +
    +

    AccountInterface

    +```rust +pub struct AccountInterface { + key: Pubkey, + account: Account, + cold: Option +} +``` +
    +
    + + +## Step 2: Implement LightProgramInterface +All rent-free programs implement the `LightProgramInterface` trait. + +This trait helps you: + +1. Maintain a cache of `&[AccountInterface]` +2. Load cold accounts into the onchain account space when building Swap transactions. + +```rust +// Program implements this. +pub trait LightProgramInterface { + fn from_keyed_accounts(accounts: &[AccountInterface]) -> Result; + fn get_accounts_to_update(&self, ix: &Self::Instruction) -> Vec; + fn update(&mut self, accounts: &[AccountInterface]) -> Result<(), Self::Error>; + fn get_specs_for_instruction(&self, ix: &Self::Instruction) -> Vec>; +} +``` + +## Step 3: Load cold accounts when building Swap instructions +When building Swap instructions, prepend a `create_load_instructions` call. + +This only adds latency if accounts are cold. + +```rust +let specs = sdk.get_specs_for_instruction(&ExampleAmmSdk::LightInstruction::Swap); +if specs.iter().any(|s| s.is_cold()) { + let load_ixs = create_load_instructions( + &specs, + payer.pubkey(), + sdk.light_config_pda(), + sdk.light_rent_sponsor_pda(), + &rpc, + ).await?; + instructions.extend(load_ixs); +} +``` + + +## Full Example + +### Dependencies + +```toml +[dependencies] +light-client = {version = "0.18.0", features = ["v2"]} + +# Example Program SDK that implements LightProgramInterface (provided by AMM team) +example-amm-sdk = "0.1" +``` + +### Code + +```rust expandable +use light_client::interface::{ + create_load_instructions, LightProgramInterface, AccountSpec, +}; +use example_amm_sdk::{ExampleAmmSdk}; + +// 1. Fetch account interfaces (works for both hot and cold) +let pool_interface = rpc + .get_account_interface(&pool_address, &ExampleAmmSdk::program_id()) + .await?; + +// 2. Initialize SDK from interfaces +let mut sdk = ExampleAmmSdk::from_keyed_accounts(&[pool_interface])?; + +// 3. Fetch related accounts and update SDK state +let accounts_to_fetch = sdk.get_accounts_to_update(&ExampleAmmSdk::LightInstruction::Swap); +let keyed_accounts = rpc.get_multiple_account_interfaces(&accounts_to_fetch).await?; +sdk.update(&keyed_accounts)?; + +// 4. Quote (works same for hot or cold) +let quote = sdk.quote(amount_in, min_out)?; + +// 5. Build transaction +let mut ixs = vec![]; + +// Prepend load instructions if any accounts are cold +let specs = sdk.get_specs_for_instruction(&ExampleAmmSdk::LightInstruction::Swap); +if specs.iter().any(|s| s.is_cold()) { + let load_ixs = create_load_instructions( + &specs, + payer.pubkey(), + sdk.light_config_pda(), + sdk.light_rent_sponsor_pda(), + &rpc, + ).await?; + ixs.extend(load_ixs); +} + +// Add actual swap instruction +ixs.push(sdk.swap_ix(&swap_params)?); + +// 6. Send +rpc.send_transaction(&ixs, &payer).await?; +``` + +### Key Types + +| Type | Source | Purpose | +|------|--------|---------| +| `Rpc` trait | `light-client` | RPC client with `get_account_interface` methods | +| `AccountInterface` | `light-client` | Unified hot/cold account type | +| `LightProgramInterface` | `light-client` | Trait that program SDKs implement | +| `AccountSpec` | `light-client` | Specifies account load requirements | + +### Reference Implementation + +| Resource | Link | +|----------|------| +| AMM Program | [cp-swap-reference](https://github.com/Lightprotocol/cp-swap-reference) | +| LightProgramInterface Trait Impl | [CpSwapSdk](https://github.com/Lightprotocol/cp-swap-reference/blob/main/programs/cp-swap/tests/program.rs#L409) | +| Client Test | [program.rs](https://github.com/Lightprotocol/cp-swap-reference/blob/main/programs/cp-swap/tests/program.rs) | + +--- + +## Hot vs Cold + +| | Hot | Cold | +|---|-----|------| +| On-chain | Yes | Ledger (compressed) | +| Quote | Works | Works | +| Swap | Direct | Load first / Bundle | +| Latency | Normal | +0-200ms* | +| Tx size | Normal | +100-2400 bytes*| +| CU | Normal | +15k-400k CU*| + +Latency, tx size, and CU depend on the number and type of cold accounts. + +### When does a market go cold? +A market is "cold" after it has been compressed by a miner. Accounts become +eligible for compression only after extended inactivity (e.g., multiple epochs +without any writes), causing the virtual rent balance to fall below threshold. + +**In practice, cold markets should be rare.** The common path (hot) has no extra +overhead and does not increase CU or txn size. + +**To ensure swaps execute regardless of whether the market is hot or cold, +always call `create_load_instructions` when building the swap instructions. It +simply no-ops if the market is hot.** + +--- +## Error Handling + +```rust +use light_client::error::LightClientError; + +match rpc.get_account_interface(&pool_address, &program_id).await { + Ok(account) => { + // Account is hot or cold + // Proceed with quote and swap + } + Err(LightClientError::AccountNotFound) => { + // Account does not exist + } + Err(e) => { + // Other error + } +} +``` + +## FAQ + + +No. In all cases swap instructions stay the same. +If the market is hot (active), the transaction is identical to today (UX, CU, latency, txn size,...). +If the market is cold (inactive) you additionally prepend `create_load_instructions`. + + + Yes. `get_account_interface` is a +superset of `get_account` and returns full account state via the same `Account` +type, regardless of whether the account is hot or cold. Quoting works all the same. + + On hot path: No. On +cold path: Yes, +0-200ms for fetching validity proof. Depending on the number of +cold accounts, and the type of accounts, load instructions may exceed Solana's +current txn size limit. In this case we recommend creating Jito bundles to + reduce latency. Future updates will continue to reduce txn size and CU usage + for loading cold accounts. + + + + +In some cases, yes. You can detect this at runtime by inspecting the Instructions returned by `create_load_instructions`. + +Note that the SDK deduplicates many of the account keys over the wire, so +instructions that may appear large in isolation will be incremental when +combined with other instructions, such as Swap and Deposit. + +If load instructions + swap instructions exceed Solana's 1232 byte limit, send as a Jito bundle: + + +```rust expandable +use solana_sdk::{instruction::Instruction, pubkey::Pubkey, system_instruction}; + +const JITO_TIP_ACCOUNTS: &[&str] = &[ + "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", + "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", + "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", + "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", + "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", + "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", + "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", + "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", +]; + +fn jito_tip_ix(payer: &Pubkey, tip_lamports: u64) -> Instruction { + let tip_account = JITO_TIP_ACCOUNTS[rand::random::() % JITO_TIP_ACCOUNTS.len()] + .parse::().unwrap(); + system_instruction::transfer(payer, &tip_account, tip_lamports) +} + +// Add tip to last transaction, serialize, send to Jito +let tip_ix = jito_tip_ix(&payer.pubkey(), 10_000); // 10k lamports +swap_ixs.push(tip_ix); + +let bundle = vec![load_tx_base64, swap_tx_base64]; +let resp = client + .post("https://mainnet.block-engine.jito.wtf/api/v1/bundles") + .json(&serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "sendBundle", + "params": [bundle, {"encoding": "base64"}] + })) + .send().await?; +``` + + + +The relevant RPC methods are supported by providers such as Helius and Triton and can also be self-hosted via the open source Photon indexer. Helius Labs maintains the Photon indexer implementation. + + + +Hot markets work all the same as long as Solana is up. Cold accounts cannot be loaded into hot state until your indexer or RPC provider recovers. +Note that compression is cryptographically verifiable, so integrity and safety are not dependent on the indexer or any other external service beyond the onchain protocol. + +--- + +API is in Beta and subject to change. + +Questions or need hands-on support? [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu) + 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 7d87c794..7972d5b8 100644 --- a/light-token/toolkits/for-streaming-mints.mdx +++ b/light-token/toolkits/for-streaming-mints.mdx @@ -17,29 +17,39 @@ 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" +helius-laserstream = "0.1.5" tokio = { version = "1", features = ["full"] } futures = "0.3" -light-event = "0.2" -light-compressed-account = "0.7" -light-token-interface = "0.1" -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"] } ``` ```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::CompressedMint; -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 +81,19 @@ 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])), + })), + }], ..Default::default() }, )] @@ -89,73 +105,33 @@ 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(); + + match Mint::deserialize(&mut account_info.data.as_slice()) { + Ok(mint) => { + println!("Mint: {}", pubkey); + println!(" decimals: {}", mint.base.decimals); + println!(" supply: {}", mint.base.supply); + } + 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 = CompressedMint::try_from_slice(data)?; - - // Check if new (address not in inputs) - let is_new = output - .compressed_account - .address - .map(|addr| { - !event.input_compressed_account_hashes.iter().any(|h| *h == addr) - }) - .unwrap_or(true); - - if is_new { - println!("New mint: {}", bs58::encode(mint.metadata.mint).into_string()); } } ``` @@ -163,10 +139,10 @@ 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: &CompressedMint) -> Option<(String, String, String)> { +fn extract_metadata(mint: &Mint) -> Option<(String, String, String)> { let extensions = mint.extensions.as_ref()?; for ext in extensions { @@ -199,13 +175,16 @@ if let Some((name, symbol, uri)) = extract_metadata(&mint) { ```rust #[repr(C)] -pub struct CompressedMint { +pub struct Mint { pub base: BaseMint, - pub metadata: CompressedMintMetadata, + pub metadata: MintMetadata, + pub reserved: [u8; 16], + pub account_type: u8, + pub compression: CompressionInfo, pub extensions: Option>, } -/// SPL compatible basemint structure (82 bytes) +/// SPL-compatible base mint structure #[repr(C)] pub struct BaseMint { pub mint_authority: Option, @@ -215,12 +194,14 @@ pub struct BaseMint { pub freeze_authority: Option, } -/// metadata used by the light token program (34 bytes) +/// Light Protocol metadata for mints (67 bytes) #[repr(C)] -pub struct CompressedMintMetadata { +pub struct MintMetadata { pub version: u8, - pub spl_mint_initialized: bool, - pub mint: Pubkey, // PDA with compressed mint seed + pub mint_decompressed: bool, + pub mint: Pubkey, + pub mint_signer: [u8; 32], + pub bump: u8, } ``` diff --git a/light-token/toolkits/for-streaming-tokens.mdx b/light-token/toolkits/for-streaming-tokens.mdx index 8b181e66..193d5a8e 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 autoatically (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/welcome.mdx b/light-token/welcome.mdx index 2b3b826b..a2bbc397 100644 --- a/light-token/welcome.mdx +++ b/light-token/welcome.mdx @@ -76,6 +76,17 @@ import WelcomePageInstall from "/snippets/setup/welcome-page-install.mdx"; +## DeFi Integration + + + + Build rent-free AMMs and DeFi programs with minimal code changes. + + + Add support for rent-free AMMs to your aggregator or router. + + + ## Integration Toolkits diff --git a/openapi/getColdMint.yaml b/openapi/getColdMint.yaml new file mode 100644 index 00000000..f20bc5a5 --- /dev/null +++ b/openapi/getColdMint.yaml @@ -0,0 +1,222 @@ +openapi: 3.0.3 +info: + title: photon-indexer + description: Solana indexer for general compression + license: + name: Apache-2.0 + version: 0.50.0 +servers: +- url: https://mainnet.helius-rpc.com +paths: + /getColdMint: + summary: getColdMint + post: + requestBody: + content: + application/json: + schema: + type: object + required: + - jsonrpc + - id + - method + - params + properties: + id: + type: string + description: An ID to identify the request. + enum: + - test-account + jsonrpc: + type: string + description: The version of the JSON-RPC protocol. + enum: + - '2.0' + method: + type: string + description: The name of the method to invoke. + enum: + - getColdMint + params: + type: object + description: Request for cold mint data. Provide either address or mintPda. + properties: + address: + allOf: + - $ref: '#/components/schemas/SerializablePubkey' + nullable: true + description: The cold account address. + mintPda: + allOf: + - $ref: '#/components/schemas/SerializablePubkey' + nullable: true + description: The mint PDA (decompressed account address). + additionalProperties: false + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - context + properties: + context: + $ref: '#/components/schemas/Context' + value: + $ref: '#/components/schemas/ColdMint' + additionalProperties: false + '429': + description: Exceeded rate limit. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + description: The server encountered an unexpected condition that prevented it from fulfilling the request. + content: + application/json: + schema: + type: object + properties: + error: + type: string +components: + schemas: + ColdMint: + type: object + required: + - mint + - account + properties: + mint: + $ref: '#/components/schemas/MintData' + account: + $ref: '#/components/schemas/Account' + additionalProperties: false + MintData: + type: object + required: + - mintPda + - mintSigner + - supply + - decimals + - version + - mintDecompressed + properties: + mintPda: + $ref: '#/components/schemas/SerializablePubkey' + description: The PDA (decompressed account address) for this mint. + mintSigner: + $ref: '#/components/schemas/Hash' + description: The signer/seed used for PDA derivation. + mintAuthority: + allOf: + - $ref: '#/components/schemas/SerializablePubkey' + nullable: true + description: Authority that can mint new tokens. + freezeAuthority: + allOf: + - $ref: '#/components/schemas/SerializablePubkey' + nullable: true + description: Authority that can freeze accounts. + supply: + type: integer + format: int64 + minimum: 0 + description: Total supply of tokens. + decimals: + type: integer + minimum: 0 + maximum: 255 + description: Number of decimals. + version: + type: integer + minimum: 0 + maximum: 255 + description: Version of the mint. + mintDecompressed: + type: boolean + description: Whether the mint has been decompressed. + extensions: + allOf: + - $ref: '#/components/schemas/Base64String' + nullable: true + description: Serialized extensions. + additionalProperties: false + Account: + type: object + required: + - hash + - owner + - lamports + - tree + - leafIndex + - seq + - slotCreated + properties: + address: + $ref: '#/components/schemas/SerializablePubkey' + data: + $ref: '#/components/schemas/AccountData' + hash: + $ref: '#/components/schemas/Hash' + lamports: + $ref: '#/components/schemas/UnsignedInteger' + leafIndex: + $ref: '#/components/schemas/UnsignedInteger' + owner: + $ref: '#/components/schemas/SerializablePubkey' + seq: + $ref: '#/components/schemas/UnsignedInteger' + slotCreated: + $ref: '#/components/schemas/UnsignedInteger' + tree: + $ref: '#/components/schemas/SerializablePubkey' + additionalProperties: false + AccountData: + type: object + required: + - discriminator + - data + - dataHash + properties: + data: + $ref: '#/components/schemas/Base64String' + dataHash: + $ref: '#/components/schemas/Hash' + discriminator: + $ref: '#/components/schemas/UnsignedInteger' + additionalProperties: false + Base64String: + type: string + description: A base 64 encoded string. + default: SGVsbG8sIFdvcmxkIQ== + example: SGVsbG8sIFdvcmxkIQ== + Context: + type: object + required: + - slot + properties: + slot: + type: integer + default: 100 + example: 100 + Hash: + type: string + description: A 32-byte hash represented as a base58 string. + example: 11111112cMQwSC9qirWGjZM6gLGwW69X22mqwLLGP + SerializablePubkey: + type: string + description: A Solana public key represented as a base58 string. + default: 11111112D1oxKts8YPdTJRG5FzxTNpMtWmq8hkVx3 + example: 11111112D1oxKts8YPdTJRG5FzxTNpMtWmq8hkVx3 + UnsignedInteger: + type: integer + default: 100 + example: 100 diff --git a/openapi/getColdMintsByAuthority.yaml b/openapi/getColdMintsByAuthority.yaml new file mode 100644 index 00000000..0852abe6 --- /dev/null +++ b/openapi/getColdMintsByAuthority.yaml @@ -0,0 +1,254 @@ +openapi: 3.0.3 +info: + title: photon-indexer + description: Solana indexer for general compression + license: + name: Apache-2.0 + version: 0.50.0 +servers: +- url: https://mainnet.helius-rpc.com +paths: + /getColdMintsByAuthority: + summary: getColdMintsByAuthority + post: + requestBody: + content: + application/json: + schema: + type: object + required: + - jsonrpc + - id + - method + - params + properties: + id: + type: string + description: An ID to identify the request. + enum: + - test-account + jsonrpc: + type: string + description: The version of the JSON-RPC protocol. + enum: + - '2.0' + method: + type: string + description: The name of the method to invoke. + enum: + - getColdMintsByAuthority + params: + type: object + required: + - authority + properties: + authority: + $ref: '#/components/schemas/SerializablePubkey' + description: The authority pubkey to filter by. + authorityType: + type: string + enum: + - mintAuthority + - freezeAuthority + - either + default: either + description: Filter by authority type. Defaults to either. + cursor: + allOf: + - $ref: '#/components/schemas/Base58String' + nullable: true + limit: + allOf: + - $ref: '#/components/schemas/Limit' + nullable: true + additionalProperties: false + required: true + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - context + - value + properties: + context: + $ref: '#/components/schemas/Context' + value: + $ref: '#/components/schemas/PaginatedColdMintList' + additionalProperties: false + '429': + description: Exceeded rate limit. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + description: The server encountered an unexpected condition that prevented it from fulfilling the request. + content: + application/json: + schema: + type: object + properties: + error: + type: string +components: + schemas: + PaginatedColdMintList: + type: object + required: + - items + properties: + cursor: + $ref: '#/components/schemas/Base58String' + items: + type: array + items: + $ref: '#/components/schemas/ColdMint' + additionalProperties: false + ColdMint: + type: object + required: + - mint + - account + properties: + mint: + $ref: '#/components/schemas/MintData' + account: + $ref: '#/components/schemas/Account' + additionalProperties: false + MintData: + type: object + required: + - mintPda + - mintSigner + - supply + - decimals + - version + - mintDecompressed + properties: + mintPda: + $ref: '#/components/schemas/SerializablePubkey' + description: The PDA (decompressed account address) for this mint. + mintSigner: + $ref: '#/components/schemas/Hash' + description: The signer/seed used for PDA derivation. + mintAuthority: + allOf: + - $ref: '#/components/schemas/SerializablePubkey' + nullable: true + description: Authority that can mint new tokens. + freezeAuthority: + allOf: + - $ref: '#/components/schemas/SerializablePubkey' + nullable: true + description: Authority that can freeze accounts. + supply: + type: integer + format: int64 + minimum: 0 + description: Total supply of tokens. + decimals: + type: integer + minimum: 0 + maximum: 255 + description: Number of decimals. + version: + type: integer + minimum: 0 + maximum: 255 + description: Version of the mint. + mintDecompressed: + type: boolean + description: Whether the mint has been decompressed. + extensions: + allOf: + - $ref: '#/components/schemas/Base64String' + nullable: true + description: Serialized extensions. + additionalProperties: false + Account: + type: object + required: + - hash + - owner + - lamports + - tree + - leafIndex + - seq + - slotCreated + properties: + address: + $ref: '#/components/schemas/SerializablePubkey' + data: + $ref: '#/components/schemas/AccountData' + hash: + $ref: '#/components/schemas/Hash' + lamports: + $ref: '#/components/schemas/UnsignedInteger' + leafIndex: + $ref: '#/components/schemas/UnsignedInteger' + owner: + $ref: '#/components/schemas/SerializablePubkey' + seq: + $ref: '#/components/schemas/UnsignedInteger' + slotCreated: + $ref: '#/components/schemas/UnsignedInteger' + tree: + $ref: '#/components/schemas/SerializablePubkey' + additionalProperties: false + AccountData: + type: object + required: + - discriminator + - data + - dataHash + properties: + data: + $ref: '#/components/schemas/Base64String' + dataHash: + $ref: '#/components/schemas/Hash' + discriminator: + $ref: '#/components/schemas/UnsignedInteger' + additionalProperties: false + Base58String: + type: string + description: A base 58 encoded string. + default: 3J98t1WpEZ73CNm + example: 3J98t1WpEZ73CNm + Base64String: + type: string + description: A base 64 encoded string. + default: SGVsbG8sIFdvcmxkIQ== + example: SGVsbG8sIFdvcmxkIQ== + Context: + type: object + required: + - slot + properties: + slot: + type: integer + default: 100 + example: 100 + Hash: + type: string + description: A 32-byte hash represented as a base58 string. + example: 11111112cMQwSC9qirWGjZM6gLGwW69X22mqwLLGP + Limit: + type: integer + format: int64 + minimum: 0 + SerializablePubkey: + type: string + description: A Solana public key represented as a base58 string. + default: 11111114d3RrygbPdAtMuFnDmzsN8T5fYKVQ7FVr7 + example: 11111114d3RrygbPdAtMuFnDmzsN8T5fYKVQ7FVr7 + UnsignedInteger: + type: integer + default: 100 + example: 100 diff --git a/references/compressed-pda.md b/references/compressed-pda.md new file mode 100644 index 00000000..dd53493e --- /dev/null +++ b/references/compressed-pda.md @@ -0,0 +1,177 @@ +# Compressed PDAs + +The base library to use Compressed Accounts in Solana on-chain Rust and Anchor programs. +Compressed accounts do not require rent-exemption, which makes them suitable for: +- user owned accounts +- not config accounts which are often read +- not pool accounts, since compressed accounts cannot be used concurrently + +Compressed Accounts store state as account hashes in State Merkle trees. +and unique addresses in Address Merkle trees. +Validity proofs (zero-knowledge proofs) verify that compressed account +state exists and new addresses do not exist yet. + +- No rent exemption payment required. +- Constant 128-byte validity proof per transaction for one or multiple compressed accounts and addresses. +- Compressed account data is sent as instruction data when accessed. +- State and address trees are managed by the protocol. + +For full program examples, see the [Program Examples](https://github.com/Lightprotocol/program-examples). +For detailed documentation, visit [zkcompression.com](https://www.zkcompression.com/). +For pinocchio solana program development see [`light-sdk-pinocchio`](https://docs.rs/light-sdk-pinocchio). +For rust client development see [`light-client`](https://docs.rs/light-client). +For rust program testing see [`light-program-test`](https://docs.rs/light-program-test). +For local test validator with light system programs see [Light CLI](https://www.npmjs.com/package/@lightprotocol/zk-compression-cli). + +### Difference to Light-Accounts (Light-PDA) +Light-PDA's are Solana accounts with sponsored rent-exemption. +There is no proof required for interactions with Light-PDA's which makes +them suitable for Defi Usecases. Compressed PDA's don't require rent-exemption, +but a proof for interactions. + +## Using Compressed Accounts in Solana Programs + +1. [`Instruction`](https://docs.rs/light-sdk/latest/light_sdk/instruction/) + - `CompressedAccountMeta` - Compressed account metadata structs for instruction data. + - `PackedAccounts` - Abstraction to prepare accounts offchain for instructions with compressed accounts. + - `ValidityProof` - Proves that new addresses don't exist yet, and compressed account state exists. +2. Compressed Account in Program + - [`LightAccount`](https://docs.rs/light-sdk/latest/light_sdk/account/) - Compressed account abstraction similar to anchor Account. + - [`derive_address`](https://docs.rs/light-sdk/latest/light_sdk/address/) - Create a compressed account address. + - `LightDiscriminator` - DeriveMacro to derive a compressed account discriminator. +3. [`Cpi`](https://docs.rs/light-sdk/latest/light_sdk/cpi/) + - `CpiAccounts` - Prepare accounts to cpi the light system program. + - `LightSystemProgramCpi` - Prepare instruction data to cpi the light system program. + - [`InvokeLightSystemProgram::invoke`](https://docs.rs/light-sdk/latest/light_sdk/cpi/) - Invoke the light system program via cpi. + +## Client Program Interaction Flow + +```text + ├─ Client + │ ├─ Get ValidityProof from RPC. + │ ├─ pack accounts with PackedAccounts into PackedAddressTreeInfo and PackedStateTreeInfo. + │ ├─ pack CompressedAccountMeta. + │ ├─ Build Instruction from PackedAccounts and CompressedAccountMetas. + │ └─ Send transaction. + │ + └─ Custom Program + ├─ CpiAccounts parse accounts consistent with PackedAccounts. + ├─ LightAccount instantiates from CompressedAccountMeta. + │ + └─ Light System Program CPI + ├─ Verify ValidityProof. + ├─ Update State Merkle tree. + ├─ Update Address Merkle tree. + └─ Complete atomic state transition. +``` + +## Features + +1. `anchor` - Derives AnchorSerialize, AnchorDeserialize instead of BorshSerialize, BorshDeserialize. + +2. `v2` + - available on devnet, localnet, and light-program-test. + - Support for optimized v2 light system program instructions. + +3. `cpi-context` - Enables CPI context operations for batched compressed account operations. + - available on devnet, localnet, and light-program-test. + - Enables the use of one validity proof across multiple cpis from different programs in one instruction. + - For example spending compressed tokens (owned by the ctoken program) and updating a compressed pda (owned by a custom program) + with one validity proof. + - An instruction should not use more than one validity proof. + - Requires the v2 feature. + +## Example: Create a Compressed Account + +```rust +use anchor_lang::{prelude::*, Discriminator}; +use light_sdk::{ + account::LightAccount, + address::v1::derive_address, + cpi::{v1::LightSystemProgramCpi, CpiAccounts, InvokeLightSystemProgram, LightCpiInstruction}, + derive_light_cpi_signer, + instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo}, + CpiSigner, LightDiscriminator, LightHasher, ValidityProof, +}; + +declare_id!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); + +#[program] +pub mod counter { + + use super::*; + + pub fn create_compressed_account<'info>( + ctx: Context<'_, '_, '_, 'info, CreateCompressedAccount<'info>>, + proof: ValidityProof, + address_tree_info: PackedAddressTreeInfo, + output_tree_index: u8, + ) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.fee_payer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + )?; + + let (address, address_seed) = derive_address( + &[b"counter", ctx.accounts.fee_payer.key().as_ref()], + &address_tree_info.get_tree_pubkey(&light_cpi_accounts)?, + &crate::ID, + ); + + let mut new_account = LightAccount::<'_, CounterAccount>::new_init( + &crate::ID, + Some(address), + output_tree_index, + ); + + new_account.counter = 0; + + let light_cpi = LightSystemProgramCpi::new(light_cpi_accounts, vec![proof])?; + let instruction_data = LightCpiInstruction { + inputs: Vec::new(), + outputs: vec![new_account.to_account_info()?], + address_tree_infos: vec![address_tree_info.into()], + address_seeds: vec![address_seed], + }; + light_cpi.invoke(instruction_data)?; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateCompressedAccount<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, +} + +#[derive(Debug, LightDiscriminator, LightHasher)] +pub struct CounterAccount { + #[hash] + pub counter: u64, +} +``` + +## Guides + +| Guide | Docs | +|-------|------| +| Create compressed accounts | [create](https://www.zkcompression.com/compressed-pdas/guides/how-to-create-compressed-accounts) | +| Update compressed accounts | [update](https://www.zkcompression.com/compressed-pdas/guides/how-to-update-compressed-accounts) | +| Close compressed accounts | [close](https://www.zkcompression.com/compressed-pdas/guides/how-to-close-compressed-accounts) | +| Reinitialize accounts | [reinit](https://www.zkcompression.com/compressed-pdas/guides/how-to-reinitialize-compressed-accounts) | +| Burn accounts | [burn](https://www.zkcompression.com/compressed-pdas/guides/how-to-burn-compressed-accounts) | +| Client guide (TS + Rust) | [client](https://www.zkcompression.com/client-library/client-guide) | +| Program examples | [examples](https://www.zkcompression.com/compressed-pdas/program-examples) | + +## SDKs + +- Rust on-chain (Anchor): [`light-sdk`](https://docs.rs/light-sdk) ([crates.io](https://crates.io/crates/light-sdk)) +- Rust on-chain (Pinocchio): [`light-sdk-pinocchio`](https://docs.rs/light-sdk-pinocchio) ([crates.io](https://crates.io/crates/light-sdk-pinocchio)) +- Rust client: [`light-client`](https://docs.rs/light-client) ([crates.io](https://crates.io/crates/light-client)) +- Rust testing: [`light-program-test`](https://docs.rs/light-program-test) ([crates.io](https://crates.io/crates/light-program-test)) +- TypeScript: [`@lightprotocol/stateless.js`](https://www.npmjs.com/package/@lightprotocol/stateless.js) +- GitHub examples: [program-examples](https://github.com/Lightprotocol/program-examples) diff --git a/references/light-token-terminology.mdx b/references/light-token-terminology.mdx new file mode 100644 index 00000000..9be0bda4 --- /dev/null +++ b/references/light-token-terminology.mdx @@ -0,0 +1,146 @@ +--- +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/references/light-token.md b/references/light-token.md new file mode 100644 index 00000000..eaa38c19 --- /dev/null +++ b/references/light-token.md @@ -0,0 +1,117 @@ +# Light Token + +## Light Token SDK + +The base library to use Light Token Accounts, Light Mints, and compressed token accounts. + +### Light Token Accounts +- are on Solana devnet. +- are Solana accounts. +- can hold tokens of Light, SPL and Token 2022 mints. +- cost 17,288 lamports to create with 24 hours rent. +- are rentfree: + - rent exemption is sponsored by the token program. + - rent is 388 lamports per rent epoch (1.5 hours). + - once the account's lamports balance is insufficient, it is auto-compressed to a compressed token account. + - the accounts state is cryptographically preserved on the Solana ledger. + - compressed tokens can be decompressed to a Light Token account. + - configurable lamports per write (eg transfer) keep the Light Token account perpetually funded when used. So you don't have to worry about funding rent. + - users load a compressed account into a light account in-flight when using the account again. + +### Light Mints +- are on Solana devnet. +- are Compressed accounts. +- cost 15,000 lamports to create. +- support `TokenMetadata`. +- have the same rent-config as light token accounts + +## Program Examples + +For full program examples, see the [Light Token Examples](https://github.com/Lightprotocol/examples-light-token). + +### 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 | +|---------|-------------| +| [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 | + +## TypeScript Client + +Rust client for light-token. Each action builds, signs, and sends the transaction. + +| Action | Description | +|--------|-------------| +| `CreateMint` | Create a light-token mint with metadata | +| `CreateAta` | Create an associated light-token account | +| `MintTo` | Mint tokens to a light-token account | +| `Transfer` | Transfer light-tokens between accounts | +| `TransferChecked` | Transfer with decimal validation | +| `TransferInterface` | Transfer between light-token, T22, and SPL accounts | +| `Approve` | Approve a delegate | +| `Revoke` | Revoke a delegate | +| `Wrap` | Wrap SPL/T22 to light-token | +| `Unwrap` | Unwrap light-token to SPL/T22 | + +### TypeScript Examples + +- **create-mint** - Create a light-token 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) +- **create-ata** - Create an associated light-token account + - [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) +- **load-ata** - Load token accounts from light-token, compressed tokens, SPL/T22 to one unified balance. + - [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** - Mint tokens to a light-account + - [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) +- **transfer-interface** - Transfer between light-token, T22, and SPL accounts + - [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) +- **wrap** - Wrap SPL/T22 to light-token + - [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/wrap.ts) +- **unwrap** - Unwrap light-token to SPL/T22 + - [Action](https://github.com/Lightprotocol/examples-light-token/blob/main/typescript-client/actions/unwrap.ts) + +## Toolkits + +| Toolkit | Docs | +|---------|------| +| Payments & wallets | [for-payments](https://zkcompression.com/light-token/toolkits/for-payments) | +| Wallets | [for-wallets](https://zkcompression.com/light-token/toolkits/for-wallets) | +| Streaming tokens | [for-streaming-tokens](https://zkcompression.com/light-token/toolkits/for-streaming-tokens) | +| Streaming mints | [for-streaming-mints](https://zkcompression.com/light-token/toolkits/for-streaming-mints) | + +## SDKs + +- Rust on-chain: [`light-token`](https://docs.rs/light-token) ([crates.io](https://crates.io/crates/light-token)) +- Rust client: [`light-token-client`](https://docs.rs/light-token-client) ([crates.io](https://crates.io/crates/light-token-client)) +- GitHub examples: [examples-light-token](https://github.com/Lightprotocol/examples-light-token) + +## Disclaimer +This library is not audited and in a beta state. Use at your own risk and expect breaking changes. diff --git a/references/migration-v1-to-v2.mdx b/references/migration-v1-to-v2.mdx index 13c2aa1f..938f97e3 100644 --- a/references/migration-v1-to-v2.mdx +++ b/references/migration-v1-to-v2.mdx @@ -253,6 +253,9 @@ let new_address_params = instruction_data + + + diff --git a/references/sdk-reference.md b/references/sdk-reference.md new file mode 100644 index 00000000..22abef52 --- /dev/null +++ b/references/sdk-reference.md @@ -0,0 +1,54 @@ +# SDK Reference + +## Rust Crates + +### Program Development + +| Crate | Description | docs.rs | crates.io | +|-------|-------------|---------|-----------| +| `light-sdk` | Compressed accounts in Anchor/Rust programs | [docs.rs/light-sdk](https://docs.rs/light-sdk) | [crates.io](https://crates.io/crates/light-sdk) | +| `light-sdk-pinocchio` | Compressed accounts in native Pinocchio programs | [docs.rs/light-sdk-pinocchio](https://docs.rs/light-sdk-pinocchio) | [crates.io](https://crates.io/crates/light-sdk-pinocchio) | +| `light-token` | Light Token accounts, mints, compressed tokens (on-chain) | [docs.rs/light-token](https://docs.rs/light-token) | [crates.io](https://crates.io/crates/light-token) | +| `light-compressed-token-sdk` | Low-level SDK for compressed token operations | [docs.rs/light-compressed-token-sdk](https://docs.rs/light-compressed-token-sdk) | [crates.io](https://crates.io/crates/light-compressed-token-sdk) | + +### Client Development + +| Crate | Description | docs.rs | crates.io | +|-------|-------------|---------|-----------| +| `light-client` | Client library for compressed accounts and RPC | [docs.rs/light-client](https://docs.rs/light-client) | [crates.io](https://crates.io/crates/light-client) | +| `light-token-client` | Rust client for light-token actions | [docs.rs/light-token-client](https://docs.rs/light-token-client) | [crates.io](https://crates.io/crates/light-token-client) | + +### Testing + +| Crate | Description | docs.rs | crates.io | +|-------|-------------|---------|-----------| +| `light-program-test` | Fast local test environment (LiteSVM) | [docs.rs/light-program-test](https://docs.rs/light-program-test) | [crates.io](https://crates.io/crates/light-program-test) | + +## TypeScript Packages + +| Package | npm | TypeDocs | +|---------|-----|---------| +| `@lightprotocol/stateless.js` | [npm](https://www.npmjs.com/package/@lightprotocol/stateless.js) | [typedocs](https://lightprotocol.github.io/light-protocol/stateless.js/index.html) | +| `@lightprotocol/compressed-token` | [npm](https://www.npmjs.com/package/@lightprotocol/compressed-token) | [typedocs](https://lightprotocol.github.io/light-protocol/compressed-token/index.html) | + +## CLI + +| Tool | npm | +|------|-----| +| `@lightprotocol/zk-compression-cli` | [npm](https://www.npmjs.com/package/@lightprotocol/zk-compression-cli) | + +Install: +```bash +npm i -g @lightprotocol/zk-compression-cli +``` + +## External Resources + +| Resource | Link | +|----------|------| +| Documentation | [zkcompression.com](https://www.zkcompression.com/) | +| Light Token examples | [examples-light-token](https://github.com/Lightprotocol/examples-light-token) | +| Compressed Token examples | [examples-zk-compression](https://github.com/Lightprotocol/examples-zk-compression) | +| Program examples | [program-examples](https://github.com/Lightprotocol/program-examples) | +| MCP server | [zkcompression.com/mcp](https://www.zkcompression.com/mcp) | +| DeepWiki | [deepwiki.com/Lightprotocol/light-protocol](https://deepwiki.com/Lightprotocol/light-protocol) | diff --git a/references/terminology.mdx b/references/terminology.mdx index a1ef54f1..aad5f15d 100644 --- a/references/terminology.mdx +++ b/references/terminology.mdx @@ -60,7 +60,7 @@ SPL tokens can be compressed if the mint has a token pool account set up. Anyone ## Token Pool Account -SPL token account that holds SPL tokens corresponding to compressed tokens in circulation. Tokens are deposited during compression and withdrawn during decompression, owned by the compressed token program's CPI authority PDA. +SPL token account that holds SPL tokens corresponding to compressed tokens in circulation. Tokens are deposited during compression and withdrawn during decompression, owned by the Light Token Program's CPI authority PDA. ## Concurrency @@ -74,9 +74,9 @@ An SPL token in compressed form. Compressed tokens do not require an associated ## Compressed Token account -An account type in the Compressed Token Program to store information about an individual's ownership of a specific token (mint). Compressed token accounts do not require a rent exempt balance upon creation. +An account type in the Light Token Program to store information about an individual's ownership of a specific token (mint). Compressed token accounts do not require a rent exempt balance upon creation. -## Compressed Token Program +## Light Token Program Light Protocol's SPL-compatible token program that enables compression and decompression of token accounts. The program enforces SPL token layout standards and allows for arbitrary transitions between compressed and regular format. diff --git a/references/testing.md b/references/testing.md new file mode 100644 index 00000000..bf18a435 --- /dev/null +++ b/references/testing.md @@ -0,0 +1,249 @@ +# Light Protocol Testing + +## Routing + +| Task | Section | +|------|---------| +| Start local validator | [Local Testing](#local-testing-with-light-test-validator) | +| Test on devnet | [Devnet Testing](#devnet-testing) | +| Rust program tests | [light-program-test](#rust-program-tests-with-light-program-test) | + +## Program Addresses + +These addresses are identical on devnet and mainnet. + +| Program | Address | +|---------|---------| +| Light System | `SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7` | +| Compressed Token | `cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m` | +| Account Compression | `compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq` | +| Light Registry | `Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX` | + +## Local Testing with light-test-validator + +Local development environment running Solana test validator with Light Protocol programs, Photon indexer, and ZK prover. + +### Quick Start + +```bash +# Start all services +light test-validator + +# Stop +light test-validator --stop +``` + +### Services & Ports + +| Service | Port | Endpoint | +|---------|------|----------| +| Solana RPC | 8899 | `http://127.0.0.1:8899` | +| Solana WebSocket | 8900 | `ws://127.0.0.1:8900` | +| Photon Indexer | 8784 | `http://127.0.0.1:8784` | +| Light Prover | 3001 | `http://127.0.0.1:3001` | + +### Command Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--skip-indexer` | false | Run without Photon indexer | +| `--skip-prover` | false | Run without Light Prover | +| `--skip-system-accounts` | false | Skip pre-initialized accounts | +| `--devnet` | false | Clone programs from devnet | +| `--mainnet` | false | Clone programs from mainnet | +| `--sbf-program ` | - | Load additional program | +| `--skip-reset` | false | Keep existing ledger | +| `--verbose` | false | Enable verbose logging | + +### Deployed Programs + +| Program | Address | +|---------|---------| +| SPL Noop | `noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV` | +| Light System | `SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7` | +| Compressed Token | `cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m` | +| Account Compression | `compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq` | +| Light Registry | `Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX` | + +### TypeScript Test Integration + +```typescript +import { getTestRpc, newAccountWithLamports } from '@lightprotocol/stateless.js/test-helpers'; +import { WasmFactory } from '@lightprotocol/hasher.rs'; + +const lightWasm = await WasmFactory.getInstance(); +const rpc = await getTestRpc(lightWasm); +const payer = await newAccountWithLamports(rpc, 1e9, 256); +``` + +Run tests: + +```bash +cd js/stateless.js +pnpm test-validator && pnpm test:e2e:all +``` + +### Troubleshooting + +Validator fails to start: + +```bash +lsof -i :8899 # Check port +light test-validator --stop # Stop existing +rm -rf test-ledger/ # Reset ledger +``` + +Photon version mismatch: + +```bash +cargo install --git https://github.com/lightprotocol/photon.git \ + --rev ac7df6c388db847b7693a7a1cb766a7c9d7809b5 --locked --force +``` + +### File Locations + +| Component | Location | +|-----------|----------| +| Program binaries | `~/.config/light/bin/` | +| Prover binary | `~/.config/light/bin/prover-{platform}-{arch}` | +| Proving keys | `~/.config/light/proving-keys/` | +| Test ledger | `./test-ledger/` | + +## Devnet Testing + +### Quick Start + +```typescript +import { createRpc } from "@lightprotocol/stateless.js"; + +const connection = createRpc( + "https://devnet.helius-rpc.com?api-key=", + "https://devnet.helius-rpc.com?api-key=", + "https://devnet.helius-rpc.com?api-key=" +); +``` + +### Endpoints + +| Service | URL | +|-----------|--------------------------------------------------| +| RPC | `https://devnet.helius-rpc.com?api-key=` | +| WebSocket | `wss://devnet.helius-rpc.com?api-key=` | +| Indexer | `https://devnet.helius-rpc.com?api-key=` | +| Prover | `https://prover.helius.dev` | + +### Client Setup + +```typescript +import { Rpc, createRpc } from "@lightprotocol/stateless.js"; + +const HELIUS_API_KEY = process.env.HELIUS_API_KEY; + +const RPC_ENDPOINT = `https://devnet.helius-rpc.com?api-key=${HELIUS_API_KEY}`; +const COMPRESSION_ENDPOINT = RPC_ENDPOINT; +const PROVER_ENDPOINT = "https://prover.helius.dev"; + +const connection: Rpc = createRpc(RPC_ENDPOINT, COMPRESSION_ENDPOINT, PROVER_ENDPOINT); + +// Fetch state trees at runtime +const { stateTrees } = await connection.getCachedActiveStateTreeInfo(); +const outputStateTree = stateTrees[0].tree; +``` + +### Key Considerations + +- **Helius or Triton required**: The photon indexer implementation is maintained by Helius. You can also use Triton. Currently these RPC's provide compression endpoints +- **Runtime tree fetch**: Always fetch active state trees at runtime via `getCachedActiveStateTreeInfo()` +- **Same programs**: Program addresses are identical on devnet and mainnet +- **Devnet-specific trees**: State tree lookup tables differ from mainnet + +### Devnet Addresses + +| Lookup Table | Address | +|---------------------------------|------------------------------------------------| +| State Tree Lookup Table | `DmRueT3LMJdGj3TEprqKtfwMxyNUHDnKrQua4xrqtbmG` | +| Address Tree Lookup Table | `G4HqCAWPJ1E3JmYX1V2RZvNMuzF6gcFdbwT8FccWX6ru` | + +Usage notes: + +- Always fetch state trees dynamically using `getCachedActiveStateTreeInfo()` +- Do not hardcode tree addresses; they rotate as trees fill up +- Lookup table addresses are stable and can be referenced directly + +## Rust Program Tests with light-program-test + +A fast local test environment for Solana programs using compressed accounts and tokens. + +**Use `light-program-test` when:** +- You need fast test execution +- You write unit/integration tests for your program or client code + +**Use `solana-test-validator` when:** +- You need RPC methods or external tools that are incompatible with LiteSVM +- Testing against real validator behavior + +### Prerequisites + +1. **ZK Compression CLI**: Required to start the prover server and download Light Protocol programs + + ```bash + npm i -g @lightprotocol/zk-compression-cli + ``` + + If programs are missing after CLI installation, run `light test-validator` once to download them + +2. **Build programs**: Run `cargo test-sbf` to build program binaries and set the required + environment variables for locating program artifacts + +### Debugging + +Set `RUST_BACKTRACE=1` to show detailed transaction information including accounts and parsed instructions: + +```bash +RUST_BACKTRACE=1 cargo test-sbf -- --nocapture +``` + +## Pre-initialized Accounts + +The test validator loads 39 pre-initialized accounts from the CLI's `accounts/` directory. + +### State Trees (V1) + +| Type | Address | +|------|---------| +| Merkle tree 1 | `smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT` | +| Nullifier queue 1 | `nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148` | +| CPI context 1 | `cpi1uHzrEhBG733DoEJNgHCyRS3XmmyVNZx5fonubE4` | +| Merkle tree 2 | `smt2rJAFdyJJupwMKAqTNAJwvjhmiZ4JYGZmbVRw1Ho` | +| Nullifier queue 2 | `nfq2hgS7NYemXsFaFUCe3EMXSDSfnZnAe27jC6aPP1X` | +| CPI context 2 | `cpi2cdhkH5roePvcudTgUL8ppEBfTay1desGh8G8QxK` | + +### Batched State Trees (V2) + +Five batched state tree triplets (bmt/oq/cpi): + +| Set | BMT | OQ | CPI | +|-----|-----|-----|-----| +| 1 | `bmt1LryLZUMmF7ZtqESaw7wifBXLfXHQYoE4GAmrahU` | `oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto` | `cpi15BoVPKgEPw5o8wc2T816GE7b378nMXnhH3Xbq4y` | +| 2 | `bmt2UxoBxB9xWev4BkLvkGdapsz6sZGkzViPNph7VFi` | `oq2UkeMsJLfXt2QHzim242SUi3nvjJs8Pn7Eac9H9vg` | `cpi2yGapXUR3As5SjnHBAVvmApNiLsbeZpF3euWnW6B` | +| 3 | `bmt3ccLd4bqSVZVeCJnH1F6C8jNygAhaDfxDwePyyGb` | `oq3AxjekBWgo64gpauB6QtuZNesuv19xrhaC1ZM1THQ` | `cpi3mbwMpSX8FAGMZVP85AwxqCaQMfEk9Em1v8QK9Rf` | +| 4 | `bmt4d3p1a4YQgk9PeZv5s4DBUmbF5NxqYpk9HGjQsd8` | `oq4ypwvVGzCUMoiKKHWh4S1SgZJ9vCvKpcz6RT6A8dq` | `cpi4yyPDc4bCgHAnsenunGA8Y77j3XEDyjgfyCKgcoc` | +| 5 | `bmt5yU97jC88YXTuSukYHa8Z5Bi2ZDUtmzfkDTA2mG2` | `oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P` | `cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6` | + +### Address Trees + +| Type | Address | +|------|---------| +| Address Merkle tree (V1) | `amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2` | +| Address queue (V1) | `aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F` | +| Batch address tree (V2) | `amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx` | + +### Protocol PDAs + +| Type | Address | +|------|---------| +| Governance authority | `CuEtcKkkbTn6qy2qxqDswq5U2ADsqoipYDAYfRvxPjcp` | +| Config counter | `8gH9tmziWsS8Wc4fnoN5ax3jsSumNYoRDuSBvmH2GMH8` | +| Registered program PDA | `35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh` | +| Registered registry program PDA | `DumMsyvkaGJG4QnQ1BhTgvoRMXsgGxfpKDUCr22Xqu4w` | +| Group PDA | `24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs` | diff --git a/references/whitepaper.mdx b/references/whitepaper.mdx index b9b36c6d..70b9bb3f 100644 --- a/references/whitepaper.mdx +++ b/references/whitepaper.mdx @@ -105,9 +105,9 @@ The dataHash is what the Protocol uses to verify the integrity of program-owned **Compressed Token Accounts** -Light Protocol provides an implementation of a compressed token program built on top of ZK Compression. +Light Protocol provides an implementation of a Light Token Program built on top of ZK Compression. -The Compressed Token program enforces a token layout that is compatible with the SPL Token standard. The program also supports SPL compression and decompression; existing SPL token accounts can be compressed and decompressed arbitrarily. +The Light Token Program enforces a token layout that is compatible with the SPL Token standard. The program also supports SPL compression and decompression; existing SPL token accounts can be compressed and decompressed arbitrarily. **Fungible Compressed Accounts** diff --git a/references/zk-nullifiers.md b/references/zk-nullifiers.md new file mode 100644 index 00000000..0bfaaab8 --- /dev/null +++ b/references/zk-nullifiers.md @@ -0,0 +1,288 @@ +# ZK Nullifiers + +Uses Compressed PDAs. + +## Overview + +Building a ZK Solana program requires: +- Nullifiers to prevent double spending +- Proof verification +- A Merkle tree to store state +- An indexer to serve Merkle proofs +- Encrypted state + +## Nullifiers on Solana + +A nullifier is a deterministically derived hash to ensure an action can only be performed once. The nullifier cannot be linked to the action or user. For example Zcash uses nullifiers to prevent double spending. + +To implement nullifiers we need a data structure that ensures every nullifier is only created once and never deleted. On Solana a straight forward way to implement nullifiers is to create a PDA account with the nullifier as seed. + +PDA accounts cannot be closed and permanently lock 890,880 lamports (per nullifier rent-exemption). +Compressed PDAs are derived similar to Solana PDAs and cost 15,000 lamports to create (no rent-exemption). + +| Storage | Cost per nullifier | +|---------|-------------------| +| PDA | 890,880 lamports | +| Compressed PDA | 15,000 lamports | +## When to Use Nullifiers + +## Pattern Overview + +``` +1. Client computes nullifier = hash(secret, context) +2. Client fetches validity proof for derived address (proves it does not exist) +3. Client calls create_nullifier with nullifier values and proof +4. Program derives address from nullifier, creates compressed account via CPI +5. Light system program rejects CPI if address already exists +``` + +## Reference Implementation + +Source: [program-examples/zk/nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk/nullifier) + +### Account Structure + +```rust +#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator)] +pub struct NullifierAccount {} +``` + +Empty struct since existence alone proves the nullifier was used. + +### Address Derivation + +```rust +pub const NULLIFIER_PREFIX: &[u8] = b"nullifier"; + +let (address, address_seed) = derive_address( + &[NULLIFIER_PREFIX, nullifier.as_slice()], // seeds + &address_tree_pubkey, // address tree + &program_id, // program ID +); +``` + +Address is deterministically derived from: +- Constant prefix (prevents collisions with other account types) +- Nullifier value (32 bytes) +- Address tree pubkey +- Program ID + +### Instruction Data + +```rust +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct NullifierInstructionData { + pub proof: ValidityProof, // ZK proof that addresses don't exist + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, + pub system_accounts_offset: u8, +} +``` + +### Create Nullifiers Function + +```rust +pub fn create_nullifiers<'info>( + nullifiers: &[[u8; 32]], + data: NullifierInstructionData, + signer: &AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], +) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + signer, + &remaining_accounts[data.system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ); + + let address_tree_pubkey = data + .address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?; + + let mut cpi_builder = LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, data.proof); + let mut new_address_params: Vec = + Vec::with_capacity(nullifiers.len()); + + for (i, nullifier) in nullifiers.iter().enumerate() { + let (address, address_seed) = derive_address( + &[NULLIFIER_PREFIX, nullifier.as_slice()], + &address_tree_pubkey, + &crate::ID, + ); + + let nullifier_account = LightAccount::::new_init( + &crate::ID, + Some(address), + data.output_state_tree_index, + ); + + cpi_builder = cpi_builder.with_light_account(nullifier_account)?; + new_address_params.push( + data.address_tree_info + .into_new_address_params_assigned_packed(address_seed, Some(i as u8)), + ); + } + + cpi_builder + .with_new_addresses(&new_address_params) + .invoke(light_cpi_accounts)?; + + Ok(()) +} +``` + +### Program Entry Point + +```rust +#[program] +pub mod nullifier { + pub fn create_nullifier<'info>( + ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>, + data: NullifierInstructionData, + nullifiers: Vec<[u8; 32]>, + ) -> Result<()> { + // Verify your ZK proof here. Use nullifiers as public inputs. + // Example: + // let public_inputs = [...nullifiers, ...your_other_inputs]; + // Groth16Verifier::new(...).verify()?; + + create_nullifiers( + &nullifiers, + data, + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + ) + } +} + +#[derive(Accounts)] +pub struct CreateNullifierAccounts<'info> { + #[account(mut)] + pub signer: Signer<'info>, +} +``` + +## Client Implementation (TypeScript) + +```typescript +const NULLIFIER_PREFIX = Buffer.from("nullifier"); +const addressTree = new web3.PublicKey(batchAddressTree); + +// Derive addresses for each nullifier +const addressesWithTree = nullifiers.map((nullifier) => { + const seed = deriveAddressSeedV2([NULLIFIER_PREFIX, nullifier]); + const address = deriveAddressV2(seed, addressTree, programId); + return { tree: addressTree, queue: addressTree, address: bn(address.toBytes()) }; +}); + +// Get validity proof (proves addresses don't exist) +const proofResult = await rpc.getValidityProofV0([], addressesWithTree); + +// Build remaining accounts +const remainingAccounts = new PackedAccounts(); +remainingAccounts.addSystemAccountsV2(SystemAccountMetaConfig.new(programId)); +const addressMerkleTreeIndex = remainingAccounts.insertOrGet(addressTree); +const outputStateTreeIndex = remainingAccounts.insertOrGet(outputStateTree); + +// Build instruction data +const data = { + proof: { 0: proofResult.compressedProof }, + addressTreeInfo: { + addressMerkleTreePubkeyIndex: addressMerkleTreeIndex, + addressQueuePubkeyIndex: addressMerkleTreeIndex, + rootIndex: proofResult.rootIndices[0], + }, + outputStateTreeIndex, + systemAccountsOffset: systemStart, +}; + +// Call program +const ix = await program.methods + .createNullifier(data, nullifiers.map((n) => Array.from(n))) + .accounts({ signer: signer.publicKey }) + .remainingAccounts(remainingAccounts) + .instruction(); +``` + +## Client Implementation (Rust) + +```rust +use light_sdk::address::v2::derive_address; + +let address_tree_info = rpc.get_address_tree_v2(); + +// Derive addresses +let address_with_trees: Vec = nullifiers + .iter() + .map(|n| { + let (address, _) = derive_address( + &[NULLIFIER_PREFIX, n.as_slice()], + &address_tree_info.tree, + &program_id, + ); + AddressWithTree { + address, + tree: address_tree_info.tree, + } + }) + .collect(); + +// Get validity proof (empty hashes = non-inclusion proof) +let rpc_result = rpc + .get_validity_proof(vec![], address_with_trees, None) + .await? + .value; + +// Build accounts +let mut remaining_accounts = PackedAccounts::default(); +let config = SystemAccountMetaConfig::new(program_id); +remaining_accounts.add_system_accounts_v2(config)?; + +let packed_address_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .address_trees; + +let output_state_tree_index = rpc + .get_random_state_tree_info()? + .pack_output_tree_index(&mut remaining_accounts)?; +``` + +## How Nullifier Trees Work + +Light Protocol uses indexed Merkle trees for nullifiers (address trees). When creating a nullifier: + +1. Program derives a deterministic address from nullifier value +2. Validity proof proves this address does NOT exist in the tree +3. Light system program inserts the address into the tree +4. Future attempts fail because address now exists + +The indexed Merkle tree structure allows efficient non-inclusion proofs. Each leaf contains not just its value but also a pointer to the next-highest value, enabling proofs that a value falls between two existing values. + +## Dependencies + +```toml +[dependencies] +anchor-lang = "0.31" +light-sdk = { version = "0.14", features = ["anchor"] } +borsh = "0.10" + +[dev-dependencies] +light-program-test = "0.14" +``` + +## Testing + +```bash +# Rust tests +cargo test-sbf -p nullifier + +# TypeScript tests (requires light test-validator) +light test-validator # separate terminal +npm run test:ts +``` + +## Resources + +- Full example: [program-examples/zk/nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk/nullifier) +- ZK overview: [zkcompression.com/zk/overview](https://www.zkcompression.com/zk/overview) +- Additional ZK examples: [program-examples/zk](https://github.com/Lightprotocol/program-examples/tree/main/zk) (nullifier, zk-id, mixer, shielded-pool) diff --git a/resources/addresses-and-urls.mdx b/resources/addresses-and-urls.mdx index c04589fc..c7f3308e 100644 --- a/resources/addresses-and-urls.mdx +++ b/resources/addresses-and-urls.mdx @@ -33,7 +33,7 @@ Find all JSON RPC Methods for ZK Compression [here](/api-reference/json-rpc-meth | | | |:-|:-| | Light System Program | **SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7** | -| Compressed Token Program | **cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m** | +| Light Token Program | **cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m** | | Account Compression Program | **compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq** | ## State Trees & Queues & CPI Accounts diff --git a/resources/cli-installation.mdx b/resources/cli-installation.mdx index d80cda7c..9ce7e4fa 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@alpha +npm i -g @lightprotocol/zk-compression-cli@beta ``` 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-privy-snippets.sh b/scripts/copy-privy-snippets.sh new file mode 100755 index 00000000..3c368c54 --- /dev/null +++ b/scripts/copy-privy-snippets.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Script to copy TypeScript code from Privy examples to docs/snippets/code-snippets/privy +# Wraps each file in typescript markdown code blocks + +NODEJS_SRC="/home/tilo/Workspace/examples-zk-compression/privy/nodejs-privy-compressed/src" +REACT_SRC="/home/tilo/Workspace/examples-zk-compression/privy/react-privy-compressed/src/hooks" +SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/privy" + +# Operations to process +OPERATIONS=("transfer" "compress" "decompress" "balances" "transaction-history") + +# Function to wrap TypeScript code in markdown +wrap_typescript() { + local input_file="$1" + local output_file="$2" + echo '```typescript' > "$output_file" + cat "$input_file" >> "$output_file" + echo '```' >> "$output_file" + echo "Created: $output_file" +} + +# Create snippet directories +for operation in "${OPERATIONS[@]}"; do + mkdir -p "$SNIPPETS_DIR/$operation" +done + +# Process Node.js operations +echo "Processing Node.js operations..." +wrap_typescript "$NODEJS_SRC/transfer.ts" "$SNIPPETS_DIR/transfer/nodejs.mdx" +wrap_typescript "$NODEJS_SRC/compress.ts" "$SNIPPETS_DIR/compress/nodejs.mdx" +wrap_typescript "$NODEJS_SRC/decompress.ts" "$SNIPPETS_DIR/decompress/nodejs.mdx" +wrap_typescript "$NODEJS_SRC/balances.ts" "$SNIPPETS_DIR/balances/nodejs.mdx" +wrap_typescript "$NODEJS_SRC/get-transaction-history.ts" "$SNIPPETS_DIR/transaction-history/nodejs.mdx" + +# Process React operations (hooks) +echo "" +echo "Processing React operations..." +wrap_typescript "$REACT_SRC/useTransfer.ts" "$SNIPPETS_DIR/transfer/react.mdx" +wrap_typescript "$REACT_SRC/useCompress.ts" "$SNIPPETS_DIR/compress/react.mdx" +wrap_typescript "$REACT_SRC/useDecompress.ts" "$SNIPPETS_DIR/decompress/react.mdx" +wrap_typescript "$REACT_SRC/useCompressedBalances.ts" "$SNIPPETS_DIR/balances/react.mdx" +wrap_typescript "$REACT_SRC/useTransactionHistory.ts" "$SNIPPETS_DIR/transaction-history/react.mdx" + +echo "" +echo "Done! Created snippets in: $SNIPPETS_DIR" +echo "" +echo "Files created:" +find "$SNIPPETS_DIR" -name "*.mdx" -type f | sort 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-mint-system-accounts-list.mdx b/snippets/accounts-list/light-mint-system-accounts-list.mdx index 0002ffe0..80e96f8f 100644 --- a/snippets/accounts-list/light-mint-system-accounts-list.mdx +++ b/snippets/accounts-list/light-mint-system-accounts-list.mdx @@ -20,12 +20,12 @@ 2 CPI Authority PDA - PDA that authorizes CPIs from the Compressed Token Program to the Light System Program. + PDA that authorizes CPIs from the Light Token Program to the Light System Program. 3 Registered Program PDA - Proves the Compressed Token Program is registered to use compression. + Proves the Light Token Program is registered to CPI the Account Compression Program. 4 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/accounts-list/transfer-interface-accounts-list-light-token-1.mdx b/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx index a81db7cc..04be05cb 100644 --- a/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx +++ b/snippets/accounts-list/transfer-interface-accounts-list-light-token-1.mdx @@ -47,7 +47,7 @@ - Compressed Token Program Authority + Light Token Program Authority - The light token program authority PDA. diff --git a/snippets/ai-prompts/privy-nodejs-compressed.mdx b/snippets/ai-prompts/privy-nodejs-compressed.mdx new file mode 100644 index 00000000..4f8df269 --- /dev/null +++ b/snippets/ai-prompts/privy-nodejs-compressed.mdx @@ -0,0 +1,77 @@ +~~~~text +--- +argument-hint: +description: Add compressed token support to Privy Node.js app +allowed-tools: [Bash, Read, Glob, Grep, Task, mcp__deepwiki, mcp__zkcompression] +--- + +## Task: Add compressed token support to Privy Node.js app + +References: +- Privy Docs: https://docs.privy.io/recipes/solana/send-spl-tokens +- Privy Node Examples: + - SPL: https://github.com/privy-io/examples/tree/main/privy-node-starter + - Compressed: https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/nodejs-privy-compressed +- SPL vs Compressed comparison: https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/COMPARISON.md +- Compressed Token Guide for Privy Node.js: https://zkcompression.com/compressed-tokens/for-privy + +MCP: +* deepwiki https://mcp.deepwiki.com/mcp +* ZK Compression https://www.zkcompression.com/mcp + +## Workflow + +- This plan must execute without user intervention +- All questions have been resolved in planning phase +- If blocked, find alternative approach - do not stop +- Keep working until ALL todos are complete +- Use Task tool with subagents for parallel research or when stuck +- Use subagents with Read, Glob, Grep, and Deepwiki permissions when stuck +- Always assign Tasks to subagents and tell the user + +## DeepWiki fallback + +``` +mcp__deepwiki__read_wiki_structure("Lightprotocol/light-protocol"), +mcp__deepwiki__read_wiki_contents("Lightprotocol/light-protocol"), +mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") +``` + +## Quick Reference + +| Operation | SPL | Compressed | +| -------------- | ------------------------------ | ---------------------------------------------- | +| Transfer | transferChecked() | transfer() | +| Compress SPL to recipient | N/A | compress() | +| Decompress to SPL | N/A | decompress() | +| Get Balance | getAccount() | getCompressedTokenAccountsByOwner() | +| Tx History | getSignaturesForAddress() | getCompressionSignaturesForOwner() | + +### Phase 1: Index project + +```bash +grep -r "createTransferInstruction\|getAssociatedTokenAddress\|@solana/spl-token" src/ +``` + +### Phase 2: Add dependencies + +```bash +npm install @lightprotocol/stateless.js @lightprotocol/compressed-token +``` + +### Phase 3: Implement operations + +See Code Reference below all signed with Privy SDK. + +1. **Setup**: Connect to ZK Compression RPC (Helius, Triton) https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/index.ts +2. **Get Balance**: Fetch SPL and compressed token balances https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/balances.ts +3. **Transfer**: Send compressed tokens to another recipient, signed with Privy SDK https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/transfer.ts +4. **Compress**: Convert SPL to compressed tokens and send to a recipient in one instruction https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/compress.ts +5. **Decompress**: Convert compressed tokens back to SPL tokens for offramps https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/decompress.ts +6. **Tx History**: Fetch compressed token transaction history for an owner + +### Phase 4: Test + +Run the implemented functions. On failure, debug and retry. Assign always to subagents with Tasktool + +~~~~ \ No newline at end of file diff --git a/snippets/ai-prompts/privy-react-compressed.mdx b/snippets/ai-prompts/privy-react-compressed.mdx new file mode 100644 index 00000000..fc34cec7 --- /dev/null +++ b/snippets/ai-prompts/privy-react-compressed.mdx @@ -0,0 +1,77 @@ +~~~~text +--- +argument-hint: +description: Add compressed token support to Privy React app +allowed-tools: [Bash, Read, Glob, Grep, Task, mcp__deepwiki, mcp__zkcompression] +--- + +## Task: Add compressed token support to Privy React app + +References: +- Privy Docs: https://docs.privy.io/recipes/solana/send-spl-tokens +- Privy React Examples: + - SPL: https://github.com/privy-io/examples/tree/main/privy-react-starter + - Compressed: https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/react-privy-compressed +- SPL vs Compressed comparison: https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/COMPARISON.md +- Compressed Token Guide for Privy React: https://zkcompression.com/compressed-tokens/for-privy + +MCP: +* deepwiki https://mcp.deepwiki.com/mcp +* ZK Compression https://www.zkcompression.com/mcp + +## Workflow + +- This plan must execute without user intervention +- All questions have been resolved in planning phase +- If blocked, find alternative approach - do not stop +- Keep working until ALL todos are complete +- Use Task tool with subagents for parallel research or when stuck +- Use subagents with Read, Glob, Grep, and Deepwiki permissions when stuck +- Always assign Tasks to subagents and tell the user + +## DeepWiki fallback + +``` +mcp__deepwiki__read_wiki_structure("Lightprotocol/light-protocol"), +mcp__deepwiki__read_wiki_contents("Lightprotocol/light-protocol"), +mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") +``` + +## Quick Reference + +| Operation | SPL | Compressed | +| -------------- | ------------------------------ | ---------------------------------------------- | +| Transfer | transferChecked() | transfer() | +| Compress SPL to recipient | N/A | compress() | +| Decompress to SPL | N/A | decompress() | +| Get Balance | getAccount() | getCompressedTokenAccountsByOwner() | +| Tx History | getSignaturesForAddress() | getCompressionSignaturesForOwner() | + +### Phase 1: Index project + +```bash +grep -r "createTransferInstruction\|getAssociatedTokenAddress\|@solana/spl-token" src/ +``` + +### Phase 2: Add dependencies + +```bash +npm install @lightprotocol/stateless.js @lightprotocol/compressed-token +``` + +### Phase 3: Implement operations + +See Code Reference below all signed with Privy SDK. + +1. **Setup**: Connect to ZK Compression RPC (Helius, Triton) https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/config/rpc.ts +2. **Get Balance**: Fetch SPL and compressed token balances https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useCompressedBalances.ts +3. **Transfer**: Send compressed tokens to another recipient, signed with Privy SDK https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useTransfer.ts +4. **Compress**: Convert SPL to compressed tokens and send to a recipient in one instruction https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useCompress.ts +5. **Decompress**: Convert compressed tokens back to SPL tokens for offramps https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useDecompress.ts +6. **Tx History**: Fetch compressed token transaction history for an owner + +### Phase 4: Test + +Run the implemented functions. On failure, debug and retry. Assign always to subagents with Tasktool + +~~~~ diff --git a/snippets/ai-prompts/v1-to-v2-migration.mdx b/snippets/ai-prompts/v1-to-v2-migration.mdx index f6743d30..b24b39ce 100644 --- a/snippets/ai-prompts/v1-to-v2-migration.mdx +++ b/snippets/ai-prompts/v1-to-v2-migration.mdx @@ -1,3 +1,4 @@ +~~~~text --- argument-hint: description: Migrate Light Protocol program from v1 to v2 Merkle trees @@ -13,9 +14,9 @@ Produce a **fully working migration** that builds and tests pass. ## Available commands Via Bash tool: -- `cargo build-sbf`, `cargo test-sbf`, `cargo fmt`, `cargo clippy` -- `anchor build`, `anchor test` -- `grep`, `sed` +- **cargo build-sbf**, **cargo test-sbf**, **cargo fmt**, **cargo clippy** +- **anchor build**, **anchor test** +- **grep**, **sed** ## Documentation @@ -78,14 +79,14 @@ Add v2 feature to Cargo.toml: **Required commands (no shortcuts):** -For Anchor programs: `anchor build && anchor test` +For Anchor programs: **anchor build && anchor test** -For Native programs: `cargo build-sbf && cargo test-sbf` +For Native programs: **cargo build-sbf && cargo test-sbf** **NO shortcuts allowed:** -- Do NOT use `cargo build` (must use `cargo build-sbf`) -- Do NOT use `cargo test` (must use `cargo test-sbf`) +- Do NOT use **cargo build** (must use **cargo build-sbf**) +- Do NOT use **cargo test** (must use **cargo test-sbf**) - Tests MUST run against real BPF bytecode **On failure:** Spawn debugger agent with error context. @@ -102,4 +103,5 @@ Do NOT proceed until all tests pass. If no matching pattern in reference repos: - mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "How to migrate {pattern} from v1 to v2?") \ No newline at end of file + mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "How to migrate {pattern} from v1 to v2?") +~~~~ diff --git a/snippets/code-samples/code-compare-snippets.jsx b/snippets/code-samples/code-compare-snippets.jsx index 9fc0e28e..3e9b4e27 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/action.mdx b/snippets/code-snippets/light-token/create-ata/action.mdx index 534a5ec0..f9c189a6 100644 --- a/snippets/code-snippets/light-token/create-ata/action.mdx +++ b/snippets/code-snippets/light-token/create-ata/action.mdx @@ -10,21 +10,18 @@ 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_URL = undefined; +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 () { - // devnet: - const rpc = createRpc(RPC_URL); - // localnet: - // const rpc = createRpc(); - const { mint } = await createMintInterface(rpc, payer, payer, null, 9); const owner = Keypair.generate(); 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/action.mdx b/snippets/code-snippets/light-token/create-mint/action.mdx index 8ed788b4..dd5afae1 100644 --- a/snippets/code-snippets/light-token/create-mint/action.mdx +++ b/snippets/code-snippets/light-token/create-mint/action.mdx @@ -2,36 +2,36 @@ import "dotenv/config"; import { Keypair } from "@solana/web3.js"; import { createRpc } from "@lightprotocol/stateless.js"; -import { createMintInterface, createTokenMetadata } from "@lightprotocol/compressed-token"; +import { + createMintInterface, + createTokenMetadata, +} 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_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const RPC_URL = undefined; +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 () { - // devnet: - const rpc = createRpc(RPC_URL); - // localnet: - // const rpc = createRpc(); - const { mint, transactionSignature } = await createMintInterface( rpc, payer, payer, null, 9, - undefined, - undefined, - undefined, - createTokenMetadata("Example Token", "EXT", "https://example.com/metadata.json") + undefined, // keypair + undefined, // confirmOptions (default) + undefined, // programId (CTOKEN_PROGRAM_ID) + createTokenMetadata("Example Token", "EXT", "https://example.com/metadata.json"), ); console.log("Mint:", mint.toBase58()); 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/action.mdx b/snippets/code-snippets/light-token/load-ata/action.mdx index 7d28cd6e..7ead6f7f 100644 --- a/snippets/code-snippets/light-token/load-ata/action.mdx +++ b/snippets/code-snippets/light-token/load-ata/action.mdx @@ -1,10 +1,10 @@ ```typescript import "dotenv/config"; import { Keypair } from "@solana/web3.js"; -import { createRpc, bn } from "@lightprotocol/stateless.js"; +import { createRpc } from "@lightprotocol/stateless.js"; import { - createMint, - mintTo, + createMintInterface, + mintToCompressed, loadAta, getAssociatedTokenAddressInterface, } from "@lightprotocol/compressed-token"; @@ -12,26 +12,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_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const RPC_URL = undefined; +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 () { - // devnet: - const rpc = createRpc(RPC_URL); - // localnet: - // const rpc = createRpc(); - - // Setup: Get compressed tokens (cold storage) - 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 }]); - // Load compressed tokens to hot balance + // Load compressed tokens to light associated token account (hot balance) const lightTokenAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); const tx = await loadAta(rpc, lightTokenAta, payer, mint, payer); 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/action.mdx b/snippets/code-snippets/light-token/mint-to/action.mdx index 31a13103..5eda7883 100644 --- a/snippets/code-snippets/light-token/mint-to/action.mdx +++ b/snippets/code-snippets/light-token/mint-to/action.mdx @@ -12,28 +12,35 @@ 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_URL = undefined; +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 () { - // devnet: - const rpc = createRpc(RPC_URL); - // localnet: - // const rpc = createRpc(); - 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); - const tx = await mintToInterface(rpc, payer, mint, destination, payer, 1_000_000_000); + const destination = getAssociatedTokenAddressInterface( + mint, + recipient.publicKey, + ); + const tx = await mintToInterface( + rpc, + payer, + mint, + destination, + payer, + 1_000_000_000, + ); console.log("Tx:", tx); })(); 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/action.mdx b/snippets/code-snippets/light-token/transfer-interface/action.mdx index 8ef25782..a0e3a9f5 100644 --- a/snippets/code-snippets/light-token/transfer-interface/action.mdx +++ b/snippets/code-snippets/light-token/transfer-interface/action.mdx @@ -13,33 +13,45 @@ 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_URL = undefined; +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 () { - // devnet: - const rpc = createRpc(RPC_URL); - // localnet: - // const rpc = createRpc(); - const { mint } = await createMintInterface(rpc, payer, payer, null, 9); const sender = Keypair.generate(); await createAtaInterface(rpc, payer, mint, sender.publicKey); - const senderAta = getAssociatedTokenAddressInterface(mint, sender.publicKey); + const senderAta = getAssociatedTokenAddressInterface( + mint, + sender.publicKey, + ); await mintToInterface(rpc, payer, mint, senderAta, payer, 1_000_000_000); const recipient = Keypair.generate(); await createAtaInterface(rpc, payer, mint, recipient.publicKey); - const recipientAta = getAssociatedTokenAddressInterface(mint, recipient.publicKey); + const recipientAta = getAssociatedTokenAddressInterface( + mint, + recipient.publicKey, + ); - const tx = await transferInterface(rpc, payer, senderAta, mint, recipientAta, sender, 500_000_000); + // Transfer tokens between light-token associated token accounts + const tx = await transferInterface( + rpc, + payer, + senderAta, + mint, + recipientAta, + sender, + 500_000_000, + ); console.log("Tx:", tx); })(); 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/action.mdx b/snippets/code-snippets/light-token/unwrap/action.mdx index 47769136..c54faa64 100644 --- a/snippets/code-snippets/light-token/unwrap/action.mdx +++ b/snippets/code-snippets/light-token/unwrap/action.mdx @@ -1,41 +1,50 @@ ```typescript import "dotenv/config"; import { Keypair } from "@solana/web3.js"; -import { createRpc, bn } from "@lightprotocol/stateless.js"; -import { createMint, mintTo } from "@lightprotocol/compressed-token"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + mintToInterface, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token"; import { unwrap } from "@lightprotocol/compressed-token/unified"; -import { createAssociatedTokenAccount } from "@solana/spl-token"; +import { + createAssociatedTokenAccount, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-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_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const RPC_URL = undefined; +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 () { - // devnet: - const rpc = createRpc(RPC_URL); - // localnet: - // const rpc = createRpc(); - - // Setup: Get compressed tokens (cold storage) - const { mint } = await createMint(rpc, payer, payer.publicKey, 9); - await mintTo(rpc, payer, mint, payer.publicKey, payer, bn(1000)); + // Setup: Create and mint tokens to light-token associated token account + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const destination = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, destination, payer, 1000); - // Unwrap rent-free tokens to SPL ATA + // Unwrap light-token to SPL associated token account const splAta = await createAssociatedTokenAccount( rpc, payer, mint, - payer.publicKey + payer.publicKey, + undefined, + TOKEN_2022_PROGRAM_ID, ); - const tx = await unwrap(rpc, payer, splAta, payer, mint, bn(500)); + const tx = await unwrap(rpc, payer, splAta, payer, mint, 500); console.log("Tx:", tx); })(); diff --git a/snippets/code-snippets/light-token/unwrap/instruction.mdx b/snippets/code-snippets/light-token/unwrap/instruction.mdx index e4ed52d0..f3e39348 100644 --- a/snippets/code-snippets/light-token/unwrap/instruction.mdx +++ b/snippets/code-snippets/light-token/unwrap/instruction.mdx @@ -1,51 +1,61 @@ ```typescript import "dotenv/config"; -import { Keypair, ComputeBudgetProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; -import { createRpc, bn } from "@lightprotocol/stateless.js"; import { - createMint, - mintTo, - loadAta, + Keypair, + ComputeBudgetProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + mintToInterface, getAssociatedTokenAddressInterface, getSplInterfaceInfos, } from "@lightprotocol/compressed-token"; import { createUnwrapInstruction } from "@lightprotocol/compressed-token/unified"; -import { createAssociatedTokenAccount } from "@solana/spl-token"; +import { + createAssociatedTokenAccount, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-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); +// 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: Create and mint tokens to light-token associated token account + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const destination = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, destination, payer, 1000); - // Setup: Get compressed tokens (cold storage) - const { mint } = await createMint(rpc, payer, payer.publicKey, 9); - await mintTo(rpc, payer, mint, payer.publicKey, payer, bn(1000)); - - // Load compressed tokens to hot balance, then create unwrap instruction + // Unwrap light-token to SPL associated token account const lightTokenAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); - await loadAta(rpc, lightTokenAta, payer, mint, payer); const splAta = await createAssociatedTokenAccount( rpc, payer, mint, - payer.publicKey + payer.publicKey, + undefined, + TOKEN_2022_PROGRAM_ID, ); const splInterfaceInfos = await getSplInterfaceInfos(rpc, mint); const splInterfaceInfo = splInterfaceInfos.find( - (info) => info.isInitialized + (info) => info.isInitialized, ); if (!splInterfaceInfo) throw new Error("No SPL interface found"); @@ -55,14 +65,15 @@ const payer = Keypair.fromSecretKey( splAta, payer.publicKey, mint, - bn(500), + 500, splInterfaceInfo, - payer.publicKey + 9, // decimals - must match the mint decimals + payer.publicKey, ); const tx = new Transaction().add( ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), - ix + ix, ); const signature = await sendAndConfirmTransaction(rpc, tx, [payer]); 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/action.mdx b/snippets/code-snippets/light-token/wrap/action.mdx index 2e00d4d5..cb752dfa 100644 --- a/snippets/code-snippets/light-token/wrap/action.mdx +++ b/snippets/code-snippets/light-token/wrap/action.mdx @@ -1,51 +1,56 @@ ```typescript import "dotenv/config"; import { Keypair } from "@solana/web3.js"; -import { createRpc, bn } from "@lightprotocol/stateless.js"; +import { createRpc } from "@lightprotocol/stateless.js"; import { - createMint, - mintTo, - decompress, + createMintInterface, + createAtaInterface, + mintToInterface, + decompressInterface, wrap, getAssociatedTokenAddressInterface, createAtaInterfaceIdempotent, } from "@lightprotocol/compressed-token"; -import { createAssociatedTokenAccount } from "@solana/spl-token"; +import { + createAssociatedTokenAccount, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-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_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`; +// const rpc = createRpc(RPC_URL); // localnet: -// const RPC_URL = undefined; +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 () { - // devnet: - const rpc = createRpc(RPC_URL); - // localnet: - // const rpc = createRpc(); - // Setup: Get SPL tokens (needed to wrap) - const { mint } = await createMint(rpc, payer, payer.publicKey, 9); + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const destination = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, destination, payer, 1000); const splAta = await createAssociatedTokenAccount( rpc, payer, mint, - payer.publicKey + payer.publicKey, + undefined, + TOKEN_2022_PROGRAM_ID, ); - await mintTo(rpc, payer, mint, payer.publicKey, payer, bn(1000)); - await decompress(rpc, payer, mint, bn(1000), payer, splAta); + await decompressInterface(rpc, payer, payer, mint, 1000); - // Wrap SPL tokens to rent-free token ATA + // Wrap SPL tokens to light-token associated token account const lightTokenAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); await createAtaInterfaceIdempotent(rpc, payer, mint, payer.publicKey); - const tx = await wrap(rpc, payer, splAta, lightTokenAta, payer, mint, bn(500)); + const tx = await wrap(rpc, payer, splAta, lightTokenAta, payer, mint, 500); console.log("Tx:", tx); })(); diff --git a/snippets/code-snippets/light-token/wrap/instruction.mdx b/snippets/code-snippets/light-token/wrap/instruction.mdx index 266715dd..93e96340 100644 --- a/snippets/code-snippets/light-token/wrap/instruction.mdx +++ b/snippets/code-snippets/light-token/wrap/instruction.mdx @@ -1,52 +1,67 @@ ```typescript import "dotenv/config"; -import { Keypair, ComputeBudgetProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; -import { createRpc, bn } from "@lightprotocol/stateless.js"; import { - createMint, - mintTo, - decompress, + Keypair, + ComputeBudgetProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + mintToInterface, + decompressInterface, createWrapInstruction, getAssociatedTokenAddressInterface, createAtaInterfaceIdempotent, getSplInterfaceInfos, } from "@lightprotocol/compressed-token"; -import { createAssociatedTokenAccount } from "@solana/spl-token"; +import { + createAssociatedTokenAccount, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-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); +// 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: Get SPL tokens (needed to wrap) - const { mint } = await createMint(rpc, payer, payer.publicKey, 9); + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const destination = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, destination, payer, 1000); const splAta = await createAssociatedTokenAccount( rpc, payer, mint, - payer.publicKey + payer.publicKey, + undefined, + TOKEN_2022_PROGRAM_ID, ); - await mintTo(rpc, payer, mint, payer.publicKey, payer, bn(1000)); - await decompress(rpc, payer, mint, bn(1000), payer, splAta); + await decompressInterface(rpc, payer, payer, mint, 1000); // Create wrap instruction - const lightTokenAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); + const lightTokenAta = getAssociatedTokenAddressInterface( + mint, + payer.publicKey, + ); await createAtaInterfaceIdempotent(rpc, payer, mint, payer.publicKey); const splInterfaceInfos = await getSplInterfaceInfos(rpc, mint); const splInterfaceInfo = splInterfaceInfos.find( - (info) => info.isInitialized + (info) => info.isInitialized, ); if (!splInterfaceInfo) throw new Error("No SPL interface found"); @@ -56,14 +71,15 @@ const payer = Keypair.fromSecretKey( lightTokenAta, payer.publicKey, mint, - bn(500), + 500, splInterfaceInfo, - payer.publicKey + 9, // decimals - must match the mint decimals + payer.publicKey, ); const tx = new Transaction().add( ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), - ix + ix, ); const signature = await sendAndConfirmTransaction(rpc, tx, [payer]); 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/code-snippets/privy-react-stash/send-react.mdx b/snippets/code-snippets/privy-react-stash/send-react.mdx new file mode 100644 index 00000000..aecb83c8 --- /dev/null +++ b/snippets/code-snippets/privy-react-stash/send-react.mdx @@ -0,0 +1,33 @@ +```typescript +import { useState } from 'react'; +import { getRpc } from '../lib/rpc'; + +export function useSendTransaction() { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const sendTransaction = async (signedTransaction: Buffer): Promise => { + setIsLoading(true); + setError(null); + + try { + const rpc = getRpc(); + + const signature = await rpc.sendRawTransaction(signedTransaction, { + skipPreflight: false, + preflightCommitment: 'confirmed', + }); + + setIsLoading(false); + return signature; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + setError(errorMessage); + setIsLoading(false); + throw err; + } + }; + + return { sendTransaction, isLoading, error }; +} +``` \ No newline at end of file diff --git a/snippets/code-snippets/privy-react-stash/sign-react.mdx b/snippets/code-snippets/privy-react-stash/sign-react.mdx new file mode 100644 index 00000000..f84bb941 --- /dev/null +++ b/snippets/code-snippets/privy-react-stash/sign-react.mdx @@ -0,0 +1,44 @@ +```typescript +import { useState } from 'react'; +import { Transaction } from '@solana/web3.js'; + +export interface WalletProvider { + request: (request: { + method: string; + params: { transaction: Transaction }; + }) => Promise<{ signedTransaction: Uint8Array }>; +} + +export function useSignTransaction() { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const signTransaction = async ( + transaction: Transaction, + provider: WalletProvider + ): Promise => { + setIsLoading(true); + setError(null); + + try { + const result = await provider.request({ + method: 'signTransaction', + params: { transaction }, + }); + + const signedTx = Transaction.from(result.signedTransaction); + const serialized = signedTx.serialize(); + + setIsLoading(false); + return Buffer.from(serialized); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + setError(errorMessage); + setIsLoading(false); + throw err; + } + }; + + return { signTransaction, isLoading, error }; +} +``` \ No newline at end of file diff --git a/snippets/code-snippets/privy/balances/nodejs.mdx b/snippets/code-snippets/privy/balances/nodejs.mdx new file mode 100644 index 00000000..ebf6e51d --- /dev/null +++ b/snippets/code-snippets/privy/balances/nodejs.mdx @@ -0,0 +1,38 @@ +```typescript +import {PublicKey} from '@solana/web3.js'; +import {createRpc} from '@lightprotocol/stateless.js'; +import {HELIUS_RPC_URL} from './config.js'; + +export async function getCompressedBalances(ownerAddress: string) { + const rpc = createRpc(HELIUS_RPC_URL); + const owner = new PublicKey(ownerAddress); + + // Get compressed SOL balance + const compressedSol = await rpc.getCompressedBalanceByOwner(owner); + const compressedSolLamports = compressedSol.value ? BigInt(compressedSol.value.toString()) : 0n; + + // Get compressed token accounts (filter out null items from indexer) + const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(owner); + const validItems = (compressedAccounts.items || []).filter((item): item is NonNullable => item !== null); + + // Aggregate balances by mint + const balances = new Map(); + for (const account of validItems) { + if (account.parsed) { + const mint = account.parsed.mint.toBase58(); + const amount = BigInt(account.parsed.amount.toString()); + const current = balances.get(mint) || 0n; + balances.set(mint, current + amount); + } + } + + return { + sol: compressedSolLamports.toString(), + tokens: Array.from(balances.entries()).map(([mint, amount]) => ({ + mint, + amount: amount.toString(), + accounts: validItems.filter(a => a.parsed?.mint.toBase58() === mint).length + })) + }; +} +``` diff --git a/snippets/code-snippets/privy/balances/react.mdx b/snippets/code-snippets/privy/balances/react.mdx new file mode 100644 index 00000000..cd6310f8 --- /dev/null +++ b/snippets/code-snippets/privy/balances/react.mdx @@ -0,0 +1,174 @@ +```typescript +import { useState, useCallback } from 'react'; +import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'; +import { getAccount, getAssociatedTokenAddressSync } from '@solana/spl-token'; +import { createRpc } from '@lightprotocol/stateless.js'; + +const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; +const TEST_MINT = '7cT3PeXyDLkyEcvr9YzsjGLuZneKsea4c8hPbJQEjMCZ'; + +export interface TokenBalance { + mint: string; + amount: string; + decimals: number; + isCompressed: boolean; + isNative: boolean; +} + +export function useCompressedBalances() { + const [balances, setBalances] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const fetchBalances = useCallback(async (ownerAddress: string) => { + if (!ownerAddress) return; + + setIsLoading(true); + try { + const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL); + const owner = new PublicKey(ownerAddress); + const allBalances: TokenBalance[] = []; + + // Get compressed SOL balance + try { + const compressedSol = await rpc.getCompressedBalanceByOwner(owner); + if (compressedSol && BigInt(compressedSol.toString()) > 0n) { + allBalances.push({ + mint: 'So11111111111111111111111111111111111111112', + amount: compressedSol.toString(), + decimals: 9, + isCompressed: true, + isNative: true, + }); + } + } catch { + // No compressed SOL + } + + // Get regular SOL balance + try { + const solBalance = await rpc.getBalance(owner); + allBalances.push({ + mint: 'So11111111111111111111111111111111111111112', + amount: solBalance.toString(), + decimals: 9, + isCompressed: false, + isNative: true, + }); + } catch { + // Failed to get SOL balance + } + + // Get regular SPL token accounts + try { + const tokenAccounts = await rpc.getTokenAccountsByOwner(owner, { + programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + }); + + for (const { account, pubkey } of tokenAccounts.value) { + const data = account.data; + if (data instanceof Buffer || (typeof data === 'object' && 'length' in data)) { + // Parse token account data + const dataBuffer = Buffer.from(data as Uint8Array); + if (dataBuffer.length >= 72) { + const mint = new PublicKey(dataBuffer.subarray(0, 32)); + const amount = dataBuffer.readBigUInt64LE(64); + + // Get mint info for decimals + try { + const mintInfo = await rpc.getAccountInfo(mint); + const decimals = mintInfo?.data ? (mintInfo.data as Buffer)[44] || 6 : 6; + + allBalances.push({ + mint: mint.toBase58(), + amount: amount.toString(), + decimals, + isCompressed: false, + isNative: false, + }); + } catch { + allBalances.push({ + mint: mint.toBase58(), + amount: amount.toString(), + decimals: 6, + isCompressed: false, + isNative: false, + }); + } + } + } + } + } catch { + // Failed to get token accounts + } + + // Get compressed token accounts + try { + const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(owner); + + if (compressedAccounts?.items) { + const mintAmounts = new Map(); + const mintDecimals = new Map(); + + for (const item of compressedAccounts.items) { + if (item?.parsed?.mint && item?.parsed?.amount) { + const mint = item.parsed.mint.toBase58(); + const amount = BigInt(item.parsed.amount.toString()); + const current = mintAmounts.get(mint) || 0n; + mintAmounts.set(mint, current + amount); + + // Store decimals (assume 6 for now, could fetch from mint) + if (!mintDecimals.has(mint)) { + mintDecimals.set(mint, 6); + } + } + } + + for (const [mint, amount] of mintAmounts) { + allBalances.push({ + mint, + amount: amount.toString(), + decimals: mintDecimals.get(mint) || 6, + isCompressed: true, + isNative: false, + }); + } + } + } catch { + // Failed to get compressed accounts + } + + // Add fallback tokens with 0 balance if not present + const hasMint = (mint: string) => allBalances.some(b => b.mint === mint); + + if (!hasMint(USDC_MINT)) { + allBalances.push({ + mint: USDC_MINT, + amount: '0', + decimals: 6, + isCompressed: false, + isNative: false, + }); + } + + if (!hasMint(TEST_MINT)) { + allBalances.push({ + mint: TEST_MINT, + amount: '0', + decimals: 6, + isCompressed: false, + isNative: false, + }); + } + + setBalances(allBalances); + } catch (error) { + console.error('Failed to fetch balances:', error); + setBalances([]); + } finally { + setIsLoading(false); + } + }, []); + + return { balances, isLoading, fetchBalances }; +} +``` diff --git a/snippets/code-snippets/privy/compress/nodejs.mdx b/snippets/code-snippets/privy/compress/nodejs.mdx new file mode 100644 index 00000000..978c51c4 --- /dev/null +++ b/snippets/code-snippets/privy/compress/nodejs.mdx @@ -0,0 +1,89 @@ +```typescript +import 'dotenv/config'; +import {PrivyClient} from '@privy-io/node'; +import {createRpc, bn, selectStateTreeInfo} from '@lightprotocol/stateless.js'; +import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js'; +import {getAssociatedTokenAddressSync, getAccount} from '@solana/spl-token'; +import {CompressedTokenProgram, getTokenPoolInfos, selectTokenPoolInfo} from '@lightprotocol/compressed-token'; + +const compressTokens = async ( + fromAddress: string, + toAddress: string, + tokenMintAddress: string, + amount: number, + decimals: number = 6 +) => { + const connection = createRpc(process.env.HELIUS_RPC_URL!); + + const privy = new PrivyClient({ + appId: process.env.PRIVY_APP_ID!, + appSecret: process.env.PRIVY_APP_SECRET!, + }); + + // Create public key objects + const fromPubkey = new PublicKey(fromAddress); + const toPubkey = new PublicKey(toAddress); + const mintPubkey = new PublicKey(tokenMintAddress); + const tokenAmount = bn(amount * Math.pow(10, decimals)); + + // Get source token account and verify balance + const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey); + const ataAccount = await getAccount(connection, ownerAta); + if (ataAccount.amount < BigInt(tokenAmount.toString())) { + throw new Error('Insufficient SPL balance'); + } + + // Get state tree to store compressed tokens + // Get token pool info. Stores SPL tokens in interface PDA when compressed. + const stateTreeInfos = await connection.getStateTreeInfos(); + const selectedTreeInfo = selectStateTreeInfo(stateTreeInfos); + const tokenPoolInfos = await getTokenPoolInfos(connection, mintPubkey); + const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos); + + // Create compress instruction + const instruction = await CompressedTokenProgram.compress({ + payer: fromPubkey, + owner: fromPubkey, + source: ownerAta, + toAddress: toPubkey, + mint: mintPubkey, + amount: tokenAmount, + outputStateTreeInfo: selectedTreeInfo, + tokenPoolInfo, + }); + + // Create transaction + const transaction = new Transaction(); + transaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: 300_000})); + transaction.add(instruction); + + // Get recent blockhash + const {blockhash} = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = fromPubkey; + + // Sign with Privy + const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, { + transaction: transaction.serialize({requireAllSignatures: false}), + authorization_context: { + authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!] + } + }); + const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction; + if (!signedTx) { + throw new Error('Privy returned invalid response: ' + JSON.stringify(signResult)); + } + const signedTransaction = Buffer.from(signedTx, 'base64'); + + // Send transaction + const signature = await connection.sendRawTransaction(signedTransaction, { + skipPreflight: false, + preflightCommitment: 'confirmed' + }); + await connection.confirmTransaction(signature, 'confirmed'); + + return signature; +}; + +export default compressTokens; +``` diff --git a/snippets/code-snippets/privy/compress/react.mdx b/snippets/code-snippets/privy/compress/react.mdx new file mode 100644 index 00000000..029bd9fa --- /dev/null +++ b/snippets/code-snippets/privy/compress/react.mdx @@ -0,0 +1,106 @@ +```typescript +import { useState } from 'react'; +import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js'; +import { getAssociatedTokenAddressSync, getAccount } from '@solana/spl-token'; +import { bn, createRpc, selectStateTreeInfo } from '@lightprotocol/stateless.js'; +import { CompressedTokenProgram, getTokenPoolInfos, selectTokenPoolInfo } from '@lightprotocol/compressed-token'; +import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core'; +import type { SignTransactionResult } from '@privy-io/react-auth/solana'; + +export interface CompressParams { + ownerPublicKey: string; + toAddress: string; + mint: string; + amount: number; + decimals?: number; +} + +export interface CompressArgs { + params: CompressParams; + wallet: ConnectedStandardSolanaWallet; + signTransaction: (args: { + transaction: Buffer; + wallet: ConnectedStandardSolanaWallet; + chain: string; + }) => Promise; +} + +export function useCompress() { + const [isLoading, setIsLoading] = useState(false); + + const compress = async (args: CompressArgs): Promise => { + setIsLoading(true); + + try { + const { params, wallet, signTransaction } = args; + const { ownerPublicKey, toAddress, mint, amount, decimals = 6 } = params; + + // Create RPC connection + const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL); + + // Create public key objects + const ownerPubkey = new PublicKey(ownerPublicKey); + const toPubkey = new PublicKey(toAddress); + const mintPubkey = new PublicKey(mint); + const tokenAmount = bn(amount * Math.pow(10, decimals)); + + // Get source token account and verify balance + const ownerAta = getAssociatedTokenAddressSync(mintPubkey, ownerPubkey); + const ataAccount = await getAccount(rpc, ownerAta); + if (ataAccount.amount < BigInt(tokenAmount.toString())) { + throw new Error('Insufficient SPL balance'); + } + + // Get state tree to store compressed tokens + // Get token pool info. Stores SPL tokens in interface PDA when compressed. + const stateTreeInfos = await rpc.getStateTreeInfos(); + const selectedTreeInfo = selectStateTreeInfo(stateTreeInfos); + const tokenPoolInfos = await getTokenPoolInfos(rpc, mintPubkey); + const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos); + + // Create compress instruction + const instruction = await CompressedTokenProgram.compress({ + payer: ownerPubkey, + owner: ownerPubkey, + source: ownerAta, + toAddress: toPubkey, + mint: mintPubkey, + amount: tokenAmount, + outputStateTreeInfo: selectedTreeInfo, + tokenPoolInfo, + }); + + // Build transaction + const { blockhash } = await rpc.getLatestBlockhash(); + const transaction = new Transaction(); + transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 })); + transaction.add(instruction); + transaction.recentBlockhash = blockhash; + transaction.feePayer = ownerPubkey; + + // Serialize unsigned transaction + const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false }); + + // Sign with Privy + const signedTx = await signTransaction({ + transaction: unsignedTxBuffer, + wallet, + chain: 'solana:devnet', + }); + + // Send transaction + const signedTxBuffer = Buffer.from(signedTx.signedTransaction); + const signature = await rpc.sendRawTransaction(signedTxBuffer, { + skipPreflight: false, + preflightCommitment: 'confirmed', + }); + + return signature; + } finally { + setIsLoading(false); + } + }; + + return { compress, isLoading }; +} +``` diff --git a/snippets/code-snippets/privy/decompress/nodejs.mdx b/snippets/code-snippets/privy/decompress/nodejs.mdx new file mode 100644 index 00000000..6f722014 --- /dev/null +++ b/snippets/code-snippets/privy/decompress/nodejs.mdx @@ -0,0 +1,81 @@ +```typescript +import 'dotenv/config'; +import {PrivyClient} from '@privy-io/node'; +import {createRpc} from '@lightprotocol/stateless.js'; +import {Keypair, PublicKey, Transaction} from '@solana/web3.js'; +import {getAssociatedTokenAddressSync, createAssociatedTokenAccount} from '@solana/spl-token'; +import {decompress} from '@lightprotocol/compressed-token'; + +const decompressTokens = async ( + fromAddress: string, + tokenMintAddress: string, + amount: number, + decimals: number = 6 +) => { + const connection = createRpc(process.env.HELIUS_RPC_URL!); + + const privy = new PrivyClient({ + appId: process.env.PRIVY_APP_ID!, + appSecret: process.env.PRIVY_APP_SECRET!, + }); + + const fromPubkey = new PublicKey(fromAddress); + const mintPubkey = new PublicKey(tokenMintAddress); + const rawAmount = amount * Math.pow(10, decimals); + + // Get destination ATA + const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey); + + // Check ATA exists (decompress action will handle creation internally) + // But we need to be aware ATA creation requires a separate signer + + // Create fake keypair for decompress action (only publicKey is used) + const dummyPayer = { + publicKey: fromPubkey, + secretKey: new Uint8Array(64), + } as any; + + // Intercept sendAndConfirmTransaction to use Privy signing + const originalSendAndConfirm = (connection as any).sendAndConfirmTransaction; + (connection as any).sendAndConfirmTransaction = async (tx: Transaction, signers: any[]) => { + const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, { + transaction: tx.serialize({requireAllSignatures: false}), + authorization_context: { + authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!] + } + }); + + const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction; + if (!signedTx) { + throw new Error('Privy returned invalid response'); + } + + const signedTransaction = Buffer.from(signedTx, 'base64'); + const signature = await connection.sendRawTransaction(signedTransaction, { + skipPreflight: false, + preflightCommitment: 'confirmed' + }); + await connection.confirmTransaction(signature, 'confirmed'); + return signature; + }; + + try { + // Use high-level decompress action (handles account configuration correctly) + const signature = await decompress( + connection, + dummyPayer, + mintPubkey, + rawAmount, + dummyPayer, + ownerAta + ); + + return signature; + } finally { + // Restore original function + (connection as any).sendAndConfirmTransaction = originalSendAndConfirm; + } +}; + +export default decompressTokens; +``` diff --git a/snippets/code-snippets/privy/decompress/react.mdx b/snippets/code-snippets/privy/decompress/react.mdx new file mode 100644 index 00000000..2ea96fc8 --- /dev/null +++ b/snippets/code-snippets/privy/decompress/react.mdx @@ -0,0 +1,124 @@ +```typescript +import { useState } from 'react'; +import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js'; +import { getAssociatedTokenAddressSync, createAssociatedTokenAccountInstruction, getAccount, TokenAccountNotFoundError } from '@solana/spl-token'; +import { CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer, getSplInterfaceInfos, selectSplInterfaceInfosForDecompression } from '@lightprotocol/compressed-token'; +import { bn, createRpc } from '@lightprotocol/stateless.js'; +import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core'; +import type { SignTransactionResult } from '@privy-io/react-auth/solana'; + +export interface DecompressParams { + ownerPublicKey: string; + mint: string; + amount: number; + decimals?: number; +} + +export interface DecompressArgs { + params: DecompressParams; + wallet: ConnectedStandardSolanaWallet; + signTransaction: (args: { + transaction: Buffer; + wallet: ConnectedStandardSolanaWallet; + chain: string; + }) => Promise; +} + +export function useDecompress() { + const [isLoading, setIsLoading] = useState(false); + + const decompress = async (args: DecompressArgs): Promise => { + setIsLoading(true); + + try { + const { params, wallet, signTransaction } = args; + const { ownerPublicKey, mint, amount, decimals = 6 } = params; + + // Create RPC connection + const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL); + + const owner = new PublicKey(ownerPublicKey); + const mintPubkey = new PublicKey(mint); + const tokenAmount = bn(Math.floor(amount * Math.pow(10, decimals))); + + // Get destination ATA + const destinationAta = getAssociatedTokenAddressSync(mintPubkey, owner); + + // Get compressed token accounts + const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint: mintPubkey }); + if (!accounts.items || accounts.items.length === 0) { + throw new Error('No compressed token accounts found'); + } + + // Select minimum accounts needed + const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(accounts.items, tokenAmount); + if (inputAccounts.length === 0) { + throw new Error('Insufficient compressed balance'); + } + + // Get validity proof and token pool info + const proof = await rpc.getValidityProof( + inputAccounts.map((account) => bn(account.compressedAccount.hash)) + ); + const splInterfaceInfos = await getSplInterfaceInfos(rpc, mintPubkey); + const tokenPoolInfos = selectSplInterfaceInfosForDecompression(splInterfaceInfos, tokenAmount); + + // Build transaction + const { blockhash } = await rpc.getLatestBlockhash(); + const transaction = new Transaction(); + transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 })); + + // Create ATA if needed + try { + await getAccount(rpc, destinationAta); + } catch (e) { + if (e instanceof TokenAccountNotFoundError) { + transaction.add( + createAssociatedTokenAccountInstruction(owner, destinationAta, owner, mintPubkey) + ); + } else { + throw e; + } + } + + // Build decompress instruction + const decompressIx = await CompressedTokenProgram.decompress({ + payer: owner, + inputCompressedTokenAccounts: inputAccounts, + toAddress: destinationAta, + amount: tokenAmount, + recentInputStateRootIndices: proof.rootIndices, + recentValidityProof: proof.compressedProof, + tokenPoolInfos, + }); + + transaction.add(decompressIx); + transaction.recentBlockhash = blockhash; + transaction.feePayer = owner; + + // Serialize unsigned transaction + const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false }); + + // Sign with Privy + const signedTx = await signTransaction({ + transaction: unsignedTxBuffer, + wallet, + chain: 'solana:devnet', + }); + + // Send transaction + const signedTxBuffer = Buffer.from(signedTx.signedTransaction); + const signature = await rpc.sendRawTransaction(signedTxBuffer, { + skipPreflight: false, + preflightCommitment: 'confirmed', + }); + + return signature; + } finally { + setIsLoading(false); + } + }; + + return { decompress, isLoading }; +} +``` diff --git a/snippets/code-snippets/privy/transaction-history/nodejs.mdx b/snippets/code-snippets/privy/transaction-history/nodejs.mdx new file mode 100644 index 00000000..9d58855f --- /dev/null +++ b/snippets/code-snippets/privy/transaction-history/nodejs.mdx @@ -0,0 +1,68 @@ +```typescript +import 'dotenv/config'; +import {createRpc} from '@lightprotocol/stateless.js'; +import {PublicKey} from '@solana/web3.js'; + +const getTransactionHistory = async ( + ownerAddress: string, + limit: number = 10, + includeDetails: boolean = false +) => { + const connection = createRpc(process.env.HELIUS_RPC_URL!); + + const owner = new PublicKey(ownerAddress); + + // Get compression signatures for token owner + const signatures = await connection.getCompressionSignaturesForTokenOwner(owner); + + if (signatures.items.length === 0) { + return { + count: 0, + transactions: [], + }; + } + + // Limit results + const limitedSignatures = signatures.items.slice(0, limit); + + // Get detailed info if requested + if (includeDetails && limitedSignatures.length > 0) { + const transactions = await Promise.all( + limitedSignatures.map(async (sig) => { + const txInfo = await connection.getTransactionWithCompressionInfo(sig.signature); + + return { + signature: sig.signature, + slot: sig.slot, + blockTime: sig.blockTime, + timestamp: new Date(sig.blockTime * 1000).toISOString(), + compressionInfo: txInfo?.compressionInfo ? { + closedAccounts: txInfo.compressionInfo.closedAccounts.length, + openedAccounts: txInfo.compressionInfo.openedAccounts.length, + } : null, + }; + }) + ); + + return { + count: signatures.items.length, + transactions, + }; + } + + // Return basic signature info + const transactions = limitedSignatures.map((sig) => ({ + signature: sig.signature, + slot: sig.slot, + blockTime: sig.blockTime, + timestamp: new Date(sig.blockTime * 1000).toISOString(), + })); + + return { + count: signatures.items.length, + transactions, + }; +}; + +export default getTransactionHistory; +``` diff --git a/snippets/code-snippets/privy/transaction-history/react.mdx b/snippets/code-snippets/privy/transaction-history/react.mdx new file mode 100644 index 00000000..b7fe5611 --- /dev/null +++ b/snippets/code-snippets/privy/transaction-history/react.mdx @@ -0,0 +1,96 @@ +```typescript +import { useState, useCallback } from 'react'; +import { PublicKey } from '@solana/web3.js'; +import { createRpc } from '@lightprotocol/stateless.js'; + +export interface TransactionCompressionInfo { + closedAccounts: number; + openedAccounts: number; +} + +export interface Transaction { + signature: string; + slot: number; + blockTime: number; + timestamp: string; + compressionInfo?: TransactionCompressionInfo | null; +} + +export function useTransactionHistory() { + const [transactions, setTransactions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTransactionHistory = useCallback( + async ( + ownerAddress: string, + limit: number = 10, + includeDetails: boolean = false + ) => { + if (!ownerAddress) { + setTransactions([]); + return; + } + + setIsLoading(true); + setError(null); + + try { + const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL); + const owner = new PublicKey(ownerAddress); + + const signatures = await rpc.getCompressionSignaturesForTokenOwner(owner); + + if (signatures.items.length === 0) { + setTransactions([]); + return; + } + + const limitedSignatures = signatures.items.slice(0, limit); + + if (includeDetails && limitedSignatures.length > 0) { + const transactionsWithDetails = await Promise.all( + limitedSignatures.map(async (sig) => { + const txInfo = await rpc.getTransactionWithCompressionInfo(sig.signature); + + return { + signature: sig.signature, + slot: sig.slot, + blockTime: sig.blockTime, + timestamp: new Date(sig.blockTime * 1000).toISOString(), + compressionInfo: txInfo?.compressionInfo + ? { + closedAccounts: txInfo.compressionInfo.closedAccounts.length, + openedAccounts: txInfo.compressionInfo.openedAccounts.length, + } + : null, + }; + }) + ); + + setTransactions(transactionsWithDetails); + return; + } + + const basicTransactions = limitedSignatures.map((sig) => ({ + signature: sig.signature, + slot: sig.slot, + blockTime: sig.blockTime, + timestamp: new Date(sig.blockTime * 1000).toISOString(), + })); + + setTransactions(basicTransactions); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + setTransactions([]); + } finally { + setIsLoading(false); + } + }, + [] + ); + + return { transactions, isLoading, error, fetchTransactionHistory }; +} +``` diff --git a/snippets/code-snippets/privy/transfer/nodejs.mdx b/snippets/code-snippets/privy/transfer/nodejs.mdx new file mode 100644 index 00000000..17820e87 --- /dev/null +++ b/snippets/code-snippets/privy/transfer/nodejs.mdx @@ -0,0 +1,87 @@ +```typescript +import 'dotenv/config'; +import {PrivyClient} from '@privy-io/node'; +import {createRpc, bn} from '@lightprotocol/stateless.js'; +import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js'; +import {CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer} from '@lightprotocol/compressed-token'; + +const transferCompressedTokens = async ( + fromAddress: string, + toAddress: string, + tokenMintAddress: string, + amount: number, + decimals: number = 6 +) => { + const connection = createRpc(process.env.HELIUS_RPC_URL!); + + const privy = new PrivyClient({ + appId: process.env.PRIVY_APP_ID!, + appSecret: process.env.PRIVY_APP_SECRET!, + }); + + // Create public key objects + const fromPubkey = new PublicKey(fromAddress); + const toPubkey = new PublicKey(toAddress); + const mintPubkey = new PublicKey(tokenMintAddress); + const tokenAmount = bn(amount * Math.pow(10, decimals)); + + // Get compressed token accounts (filter out null items from indexer) + const accounts = await connection.getCompressedTokenAccountsByOwner(fromPubkey, {mint: mintPubkey}); + const validItems = (accounts.items || []).filter((item): item is NonNullable => item !== null); + if (validItems.length === 0) { + throw new Error('No compressed token accounts found'); + } + + // Select minimum accounts needed for transfer + const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(validItems, tokenAmount); + if (inputAccounts.length === 0) { + throw new Error('Insufficient balance'); + } + + // Get validity proof to prove compressed token accounts exist in state tree. + const proof = await connection.getValidityProof(inputAccounts.map(account => bn(account.compressedAccount.hash))); + + // Create transfer instruction + const instruction = await CompressedTokenProgram.transfer({ + payer: fromPubkey, + inputCompressedTokenAccounts: inputAccounts, + toAddress: toPubkey, + amount: tokenAmount, + recentInputStateRootIndices: proof.rootIndices, + recentValidityProof: proof.compressedProof, + }); + + // Create transaction + const transaction = new Transaction(); + transaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: 300_000})); + transaction.add(instruction); + + // Get recent blockhash + const {blockhash} = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = fromPubkey; + + // Sign with Privy + const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, { + transaction: transaction.serialize({requireAllSignatures: false}), + authorization_context: { + authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!] + } + }); + const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction; + if (!signedTx) { + throw new Error('Privy returned invalid response: ' + JSON.stringify(signResult)); + } + const signedTransaction = Buffer.from(signedTx, 'base64'); + + // Send transaction + const signature = await connection.sendRawTransaction(signedTransaction, { + skipPreflight: false, + preflightCommitment: 'confirmed' + }); + await connection.confirmTransaction(signature, 'confirmed'); + + return signature; +}; + +export default transferCompressedTokens;``` diff --git a/snippets/code-snippets/privy/transfer/react.mdx b/snippets/code-snippets/privy/transfer/react.mdx new file mode 100644 index 00000000..73bdbeed --- /dev/null +++ b/snippets/code-snippets/privy/transfer/react.mdx @@ -0,0 +1,105 @@ +```typescript +import { useState } from 'react'; +import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js'; +import { CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer } from '@lightprotocol/compressed-token'; +import { bn, createRpc } from '@lightprotocol/stateless.js'; +import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core'; +import type { SignTransactionResult } from '@privy-io/react-auth/solana'; + +export interface TransferParams { + ownerPublicKey: string; + mint: string; + toAddress: string; + amount: number; + decimals?: number; +} + +export interface TransferArgs { + params: TransferParams; + wallet: ConnectedStandardSolanaWallet; + signTransaction: (args: { + transaction: Buffer; + wallet: ConnectedStandardSolanaWallet; + chain: string; + }) => Promise; +} + +export function useTransfer() { + const [isLoading, setIsLoading] = useState(false); + + const transfer = async (args: TransferArgs): Promise => { + setIsLoading(true); + + try { + const { params, wallet, signTransaction } = args; + const { ownerPublicKey, mint, toAddress, amount, decimals = 6 } = params; + + // Create RPC connection + const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL); + + const owner = new PublicKey(ownerPublicKey); + const mintPubkey = new PublicKey(mint); + const recipient = new PublicKey(toAddress); + const tokenAmount = bn(Math.floor(amount * Math.pow(10, decimals))); + + // Get compressed token accounts + const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint: mintPubkey }); + if (!accounts.items || accounts.items.length === 0) { + throw new Error('No compressed token accounts found'); + } + + // Select minimum accounts needed + const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(accounts.items, tokenAmount); + if (inputAccounts.length === 0) { + throw new Error('Insufficient balance'); + } + + // Get validity proof + const proof = await rpc.getValidityProof( + inputAccounts.map((account) => bn(account.compressedAccount.hash)) + ); + + // Build transfer instruction + const transferIx = await CompressedTokenProgram.transfer({ + payer: owner, + inputCompressedTokenAccounts: inputAccounts, + toAddress: recipient, + amount: tokenAmount, + recentInputStateRootIndices: proof.rootIndices, + recentValidityProof: proof.compressedProof, + }); + + // Build transaction + const { blockhash } = await rpc.getLatestBlockhash(); + const transaction = new Transaction(); + transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 })); + transaction.add(transferIx); + transaction.recentBlockhash = blockhash; + transaction.feePayer = owner; + + // Serialize unsigned transaction + const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false }); + + // Sign with Privy + const signedTx = await signTransaction({ + transaction: unsignedTxBuffer, + wallet, + chain: 'solana:devnet', + }); + + // Send transaction + const signedTxBuffer = Buffer.from(signedTx.signedTransaction); + const signature = await rpc.sendRawTransaction(signedTxBuffer, { + skipPreflight: false, + preflightCommitment: 'confirmed', + }); + + return signature; + } finally { + setIsLoading(false); + } + }; + + return { transfer, isLoading }; +} +``` diff --git a/snippets/compressible-rent-explained.mdx b/snippets/compressible-rent-explained.mdx index 86d0ddee..62b46d1c 100644 --- a/snippets/compressible-rent-explained.mdx +++ b/snippets/compressible-rent-explained.mdx @@ -1,7 +1,11 @@ 1. The rent-exemption for light account creation is sponsored by the Light Token Program. 2. Transaction payer's pay rent per rent-epoch (388 lamports for 1.5h)
    - to keep accounts "active". + to keep accounts "active" 3. "Inactive" accounts (rent below one epoch) get automatically compressed. 4. The account's state is cryptographically preserved and will be loaded into hot account state in-flight, when the account is used again.
    -The hot state fee is paid for by the transaction payer when writing to the respective account. +The hot state fee is paid for by the transaction payer when writing to the respective account: + +* At account creation ~17,208 lamports for 24h of rent
    +and compression incentive. +* When the account's rent is below 3h, the transaction payer tops up 776 lamports. diff --git a/snippets/jsx/code-compare.jsx b/snippets/jsx/code-compare.jsx index 86ec2b1f..3d8370ab 100644 --- a/snippets/jsx/code-compare.jsx +++ b/snippets/jsx/code-compare.jsx @@ -3,18 +3,49 @@ 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 +91,7 @@ export const CodeCompare = ({ }; const handleToggle = () => { - animateTo(isLightMode ? 0 : 100); + animateTo(showingFirst ? 0 : 100); }; const handleMouseDown = (e) => { @@ -123,57 +154,75 @@ 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 */} -
    +
    + {/* Copy button */} + + + {/* Neumorphic Toggle Switch */} +
    {/* Toggle button */}
    +
    {/* Code container */} @@ -209,27 +259,35 @@ 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 +314,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/jsx/light-token-vs-spl-calculator.jsx b/snippets/jsx/light-token-vs-spl-calculator.jsx index 6e55145a..6f3ab56b 100644 --- a/snippets/jsx/light-token-vs-spl-calculator.jsx +++ b/snippets/jsx/light-token-vs-spl-calculator.jsx @@ -1,5 +1,3 @@ -import { useState } from "react"; - export const LightTokenVsSplCalculator = () => { const [numAccounts, setNumAccounts] = useState(100000); const [showCustomAccounts, setShowCustomAccounts] = useState(false); diff --git a/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx b/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx index f5cb6fea..ac09b01c 100644 --- a/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx +++ b/snippets/overview-tables/compressed-tokens-advanced-guides-table.mdx @@ -4,5 +4,4 @@ | [Combine Instructions in One Transaction](/compressed-tokens/advanced-guides/how-to-combine-operations-in-one-transaction) | Execute multiple token instructions within a single transaction | | [For Wallet Applications](/compressed-tokens/advanced-guides/add-wallet-support-for-compressed-tokens) | Add compressed token support in your wallet application | | [Use Token-2022 with Compression](/compressed-tokens/advanced-guides/use-token-2022-with-compression) | Create compressed Token-2022 mints with metadata and other extensions | -| [Example Web Client](https://github.com/Lightprotocol/example-web-client) | Demonstrates how to use @lightprotocol/stateless.js in a browser environment to interact with ZK Compression | -| [Example Node.js Client](https://github.com/Lightprotocol/example-nodejs-client) | Script to execute basic compression/decompression/transfers | +| [Privy Guide](/compressed-tokens/for-privy) | Integrate compressed tokens with Privy embedded wallets for rent-free token accounts | 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/development-environment-setup.mdx b/snippets/setup/development-environment-setup.mdx index 35f8f8d6..e1ad5eb1 100644 --- a/snippets/setup/development-environment-setup.mdx +++ b/snippets/setup/development-environment-setup.mdx @@ -1,4 +1,4 @@ -import CliInstall from '/snippets/versions/cli-install-0.27.1-alpha.2.mdx'; +import CliInstall from '/snippets/versions/cli-install-0.28.0-beta.3.mdx'; **Install Solana CLI:** diff --git a/snippets/setup/full-setup.mdx b/snippets/setup/full-setup.mdx index 39c0aade..9f078886 100644 --- a/snippets/setup/full-setup.mdx +++ b/snippets/setup/full-setup.mdx @@ -5,37 +5,37 @@ Install packages in your working directory: ```bash - npm install @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + npm install @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` Install the CLI globally: ```bash - npm install -g @lightprotocol/zk-compression-cli@alpha + npm install -g @lightprotocol/zk-compression-cli@beta ``` Install packages in your working directory: ```bash - yarn add @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + yarn add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` Install the CLI globally: ```bash - yarn global add @lightprotocol/zk-compression-cli@alpha + yarn global add @lightprotocol/zk-compression-cli@beta ``` Install packages in your working directory: ```bash - pnpm add @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + pnpm add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` Install the CLI globally: ```bash - pnpm add -g @lightprotocol/zk-compression-cli@alpha + pnpm add -g @lightprotocol/zk-compression-cli@beta ``` diff --git a/snippets/setup/install-dependencies-codegroup.mdx b/snippets/setup/install-dependencies-codegroup.mdx index 35b4346f..f3704a8b 100644 --- a/snippets/setup/install-dependencies-codegroup.mdx +++ b/snippets/setup/install-dependencies-codegroup.mdx @@ -1,3 +1,3 @@ -import Snippet from '/snippets/versions/sdk-install-0.22.1-alpha.1.mdx'; +import Snippet from '/snippets/versions/sdk-install-0.23.0-beta.3.mdx'; 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/setup/rust-setup-environment-tabs.mdx b/snippets/setup/rust-setup-environment-tabs.mdx index 1b356028..31c9ba24 100644 --- a/snippets/setup/rust-setup-environment-tabs.mdx +++ b/snippets/setup/rust-setup-environment-tabs.mdx @@ -1,4 +1,4 @@ -import CliInstall from '/snippets/versions/cli-install-0.27.1-alpha.2.mdx'; +import CliInstall from '/snippets/versions/cli-install-0.28.0-beta.3.mdx'; diff --git a/snippets/setup/setup-environment-tabs.mdx b/snippets/setup/setup-environment-tabs.mdx index 1d040421..1dad2679 100644 --- a/snippets/setup/setup-environment-tabs.mdx +++ b/snippets/setup/setup-environment-tabs.mdx @@ -1,4 +1,4 @@ -import CliInstall from '/snippets/versions/cli-install-0.27.1-alpha.2.mdx'; +import CliInstall from '/snippets/versions/cli-install-0.28.0-beta.3.mdx'; diff --git a/snippets/setup/toolkits-setup.mdx b/snippets/setup/toolkits-setup.mdx index 4dcbfc06..fc9431c5 100644 --- a/snippets/setup/toolkits-setup.mdx +++ b/snippets/setup/toolkits-setup.mdx @@ -1,4 +1,4 @@ ```bash -npm install @lightprotocol/compressed-token@alpha \ - @lightprotocol/stateless.js@alpha +npm install @lightprotocol/compressed-token@beta \ + @lightprotocol/stateless.js@beta ``` diff --git a/snippets/setup/toolkits2-setup.mdx b/snippets/setup/toolkits2-setup.mdx new file mode 100644 index 00000000..3f576897 --- /dev/null +++ b/snippets/setup/toolkits2-setup.mdx @@ -0,0 +1,4 @@ +```bash +npm install @lightprotocol/compressed-token \ + @lightprotocol/stateless.js +``` diff --git a/snippets/setup/welcome-page-install.mdx b/snippets/setup/welcome-page-install.mdx index ec90ad57..94408d33 100644 --- a/snippets/setup/welcome-page-install.mdx +++ b/snippets/setup/welcome-page-install.mdx @@ -3,37 +3,37 @@ Install packages in your working directory: ```bash - npm install @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + npm install @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` Install the CLI globally: ```bash - npm install -g @lightprotocol/zk-compression-cli@alpha + npm install -g @lightprotocol/zk-compression-cli@beta ``` Install packages in your working directory: ```bash - yarn add @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + yarn add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` Install the CLI globally: ```bash - yarn global add @lightprotocol/zk-compression-cli@alpha + yarn global add @lightprotocol/zk-compression-cli@beta ``` Install packages in your working directory: ```bash - pnpm add @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + pnpm add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` Install the CLI globally: ```bash - pnpm add -g @lightprotocol/zk-compression-cli@alpha + pnpm add -g @lightprotocol/zk-compression-cli@beta ``` diff --git a/snippets/versions/cli-install-0.27.1-alpha.2.mdx b/snippets/versions/cli-install-0.27.1-alpha.2.mdx index 814600d5..8adef93c 100644 --- a/snippets/versions/cli-install-0.27.1-alpha.2.mdx +++ b/snippets/versions/cli-install-0.27.1-alpha.2.mdx @@ -1,17 +1,17 @@ ```bash - npm install -g @lightprotocol/zk-compression-cli@alpha + npm install -g @lightprotocol/zk-compression-cli@beta ``` ```bash - yarn global add @lightprotocol/zk-compression-cli@alpha + yarn global add @lightprotocol/zk-compression-cli@beta ``` ```bash - pnpm add -g @lightprotocol/zk-compression-cli@alpha + pnpm add -g @lightprotocol/zk-compression-cli@beta ``` diff --git a/snippets/versions/cli-install-0.28.0-beta.3.mdx b/snippets/versions/cli-install-0.28.0-beta.3.mdx new file mode 100644 index 00000000..8adef93c --- /dev/null +++ b/snippets/versions/cli-install-0.28.0-beta.3.mdx @@ -0,0 +1,17 @@ + + + ```bash + npm install -g @lightprotocol/zk-compression-cli@beta + ``` + + + ```bash + yarn global add @lightprotocol/zk-compression-cli@beta + ``` + + + ```bash + pnpm add -g @lightprotocol/zk-compression-cli@beta + ``` + + 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" +``` diff --git a/snippets/versions/sdk-install-0.22.1-alpha.1.mdx b/snippets/versions/sdk-install-0.22.1-alpha.1.mdx index 5d176511..c710118a 100644 --- a/snippets/versions/sdk-install-0.22.1-alpha.1.mdx +++ b/snippets/versions/sdk-install-0.22.1-alpha.1.mdx @@ -1,20 +1,20 @@ ```bash - npm install @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + npm install @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` ```bash - yarn add @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + yarn add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` ```bash - pnpm add @lightprotocol/stateless.js@alpha \ - @lightprotocol/compressed-token@alpha + pnpm add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta ``` diff --git a/snippets/versions/sdk-install-0.23.0-beta.3.mdx b/snippets/versions/sdk-install-0.23.0-beta.3.mdx new file mode 100644 index 00000000..c710118a --- /dev/null +++ b/snippets/versions/sdk-install-0.23.0-beta.3.mdx @@ -0,0 +1,20 @@ + + + ```bash + npm install @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta + ``` + + + ```bash + yarn add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta + ``` + + + ```bash + pnpm add @lightprotocol/stateless.js@beta \ + @lightprotocol/compressed-token@beta + ``` + +