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::{DGT_GROUP, 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 (mission_address, _) = mission_pda(&program_id(), &dojo_address);
56    let treasury_address = treasury_pda(&program_id()).0;
57
58    let referrer_bytes = referrer.map(|p| p.to_bytes()).unwrap_or([0u8; 32]);
59
60    let game_address = game_pda(&program_id()).0;
61    let mut accounts = vec![
62        AccountMeta::new(signer, true),
63        AccountMeta::new(config_address, false),
64        AccountMeta::new(game_address, false),
65        AccountMeta::new(dojo_address, false),
66        AccountMeta::new(barracks_address, false),
67        AccountMeta::new(forge_address, false),
68        AccountMeta::new(tasks_address, false),
69        AccountMeta::new(mission_address, false),
70        AccountMeta::new(treasury_address, false),
71        AccountMeta::new(FEE_COLLECTOR, false),
72        AccountMeta::new_readonly(system_program::ID, false),
73    ];
74    if let Some(ref_dojo) = referrer {
75        let (referral_address, _) = referral_pda(&program_id(), &ref_dojo);
76        accounts.push(AccountMeta::new_readonly(ref_dojo, false));
77        accounts.push(AccountMeta::new(referral_address, false));
78    }
79
80    Instruction {
81        program_id: program_id(),
82        accounts,
83        data: BuyStarterPack { referrer: referrer_bytes }.to_bytes(),
84    }
85}
86
87/// Recruit shogun(s) — pay with recruitment tickets. Adds to fodder_counts.
88/// seed: from BSM POST /seed.
89/// prestige: if Some, include Prestige account (must exist) to track per-prestige fodder.
90pub fn recruit_shogun_tickets(
91    signer: Pubkey,
92    count: u64,
93    seed: [u8; 32],
94    prestige: Option<Pubkey>,
95) -> Instruction {
96    let config_address = config_pda(&program_id()).0;
97    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
98    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
99
100    let mut accounts = vec![
101        AccountMeta::new(signer, true),
102        AccountMeta::new_readonly(config_address, false),
103        AccountMeta::new(dojo_address, false),
104        AccountMeta::new(tasks_address, false),
105    ];
106    if let Some(addr) = prestige {
107        accounts.push(AccountMeta::new(addr, false));
108    }
109
110    Instruction {
111        program_id: program_id(),
112        accounts,
113        data: RecruitShogunTickets {
114            count: count.to_le_bytes(),
115            seed,
116        }
117        .to_bytes(),
118    }
119}
120
121/// Recruit shogun(s) — pay with SOL. Adds to fodder_counts.
122/// seed: from BSM POST /seed.
123/// prestige: if Some, include Prestige account (must exist).
124pub fn recruit_shogun_sol(
125    signer: Pubkey,
126    count: u64,
127    seed: [u8; 32],
128    prestige: Option<Pubkey>,
129) -> Instruction {
130    let config_address = config_pda(&program_id()).0;
131    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
132    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
133
134    let treasury_address = treasury_pda(&program_id()).0;
135    let mut accounts = vec![
136        AccountMeta::new(signer, true),
137        AccountMeta::new_readonly(config_address, false),
138        AccountMeta::new(dojo_address, false),
139        AccountMeta::new(tasks_address, false),
140        AccountMeta::new(treasury_address, false),
141        AccountMeta::new(FEE_COLLECTOR, false),
142    ];
143    if let Some(addr) = prestige {
144        accounts.push(AccountMeta::new(addr, false));
145    }
146
147    Instruction {
148        program_id: program_id(),
149        accounts,
150        data: RecruitShogunSol {
151            count: count.to_le_bytes(),
152            seed,
153        }
154        .to_bytes(),
155    }
156}
157
158/// Seat: promote one from fodder to barracks slot. rarity 0-4, element 0-4.
159/// prestige: if Some, include Prestige account (must exist).
160pub fn seat_shogun(
161    signer: Pubkey,
162    slot: u64,
163    rarity: u64,
164    element: u64,
165    prestige: Option<Pubkey>,
166) -> Instruction {
167    let config_address = config_pda(&program_id()).0;
168    let game_address = game_pda(&program_id()).0;
169    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
170    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
171
172    let mut accounts = vec![
173        AccountMeta::new(signer, true),
174        AccountMeta::new_readonly(config_address, false),
175        AccountMeta::new(game_address, false),
176        AccountMeta::new(dojo_address, false),
177        AccountMeta::new(barracks_address, false),
178    ];
179    if let Some(addr) = prestige {
180        accounts.push(AccountMeta::new(addr, false));
181    }
182
183    Instruction {
184        program_id: program_id(),
185        accounts,
186        data: SeatShogun {
187            slot: slot.to_le_bytes(),
188            rarity: rarity.to_le_bytes(),
189            element: element.to_le_bytes(),
190        }
191        .to_bytes(),
192    }
193}
194
195/// Replace: return old to fodder, promote new from fodder. Same slot.
196/// prestige: if Some, include Prestige account (must exist).
197pub fn replace_shogun(
198    signer: Pubkey,
199    slot: u64,
200    new_rarity: u64,
201    new_element: u64,
202    prestige: Option<Pubkey>,
203) -> Instruction {
204    let config_address = config_pda(&program_id()).0;
205    let game_address = game_pda(&program_id()).0;
206    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
207    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
208    let (mission_address, _) = mission_pda(&program_id(), &dojo_address);
209
210    let mut accounts = vec![
211        AccountMeta::new(signer, true),
212        AccountMeta::new_readonly(config_address, false),
213        AccountMeta::new(game_address, false),
214        AccountMeta::new(dojo_address, false),
215        AccountMeta::new(barracks_address, false),
216        AccountMeta::new_readonly(mission_address, false),
217    ];
218    if let Some(addr) = prestige {
219        accounts.push(AccountMeta::new(addr, false));
220    }
221
222    Instruction {
223        program_id: program_id(),
224        accounts,
225        data: ReplaceShogun {
226            slot: slot.to_le_bytes(),
227            new_rarity: new_rarity.to_le_bytes(),
228            new_element: new_element.to_le_bytes(),
229        }
230        .to_bytes(),
231    }
232}
233
234/// Seat multiple shoguns from fodder into empty slots. Slots inferred.
235/// prestige: if Some, include Prestige account (must exist).
236pub fn seat_shogun_fill_all(
237    signer: Pubkey,
238    entries: impl IntoIterator<Item = (u64, u64)>,
239    prestige: Option<Pubkey>,
240) -> Instruction {
241    let config_address = config_pda(&program_id()).0;
242    let game_address = game_pda(&program_id()).0;
243    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
244    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
245
246    let mut arr: [SeatShogunFillAllEntry; 12] = [SeatShogunFillAllEntry {
247        rarity: [0; 8],
248        element: [0; 8],
249    }; 12];
250    let mut count = 0u8;
251    for (i, (rarity, element)) in entries.into_iter().take(12).enumerate() {
252        arr[i] = SeatShogunFillAllEntry {
253            rarity: rarity.to_le_bytes(),
254            element: element.to_le_bytes(),
255        };
256        count += 1;
257    }
258
259    let mut accounts = vec![
260        AccountMeta::new(signer, true),
261        AccountMeta::new_readonly(config_address, false),
262        AccountMeta::new(game_address, false),
263        AccountMeta::new(dojo_address, false),
264        AccountMeta::new(barracks_address, false),
265    ];
266    if let Some(addr) = prestige {
267        accounts.push(AccountMeta::new(addr, false));
268    }
269
270    Instruction {
271        program_id: program_id(),
272        accounts,
273        data: SeatShogunFillAll {
274            count,
275            _pad: [0; 7],
276            entries: arr,
277        }
278        .to_bytes(),
279    }
280}
281
282/// Dine. Tier: 0=24h, 1=48h, 2=72h. Burns shards. Restores chakra for seated shogun.
283pub fn dine(signer: Pubkey, slot: u64, tier: u64) -> Instruction {
284    let config_address = config_pda(&program_id()).0;
285    let game_address = game_pda(&program_id()).0;
286    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
287    let treasury_address = treasury_pda(&program_id()).0;
288    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
289    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
290    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
291
292    Instruction {
293        program_id: program_id(),
294        accounts: vec![
295            AccountMeta::new(signer, true),
296            AccountMeta::new_readonly(config_address, false),
297            AccountMeta::new(game_address, false),
298            AccountMeta::new(dojo_address, false),
299            AccountMeta::new(barracks_address, false),
300            AccountMeta::new(user_ata, false),
301            AccountMeta::new(treasury_ata, false),
302            AccountMeta::new(DOJO_MINT, false),
303            AccountMeta::new(treasury_address, false),
304            AccountMeta::new_readonly(spl_token::ID, false),
305        ],
306        data: Dine {
307            tier: tier.to_le_bytes(),
308            slot: slot.to_le_bytes(),
309        }
310        .to_bytes(),
311    }
312}
313
314/// Upgrade barracks (Ninja Hut) level. Pay with shards. 1→2, 2→3, 3→4. Burns shards.
315pub fn upgrade_barracks_shards(signer: Pubkey) -> Instruction {
316    let config_address = config_pda(&program_id()).0;
317    let game_address = game_pda(&program_id()).0;
318    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
319    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
320    let treasury_address = treasury_pda(&program_id()).0;
321    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
322    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
323
324    Instruction {
325        program_id: program_id(),
326        accounts: vec![
327            AccountMeta::new(signer, true),
328            AccountMeta::new_readonly(config_address, false),
329            AccountMeta::new(game_address, false),
330            AccountMeta::new(dojo_address, false),
331            AccountMeta::new(barracks_address, false),
332            AccountMeta::new(user_ata, false),
333            AccountMeta::new(treasury_ata, false),
334            AccountMeta::new(DOJO_MINT, false),
335            AccountMeta::new(treasury_address, false),
336            AccountMeta::new_readonly(spl_token::ID, false),
337        ],
338        data: UpgradeBarracksShards {}.to_bytes(),
339    }
340}
341
342/// Upgrade barracks (Ninja Hut) level. Pay with SOL. 1→2, 2→3 only (3→4 shards only).
343pub fn upgrade_barracks_sol(signer: Pubkey) -> Instruction {
344    let config_address = config_pda(&program_id()).0;
345    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
346    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
347    let treasury_address = treasury_pda(&program_id()).0;
348
349    Instruction {
350        program_id: program_id(),
351        accounts: vec![
352            AccountMeta::new(signer, true),
353            AccountMeta::new_readonly(config_address, false),
354            AccountMeta::new(dojo_address, false),
355            AccountMeta::new(barracks_address, false),
356            AccountMeta::new(treasury_address, false),
357            AccountMeta::new(FEE_COLLECTOR, false),
358            AccountMeta::new_readonly(system_program::ID, false),
359        ],
360        data: UpgradeBarracksSol {}.to_bytes(),
361    }
362}
363
364/// Upgrade forge level. Pay SOL (1–7, max level 7).
365pub fn upgrade_forge(signer: Pubkey) -> Instruction {
366    let config_address = config_pda(&program_id()).0;
367    let game_address = game_pda(&program_id()).0;
368    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
369    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
370    let treasury_address = treasury_pda(&program_id()).0;
371    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
372    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
373
374    Instruction {
375        program_id: program_id(),
376        accounts: vec![
377            AccountMeta::new(signer, true),
378            AccountMeta::new_readonly(config_address, false),
379            AccountMeta::new(game_address, false),
380            AccountMeta::new(dojo_address, false),
381            AccountMeta::new(forge_address, false),
382            AccountMeta::new(FEE_COLLECTOR, false),
383            AccountMeta::new_readonly(system_program::ID, false),
384            AccountMeta::new(user_ata, false),
385            AccountMeta::new(treasury_ata, false),
386            AccountMeta::new(DOJO_MINT, false),
387            AccountMeta::new(treasury_address, false),
388            AccountMeta::new_readonly(spl_token::ID, false),
389        ],
390        data: UpgradeForge {}.to_bytes(),
391    }
392}
393
394/// Merge: consume from fodder_counts. merge_type: 0=10×N, 1=5×R, 2=3×SR. Output rarity uses same chances as recruit (rarity_from_hash).
395/// seed: from BSM POST /seed.
396/// prestige: if Some, include Prestige account (must exist).
397pub fn merge_shogun(
398    signer: Pubkey,
399    merge_type: u64,
400    seed: [u8; 32],
401    prestige: Option<Pubkey>,
402) -> Instruction {
403    let config_address = config_pda(&program_id()).0;
404    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
405    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
406
407    let mut accounts = vec![
408        AccountMeta::new(signer, true),
409        AccountMeta::new_readonly(config_address, false),
410        AccountMeta::new(dojo_address, false),
411        AccountMeta::new(tasks_address, false),
412    ];
413    if let Some(addr) = prestige {
414        accounts.push(AccountMeta::new(addr, false));
415    }
416
417    Instruction {
418        program_id: program_id(),
419        accounts,
420        data: MergeShogun {
421            merge_type: merge_type.to_le_bytes(),
422            seed,
423        }
424        .to_bytes(),
425    }
426}
427
428/// Prestige: consume dupes from fodder, upgrade seated shogun in slot. SSR/UR only.
429/// prestige: if Some, include Prestige account (must exist).
430pub fn prestige_upgrade(signer: Pubkey, slot: u64, prestige: Option<Pubkey>) -> Instruction {
431    let config_address = config_pda(&program_id()).0;
432    let game_address = game_pda(&program_id()).0;
433    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
434    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
435    let (mission_address, _) = mission_pda(&program_id(), &dojo_address);
436    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
437
438    let mut accounts = vec![
439        AccountMeta::new(signer, true),
440        AccountMeta::new_readonly(config_address, false),
441        AccountMeta::new(game_address, false),
442        AccountMeta::new(dojo_address, false),
443        AccountMeta::new(barracks_address, false),
444        AccountMeta::new_readonly(mission_address, false),
445        AccountMeta::new(tasks_address, false),
446    ];
447    if let Some(addr) = prestige {
448        accounts.push(AccountMeta::new(addr, false));
449    }
450
451    Instruction {
452        program_id: program_id(),
453        accounts,
454        data: PrestigeUpgrade {
455            slot: slot.to_le_bytes(),
456        }
457        .to_bytes(),
458    }
459}
460
461/// Level up: spend shards, +10% SP per level. Burns shards.
462pub fn level_up_shogun(signer: Pubkey, slot: u64) -> Instruction {
463    let config_address = config_pda(&program_id()).0;
464    let game_address = game_pda(&program_id()).0;
465    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
466    let treasury_address = treasury_pda(&program_id()).0;
467    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
468    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
469    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
470    let (mission_address, _) = mission_pda(&program_id(), &dojo_address);
471
472    Instruction {
473        program_id: program_id(),
474        accounts: vec![
475            AccountMeta::new(signer, true),
476            AccountMeta::new_readonly(config_address, false),
477            AccountMeta::new(game_address, false),
478            AccountMeta::new(dojo_address, false),
479            AccountMeta::new(barracks_address, false),
480            AccountMeta::new_readonly(mission_address, false),
481            AccountMeta::new(user_ata, false),
482            AccountMeta::new(treasury_ata, false),
483            AccountMeta::new(DOJO_MINT, false),
484            AccountMeta::new(treasury_address, false),
485            AccountMeta::new_readonly(spl_token::ID, false),
486        ],
487        data: LevelUpShogun {
488            slot: slot.to_le_bytes(),
489        }
490        .to_bytes(),
491    }
492}
493
494/// Claim shards as $DOJO token. Pool-split: fixed emission per slot, your share = your_SP / total_SP.
495/// Amount computed entirely on-chain; no client input (security).
496pub fn claim_shards(signer: Pubkey) -> Instruction {
497    let config_address = config_pda(&program_id()).0;
498    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
499    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
500    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
501    let (mission_address, _) = mission_pda(&program_id(), &dojo_address);
502    let game_address = game_pda(&program_id()).0;
503    let treasury_address = treasury_pda(&program_id()).0;
504    let dojo_ata = get_associated_token_address(&signer, &DOJO_MINT);
505
506    Instruction {
507        program_id: program_id(),
508        accounts: vec![
509            AccountMeta::new(signer, true),
510            AccountMeta::new_readonly(config_address, false),
511            AccountMeta::new(dojo_address, false),
512            AccountMeta::new(forge_address, false),
513            AccountMeta::new(barracks_address, false),
514            AccountMeta::new_readonly(mission_address, false),
515            AccountMeta::new_readonly(game_address, false),
516            AccountMeta::new(dojo_ata, false),
517            AccountMeta::new(DOJO_MINT, false),
518            AccountMeta::new(treasury_address, false),
519            AccountMeta::new_readonly(spl_token::ID, false),
520        ],
521        data: ClaimShards {}.to_bytes(),
522    }
523}
524
525/// Claim referral reward (SOL).
526pub fn claim_referral_reward(signer: Pubkey, referrer_dojo: Pubkey) -> Instruction {
527    let (referral_address, _) = referral_pda(&program_id(), &referrer_dojo);
528    let treasury_address = treasury_pda(&program_id()).0;
529
530    Instruction {
531        program_id: program_id(),
532        accounts: vec![
533            AccountMeta::new(signer, true),
534            AccountMeta::new(referrer_dojo, false),
535            AccountMeta::new(referral_address, false),
536            AccountMeta::new(treasury_address, false),
537            AccountMeta::new_readonly(system_program::ID, false),
538        ],
539        data: ClaimReferralReward {}.to_bytes(),
540    }
541}
542
543/// Claim next recruit-tier reward.
544pub fn claim_recruit_reward(signer: Pubkey) -> Instruction {
545    let config_address = config_pda(&program_id()).0;
546    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
547    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
548
549    Instruction {
550        program_id: program_id(),
551        accounts: vec![
552            AccountMeta::new(signer, true),
553            AccountMeta::new_readonly(config_address, false),
554            AccountMeta::new(dojo_address, false),
555            AccountMeta::new(tasks_address, false),
556        ],
557        data: ClaimRecruitReward {}.to_bytes(),
558    }
559}
560
561/// Claim next forge-tier reward.
562pub fn claim_forge_reward(signer: Pubkey) -> Instruction {
563    let config_address = config_pda(&program_id()).0;
564    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
565    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
566
567    Instruction {
568        program_id: program_id(),
569        accounts: vec![
570            AccountMeta::new(signer, true),
571            AccountMeta::new_readonly(config_address, false),
572            AccountMeta::new(dojo_address, false),
573            AccountMeta::new(tasks_address, false),
574        ],
575        data: ClaimForgeReward {}.to_bytes(),
576    }
577}
578
579/// Claim next dine-tier reward.
580pub fn claim_dine_reward(signer: Pubkey) -> Instruction {
581    let config_address = config_pda(&program_id()).0;
582    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
583    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
584
585    Instruction {
586        program_id: program_id(),
587        accounts: vec![
588            AccountMeta::new(signer, true),
589            AccountMeta::new_readonly(config_address, false),
590            AccountMeta::new(dojo_address, false),
591            AccountMeta::new(tasks_address, false),
592        ],
593        data: ClaimDineReward {}.to_bytes(),
594    }
595}
596
597/// Ed25519 verify instruction for daily claim. Must be prepended before claim_daily_reward.
598/// Client builds the same message as the server signs: prefix + dojo_pda + task_id.
599pub fn ed25519_verify_instruction_for_daily_claim(
600    dojo_pda: Pubkey,
601    signature: [u8; 64],
602) -> Instruction {
603    use crate::consts::{CLAIM_TASK_PREFIX, DAILY_TASK_START, TASK_VERIFIER};
604    let mut message = Vec::with_capacity(CLAIM_TASK_PREFIX.len() + 32 + 8);
605    message.extend_from_slice(CLAIM_TASK_PREFIX);
606    message.extend_from_slice(dojo_pda.as_ref());
607    message.extend_from_slice(&DAILY_TASK_START.to_le_bytes());
608    let verifier_bytes: [u8; 32] = TASK_VERIFIER.to_bytes();
609    crate::utils::new_ed25519_instruction_with_signature(&message, &signature, &verifier_bytes)
610}
611
612/// Claim daily reward (1 ticket per day, no stacking). Backend signature required.
613/// Transaction must include ed25519_verify_instruction_for_daily_claim as the preceding instruction.
614pub fn claim_daily_reward(signer: Pubkey, signature: [u8; 64]) -> Instruction {
615    let config_address = config_pda(&program_id()).0;
616    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
617    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
618    let instructions_sysvar = solana_program::sysvar::instructions::ID;
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(dojo_address, false),
626            AccountMeta::new(tasks_address, false),
627            AccountMeta::new_readonly(instructions_sysvar, false),
628        ],
629        data: ClaimDailyReward { signature }.to_bytes(),
630    }
631}
632
633/// Ed25519 verify instruction for off-chain task (9–16). Must be prepended before claim_off_chain_task_reward.
634pub fn ed25519_verify_instruction_for_off_chain_task(
635    dojo_pda: Pubkey,
636    task_id: u64,
637    signature: [u8; 64],
638) -> Instruction {
639    use crate::consts::{CLAIM_TASK_PREFIX, TASK_VERIFIER};
640    let mut message = Vec::with_capacity(CLAIM_TASK_PREFIX.len() + 32 + 8);
641    message.extend_from_slice(CLAIM_TASK_PREFIX);
642    message.extend_from_slice(dojo_pda.as_ref());
643    message.extend_from_slice(&task_id.to_le_bytes());
644    let verifier_bytes: [u8; 32] = TASK_VERIFIER.to_bytes();
645    crate::utils::new_ed25519_instruction_with_signature(&message, &signature, &verifier_bytes)
646}
647
648/// Claim off-chain task reward (task_id 9–16). Backend signature required.
649/// Transaction must include ed25519_verify_instruction_for_off_chain_task as the preceding instruction.
650pub fn claim_off_chain_task_reward(
651    signer: Pubkey,
652    task_id: u64,
653    signature: [u8; 64],
654) -> Instruction {
655    let config_address = config_pda(&program_id()).0;
656    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
657    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
658    let instructions_sysvar = solana_program::sysvar::instructions::ID;
659
660    Instruction {
661        program_id: program_id(),
662        accounts: vec![
663            AccountMeta::new(signer, true),
664            AccountMeta::new_readonly(config_address, false),
665            AccountMeta::new(dojo_address, false),
666            AccountMeta::new(tasks_address, false),
667            AccountMeta::new_readonly(instructions_sysvar, false),
668        ],
669        data: ClaimOffChainTaskReward {
670            task_id: task_id.to_le_bytes(),
671            signature,
672        }
673        .to_bytes(),
674    }
675}
676
677/// Claim Seeker task reward. Verifies user owns a Seeker Genesis Token (SGT) on-chain; one-time claim.
678/// Anti-Sybil: Seeker PDA (per SGT mint) prevents reusing same SGT across wallets.
679/// Pass the signer's SGT token account (Token-2022 ATA) and the SGT mint.
680/// Client should use getTokenAccountsByOwner or similar to find the user's SGT.
681pub fn claim_seeker_task_reward(
682    signer: Pubkey,
683    signer_sgt_token_account: Pubkey,
684    sgt_mint: Pubkey,
685) -> Instruction {
686    let config_address = config_pda(&program_id()).0;
687    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
688    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
689    let (seeker_address, _) = seeker_pda(&program_id(), &sgt_mint);
690
691    Instruction {
692        program_id: program_id(),
693        accounts: vec![
694            AccountMeta::new(signer, true),
695            AccountMeta::new_readonly(config_address, false),
696            AccountMeta::new(dojo_address, false),
697            AccountMeta::new(tasks_address, false),
698            AccountMeta::new_readonly(signer_sgt_token_account, false),
699            AccountMeta::new_readonly(sgt_mint, false),
700            AccountMeta::new_readonly(spl_token_2022::ID, false),
701            AccountMeta::new(seeker_address, false),
702            AccountMeta::new_readonly(system_program::ID, false),
703        ],
704        data: ClaimSeekerTaskReward {}.to_bytes(),
705    }
706}
707
708/// Mint soulbound NFT for Seeker users. User can either claim Seeker task first or mint directly
709/// (creates Seeker if needed). One soulbound per SGT mint.
710/// Pass soulbound_mint as a new keypair pubkey (client generates keypair for the mint account).
711pub fn mint_soulbound(
712    signer: Pubkey,
713    signer_sgt_token_account: Pubkey,
714    sgt_mint: Pubkey,
715    soulbound_mint: Pubkey,
716) -> Instruction {
717    let config_address = config_pda(&program_id()).0;
718    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
719    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
720    let (seeker_address, _) = seeker_pda(&program_id(), &sgt_mint);
721    let (treasury_address, _) = treasury_pda(&program_id());
722    let soulbound_token = get_associated_token_address(&signer, &soulbound_mint);
723
724    Instruction {
725        program_id: program_id(),
726        accounts: vec![
727            AccountMeta::new(signer, true),
728            AccountMeta::new_readonly(config_address, false),
729            AccountMeta::new(dojo_address, false),
730            AccountMeta::new(tasks_address, false),
731            AccountMeta::new(seeker_address, false),
732            AccountMeta::new_readonly(signer_sgt_token_account, false),
733            AccountMeta::new_readonly(sgt_mint, false),
734            AccountMeta::new(soulbound_mint, true), // client keypair must sign for create_account
735            AccountMeta::new_readonly(treasury_address, false),
736            AccountMeta::new(DGT_GROUP, false), // DGT group mint (writable for initialize_member)
737            AccountMeta::new(soulbound_token, false),
738            AccountMeta::new_readonly(spl_token_2022::ID, false),
739            AccountMeta::new_readonly(system_program::ID, false),
740            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
741        ],
742        data: MintSoulbound {}.to_bytes(),
743    }
744}
745
746/// Start mission (expedition). Pay DOJO, burn. Slots on mission excluded from mining.
747/// mission_type: 0=Short, 1=Medium, 2=Long. slot_mask: bits for participating slots.
748/// Mission account created if needed (existing dojos). New dojos get Mission in BuyStarterPack.
749pub fn start_mission(signer: Pubkey, mission_type: u64, slot_mask: u64) -> Instruction {
750    let config_address = config_pda(&program_id()).0;
751    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
752    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
753    let (mission_address, _) = mission_pda(&program_id(), &dojo_address);
754    let game_address = game_pda(&program_id()).0;
755    let treasury_address = treasury_pda(&program_id()).0;
756    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
757    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
758
759    Instruction {
760        program_id: program_id(),
761        accounts: vec![
762            AccountMeta::new(signer, true),
763            AccountMeta::new_readonly(config_address, false),
764            AccountMeta::new(dojo_address, false),
765            AccountMeta::new(barracks_address, false),
766            AccountMeta::new(mission_address, false),
767            AccountMeta::new(game_address, false),
768            AccountMeta::new(user_ata, false),
769            AccountMeta::new(treasury_ata, false),
770            AccountMeta::new(DOJO_MINT, false),
771            AccountMeta::new(treasury_address, false),
772            AccountMeta::new_readonly(spl_token::ID, false),
773            AccountMeta::new_readonly(system_program::ID, false),
774        ],
775        data: StartMission {
776            mission_type: mission_type.to_le_bytes(),
777            slot_mask: slot_mask.to_le_bytes(),
778        }
779        .to_bytes(),
780    }
781}
782
783/// Claim mission reward. Requires BSM seed from POST /mission/seed. prestige: optional for free-roll tracking.
784pub fn claim_mission_reward(
785    signer: Pubkey,
786    mission_index: u64,
787    seed: [u8; 32],
788    prestige: Option<Pubkey>,
789) -> Instruction {
790    let config_address = config_pda(&program_id()).0;
791    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
792    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
793    let (mission_address, _) = mission_pda(&program_id(), &dojo_address);
794    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
795    let game_address = game_pda(&program_id()).0;
796    let treasury_address = treasury_pda(&program_id()).0;
797    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
798    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
799
800    let mut accounts = vec![
801        AccountMeta::new(signer, true),
802        AccountMeta::new_readonly(config_address, false),
803        AccountMeta::new(dojo_address, false),
804        AccountMeta::new_readonly(barracks_address, false),
805        AccountMeta::new(mission_address, false),
806        AccountMeta::new(game_address, false),
807        AccountMeta::new(tasks_address, false),
808        AccountMeta::new(user_ata, false),
809        AccountMeta::new(DOJO_MINT, false),
810        AccountMeta::new(treasury_address, false),
811        AccountMeta::new(treasury_ata, false),
812        AccountMeta::new_readonly(spl_token::ID, false),
813    ];
814    if let Some(addr) = prestige {
815        accounts.push(AccountMeta::new(addr, false));
816    }
817
818    Instruction {
819        program_id: program_id(),
820        accounts,
821        data: ClaimMissionReward {
822            mission_index: mission_index.to_le_bytes(),
823            seed,
824        }
825        .to_bytes(),
826    }
827}
828
829/// Claim collection reward (3 ninjas same element+rarity). Pass collection_index (element×5 + rarity, 0–24).
830/// Program finds 3 matching shoguns in pool.
831pub fn claim_collection_reward(signer: Pubkey, collection_index: u8) -> Instruction {
832    let config_address = config_pda(&program_id()).0;
833    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
834    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
835
836    Instruction {
837        program_id: program_id(),
838        accounts: vec![
839            AccountMeta::new(signer, true),
840            AccountMeta::new_readonly(config_address, false),
841            AccountMeta::new(dojo_address, false),
842            AccountMeta::new(tasks_address, false),
843        ],
844        data: ClaimCollectionReward { collection_index }.to_bytes(),
845    }
846}
847
848/// Flash sale: 50 tickets for 5000 shards, max 5 per day.
849pub fn buy_flash_sale(signer: Pubkey) -> Instruction {
850    let config_address = config_pda(&program_id()).0;
851    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
852    let treasury_address = treasury_pda(&program_id()).0;
853    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
854    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
855
856    Instruction {
857        program_id: program_id(),
858        accounts: vec![
859            AccountMeta::new(signer, true),
860            AccountMeta::new_readonly(config_address, false),
861            AccountMeta::new(dojo_address, false),
862            AccountMeta::new(user_ata, false),
863            AccountMeta::new_readonly(treasury_address, false),
864            AccountMeta::new(treasury_ata, false),
865            AccountMeta::new_readonly(spl_token::ID, false),
866        ],
867        data: BuyFlashSale {}.to_bytes(),
868    }
869}
870
871/// Daily deal: 5 tickets for 300 shards. Burns shards.
872pub fn buy_tickets_with_shards(signer: Pubkey) -> Instruction {
873    let config_address = config_pda(&program_id()).0;
874    let game_address = game_pda(&program_id()).0;
875    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
876    let treasury_address = treasury_pda(&program_id()).0;
877    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
878    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
879
880    Instruction {
881        program_id: program_id(),
882        accounts: vec![
883            AccountMeta::new(signer, true),
884            AccountMeta::new_readonly(config_address, false),
885            AccountMeta::new(game_address, false),
886            AccountMeta::new(dojo_address, false),
887            AccountMeta::new(user_ata, false),
888            AccountMeta::new(treasury_ata, false),
889            AccountMeta::new(DOJO_MINT, false),
890            AccountMeta::new(treasury_address, false),
891            AccountMeta::new_readonly(spl_token::ID, false),
892        ],
893        data: BuyTicketsWithShards {}.to_bytes(),
894    }
895}
896
897/// Buy bundle: 150 recruitment tickets for 5 SOL (event deal).
898pub fn buy_bundle(signer: Pubkey) -> Instruction {
899    let config_address = config_pda(&program_id()).0;
900    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
901    let treasury_address = treasury_pda(&program_id()).0;
902
903    Instruction {
904        program_id: program_id(),
905        accounts: vec![
906            AccountMeta::new(signer, true),
907            AccountMeta::new_readonly(config_address, false),
908            AccountMeta::new(dojo_address, false),
909            AccountMeta::new(treasury_address, false),
910            AccountMeta::new(FEE_COLLECTOR, false),
911            AccountMeta::new_readonly(system_program::ID, false),
912        ],
913        data: BuyBundle {}.to_bytes(),
914    }
915}
916
917/// Clear forge upgrade cooldown. Cost = remaining minutes (shards). One tx clears all.
918pub fn clear_forge_cooldown(signer: Pubkey) -> Instruction {
919    let config_address = config_pda(&program_id()).0;
920    let game_address = game_pda(&program_id()).0;
921    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
922    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
923    let treasury_address = treasury_pda(&program_id()).0;
924    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
925    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
926
927    Instruction {
928        program_id: program_id(),
929        accounts: vec![
930            AccountMeta::new(signer, true),
931            AccountMeta::new_readonly(config_address, false),
932            AccountMeta::new(game_address, false),
933            AccountMeta::new(dojo_address, false),
934            AccountMeta::new(forge_address, false),
935            AccountMeta::new(user_ata, false),
936            AccountMeta::new(treasury_ata, false),
937            AccountMeta::new(DOJO_MINT, false),
938            AccountMeta::new(treasury_address, false),
939            AccountMeta::new_readonly(spl_token::ID, false),
940        ],
941        data: ClearForgeCooldown {}.to_bytes(),
942    }
943}
944
945/// Set genesis slot and game.last_emission_slot (admin). halving_period_slots: 0 = use default (~58 days, matches Hyper Ninja).
946pub fn set_genesis_slot(authority: Pubkey, genesis_slot: u64, halving_period_slots: u64) -> Instruction {
947    let config_address = config_pda(&program_id()).0;
948    let game_address = game_pda(&program_id()).0;
949
950    Instruction {
951        program_id: program_id(),
952        accounts: vec![
953            AccountMeta::new(authority, true),
954            AccountMeta::new(config_address, false),
955            AccountMeta::new(game_address, false),
956        ],
957        data: SetGenesisSlot {
958            genesis_slot: genesis_slot.to_le_bytes(),
959            halving_period_slots: halving_period_slots.to_le_bytes(),
960        }
961        .to_bytes(),
962    }
963}
964
965/// Roll scene sections (1 or 10) — pay with Amethyst.
966/// seed: from BSM POST /roll/instruction (Option 7 centralized oracle).
967pub fn roll_scene_section_amethyst(signer: Pubkey, count: u64, seed: [u8; 32]) -> Instruction {
968    let config_address = config_pda(&program_id()).0;
969    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
970    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
971
972    Instruction {
973        program_id: program_id(),
974        accounts: vec![
975            AccountMeta::new(signer, true),
976            AccountMeta::new_readonly(config_address, false),
977            AccountMeta::new(dojo_address, false),
978            AccountMeta::new(scenes_address, false),
979            AccountMeta::new_readonly(system_program::ID, false),
980        ],
981        data: RollSceneSectionAmethyst {
982            count: count.to_le_bytes(),
983            seed,
984        }
985        .to_bytes(),
986    }
987}
988
989/// Roll scene sections (1 or 10) — pay with Shards (SPL $DOJO).
990/// seed: from BSM POST /roll/instruction (Option 7 centralized oracle).
991pub fn roll_scene_section_shards(signer: Pubkey, count: u64, seed: [u8; 32]) -> Instruction {
992    let config_address = config_pda(&program_id()).0;
993    let game_address = game_pda(&program_id()).0;
994    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
995    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
996    let treasury_address = treasury_pda(&program_id()).0;
997    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
998    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
999
1000    Instruction {
1001        program_id: program_id(),
1002        accounts: vec![
1003            AccountMeta::new(signer, true),
1004            AccountMeta::new_readonly(config_address, false),
1005            AccountMeta::new(game_address, false),
1006            AccountMeta::new(dojo_address, false),
1007            AccountMeta::new(scenes_address, false),
1008            AccountMeta::new(user_ata, false),
1009            AccountMeta::new(treasury_ata, false),
1010            AccountMeta::new(DOJO_MINT, false),
1011            AccountMeta::new(treasury_address, false),
1012            AccountMeta::new_readonly(spl_token::ID, false),
1013            AccountMeta::new_readonly(system_program::ID, false),
1014        ],
1015        data: RollSceneSectionShards {
1016            count: count.to_le_bytes(),
1017            seed,
1018        }
1019        .to_bytes(),
1020    }
1021}
1022
1023/// Salvage all duplicate scene sections for Amethyst refund. Program derives from on-chain state.
1024pub fn salvage_scene_section(signer: Pubkey) -> Instruction {
1025    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1026    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
1027
1028    Instruction {
1029        program_id: program_id(),
1030        accounts: vec![
1031            AccountMeta::new(signer, true),
1032            AccountMeta::new(dojo_address, false),
1033            AccountMeta::new(scenes_address, false),
1034        ],
1035        data: SalvageSceneSection {}.to_bytes(),
1036    }
1037}
1038
1039/// Set active scene (background). Requires scene unlocked.
1040/// Updates Game.total_effective_spirit_power for pool-split.
1041pub fn update_active_scene(signer: Pubkey, scene_id: u64) -> Instruction {
1042    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1043    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
1044    let game_address = game_pda(&program_id()).0;
1045    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
1046
1047    Instruction {
1048        program_id: program_id(),
1049        accounts: vec![
1050            AccountMeta::new(signer, true),
1051            AccountMeta::new(dojo_address, false),
1052            AccountMeta::new(scenes_address, false),
1053            AccountMeta::new(game_address, false),
1054            AccountMeta::new(barracks_address, false),
1055        ],
1056        data: UpdateActiveScene {
1057            scene_id: scene_id.to_le_bytes(),
1058        }
1059        .to_bytes(),
1060    }
1061}
1062
1063/// Buy chest: 1 SOL → 5000 Amethyst. 90% to treasury, 10% to fee collector.
1064pub fn buy_chest(signer: Pubkey) -> Instruction {
1065    let config_address = config_pda(&program_id()).0;
1066    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1067    let treasury_address = treasury_pda(&program_id()).0;
1068
1069    Instruction {
1070        program_id: program_id(),
1071        accounts: vec![
1072            AccountMeta::new(signer, true),
1073            AccountMeta::new_readonly(config_address, false),
1074            AccountMeta::new(dojo_address, false),
1075            AccountMeta::new(treasury_address, false),
1076            AccountMeta::new(FEE_COLLECTOR, false),
1077            AccountMeta::new_readonly(system_program::ID, false),
1078        ],
1079        data: BuyChest {}.to_bytes(),
1080    }
1081}
1082
1083/// Buy scene 6, 7, or 8 with Amethyst. Unlocks entire scene (all 12 sections).
1084pub fn buy_scene(signer: Pubkey, scene_id: u64) -> Instruction {
1085    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1086    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
1087
1088    Instruction {
1089        program_id: program_id(),
1090        accounts: vec![
1091            AccountMeta::new(signer, true),
1092            AccountMeta::new(dojo_address, false),
1093            AccountMeta::new(scenes_address, false),
1094            AccountMeta::new_readonly(system_program::ID, false),
1095        ],
1096        data: BuyScene {
1097            scene_id: scene_id.to_le_bytes(),
1098        }
1099        .to_bytes(),
1100    }
1101}
1102
1103/// Buy scene (6/7/8) with mixed payment: spend all amethyst, cover shortfall with DOJO.
1104pub fn buy_scene_dojo(signer: Pubkey, scene_id: u64) -> Instruction {
1105    let config_address = config_pda(&program_id()).0;
1106    let (game_address, _) = game_pda(&program_id());
1107    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1108    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
1109    let treasury_address = treasury_pda(&program_id()).0;
1110    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
1111    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
1112
1113    Instruction {
1114        program_id: program_id(),
1115        accounts: vec![
1116            AccountMeta::new(signer, true),
1117            AccountMeta::new_readonly(config_address, false),
1118            AccountMeta::new(game_address, false),
1119            AccountMeta::new(dojo_address, false),
1120            AccountMeta::new(scenes_address, false),
1121            AccountMeta::new(user_ata, false),
1122            AccountMeta::new(treasury_ata, false),
1123            AccountMeta::new(DOJO_MINT, false),
1124            AccountMeta::new(treasury_address, false),
1125            AccountMeta::new_readonly(spl_token::ID, false),
1126            AccountMeta::new_readonly(system_program::ID, false),
1127        ],
1128        data: BuySceneDojo {
1129            scene_id: scene_id.to_le_bytes(),
1130        }
1131        .to_bytes(),
1132    }
1133}
1134
1135/// Log (CPI from program; config signs). Variable-length message.
1136pub fn log(config: Pubkey, msg: &[u8]) -> Instruction {
1137    let mut data = Log {
1138        _reserved: [0u8; 8],
1139    }
1140    .to_bytes();
1141    data.extend_from_slice(msg);
1142    Instruction {
1143        program_id: program_id(),
1144        accounts: vec![AccountMeta::new(config, true)],
1145        data,
1146    }
1147}