Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions toolkits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Light-token operations signed with [Privy](https://privy.io) wallets. Server-sid

[Rust program example to stream mint events](streaming-tokens/) of the Light-Token Program.

### Sponsor Rent Top-Ups

Sponsor rent top-ups for users by setting your application as the fee payer.
- **[TypeScript](sponsor-rent-top-ups/typescript/)** - Sponsored Light transfer
- **[Rust](sponsor-rent-top-ups/rust/)** - Sponsored Light transfer

## Documentation

Learn more [about Light-Token here](https://www.zkcompression.com/light-token/welcome).
39 changes: 39 additions & 0 deletions toolkits/sponsor-rent-top-ups/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Sponsor Rent Top-Ups

Light Token sponsors rent-exemption for Solana accounts and keeps them active through periodic top-ups paid by the fee payer. Set your application as the fee payer to abstract away holding SOL from your users — a rent-free experience similar to transaction fee sponsorship.

- **[TypeScript](typescript/)** — Sponsored transfer using `@lightprotocol/compressed-token`
- **[Rust](rust/)** — Sponsored transfer using `light-sdk-client`

Set the `feePayer` field to the public key of the account that will pay the top-ups. Any account that signs the transaction can be the fee payer.

1. Build the transfer instruction with your server as the `feePayer`
2. User signs to authorize the transfer (no SOL needed)
3. Fee payer covers rent top-ups when the transaction lands

> You can set the fee payer to any signing account on any transaction with Light Token.
## Source files

- **[sponsor-top-ups.ts](typescript/sponsor-top-ups.ts)** / **[sponsor-top-ups.rs](rust/sponsor-top-ups.rs)** — Sponsored transfer: creates recipient ATA, transfers tokens with sponsor as fee payer
- **[setup.ts](typescript/setup.ts)** / **[setup.rs](rust/setup.rs)** — Test setup: creates SPL mint, mints tokens, wraps into Light

## Setup

```bash
cd typescript
npm install
```

For localnet:

```bash
npm i -g @lightprotocol/zk-compression-cli@beta
light test-validator
npm run sponsor-top-ups
```

## Documentation

- [Sponsor rent top-ups](https://www.zkcompression.com/light-token/toolkits/sponsor-top-ups)
- [Light Token overview](https://www.zkcompression.com/light-token/welcome)
67 changes: 67 additions & 0 deletions toolkits/sponsor-rent-top-ups/rust/setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Test setup: creates an SPL mint, funds the sponsor, wraps into Light.
// In production the mint already exists (e.g. USDC) and the sender
// already holds Light tokens.

use anchor_spl::token::spl_token;
use light_client::rpc::Rpc;
use light_program_test::LightProgramTest;
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::{pubkey::Pubkey, signature::Keypair, signer::Signer};

pub struct SetupResult {
pub mint: Pubkey,
pub sender_ata: Pubkey,
}

pub async fn setup(
rpc: &mut LightProgramTest,
sponsor: &Keypair,
sender: &Keypair,
) -> Result<SetupResult, Box<dyn std::error::Error>> {
let decimals = 6u8;
let amount = 1_000_000u64;

let mint = setup_spl_mint(rpc, sponsor, decimals).await;
let sponsor_spl_ata = setup_spl_associated_token_account(
rpc, sponsor, &mint, &sponsor.pubkey(), amount,
).await;
let (interface_pda, interface_bump) = find_spl_interface_pda_with_index(&mint, 0, false);

let sender_ata = get_associated_token_address(&sender.pubkey(), &mint);
let create_ata_ix =
CreateAssociatedTokenAccount::new(sponsor.pubkey(), sender.pubkey(), mint).instruction()?;
rpc.create_and_send_transaction(&[create_ata_ix], &sponsor.pubkey(), &[sponsor])
.await?;

let spl_interface = SplInterface {
mint,
spl_token_program: spl_token::ID,
spl_interface_pda: interface_pda,
spl_interface_pda_bump: interface_bump,
};

let wrap_ix = TransferInterface {
source: sponsor_spl_ata,
destination: sender_ata,
amount,
decimals,
authority: sponsor.pubkey(),
payer: sponsor.pubkey(),
spl_interface: Some(spl_interface),
source_owner: spl_token::ID,
destination_owner: LIGHT_TOKEN_PROGRAM_ID,
}
.instruction()?;

rpc.create_and_send_transaction(&[wrap_ix], &sponsor.pubkey(), &[sponsor])
.await?;

Ok(SetupResult { mint, sender_ata })
}
52 changes: 52 additions & 0 deletions toolkits/sponsor-rent-top-ups/rust/sponsor-top-ups.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
mod setup;

use light_client::rpc::Rpc;
use light_program_test::{LightProgramTest, ProgramTestConfig};
use light_token::instruction::{
get_associated_token_address, CreateAssociatedTokenAccount,
TransferInterface, LIGHT_TOKEN_PROGRAM_ID,
};
use solana_sdk::{signature::Keypair, signer::Signer};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(true, None)).await?;

// Top-Up Sponsor: your application server, pays SOL for rent top-ups
let sponsor = rpc.get_payer().insecure_clone();

// User: only signs to authorize the transfer
let sender = Keypair::new();

let setup::SetupResult { mint, sender_ata } =
setup::setup(&mut rpc, &sponsor, &sender).await?;

// Create recipient associated token account
let recipient = Keypair::new();
let recipient_ata = get_associated_token_address(&recipient.pubkey(), &mint);
let create_ata_ix =
CreateAssociatedTokenAccount::new(sponsor.pubkey(), recipient.pubkey(), mint).instruction()?;
rpc.create_and_send_transaction(&[create_ata_ix], &sponsor.pubkey(), &[&sponsor])
.await?;

let transfer_ix = TransferInterface {
source: sender_ata,
destination: recipient_ata,
amount: 500_000,
decimals: 6,
authority: sender.pubkey(),
payer: sponsor.pubkey(),
spl_interface: None,
source_owner: LIGHT_TOKEN_PROGRAM_ID,
destination_owner: LIGHT_TOKEN_PROGRAM_ID,
}
.instruction()?;

let sig = rpc
.create_and_send_transaction(&[transfer_ix], &sponsor.pubkey(), &[&sponsor, &sender])
.await?;

println!("Tx: {sig}");

Ok(())
}
13 changes: 13 additions & 0 deletions toolkits/sponsor-rent-top-ups/typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "light-token-toolkit-sponsor-rent-top-ups",
"version": "1.0.0",
"description": "Sponsor rent top-ups for users",
"type": "module",
"scripts": {
"sponsor-top-ups": "tsx sponsor-top-ups.ts"
},
"dependencies": {
"@lightprotocol/compressed-token": "^0.23.0-beta.9",
"@lightprotocol/stateless.js": "^0.23.0-beta.9"
}
Comment on lines +9 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing workspace entry causes unresolvable dependencies at install time

The new toolkits/sponsor-rent-top-ups/typescript package is not added to the root package.json workspaces array, unlike every other toolkit (e.g. toolkits/payments-and-wallets, toolkits/sign-with-privy/*). This means npm install from the repo root will not link this package into the workspace and will not hoist shared dependencies to it.

Root cause and impact

The root package.json lists shared dependencies (dotenv, @solana/web3.js, @solana/spl-token) and devDependencies (tsx) that all workspace toolkits rely on via hoisting. The existing toolkits/payments-and-wallets works because it's listed in workspaces and inherits these.

The new toolkit's own package.json only declares @lightprotocol/compressed-token and @lightprotocol/stateless.js, but the code imports:

  • dotenv/config (sponsor-top-ups.ts:1)
  • @solana/web3.js (sponsor-top-ups.ts:2, setup.ts:6)
  • @solana/spl-token (setup.ts:14-18)

And the npm script uses tsx (package.json:7).

Without the workspace entry, running npm install at the root won't set up this package, and running cd typescript && npm install standalone will only install the two declared @lightprotocol packages. The import "dotenv/config" at sponsor-top-ups.ts:1 will crash immediately with ERR_MODULE_NOT_FOUND, and npm run sponsor-top-ups will fail with tsx: command not found.

Impact: The toolkit is broken out of the box for anyone following the README instructions (cd typescript && npm install && npm run sponsor-top-ups).

Prompt for agents
Two changes are needed to fix this:

1. In the root package.json, add the new toolkit to the workspaces array. Around line 6, add "toolkits/sponsor-rent-top-ups/typescript" to the workspaces list, e.g.:
   "workspaces": [
       "typescript-client",
       "toolkits/payments-and-wallets",
       "toolkits/sign-with-privy/react",
       "toolkits/sign-with-privy/nodejs",
       "toolkits/sign-with-privy/scripts",
       "toolkits/sponsor-rent-top-ups/typescript"
   ]

This matches the pattern used by all other toolkits and ensures hoisted dependencies (dotenv, tsx, @solana/web3.js, @solana/spl-token) are available.

Alternatively, if standalone usage is intended, add the missing dependencies to toolkits/sponsor-rent-top-ups/typescript/package.json:
   "dependencies": {
       "@lightprotocol/compressed-token": "^0.23.0-beta.9",
       "@lightprotocol/stateless.js": "^0.23.0-beta.9",
       "@solana/web3.js": "1.98.4",
       "@solana/spl-token": "^0.4.13",
       "dotenv": "^16.5.0"
   },
   "devDependencies": {
       "tsx": "^4.7.0"
   }
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
43 changes: 43 additions & 0 deletions toolkits/sponsor-rent-top-ups/typescript/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Test setup: creates an SPL mint, funds the sponsor, wraps into Light.
// In production the mint already exists (e.g. USDC) and the sender
// already holds Light tokens. Use wrap.ts to wrap SPL tokens into Light tokens.
// Use unwrap.ts to unwrap Light tokens into SPL tokens.

import { Keypair } from "@solana/web3.js";
import { Rpc } from "@lightprotocol/stateless.js";
import {
createMintInterface,
createAtaInterface,
getAssociatedTokenAddressInterface,
} from "@lightprotocol/compressed-token";
import { wrap } from "@lightprotocol/compressed-token/unified";
import {
TOKEN_PROGRAM_ID,
createAssociatedTokenAccount,
mintTo,
} from "@solana/spl-token";

export async function setup(
rpc: Rpc,
sponsor: Keypair,
sender: Keypair,
) {
const decimals = 6;
const amount = 1_000_000;

const { mint } = await createMintInterface(
rpc, sponsor, sponsor, null, decimals,
undefined, undefined, TOKEN_PROGRAM_ID,
);

const sponsorSplAta = await createAssociatedTokenAccount(
rpc, sponsor, mint, sponsor.publicKey, undefined, TOKEN_PROGRAM_ID,
);
await mintTo(rpc, sponsor, mint, sponsorSplAta, sponsor, amount);

await createAtaInterface(rpc, sponsor, mint, sender.publicKey);
const senderAta = getAssociatedTokenAddressInterface(mint, sender.publicKey);
await wrap(rpc, sponsor, sponsorSplAta, senderAta, sponsor, mint, BigInt(amount));

return { mint, senderAta };
}
49 changes: 49 additions & 0 deletions toolkits/sponsor-rent-top-ups/typescript/sponsor-top-ups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import "dotenv/config";
import { Keypair, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import {
createAtaInterface,
createLightTokenTransferInstruction,
getAssociatedTokenAddressInterface,
} from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
import { setup } from "./setup.js";

// devnet:
// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// const rpc = createRpc(RPC_URL);
// localnet:
const rpc = createRpc();

// Top-Up Sponsor: your application server, pays SOL for rent top-ups
const sponsor = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")),
),
);

// User: only signs to authorize the transfer
const sender = Keypair.generate();

(async function () {
const { mint, senderAta } = await setup(rpc, sponsor, sender);

// Create recipient associated token account
const recipient = Keypair.generate();
await createAtaInterface(rpc, sponsor, mint, recipient.publicKey);
const recipientAta = getAssociatedTokenAddressInterface(mint, recipient.publicKey);

const ix = createLightTokenTransferInstruction(
senderAta,
recipientAta,
sender.publicKey,
500_000,
sponsor.publicKey,
);

const tx = new Transaction().add(ix);
const sig = await sendAndConfirmTransaction(rpc, tx, [sponsor, sender]);

console.log("Tx:", sig);
})();
15 changes: 15 additions & 0 deletions toolkits/sponsor-rent-top-ups/typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist"
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Loading