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