diff --git a/.gitmodules b/.gitmodules index ce4eb81d..294b3d02 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "programs/anchor/cp-swap-reference"] path = programs/anchor/cp-swap-reference url = https://github.com/Lightprotocol/cp-swap-reference.git +[submodule "toolkits/nullifier-program"] + path = toolkits/nullifier-program + url = https://github.com/Lightprotocol/nullifier-program.git diff --git a/README.md b/README.md index e5fb9464..239a1081 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,11 @@ Light token is a high-performance token standard that reduces the cost of mint a | | | | Description | |---------|--------|-------------|-------------| | create-mint | [Action](typescript-client/actions/create-mint.ts) | [Instruction](typescript-client/instructions/create-mint.ts) | Create a light-token mint with metadata | +| create-spl-mint | [Action](typescript-client/actions/create-spl-mint.ts) | [Instruction](typescript-client/instructions/create-spl-mint.ts) | Create an SPL mint with SPL interface PDA | +| create-t22-mint | [Action](typescript-client/actions/create-t22-mint.ts) | [Instruction](typescript-client/instructions/create-t22-mint.ts) | Create a Token 2022 mint with SPL interface PDA | +| create-spl-interface | [Action](typescript-client/actions/create-spl-interface.ts) | [Instruction](typescript-client/instructions/create-spl-interface.ts) | Register SPL interface PDA for an existing mint | | create-ata | [Action](typescript-client/actions/create-ata.ts) | [Instruction](typescript-client/instructions/create-ata.ts) | Create an associated light-token account | +| create-ata-explicit-rent-sponsor | [Action](typescript-client/actions/create-ata-explicit-rent-sponsor.ts) | | Create an ATA with explicit rent sponsor | | load-ata | [Action](typescript-client/actions/load-ata.ts) | [Instruction](typescript-client/instructions/load-ata.ts) | Load token accounts from light-token, compressed tokens, SPL/T22 to one unified balance | | mint-to | [Action](typescript-client/actions/mint-to.ts) | [Instruction](typescript-client/instructions/mint-to.ts) | Mint tokens to a light-account | | transfer-interface | [Action](typescript-client/actions/transfer-interface.ts) | [Instruction](typescript-client/instructions/transfer-interface.ts) | Transfer between light-token, T22, and SPL accounts | @@ -32,6 +36,7 @@ Light token is a high-performance token standard that reduces the cost of mint a | unwrap | [Action](typescript-client/actions/unwrap.ts) | [Instruction](typescript-client/instructions/unwrap.ts) | Unwrap light-token to SPL/T22 | | approve | [Action](typescript-client/actions/delegate-approve.ts) | | Approve delegate | | revoke | [Action](typescript-client/actions/delegate-revoke.ts) | | Revoke delegate | +| delegate-transfer | [Action](typescript-client/actions/delegate-transfer.ts) | | Delegate transfers tokens on behalf of owner | ### Rust diff --git a/toolkits/gasless-transactions/rust/Cargo.toml b/toolkits/gasless-transactions/rust/Cargo.toml index 2ee2aebc..7ecd74fd 100644 --- a/toolkits/gasless-transactions/rust/Cargo.toml +++ b/toolkits/gasless-transactions/rust/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" publish = false [[bin]] -name = "sponsor-top-ups" -path = "sponsor-top-ups.rs" +name = "gasless-transfer" +path = "gasless-transfer.rs" [dependencies] light-token = "0.23.0" diff --git a/toolkits/nullifier-program b/toolkits/nullifier-program new file mode 160000 index 00000000..8a5e0ced --- /dev/null +++ b/toolkits/nullifier-program @@ -0,0 +1 @@ +Subproject commit 8a5e0ced6b142dbd74fe4585a0d53cc032166660 diff --git a/toolkits/payments/README.md b/toolkits/payments/README.md index 15a27e47..6537a477 100644 --- a/toolkits/payments/README.md +++ b/toolkits/payments/README.md @@ -53,7 +53,7 @@ Light Token reduces account creation cost by 200x compared to SPL. Transfer cost | [delegate-approve.ts](spend-permissions/delegate-approve.ts) | Let a delegate spend tokens on your behalf. | `approveInterface` | | [delegate-revoke.ts](spend-permissions/delegate-revoke.ts) | Revoke delegate access. | `revokeInterface` | | [delegate-check.ts](spend-permissions/delegate-check.ts) | Check current delegation status and remaining allowance. | `getAtaInterface` | -| [delegate-full-flow.ts](spend-permissions/delegate-full-flow.ts) | Approve, check, and revoke in one script. | `approveInterface`, `revokeInterface`, `getAtaInterface` | +| [delegate-transfer.ts](spend-permissions/delegate-transfer.ts) | Delegate transfers tokens on behalf of the owner. | `transferInterface` | ### Interop (wrap and unwrap) @@ -84,7 +84,7 @@ Run any script: npx tsx send/send-action.ts npx tsx send/payment-with-memo.ts npx tsx send/batch-send.ts -npx tsx spend-permissions/delegate-full-flow.ts +npx tsx spend-permissions/delegate-approve.ts ``` ### Devnet @@ -104,6 +104,13 @@ export const rpc = createRpc(RPC_URL); // export const rpc = createRpc(); ``` +### Nullifier Program + +For some use cases, such as sending payments, you might want to prevent your onchain instruction from being executed more than once. The [nullifier program](../nullifier-program/) utility solves this for you. We also deployed a reference implementation to public networks so you can get started quickly. +- **[TypeScript example](../nullifier-program/examples/action-create-nullifier.ts)** +- **[Rust example](../nullifier-program/examples/rust/src/main.rs)** +- **[Documentation](https://www.zkcompression.com/compressed-pdas/guides/how-to-create-nullifier-pdas)** + ## Documentation - [Payment flows overview](https://www.zkcompression.com/light-token/payments/overview) diff --git a/toolkits/payments/package.json b/toolkits/payments/package.json index fd19fce8..d321c42b 100644 --- a/toolkits/payments/package.json +++ b/toolkits/payments/package.json @@ -4,6 +4,7 @@ "description": "Light Token Payments Toolkit Examples", "type": "module", "scripts": { + "test:all": "tsx send/send-action.ts && tsx send/send-instruction.ts && tsx send/payment-with-memo.ts && tsx send/batch-send.ts && tsx send/sign-all-transactions.ts && tsx receive/receive.ts && tsx spend-permissions/delegate-approve.ts && tsx spend-permissions/delegate-revoke.ts && tsx spend-permissions/delegate-check.ts && tsx spend-permissions/delegate-transfer.ts", "send-action": "tsx send/send-action.ts", "send-instruction": "tsx send/send-instruction.ts", "payment-with-memo": "tsx send/payment-with-memo.ts", @@ -19,11 +20,12 @@ "delegate-approve": "tsx spend-permissions/delegate-approve.ts", "delegate-revoke": "tsx spend-permissions/delegate-revoke.ts", "delegate-check": "tsx spend-permissions/delegate-check.ts", - "delegate-full-flow": "tsx spend-permissions/delegate-full-flow.ts" + "delegate-transfer": "tsx spend-permissions/delegate-transfer.ts" }, "dependencies": { - "@lightprotocol/compressed-token": "^0.23.0-beta.10", - "@lightprotocol/stateless.js": "^0.23.0-beta.10", - "@solana/spl-memo": "^0.2.5" + "@lightprotocol/compressed-token": "^0.23.0-beta.12", + "@lightprotocol/stateless.js": "^0.23.0-beta.12", + "@solana/spl-memo": "^0.2.5", + "@solana/spl-token": "^0.4.13" } } diff --git a/toolkits/payments/receive/receive.ts b/toolkits/payments/receive/receive.ts index a05cd948..37363e4f 100644 --- a/toolkits/payments/receive/receive.ts +++ b/toolkits/payments/receive/receive.ts @@ -4,10 +4,10 @@ import { sendAndConfirmTransaction, } from "@solana/web3.js"; import { + transferInterface, getAssociatedTokenAddressInterface, createLoadAtaInstructions, -} from "@lightprotocol/compressed-token"; -import { transferInterface } from "@lightprotocol/compressed-token/unified"; +} from "@lightprotocol/compressed-token/unified"; import { rpc, payer, setup } from "../setup.js"; (async function () { diff --git a/toolkits/payments/send/batch-send.ts b/toolkits/payments/send/batch-send.ts index 2f93935f..ea1178c5 100644 --- a/toolkits/payments/send/batch-send.ts +++ b/toolkits/payments/send/batch-send.ts @@ -4,68 +4,52 @@ import { sendAndConfirmTransaction, } from "@solana/web3.js"; import { - createAtaInterfaceIdempotent, - getAssociatedTokenAddressInterface, + createTransferInterfaceInstructions, getAtaInterface, -} from "@lightprotocol/compressed-token"; -import { - createTransferToAccountInterfaceInstructions, - createLoadAtaInstructions, + getAssociatedTokenAddressInterface, } from "@lightprotocol/compressed-token/unified"; import { rpc, payer, setup } from "../setup.js"; (async function () { - // Step 1: Setup — create mint and fund sender const { mint } = await setup(); - const recipients = Array.from({ length: 5 }, (_, i) => ({ - address: Keypair.generate().publicKey, - amount: (i + 1) * 100, - })); - - // Step 2: Create ATAs idempotently for sender + all recipients - await createAtaInterfaceIdempotent(rpc, payer, mint, payer.publicKey); - for (const { address } of recipients) { - await createAtaInterfaceIdempotent(rpc, payer, mint, address); - } - - // Step 3: Derive ATA addresses - const senderAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); - const recipientAtas = recipients.map(({ address }) => - getAssociatedTokenAddressInterface(mint, address) - ); + const recipients = [ + { address: Keypair.generate().publicKey, amount: 100 }, + { address: Keypair.generate().publicKey, amount: 200 }, + { address: Keypair.generate().publicKey, amount: 300 }, + ]; - // Step 4: Create transfer instructions using explicit-account variant - const COMPUTE_BUDGET_ID = - "ComputeBudget111111111111111111111111111111"; + // Build transfer instructions for each recipient + const COMPUTE_BUDGET = "ComputeBudget111111111111111111111111111111"; const allTransferIxs = []; - let isFirst = true; + let hasComputeBudget = false; for (const { address, amount } of recipients) { - const destination = getAssociatedTokenAddressInterface(mint, address); - const ixs = await createTransferToAccountInterfaceInstructions( + const instructions = await createTransferInterfaceInstructions( rpc, payer.publicKey, mint, amount, payer.publicKey, - destination + address ); - // Deduplicate ComputeBudget across transfers. - for (const ix of ixs[0]) { - if (!isFirst && ix.programId.toBase58() === COMPUTE_BUDGET_ID) - continue; + + for (const ix of instructions[instructions.length - 1]) { + // Deduplicate ComputeBudget instructions + if (ix.programId.toBase58() === COMPUTE_BUDGET) { + if (hasComputeBudget) continue; + hasComputeBudget = true; + } allTransferIxs.push(ix); } - isFirst = false; } - // Step 5: Batch into single transaction - const batchTx = new Transaction().add(...allTransferIxs); - const sig = await sendAndConfirmTransaction(rpc, batchTx, [payer]); + // Send all transfers in a single transaction + const tx = new Transaction().add(...allTransferIxs); + const sig = await sendAndConfirmTransaction(rpc, tx, [payer]); console.log("Batch tx:", sig); - // Step 6: Verify balances + // Verify balances for (const { address, amount } of recipients) { const ata = getAssociatedTokenAddressInterface(mint, address); const { parsed } = await getAtaInterface(rpc, ata, address, mint); diff --git a/toolkits/payments/send/payment-with-memo.ts b/toolkits/payments/send/payment-with-memo.ts index 83d1054d..ca077fac 100644 --- a/toolkits/payments/send/payment-with-memo.ts +++ b/toolkits/payments/send/payment-with-memo.ts @@ -1,20 +1,12 @@ import { Keypair, Transaction, - TransactionInstruction, - PublicKey, sendAndConfirmTransaction, } from "@solana/web3.js"; -import { - createTransferInterfaceInstructions, - sliceLast, -} from "@lightprotocol/compressed-token/unified"; +import { createTransferInterfaceInstructions } from "@lightprotocol/compressed-token/unified"; +import { createMemoInstruction } from "@solana/spl-memo"; import { rpc, payer, setup } from "../setup.js"; -const MEMO_PROGRAM_ID = new PublicKey( - "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" -); - (async function () { const { mint } = await setup(); const recipient = Keypair.generate(); @@ -28,28 +20,19 @@ const MEMO_PROGRAM_ID = new PublicKey( recipient.publicKey ); - const { rest: loadInstructions, last: transferInstructions } = - sliceLast(instructions); + // Append memo to the transfer transaction (last batch) + const memoIx = createMemoInstruction("INV-2024-001"); + instructions[instructions.length - 1].push(memoIx); - // Send load transactions first (if any) - for (const ixs of loadInstructions) { + let signature; + for (const ixs of instructions) { const tx = new Transaction().add(...ixs); - await sendAndConfirmTransaction(rpc, tx, [payer]); + signature = await sendAndConfirmTransaction(rpc, tx, [payer]); } - - // Add memo to the transfer transaction - const memoIx = new TransactionInstruction({ - keys: [], - programId: MEMO_PROGRAM_ID, - data: Buffer.from("INV-2024-001"), - }); - - const transferTx = new Transaction().add(...transferInstructions, memoIx); - const signature = await sendAndConfirmTransaction(rpc, transferTx, [payer]); console.log("Tx with memo:", signature); // Read memo back from transaction logs - const txDetails = await rpc.getTransaction(signature, { + const txDetails = await rpc.getTransaction(signature!, { maxSupportedTransactionVersion: 0, }); diff --git a/toolkits/payments/send/send-action.ts b/toolkits/payments/send/send-action.ts index 28589f4d..f52d381c 100644 --- a/toolkits/payments/send/send-action.ts +++ b/toolkits/payments/send/send-action.ts @@ -15,6 +15,5 @@ import { rpc, payer, setup } from "../setup.js"; payer, 100 ); - console.log("Tx:", sig); })(); diff --git a/toolkits/payments/setup.ts b/toolkits/payments/setup.ts index 09f330b2..3033482e 100644 --- a/toolkits/payments/setup.ts +++ b/toolkits/payments/setup.ts @@ -28,7 +28,7 @@ export const payer = Keypair.fromSecretKey( ); /** Create SPL mint, fund payer, wrap into light-token ATA. */ -export async function setup(amount = 1_000_000) { +export async function setup(amount = 1_000_000_000) { const { mint } = await createMintInterface( rpc, payer, diff --git a/toolkits/payments/spend-permissions/delegate-approve.ts b/toolkits/payments/spend-permissions/delegate-approve.ts index 11faf096..792686c5 100644 --- a/toolkits/payments/spend-permissions/delegate-approve.ts +++ b/toolkits/payments/spend-permissions/delegate-approve.ts @@ -5,7 +5,7 @@ import { rpc, payer, setup } from "../setup.js"; (async function () { const { mint, senderAta } = await setup(); - // Approve: grant delegate permission to spend up to 500,000 tokens + // Approve: grant delegate permission to spend up to 500,000,000 tokens const delegate = Keypair.generate(); const tx = await approveInterface( rpc, @@ -13,11 +13,11 @@ import { rpc, payer, setup } from "../setup.js"; senderAta, mint, delegate.publicKey, - 500_000, + 500_000_000, payer ); console.log("Approved delegate:", delegate.publicKey.toBase58()); - console.log("Allowance: 500,000 tokens"); + console.log("Allowance: 500,000,000 tokens"); console.log("Tx:", tx); })(); diff --git a/toolkits/payments/spend-permissions/delegate-check.ts b/toolkits/payments/spend-permissions/delegate-check.ts index b38932d3..93df30f9 100644 --- a/toolkits/payments/spend-permissions/delegate-check.ts +++ b/toolkits/payments/spend-permissions/delegate-check.ts @@ -1,6 +1,8 @@ import { Keypair } from "@solana/web3.js"; -import { getAtaInterface } from "@lightprotocol/compressed-token"; -import { approveInterface } from "@lightprotocol/compressed-token/unified"; +import { + getAtaInterface, + approveInterface, +} from "@lightprotocol/compressed-token/unified"; import { rpc, payer, setup } from "../setup.js"; (async function () { @@ -31,7 +33,7 @@ import { rpc, payer, setup } from "../setup.js"; senderAta, mint, delegate.publicKey, - 500_000, + 500_000_000, payer ); diff --git a/toolkits/payments/spend-permissions/delegate-full-flow.ts b/toolkits/payments/spend-permissions/delegate-full-flow.ts deleted file mode 100644 index 12e0054d..00000000 --- a/toolkits/payments/spend-permissions/delegate-full-flow.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Keypair } from "@solana/web3.js"; -import { getAtaInterface } from "@lightprotocol/compressed-token"; -import { - approveInterface, - revokeInterface, -} from "@lightprotocol/compressed-token/unified"; -import { rpc, payer, setup } from "../setup.js"; - -(async function () { - const { mint, senderAta } = await setup(); - - // 1. Owner approves delegate to spend up to 500,000 tokens - const delegate = Keypair.generate(); - const approveTx = await approveInterface( - rpc, - payer, - senderAta, - mint, - delegate.publicKey, - 500_000, - payer - ); - console.log("1. Approved delegate:", delegate.publicKey.toBase58()); - console.log(" Allowance: 500,000 tokens"); - console.log(" Tx:", approveTx); - - // 2. Check delegation status - const account = await getAtaInterface( - rpc, - senderAta, - payer.publicKey, - mint - ); - console.log("\n2. Account status after approval:"); - console.log( - " Delegate:", - account.parsed.delegate?.toBase58() ?? "none" - ); - console.log( - " Delegated amount:", - account.parsed.delegatedAmount.toString() - ); - console.log(" Account balance:", account.parsed.amount.toString()); - - // 3. Owner revokes all delegate permissions - const revokeTx = await revokeInterface(rpc, payer, senderAta, mint, payer); - console.log("\n3. Revoked all delegate permissions"); - console.log(" Tx:", revokeTx); - - // 4. Verify delegate is removed - const afterRevoke = await getAtaInterface( - rpc, - senderAta, - payer.publicKey, - mint - ); - console.log("\n4. Account status after revoke:"); - console.log( - " Delegate:", - afterRevoke.parsed.delegate?.toBase58() ?? "none" - ); - console.log(" Balance:", afterRevoke.parsed.amount.toString()); -})(); diff --git a/toolkits/payments/spend-permissions/delegate-revoke.ts b/toolkits/payments/spend-permissions/delegate-revoke.ts index 252e23d5..d29beac9 100644 --- a/toolkits/payments/spend-permissions/delegate-revoke.ts +++ b/toolkits/payments/spend-permissions/delegate-revoke.ts @@ -16,7 +16,7 @@ import { rpc, payer, setup } from "../setup.js"; senderAta, mint, delegate.publicKey, - 500_000, + 500_000_000, payer ); console.log("Approved delegate:", delegate.publicKey.toBase58()); diff --git a/toolkits/payments/spend-permissions/delegate-transfer.ts b/toolkits/payments/spend-permissions/delegate-transfer.ts new file mode 100644 index 00000000..3f3a2bd8 --- /dev/null +++ b/toolkits/payments/spend-permissions/delegate-transfer.ts @@ -0,0 +1,41 @@ +import { Keypair } from "@solana/web3.js"; +import { + approveInterface, + transferInterface, +} from "@lightprotocol/compressed-token/unified"; +import { rpc, payer, setup } from "../setup.js"; + +(async function () { + const { mint, senderAta } = await setup(); + + // Approve: grant delegate permission to spend up to 500,000,000 tokens + const delegate = Keypair.generate(); + await approveInterface( + rpc, + payer, + senderAta, + mint, + delegate.publicKey, + 500_000_000, + payer + ); + console.log("Approved delegate:", delegate.publicKey.toBase58()); + + // Delegate transfers tokens on behalf of the owner + const recipient = Keypair.generate(); + const tx = await transferInterface( + rpc, + payer, + senderAta, + mint, + recipient.publicKey, + delegate, + 200_000_000, + undefined, + { owner: payer.publicKey } + ); + + console.log("Delegated transfer to:", recipient.publicKey.toBase58()); + console.log("Amount: 200,000,000 tokens"); + console.log("Tx:", tx); +})(); diff --git a/toolkits/payments/verify/get-balance.ts b/toolkits/payments/verify/get-balance.ts index e3bbdf25..61ee53b9 100644 --- a/toolkits/payments/verify/get-balance.ts +++ b/toolkits/payments/verify/get-balance.ts @@ -1,35 +1,14 @@ -import { Keypair } from "@solana/web3.js"; +import { PublicKey } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; import { getAssociatedTokenAddressInterface, getAtaInterface, -} from "@lightprotocol/compressed-token"; -import { transferInterface } from "@lightprotocol/compressed-token/unified"; -import { rpc, payer, setup } from "../setup.js"; +} from "@lightprotocol/compressed-token/unified"; -(async function () { - const { mint, senderAta } = await setup(); +const rpc = createRpc(); +const mint = new PublicKey("YOUR_MINT_ADDRESS"); +const owner = new PublicKey("YOUR_OWNER_ADDRESS"); - const recipient = Keypair.generate(); - await transferInterface( - rpc, - payer, - senderAta, - mint, - recipient.publicKey, - payer, - 100 - ); - - // Get recipient's balance - const recipientAta = getAssociatedTokenAddressInterface( - mint, - recipient.publicKey - ); - const { parsed: account } = await getAtaInterface( - rpc, - recipientAta, - recipient.publicKey, - mint - ); - console.log("Recipient's balance:", account.amount); -})(); +const ata = getAssociatedTokenAddressInterface(mint, owner); +const account = await getAtaInterface(rpc, ata, owner, mint); +console.log("Balance:", account.parsed.amount.toString()); diff --git a/toolkits/payments/verify/get-history.ts b/toolkits/payments/verify/get-history.ts index 725ccd6c..12b9998f 100644 --- a/toolkits/payments/verify/get-history.ts +++ b/toolkits/payments/verify/get-history.ts @@ -1,22 +1,8 @@ -import { Keypair } from "@solana/web3.js"; -import { transferInterface } from "@lightprotocol/compressed-token/unified"; -import { rpc, payer, setup } from "../setup.js"; +import { PublicKey } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; -(async function () { - const { mint, senderAta } = await setup(); +const rpc = createRpc(); +const owner = new PublicKey("YOUR_OWNER_ADDRESS"); - const recipient = Keypair.generate(); - await transferInterface( - rpc, - payer, - senderAta, - mint, - recipient.publicKey, - payer, - 100 - ); - - // Get transaction history - const result = await rpc.getSignaturesForOwnerInterface(payer.publicKey); - console.log("Signatures:", result.signatures.length); -})(); +const result = await rpc.getSignaturesForOwnerInterface(owner); +console.log("Signatures:", result.signatures.length); diff --git a/toolkits/payments/verify/verify-address.ts b/toolkits/payments/verify/verify-address.ts index deeee985..8e3a1334 100644 --- a/toolkits/payments/verify/verify-address.ts +++ b/toolkits/payments/verify/verify-address.ts @@ -1,59 +1,18 @@ -import { Keypair } from "@solana/web3.js"; +import { PublicKey } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; import { - createMintInterface, - createAtaInterface, getAssociatedTokenAddressInterface, getAtaInterface, -} from "@lightprotocol/compressed-token"; -import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; -import { rpc, payer } from "../setup.js"; +} from "@lightprotocol/compressed-token/unified"; -(async function () { - const { mint } = await createMintInterface( - rpc, - payer, - payer, - null, - 9, - undefined, - undefined, - TOKEN_PROGRAM_ID - ); +const rpc = createRpc(); +const mint = new PublicKey("YOUR_MINT_ADDRESS"); +const owner = new PublicKey("YOUR_OWNER_ADDRESS"); - // Create an associated token account for the payer (so it exists) - await createAtaInterface(rpc, payer, mint, payer.publicKey); - - // --- Verify an address that HAS an associated token account --- - const existingRecipient = payer.publicKey; - const expectedAta = getAssociatedTokenAddressInterface( - mint, - existingRecipient - ); - - try { - const account = await getAtaInterface( - rpc, - expectedAta, - existingRecipient, - mint - ); - console.log("Account exists, balance:", account.parsed.amount); - } catch { - console.log( - "Account does not exist yet — will be created on first transfer" - ); - } - - // --- Verify an address that does NOT have an associated token account --- - const newRecipient = Keypair.generate().publicKey; - const newAta = getAssociatedTokenAddressInterface(mint, newRecipient); - - try { - const account = await getAtaInterface(rpc, newAta, newRecipient, mint); - console.log("Account exists, balance:", account.parsed.amount); - } catch { - console.log( - "Account does not exist yet — will be created on first transfer" - ); - } -})(); +const ata = getAssociatedTokenAddressInterface(mint, owner); +try { + const account = await getAtaInterface(rpc, ata, owner, mint); + console.log("Account exists, balance:", account.parsed.amount.toString()); +} catch { + console.log("Account does not exist yet"); +} diff --git a/typescript-client/README.md b/typescript-client/README.md index 2c6cf2fe..02f19109 100644 --- a/typescript-client/README.md +++ b/typescript-client/README.md @@ -10,8 +10,10 @@ TypeScript client examples for light-token-sdk. - **[load-ata](actions/load-ata.ts)** - Load token accounts from light-token, compressed tokens, SPL/T22 to one unified balance - **[mint-to](actions/mint-to.ts)** - Mint tokens to a light-account - **[transfer-interface](actions/transfer-interface.ts)** - Transfer between light-token, T22, and SPL accounts +- **[create-ata-explicit-rent-sponsor](actions/create-ata-explicit-rent-sponsor.ts)** - Create an ATA with explicit rent sponsor - **[delegate-approve](actions/delegate-approve.ts)** - Approve delegate - **[delegate-revoke](actions/delegate-revoke.ts)** - Revoke delegate +- **[delegate-transfer](actions/delegate-transfer.ts)** - Delegate transfers tokens on behalf of owner - **[wrap](actions/wrap.ts)** - Wrap SPL/T22 to light-token - **[unwrap](actions/unwrap.ts)** - Unwrap light-token to SPL/T22 @@ -27,6 +29,8 @@ TypeScript client examples for light-token-sdk. - **[transfer-interface](instructions/transfer-interface.ts)** - Build transfer instruction - **[wrap](instructions/wrap.ts)** - Wrap SPL/T22 to light-token - **[unwrap](instructions/unwrap.ts)** - Unwrap light-token to SPL/T22 +- **[delegate-approve](instructions/delegate-approve.ts)** - Build approve delegate instructions +- **[delegate-revoke](instructions/delegate-revoke.ts)** - Build revoke delegate instructions ## Setup diff --git a/typescript-client/actions/delegate-approve.ts b/typescript-client/actions/delegate-approve.ts index bb0c59d6..c4bdb6bb 100644 --- a/typescript-client/actions/delegate-approve.ts +++ b/typescript-client/actions/delegate-approve.ts @@ -4,8 +4,9 @@ import { createRpc } from "@lightprotocol/stateless.js"; import { createMintInterface, mintToCompressed, - approve, + getAssociatedTokenAddressInterface, } from "@lightprotocol/compressed-token"; +import { approveInterface } from "@lightprotocol/compressed-token/unified"; import { homedir } from "os"; import { readFileSync } from "fs"; @@ -24,11 +25,22 @@ const payer = Keypair.fromSecretKey( (async function () { const { mint } = await createMintInterface(rpc, payer, payer, null, 9); await mintToCompressed(rpc, payer, mint, payer, [ - { recipient: payer.publicKey, amount: 1000n }, + { recipient: payer.publicKey, amount: 1_000_000_000n }, ]); + const senderAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); const delegate = Keypair.generate(); - const tx = await approve(rpc, payer, mint, 500, payer, delegate.publicKey); + const tx = await approveInterface( + rpc, + payer, + senderAta, + mint, + delegate.publicKey, + 500_000_000, + payer + ); + console.log("Approved delegate:", delegate.publicKey.toBase58()); + console.log("Allowance: 500,000,000 tokens"); console.log("Tx:", tx); })(); diff --git a/typescript-client/actions/delegate-revoke.ts b/typescript-client/actions/delegate-revoke.ts index 7eb3d068..b149e7be 100644 --- a/typescript-client/actions/delegate-revoke.ts +++ b/typescript-client/actions/delegate-revoke.ts @@ -4,9 +4,12 @@ import { createRpc } from "@lightprotocol/stateless.js"; import { createMintInterface, mintToCompressed, - approve, - revoke, + getAssociatedTokenAddressInterface, } from "@lightprotocol/compressed-token"; +import { + approveInterface, + revokeInterface, +} from "@lightprotocol/compressed-token/unified"; import { homedir } from "os"; import { readFileSync } from "fs"; @@ -25,17 +28,24 @@ const payer = Keypair.fromSecretKey( (async function () { const { mint } = await createMintInterface(rpc, payer, payer, null, 9); await mintToCompressed(rpc, payer, mint, payer, [ - { recipient: payer.publicKey, amount: 1000n }, + { recipient: payer.publicKey, amount: 1_000_000_000n }, ]); + const senderAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); const delegate = Keypair.generate(); - await approve(rpc, payer, mint, 500, payer, delegate.publicKey); - - const delegatedAccounts = await rpc.getCompressedTokenAccountsByDelegate( + await approveInterface( + rpc, + payer, + senderAta, + mint, delegate.publicKey, - { mint } + 500_000_000, + payer ); - const tx = await revoke(rpc, payer, delegatedAccounts.items, payer); + console.log("Approved delegate:", delegate.publicKey.toBase58()); + + const tx = await revokeInterface(rpc, payer, senderAta, mint, payer); + console.log("Revoked all delegate permissions"); console.log("Tx:", tx); })(); diff --git a/typescript-client/actions/delegate-transfer.ts b/typescript-client/actions/delegate-transfer.ts new file mode 100644 index 00000000..cdd718f3 --- /dev/null +++ b/typescript-client/actions/delegate-transfer.ts @@ -0,0 +1,65 @@ +import "dotenv/config"; +import { Keypair } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + mintToCompressed, + getAssociatedTokenAddressInterface, +} from "@lightprotocol/compressed-token"; +import { + approveInterface, + transferInterface, +} from "@lightprotocol/compressed-token/unified"; +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: 1_000_000_000n }, + ]); + + const senderAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); + const delegate = Keypair.generate(); + const recipient = Keypair.generate(); + + // Approve delegate + await approveInterface( + rpc, + payer, + senderAta, + mint, + delegate.publicKey, + 500_000_000, + payer + ); + console.log("Approved delegate:", delegate.publicKey.toBase58()); + + // Delegate transfers tokens on behalf of the owner + const tx = await transferInterface( + rpc, + payer, + senderAta, + mint, + recipient.publicKey, + delegate, + 200_000_000, + undefined, + undefined, + { owner: payer.publicKey } + ); + + console.log("Delegated transfer:", tx); +})(); diff --git a/typescript-client/instructions/create-token-pool.ts b/typescript-client/instructions/create-token-pool.ts deleted file mode 100644 index 7f375705..00000000 --- a/typescript-client/instructions/create-token-pool.ts +++ /dev/null @@ -1,40 +0,0 @@ -import "dotenv/config"; -import { - Keypair, - PublicKey, - Transaction, - sendAndConfirmTransaction, -} from "@solana/web3.js"; -import { createRpc } from "@lightprotocol/stateless.js"; -import { LightTokenProgram } from "@lightprotocol/compressed-token"; -import { TOKEN_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); -// localnet: -const rpc = createRpc(); - -const payer = Keypair.fromSecretKey( - new Uint8Array( - JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) - ) -); - -(async function () { - const existingMint = new PublicKey("YOUR_EXISTING_MINT_ADDRESS"); - - const ix = await LightTokenProgram.createSplInterface({ - feePayer: payer.publicKey, - mint: existingMint, - tokenProgramId: TOKEN_PROGRAM_ID, - }); - - const tx = new Transaction().add(ix); - const signature = await sendAndConfirmTransaction(rpc, tx, [payer]); - - console.log("Mint:", existingMint.toBase58()); - console.log("Tx:", signature); -})(); diff --git a/typescript-client/instructions/delegate-approve.ts b/typescript-client/instructions/delegate-approve.ts new file mode 100644 index 00000000..a26cdb66 --- /dev/null +++ b/typescript-client/instructions/delegate-approve.ts @@ -0,0 +1,65 @@ +import "dotenv/config"; +import { Keypair, sendAndConfirmTransaction, Transaction } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + mintToInterface, + getAssociatedTokenAddressInterface, + createApproveInterfaceInstructions, +} 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); + + const owner = Keypair.generate(); + await createAtaInterface(rpc, payer, mint, owner.publicKey); + const ownerAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); + await mintToInterface(rpc, payer, mint, ownerAta, payer, 1_000_000_000); + + const delegate = Keypair.generate(); + + // Build approve instruction batches. + // Returns TransactionInstruction[][] — send [0..n-2] first (load batches), + // then send [n-1] (the approve instruction). + const batches = await createApproveInterfaceInstructions( + rpc, + payer.publicKey, + mint, + ownerAta, + delegate.publicKey, + 500_000_000, + owner.publicKey, + 9 + ); + + // Send load batches sequentially, then the final approve batch + for (let i = 0; i < batches.length - 1; i++) { + const tx = new Transaction().add(...batches[i]); + await sendAndConfirmTransaction(rpc, tx, [payer, owner]); + } + + const approveTx = new Transaction().add(...batches[batches.length - 1]); + const signature = await sendAndConfirmTransaction(rpc, approveTx, [ + payer, + owner, + ]); + + console.log("Approved delegate:", delegate.publicKey.toBase58()); + console.log("Allowance: 500,000,000 tokens"); + console.log("Tx:", signature); +})(); diff --git a/typescript-client/instructions/delegate-revoke.ts b/typescript-client/instructions/delegate-revoke.ts new file mode 100644 index 00000000..9f339a1f --- /dev/null +++ b/typescript-client/instructions/delegate-revoke.ts @@ -0,0 +1,80 @@ +import "dotenv/config"; +import { Keypair, sendAndConfirmTransaction, Transaction } from "@solana/web3.js"; +import { createRpc } from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + mintToInterface, + getAssociatedTokenAddressInterface, + createApproveInterfaceInstructions, + createRevokeInterfaceInstructions, +} 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); + + const owner = Keypair.generate(); + await createAtaInterface(rpc, payer, mint, owner.publicKey); + const ownerAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); + await mintToInterface(rpc, payer, mint, ownerAta, payer, 1_000_000_000); + + // Approve a delegate first (so we have something to revoke) + const delegate = Keypair.generate(); + const approveBatches = await createApproveInterfaceInstructions( + rpc, + payer.publicKey, + mint, + ownerAta, + delegate.publicKey, + 500_000_000, + owner.publicKey, + 9 + ); + for (const batch of approveBatches) { + const tx = new Transaction().add(...batch); + await sendAndConfirmTransaction(rpc, tx, [payer, owner]); + } + console.log("Approved delegate:", delegate.publicKey.toBase58()); + + // Build revoke instruction batches. + // Returns TransactionInstruction[][] — send [0..n-2] first (load batches), + // then send [n-1] (the revoke instruction). + const revokeBatches = await createRevokeInterfaceInstructions( + rpc, + payer.publicKey, + mint, + ownerAta, + owner.publicKey, + 9 + ); + + for (let i = 0; i < revokeBatches.length - 1; i++) { + const tx = new Transaction().add(...revokeBatches[i]); + await sendAndConfirmTransaction(rpc, tx, [payer, owner]); + } + + const revokeTx = new Transaction().add( + ...revokeBatches[revokeBatches.length - 1] + ); + const signature = await sendAndConfirmTransaction(rpc, revokeTx, [ + payer, + owner, + ]); + + console.log("Revoked all delegate permissions"); + console.log("Tx:", signature); +})(); diff --git a/typescript-client/package.json b/typescript-client/package.json index d94ae090..a0b2c947 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -42,6 +42,9 @@ "load-ata:instruction": "tsx instructions/load-ata.ts", "delegate:approve": "tsx actions/delegate-approve.ts", "delegate:revoke": "tsx actions/delegate-revoke.ts", + "delegate:transfer": "tsx actions/delegate-transfer.ts", + "delegate-approve:instruction": "tsx instructions/delegate-approve.ts", + "delegate-revoke:instruction": "tsx instructions/delegate-revoke.ts", "merge-accounts": "tsx actions/merge-token-accounts.ts" } }