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