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, ClaimUsd, ClaimUsdInstructionArgs, CloseRound, CreateBoard, CreateEpochVault,
10    CreateOneBtcVault, CreatePublicAutomation, CreatePublicAutomationInstructionArgs, CreateSatrushConfig,
11    CreateSatrushConfigInstructionArgs, CreateSatsVault, CreateTreasury, DeployPublic, DeployPublicInstructionArgs,
12    ExecutePublicAutomation, ExecutePublicAutomationInstructionArgs, RotateRound, SettleDeployPublic, SwapRoundStake,
13    SwapRoundStakeInstructionArgs, TopUpPublicAutomation, TopUpPublicAutomationInstructionArgs,
14    UpdateBoardRoundDuration, UpdateBoardRoundDurationInstructionArgs, UpdateDeploymentSettleGraceDuration,
15    UpdateDeploymentSettleGraceDurationInstructionArgs, UpdateEpochVaultIterationDuration,
16    UpdateEpochVaultIterationDurationInstructionArgs, UpdateMinDeployUsdAmount,
17    UpdateMinDeployUsdAmountInstructionArgs, UpdateUnclaimedHashrateBps, UpdateUnclaimedHashrateBpsInstructionArgs,
18};
19use crate::types::AutomationStrategy;
20use crate::{
21    get_board_address, get_epoch_vault_address, get_epoch_vault_iteration_address, get_event_authority_address,
22    get_miner_address, get_one_btc_vault_address, get_one_btc_vault_iteration_address, get_public_automation_address,
23    get_public_deployment_address, get_round_address, get_satrush_config_address, get_sats_vault_address,
24    get_treasury_address,
25};
26use solana_instruction::{AccountMeta, Instruction};
27use solana_pubkey::Pubkey;
28
29/// SPL Token program.
30pub const TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
31/// SPL Associated Token Account program.
32pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str_const("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
33/// System program.
34pub const SYSTEM_PROGRAM_ID: Pubkey = Pubkey::from_str_const("11111111111111111111111111111111");
35/// SlotHashes sysvar.
36pub const SLOT_HASHES_ID: Pubkey = Pubkey::from_str_const("SysvarS1otHashes111111111111111111111111111");
37
38/// Derive the canonical associated token account for `wallet` holding `mint`.
39pub fn get_associated_token_address(wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
40    Pubkey::find_program_address(
41        &[wallet.as_ref(), TOKEN_PROGRAM_ID.as_ref(), mint.as_ref()],
42        &ASSOCIATED_TOKEN_PROGRAM_ID,
43    )
44    .0
45}
46
47/// `create_satrush_config`: the config PDA holding authorities, mints and fee
48/// parameters. `authority` pays and must equal the program's upgrade authority.
49pub fn get_create_satrush_config_instruction(
50    authority: Pubkey,
51    args: CreateSatrushConfigInstructionArgs,
52) -> Instruction {
53    CreateSatrushConfig {
54        authority,
55        satrush_config: get_satrush_config_address().0,
56        system_program: SYSTEM_PROGRAM_ID,
57    }
58    .instruction(args)
59}
60
61/// `create_board`: the board singleton, its USD/BTC pools and the initial round
62/// (id 1). Signed by the config's admin authority.
63pub fn get_create_board_instruction(authority: Pubkey, usd_mint: Pubkey, btc_mint: Pubkey) -> Instruction {
64    let board = get_board_address().0;
65    CreateBoard {
66        authority,
67        satrush_config: get_satrush_config_address().0,
68        board,
69        initial_round: get_round_address(1).0,
70        usd_mint,
71        btc_mint,
72        board_usd_ata: get_associated_token_address(&board, &usd_mint),
73        board_btc_ata: get_associated_token_address(&board, &btc_mint),
74        token_program: TOKEN_PROGRAM_ID,
75        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
76        system_program: SYSTEM_PROGRAM_ID,
77    }
78    .instruction()
79}
80
81/// `update_board_round_duration`: overwrite the board's `round_duration` (slots).
82/// Applies to future round activations only; the active round keeps its window.
83/// Signed by the config's admin authority.
84pub fn get_update_board_round_duration_instruction(authority: Pubkey, new_round_duration: u32) -> Instruction {
85    UpdateBoardRoundDuration {
86        authority,
87        satrush_config: get_satrush_config_address().0,
88        board: get_board_address().0,
89    }
90    .instruction(UpdateBoardRoundDurationInstructionArgs { new_round_duration })
91}
92
93/// `update_min_deploy_usd_amount`: overwrite the config's minimum gross deploy
94/// size (USD mint base units). Applies to every subsequent manual deploy,
95/// automation creation and automation execution. Signed by the config's admin
96/// authority.
97pub fn get_update_min_deploy_usd_amount_instruction(authority: Pubkey, new_min_deploy_usd_amount: u64) -> Instruction {
98    UpdateMinDeployUsdAmount {
99        authority,
100        satrush_config: get_satrush_config_address().0,
101    }
102    .instruction(UpdateMinDeployUsdAmountInstructionArgs {
103        new_min_deploy_usd_amount,
104    })
105}
106
107/// `update_unclaimed_hashrate_bps`: overwrite the config's deferred hashrate
108/// bonus ratio (bps of each settled play's reward). Signed by the config's
109/// admin authority.
110pub fn get_update_unclaimed_hashrate_bps_instruction(
111    authority: Pubkey,
112    new_unclaimed_hashrate_bps: u32,
113) -> Instruction {
114    UpdateUnclaimedHashrateBps {
115        authority,
116        satrush_config: get_satrush_config_address().0,
117    }
118    .instruction(UpdateUnclaimedHashrateBpsInstructionArgs {
119        new_unclaimed_hashrate_bps,
120    })
121}
122
123/// `update_epoch_vault_iteration_duration`: overwrite the config's epoch vault
124/// iteration length (slots). Applies to the currently accumulating iteration
125/// immediately. Signed by the config's admin authority.
126pub fn get_update_epoch_vault_iteration_duration_instruction(
127    authority: Pubkey,
128    new_iteration_duration: u64,
129) -> Instruction {
130    UpdateEpochVaultIterationDuration {
131        authority,
132        satrush_config: get_satrush_config_address().0,
133    }
134    .instruction(UpdateEpochVaultIterationDurationInstructionArgs { new_iteration_duration })
135}
136
137/// `update_deployment_settle_grace_duration`: admin retunes the settle grace
138/// window (slots after a round settles before stale deployments may be
139/// force-cleaned; 0 disables cleanup).
140pub fn get_update_deployment_settle_grace_duration_instruction(
141    authority: Pubkey,
142    new_grace_duration: u64,
143) -> Instruction {
144    UpdateDeploymentSettleGraceDuration {
145        authority,
146        satrush_config: get_satrush_config_address().0,
147    }
148    .instruction(UpdateDeploymentSettleGraceDurationInstructionArgs { new_grace_duration })
149}
150
151/// `create_epoch_vault`: the epoch vault singleton, its USD/BTC pools and the
152/// first iteration (id 1).
153pub fn get_create_epoch_vault_instruction(authority: Pubkey, usd_mint: Pubkey, btc_mint: Pubkey) -> Instruction {
154    let epoch_vault = get_epoch_vault_address().0;
155    CreateEpochVault {
156        authority,
157        satrush_config: get_satrush_config_address().0,
158        epoch_vault,
159        epoch_vault_iteration: get_epoch_vault_iteration_address(1).0,
160        usd_mint,
161        btc_mint,
162        epoch_vault_usd_ata: get_associated_token_address(&epoch_vault, &usd_mint),
163        epoch_vault_btc_ata: get_associated_token_address(&epoch_vault, &btc_mint),
164        token_program: TOKEN_PROGRAM_ID,
165        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
166        system_program: SYSTEM_PROGRAM_ID,
167    }
168    .instruction()
169}
170
171/// `create_one_btc_vault`: the 1 BTC vault singleton, its USD/BTC pools and the
172/// first iteration (id 1).
173pub fn get_create_one_btc_vault_instruction(authority: Pubkey, usd_mint: Pubkey, btc_mint: Pubkey) -> Instruction {
174    let one_btc_vault = get_one_btc_vault_address().0;
175    CreateOneBtcVault {
176        authority,
177        satrush_config: get_satrush_config_address().0,
178        one_btc_vault,
179        one_btc_vault_iteration: get_one_btc_vault_iteration_address(1).0,
180        usd_mint,
181        btc_mint,
182        one_btc_vault_usd_ata: get_associated_token_address(&one_btc_vault, &usd_mint),
183        one_btc_vault_btc_ata: get_associated_token_address(&one_btc_vault, &btc_mint),
184        token_program: TOKEN_PROGRAM_ID,
185        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
186        system_program: SYSTEM_PROGRAM_ID,
187    }
188    .instruction()
189}
190
191/// `create_treasury`: the treasury singleton and its USD fee pool.
192pub fn get_create_treasury_instruction(authority: Pubkey, usd_mint: Pubkey) -> Instruction {
193    let treasury = get_treasury_address().0;
194    CreateTreasury {
195        authority,
196        satrush_config: get_satrush_config_address().0,
197        treasury,
198        usd_mint,
199        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
200        token_program: TOKEN_PROGRAM_ID,
201        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
202        system_program: SYSTEM_PROGRAM_ID,
203    }
204    .instruction()
205}
206
207/// `deploy_public`: stake `amount` USD (base units; all fees are deducted from
208/// it, so the miner parts with exactly `amount`) on the tiles
209/// in `selection_mask` for round `round_id`, as the miner `authority`. The round
210/// must be the board's current one and open for deploys; the miner profile and
211/// deployment record PDAs are created by the instruction.
212pub fn get_deploy_public_instruction(
213    authority: Pubkey,
214    usd_mint: Pubkey,
215    round_id: u32,
216    selection_mask: u32,
217    amount: u64,
218) -> Instruction {
219    let board = get_board_address().0;
220
221    DeployPublic {
222        authority,
223        satrush_config: get_satrush_config_address().0,
224        board,
225        usd_mint,
226        board_usd_ata: get_associated_token_address(&board, &usd_mint),
227        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
228        round: get_round_address(round_id).0,
229        public_deployment: get_public_deployment_address(authority, round_id).0,
230        miner: get_miner_address(authority).0,
231        token_program: TOKEN_PROGRAM_ID,
232        system_program: SYSTEM_PROGRAM_ID,
233    }
234    .instruction(DeployPublicInstructionArgs { selection_mask, amount })
235}
236
237/// `rotate_round`: reveal `current_round_id`'s winning tile, sweep the round's
238/// accumulated fee legs from the board pool to the epoch/1 BTC/treasury pools,
239/// and deploy round `current_round_id + 1` as the board's new active round.
240/// Signed by the config's round authority; valid once the round's window elapsed.
241pub fn get_rotate_round_instruction(authority: Pubkey, usd_mint: Pubkey, current_round_id: u32) -> Instruction {
242    let board = get_board_address().0;
243    let epoch_vault = get_epoch_vault_address().0;
244    let one_btc_vault = get_one_btc_vault_address().0;
245    let treasury = get_treasury_address().0;
246
247    RotateRound {
248        authority,
249        satrush_config: get_satrush_config_address().0,
250        board,
251        current_round: get_round_address(current_round_id).0,
252        next_round: get_round_address(current_round_id + 1).0,
253        usd_mint,
254        board_usd_ata: get_associated_token_address(&board, &usd_mint),
255        epoch_vault,
256        epoch_vault_usd_ata: get_associated_token_address(&epoch_vault, &usd_mint),
257        one_btc_vault,
258        one_btc_vault_usd_ata: get_associated_token_address(&one_btc_vault, &usd_mint),
259        treasury,
260        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
261        slot_hashes: SLOT_HASHES_ID,
262        token_program: TOKEN_PROGRAM_ID,
263        system_program: SYSTEM_PROGRAM_ID,
264        event_authority: get_event_authority_address().0,
265        program: crate::SATRUSH_ID,
266    }
267    .instruction()
268}
269
270/// `swap_round_stake`: relay a pre-built aggregator route that converts part of
271/// the revealed round's USD into BTC. `swap_data` and `route_accounts` are the
272/// route's opaque instruction data and account list; the on-chain handler
273/// enforces the economics against observed balance deltas. Signed by the
274/// config's round authority.
275#[allow(clippy::too_many_arguments)]
276pub fn get_swap_round_stake_instruction(
277    authority: Pubkey,
278    usd_mint: Pubkey,
279    btc_mint: Pubkey,
280    round_id: u32,
281    min_btc_out: u64,
282    swap_program: Pubkey,
283    swap_data: Vec<u8>,
284    route_accounts: &[AccountMeta],
285) -> Instruction {
286    let board = get_board_address().0;
287    SwapRoundStake {
288        authority,
289        satrush_config: get_satrush_config_address().0,
290        board,
291        round: get_round_address(round_id).0,
292        usd_mint,
293        btc_mint,
294        board_usd_ata: get_associated_token_address(&board, &usd_mint),
295        board_btc_ata: get_associated_token_address(&board, &btc_mint),
296        swap_program,
297        token_program: TOKEN_PROGRAM_ID,
298        event_authority: get_event_authority_address().0,
299        program: crate::SATRUSH_ID,
300    }
301    .instruction_with_remaining_accounts(SwapRoundStakeInstructionArgs { min_btc_out, swap_data }, route_accounts)
302}
303
304/// `settle_deploy_public`: settle `deployment_authority`'s deployment in a
305/// Settled round. `authority` signs (the owner, or the round authority for
306/// automated deployments); `rent_recipient` must be the round authority for
307/// automated deployments and the owner for manual ones.
308pub fn get_settle_deploy_public_instruction(
309    authority: Pubkey,
310    deployment_authority: Pubkey,
311    rent_recipient: Pubkey,
312    usd_mint: Pubkey,
313    btc_mint: Pubkey,
314    round_id: u32,
315) -> Instruction {
316    let board = get_board_address().0;
317    let sats_vault = get_sats_vault_address().0;
318    let public_automation = get_public_automation_address(deployment_authority).0;
319    SettleDeployPublic {
320        authority,
321        rent_recipient,
322        satrush_config: get_satrush_config_address().0,
323        round: get_round_address(round_id).0,
324        board,
325        public_deployment: get_public_deployment_address(deployment_authority, round_id).0,
326        miner: get_miner_address(deployment_authority).0,
327        public_automation,
328        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
329        board_usd_ata: get_associated_token_address(&board, &usd_mint),
330        sats_vault,
331        btc_mint,
332        usd_mint,
333        board_btc_ata: get_associated_token_address(&board, &btc_mint),
334        sats_vault_btc_ata: get_associated_token_address(&sats_vault, &btc_mint),
335        token_program: TOKEN_PROGRAM_ID,
336        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
337        system_program: SYSTEM_PROGRAM_ID,
338        event_authority: get_event_authority_address().0,
339        program: crate::SATRUSH_ID,
340    }
341    .instruction()
342}
343
344/// `create_public_automation`: escrow `deposit_usd_amount` and configure the
345/// crank to deploy `per_round_usd_amount` per round with the given strategy.
346/// One automation per authority.
347#[allow(clippy::too_many_arguments)]
348pub fn get_create_public_automation_instruction(
349    authority: Pubkey,
350    usd_mint: Pubkey,
351    strategy: AutomationStrategy,
352    selection_mask: u32,
353    per_round_usd_amount: u64,
354    reload: bool,
355    deposit_usd_amount: u64,
356) -> Instruction {
357    let public_automation = get_public_automation_address(authority).0;
358    CreatePublicAutomation {
359        authority,
360        satrush_config: get_satrush_config_address().0,
361        public_automation,
362        usd_mint,
363        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
364        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
365        miner: get_miner_address(authority).0,
366        token_program: TOKEN_PROGRAM_ID,
367        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
368        system_program: SYSTEM_PROGRAM_ID,
369    }
370    .instruction(CreatePublicAutomationInstructionArgs {
371        strategy,
372        selection_mask,
373        per_round_usd_amount,
374        reload,
375        deposit_usd_amount,
376    })
377}
378
379/// `top_up_public_automation`: move `amount` USD from the authority's account
380/// into the automation's escrow.
381pub fn get_top_up_public_automation_instruction(authority: Pubkey, usd_mint: Pubkey, amount: u64) -> Instruction {
382    let public_automation = get_public_automation_address(authority).0;
383    TopUpPublicAutomation {
384        authority,
385        satrush_config: get_satrush_config_address().0,
386        public_automation,
387        usd_mint,
388        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
389        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
390        token_program: TOKEN_PROGRAM_ID,
391    }
392    .instruction(TopUpPublicAutomationInstructionArgs { amount })
393}
394
395/// `cancel_public_automation`: refund the full escrow balance and close the
396/// automation and its token account. Unconditional; in-flight deployments
397/// settle to the miner profile later.
398pub fn get_cancel_public_automation_instruction(authority: Pubkey, usd_mint: Pubkey) -> Instruction {
399    let public_automation = get_public_automation_address(authority).0;
400    CancelPublicAutomation {
401        authority,
402        satrush_config: get_satrush_config_address().0,
403        public_automation,
404        usd_mint,
405        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
406        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
407        token_program: TOKEN_PROGRAM_ID,
408        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
409        system_program: SYSTEM_PROGRAM_ID,
410    }
411    .instruction()
412}
413
414/// `execute_public_automation`: crank-signed per-round deploy funded from
415/// `automation_authority`'s escrow. `selection_mask` must be `Some` for
416/// Discretionary automations and `None` otherwise.
417pub fn get_execute_public_automation_instruction(
418    crank_authority: Pubkey,
419    automation_authority: Pubkey,
420    usd_mint: Pubkey,
421    round_id: u32,
422    selection_mask: Option<u32>,
423) -> Instruction {
424    let board = get_board_address().0;
425    let public_automation = get_public_automation_address(automation_authority).0;
426
427    ExecutePublicAutomation {
428        authority: crank_authority,
429        satrush_config: get_satrush_config_address().0,
430        board,
431        round: get_round_address(round_id).0,
432        public_automation,
433        usd_mint,
434        automation_usd_ata: get_associated_token_address(&public_automation, &usd_mint),
435        board_usd_ata: get_associated_token_address(&board, &usd_mint),
436        public_deployment: get_public_deployment_address(automation_authority, round_id).0,
437        miner: get_miner_address(automation_authority).0,
438        slot_hashes: SLOT_HASHES_ID,
439        token_program: TOKEN_PROGRAM_ID,
440        system_program: SYSTEM_PROGRAM_ID,
441    }
442    .instruction(ExecutePublicAutomationInstructionArgs { selection_mask })
443}
444
445/// `claim_usd`: withdraw `amount` of the miner `authority`'s unclaimed USD
446/// winnings from the board's USD pool to the authority's USD account. No exit
447/// fee — the full amount transfers.
448pub fn get_claim_usd_instruction(authority: Pubkey, usd_mint: Pubkey, amount: u64) -> Instruction {
449    let board = get_board_address().0;
450    ClaimUsd {
451        authority,
452        satrush_config: get_satrush_config_address().0,
453        board,
454        miner: get_miner_address(authority).0,
455        usd_mint,
456        board_usd_ata: get_associated_token_address(&board, &usd_mint),
457        authority_usd_ata: get_associated_token_address(&authority, &usd_mint),
458        token_program: TOKEN_PROGRAM_ID,
459        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
460        system_program: SYSTEM_PROGRAM_ID,
461    }
462    .instruction(ClaimUsdInstructionArgs { amount })
463}
464
465/// `create_sats_vault`: the sats vault singleton and its BTC reserve pool.
466pub fn get_create_sats_vault_instruction(authority: Pubkey, btc_mint: Pubkey) -> Instruction {
467    let sats_vault = get_sats_vault_address().0;
468    CreateSatsVault {
469        authority,
470        satrush_config: get_satrush_config_address().0,
471        sats_vault,
472        btc_mint,
473        sats_vault_btc_ata: get_associated_token_address(&sats_vault, &btc_mint),
474        token_program: TOKEN_PROGRAM_ID,
475        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
476        system_program: SYSTEM_PROGRAM_ID,
477    }
478    .instruction()
479}
480
481/// `close_round`: tear down a Finished round, sweeping a no-winner round's
482/// orphaned pot (USD and BTC) into the treasury. Signed by the config's admin
483/// authority, which receives the round account's rent.
484pub fn get_close_round_instruction(
485    authority: Pubkey,
486    usd_mint: Pubkey,
487    btc_mint: Pubkey,
488    round_id: u32,
489) -> Instruction {
490    let board = get_board_address().0;
491    let treasury = get_treasury_address().0;
492    CloseRound {
493        authority,
494        satrush_config: get_satrush_config_address().0,
495        board,
496        round: get_round_address(round_id).0,
497        treasury,
498        usd_mint,
499        btc_mint,
500        board_usd_ata: get_associated_token_address(&board, &usd_mint),
501        board_btc_ata: get_associated_token_address(&board, &btc_mint),
502        treasury_usd_ata: get_associated_token_address(&treasury, &usd_mint),
503        treasury_btc_ata: get_associated_token_address(&treasury, &btc_mint),
504        token_program: TOKEN_PROGRAM_ID,
505        associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
506        system_program: SYSTEM_PROGRAM_ID,
507        event_authority: get_event_authority_address().0,
508        program: crate::SATRUSH_ID,
509    }
510    .instruction()
511}