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