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 and Treasury. Admin only.
21pub fn initialize(authority: Pubkey) -> Instruction {
22    let config_address = config_pda(&program_id()).0;
23    let treasury_address = treasury_pda(&program_id()).0;
24    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
25
26    Instruction {
27        program_id: program_id(),
28        accounts: vec![
29            AccountMeta::new(authority, true),
30            AccountMeta::new(config_address, false),
31            AccountMeta::new(treasury_address, false),
32            AccountMeta::new(treasury_ata, false),
33            AccountMeta::new_readonly(DOJO_MINT, false),
34            AccountMeta::new_readonly(spl_token::ID, false),
35            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
36            AccountMeta::new_readonly(system_program::ID, false),
37        ],
38        data: Initialize {}.to_bytes(),
39    }
40}
41
42/// Buy Starter Pack (initialize player Dojo). Optional referrer.
43/// Creates 1 starter shogun (assigned to barracks slot 0) + 1 recruitment ticket.
44pub fn buy_starter_pack(
45    signer: Pubkey,
46    referrer: Option<Pubkey>,
47) -> Instruction {
48    let config_address = config_pda(&program_id()).0;
49    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
50    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
51    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
52    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
53    let (battle_address, _) = battle_pda(&program_id(), &dojo_address);
54    let treasury_address = treasury_pda(&program_id()).0;
55
56    let referrer_bytes = referrer.map(|p| p.to_bytes()).unwrap_or([0u8; 32]);
57
58    let mut accounts = vec![
59        AccountMeta::new(signer, true),
60        AccountMeta::new(config_address, false),
61        AccountMeta::new(dojo_address, false),
62        AccountMeta::new(barracks_address, false),
63        AccountMeta::new(forge_address, false),
64        AccountMeta::new(tasks_address, false),
65        AccountMeta::new(battle_address, false),
66        AccountMeta::new(treasury_address, false),
67        AccountMeta::new(FEE_COLLECTOR, false),
68        AccountMeta::new_readonly(system_program::ID, false),
69    ];
70    if let Some(ref_dojo) = referrer {
71        let (referral_address, _) = referral_pda(&program_id(), &ref_dojo);
72        accounts.push(AccountMeta::new_readonly(ref_dojo, false));
73        accounts.push(AccountMeta::new(referral_address, false));
74    }
75
76    Instruction {
77        program_id: program_id(),
78        accounts,
79        data: BuyStarterPack { referrer: referrer_bytes }.to_bytes(),
80    }
81}
82
83/// Recruit shogun(s) — pay with recruitment tickets. Adds to fodder_counts.
84/// seed: from BSM POST /seed.
85/// prestige: if Some, include Prestige account (must exist) to track per-prestige fodder.
86pub fn recruit_shogun_tickets(
87    signer: Pubkey,
88    count: u64,
89    seed: [u8; 32],
90    prestige: Option<Pubkey>,
91) -> Instruction {
92    let config_address = config_pda(&program_id()).0;
93    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
94    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
95    let treasury_address = treasury_pda(&program_id()).0;
96
97    let mut accounts = vec![
98        AccountMeta::new(signer, true),
99        AccountMeta::new_readonly(config_address, false),
100        AccountMeta::new(dojo_address, false),
101        AccountMeta::new(tasks_address, false),
102        AccountMeta::new(treasury_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 treasury_address = treasury_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(treasury_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 treasury_address = treasury_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(treasury_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 treasury_address = treasury_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(treasury_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 (dojo_address, _) = dojo_pda(&program_id(), &signer);
282    let treasury_address = treasury_pda(&program_id()).0;
283    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
284    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
285    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
286    let (battle_address, _) = battle_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(treasury_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            AccountMeta::new(battle_address, false),
302        ],
303        data: Dine {
304            tier: tier.to_le_bytes(),
305            slot: slot.to_le_bytes(),
306        }
307        .to_bytes(),
308    }
309}
310
311/// Upgrade barracks (Ninja Hut) level. Pay with shards. 1→2, 2→3, 3→4. Burns shards.
312pub fn upgrade_barracks_shards(signer: Pubkey) -> Instruction {
313    let config_address = config_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(treasury_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 (dojo_address, _) = dojo_pda(&program_id(), &signer);
364    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
365    let treasury_address = treasury_pda(&program_id()).0;
366    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
367    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
368
369    Instruction {
370        program_id: program_id(),
371        accounts: vec![
372            AccountMeta::new(signer, true),
373            AccountMeta::new_readonly(config_address, false),
374            AccountMeta::new(treasury_address, false),
375            AccountMeta::new(dojo_address, false),
376            AccountMeta::new(forge_address, false),
377            AccountMeta::new(FEE_COLLECTOR, false),
378            AccountMeta::new_readonly(system_program::ID, false),
379            AccountMeta::new(user_ata, false),
380            AccountMeta::new(treasury_ata, false),
381            AccountMeta::new(DOJO_MINT, false),
382            AccountMeta::new(treasury_address, false),
383            AccountMeta::new_readonly(spl_token::ID, false),
384        ],
385        data: UpgradeForge {}.to_bytes(),
386    }
387}
388
389/// 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).
390/// seed: from BSM POST /seed.
391/// prestige: if Some, include Prestige account (must exist).
392pub fn merge_shogun(
393    signer: Pubkey,
394    merge_type: u64,
395    seed: [u8; 32],
396    prestige: Option<Pubkey>,
397) -> Instruction {
398    let config_address = config_pda(&program_id()).0;
399    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
400    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
401
402    let mut accounts = vec![
403        AccountMeta::new(signer, true),
404        AccountMeta::new_readonly(config_address, false),
405        AccountMeta::new(dojo_address, false),
406        AccountMeta::new(tasks_address, false),
407    ];
408    if let Some(addr) = prestige {
409        accounts.push(AccountMeta::new(addr, false));
410    }
411
412    Instruction {
413        program_id: program_id(),
414        accounts,
415        data: MergeShogun {
416            merge_type: merge_type.to_le_bytes(),
417            seed,
418        }
419        .to_bytes(),
420    }
421}
422
423/// Prestige: consume dupes from fodder, upgrade seated shogun in slot. SSR/UR only.
424/// prestige: if Some, include Prestige account (must exist).
425pub fn prestige_upgrade(signer: Pubkey, slot: u64, prestige: Option<Pubkey>) -> Instruction {
426    let config_address = config_pda(&program_id()).0;
427    let treasury_address = treasury_pda(&program_id()).0;
428    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
429    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
430    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
431
432    let mut accounts = vec![
433        AccountMeta::new(signer, true),
434        AccountMeta::new_readonly(config_address, false),
435        AccountMeta::new(treasury_address, false),
436        AccountMeta::new(dojo_address, false),
437        AccountMeta::new(barracks_address, false),
438        AccountMeta::new(tasks_address, false),
439    ];
440    if let Some(addr) = prestige {
441        accounts.push(AccountMeta::new(addr, false));
442    }
443
444    Instruction {
445        program_id: program_id(),
446        accounts,
447        data: PrestigeUpgrade {
448            slot: slot.to_le_bytes(),
449        }
450        .to_bytes(),
451    }
452}
453
454/// Level up: spend shards, +10% SP per level. Burns shards.
455pub fn level_up_shogun(signer: Pubkey, slot: u64) -> Instruction {
456    let config_address = config_pda(&program_id()).0;
457    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
458    let treasury_address = treasury_pda(&program_id()).0;
459    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
460    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
461    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
462
463    Instruction {
464        program_id: program_id(),
465        accounts: vec![
466            AccountMeta::new(signer, true),
467            AccountMeta::new_readonly(config_address, false),
468            AccountMeta::new(treasury_address, false),
469            AccountMeta::new(dojo_address, false),
470            AccountMeta::new(barracks_address, false),
471            AccountMeta::new(user_ata, false),
472            AccountMeta::new(treasury_ata, false),
473            AccountMeta::new(DOJO_MINT, false),
474            AccountMeta::new(treasury_address, false),
475            AccountMeta::new_readonly(spl_token::ID, false),
476        ],
477        data: LevelUpShogun {
478            slot: slot.to_le_bytes(),
479        }
480        .to_bytes(),
481    }
482}
483
484/// Sync ore/refine + mint **all** refined SPL $DOJO to the signer ATA (no amount arg).
485pub fn claim_shards(signer: Pubkey) -> Instruction {
486    let config_address = config_pda(&program_id()).0;
487    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
488    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
489    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
490    let treasury_address = treasury_pda(&program_id()).0;
491    let dojo_ata = get_associated_token_address(&signer, &DOJO_MINT);
492
493    Instruction {
494        program_id: program_id(),
495        accounts: vec![
496            AccountMeta::new(signer, true),
497            AccountMeta::new_readonly(config_address, false),
498            AccountMeta::new(dojo_address, false),
499            AccountMeta::new(forge_address, false),
500            AccountMeta::new(barracks_address, false),
501            AccountMeta::new(treasury_address, false),
502            AccountMeta::new(dojo_ata, false),
503            AccountMeta::new(DOJO_MINT, false),
504            AccountMeta::new_readonly(spl_token::ID, false),
505        ],
506        data: ClaimShards {}.to_bytes(),
507    }
508}
509
510/// Claim referral reward (SOL). Transfer from Referral PDA to referrer.
511pub fn claim_referral_reward(signer: Pubkey, referrer_dojo: Pubkey) -> Instruction {
512    let (referral_address, _) = referral_pda(&program_id(), &referrer_dojo);
513
514    Instruction {
515        program_id: program_id(),
516        accounts: vec![
517            AccountMeta::new(signer, true),
518            AccountMeta::new_readonly(referrer_dojo, false),
519            AccountMeta::new(referral_address, false),
520            AccountMeta::new_readonly(system_program::ID, false),
521        ],
522        data: ClaimReferralReward {}.to_bytes(),
523    }
524}
525
526/// Claim next recruit-tier reward.
527pub fn claim_recruit_reward(signer: Pubkey) -> Instruction {
528    let config_address = config_pda(&program_id()).0;
529    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
530    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
531
532    Instruction {
533        program_id: program_id(),
534        accounts: vec![
535            AccountMeta::new(signer, true),
536            AccountMeta::new_readonly(config_address, false),
537            AccountMeta::new(dojo_address, false),
538            AccountMeta::new(tasks_address, false),
539        ],
540        data: ClaimRecruitReward {}.to_bytes(),
541    }
542}
543
544/// Claim next forge-tier reward.
545pub fn claim_forge_reward(signer: Pubkey) -> Instruction {
546    let config_address = config_pda(&program_id()).0;
547    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
548    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
549    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
550
551    Instruction {
552        program_id: program_id(),
553        accounts: vec![
554            AccountMeta::new(signer, true),
555            AccountMeta::new_readonly(config_address, false),
556            AccountMeta::new(dojo_address, false),
557            AccountMeta::new_readonly(forge_address, false),
558            AccountMeta::new(tasks_address, false),
559        ],
560        data: ClaimForgeReward {}.to_bytes(),
561    }
562}
563
564/// Claim next dine-tier reward.
565pub fn claim_dine_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: ClaimDineReward {}.to_bytes(),
579    }
580}
581
582/// Ed25519 verify instruction for daily claim. Must be prepended before claim_daily_reward.
583/// Client builds the same message as the server signs: prefix + dojo_pda + task_id.
584pub fn ed25519_verify_instruction_for_daily_claim(
585    dojo_pda: Pubkey,
586    signature: [u8; 64],
587) -> Instruction {
588    use crate::consts::{CLAIM_TASK_PREFIX, DAILY_TASK_START, TASK_VERIFIER};
589    let mut message = Vec::with_capacity(CLAIM_TASK_PREFIX.len() + 32 + 8);
590    message.extend_from_slice(CLAIM_TASK_PREFIX);
591    message.extend_from_slice(dojo_pda.as_ref());
592    message.extend_from_slice(&DAILY_TASK_START.to_le_bytes());
593    let verifier_bytes: [u8; 32] = TASK_VERIFIER.to_bytes();
594    crate::utils::new_ed25519_instruction_with_signature(&message, &signature, &verifier_bytes)
595}
596
597/// Claim daily reward (1 ticket per day, no stacking). Backend signature required.
598/// Transaction must include ed25519_verify_instruction_for_daily_claim as the preceding instruction.
599pub fn claim_daily_reward(signer: Pubkey, signature: [u8; 64]) -> Instruction {
600    let config_address = config_pda(&program_id()).0;
601    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
602    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
603    let instructions_sysvar = solana_program::sysvar::instructions::ID;
604
605    Instruction {
606        program_id: program_id(),
607        accounts: vec![
608            AccountMeta::new(signer, true),
609            AccountMeta::new_readonly(config_address, false),
610            AccountMeta::new(dojo_address, false),
611            AccountMeta::new(tasks_address, false),
612            AccountMeta::new_readonly(instructions_sysvar, false),
613        ],
614        data: ClaimDailyReward { signature }.to_bytes(),
615    }
616}
617
618/// Ed25519 verify instruction for off-chain task (9–16). Must be prepended before claim_off_chain_task_reward.
619pub fn ed25519_verify_instruction_for_off_chain_task(
620    dojo_pda: Pubkey,
621    task_id: u64,
622    signature: [u8; 64],
623) -> Instruction {
624    use crate::consts::{CLAIM_TASK_PREFIX, 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(&task_id.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 off-chain task reward (task_id 9–16). Backend signature required.
634/// Transaction must include ed25519_verify_instruction_for_off_chain_task as the preceding instruction.
635pub fn claim_off_chain_task_reward(
636    signer: Pubkey,
637    task_id: u64,
638    signature: [u8; 64],
639) -> Instruction {
640    let config_address = config_pda(&program_id()).0;
641    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
642    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
643    let instructions_sysvar = solana_program::sysvar::instructions::ID;
644
645    Instruction {
646        program_id: program_id(),
647        accounts: vec![
648            AccountMeta::new(signer, true),
649            AccountMeta::new_readonly(config_address, false),
650            AccountMeta::new(dojo_address, false),
651            AccountMeta::new(tasks_address, false),
652            AccountMeta::new_readonly(instructions_sysvar, false),
653        ],
654        data: ClaimOffChainTaskReward {
655            task_id: task_id.to_le_bytes(),
656            signature,
657        }
658        .to_bytes(),
659    }
660}
661
662/// Claim Seeker task reward. Verifies user owns a Seeker Genesis Token (SGT) on-chain; one-time claim.
663/// Anti-Sybil: Seeker PDA (per SGT mint) prevents reusing same SGT across wallets.
664/// Pass the signer's SGT token account (Token-2022 ATA) and the SGT mint.
665/// Client should use getTokenAccountsByOwner or similar to find the user's SGT.
666pub fn claim_seeker_task_reward(
667    signer: Pubkey,
668    signer_sgt_token_account: Pubkey,
669    sgt_mint: Pubkey,
670) -> Instruction {
671    let config_address = config_pda(&program_id()).0;
672    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
673    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
674    let (seeker_address, _) = seeker_pda(&program_id(), &sgt_mint);
675
676    Instruction {
677        program_id: program_id(),
678        accounts: vec![
679            AccountMeta::new(signer, true),
680            AccountMeta::new_readonly(config_address, false),
681            AccountMeta::new(dojo_address, false),
682            AccountMeta::new(tasks_address, false),
683            AccountMeta::new_readonly(signer_sgt_token_account, false),
684            AccountMeta::new_readonly(sgt_mint, false),
685            AccountMeta::new_readonly(spl_token_2022::ID, false),
686            AccountMeta::new(seeker_address, false),
687            AccountMeta::new_readonly(system_program::ID, false),
688        ],
689        data: ClaimSeekerTaskReward {}.to_bytes(),
690    }
691}
692
693/// Mint soulbound NFT for Seeker users. User can either claim Seeker task first or mint directly
694/// (creates Seeker if needed). One soulbound per SGT mint.
695/// Pass soulbound_mint as a new keypair pubkey (client generates keypair for the mint account).
696pub fn mint_soulbound(
697    signer: Pubkey,
698    signer_sgt_token_account: Pubkey,
699    sgt_mint: Pubkey,
700    soulbound_mint: Pubkey,
701) -> Instruction {
702    let config_address = config_pda(&program_id()).0;
703    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
704    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
705    let (seeker_address, _) = seeker_pda(&program_id(), &sgt_mint);
706    let treasury_address = treasury_pda(&program_id()).0;
707    let soulbound_token = get_associated_token_address(&signer, &soulbound_mint);
708
709    Instruction {
710        program_id: program_id(),
711        accounts: vec![
712            AccountMeta::new(signer, true),
713            AccountMeta::new_readonly(config_address, false),
714            AccountMeta::new(dojo_address, false),
715            AccountMeta::new(tasks_address, false),
716            AccountMeta::new(seeker_address, false),
717            AccountMeta::new_readonly(signer_sgt_token_account, false),
718            AccountMeta::new_readonly(sgt_mint, false),
719            AccountMeta::new(soulbound_mint, true), // client keypair must sign for create_account
720            AccountMeta::new_readonly(treasury_address, false),
721            AccountMeta::new(DGT_GROUP, false), // DGT group mint (writable for initialize_member)
722            AccountMeta::new(soulbound_token, false),
723            AccountMeta::new_readonly(spl_token_2022::ID, false),
724            AccountMeta::new_readonly(system_program::ID, false),
725            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
726        ],
727        data: MintSoulbound {}.to_bytes(),
728    }
729}
730
731/// PvP: attack another player's dojo. Odds use each side's champion shogun spirit power.
732pub fn battle_attack(signer: Pubkey, defender_owner: Pubkey) -> Instruction {
733    let config_address = config_pda(&program_id()).0;
734    let (attacker_dojo, _) = dojo_pda(&program_id(), &signer);
735    let (defender_dojo, _) = dojo_pda(&program_id(), &defender_owner);
736    let (attacker_barracks, _) = barracks_pda(&program_id(), &attacker_dojo);
737    let (attacker_battle, _) = battle_pda(&program_id(), &attacker_dojo);
738    let (defender_barracks, _) = barracks_pda(&program_id(), &defender_dojo);
739    let (defender_battle, _) = battle_pda(&program_id(), &defender_dojo);
740    let treasury_address = treasury_pda(&program_id()).0;
741
742    Instruction {
743        program_id: program_id(),
744        accounts: vec![
745            AccountMeta::new(signer, true),
746            AccountMeta::new_readonly(config_address, false),
747            AccountMeta::new(attacker_dojo, false),
748            AccountMeta::new(attacker_barracks, false),
749            AccountMeta::new(attacker_battle, false),
750            AccountMeta::new_readonly(defender_dojo, false),
751            AccountMeta::new_readonly(defender_barracks, false),
752            AccountMeta::new(defender_battle, false),
753            AccountMeta::new(treasury_address, false),
754        ],
755        data: BattleAttack {
756            defender_owner: defender_owner.to_bytes(),
757        }
758        .to_bytes(),
759    }
760}
761
762/// Set PvP champion (barracks slot 0–11). Cooldowns apply when changing to a different slot.
763pub fn set_champion(signer: Pubkey, slot: u64) -> Instruction {
764    let config_address = config_pda(&program_id()).0;
765    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
766    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
767    let (battle_address, _) = battle_pda(&program_id(), &dojo_address);
768
769    Instruction {
770        program_id: program_id(),
771        accounts: vec![
772            AccountMeta::new(signer, true),
773            AccountMeta::new_readonly(config_address, false),
774            AccountMeta::new_readonly(dojo_address, false),
775            AccountMeta::new_readonly(barracks_address, false),
776            AccountMeta::new(battle_address, false),
777        ],
778        data: SetChampion {
779            slot: slot.to_le_bytes(),
780        }
781        .to_bytes(),
782    }
783}
784
785/// Claim collection reward (3 ninjas same element+rarity). Pass collection_index (element×5 + rarity, 0–24).
786/// Program finds 3 matching shoguns in pool.
787pub fn claim_collection_reward(signer: Pubkey, collection_index: u8) -> Instruction {
788    let config_address = config_pda(&program_id()).0;
789    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
790    let (tasks_address, _) = tasks_pda(&program_id(), &dojo_address);
791
792    Instruction {
793        program_id: program_id(),
794        accounts: vec![
795            AccountMeta::new(signer, true),
796            AccountMeta::new_readonly(config_address, false),
797            AccountMeta::new(dojo_address, false),
798            AccountMeta::new(tasks_address, false),
799        ],
800        data: ClaimCollectionReward { collection_index }.to_bytes(),
801    }
802}
803
804/// Flash sale: 50 tickets for 5000 shards, max 5 per day.
805pub fn buy_flash_sale(signer: Pubkey) -> Instruction {
806    let config_address = config_pda(&program_id()).0;
807    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
808    let treasury_address = treasury_pda(&program_id()).0;
809    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
810    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
811
812    Instruction {
813        program_id: program_id(),
814        accounts: vec![
815            AccountMeta::new(signer, true),
816            AccountMeta::new_readonly(config_address, false),
817            AccountMeta::new(dojo_address, false),
818            AccountMeta::new(user_ata, false),
819            AccountMeta::new_readonly(treasury_address, false),
820            AccountMeta::new(treasury_ata, false),
821            AccountMeta::new_readonly(spl_token::ID, false),
822        ],
823        data: BuyFlashSale {}.to_bytes(),
824    }
825}
826
827/// Daily deal: 5 tickets for 300 shards. Burns shards.
828pub fn buy_tickets_with_shards(signer: Pubkey) -> Instruction {
829    let config_address = config_pda(&program_id()).0;
830    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
831    let treasury_address = treasury_pda(&program_id()).0;
832    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
833    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
834
835    Instruction {
836        program_id: program_id(),
837        accounts: vec![
838            AccountMeta::new(signer, true),
839            AccountMeta::new_readonly(config_address, false),
840            AccountMeta::new(treasury_address, false),
841            AccountMeta::new(dojo_address, false),
842            AccountMeta::new(user_ata, false),
843            AccountMeta::new(treasury_ata, false),
844            AccountMeta::new(DOJO_MINT, false),
845            AccountMeta::new(treasury_address, false),
846            AccountMeta::new_readonly(spl_token::ID, false),
847        ],
848        data: BuyTicketsWithShards {}.to_bytes(),
849    }
850}
851
852/// Buy bundle: 150 recruitment tickets for 5 SOL (event deal).
853pub fn buy_bundle(signer: Pubkey) -> Instruction {
854    let config_address = config_pda(&program_id()).0;
855    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
856    let treasury_address = treasury_pda(&program_id()).0;
857
858    Instruction {
859        program_id: program_id(),
860        accounts: vec![
861            AccountMeta::new(signer, true),
862            AccountMeta::new_readonly(config_address, false),
863            AccountMeta::new(dojo_address, false),
864            AccountMeta::new(treasury_address, false),
865            AccountMeta::new(FEE_COLLECTOR, false),
866            AccountMeta::new_readonly(system_program::ID, false),
867        ],
868        data: BuyBundle {}.to_bytes(),
869    }
870}
871
872/// Clear forge upgrade cooldown. Cost = remaining minutes (shards). One tx clears all.
873pub fn clear_forge_cooldown(signer: Pubkey) -> Instruction {
874    let config_address = config_pda(&program_id()).0;
875    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
876    let (forge_address, _) = forge_pda(&program_id(), &dojo_address);
877    let treasury_address = treasury_pda(&program_id()).0;
878    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
879    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
880
881    Instruction {
882        program_id: program_id(),
883        accounts: vec![
884            AccountMeta::new(signer, true),
885            AccountMeta::new_readonly(config_address, false),
886            AccountMeta::new(treasury_address, false),
887            AccountMeta::new(dojo_address, false),
888            AccountMeta::new(forge_address, false),
889            AccountMeta::new(user_ata, false),
890            AccountMeta::new(treasury_ata, false),
891            AccountMeta::new(DOJO_MINT, false),
892            AccountMeta::new(treasury_address, false),
893            AccountMeta::new_readonly(spl_token::ID, false),
894        ],
895        data: ClearForgeCooldown {}.to_bytes(),
896    }
897}
898
899/// Set genesis slot and treasury.last_emission_slot (admin). halving_period_slots: 0 = use default (~28 days).
900pub fn set_genesis_slot(authority: Pubkey, genesis_slot: u64, halving_period_slots: u64) -> Instruction {
901    let config_address = config_pda(&program_id()).0;
902    let treasury_address = treasury_pda(&program_id()).0;
903
904    Instruction {
905        program_id: program_id(),
906        accounts: vec![
907            AccountMeta::new(authority, true),
908            AccountMeta::new(config_address, false),
909            AccountMeta::new(treasury_address, false),
910        ],
911        data: SetGenesisSlot {
912            genesis_slot: genesis_slot.to_le_bytes(),
913            halving_period_slots: halving_period_slots.to_le_bytes(),
914        }
915        .to_bytes(),
916    }
917}
918
919/// Roll scene sections (1 or 10) — pay with Amethyst.
920/// seed: from BSM POST /roll/instruction (Option 7 centralized oracle).
921pub fn roll_scene_section_amethyst(signer: Pubkey, count: u64, seed: [u8; 32]) -> Instruction {
922    let config_address = config_pda(&program_id()).0;
923    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
924    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
925
926    Instruction {
927        program_id: program_id(),
928        accounts: vec![
929            AccountMeta::new(signer, true),
930            AccountMeta::new_readonly(config_address, false),
931            AccountMeta::new(dojo_address, false),
932            AccountMeta::new(scenes_address, false),
933            AccountMeta::new_readonly(system_program::ID, false),
934        ],
935        data: RollSceneSectionAmethyst {
936            count: count.to_le_bytes(),
937            seed,
938        }
939        .to_bytes(),
940    }
941}
942
943/// Roll scene sections (1 or 10) — pay with Shards (SPL $DOJO).
944/// seed: from BSM POST /roll/instruction (Option 7 centralized oracle).
945pub fn roll_scene_section_shards(signer: Pubkey, count: u64, seed: [u8; 32]) -> Instruction {
946    let config_address = config_pda(&program_id()).0;
947    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
948    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
949    let treasury_address = treasury_pda(&program_id()).0;
950    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
951    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
952
953    Instruction {
954        program_id: program_id(),
955        accounts: vec![
956            AccountMeta::new(signer, true),
957            AccountMeta::new_readonly(config_address, false),
958            AccountMeta::new(treasury_address, false),
959            AccountMeta::new(dojo_address, false),
960            AccountMeta::new(scenes_address, false),
961            AccountMeta::new(user_ata, false),
962            AccountMeta::new(treasury_ata, false),
963            AccountMeta::new(DOJO_MINT, false),
964            AccountMeta::new(treasury_address, false),
965            AccountMeta::new_readonly(spl_token::ID, false),
966            AccountMeta::new_readonly(system_program::ID, false),
967        ],
968        data: RollSceneSectionShards {
969            count: count.to_le_bytes(),
970            seed,
971        }
972        .to_bytes(),
973    }
974}
975
976/// Salvage all duplicate scene sections for Amethyst refund. Program derives from on-chain state.
977pub fn salvage_scene_section(signer: Pubkey) -> Instruction {
978    let config_address = config_pda(&program_id()).0;
979    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
980    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
981
982    Instruction {
983        program_id: program_id(),
984        accounts: vec![
985            AccountMeta::new(signer, true),
986            AccountMeta::new_readonly(config_address, false),
987            AccountMeta::new(dojo_address, false),
988            AccountMeta::new(scenes_address, false),
989        ],
990        data: SalvageSceneSection {}.to_bytes(),
991    }
992}
993
994/// Set active scene (background). Requires scene unlocked.
995/// Updates Game.total_effective_spirit_power for pool-split.
996pub fn update_active_scene(signer: Pubkey, scene_id: u64) -> Instruction {
997    let config_address = config_pda(&program_id()).0;
998    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
999    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
1000    let treasury_address = treasury_pda(&program_id()).0;
1001    let (barracks_address, _) = barracks_pda(&program_id(), &dojo_address);
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(scenes_address, false),
1010            AccountMeta::new(treasury_address, false),
1011            AccountMeta::new(barracks_address, false),
1012        ],
1013        data: UpdateActiveScene {
1014            scene_id: scene_id.to_le_bytes(),
1015        }
1016        .to_bytes(),
1017    }
1018}
1019
1020/// Buy chest: 1 SOL → 5000 Amethyst. 90% to treasury, 10% to fee collector.
1021pub fn buy_chest(signer: Pubkey) -> Instruction {
1022    let config_address = config_pda(&program_id()).0;
1023    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1024    let treasury_address = treasury_pda(&program_id()).0;
1025
1026    Instruction {
1027        program_id: program_id(),
1028        accounts: vec![
1029            AccountMeta::new(signer, true),
1030            AccountMeta::new_readonly(config_address, false),
1031            AccountMeta::new(dojo_address, false),
1032            AccountMeta::new(treasury_address, false),
1033            AccountMeta::new(FEE_COLLECTOR, false),
1034            AccountMeta::new_readonly(system_program::ID, false),
1035        ],
1036        data: BuyChest {}.to_bytes(),
1037    }
1038}
1039
1040/// Buy scene 6, 7, or 8 with Amethyst. Unlocks entire scene (all 12 sections).
1041pub fn buy_scene(signer: Pubkey, scene_id: u64) -> Instruction {
1042    let config_address = config_pda(&program_id()).0;
1043    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1044    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
1045
1046    Instruction {
1047        program_id: program_id(),
1048        accounts: vec![
1049            AccountMeta::new(signer, true),
1050            AccountMeta::new_readonly(config_address, false),
1051            AccountMeta::new(dojo_address, false),
1052            AccountMeta::new(scenes_address, false),
1053            AccountMeta::new_readonly(system_program::ID, false),
1054        ],
1055        data: BuyScene {
1056            scene_id: scene_id.to_le_bytes(),
1057        }
1058        .to_bytes(),
1059    }
1060}
1061
1062/// Buy scene (6/7/8) with mixed payment: spend all amethyst, cover shortfall with DOJO.
1063pub fn buy_scene_dojo(signer: Pubkey, scene_id: u64) -> Instruction {
1064    let config_address = config_pda(&program_id()).0;
1065    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1066    let (scenes_address, _) = scenes_pda(&program_id(), &dojo_address);
1067    let treasury_address = treasury_pda(&program_id()).0;
1068    let user_ata = get_associated_token_address(&signer, &DOJO_MINT);
1069    let treasury_ata = get_associated_token_address(&treasury_address, &DOJO_MINT);
1070
1071    Instruction {
1072        program_id: program_id(),
1073        accounts: vec![
1074            AccountMeta::new(signer, true),
1075            AccountMeta::new_readonly(config_address, false),
1076            AccountMeta::new(treasury_address, false),
1077            AccountMeta::new(dojo_address, false),
1078            AccountMeta::new(scenes_address, false),
1079            AccountMeta::new(user_ata, false),
1080            AccountMeta::new(treasury_ata, false),
1081            AccountMeta::new(DOJO_MINT, false),
1082            AccountMeta::new(treasury_address, false),
1083            AccountMeta::new_readonly(spl_token::ID, false),
1084            AccountMeta::new_readonly(system_program::ID, false),
1085        ],
1086        data: BuySceneDojo {
1087            scene_id: scene_id.to_le_bytes(),
1088        }
1089        .to_bytes(),
1090    }
1091}
1092
1093/// Deposit DOJO into S2P stake (30-day linear vesting per Fren Pet).
1094/// Transfers tokens from signer's DOJO ATA to stake PDA's ATA.
1095pub fn deposit(signer: Pubkey, amount: u64) -> Instruction {
1096    let (stake_address, _) = stake_pda(&program_id(), &signer);
1097    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1098    let treasury_address = treasury_pda(&program_id()).0;
1099    let sender_ata = get_associated_token_address(&signer, &DOJO_MINT);
1100    let stake_tokens_ata = get_associated_token_address(&stake_address, &DOJO_MINT);
1101
1102    Instruction {
1103        program_id: program_id(),
1104        accounts: vec![
1105            AccountMeta::new(signer, true),
1106            AccountMeta::new(signer, true), // payer (same as signer for user-initiated)
1107            AccountMeta::new_readonly(DOJO_MINT, false),
1108            AccountMeta::new(sender_ata, false),
1109            AccountMeta::new(stake_address, false),
1110            AccountMeta::new(stake_tokens_ata, false),
1111            AccountMeta::new(dojo_address, false),
1112            AccountMeta::new(treasury_address, false),
1113            AccountMeta::new_readonly(system_program::ID, false),
1114            AccountMeta::new_readonly(spl_token::ID, false),
1115            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
1116        ],
1117        data: Deposit {
1118            amount: amount.to_le_bytes(),
1119        }
1120        .to_bytes(),
1121    }
1122}
1123
1124/// Withdraw unlocked DOJO from S2P stake (up to vested amount).
1125pub fn withdraw(signer: Pubkey, amount: u64) -> Instruction {
1126    let (stake_address, _) = stake_pda(&program_id(), &signer);
1127    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1128    let treasury_address = treasury_pda(&program_id()).0;
1129    let recipient_ata = get_associated_token_address(&signer, &DOJO_MINT);
1130    let stake_tokens_ata = get_associated_token_address(&stake_address, &DOJO_MINT);
1131
1132    Instruction {
1133        program_id: program_id(),
1134        accounts: vec![
1135            AccountMeta::new(signer, true),
1136            AccountMeta::new(signer, true), // payer (for creating recipient ATA if needed)
1137            AccountMeta::new_readonly(DOJO_MINT, false),
1138            AccountMeta::new(recipient_ata, false),
1139            AccountMeta::new(stake_address, false),
1140            AccountMeta::new(stake_tokens_ata, false),
1141            AccountMeta::new(dojo_address, false),
1142            AccountMeta::new(treasury_address, false),
1143            AccountMeta::new_readonly(system_program::ID, false),
1144            AccountMeta::new_readonly(spl_token::ID, false),
1145            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
1146        ],
1147        data: Withdraw {
1148            amount: amount.to_le_bytes(),
1149        }
1150        .to_bytes(),
1151    }
1152}
1153
1154/// Claim SOL from XP reward pool (proportional to dojo XP / total XP), then burn dojo XP.
1155pub fn claim_xp_rewards(signer: Pubkey) -> Instruction {
1156    let (dojo_address, _) = dojo_pda(&program_id(), &signer);
1157    let treasury_address = treasury_pda(&program_id()).0;
1158
1159    Instruction {
1160        program_id: program_id(),
1161        accounts: vec![
1162            AccountMeta::new(signer, true),
1163            AccountMeta::new(dojo_address, false),
1164            AccountMeta::new(treasury_address, false),
1165            AccountMeta::new_readonly(system_program::ID, false),
1166        ],
1167        data: ClaimXpRewards {}.to_bytes(),
1168    }
1169}
1170
1171/// Log (CPI from program; config signs). Variable-length message.
1172pub fn log(config: Pubkey, msg: &[u8]) -> Instruction {
1173    let mut data = Log {
1174        _reserved: [0u8; 8],
1175    }
1176    .to_bytes();
1177    data.extend_from_slice(msg);
1178    Instruction {
1179        program_id: program_id(),
1180        accounts: vec![AccountMeta::new(config, true)],
1181        data,
1182    }
1183}