Skip to main content

satrush_client/
builders.rs

1//! Environment-free instruction builders.
2//!
3//! Thin composition helpers over the generated instruction structs: they derive
4//! every PDA and associated token account from the program's fixed seeds, so a
5//! caller only supplies the signing authority and the mint addresses. Shared by
6//! the LiteSVM test-suite and the `satrush-cli` binary.
7
8use crate::instructions::{
9    CancelPublicAutomation, ClaimGrubstakeAirdrop, ClaimUsd, ClaimUsdInstructionArgs, CloseRound, CreateBoard,
10    CreateEpochVault, CreateGrubstakeAirdrop, CreateGrubstakeAirdropInstructionArgs, CreateOneBtcVault,
11    CreatePublicAutomation, CreatePublicAutomationInstructionArgs, CreateSatrushConfig,
12    CreateSatrushConfigInstructionArgs, CreateSatsVault, CreateTreasury, DeployPublic, DeployPublicInstructionArgs,
13    DepositGrubstake, DepositGrubstakeInstructionArgs, ExchangeAffiliatePoints, ExchangeAffiliatePointsInstructionArgs,
14    ExecutePublicAutomation, ExecutePublicAutomationInstructionArgs, MigrateMiner, ReclaimGrubstakeAirdrop,
15    ReclaimGrubstakeAutomation, ReclaimMinerGrubstake, RotateRound, SetAffiliateRate, SetAffiliateRateInstructionArgs,
16    SetMinerTag, SetMinerTagInstructionArgs, SettleDeployPublic, SwapRoundStake, SwapRoundStakeInstructionArgs,
17    TopUpPublicAutomation, TopUpPublicAutomationInstructionArgs, UpdateBoardRoundDuration,
18    UpdateBoardRoundDurationInstructionArgs, UpdateDeployFees, UpdateDeployFeesInstructionArgs,
19    UpdateDeploymentSettleGraceDuration, UpdateDeploymentSettleGraceDurationInstructionArgs,
20    UpdateEpochVaultIterationDuration, UpdateEpochVaultIterationDurationInstructionArgs, UpdateMinDeployUsdAmount,
21    UpdateMinDeployUsdAmountInstructionArgs, UpdateStrikeTriggerModulus, UpdateStrikeTriggerModulusInstructionArgs,
22    UpdateUnclaimedHashrateBps, UpdateUnclaimedHashrateBpsInstructionArgs, WithdrawGrubstake,
23    WithdrawGrubstakeInstructionArgs,
24};
25use crate::types::AutomationStrategy;
26use crate::{
27    get_affiliate_address, get_affiliate_tag_address, get_board_address, get_epoch_vault_address,
28    get_epoch_vault_iteration_address, get_event_authority_address, get_miner_address, get_one_btc_vault_address,
29    get_one_btc_vault_iteration_address, get_public_automation_address, get_public_deployment_address,
30    get_round_address, get_satrush_config_address, get_sats_vault_address, get_treasury_address,
31};
32use solana_instruction::{AccountMeta, Instruction};
33use solana_pubkey::Pubkey;
34
35/// SPL Token program.
36pub const TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
37/// SPL Associated Token Account program.
38pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str_const("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
39/// System program.
40pub const SYSTEM_PROGRAM_ID: Pubkey = Pubkey::from_str_const("11111111111111111111111111111111");
41/// SlotHashes sysvar.
42pub const SLOT_HASHES_ID: Pubkey = Pubkey::from_str_const("SysvarS1otHashes111111111111111111111111111");
43
44/// Derive the canonical associated token account for `wallet` holding `mint`.
45pub fn get_associated_token_address(wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
46    Pubkey::find_program_address(
47        &[wallet.as_ref(), TOKEN_PROGRAM_ID.as_ref(), mint.as_ref()],
48        &ASSOCIATED_TOKEN_PROGRAM_ID,
49    )
50    .0
51}
52
53/// `create_satrush_config`: the config PDA holding authorities, mints and fee
54/// parameters. `authority` pays and must equal the program's upgrade authority.
55pub fn get_create_satrush_config_instruction(
56    authority: Pubkey,
57    args: CreateSatrushConfigInstructionArgs,
58) -> Instruction {
59    CreateSatrushConfig {
60        authority,
61        satrush_config: get_satrush_config_address().0,
62        system_program: SYSTEM_PROGRAM_ID,
63    }
64    .instruction(args)
65}
66
67/// `create_board`: the board singleton, its USD/BTC pools and the initial round
68/// (id 1). Signed by the config's admin authority.
69pub fn get_create_board_instruction(authority: Pubkey, usd_mint: Pubkey, btc_mint: Pubkey) -> Instruction {
70    let board = get_board_address().0;
71    CreateBoard {
72        authority,
73        satrush_config: get_satrush_config_address().0,
74        board,
75        initial_round: get_round_address(1).0,
76        usd_mint,
77        btc_mint,
78        board_usd_ata: get_associated_token_address(&board, &usd_mint),
79        board_btc_ata: get_associated_token_address(&board, &btc_mint),
80        token_program: TOKEN_PROGRAM_ID,
81        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
82        system_program: SYSTEM_PROGRAM_ID,
83    }
84    .instruction()
85}
86
87/// `update_board_round_duration`: overwrite the board's `round_duration` (slots).
88/// Applies to future round activations only; the active round keeps its window.
89/// Signed by the config's admin authority.
90pub fn get_update_board_round_duration_instruction(authority: Pubkey, new_round_duration: u32) -> Instruction {
91    UpdateBoardRoundDuration {
92        authority,
93        satrush_config: get_satrush_config_address().0,
94        board: get_board_address().0,
95    }
96    .instruction(UpdateBoardRoundDurationInstructionArgs { new_round_duration })
97}
98
99/// `update_min_deploy_usd_amount`: overwrite the config's minimum gross deploy
100/// size (USD mint base units). Applies to every subsequent manual deploy,
101/// automation creation and automation execution. Signed by the config's admin
102/// authority.
103pub fn get_update_min_deploy_usd_amount_instruction(authority: Pubkey, new_min_deploy_usd_amount: u64) -> Instruction {
104    UpdateMinDeployUsdAmount {
105        authority,
106        satrush_config: get_satrush_config_address().0,
107    }
108    .instruction(UpdateMinDeployUsdAmountInstructionArgs {
109        new_min_deploy_usd_amount,
110    })
111}
112
113/// `update_unclaimed_hashrate_bps`: overwrite the config's deferred hashrate
114/// bonus ratio (bps of each settled play's reward). Signed by the config's
115/// admin authority.
116pub fn get_update_unclaimed_hashrate_bps_instruction(
117    authority: Pubkey,
118    new_unclaimed_hashrate_bps: u32,
119) -> Instruction {
120    UpdateUnclaimedHashrateBps {
121        authority,
122        satrush_config: get_satrush_config_address().0,
123    }
124    .instruction(UpdateUnclaimedHashrateBpsInstructionArgs {
125        new_unclaimed_hashrate_bps,
126    })
127}
128
129/// `update_deploy_fees`: atomically overwrite the five deploy-time fee legs.
130/// Each leg must be at least 1 bps and the legs must sum to exactly 600 bps
131/// (6%); the split may vary but the total never changes. Signed by the
132/// config's admin authority.
133pub fn get_update_deploy_fees_instruction(
134    authority: Pubkey,
135    new_strike_fee_bps: u32,
136    new_epoch_fee_bps: u32,
137    new_one_btc_fee_bps: u32,
138    new_protocol_fee_bps: u32,
139    new_buybacks_fee_bps: u32,
140) -> Instruction {
141    UpdateDeployFees {
142        authority,
143        satrush_config: get_satrush_config_address().0,
144    }
145    .instruction(UpdateDeployFeesInstructionArgs {
146        new_strike_fee_bps,
147        new_epoch_fee_bps,
148        new_one_btc_fee_bps,
149        new_protocol_fee_bps,
150        new_buybacks_fee_bps,
151    })
152}
153
154/// `update_epoch_vault_iteration_duration`: overwrite the config's epoch vault
155/// iteration length (slots). Applies to the currently accumulating iteration
156/// immediately. Signed by the config's admin authority.
157pub fn get_update_epoch_vault_iteration_duration_instruction(
158    authority: Pubkey,
159    new_iteration_duration: u64,
160) -> Instruction {
161    UpdateEpochVaultIterationDuration {
162        authority,
163        satrush_config: get_satrush_config_address().0,
164    }
165    .instruction(UpdateEpochVaultIterationDurationInstructionArgs { new_iteration_duration })
166}
167
168/// `update_strike_trigger_modulus`: admin retunes the Sat Strike odds (the
169/// jackpot fires when `rng % modulus == 0`). Signed by the config's admin
170/// authority.
171pub fn get_update_strike_trigger_modulus_instruction(authority: Pubkey, new_modulus: u16) -> Instruction {
172    UpdateStrikeTriggerModulus {
173        authority,
174        satrush_config: get_satrush_config_address().0,
175    }
176    .instruction(UpdateStrikeTriggerModulusInstructionArgs { new_modulus })
177}
178
179/// `update_deployment_settle_grace_duration`: admin retunes the settle grace
180/// window (slots after a round settles before stale deployments may be
181/// force-cleaned; 0 disables cleanup).
182pub fn get_update_deployment_settle_grace_duration_instruction(
183    authority: Pubkey,
184    new_grace_duration: u64,
185) -> Instruction {
186    UpdateDeploymentSettleGraceDuration {
187        authority,
188        satrush_config: get_satrush_config_address().0,
189    }
190    .instruction(UpdateDeploymentSettleGraceDurationInstructionArgs { new_grace_duration })
191}
192
193/// `create_epoch_vault`: the epoch vault singleton, its USD/BTC pools and the
194/// first iteration (id 1).
195pub fn get_create_epoch_vault_instruction(authority: Pubkey, usd_mint: Pubkey, btc_mint: Pubkey) -> Instruction {
196    let epoch_vault = get_epoch_vault_address().0;
197    CreateEpochVault {
198        authority,
199        satrush_config: get_satrush_config_address().0,
200        epoch_vault,
201        epoch_vault_iteration: get_epoch_vault_iteration_address(1).0,
202        usd_mint,
203        btc_mint,
204        epoch_vault_usd_ata: get_associated_token_address(&epoch_vault, &usd_mint),
205        epoch_vault_btc_ata: get_associated_token_address(&epoch_vault, &btc_mint),
206        token_program: TOKEN_PROGRAM_ID,
207        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
208        system_program: SYSTEM_PROGRAM_ID,
209    }
210    .instruction()
211}
212
213/// `create_one_btc_vault`: the 1 BTC vault singleton, its USD/BTC pools and the
214/// first iteration (id 1).
215pub fn get_create_one_btc_vault_instruction(authority: Pubkey, usd_mint: Pubkey, btc_mint: Pubkey) -> Instruction {
216    let one_btc_vault = get_one_btc_vault_address().0;
217    CreateOneBtcVault {
218        authority,
219        satrush_config: get_satrush_config_address().0,
220        one_btc_vault,
221        one_btc_vault_iteration: get_one_btc_vault_iteration_address(1).0,
222        usd_mint,
223        btc_mint,
224        one_btc_vault_usd_ata: get_associated_token_address(&one_btc_vault, &usd_mint),
225        one_btc_vault_btc_ata: get_associated_token_address(&one_btc_vault, &btc_mint),
226        token_program: TOKEN_PROGRAM_ID,
227        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
228        system_program: SYSTEM_PROGRAM_ID,
229    }
230    .instruction()
231}
232
233/// `create_treasury`: the treasury singleton and its USD fee pool.
234pub fn get_create_treasury_instruction(authority: Pubkey, usd_mint: Pubkey) -> Instruction {
235    let treasury = get_treasury_address().0;
236    CreateTreasury {
237        authority,
238        satrush_config: get_satrush_config_address().0,
239        treasury,
240        usd_mint,
241        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
242        token_program: TOKEN_PROGRAM_ID,
243        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
244        system_program: SYSTEM_PROGRAM_ID,
245    }
246    .instruction()
247}
248
249/// `deploy_public`: stake `amount` USD (base units; all fees are deducted from
250/// it, so the miner parts with exactly `amount`) on the tiles
251/// in `selection_mask` for round `round_id`, as the miner `authority`. The round
252/// must be the board's current one and open for deploys; the miner profile and
253/// deployment record PDAs are created by the instruction.
254pub fn get_deploy_public_instruction(
255    authority: Pubkey,
256    usd_mint: Pubkey,
257    round_id: u32,
258    selection_mask: u32,
259    amount: u64,
260) -> Instruction {
261    let funding_usd_ata = get_associated_token_address(&authority, &usd_mint);
262    build_deploy_public_instruction(authority, usd_mint, round_id, selection_mask, amount, funding_usd_ata, false, None)
263}
264
265/// `deploy_public` carrying the affiliate account of `affiliate_authority`:
266/// binds the miner when this deploy creates it, and accrues the affiliate's
267/// rate share of the deploy's protocol fee leg
268/// (`protocol_fee × rate_bps / 10_000`) as points.
269pub fn get_deploy_public_with_affiliate_instruction(
270    authority: Pubkey,
271    usd_mint: Pubkey,
272    round_id: u32,
273    selection_mask: u32,
274    amount: u64,
275    affiliate_authority: Pubkey,
276) -> Instruction {
277    let funding_usd_ata = get_associated_token_address(&authority, &usd_mint);
278    build_deploy_public_instruction(
279        authority,
280        usd_mint,
281        round_id,
282        selection_mask,
283        amount,
284        funding_usd_ata,
285        false,
286        Some(get_affiliate_address(affiliate_authority).0),
287    )
288}
289
290/// `deploy_public` funded from the miner's grubstake balance: the stake is
291/// pulled from the miner PDA's USD ATA and no hashrate is earned; settled USD
292/// winnings return to the grubstake.
293pub fn get_deploy_public_grubstake_instruction(
294    authority: Pubkey,
295    usd_mint: Pubkey,
296    round_id: u32,
297    selection_mask: u32,
298    amount: u64,
299) -> Instruction {
300    let miner = get_miner_address(authority).0;
301    let funding_usd_ata = get_associated_token_address(&miner, &usd_mint);
302    build_deploy_public_instruction(authority, usd_mint, round_id, selection_mask, amount, funding_usd_ata, true, None)
303}
304
305/// Grubstake-funded `deploy_public` carrying the affiliate account of
306/// `affiliate_authority`. The affiliate must match the miner's bound tag;
307/// bonus-funded deploys accrue no points.
308pub fn get_deploy_public_grubstake_with_affiliate_instruction(
309    authority: Pubkey,
310    usd_mint: Pubkey,
311    round_id: u32,
312    selection_mask: u32,
313    amount: u64,
314    affiliate_authority: Pubkey,
315) -> Instruction {
316    let miner = get_miner_address(authority).0;
317    let funding_usd_ata = get_associated_token_address(&miner, &usd_mint);
318    build_deploy_public_instruction(
319        authority,
320        usd_mint,
321        round_id,
322        selection_mask,
323        amount,
324        funding_usd_ata,
325        true,
326        Some(get_affiliate_address(affiliate_authority).0),
327    )
328}
329
330#[allow(clippy::too_many_arguments)]
331fn build_deploy_public_instruction(
332    authority: Pubkey,
333    usd_mint: Pubkey,
334    round_id: u32,
335    selection_mask: u32,
336    amount: u64,
337    funding_usd_ata: Pubkey,
338    is_grubstake_funded: bool,
339    affiliate: Option<Pubkey>,
340) -> Instruction {
341    let board = get_board_address().0;
342
343    DeployPublic {
344        authority,
345        satrush_config: get_satrush_config_address().0,
346        board,
347        usd_mint,
348        board_usd_ata: get_associated_token_address(&board, &usd_mint),
349        authority_usd_ata: funding_usd_ata,
350        round: get_round_address(round_id).0,
351        public_deployment: get_public_deployment_address(authority, round_id).0,
352        miner: get_miner_address(authority).0,
353        affiliate,
354        token_program: TOKEN_PROGRAM_ID,
355        system_program: SYSTEM_PROGRAM_ID,
356        event_authority: get_event_authority_address().0,
357        program: crate::SATRUSH_ID,
358    }
359    .instruction_with_remaining_accounts(
360        DeployPublicInstructionArgs {
361            selection_mask,
362            amount,
363            is_grubstake_funded,
364        },
365        // Always carried: when this deploy is the round's first entry it arms
366        // the round rotor via CPI, and the caller cannot know in advance.
367        &crate::rng::get_rng_remaining_accounts(),
368    )
369}
370
371/// `rotate_round`: reveal `current_round_id`'s winning tile, sweep the round's
372/// accumulated fee legs from the board pool to the epoch/1 BTC/treasury pools
373/// (plus, on a strike trigger, the strike skim's USD and BTC legs to the epoch
374/// vault), and deploy round `current_round_id + 1` as the board's new active
375/// round. Signed by the config's round authority; valid once the round's
376/// window elapsed.
377pub fn get_rotate_round_instruction(
378    authority: Pubkey,
379    usd_mint: Pubkey,
380    btc_mint: Pubkey,
381    current_round_id: u32,
382) -> Instruction {
383    let board = get_board_address().0;
384    let epoch_vault = get_epoch_vault_address().0;
385    let one_btc_vault = get_one_btc_vault_address().0;
386    let treasury = get_treasury_address().0;
387
388    RotateRound {
389        authority,
390        satrush_config: get_satrush_config_address().0,
391        board,
392        current_round: get_round_address(current_round_id).0,
393        next_round: get_round_address(current_round_id + 1).0,
394        usd_mint,
395        btc_mint,
396        board_usd_ata: get_associated_token_address(&board, &usd_mint),
397        board_btc_ata: get_associated_token_address(&board, &btc_mint),
398        epoch_vault,
399        epoch_vault_usd_ata: get_associated_token_address(&epoch_vault, &usd_mint),
400        epoch_vault_btc_ata: get_associated_token_address(&epoch_vault, &btc_mint),
401        one_btc_vault,
402        one_btc_vault_usd_ata: get_associated_token_address(&one_btc_vault, &usd_mint),
403        treasury,
404        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
405        round_rotor: crate::rng::get_rotor_address(crate::rng::ROUND_ROTOR_TAG).0,
406        token_program: TOKEN_PROGRAM_ID,
407        system_program: SYSTEM_PROGRAM_ID,
408        event_authority: get_event_authority_address().0,
409        program: crate::SATRUSH_ID,
410    }
411    .instruction()
412}
413
414/// `swap_round_stake`: relay a pre-built aggregator route that converts part of
415/// the revealed round's USD into BTC. `swap_data` and `route_accounts` are the
416/// route's opaque instruction data and account list; the on-chain handler
417/// enforces the economics against observed balance deltas. Signed by the
418/// config's round authority.
419#[allow(clippy::too_many_arguments)]
420pub fn get_swap_round_stake_instruction(
421    authority: Pubkey,
422    usd_mint: Pubkey,
423    btc_mint: Pubkey,
424    round_id: u32,
425    min_btc_out: u64,
426    swap_program: Pubkey,
427    swap_data: Vec<u8>,
428    route_accounts: &[AccountMeta],
429) -> Instruction {
430    let board = get_board_address().0;
431    SwapRoundStake {
432        authority,
433        satrush_config: get_satrush_config_address().0,
434        board,
435        round: get_round_address(round_id).0,
436        usd_mint,
437        btc_mint,
438        board_usd_ata: get_associated_token_address(&board, &usd_mint),
439        board_btc_ata: get_associated_token_address(&board, &btc_mint),
440        swap_program,
441        token_program: TOKEN_PROGRAM_ID,
442        event_authority: get_event_authority_address().0,
443        program: crate::SATRUSH_ID,
444    }
445    .instruction_with_remaining_accounts(SwapRoundStakeInstructionArgs { min_btc_out, swap_data }, route_accounts)
446}
447
448/// `settle_deploy_public`: settle `deployment_authority`'s deployment in a
449/// Settled round. `authority` signs (the owner, or the round authority for
450/// automated deployments); `rent_recipient` must be the round authority for
451/// automated deployments and the owner for manual ones.
452pub fn get_settle_deploy_public_instruction(
453    authority: Pubkey,
454    deployment_authority: Pubkey,
455    rent_recipient: Pubkey,
456    usd_mint: Pubkey,
457    btc_mint: Pubkey,
458    round_id: u32,
459) -> Instruction {
460    let board = get_board_address().0;
461    let sats_vault = get_sats_vault_address().0;
462    let public_automation = get_public_automation_address(deployment_authority).0;
463    let miner = get_miner_address(deployment_authority).0;
464    SettleDeployPublic {
465        authority,
466        rent_recipient,
467        satrush_config: get_satrush_config_address().0,
468        round: get_round_address(round_id).0,
469        board,
470        public_deployment: get_public_deployment_address(deployment_authority, round_id).0,
471        miner,
472        public_automation,
473        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
474        miner_usd_ata: get_associated_token_address(&miner, &usd_mint),
475        board_usd_ata: get_associated_token_address(&board, &usd_mint),
476        sats_vault,
477        btc_mint,
478        usd_mint,
479        board_btc_ata: get_associated_token_address(&board, &btc_mint),
480        sats_vault_btc_ata: get_associated_token_address(&sats_vault, &btc_mint),
481        token_program: TOKEN_PROGRAM_ID,
482        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
483        system_program: SYSTEM_PROGRAM_ID,
484        event_authority: get_event_authority_address().0,
485        program: crate::SATRUSH_ID,
486    }
487    .instruction()
488}
489
490/// `create_public_automation`: escrow `deposit_usd_amount` and configure the
491/// crank to deploy `per_round_usd_amount` per round with the given strategy.
492/// One automation per authority.
493#[allow(clippy::too_many_arguments)]
494pub fn get_create_public_automation_instruction(
495    authority: Pubkey,
496    usd_mint: Pubkey,
497    strategy: AutomationStrategy,
498    selection_mask: u32,
499    per_round_usd_amount: u64,
500    reload: bool,
501    deposit_usd_amount: u64,
502) -> Instruction {
503    let funding_usd_ata = get_associated_token_address(&authority, &usd_mint);
504    build_create_public_automation_instruction(
505        authority,
506        usd_mint,
507        strategy,
508        selection_mask,
509        per_round_usd_amount,
510        reload,
511        deposit_usd_amount,
512        funding_usd_ata,
513        false,
514        None,
515    )
516}
517
518/// `create_public_automation` carrying the affiliate account of
519/// `affiliate_authority`: binds the miner when this creation initializes it.
520/// The deposit itself accrues no points — volume accrues at execution.
521#[allow(clippy::too_many_arguments)]
522pub fn get_create_public_automation_with_affiliate_instruction(
523    authority: Pubkey,
524    usd_mint: Pubkey,
525    strategy: AutomationStrategy,
526    selection_mask: u32,
527    per_round_usd_amount: u64,
528    reload: bool,
529    deposit_usd_amount: u64,
530    affiliate_authority: Pubkey,
531) -> Instruction {
532    let funding_usd_ata = get_associated_token_address(&authority, &usd_mint);
533    build_create_public_automation_instruction(
534        authority,
535        usd_mint,
536        strategy,
537        selection_mask,
538        per_round_usd_amount,
539        reload,
540        deposit_usd_amount,
541        funding_usd_ata,
542        false,
543        Some(get_affiliate_address(affiliate_authority).0),
544    )
545}
546
547/// `create_public_automation` funded from the miner's grubstake balance: the
548/// deposit is pulled from the miner PDA's USD ATA, and every deployment the
549/// automation creates plays as grubstake-funded.
550#[allow(clippy::too_many_arguments)]
551pub fn get_create_public_automation_grubstake_instruction(
552    authority: Pubkey,
553    usd_mint: Pubkey,
554    strategy: AutomationStrategy,
555    selection_mask: u32,
556    per_round_usd_amount: u64,
557    reload: bool,
558    deposit_usd_amount: u64,
559) -> Instruction {
560    let miner = get_miner_address(authority).0;
561    let funding_usd_ata = get_associated_token_address(&miner, &usd_mint);
562    build_create_public_automation_instruction(
563        authority,
564        usd_mint,
565        strategy,
566        selection_mask,
567        per_round_usd_amount,
568        reload,
569        deposit_usd_amount,
570        funding_usd_ata,
571        true,
572        None,
573    )
574}
575
576#[allow(clippy::too_many_arguments)]
577fn build_create_public_automation_instruction(
578    authority: Pubkey,
579    usd_mint: Pubkey,
580    strategy: AutomationStrategy,
581    selection_mask: u32,
582    per_round_usd_amount: u64,
583    reload: bool,
584    deposit_usd_amount: u64,
585    funding_usd_ata: Pubkey,
586    is_grubstake_funded: bool,
587    affiliate: Option<Pubkey>,
588) -> Instruction {
589    let public_automation = get_public_automation_address(authority).0;
590    CreatePublicAutomation {
591        authority,
592        satrush_config: get_satrush_config_address().0,
593        public_automation,
594        usd_mint,
595        authority_usd_ata: funding_usd_ata,
596        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
597        miner: get_miner_address(authority).0,
598        affiliate,
599        token_program: TOKEN_PROGRAM_ID,
600        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
601        system_program: SYSTEM_PROGRAM_ID,
602    }
603    .instruction(CreatePublicAutomationInstructionArgs {
604        strategy,
605        selection_mask,
606        per_round_usd_amount,
607        reload,
608        deposit_usd_amount,
609        is_grubstake_funded,
610    })
611}
612
613/// `top_up_public_automation`: move `amount` USD from the authority's account
614/// into the automation's escrow. For grubstake automations use
615/// `get_top_up_public_automation_grubstake_instruction` — the program only
616/// accepts the matching funding source.
617pub fn get_top_up_public_automation_instruction(authority: Pubkey, usd_mint: Pubkey, amount: u64) -> Instruction {
618    let funding_usd_ata = get_associated_token_address(&authority, &usd_mint);
619    build_top_up_public_automation_instruction(authority, usd_mint, amount, funding_usd_ata)
620}
621
622/// `top_up_public_automation` for a grubstake automation: `amount` moves from
623/// the miner PDA's USD ATA (debiting the grubstake balance) into the escrow.
624pub fn get_top_up_public_automation_grubstake_instruction(
625    authority: Pubkey,
626    usd_mint: Pubkey,
627    amount: u64,
628) -> Instruction {
629    let miner = get_miner_address(authority).0;
630    let funding_usd_ata = get_associated_token_address(&miner, &usd_mint);
631    build_top_up_public_automation_instruction(authority, usd_mint, amount, funding_usd_ata)
632}
633
634fn build_top_up_public_automation_instruction(
635    authority: Pubkey,
636    usd_mint: Pubkey,
637    amount: u64,
638    funding_usd_ata: Pubkey,
639) -> Instruction {
640    let public_automation = get_public_automation_address(authority).0;
641    TopUpPublicAutomation {
642        authority,
643        satrush_config: get_satrush_config_address().0,
644        public_automation,
645        miner: get_miner_address(authority).0,
646        usd_mint,
647        authority_usd_ata: funding_usd_ata,
648        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
649        token_program: TOKEN_PROGRAM_ID,
650    }
651    .instruction(TopUpPublicAutomationInstructionArgs { amount })
652}
653
654/// `cancel_public_automation`: refund the full escrow balance and close the
655/// automation and its token account. Unconditional; in-flight deployments
656/// settle to the miner profile later. Grubstake escrows refund into the
657/// miner's grubstake instead of the wallet.
658pub fn get_cancel_public_automation_instruction(authority: Pubkey, usd_mint: Pubkey) -> Instruction {
659    let public_automation = get_public_automation_address(authority).0;
660    let miner = get_miner_address(authority).0;
661    CancelPublicAutomation {
662        authority,
663        satrush_config: get_satrush_config_address().0,
664        public_automation,
665        miner,
666        usd_mint,
667        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
668        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
669        miner_usd_ata: get_associated_token_address(&miner, &usd_mint),
670        token_program: TOKEN_PROGRAM_ID,
671        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
672        system_program: SYSTEM_PROGRAM_ID,
673    }
674    .instruction()
675}
676
677/// `execute_public_automation`: crank-signed per-round deploy funded from
678/// `automation_authority`'s escrow. `selection_mask` must be `Some` for
679/// Discretionary automations and `None` otherwise.
680pub fn get_execute_public_automation_instruction(
681    crank_authority: Pubkey,
682    automation_authority: Pubkey,
683    usd_mint: Pubkey,
684    round_id: u32,
685    selection_mask: Option<u32>,
686) -> Instruction {
687    let board = get_board_address().0;
688    let public_automation = get_public_automation_address(automation_authority).0;
689
690    ExecutePublicAutomation {
691        authority: crank_authority,
692        satrush_config: get_satrush_config_address().0,
693        board,
694        round: get_round_address(round_id).0,
695        public_automation,
696        usd_mint,
697        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
698        board_usd_ata: get_associated_token_address(&board, &usd_mint),
699        public_deployment: get_public_deployment_address(automation_authority, round_id).0,
700        miner: get_miner_address(automation_authority).0,
701        affiliate: None,
702        slot_hashes: SLOT_HASHES_ID,
703        token_program: TOKEN_PROGRAM_ID,
704        system_program: SYSTEM_PROGRAM_ID,
705        event_authority: get_event_authority_address().0,
706        program: crate::SATRUSH_ID,
707    }
708    .instruction_with_remaining_accounts(
709        ExecutePublicAutomationInstructionArgs { selection_mask },
710        // Same first-entry rotor arming as the manual deploy builders.
711        &crate::rng::get_rng_remaining_accounts(),
712    )
713}
714
715/// `execute_public_automation` carrying the affiliate account of
716/// `affiliate_authority`, so a bound miner's wallet-funded automation deploy
717/// accrues points.
718pub fn get_execute_public_automation_with_affiliate_instruction(
719    crank_authority: Pubkey,
720    automation_authority: Pubkey,
721    usd_mint: Pubkey,
722    round_id: u32,
723    selection_mask: Option<u32>,
724    affiliate_authority: Pubkey,
725) -> Instruction {
726    let board = get_board_address().0;
727    let public_automation = get_public_automation_address(automation_authority).0;
728
729    ExecutePublicAutomation {
730        authority: crank_authority,
731        satrush_config: get_satrush_config_address().0,
732        board,
733        round: get_round_address(round_id).0,
734        public_automation,
735        usd_mint,
736        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
737        board_usd_ata: get_associated_token_address(&board, &usd_mint),
738        public_deployment: get_public_deployment_address(automation_authority, round_id).0,
739        miner: get_miner_address(automation_authority).0,
740        affiliate: Some(get_affiliate_address(affiliate_authority).0),
741        slot_hashes: SLOT_HASHES_ID,
742        token_program: TOKEN_PROGRAM_ID,
743        system_program: SYSTEM_PROGRAM_ID,
744        event_authority: get_event_authority_address().0,
745        program: crate::SATRUSH_ID,
746    }
747    .instruction_with_remaining_accounts(
748        ExecutePublicAutomationInstructionArgs { selection_mask },
749        &crate::rng::get_rng_remaining_accounts(),
750    )
751}
752
753/// `claim_usd`: withdraw `amount` of the miner `authority`'s unclaimed USD
754/// winnings from the board's USD pool to the authority's USD account. No exit
755/// fee — the full amount transfers.
756pub fn get_claim_usd_instruction(authority: Pubkey, usd_mint: Pubkey, amount: u64) -> Instruction {
757    let board = get_board_address().0;
758    ClaimUsd {
759        authority,
760        satrush_config: get_satrush_config_address().0,
761        board,
762        miner: get_miner_address(authority).0,
763        usd_mint,
764        board_usd_ata: get_associated_token_address(&board, &usd_mint),
765        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
766        token_program: TOKEN_PROGRAM_ID,
767        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
768        system_program: SYSTEM_PROGRAM_ID,
769    }
770    .instruction(ClaimUsdInstructionArgs { amount })
771}
772
773/// `migrate_miner`: extend a 115-byte v1 miner account to the v2 layout
774/// (`affiliate` Pubkey + reserved tail, version stamped to 2). Signed by the
775/// config's admin authority, who also funds the rent top-up.
776pub fn get_migrate_miner_instruction(authority: Pubkey, miner: Pubkey) -> Instruction {
777    MigrateMiner {
778        authority,
779        satrush_config: get_satrush_config_address().0,
780        miner,
781        system_program: SYSTEM_PROGRAM_ID,
782    }
783    .instruction()
784}
785
786/// `create_sats_vault`: the sats vault singleton and its BTC reserve pool.
787pub fn get_create_sats_vault_instruction(authority: Pubkey, btc_mint: Pubkey) -> Instruction {
788    let sats_vault = get_sats_vault_address().0;
789    CreateSatsVault {
790        authority,
791        satrush_config: get_satrush_config_address().0,
792        sats_vault,
793        btc_mint,
794        sats_vault_btc_ata: get_associated_token_address(&sats_vault, &btc_mint),
795        token_program: TOKEN_PROGRAM_ID,
796        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
797        system_program: SYSTEM_PROGRAM_ID,
798    }
799    .instruction()
800}
801
802/// `close_round`: tear down a Finished round, sweeping a no-winner round's
803/// orphaned pot (USD and BTC) into the treasury. Signed by the config's admin
804/// authority, which receives the round account's rent.
805pub fn get_close_round_instruction(
806    authority: Pubkey,
807    usd_mint: Pubkey,
808    btc_mint: Pubkey,
809    round_id: u32,
810) -> Instruction {
811    let board = get_board_address().0;
812    let treasury = get_treasury_address().0;
813    CloseRound {
814        authority,
815        satrush_config: get_satrush_config_address().0,
816        board,
817        round: get_round_address(round_id).0,
818        treasury,
819        usd_mint,
820        btc_mint,
821        board_usd_ata: get_associated_token_address(&board, &usd_mint),
822        board_btc_ata: get_associated_token_address(&board, &btc_mint),
823        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
824        treasury_btc_ata: get_associated_token_address(&treasury, &btc_mint),
825        token_program: TOKEN_PROGRAM_ID,
826        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
827        system_program: SYSTEM_PROGRAM_ID,
828        event_authority: get_event_authority_address().0,
829        program: crate::SATRUSH_ID,
830    }
831    .instruction()
832}
833
834/// `set_miner_tag`: claim `tag` for `authority`, creating the wallet's
835/// affiliate identity automatically. The affiliate PDA is seeded by the
836/// authority, so a wallet can only ever register once; the tag registry PDA
837/// is seeded by the tag, so a taken tag fails at creation.
838pub fn get_set_miner_tag_instruction(authority: Pubkey, tag: &str) -> Instruction {
839    SetMinerTag {
840        authority,
841        affiliate: get_affiliate_address(authority).0,
842        affiliate_tag: get_affiliate_tag_address(tag).0,
843        system_program: SYSTEM_PROGRAM_ID,
844    }
845    .instruction(SetMinerTagInstructionArgs { tag: tag.to_string() })
846}
847
848/// `exchange_affiliate_points`: convert `points_amount` of the affiliate's
849/// spendable points into a grubstake credit on the affiliate's own miner.
850/// Points are USD base units valued at accrual, so the exchange is 1:1.
851pub fn get_exchange_affiliate_points_instruction(
852    authority: Pubkey,
853    usd_mint: Pubkey,
854    points_amount: u64,
855) -> Instruction {
856    let miner = get_miner_address(authority).0;
857    let treasury = get_treasury_address().0;
858    ExchangeAffiliatePoints {
859        authority,
860        satrush_config: get_satrush_config_address().0,
861        affiliate: get_affiliate_address(authority).0,
862        miner,
863        treasury,
864        usd_mint,
865        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
866        miner_usd_ata: get_associated_token_address(&miner, &usd_mint),
867        token_program: TOKEN_PROGRAM_ID,
868        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
869        system_program: SYSTEM_PROGRAM_ID,
870    }
871    .instruction(ExchangeAffiliatePointsInstructionArgs { points_amount })
872}
873
874/// `set_affiliate_rate`: set the affiliate's share of the protocol fee leg
875/// (bps, admin only). Shapes future accruals; earned points keep their value.
876pub fn get_set_affiliate_rate_instruction(
877    authority: Pubkey,
878    affiliate_authority: Pubkey,
879    rate_bps: u16,
880) -> Instruction {
881    SetAffiliateRate {
882        authority,
883        satrush_config: get_satrush_config_address().0,
884        affiliate: get_affiliate_address(affiliate_authority).0,
885    }
886    .instruction(SetAffiliateRateInstructionArgs { rate_bps })
887}
888
889/// `deposit_grubstake`: fund the treasury's grubstake pool with `usd_amount`
890/// from `authority`'s own USD account. Admin authority or fee recipient.
891pub fn get_deposit_grubstake_instruction(authority: Pubkey, usd_mint: Pubkey, usd_amount: u64) -> Instruction {
892    let treasury = get_treasury_address().0;
893    DepositGrubstake {
894        authority,
895        satrush_config: get_satrush_config_address().0,
896        treasury,
897        usd_mint,
898        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
899        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
900        token_program: TOKEN_PROGRAM_ID,
901    }
902    .instruction(DepositGrubstakeInstructionArgs { usd_amount })
903}
904
905/// `withdraw_grubstake`: pull `usd_amount` of unreserved grubstake back out to
906/// `fee_recipient`. Only the portion not promised to issued airdrops may leave.
907pub fn get_withdraw_grubstake_instruction(
908    authority: Pubkey,
909    fee_recipient: Pubkey,
910    usd_mint: Pubkey,
911    usd_amount: u64,
912) -> Instruction {
913    let treasury = get_treasury_address().0;
914    WithdrawGrubstake {
915        authority,
916        satrush_config: get_satrush_config_address().0,
917        treasury,
918        usd_mint,
919        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
920        fee_recipient,
921        fee_recipient_usd_ata: get_associated_token_address(&fee_recipient, &usd_mint),
922        token_program: TOKEN_PROGRAM_ID,
923        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
924        system_program: SYSTEM_PROGRAM_ID,
925    }
926    .instruction(WithdrawGrubstakeInstructionArgs { usd_amount })
927}
928
929/// `create_grubstake_airdrop`: reserve `usd_amount` of the pool for
930/// `airdrop_authority` to claim before `expiration_timestamp`. `airdrop` is a
931/// fresh keypair's pubkey and must also sign the transaction. Fee recipient
932/// only.
933pub fn get_create_grubstake_airdrop_instruction(
934    authority: Pubkey,
935    airdrop: Pubkey,
936    airdrop_authority: Pubkey,
937    usd_amount: u64,
938    expiration_timestamp: i64,
939) -> Instruction {
940    CreateGrubstakeAirdrop {
941        authority,
942        satrush_config: get_satrush_config_address().0,
943        grubstake_airdrop: airdrop,
944        treasury: get_treasury_address().0,
945        system_program: SYSTEM_PROGRAM_ID,
946    }
947    .instruction(CreateGrubstakeAirdropInstructionArgs {
948        airdrop_authority,
949        usd_amount,
950        expiration_timestamp,
951    })
952}
953
954/// `claim_grubstake_airdrop`: claim `airdrop` as `authority`. The payout lands
955/// in the miner PDA's USD account and opens the 14-day spend window.
956pub fn get_claim_grubstake_airdrop_instruction(
957    authority: Pubkey,
958    airdrop: Pubkey,
959    fee_recipient: Pubkey,
960    usd_mint: Pubkey,
961) -> Instruction {
962    let treasury = get_treasury_address().0;
963    let miner = get_miner_address(authority).0;
964    ClaimGrubstakeAirdrop {
965        authority,
966        satrush_config: get_satrush_config_address().0,
967        fee_recipient,
968        grubstake_airdrop: airdrop,
969        miner,
970        treasury,
971        usd_mint,
972        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
973        miner_usd_ata: get_associated_token_address(&miner, &usd_mint),
974        token_program: TOKEN_PROGRAM_ID,
975        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
976        system_program: SYSTEM_PROGRAM_ID,
977    }
978    .instruction()
979}
980
981/// `reclaim_grubstake_airdrop`: release an expired, unclaimed airdrop's
982/// reservation back to the pool and close the account. Fee recipient only.
983pub fn get_reclaim_grubstake_airdrop_instruction(authority: Pubkey, airdrop: Pubkey) -> Instruction {
984    ReclaimGrubstakeAirdrop {
985        authority,
986        satrush_config: get_satrush_config_address().0,
987        grubstake_airdrop: airdrop,
988        treasury: get_treasury_address().0,
989    }
990    .instruction()
991}
992
993/// `reclaim_miner_grubstake`: sweep `miner_authority`'s expired grubstake into
994/// the treasury's withdrawable protocol fees. Sized from the miner's grubstake
995/// counter, so anything on the ATA above it is deliberately left behind. Fee
996/// recipient only.
997pub fn get_reclaim_miner_grubstake_instruction(
998    authority: Pubkey,
999    miner_authority: Pubkey,
1000    usd_mint: Pubkey,
1001) -> Instruction {
1002    let treasury = get_treasury_address().0;
1003    let miner = get_miner_address(miner_authority).0;
1004    ReclaimMinerGrubstake {
1005        authority,
1006        satrush_config: get_satrush_config_address().0,
1007        miner_authority,
1008        miner,
1009        treasury,
1010        usd_mint,
1011        miner_usd_ata: get_associated_token_address(&miner, &usd_mint),
1012        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
1013        token_program: TOKEN_PROGRAM_ID,
1014    }
1015    .instruction()
1016}
1017
1018/// `reclaim_grubstake_automation`: sweep a grubstake automation's escrow back
1019/// to the treasury once its owner's spend window has closed, and close the
1020/// automation. Fee recipient only.
1021pub fn get_reclaim_grubstake_automation_instruction(
1022    authority: Pubkey,
1023    automation_owner: Pubkey,
1024    usd_mint: Pubkey,
1025) -> Instruction {
1026    let treasury = get_treasury_address().0;
1027    let automation = get_public_automation_address(automation_owner).0;
1028    ReclaimGrubstakeAutomation {
1029        authority,
1030        satrush_config: get_satrush_config_address().0,
1031        public_automation: automation,
1032        miner: get_miner_address(automation_owner).0,
1033        automation_owner,
1034        treasury,
1035        usd_mint,
1036        automation_usd_ata: get_associated_token_address(&automation, &usd_mint),
1037        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
1038        token_program: TOKEN_PROGRAM_ID,
1039    }
1040    .instruction()
1041}