Skip to main content

airdrop_api/
sdk.rs

1// SDK functions for building instructions and interacting with the airdrop program
2use crate::instruction::*;
3use crate::pda::*;
4use solana_program::{
5    instruction::{AccountMeta, Instruction},
6    program_error::ProgramError,
7    pubkey::Pubkey,
8    sysvar,
9};
10use spl_associated_token_account::get_associated_token_address;
11
12/// Create an instruction to initialize the program
13pub fn initialize(
14    initializer: &Pubkey,
15    admin: Pubkey,
16    mint_fee_lamports: u64,
17    allocation_fee_per_recipient_lamports: u64,
18    merkle_fee_per_recipient_lamports: u64,
19    direct_transfer_fee_per_recipient_lamports: u64,
20) -> Result<Instruction, ProgramError> {
21    let (config_pda, _) = config_pda();
22    let (fee_account_pda, _) = fee_account_pda();
23
24    let data = Initialize {
25        admin,
26        mint_fee_lamports: mint_fee_lamports.to_le_bytes(),
27        allocation_fee_per_recipient_lamports: allocation_fee_per_recipient_lamports.to_le_bytes(),
28        merkle_fee_per_recipient_lamports: merkle_fee_per_recipient_lamports.to_le_bytes(),
29        direct_transfer_fee_per_recipient_lamports: direct_transfer_fee_per_recipient_lamports
30            .to_le_bytes(),
31    };
32
33    let accounts = vec![
34        AccountMeta::new(*initializer, true),
35        AccountMeta::new(config_pda, false),
36        AccountMeta::new(fee_account_pda, false),
37        AccountMeta::new_readonly(solana_program::system_program::ID, false),
38        AccountMeta::new_readonly(sysvar::rent::ID, false),
39    ];
40
41    Ok(Instruction {
42        program_id: crate::id(),
43        accounts,
44        data: data.to_bytes().to_vec(),
45    })
46}
47
48/// Create an instruction to create a new SPL token mint with metadata
49pub fn create_mint(
50    payer: &Pubkey,
51    mint_id: [u8; 32],
52    noise: [u8; 4],
53    mint: Pubkey, // Use Pubkey::default() to derive PDA, or provide specific address
54    name: &[u8],
55    symbol: &[u8],
56    uri: &[u8],
57    decimals: u8,
58    max_supply: u64,
59) -> Result<Instruction, ProgramError> {
60    // Derive PDAs
61    let (config_pda, _) = config_pda();
62    let (fee_account_pda, _) = fee_account_pda();
63    let (mint_pda, _) = mint_pda(&mint_id, &noise);
64    let (mint_authority_pda, _) = mint_authority_pda(&mint_pda);
65    let (mint_treasury_pda, _) = mint_treasury_pda(&mint_pda);
66
67    // Derive metadata PDA (Metaplex standard) - same as miracle-copy
68    let (mint_metadata_pda, _) = Pubkey::find_program_address(
69        &[
70            b"metadata",
71            mpl_token_metadata::ID.as_ref(),
72            mint_pda.as_ref(),
73        ],
74        &mpl_token_metadata::ID,
75    );
76
77    // Derive mint treasury token account (ATA)
78    let mint_treasury_tokens = get_associated_token_address(&mint_treasury_pda, &mint_pda);
79
80    // Pad name, symbol, uri
81    let mut name_padded = [0u8; 32];
82    let name_len = name.len().min(32);
83    name_padded[..name_len].copy_from_slice(&name[..name_len]);
84
85    let mut symbol_padded = [0u8; 10];
86    let symbol_len = symbol.len().min(10);
87    symbol_padded[..symbol_len].copy_from_slice(&symbol[..symbol_len]);
88
89    let mut uri_padded = [0u8; 128];
90    let uri_len = uri.len().min(128);
91    uri_padded[..uri_len].copy_from_slice(&uri[..uri_len]);
92
93    let data = CreateMint {
94        mint_id,
95        noise,
96        mint: if mint == Pubkey::default() {
97            mint_pda
98        } else {
99            mint
100        },
101        name: name_padded,
102        symbol: symbol_padded,
103        uri: uri_padded,
104        decimals,
105        max_supply: max_supply.to_le_bytes(),
106        _padding: [0u8; 3],
107    };
108
109    let accounts = vec![
110        AccountMeta::new(*payer, true),
111        AccountMeta::new_readonly(config_pda, false),
112        AccountMeta::new(mint_pda, false),
113        AccountMeta::new_readonly(mint_authority_pda, false),
114        AccountMeta::new(mint_metadata_pda, false),
115        AccountMeta::new_readonly(mint_treasury_pda, false),
116        AccountMeta::new(mint_treasury_tokens, false),
117        AccountMeta::new(fee_account_pda, false),
118        AccountMeta::new_readonly(solana_program::system_program::ID, false),
119        AccountMeta::new_readonly(spl_token::ID, false),
120        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
121        AccountMeta::new_readonly(mpl_token_metadata::ID, false),
122        AccountMeta::new_readonly(sysvar::rent::ID, false),
123    ];
124
125    Ok(Instruction {
126        program_id: crate::id(),
127        accounts,
128        data: data.to_bytes().to_vec(),
129    })
130}
131
132/// Create an instruction to create a new campaign using an existing mint
133pub fn create_campaign(
134    payer: &Pubkey,
135    campaign_owner: &Pubkey,
136    campaign_id: [u8; 32],
137    mint: Pubkey,
138    merkle_root: [u8; 32],
139    recipient_count: u64,
140    initial_supply: u64,
141    max_supply: u64,
142) -> Result<Instruction, ProgramError> {
143    let (config_pda, _) = config_pda();
144    let (fee_account_pda, _) = fee_account_pda();
145    let (campaign_pda, _) = campaign_pda(&campaign_id);
146    let (mint_treasury_pda, _) = mint_treasury_pda(&mint);
147    let (campaign_treasury_pda, _) = campaign_treasury_pda(&campaign_id);
148
149    // Derive token accounts
150    let mint_treasury_tokens = get_associated_token_address(&mint_treasury_pda, &mint);
151    let campaign_treasury_tokens = get_associated_token_address(&campaign_treasury_pda, &mint);
152
153    let data = CreateCampaign {
154        campaign_id,
155        mint,
156        merkle_root,
157        recipient_count: recipient_count.to_le_bytes(),
158        initial_supply: initial_supply.to_le_bytes(),
159        max_supply: max_supply.to_le_bytes(),
160        _padding: [0u8; 7],
161    };
162
163    let accounts = vec![
164        AccountMeta::new(*payer, true),
165        AccountMeta::new(*campaign_owner, true),
166        AccountMeta::new_readonly(config_pda, false),
167        AccountMeta::new(campaign_pda, false),
168        AccountMeta::new_readonly(mint, false),
169        AccountMeta::new_readonly(mint_treasury_pda, false),
170        AccountMeta::new(mint_treasury_tokens, false),
171        AccountMeta::new_readonly(campaign_treasury_pda, false),
172        AccountMeta::new(campaign_treasury_tokens, false),
173        AccountMeta::new(fee_account_pda, false),
174        AccountMeta::new_readonly(spl_token::ID, false),
175        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
176        AccountMeta::new_readonly(solana_program::system_program::ID, false),
177        AccountMeta::new_readonly(sysvar::rent::ID, false),
178    ];
179
180    Ok(Instruction {
181        program_id: crate::id(),
182        accounts,
183        data: data.to_bytes().to_vec(),
184    })
185}
186
187/// Create an instruction to transfer tokens directly to recipients in batches
188pub fn direct_transfer(
189    owner: &Pubkey,
190    campaign_id: &[u8; 32],
191    campaign: &Pubkey,
192    config: &Pubkey,
193    fee_account: &Pubkey,
194    treasury: &Pubkey,
195    treasury_tokens: &Pubkey,
196    mint: &Pubkey,
197    token_program: &Pubkey,
198    associated_token_program: &Pubkey,
199    system_program: &Pubkey,
200    transfers: Vec<(Pubkey, u64)>,
201) -> Result<Instruction, ProgramError> {
202    if transfers.is_empty() || transfers.len() > 10 {
203        return Err(ProgramError::InvalidInstructionData);
204    }
205
206    let mut data = DirectTransfer {
207        campaign_id: *campaign_id,
208        count: transfers.len() as u8,
209        _padding: [0u8; 7],
210    }
211    .to_bytes()
212    .to_vec();
213
214    // Append transfer data
215    for (recipient, amount) in &transfers {
216        data.extend_from_slice(recipient.as_ref());
217        data.extend_from_slice(&amount.to_le_bytes());
218    }
219
220    let mut accounts = vec![
221        AccountMeta::new(*owner, true),
222        AccountMeta::new_readonly(*campaign, false),
223        AccountMeta::new_readonly(*config, false),
224        AccountMeta::new(*fee_account, false),
225        AccountMeta::new_readonly(*treasury, false),
226        AccountMeta::new(*treasury_tokens, false),
227        AccountMeta::new_readonly(*mint, false),
228        AccountMeta::new_readonly(*token_program, false),
229        AccountMeta::new_readonly(*associated_token_program, false),
230        AccountMeta::new_readonly(*system_program, false),
231    ];
232
233    // Add recipient token accounts
234    for (recipient, _) in &transfers {
235        let recipient_tokens = get_associated_token_address(recipient, mint);
236        accounts.push(AccountMeta::new(recipient_tokens, false));
237    }
238
239    Ok(Instruction {
240        program_id: crate::id(),
241        accounts,
242        data,
243    })
244}
245
246/// Create an instruction to replenish campaign treasury from mint treasury
247pub fn replenish(
248    campaign_owner: &Pubkey,
249    campaign_id: &[u8; 32],
250    campaign: &Pubkey,
251    mint: &Pubkey,
252    mint_treasury: &Pubkey,
253    mint_treasury_tokens: &Pubkey,
254    campaign_treasury: &Pubkey,
255    campaign_treasury_tokens: &Pubkey,
256    token_program: &Pubkey,
257    amount: u64,
258) -> Result<Instruction, ProgramError> {
259    let data = Replenish {
260        campaign_id: *campaign_id,
261        amount: amount.to_le_bytes(),
262        _padding: [0u8; 7],
263    };
264
265    let accounts = vec![
266        AccountMeta::new(*campaign_owner, true),
267        AccountMeta::new(*campaign, false),
268        AccountMeta::new_readonly(*mint, false),
269        AccountMeta::new_readonly(*mint_treasury, false),
270        AccountMeta::new(*mint_treasury_tokens, false),
271        AccountMeta::new_readonly(*campaign_treasury, false),
272        AccountMeta::new(*campaign_treasury_tokens, false),
273        AccountMeta::new_readonly(*token_program, false),
274    ];
275
276    Ok(Instruction {
277        program_id: crate::id(),
278        accounts,
279        data: data.to_bytes().to_vec(),
280    })
281}
282
283/// Create an instruction to update program config (admin-only)
284pub fn update_config(
285    admin: &Pubkey,
286    new_admin: Pubkey,                          // Pubkey::default() = keep current
287    mint_fee_lamports: u64,                     // u64::MAX = keep current
288    allocation_fee_per_recipient_lamports: u64, // u64::MAX = keep current
289    merkle_fee_per_recipient_lamports: u64,     // u64::MAX = keep current
290    direct_transfer_fee_per_recipient_lamports: u64, // u64::MAX = keep current
291) -> Result<Instruction, ProgramError> {
292    let (config_pda, _) = config_pda();
293
294    let data = UpdateConfig {
295        new_admin,
296        mint_fee_lamports: mint_fee_lamports.to_le_bytes(),
297        allocation_fee_per_recipient_lamports: allocation_fee_per_recipient_lamports.to_le_bytes(),
298        merkle_fee_per_recipient_lamports: merkle_fee_per_recipient_lamports.to_le_bytes(),
299        direct_transfer_fee_per_recipient_lamports: direct_transfer_fee_per_recipient_lamports
300            .to_le_bytes(),
301    };
302
303    let accounts = vec![
304        AccountMeta::new(*admin, true),
305        AccountMeta::new(config_pda, false),
306    ];
307
308    Ok(Instruction {
309        program_id: crate::id(),
310        accounts,
311        data: data.to_bytes().to_vec(),
312    })
313}
314
315/// Create an instruction to withdraw accumulated fees (admin-only)
316pub fn withdraw_fees(
317    admin: &Pubkey,
318    config: &Pubkey,
319    fee_account: &Pubkey,
320    beneficiary: &Pubkey,
321    system_program: &Pubkey,
322    amount: u64, // 0 = withdraw all
323) -> Result<Instruction, ProgramError> {
324    let data = WithdrawFees {
325        amount: amount.to_le_bytes(),
326    };
327
328    let accounts = vec![
329        AccountMeta::new(*admin, true),
330        AccountMeta::new_readonly(*config, false),
331        AccountMeta::new(*fee_account, false),
332        AccountMeta::new(*beneficiary, false),
333        AccountMeta::new_readonly(*system_program, false),
334    ];
335
336    Ok(Instruction {
337        program_id: crate::id(),
338        accounts,
339        data: data.to_bytes().to_vec(),
340    })
341}
342
343/// Create an instruction to allocate tokens (on-chain allocation accounts)
344pub fn allocate_onchain(
345    owner: &Pubkey,
346    campaign_id: &[u8; 32],
347    campaign: &Pubkey,
348    config: &Pubkey,
349    fee_account: &Pubkey,
350    treasury_tokens: &Pubkey,
351    token_program: &Pubkey,
352    system_program: &Pubkey,
353    allocations: Vec<(Pubkey, u64)>,
354) -> Result<Instruction, ProgramError> {
355    let mut data = Allocate {
356        campaign_id: *campaign_id,
357        count: allocations.len() as u8,
358        _padding: [0u8; 7],
359    }
360    .to_bytes()
361    .to_vec();
362
363    // Append allocation data
364    for (recipient, amount) in &allocations {
365        data.extend_from_slice(recipient.as_ref());
366        data.extend_from_slice(&amount.to_le_bytes());
367    }
368
369    let mut accounts = vec![
370        AccountMeta::new(*owner, true),
371        AccountMeta::new(*campaign, false),
372        AccountMeta::new_readonly(*config, false),
373        AccountMeta::new(*fee_account, false),
374        AccountMeta::new(*treasury_tokens, false),
375        AccountMeta::new_readonly(*token_program, false),
376        AccountMeta::new_readonly(*system_program, false),
377    ];
378
379    // Add allocation accounts (one per recipient)
380    for (recipient, _) in &allocations {
381        let (allocation_pda, _) = crate::pda::allocation_pda(*campaign, *recipient);
382        accounts.push(AccountMeta::new(allocation_pda, false));
383    }
384
385    Ok(Instruction {
386        program_id: crate::id(),
387        accounts,
388        data,
389    })
390}
391
392/// Create an instruction to claim tokens (on-chain mode)
393pub fn claim_onchain(
394    recipient: &Pubkey,
395    campaign_id: &[u8; 32],
396    campaign: &Pubkey,
397    allocation: &Pubkey,
398    treasury: &Pubkey,
399    treasury_tokens: &Pubkey,
400    recipient_tokens: &Pubkey,
401    mint: &Pubkey,
402    token_program: &Pubkey,
403    associated_token_program: &Pubkey,
404    system_program: &Pubkey,
405) -> Result<Instruction, ProgramError> {
406    let data = Claim {
407        campaign_id: *campaign_id,
408        method: 0, // OnChain mode
409        amount: [0u8; 8],
410        merkle_proof_length: 0,
411        _padding: [0u8; 6],
412    };
413
414    let accounts = vec![
415        AccountMeta::new(*recipient, true),
416        AccountMeta::new_readonly(*campaign, false),
417        AccountMeta::new(*allocation, false),
418        AccountMeta::new_readonly(*treasury, false), // Treasury PDA (for signing)
419        AccountMeta::new(*treasury_tokens, false),
420        AccountMeta::new(*recipient_tokens, false),
421        AccountMeta::new_readonly(*mint, false),
422        AccountMeta::new_readonly(*token_program, false),
423        AccountMeta::new_readonly(*associated_token_program, false),
424        AccountMeta::new_readonly(*system_program, false),
425    ];
426
427    Ok(Instruction {
428        program_id: crate::id(),
429        accounts,
430        data: data.to_bytes().to_vec(),
431    })
432}
433
434/// Create an instruction to claim tokens (off-chain mode)
435/// Note: allocation account is required but can be empty/uninitialized for off-chain mode
436pub fn claim_offchain(
437    recipient: &Pubkey,
438    campaign_id: &[u8; 32],
439    campaign: &Pubkey,
440    allocation: &Pubkey, // Can be empty/uninitialized for off-chain mode
441    treasury: &Pubkey,
442    treasury_tokens: &Pubkey,
443    recipient_tokens: &Pubkey,
444    mint: &Pubkey,
445    token_program: &Pubkey,
446    associated_token_program: &Pubkey,
447    system_program: &Pubkey,
448    amount: u64,
449    signature: &[u8; 64],
450) -> Result<Instruction, ProgramError> {
451    let mut data = Claim {
452        campaign_id: *campaign_id,
453        method: 1, // OffChain mode
454        amount: amount.to_le_bytes(),
455        merkle_proof_length: 0,
456        _padding: [0u8; 6],
457    }
458    .to_bytes()
459    .to_vec();
460
461    // Append signature
462    data.extend_from_slice(signature);
463
464    let accounts = vec![
465        AccountMeta::new(*recipient, true),
466        AccountMeta::new_readonly(*campaign, false),
467        AccountMeta::new_readonly(*allocation, false), // Empty/uninitialized for off-chain mode
468        AccountMeta::new_readonly(*treasury, false),   // Treasury PDA (for signing)
469        AccountMeta::new(*treasury_tokens, false),
470        AccountMeta::new(*recipient_tokens, false),
471        AccountMeta::new_readonly(*mint, false),
472        AccountMeta::new_readonly(*token_program, false),
473        AccountMeta::new_readonly(*associated_token_program, false),
474        AccountMeta::new_readonly(*system_program, false),
475    ];
476
477    Ok(Instruction {
478        program_id: crate::id(),
479        accounts,
480        data,
481    })
482}
483
484/// Create an instruction to claim tokens (merkle mode)
485/// Uses merkle proof verification against campaign's merkle root
486pub fn claim_merkle(
487    recipient: &Pubkey,
488    campaign_id: &[u8; 32],
489    campaign: &Pubkey,
490    allocation: &Pubkey, // Can be empty/uninitialized for merkle mode
491    treasury: &Pubkey,
492    treasury_tokens: &Pubkey,
493    recipient_tokens: &Pubkey,
494    mint: &Pubkey,
495    token_program: &Pubkey,
496    associated_token_program: &Pubkey,
497    system_program: &Pubkey,
498    amount: u64,
499    proof_path: &[[u8; 32]],
500    proof_indices: &[bool],
501) -> Result<Instruction, ProgramError> {
502    if proof_path.len() != proof_indices.len() {
503        return Err(ProgramError::InvalidInstructionData);
504    }
505    if proof_path.len() > crate::consts::MAX_MERKLE_PROOF_LENGTH as usize {
506        return Err(ProgramError::InvalidInstructionData);
507    }
508
509    let proof_length = proof_path.len() as u8;
510    let mut data = Claim {
511        campaign_id: *campaign_id,
512        method: 2, // Merkle mode
513        amount: amount.to_le_bytes(),
514        merkle_proof_length: proof_length,
515        _padding: [0u8; 6],
516    }
517    .to_bytes()
518    .to_vec();
519
520    // Append merkle proof path (32-byte hashes)
521    for hash in proof_path {
522        data.extend_from_slice(hash);
523    }
524
525    // Append merkle proof indices (1-byte booleans: 0=false, 1=true)
526    for &is_right in proof_indices {
527        data.push(if is_right { 1 } else { 0 });
528    }
529
530    let accounts = vec![
531        AccountMeta::new(*recipient, true),
532        AccountMeta::new_readonly(*campaign, false),
533        AccountMeta::new_readonly(*allocation, false), // Can be empty/uninitialized for merkle mode
534        AccountMeta::new_readonly(*treasury, false),   // Treasury PDA (for signing)
535        AccountMeta::new(*treasury_tokens, false),
536        AccountMeta::new(*recipient_tokens, false),
537        AccountMeta::new_readonly(*mint, false),
538        AccountMeta::new_readonly(*token_program, false),
539        AccountMeta::new_readonly(*associated_token_program, false),
540        AccountMeta::new_readonly(*system_program, false),
541    ];
542
543    Ok(Instruction {
544        program_id: crate::id(),
545        accounts,
546        data,
547    })
548}
549
550/// Create an instruction to mint tokens to campaign treasury
551/// Allows campaign owner to mint more tokens after campaign creation
552pub fn mint_tokens(
553    campaign_owner: &Pubkey,
554    campaign_id: &[u8; 32],
555    campaign: &Pubkey,
556    mint: &Pubkey,
557    mint_authority: &Pubkey,
558    treasury_tokens: &Pubkey,
559    token_program: &Pubkey,
560    amount: u64,
561) -> Result<Instruction, ProgramError> {
562    let data = MintTokens {
563        campaign_id: *campaign_id,
564        amount: amount.to_le_bytes(),
565        _padding: [0u8; 7],
566    };
567
568    let accounts = vec![
569        AccountMeta::new(*campaign_owner, true),
570        AccountMeta::new(*campaign, false),
571        AccountMeta::new_readonly(*mint, false),
572        AccountMeta::new_readonly(*mint_authority, false),
573        AccountMeta::new(*treasury_tokens, false),
574        AccountMeta::new_readonly(*token_program, false),
575    ];
576
577    Ok(Instruction {
578        program_id: crate::id(),
579        accounts,
580        data: data.to_bytes().to_vec(),
581    })
582}