Skip to main content

tengu_api/
sdk.rs

1//! Instruction builders for client SDK. Builds `Instruction` with correct accounts and data.
2
3use solana_program::{
4    instruction::{AccountMeta, Instruction},
5    pubkey::Pubkey,
6};
7use solana_sdk_ids::system_program;
8use spl_associated_token_account::get_associated_token_address;
9
10use crate::{
11    consts::FEE_COLLECTOR,
12    instruction::*,
13    state::*,
14};
15
16fn program_id() -> Pubkey {
17    crate::ID
18}
19
20/// Initialize Config, Game, and Treasury. Admin only.
21pub fn initialize(
22    authority: Pubkey,
23    dojo_mint: Pubkey,
24) -> Instruction {
25    let config_address = config_pda(&program_id()).0;
26    let game_address = game_pda(&program_id()).0;
27    let treasury_address = treasury_pda(&program_id()).0;
28    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
29
30    Instruction {
31        program_id: program_id(),
32        accounts: vec![
33            AccountMeta::new(authority, true),
34            AccountMeta::new(config_address, false),
35            AccountMeta::new(game_address, false),
36            AccountMeta::new(treasury_address, false),
37            AccountMeta::new(treasury_ata, false),
38            AccountMeta::new_readonly(dojo_mint, false),
39            AccountMeta::new_readonly(spl_token::ID, false),
40            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
41            AccountMeta::new_readonly(system_program::ID, false),
42        ],
43        data: Initialize {}.to_bytes(),
44    }
45}
46
47/// Buy Starter Pack (initialize player Dojo). Optional referrer.
48/// Creates 1 starter shogun (assigned to barracks slot 0) + 1 recruitment ticket.
49pub fn buy_starter_pack(
50    signer: Pubkey,
51    referrer: Option<Pubkey>,
52) -> Instruction {
53    let config_address = config_pda(&program_id()).0;
54    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
55    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
56    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
57    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
58    let treasury_address = treasury_pda(&program_id()).0;
59    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
60
61    let referrer_bytes = referrer.map(|p| p.to_bytes()).unwrap_or([0u8; 32]);
62
63    let game_address = game_pda(&program_id()).0;
64    let mut accounts = vec![
65        AccountMeta::new(signer, true),
66        AccountMeta::new(config_address, false),
67        AccountMeta::new(game_address, false),
68        AccountMeta::new(dojo_address, false),
69        AccountMeta::new(barracks_address, false),
70        AccountMeta::new(forge_address, false),
71        AccountMeta::new(tasks_address, false),
72        AccountMeta::new(treasury_address, false),
73        AccountMeta::new(FEE_COLLECTOR, false),
74        AccountMeta::new_readonly(system_program::ID, false),
75        AccountMeta::new(shogun_pool_address, false),
76    ];
77    if let Some(ref_dojo) = referrer {
78        let (referral_address, _) = referral_pda(&program_id(), &ref_dojo);
79        accounts.push(AccountMeta::new_readonly(ref_dojo, false));
80        accounts.push(AccountMeta::new(referral_address, false));
81    }
82
83    Instruction {
84        program_id: program_id(),
85        accounts,
86        data: BuyStarterPack { referrer: referrer_bytes }.to_bytes(),
87    }
88}
89
90/// Recruit shogun(s) — pay with recruitment tickets.
91pub fn recruit_shogun_tickets(
92    signer: Pubkey,
93    count: u64,
94    entropy_var: Pubkey,
95) -> Instruction {
96    let config_address = config_pda(&program_id()).0;
97    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
98    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
99
100    Instruction {
101        program_id: program_id(),
102        accounts: vec![
103            AccountMeta::new(signer, true),
104            AccountMeta::new_readonly(config_address, false),
105            AccountMeta::new(dojo_address, false),
106            AccountMeta::new(shogun_pool_address, false),
107            AccountMeta::new(entropy_var, false),
108            AccountMeta::new_readonly(tengu_entropy::ID, false),
109            AccountMeta::new_readonly(system_program::ID, false),
110        ],
111        data: RecruitShogunTickets {
112            count: count.to_le_bytes(),
113        }
114        .to_bytes(),
115    }
116}
117
118/// Recruit shogun(s) — pay with SOL.
119pub fn recruit_shogun_sol(
120    signer: Pubkey,
121    count: u64,
122    entropy_var: Pubkey,
123) -> Instruction {
124    let config_address = config_pda(&program_id()).0;
125    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
126    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
127
128    Instruction {
129        program_id: program_id(),
130        accounts: vec![
131            AccountMeta::new(signer, true),
132            AccountMeta::new_readonly(config_address, false),
133            AccountMeta::new(dojo_address, false),
134            AccountMeta::new(shogun_pool_address, false),
135            AccountMeta::new(entropy_var, false),
136            AccountMeta::new_readonly(tengu_entropy::ID, false),
137            AccountMeta::new(FEE_COLLECTOR, false),
138            AccountMeta::new_readonly(system_program::ID, false),
139        ],
140        data: RecruitShogunSol {
141            count: count.to_le_bytes(),
142        }
143        .to_bytes(),
144    }
145}
146
147/// Seat shogun in barracks slot. Slot must be empty, shogun must be available (not seated).
148pub fn seat_shogun(signer: Pubkey, pool_index: u64, slot: u64) -> Instruction {
149    let config_address = config_pda(&program_id()).0;
150    let game_address = game_pda(&program_id()).0;
151    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
152    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
153    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
154
155    Instruction {
156        program_id: program_id(),
157        accounts: vec![
158            AccountMeta::new(signer, true),
159            AccountMeta::new_readonly(config_address, false),
160            AccountMeta::new(game_address, false),
161            AccountMeta::new(dojo_address, false),
162            AccountMeta::new(shogun_pool_address, false),
163            AccountMeta::new(barracks_address, false),
164        ],
165        data: SeatShogun {
166            slot: slot.to_le_bytes(),
167            pool_index: pool_index.to_le_bytes(),
168        }
169        .to_bytes(),
170    }
171}
172
173/// Replace seated shogun with available shogun. Slot must be occupied.
174/// New shogun inherits level and chakra from the old one.
175pub fn replace_shogun(signer: Pubkey, slot: u64, new_pool_index: u64) -> Instruction {
176    let config_address = config_pda(&program_id()).0;
177    let game_address = game_pda(&program_id()).0;
178    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
179    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
180    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
181
182    Instruction {
183        program_id: program_id(),
184        accounts: vec![
185            AccountMeta::new(signer, true),
186            AccountMeta::new_readonly(config_address, false),
187            AccountMeta::new(game_address, false),
188            AccountMeta::new(dojo_address, false),
189            AccountMeta::new(shogun_pool_address, false),
190            AccountMeta::new(barracks_address, false),
191        ],
192        data: ReplaceShogun {
193            slot: slot.to_le_bytes(),
194            new_pool_index: new_pool_index.to_le_bytes(),
195        }
196        .to_bytes(),
197    }
198}
199
200/// Seat multiple shoguns in empty slots (fill-all). Each (pool_index, slot) must have slot empty.
201/// Returns instructions to be combined into a single transaction.
202/// Each element is `(pool_index, slot)` for an open slot. Caller determines open slots and ninja order.
203pub fn seat_shogun_fill_all(
204    signer: Pubkey,
205    assignments: impl IntoIterator<Item = (u64, u64)>,
206) -> Vec<Instruction> {
207    assignments
208        .into_iter()
209        .map(|(pool_index, slot)| seat_shogun(signer, pool_index, slot))
210        .collect()
211}
212
213/// Dine. Tier: 0=24h, 1=48h, 2=72h. Burns shards.
214pub fn dine(signer: Pubkey, dojo_mint: Pubkey, pool_index: u64, tier: u64) -> Instruction {
215    let config_address = config_pda(&program_id()).0;
216    let game_address = game_pda(&program_id()).0;
217    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
218    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
219    let treasury_address = treasury_pda(&program_id()).0;
220    let user_ata = get_associated_token_address(&signer, &dojo_mint);
221    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
222
223    Instruction {
224        program_id: program_id(),
225        accounts: vec![
226            AccountMeta::new(signer, true),
227            AccountMeta::new_readonly(config_address, false),
228            AccountMeta::new(game_address, false),
229            AccountMeta::new(dojo_address, false),
230            AccountMeta::new(shogun_pool_address, false),
231            AccountMeta::new(user_ata, false),
232            AccountMeta::new(treasury_ata, false),
233            AccountMeta::new(dojo_mint, false),
234            AccountMeta::new(treasury_address, false),
235            AccountMeta::new_readonly(spl_token::ID, false),
236        ],
237        data: Dine {
238            tier: tier.to_le_bytes(),
239            pool_index: pool_index.to_le_bytes(),
240        }
241        .to_bytes(),
242    }
243}
244
245/// Upgrade barracks (Ninja Hut) level. Pay with shards. 1→2, 2→3, 3→4. Burns shards.
246pub fn upgrade_barracks_shards(signer: Pubkey, dojo_mint: Pubkey) -> Instruction {
247    let config_address = config_pda(&program_id()).0;
248    let game_address = game_pda(&program_id()).0;
249    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
250    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
251    let treasury_address = treasury_pda(&program_id()).0;
252    let user_ata = get_associated_token_address(&signer, &dojo_mint);
253    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
254
255    Instruction {
256        program_id: program_id(),
257        accounts: vec![
258            AccountMeta::new(signer, true),
259            AccountMeta::new_readonly(config_address, false),
260            AccountMeta::new(game_address, false),
261            AccountMeta::new(dojo_address, false),
262            AccountMeta::new(barracks_address, false),
263            AccountMeta::new(user_ata, false),
264            AccountMeta::new(treasury_ata, false),
265            AccountMeta::new(dojo_mint, false),
266            AccountMeta::new(treasury_address, false),
267            AccountMeta::new_readonly(spl_token::ID, false),
268        ],
269        data: UpgradeBarracksShards {}.to_bytes(),
270    }
271}
272
273/// Upgrade barracks (Ninja Hut) level. Pay with SOL. 1→2, 2→3 only (3→4 shards only).
274pub fn upgrade_barracks_sol(signer: Pubkey) -> Instruction {
275    let config_address = config_pda(&program_id()).0;
276    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
277    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
278
279    Instruction {
280        program_id: program_id(),
281        accounts: vec![
282            AccountMeta::new(signer, true),
283            AccountMeta::new_readonly(config_address, false),
284            AccountMeta::new(dojo_address, false),
285            AccountMeta::new(barracks_address, false),
286            AccountMeta::new(FEE_COLLECTOR, false),
287            AccountMeta::new_readonly(system_program::ID, false),
288        ],
289        data: UpgradeBarracksSol {}.to_bytes(),
290    }
291}
292
293/// Upgrade forge level. Pay SOL (1–4) or burn shards (5–7).
294pub fn upgrade_forge(signer: Pubkey, dojo_mint: Pubkey) -> Instruction {
295    let config_address = config_pda(&program_id()).0;
296    let game_address = game_pda(&program_id()).0;
297    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
298    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
299    let treasury_address = treasury_pda(&program_id()).0;
300    let user_ata = get_associated_token_address(&signer, &dojo_mint);
301    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
302
303    Instruction {
304        program_id: program_id(),
305        accounts: vec![
306            AccountMeta::new(signer, true),
307            AccountMeta::new_readonly(config_address, false),
308            AccountMeta::new(game_address, false),
309            AccountMeta::new(dojo_address, false),
310            AccountMeta::new(forge_address, false),
311            AccountMeta::new(FEE_COLLECTOR, false),
312            AccountMeta::new_readonly(system_program::ID, false),
313            AccountMeta::new(user_ata, false),
314            AccountMeta::new(treasury_ata, false),
315            AccountMeta::new(dojo_mint, false),
316            AccountMeta::new(treasury_address, false),
317            AccountMeta::new_readonly(spl_token::ID, false),
318        ],
319        data: UpgradeForge {}.to_bytes(),
320    }
321}
322
323/// Merge shoguns. merge_type: 0=10×N, 1=5×R, 2=3×SR.
324pub fn merge_shogun(
325    signer: Pubkey,
326    merge_type: u64,
327    entropy_var: Pubkey,
328    pool_indices: [u64; 10],
329) -> Instruction {
330    let config_address = config_pda(&program_id()).0;
331    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
332    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
333    let game_address = game_pda(&program_id()).0;
334
335    Instruction {
336        program_id: program_id(),
337        accounts: vec![
338            AccountMeta::new(signer, true),
339            AccountMeta::new(config_address, false),
340            AccountMeta::new(game_address, false),
341            AccountMeta::new(dojo_address, false),
342            AccountMeta::new(shogun_pool_address, false),
343            AccountMeta::new(entropy_var, false),
344            AccountMeta::new_readonly(tengu_entropy::ID, false),
345        ],
346        data: MergeShogun {
347            merge_type: merge_type.to_le_bytes(),
348            pool_indices,
349        }
350        .to_bytes(),
351    }
352}
353
354/// Prestige upgrade (SSR/UR with 2 duplicate fodder). Uses pool indices.
355pub fn prestige_upgrade(
356    signer: Pubkey,
357    target_pool_index: u64,
358    fodder_1_pool_index: u64,
359    fodder_2_pool_index: u64,
360) -> Instruction {
361    let config_address = config_pda(&program_id()).0;
362    let game_address = game_pda(&program_id()).0;
363    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
364    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
365
366    Instruction {
367        program_id: program_id(),
368        accounts: vec![
369            AccountMeta::new(signer, true),
370            AccountMeta::new_readonly(config_address, false),
371            AccountMeta::new(game_address, false),
372            AccountMeta::new(dojo_address, false),
373            AccountMeta::new(shogun_pool_address, false),
374        ],
375        data: PrestigeUpgrade {
376            target_pool_index: target_pool_index.to_le_bytes(),
377            fodder_1_pool_index: fodder_1_pool_index.to_le_bytes(),
378            fodder_2_pool_index: fodder_2_pool_index.to_le_bytes(),
379        }
380        .to_bytes(),
381    }
382}
383
384/// Level up shogun: spend shards, +10% SP per level. Burns shards.
385pub fn level_up_shogun(signer: Pubkey, dojo_mint: Pubkey, pool_index: u64) -> Instruction {
386    let config_address = config_pda(&program_id()).0;
387    let game_address = game_pda(&program_id()).0;
388    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
389    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
390    let treasury_address = treasury_pda(&program_id()).0;
391    let user_ata = get_associated_token_address(&signer, &dojo_mint);
392    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
393
394    Instruction {
395        program_id: program_id(),
396        accounts: vec![
397            AccountMeta::new(signer, true),
398            AccountMeta::new_readonly(config_address, false),
399            AccountMeta::new(game_address, false),
400            AccountMeta::new(dojo_address, false),
401            AccountMeta::new(shogun_pool_address, false),
402            AccountMeta::new(user_ata, false),
403            AccountMeta::new(treasury_ata, false),
404            AccountMeta::new(dojo_mint, false),
405            AccountMeta::new(treasury_address, false),
406            AccountMeta::new_readonly(spl_token::ID, false),
407        ],
408        data: LevelUpShogun {
409            pool_index: pool_index.to_le_bytes(),
410        }
411        .to_bytes(),
412    }
413}
414
415/// Claim shards as $DOJO token. Hyper Ninja–aligned: computes ore from shoguns, adds to shards, mints.
416/// Amount computed entirely on-chain; no client input (security).
417pub fn claim_shards(signer: Pubkey, dojo_mint: Pubkey) -> Instruction {
418    let config_address = config_pda(&program_id()).0;
419    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
420    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
421    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
422    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
423    let treasury_address = treasury_pda(&program_id()).0;
424    let dojo_ata = get_associated_token_address(&signer, &dojo_mint);
425
426    Instruction {
427        program_id: program_id(),
428        accounts: vec![
429            AccountMeta::new(signer, true),
430            AccountMeta::new_readonly(config_address, false),
431            AccountMeta::new(dojo_address, false),
432            AccountMeta::new(forge_address, false),
433            AccountMeta::new(barracks_address, false),
434            AccountMeta::new(shogun_pool_address, false),
435            AccountMeta::new(dojo_ata, false),
436            AccountMeta::new(dojo_mint, false),
437            AccountMeta::new(treasury_address, false),
438            AccountMeta::new_readonly(spl_token::ID, false),
439        ],
440        data: ClaimShards {}.to_bytes(),
441    }
442}
443
444/// Claim referral reward (SOL).
445pub fn claim_referral_reward(signer: Pubkey, referrer_dojo: Pubkey) -> Instruction {
446    let (referral_address, _) = referral_pda(&program_id(), &referrer_dojo);
447    let treasury_address = treasury_pda(&program_id()).0;
448
449    Instruction {
450        program_id: program_id(),
451        accounts: vec![
452            AccountMeta::new(signer, true),
453            AccountMeta::new(referrer_dojo, false),
454            AccountMeta::new(referral_address, false),
455            AccountMeta::new(treasury_address, false),
456            AccountMeta::new_readonly(system_program::ID, false),
457        ],
458        data: ClaimReferralReward {}.to_bytes(),
459    }
460}
461
462/// Claim next recruit-tier reward.
463pub fn claim_recruit_reward(signer: Pubkey) -> Instruction {
464    let config_address = config_pda(&program_id()).0;
465    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
466    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
467
468    Instruction {
469        program_id: program_id(),
470        accounts: vec![
471            AccountMeta::new(signer, true),
472            AccountMeta::new_readonly(config_address, false),
473            AccountMeta::new(dojo_address, false),
474            AccountMeta::new(tasks_address, false),
475        ],
476        data: ClaimRecruitReward {}.to_bytes(),
477    }
478}
479
480/// Claim next forge-tier reward.
481pub fn claim_forge_reward(signer: Pubkey) -> Instruction {
482    let config_address = config_pda(&program_id()).0;
483    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
484    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
485
486    Instruction {
487        program_id: program_id(),
488        accounts: vec![
489            AccountMeta::new(signer, true),
490            AccountMeta::new_readonly(config_address, false),
491            AccountMeta::new(dojo_address, false),
492            AccountMeta::new(tasks_address, false),
493        ],
494        data: ClaimForgeReward {}.to_bytes(),
495    }
496}
497
498/// Claim next dine-tier reward.
499pub fn claim_dine_reward(signer: Pubkey) -> Instruction {
500    let config_address = config_pda(&program_id()).0;
501    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
502    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
503
504    Instruction {
505        program_id: program_id(),
506        accounts: vec![
507            AccountMeta::new(signer, true),
508            AccountMeta::new_readonly(config_address, false),
509            AccountMeta::new(dojo_address, false),
510            AccountMeta::new(tasks_address, false),
511        ],
512        data: ClaimDineReward {}.to_bytes(),
513    }
514}
515
516/// Ed25519 verify instruction for daily claim. Must be prepended before claim_daily_reward.
517/// Client builds the same message as the server signs: prefix + dojo_pda + task_id.
518pub fn ed25519_verify_instruction_for_daily_claim(
519    dojo_pda: Pubkey,
520    signature: [u8; 64],
521) -> Instruction {
522    use crate::consts::{CLAIM_TASK_PREFIX, DAILY_TASK_START, TASK_VERIFIER};
523    let mut message = Vec::with_capacity(CLAIM_TASK_PREFIX.len() + 32 + 8);
524    message.extend_from_slice(CLAIM_TASK_PREFIX);
525    message.extend_from_slice(dojo_pda.as_ref());
526    message.extend_from_slice(&DAILY_TASK_START.to_le_bytes());
527    let verifier_bytes: [u8; 32] = TASK_VERIFIER.to_bytes();
528    crate::utils::new_ed25519_instruction_with_signature(&message, &signature, &verifier_bytes)
529}
530
531/// Claim daily reward (1 ticket per day, stacks if not claimed). Backend signature required.
532/// Transaction must include ed25519_verify_instruction_for_daily_claim as the preceding instruction.
533pub fn claim_daily_reward(signer: Pubkey, signature: [u8; 64]) -> Instruction {
534    let config_address = config_pda(&program_id()).0;
535    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
536    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
537    let instructions_sysvar = solana_program::sysvar::instructions::ID;
538
539    Instruction {
540        program_id: program_id(),
541        accounts: vec![
542            AccountMeta::new(signer, true),
543            AccountMeta::new_readonly(config_address, false),
544            AccountMeta::new(dojo_address, false),
545            AccountMeta::new(tasks_address, false),
546            AccountMeta::new_readonly(instructions_sysvar, false),
547        ],
548        data: ClaimDailyReward { signature }.to_bytes(),
549    }
550}
551
552/// Claim collection reward (3 ninjas same element+rarity). Pass pool indices.
553pub fn claim_collection_reward(signer: Pubkey, pool_indices: [u64; 3]) -> Instruction {
554    let config_address = config_pda(&program_id()).0;
555    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
556    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
557    let (shogun_pool_address, _) = shogun_account_pda(&program_id(), &dojo_address);
558
559    Instruction {
560        program_id: program_id(),
561        accounts: vec![
562            AccountMeta::new(signer, true),
563            AccountMeta::new_readonly(config_address, false),
564            AccountMeta::new(dojo_address, false),
565            AccountMeta::new(tasks_address, false),
566            AccountMeta::new(shogun_pool_address, false),
567        ],
568        data: ClaimCollectionReward { pool_indices }.to_bytes(),
569    }
570}
571
572/// Flash sale: 50 tickets for 5000 shards, max 5 per day.
573pub fn buy_flash_sale(signer: Pubkey, dojo_mint: Pubkey) -> Instruction {
574    let config_address = config_pda(&program_id()).0;
575    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
576    let treasury_address = treasury_pda(&program_id()).0;
577    let user_ata = get_associated_token_address(&signer, &dojo_mint);
578    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
579
580    Instruction {
581        program_id: program_id(),
582        accounts: vec![
583            AccountMeta::new(signer, true),
584            AccountMeta::new_readonly(config_address, false),
585            AccountMeta::new(dojo_address, false),
586            AccountMeta::new(user_ata, false),
587            AccountMeta::new_readonly(treasury_address, false),
588            AccountMeta::new(treasury_ata, false),
589            AccountMeta::new_readonly(spl_token::ID, false),
590        ],
591        data: BuyFlashSale {}.to_bytes(),
592    }
593}
594
595/// Daily deal: 5 tickets for 300 shards. Burns shards.
596pub fn buy_tickets_with_shards(signer: Pubkey, dojo_mint: Pubkey) -> Instruction {
597    let config_address = config_pda(&program_id()).0;
598    let game_address = game_pda(&program_id()).0;
599    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
600    let treasury_address = treasury_pda(&program_id()).0;
601    let user_ata = get_associated_token_address(&signer, &dojo_mint);
602    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
603
604    Instruction {
605        program_id: program_id(),
606        accounts: vec![
607            AccountMeta::new(signer, true),
608            AccountMeta::new_readonly(config_address, false),
609            AccountMeta::new(game_address, false),
610            AccountMeta::new(dojo_address, false),
611            AccountMeta::new(user_ata, false),
612            AccountMeta::new(treasury_ata, false),
613            AccountMeta::new(dojo_mint, false),
614            AccountMeta::new(treasury_address, false),
615            AccountMeta::new_readonly(spl_token::ID, false),
616        ],
617        data: BuyTicketsWithShards {}.to_bytes(),
618    }
619}
620
621/// Buy bundle: 150 recruitment tickets for 5 SOL (event deal).
622pub fn buy_bundle(signer: Pubkey) -> Instruction {
623    let config_address = config_pda(&program_id()).0;
624    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
625
626    Instruction {
627        program_id: program_id(),
628        accounts: vec![
629            AccountMeta::new(signer, true),
630            AccountMeta::new_readonly(config_address, false),
631            AccountMeta::new(dojo_address, false),
632            AccountMeta::new(FEE_COLLECTOR, false),
633            AccountMeta::new_readonly(system_program::ID, false),
634        ],
635        data: BuyBundle {}.to_bytes(),
636    }
637}
638
639/// Speed up forge upgrade cooldown. Burns shards.
640pub fn speed_up_forge(signer: Pubkey, dojo_mint: Pubkey, seconds: u64) -> Instruction {
641    let config_address = config_pda(&program_id()).0;
642    let game_address = game_pda(&program_id()).0;
643    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
644    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
645    let treasury_address = treasury_pda(&program_id()).0;
646    let user_ata = get_associated_token_address(&signer, &dojo_mint);
647    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
648
649    Instruction {
650        program_id: program_id(),
651        accounts: vec![
652            AccountMeta::new(signer, true),
653            AccountMeta::new_readonly(config_address, false),
654            AccountMeta::new(game_address, false),
655            AccountMeta::new(dojo_address, false),
656            AccountMeta::new(forge_address, false),
657            AccountMeta::new(user_ata, false),
658            AccountMeta::new(treasury_ata, false),
659            AccountMeta::new(dojo_mint, false),
660            AccountMeta::new(treasury_address, false),
661            AccountMeta::new_readonly(spl_token::ID, false),
662        ],
663        data: SpeedUpForge {
664            seconds: seconds.to_le_bytes(),
665        }
666        .to_bytes(),
667    }
668}
669
670/// Create new Entropy Var (admin).
671pub fn new_var(
672    authority: Pubkey,
673    provider: Pubkey,
674    id: u64,
675    commit: [u8; 32],
676    end_at: u64,
677    samples: u64,
678    is_auto: bool,
679) -> Instruction {
680    let config_address = config_pda(&program_id()).0;
681    let var_address = tengu_entropy::state::var_pda(config_address, id).0;
682
683    Instruction {
684        program_id: program_id(),
685        accounts: vec![
686            AccountMeta::new(authority, true),
687            AccountMeta::new(config_address, false),
688            AccountMeta::new(provider, false),
689            AccountMeta::new(var_address, false),
690            AccountMeta::new_readonly(system_program::ID, false),
691            AccountMeta::new_readonly(tengu_entropy::ID, false),
692        ],
693        data: NewVar {
694            id: id.to_le_bytes(),
695            commit,
696            provider: provider.to_bytes(),
697            end_at: end_at.to_le_bytes(),
698            samples: samples.to_le_bytes(),
699            is_auto: (is_auto as u64).to_le_bytes(),
700        }
701        .to_bytes(),
702    }
703}
704
705/// Set genesis slot and game.last_emission_slot (admin). halving_period_slots: 0 = use default (~58 days, matches Hyper Ninja).
706pub fn set_genesis_slot(authority: Pubkey, genesis_slot: u64, halving_period_slots: u64) -> Instruction {
707    let config_address = config_pda(&program_id()).0;
708    let game_address = game_pda(&program_id()).0;
709
710    Instruction {
711        program_id: program_id(),
712        accounts: vec![
713            AccountMeta::new(authority, true),
714            AccountMeta::new(config_address, false),
715            AccountMeta::new(game_address, false),
716        ],
717        data: SetGenesisSlot {
718            genesis_slot: genesis_slot.to_le_bytes(),
719            halving_period_slots: halving_period_slots.to_le_bytes(),
720        }
721        .to_bytes(),
722    }
723}
724
725/// Set entropy var address (admin).
726pub fn set_var_address(authority: Pubkey, entropy_var: Pubkey) -> Instruction {
727    let config_address = config_pda(&program_id()).0;
728
729    Instruction {
730        program_id: program_id(),
731        accounts: vec![
732            AccountMeta::new(authority, true),
733            AccountMeta::new(config_address, false),
734        ],
735        data: SetVarAddress {
736            entropy_var: entropy_var.to_bytes(),
737        }
738        .to_bytes(),
739    }
740}
741
742/// Roll scene sections (1 or 10) — pay with Amethyst.
743pub fn roll_scene_section_amethyst(
744    signer: Pubkey,
745    count: u64,
746    entropy_var: Pubkey,
747) -> Instruction {
748    let config_address = config_pda(&program_id()).0;
749    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
750    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
751
752    Instruction {
753        program_id: program_id(),
754        accounts: vec![
755            AccountMeta::new(signer, true),
756            AccountMeta::new_readonly(config_address, false),
757            AccountMeta::new(dojo_address, false),
758            AccountMeta::new(scenes_address, false),
759            AccountMeta::new(entropy_var, false),
760            AccountMeta::new_readonly(tengu_entropy::ID, false),
761            AccountMeta::new_readonly(system_program::ID, false),
762        ],
763        data: RollSceneSectionAmethyst {
764            count: count.to_le_bytes(),
765        }
766        .to_bytes(),
767    }
768}
769
770/// Roll scene sections (1 or 10) — pay with Shards (SPL $DOJO).
771pub fn roll_scene_section_shards(
772    signer: Pubkey,
773    count: u64,
774    dojo_mint: Pubkey,
775    entropy_var: Pubkey,
776) -> Instruction {
777    let config_address = config_pda(&program_id()).0;
778    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
779    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
780    let treasury_address = treasury_pda(&program_id()).0;
781    let user_ata = get_associated_token_address(&signer, &dojo_mint);
782    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
783
784    Instruction {
785        program_id: program_id(),
786        accounts: vec![
787            AccountMeta::new(signer, true),
788            AccountMeta::new_readonly(config_address, false),
789            AccountMeta::new(dojo_address, false),
790            AccountMeta::new(scenes_address, false),
791            AccountMeta::new(entropy_var, false),
792            AccountMeta::new_readonly(tengu_entropy::ID, false),
793            AccountMeta::new(user_ata, false),
794            AccountMeta::new(treasury_ata, false),
795            AccountMeta::new_readonly(spl_token::ID, false),
796            AccountMeta::new_readonly(system_program::ID, false),
797        ],
798        data: RollSceneSectionShards {
799            count: count.to_le_bytes(),
800        }
801        .to_bytes(),
802    }
803}
804
805/// Salvage duplicate scene sections for Amethyst refund. Single ix handles any amount.
806/// items: (scene_id, section_id, count) — max 64 entries. scene_id 0–8, section_id 0–11.
807pub fn salvage_scene_section(
808    signer: Pubkey,
809    items: impl IntoIterator<Item = (u8, u8, u16)>,
810) -> Instruction {
811    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
812    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
813
814    let items: Vec<_> = items.into_iter().take(64).collect();
815    let item_count = items.len() as u8;
816    let mut salvage_items = [SalvageItem {
817        scene_id: 0,
818        section_id: 0,
819        count: 0,
820    }; 64];
821    for (i, (scene_id, section_id, count)) in items.into_iter().enumerate() {
822        salvage_items[i] = SalvageItem {
823            scene_id,
824            section_id,
825            count,
826        };
827    }
828
829    Instruction {
830        program_id: program_id(),
831        accounts: vec![
832            AccountMeta::new(signer, true),
833            AccountMeta::new(dojo_address, false),
834            AccountMeta::new(scenes_address, false),
835        ],
836        data: SalvageSceneSection {
837            item_count,
838            _pad: [0; 3],
839            items: salvage_items,
840        }
841        .to_bytes(),
842    }
843}
844
845/// Buy scene 6, 7, or 8 with Amethyst. Unlocks entire scene (all 12 sections).
846pub fn buy_scene(signer: Pubkey, scene_id: u64) -> Instruction {
847    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
848    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
849
850    Instruction {
851        program_id: program_id(),
852        accounts: vec![
853            AccountMeta::new(signer, true),
854            AccountMeta::new(dojo_address, false),
855            AccountMeta::new(scenes_address, false),
856            AccountMeta::new_readonly(system_program::ID, false),
857        ],
858        data: BuyScene {
859            scene_id: scene_id.to_le_bytes(),
860        }
861        .to_bytes(),
862    }
863}
864
865/// Buy scene (6/7/8) with mixed payment: spend all amethyst, cover shortfall with DOJO.
866pub fn buy_scene_dojo(signer: Pubkey, dojo_mint: Pubkey, scene_id: u64) -> Instruction {
867    let config_address = config_pda(&program_id()).0;
868    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
869    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
870    let treasury_address = treasury_pda(&program_id()).0;
871    let user_ata = get_associated_token_address(&signer, &dojo_mint);
872    let treasury_ata = get_associated_token_address(&treasury_address, &dojo_mint);
873
874    Instruction {
875        program_id: program_id(),
876        accounts: vec![
877            AccountMeta::new(signer, true),
878            AccountMeta::new_readonly(config_address, false),
879            AccountMeta::new(dojo_address, false),
880            AccountMeta::new(scenes_address, false),
881            AccountMeta::new(user_ata, false),
882            AccountMeta::new(treasury_ata, false),
883            AccountMeta::new(dojo_mint, false),
884            AccountMeta::new(treasury_address, false),
885            AccountMeta::new_readonly(spl_token::ID, false),
886            AccountMeta::new_readonly(system_program::ID, false),
887        ],
888        data: BuySceneDojo {
889            scene_id: scene_id.to_le_bytes(),
890        }
891        .to_bytes(),
892    }
893}
894
895/// Log (CPI from program; config signs). Variable-length message.
896pub fn log(config: Pubkey, msg: &[u8]) -> Instruction {
897    let mut data = Log {
898        _reserved: [0u8; 8],
899    }
900    .to_bytes();
901    data.extend_from_slice(msg);
902    Instruction {
903        program_id: program_id(),
904        accounts: vec![AccountMeta::new(config, true)],
905        data,
906    }
907}