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/// Ed25519 verify instruction for off-chain task (9–16). Must be prepended before claim_off_chain_task_reward.
651pub fn ed25519_verify_instruction_for_off_chain_task(
652    dojo_pda: Pubkey,
653    task_id: u64,
654    signature: [u8; 64],
655) -> Instruction {
656    use crate::consts::{CLAIM_TASK_PREFIX, TASK_VERIFIER};
657    let mut message = Vec::with_capacity(CLAIM_TASK_PREFIX.len() + 32 + 8);
658    message.extend_from_slice(CLAIM_TASK_PREFIX);
659    message.extend_from_slice(dojo_pda.as_ref());
660    message.extend_from_slice(&task_id.to_le_bytes());
661    let verifier_bytes: [u8; 32] = TASK_VERIFIER.to_bytes();
662    crate::utils::new_ed25519_instruction_with_signature(&message, &signature, &verifier_bytes)
663}
664
665/// Claim off-chain task reward (task_id 9–16). Backend signature required.
666/// Transaction must include ed25519_verify_instruction_for_off_chain_task as the preceding instruction.
667pub fn claim_off_chain_task_reward(
668    signer: Pubkey,
669    task_id: u64,
670    signature: [u8; 64],
671) -> Instruction {
672    let config_address = config_pda(&program_id()).0;
673    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
674    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
675    let instructions_sysvar = solana_program::sysvar::instructions::ID;
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(tasks_address, false),
684            AccountMeta::new_readonly(instructions_sysvar, false),
685        ],
686        data: ClaimOffChainTaskReward {
687            task_id: task_id.to_le_bytes(),
688            signature,
689        }
690        .to_bytes(),
691    }
692}
693
694/// Claim collection reward (3 ninjas same element+rarity). Pass collection_index (element×5 + rarity, 0–24).
695/// Program finds 3 matching shoguns in pool.
696pub fn claim_collection_reward(signer: Pubkey, collection_index: u8) -> Instruction {
697    let config_address = config_pda(&program_id()).0;
698    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
699    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
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(dojo_address, false),
707            AccountMeta::new(tasks_address, false),
708        ],
709        data: ClaimCollectionReward { collection_index }.to_bytes(),
710    }
711}
712
713/// Flash sale: 50 tickets for 5000 shards, max 5 per day.
714pub fn buy_flash_sale(signer: Pubkey) -> Instruction {
715    let config_address = config_pda(&program_id()).0;
716    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
717    let treasury_address = treasury_pda(&program_id()).0;
718    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
719    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
720
721    Instruction {
722        program_id: program_id(),
723        accounts: vec![
724            AccountMeta::new(signer, true),
725            AccountMeta::new_readonly(config_address, false),
726            AccountMeta::new(dojo_address, false),
727            AccountMeta::new(user_ata, false),
728            AccountMeta::new_readonly(treasury_address, false),
729            AccountMeta::new(treasury_ata, false),
730            AccountMeta::new_readonly(spl_token::ID, false),
731        ],
732        data: BuyFlashSale {}.to_bytes(),
733    }
734}
735
736/// Daily deal: 5 tickets for 300 shards. Burns shards.
737pub fn buy_tickets_with_shards(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 treasury_address = treasury_pda(&program_id()).0;
742    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
743    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
744
745    Instruction {
746        program_id: program_id(),
747        accounts: vec![
748            AccountMeta::new(signer, true),
749            AccountMeta::new_readonly(config_address, false),
750            AccountMeta::new(game_address, false),
751            AccountMeta::new(dojo_address, false),
752            AccountMeta::new(user_ata, false),
753            AccountMeta::new(treasury_ata, false),
754            AccountMeta::new(DOJO_MINT, false),
755            AccountMeta::new(treasury_address, false),
756            AccountMeta::new_readonly(spl_token::ID, false),
757        ],
758        data: BuyTicketsWithShards {}.to_bytes(),
759    }
760}
761
762/// Buy bundle: 150 recruitment tickets for 5 SOL (event deal).
763pub fn buy_bundle(signer: Pubkey) -> Instruction {
764    let config_address = config_pda(&program_id()).0;
765    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
766
767    Instruction {
768        program_id: program_id(),
769        accounts: vec![
770            AccountMeta::new(signer, true),
771            AccountMeta::new_readonly(config_address, false),
772            AccountMeta::new(dojo_address, false),
773            AccountMeta::new(FEE_COLLECTOR, false),
774            AccountMeta::new_readonly(system_program::ID, false),
775        ],
776        data: BuyBundle {}.to_bytes(),
777    }
778}
779
780/// Clear forge upgrade cooldown. Cost = remaining minutes (shards). One tx clears all.
781pub fn clear_forge_cooldown(signer: Pubkey) -> Instruction {
782    let config_address = config_pda(&program_id()).0;
783    let game_address = game_pda(&program_id()).0;
784    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
785    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
786    let treasury_address = treasury_pda(&program_id()).0;
787    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
788    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
789
790    Instruction {
791        program_id: program_id(),
792        accounts: vec![
793            AccountMeta::new(signer, true),
794            AccountMeta::new_readonly(config_address, false),
795            AccountMeta::new(game_address, false),
796            AccountMeta::new(dojo_address, false),
797            AccountMeta::new(forge_address, false),
798            AccountMeta::new(user_ata, false),
799            AccountMeta::new(treasury_ata, false),
800            AccountMeta::new(DOJO_MINT, false),
801            AccountMeta::new(treasury_address, false),
802            AccountMeta::new_readonly(spl_token::ID, false),
803        ],
804        data: ClearForgeCooldown {}.to_bytes(),
805    }
806}
807
808/// Set genesis slot and game.last_emission_slot (admin). halving_period_slots: 0 = use default (~58 days, matches Hyper Ninja).
809pub fn set_genesis_slot(authority: Pubkey, genesis_slot: u64, halving_period_slots: u64) -> Instruction {
810    let config_address = config_pda(&program_id()).0;
811    let game_address = game_pda(&program_id()).0;
812
813    Instruction {
814        program_id: program_id(),
815        accounts: vec![
816            AccountMeta::new(authority, true),
817            AccountMeta::new(config_address, false),
818            AccountMeta::new(game_address, false),
819        ],
820        data: SetGenesisSlot {
821            genesis_slot: genesis_slot.to_le_bytes(),
822            halving_period_slots: halving_period_slots.to_le_bytes(),
823        }
824        .to_bytes(),
825    }
826}
827
828/// Roll scene sections (1 or 10) — pay with Amethyst.
829/// seed: from BSM POST /roll/instruction (Option 7 centralized oracle).
830pub fn roll_scene_section_amethyst(signer: Pubkey, count: u64, seed: [u8; 32]) -> Instruction {
831    let config_address = config_pda(&program_id()).0;
832    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
833    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
834
835    Instruction {
836        program_id: program_id(),
837        accounts: vec![
838            AccountMeta::new(signer, true),
839            AccountMeta::new_readonly(config_address, false),
840            AccountMeta::new(dojo_address, false),
841            AccountMeta::new(scenes_address, false),
842            AccountMeta::new_readonly(system_program::ID, false),
843        ],
844        data: RollSceneSectionAmethyst {
845            count: count.to_le_bytes(),
846            seed,
847        }
848        .to_bytes(),
849    }
850}
851
852/// Roll scene sections (1 or 10) — pay with Shards (SPL $DOJO).
853/// seed: from BSM POST /roll/instruction (Option 7 centralized oracle).
854pub fn roll_scene_section_shards(signer: Pubkey, count: u64, seed: [u8; 32]) -> Instruction {
855    let config_address = config_pda(&program_id()).0;
856    let game_address = game_pda(&program_id()).0;
857    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
858    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
859    let treasury_address = treasury_pda(&program_id()).0;
860    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
861    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
862
863    Instruction {
864        program_id: program_id(),
865        accounts: vec![
866            AccountMeta::new(signer, true),
867            AccountMeta::new_readonly(config_address, false),
868            AccountMeta::new(game_address, false),
869            AccountMeta::new(dojo_address, false),
870            AccountMeta::new(scenes_address, false),
871            AccountMeta::new(user_ata, false),
872            AccountMeta::new(treasury_ata, false),
873            AccountMeta::new(DOJO_MINT, false),
874            AccountMeta::new(treasury_address, false),
875            AccountMeta::new_readonly(spl_token::ID, false),
876            AccountMeta::new_readonly(system_program::ID, false),
877        ],
878        data: RollSceneSectionShards {
879            count: count.to_le_bytes(),
880            seed,
881        }
882        .to_bytes(),
883    }
884}
885
886/// Salvage all duplicate scene sections for Amethyst refund. Program derives from on-chain state.
887pub fn salvage_scene_section(signer: Pubkey) -> Instruction {
888    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
889    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
890
891    Instruction {
892        program_id: program_id(),
893        accounts: vec![
894            AccountMeta::new(signer, true),
895            AccountMeta::new(dojo_address, false),
896            AccountMeta::new(scenes_address, false),
897        ],
898        data: SalvageSceneSection {}.to_bytes(),
899    }
900}
901
902/// Set active scene (background). Requires scene unlocked.
903/// Updates Game.total_effective_spirit_power for pool-split.
904pub fn update_active_scene(signer: Pubkey, scene_id: u64) -> Instruction {
905    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
906    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
907    let game_address = game_pda(&program_id()).0;
908    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
909
910    Instruction {
911        program_id: program_id(),
912        accounts: vec![
913            AccountMeta::new(signer, true),
914            AccountMeta::new(dojo_address, false),
915            AccountMeta::new(scenes_address, false),
916            AccountMeta::new(game_address, false),
917            AccountMeta::new(barracks_address, false),
918        ],
919        data: UpdateActiveScene {
920            scene_id: scene_id.to_le_bytes(),
921        }
922        .to_bytes(),
923    }
924}
925
926/// Buy scene 6, 7, or 8 with Amethyst. Unlocks entire scene (all 12 sections).
927pub fn buy_scene(signer: Pubkey, scene_id: u64) -> Instruction {
928    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
929    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
930
931    Instruction {
932        program_id: program_id(),
933        accounts: vec![
934            AccountMeta::new(signer, true),
935            AccountMeta::new(dojo_address, false),
936            AccountMeta::new(scenes_address, false),
937            AccountMeta::new_readonly(system_program::ID, false),
938        ],
939        data: BuyScene {
940            scene_id: scene_id.to_le_bytes(),
941        }
942        .to_bytes(),
943    }
944}
945
946/// Buy scene (6/7/8) with mixed payment: spend all amethyst, cover shortfall with DOJO.
947pub fn buy_scene_dojo(signer: Pubkey, scene_id: u64) -> Instruction {
948    let config_address = config_pda(&program_id()).0;
949    let (game_address, _) = game_pda(&program_id());
950    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
951    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
952    let treasury_address = treasury_pda(&program_id()).0;
953    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
954    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
955
956    Instruction {
957        program_id: program_id(),
958        accounts: vec![
959            AccountMeta::new(signer, true),
960            AccountMeta::new_readonly(config_address, false),
961            AccountMeta::new(game_address, false),
962            AccountMeta::new(dojo_address, false),
963            AccountMeta::new(scenes_address, false),
964            AccountMeta::new(user_ata, false),
965            AccountMeta::new(treasury_ata, false),
966            AccountMeta::new(DOJO_MINT, false),
967            AccountMeta::new(treasury_address, false),
968            AccountMeta::new_readonly(spl_token::ID, false),
969            AccountMeta::new_readonly(system_program::ID, false),
970        ],
971        data: BuySceneDojo {
972            scene_id: scene_id.to_le_bytes(),
973        }
974        .to_bytes(),
975    }
976}
977
978/// Log (CPI from program; config signs). Variable-length message.
979pub fn log(config: Pubkey, msg: &[u8]) -> Instruction {
980    let mut data = Log {
981        _reserved: [0u8; 8],
982    }
983    .to_bytes();
984    data.extend_from_slice(msg);
985    Instruction {
986        program_id: program_id(),
987        accounts: vec![AccountMeta::new(config, true)],
988        data,
989    }
990}