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