1+ // SPL to light-token to light-token scenario test
2+ //
3+ // This test demonstrates the complete flow:
4+ // 1. Create SPL mint manually
5+ // 2. Create SPL interface PDA (token pool) using SDK instruction
6+ // 3. Create SPL token account
7+ // 4. Mint SPL tokens
8+ // 5. Create sender's cToken ATA (compressible)
9+ // 6. Transfer SPL tokens to cToken account
10+ // 7. Create recipient cATA + transfer cToken→cToken in SAME transaction
11+ // 8. Verify balances
12+
13+ use anchor_spl:: token:: { spl_token, Mint } ;
14+ use light_client:: rpc:: { LightClient , LightClientConfig , Rpc } ;
15+ use light_ctoken_sdk:: {
16+ ctoken:: { derive_ctoken_ata, CreateAssociatedCTokenAccount , TransferCToken , TransferSplToCtoken } ,
17+ spl_interface:: { find_spl_interface_pda_with_index, CreateSplInterfacePda } ,
18+ } ;
19+ use serde_json;
20+ use solana_sdk:: compute_budget:: ComputeBudgetInstruction ;
21+ use solana_sdk:: program_pack:: Pack ;
22+ use solana_sdk:: { signature:: Keypair , signer:: Signer } ;
23+ use spl_token_2022:: pod:: PodAccount ;
24+ use std:: convert:: TryFrom ;
25+ use std:: env;
26+ use std:: fs;
27+
28+ /// Test SPL → cToken → cToken flow with combined ATA creation + transfer
29+ #[ tokio:: test( flavor = "multi_thread" ) ]
30+ async fn test_spl_to_ctoken_to_ctoken ( ) {
31+ // 1. Setup test environment - load from .env
32+ dotenvy:: dotenv ( ) . ok ( ) ;
33+
34+ let keypair_path = env:: var ( "KEYPAIR_PATH" )
35+ . unwrap_or_else ( |_| format ! ( "{}/.config/solana/id.json" , env:: var( "HOME" ) . unwrap( ) ) ) ;
36+ let payer = load_keypair ( & keypair_path) . expect ( "Failed to load keypair" ) ;
37+ let api_key = env:: var ( "api_key" )
38+ . expect ( "api_key environment variable must be set. Create a .env file or set it in your environment." ) ;
39+
40+ let config = LightClientConfig :: devnet (
41+ Some ( "https://devnet.helius-rpc.com" . to_string ( ) ) ,
42+ Some ( api_key) ,
43+ ) ;
44+ let mut rpc = LightClient :: new_with_retry ( config, None )
45+ . await
46+ . expect ( "Failed to initialize LightClient" ) ;
47+
48+ // 2. Create SPL mint manually
49+ let mint_keypair = Keypair :: new ( ) ;
50+ let mint = mint_keypair. pubkey ( ) ;
51+ let decimals = 2u8 ;
52+
53+ // Get rent for mint account
54+ let mint_rent = rpc
55+ . get_minimum_balance_for_rent_exemption ( Mint :: LEN )
56+ . await
57+ . unwrap ( ) ;
58+
59+ // Create mint account instruction
60+ let create_mint_account_ix = solana_sdk:: system_instruction:: create_account (
61+ & payer. pubkey ( ) ,
62+ & mint,
63+ mint_rent,
64+ Mint :: LEN as u64 ,
65+ & spl_token:: ID ,
66+ ) ;
67+
68+ // Initialize mint instruction
69+ let initialize_mint_ix = spl_token:: instruction:: initialize_mint (
70+ & spl_token:: ID ,
71+ & mint,
72+ & payer. pubkey ( ) , // mint authority
73+ None , // freeze authority
74+ decimals,
75+ )
76+ . unwrap ( ) ;
77+
78+ rpc. create_and_send_transaction (
79+ & [ create_mint_account_ix, initialize_mint_ix] ,
80+ & payer. pubkey ( ) ,
81+ & [ & payer, & mint_keypair] ,
82+ )
83+ . await
84+ . unwrap ( ) ;
85+
86+ // 3. Create SPL interface PDA (token pool) using SDK instruction
87+ let create_spl_interface_pda_ix =
88+ CreateSplInterfacePda :: new ( payer. pubkey ( ) , mint, anchor_spl:: token:: ID ) . instruction ( ) ;
89+
90+ rpc. create_and_send_transaction ( & [ create_spl_interface_pda_ix] , & payer. pubkey ( ) , & [ & payer] )
91+ . await
92+ . unwrap ( ) ;
93+
94+ let mint_amount = 10_000u64 ;
95+ let spl_to_ctoken_amount = 7_000u64 ;
96+ let ctoken_transfer_amount = 3_000u64 ;
97+
98+ // 4. Create SPL token account (inline)
99+ let spl_token_account_keypair = Keypair :: new ( ) ;
100+ let token_account_rent = rpc
101+ . get_minimum_balance_for_rent_exemption ( spl_token:: state:: Account :: LEN )
102+ . await
103+ . unwrap ( ) ;
104+ let create_token_account_ix = solana_sdk:: system_instruction:: create_account (
105+ & payer. pubkey ( ) ,
106+ & spl_token_account_keypair. pubkey ( ) ,
107+ token_account_rent,
108+ spl_token:: state:: Account :: LEN as u64 ,
109+ & spl_token:: ID ,
110+ ) ;
111+ let init_token_account_ix = spl_token:: instruction:: initialize_account (
112+ & spl_token:: ID ,
113+ & spl_token_account_keypair. pubkey ( ) ,
114+ & mint,
115+ & payer. pubkey ( ) ,
116+ )
117+ . unwrap ( ) ;
118+ rpc. create_and_send_transaction (
119+ & [ create_token_account_ix, init_token_account_ix] ,
120+ & payer. pubkey ( ) ,
121+ & [ & spl_token_account_keypair, & payer] ,
122+ )
123+ . await
124+ . unwrap ( ) ;
125+
126+ // 5. Mint SPL tokens to the SPL account (inline)
127+ let mint_to_ix = spl_token:: instruction:: mint_to (
128+ & spl_token:: ID ,
129+ & mint,
130+ & spl_token_account_keypair. pubkey ( ) ,
131+ & payer. pubkey ( ) ,
132+ & [ & payer. pubkey ( ) ] ,
133+ mint_amount,
134+ )
135+ . unwrap ( ) ;
136+ rpc. create_and_send_transaction ( & [ mint_to_ix] , & payer. pubkey ( ) , & [ & payer] )
137+ . await
138+ . unwrap ( ) ;
139+
140+ // Verify SPL account has tokens
141+ let spl_account_data = rpc
142+ . get_account ( spl_token_account_keypair. pubkey ( ) )
143+ . await
144+ . unwrap ( )
145+ . unwrap ( ) ;
146+ let spl_account =
147+ spl_pod:: bytemuck:: pod_from_bytes :: < PodAccount > ( & spl_account_data. data ) . unwrap ( ) ;
148+ let initial_spl_balance: u64 = spl_account. amount . into ( ) ;
149+ assert_eq ! ( initial_spl_balance, mint_amount) ;
150+
151+ // 6. Create sender's cToken ATA (compressible with default 16 prepaid epochs)
152+ let ( sender_ctoken_ata, _bump) = derive_ctoken_ata ( & payer. pubkey ( ) , & mint) ;
153+ let create_ata_instruction =
154+ CreateAssociatedCTokenAccount :: new ( payer. pubkey ( ) , payer. pubkey ( ) , mint)
155+ . instruction ( )
156+ . unwrap ( ) ;
157+
158+ rpc. create_and_send_transaction ( & [ create_ata_instruction] , & payer. pubkey ( ) , & [ & payer] )
159+ . await
160+ . unwrap ( ) ;
161+
162+ // Verify sender's cToken ATA was created
163+ let ctoken_account_data = rpc. get_account ( sender_ctoken_ata) . await . unwrap ( ) . unwrap ( ) ;
164+ assert ! (
165+ !ctoken_account_data. data. is_empty( ) ,
166+ "Sender cToken ATA should exist"
167+ ) ;
168+
169+ // 7. Transfer SPL tokens to sender's cToken account
170+ let ( spl_interface_pda, spl_interface_pda_bump) = find_spl_interface_pda_with_index ( & mint, 0 ) ;
171+
172+ let spl_to_ctoken_ix = TransferSplToCtoken {
173+ amount : spl_to_ctoken_amount,
174+ spl_interface_pda_bump,
175+ source_spl_token_account : spl_token_account_keypair. pubkey ( ) ,
176+ destination_ctoken_account : sender_ctoken_ata,
177+ authority : payer. pubkey ( ) ,
178+ mint,
179+ payer : payer. pubkey ( ) ,
180+ spl_interface_pda,
181+ spl_token_program : anchor_spl:: token:: ID ,
182+ }
183+ . instruction ( )
184+ . unwrap ( ) ;
185+
186+ rpc. create_and_send_transaction ( & [ spl_to_ctoken_ix] , & payer. pubkey ( ) , & [ & payer] )
187+ . await
188+ . unwrap ( ) ;
189+
190+ // 8. Create recipient cATA + transfer cToken→cToken in SAME transaction
191+ let recipient = Keypair :: new ( ) ;
192+ let ( recipient_ctoken_ata, _) = derive_ctoken_ata ( & recipient. pubkey ( ) , & mint) ;
193+
194+ let create_recipient_ata_ix = CreateAssociatedCTokenAccount :: new (
195+ payer. pubkey ( ) ,
196+ recipient. pubkey ( ) ,
197+ mint,
198+ )
199+ . instruction ( )
200+ . unwrap ( ) ;
201+
202+ let ctoken_transfer_ix = TransferCToken {
203+ source : sender_ctoken_ata,
204+ destination : recipient_ctoken_ata,
205+ amount : ctoken_transfer_amount,
206+ authority : payer. pubkey ( ) ,
207+ max_top_up : None ,
208+ }
209+ . instruction ( )
210+ . unwrap ( ) ;
211+
212+ // COMBINED: create recipient ATA + transfer in one transaction
213+ let compute_unit_ix = ComputeBudgetInstruction :: set_compute_unit_limit ( 10_000 ) ;
214+ let tx_id = rpc. create_and_send_transaction (
215+ & [ compute_unit_ix, create_recipient_ata_ix, ctoken_transfer_ix] ,
216+ & payer. pubkey ( ) ,
217+ & [ & payer] ,
218+ )
219+ . await
220+ . unwrap ( ) ;
221+ println ! ( "tx_id: {}" , tx_id) ;
222+
223+ // 9. Verify results
224+ // Check SPL account balance decreased
225+ let spl_account_data = rpc
226+ . get_account ( spl_token_account_keypair. pubkey ( ) )
227+ . await
228+ . unwrap ( )
229+ . unwrap ( ) ;
230+ let spl_account =
231+ spl_pod:: bytemuck:: pod_from_bytes :: < PodAccount > ( & spl_account_data. data ) . unwrap ( ) ;
232+ let final_spl_balance: u64 = spl_account. amount . into ( ) ;
233+ assert_eq ! (
234+ final_spl_balance,
235+ mint_amount - spl_to_ctoken_amount,
236+ "SPL account balance should be 3000"
237+ ) ;
238+
239+ // Check sender cToken balance (7000 - 3000 = 4000)
240+ let sender_ctoken_data = rpc. get_account ( sender_ctoken_ata) . await . unwrap ( ) . unwrap ( ) ;
241+ let sender_ctoken =
242+ spl_pod:: bytemuck:: pod_from_bytes :: < PodAccount > ( & sender_ctoken_data. data [ ..165 ] ) . unwrap ( ) ;
243+ let sender_ctoken_balance: u64 = sender_ctoken. amount . into ( ) ;
244+ assert_eq ! (
245+ sender_ctoken_balance,
246+ spl_to_ctoken_amount - ctoken_transfer_amount,
247+ "Sender cToken balance should be 4000"
248+ ) ;
249+
250+ // Check recipient cToken balance (3000)
251+ let recipient_ctoken_data = rpc. get_account ( recipient_ctoken_ata) . await . unwrap ( ) . unwrap ( ) ;
252+ let recipient_ctoken =
253+ spl_pod:: bytemuck:: pod_from_bytes :: < PodAccount > ( & recipient_ctoken_data. data [ ..165 ] ) . unwrap ( ) ;
254+ let recipient_ctoken_balance: u64 = recipient_ctoken. amount . into ( ) ;
255+ assert_eq ! (
256+ recipient_ctoken_balance, ctoken_transfer_amount,
257+ "Recipient cToken balance should be 3000"
258+ ) ;
259+
260+ println ! ( "SPL → cToken → cToken transfer completed!" ) ;
261+ println ! ( " - Created SPL mint: {}" , mint) ;
262+ println ! ( " - Minted {} tokens to SPL account" , mint_amount) ;
263+ println ! ( " - Transferred {} SPL → sender cToken" , spl_to_ctoken_amount) ;
264+ println ! (
265+ " - Transferred {} cToken → recipient cToken (in same tx as ATA creation)" ,
266+ ctoken_transfer_amount
267+ ) ;
268+ println ! ( "\n Final balances:" ) ;
269+ println ! ( " - SPL account: {}" , final_spl_balance) ;
270+ println ! ( " - Sender cToken: {}" , sender_ctoken_balance) ;
271+ println ! ( " - Recipient cToken: {}" , recipient_ctoken_balance) ;
272+
273+ println ! ( "\n Test passed!" ) ;
274+ }
275+
276+ fn load_keypair ( path : & str ) -> Result < Keypair , Box < dyn std:: error:: Error > > {
277+ let path = if path. starts_with ( "~" ) {
278+ path. replace ( "~" , & env:: var ( "HOME" ) . unwrap_or_default ( ) )
279+ } else {
280+ path. to_string ( )
281+ } ;
282+ let file = fs:: read_to_string ( & path) ?;
283+ let bytes: Vec < u8 > = serde_json:: from_str ( & file) ?;
284+ Ok ( Keypair :: try_from ( & bytes[ ..] ) ?)
285+ }
0 commit comments