Skip to main content

arcium_client/
transactions.rs

1//! Transaction builders for the Arcium programs.
2use crate::{
3    idl::arcium::{
4        accounts::{ArxNode, MXEAccount},
5        types::{
6            ArgumentList,
7            BN254G2BLSPublicKey,
8            CallbackInstruction,
9            CircuitSource,
10            MempoolSize,
11            NodeMetadata,
12            Output,
13            Parameter,
14        },
15        ID as ARCIUM_PROG_ID,
16    },
17    instruction::{
18        activate_cluster_ix,
19        bind_recovery_peer_migration_ix,
20        claim_computation_rent_ix,
21        claim_failure_append_ix,
22        claim_failure_finalize_ix,
23        claim_failure_init_ix,
24        claim_node_fees_ix,
25        close_aborted_key_recovery_ix,
26        close_successful_key_recovery_ix,
27        deactivate_arx_ix,
28        finalize_mxe_keys_ix,
29        increase_mempool_size_ix,
30        init_arx_node_acc_ix,
31        init_cluster_ix,
32        init_computation_definition_ix,
33        init_key_recovery_execution_part1_ix,
34        init_key_recovery_execution_part2_ix,
35        init_mxe_part1_ix,
36        init_mxe_part2_ix,
37        init_network_program_ix,
38        init_node_operator_acc_ix,
39        init_recovery_peer_acc_ix,
40        join_cluster_ix,
41        migrate_arx_node_ix,
42        migrate_cluster_ix,
43        migrate_mxe_account_ix,
44        migrate_recovery_peer_ix,
45        propose_fee_ix,
46        propose_join_cluster_ix,
47        queue_computation_ix,
48        queue_key_recovery_finalize_ix,
49        queue_key_recovery_init_ix,
50        reclaim_expired_computation_fee_ix,
51        reclaim_failure_rent_idempotent_ix,
52        register_account_in_lut_ix,
53        requeue_mxe_keygen_ix,
54        set_active_ix,
55        set_arx_node_metadata_ix,
56        set_cluster_td_info_ix,
57        set_migration_ix,
58        staking::{
59            add_primary_stake_to_legacy_acc_ix,
60            init_cluster_stake_state_ix,
61            init_mxe_recovery_stake_state_ix,
62        },
63        submit_aggregated_bls_pubkey_ix,
64        submit_key_recovery_share_ix,
65        vote_fee_ix,
66    },
67    pda::{arx_acc, mxe_acc},
68    utils::MAX_RECOVERY_PEERS,
69};
70use anchor_client::{Client, Cluster as SolanaCluster, Program, RequestBuilder, ThreadSafeSigner};
71use anchor_lang::{prelude::*, solana_program::instruction::Instruction as SolanaInstruction};
72use rand::RngCore;
73use solana_keypair::Keypair;
74use solana_rpc_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient;
75use solana_rpc_client_api::config::RpcSendTransactionConfig;
76use solana_signature::Signature;
77use solana_signer::Signer;
78use solana_transaction::Transaction;
79// Shadows `anchor_lang::prelude::Result` from the glob import above.
80use std::{ops::Deref, result::Result, sync::Arc, vec};
81
82// Maximum callback data size that fits in a single transaction (init + append + finalize).
83// Computed as: 1232 (max serialized tx size) - 645 (overhead from signatures, accounts, ix data)
84// See test_build_claim_failure_txs_single_tx_at_max_size for validation.
85pub(crate) const MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE: usize = 587;
86
87#[derive(::thiserror::Error, Debug, Clone, PartialEq, Eq)]
88pub enum ClaimFailureError {
89    #[error(
90        "Multi-transaction failure claims not yet supported: callback data size {size} exceeds single-transaction limit of {limit} bytes"
91    )]
92    CallbackDataTooLarge { size: usize, limit: usize },
93}
94
95// Local alias for operations that fail with `ClaimFailureError`.
96type ClaimResult<T> = Result<T, ClaimFailureError>;
97
98pub const DEFAULT_PRIM_STAKE_AMOUNT: u64 = 1000;
99pub const DEFAULT_FEE_BASIS_POINTS: u16 = 0;
100pub const DEFAULT_LOCKUP_EPOCHS: u64 = 10;
101pub const DEFAULT_URL: &str = "https://arcium.com";
102pub const DEFAULT_LOCATION: u8 = 0;
103pub const DEFAULT_CU_CLAIM: u64 = 1000;
104pub const DEFAULT_MAX_CLUSTERS: u32 = 1;
105pub const DEFAULT_MAX_SIZE: u32 = 1;
106pub const DEFAULT_CLUSTER_ENCRYPTION_PUBKEY: [u8; 32] = [0; 32];
107pub const DEFAULT_CU_PRICE: u64 = 1;
108pub const DEFAULT_COMPILED_CIRCUIT: [u8; 24] = [0; 24];
109pub const DEFAULT_AUTHORITY_PUBKEYS: Option<Vec<Pubkey>> = None;
110pub const DEFAULT_PARAMS: Vec<Parameter> = vec![];
111pub const DEFAULT_CALLBACK_DATA_OBJS: Vec<Pubkey> = vec![];
112pub const DEFAULT_OUTPUT_SCALAR_LEN: u8 = 0;
113pub const DEFAULT_DUMMY_CALLBACK_DISC: [u8; 8] = [0; 8];
114pub const DEFAULT_DUMMY_ENCRYPTION_PUBKEY: [u8; 32] = [0; 32];
115pub const DEFAULT_DUMMY_PROGRAM_ID: Pubkey =
116    pubkey!("Bzabqe5qowkb54kQ96frT6WY7KNLQiMhj4pGGmrZMHRa");
117
118pub async fn init_network_program(
119    arcium_program: &Program<Arc<Keypair>>,
120    signer: Arc<Keypair>,
121    config: RpcSendTransactionConfig,
122) -> Signature {
123    let current_time = current_unix_timestamp(arcium_program.internal_rpc()).await;
124    let ix = init_network_program_ix(current_time, &signer.pubkey());
125    let tx = arcium_program.request().instruction(ix);
126    send_tx(vec![signer], tx, config).await
127}
128
129#[allow(clippy::too_many_arguments)]
130pub async fn submit_key_recovery_share(
131    arcium_program: &Program<Arc<Keypair>>,
132    signer: Arc<Keypair>,
133    original_mxe_program: &Pubkey,
134    backup_mxe_program: &Pubkey,
135    peer_offset: u32,
136    peer_index: u32,
137    share: [[u8; 32]; 5],
138    config: RpcSendTransactionConfig,
139) -> Signature {
140    let ix = submit_key_recovery_share_ix(
141        &signer.pubkey(),
142        original_mxe_program,
143        backup_mxe_program,
144        peer_offset,
145        peer_index,
146        share,
147    );
148    let tx = arcium_program.request().instruction(ix);
149    send_tx(vec![signer], tx, config).await
150}
151
152// Location as [ISO 3166-1 alpha-2](https://www.iso.org/iso-3166-country-codes.html) country code
153
154pub async fn init_node_operator_acc(
155    arcium_program: &Program<Arc<Keypair>>,
156    signer: Arc<Keypair>,
157    url: String,
158    location: u8,
159
160    config: RpcSendTransactionConfig,
161) -> Signature {
162    let ix = init_node_operator_acc_ix(&signer.pubkey(), url, location);
163    let tx = arcium_program.request().instruction(ix);
164    send_tx(vec![signer], tx, config).await
165}
166
167#[allow(clippy::too_many_arguments)]
168pub async fn init_arx_node_acc(
169    arcium_program: &Program<Arc<Keypair>>,
170    operator_signer: Arc<Keypair>,
171    node_signer: Arc<Keypair>,
172    node_offset: u32,
173    callback_authority: Pubkey,
174    cu_claim: u64,
175    metadata: NodeMetadata,
176    bls_pubkey: BN254G2BLSPublicKey,
177    bls_pop_sig: [u8; 64],
178    x25519_pubkey: [u8; 32],
179    primary_stake_account: Pubkey,
180    staker: Arc<Keypair>,
181    config: RpcSendTransactionConfig,
182) -> Signature {
183    let ix = init_arx_node_acc_ix(
184        &operator_signer.pubkey(),
185        &node_signer.pubkey(),
186        node_offset,
187        &callback_authority,
188        cu_claim,
189        bls_pubkey,
190        bls_pop_sig,
191        metadata,
192        x25519_pubkey,
193        &primary_stake_account,
194        &staker.pubkey(),
195    );
196    let tx = arcium_program.request().instruction(ix);
197    send_tx(vec![operator_signer, node_signer, staker], tx, config).await
198}
199
200pub async fn set_arx_node_metadata(
201    arcium_program: &Program<Arc<Keypair>>,
202    signer: Arc<Keypair>,
203    node_offset: u32,
204    metadata: NodeMetadata,
205    config: RpcSendTransactionConfig,
206) -> Signature {
207    let ix = set_arx_node_metadata_ix(&signer.pubkey(), node_offset, metadata);
208    let tx = arcium_program.request().instruction(ix);
209    send_tx(vec![signer], tx, config).await
210}
211
212pub async fn deactivate_arx(
213    arcium_program: &Program<Arc<Keypair>>,
214    signer: Arc<Keypair>,
215    node_offset: u32,
216    cluster_offset: Option<u32>,
217    config: RpcSendTransactionConfig,
218) -> Signature {
219    let ix = deactivate_arx_ix(&signer.pubkey(), node_offset, cluster_offset);
220    let tx = arcium_program.request().instruction(ix);
221    send_tx(vec![signer], tx, config).await
222}
223
224/// Initializes a cluster and its staking-side `ClusterStakeState` in one transaction.
225///
226/// Requires the `arcium_staking` program to be deployed.
227#[allow(clippy::too_many_arguments)]
228pub async fn init_cluster(
229    arcium_program: &Program<Arc<Keypair>>,
230    payer: Arc<Keypair>,
231    cluster_authority: Pubkey,
232    cluster_offset: u32,
233    cluster_size: u16,
234    cu_price: u64,
235    mempool_size: MempoolSize,
236    td_info: Option<NodeMetadata>,
237    config: RpcSendTransactionConfig,
238) -> Signature {
239    let tx = init_cluster_instructions(
240        &payer.pubkey(),
241        cluster_authority,
242        cluster_offset,
243        cluster_size,
244        cu_price,
245        mempool_size,
246        td_info,
247    )
248    .into_iter()
249    .fold(arcium_program.request(), |tx, ix| tx.instruction(ix));
250    send_tx(vec![payer], tx, config).await
251}
252
253fn init_cluster_instructions(
254    payer: &Pubkey,
255    cluster_authority: Pubkey,
256    cluster_offset: u32,
257    cluster_size: u16,
258    cu_price: u64,
259    mempool_size: MempoolSize,
260    td_info: Option<NodeMetadata>,
261) -> [SolanaInstruction; 2] {
262    [
263        init_cluster_ix(
264            payer,
265            cluster_authority,
266            cluster_offset,
267            cluster_size,
268            cu_price,
269            mempool_size,
270            td_info,
271        ),
272        init_cluster_stake_state_ix(payer, cluster_offset),
273    ]
274}
275
276pub async fn init_recovery_peer_acc(
277    arcium_program: &Program<Arc<Keypair>>,
278    signer: Arc<Keypair>,
279    peer_offset: u32,
280    x25519_pubkey: [u8; 32],
281    primary_stake_account: &Pubkey,
282    staker: Arc<Keypair>,
283    config: RpcSendTransactionConfig,
284) -> Signature {
285    let ix = init_recovery_peer_acc_ix(
286        &signer.pubkey(),
287        peer_offset,
288        x25519_pubkey,
289        primary_stake_account,
290        &staker.pubkey(),
291    );
292    let tx = arcium_program.request().instruction(ix);
293    send_tx(vec![signer, staker], tx, config).await
294}
295
296#[allow(clippy::too_many_arguments)]
297pub async fn init_mxe(
298    arcium_program: &Program<Arc<Keypair>>,
299    payer: Arc<Keypair>,
300    mxe_program: &Pubkey,
301    mxe_authority: Option<Pubkey>,
302    keygen_comp_offset: u64,
303    key_recovery_init_comp_offset: u64,
304    cluster_offset: u32,
305    recovery_peers: [u32; MAX_RECOVERY_PEERS],
306    recent_slot: u64,
307    skip_init_mxe_part1: bool,
308    config: RpcSendTransactionConfig,
309) -> Signature {
310    // TODO: add check that recovery_peers contains at least MIN_RECOVERY_PEERS non-zero offsets
311    if !skip_init_mxe_part1 {
312        let ix_pt1 = init_mxe_part1_ix(&payer.pubkey(), mxe_program, recovery_peers);
313        let tx_1 = arcium_program.request().instruction(ix_pt1);
314        let tx_1 = send_tx(vec![payer.clone()], tx_1, config);
315        tx_1.await;
316    }
317    let ix_pt2 = init_mxe_part2_ix(
318        &payer.pubkey(),
319        mxe_program,
320        cluster_offset,
321        keygen_comp_offset,
322        key_recovery_init_comp_offset,
323        mxe_authority,
324        recent_slot,
325    );
326    let ix_pt3 = init_mxe_recovery_stake_state_ix(&payer.pubkey(), mxe_program);
327    let tx_2 = arcium_program
328        .request()
329        .instruction(ix_pt2)
330        .instruction(ix_pt3);
331    let tx_2 = send_tx(vec![payer], tx_2, config);
332    tx_2.await
333}
334
335pub async fn register_account_in_lut(
336    arcium_program: &Program<Arc<Keypair>>,
337    signer: Arc<Keypair>,
338    mxe_program: &Pubkey,
339    account: &Pubkey,
340    config: RpcSendTransactionConfig,
341) -> Signature {
342    let mxe_account: MXEAccount = arcium_program
343        .account::<MXEAccount>(mxe_acc(mxe_program))
344        .await
345        .expect("failed to fetch MXE account when adding account to MXE LUT");
346    let lut_slot = mxe_account.lut_offset_slot;
347    let ix = register_account_in_lut_ix(&signer.pubkey(), mxe_program, lut_slot, *account);
348    let tx = arcium_program.request().instruction(ix);
349    send_tx(vec![signer], tx, config).await
350}
351
352#[allow(clippy::too_many_arguments)]
353pub async fn init_ephemeral_mxe(
354    arcium_program: &Program<Arc<Keypair>>,
355    payer: Arc<Keypair>,
356    mxe_program: &Pubkey,
357    mxe_authority: Option<Pubkey>,
358    keygen_comp_offset: u64,
359    cluster_offset: u32,
360    recent_slot: u64,
361    config: RpcSendTransactionConfig,
362) -> Signature {
363    let recovery_peers = [0u32; MAX_RECOVERY_PEERS];
364    let key_recovery_init_comp_offset = rand::thread_rng().next_u64();
365    let ix_pt1 = init_mxe_part1_ix(&payer.pubkey(), mxe_program, recovery_peers);
366    let ix_pt2 = init_mxe_part2_ix(
367        &payer.pubkey(),
368        mxe_program,
369        cluster_offset,
370        keygen_comp_offset,
371        key_recovery_init_comp_offset,
372        mxe_authority,
373        recent_slot,
374    );
375    let ix_pt3 = init_mxe_recovery_stake_state_ix(&payer.pubkey(), mxe_program);
376    let tx_1 = arcium_program.request().instruction(ix_pt1);
377    let tx_1 = send_tx(vec![payer.clone()], tx_1, config);
378    tx_1.await;
379    let tx_2 = arcium_program
380        .request()
381        .instruction(ix_pt2)
382        .instruction(ix_pt3);
383    let tx_2 = send_tx(vec![payer], tx_2, config);
384    tx_2.await
385}
386
387pub async fn finalize_mxe_keys(
388    arcium_program: &Program<Arc<Keypair>>,
389    payer: Arc<Keypair>,
390    mxe_program: &Pubkey,
391    cluster_offset: u32,
392    config: RpcSendTransactionConfig,
393) -> Signature {
394    let ix = finalize_mxe_keys_ix(&payer.pubkey(), mxe_program, cluster_offset);
395    let tx = arcium_program.request().instruction(ix);
396    send_tx(vec![payer], tx, config).await
397}
398
399/// Migrate a `Cluster` account to the current (post-staking) layout (moving rewards into the new
400/// fields). Layout-only and idempotent — does NOT create the `ClusterStakeState`, so it needs no
401/// staking program on-chain. Use `migrate_cluster_full` once staking is deployed.
402pub async fn migrate_cluster_account(
403    arcium_program: &Program<Arc<Keypair>>,
404    payer: Arc<Keypair>,
405    cluster_offset: u32,
406    config: RpcSendTransactionConfig,
407) -> Signature {
408    let tx = arcium_program
409        .request()
410        .instruction(migrate_cluster_ix(&payer.pubkey(), cluster_offset));
411    send_tx(vec![payer], tx, config).await
412}
413
414/// Full `Cluster` migration: layout grow + create the cluster's `ClusterStakeState`, in one
415/// transaction. Requires the `arcium_staking` program deployed. Permissionless (`payer` signs).
416/// The layout step is idempotent, but the bundled `init_cluster_stake_state` is not — run once.
417pub async fn migrate_cluster_full(
418    arcium_program: &Program<Arc<Keypair>>,
419    payer: Arc<Keypair>,
420    cluster_offset: u32,
421    config: RpcSendTransactionConfig,
422) -> Signature {
423    let tx = arcium_program
424        .request()
425        .instruction(migrate_cluster_ix(&payer.pubkey(), cluster_offset))
426        .instruction(init_cluster_stake_state_ix(&payer.pubkey(), cluster_offset));
427    send_tx(vec![payer], tx, config).await
428}
429
430/// Migrate an `MXEAccount` to the current layout. Layout-only and idempotent — does NOT create the
431/// `MxeRecoveryStakeState`, so it needs no staking program on-chain. Use `migrate_mxe_account_full`
432/// once staking is deployed.
433pub async fn migrate_mxe_account(
434    arcium_program: &Program<Arc<Keypair>>,
435    payer: Arc<Keypair>,
436    mxe_program: &Pubkey,
437    config: RpcSendTransactionConfig,
438) -> Signature {
439    let tx = arcium_program
440        .request()
441        .instruction(migrate_mxe_account_ix(&payer.pubkey(), mxe_program));
442    send_tx(vec![payer], tx, config).await
443}
444
445/// Full `MXEAccount` migration: layout grow + create the `MxeRecoveryStakeState`, in one
446/// transaction — the stake-state init's gate *requires* it be preceded by `migrate_mxe_account` in
447/// the same tx. Requires the `arcium_staking` program deployed. Permissionless. Run once per MXE.
448pub async fn migrate_mxe_account_full(
449    arcium_program: &Program<Arc<Keypair>>,
450    payer: Arc<Keypair>,
451    mxe_program: &Pubkey,
452    config: RpcSendTransactionConfig,
453) -> Signature {
454    let tx = arcium_program
455        .request()
456        .instruction(migrate_mxe_account_ix(&payer.pubkey(), mxe_program))
457        .instruction(init_mxe_recovery_stake_state_ix(
458            &payer.pubkey(),
459            mxe_program,
460        ));
461    send_tx(vec![payer], tx, config).await
462}
463
464/// Migrate a (zero-copy) `RecoveryPeerAccount` to the current layout. Permissionless + idempotent.
465pub async fn migrate_recovery_peer(
466    arcium_program: &Program<Arc<Keypair>>,
467    payer: Arc<Keypair>,
468    peer_offset: u32,
469    config: RpcSendTransactionConfig,
470) -> Signature {
471    let ix = migrate_recovery_peer_ix(&payer.pubkey(), peer_offset);
472    let tx = arcium_program.request().instruction(ix);
473    send_tx(vec![payer], tx, config).await
474}
475
476/// Full `ArxNode` bind migration: create a real-stake `PrimaryStakingAccount` for `authority`
477/// (`amount >= MIN_SELF_DELEGATION`, pulled from `authority`'s ARCIUM ATA) and bind it to the node,
478/// in one transaction. `authority` must be the node's `config.authority` (it authorizes the bind
479/// and owns the stake PDA). Run once per node.
480pub async fn migrate_arx_node(
481    arcium_program: &Program<Arc<Keypair>>,
482    authority: Arc<Keypair>,
483    node_offset: u32,
484    amount: u64,
485    fee_basis_points: u16,
486    config: RpcSendTransactionConfig,
487) -> Signature {
488    let tx = arcium_program
489        .request()
490        .instruction(add_primary_stake_to_legacy_acc_ix(
491            &authority.pubkey(),
492            amount,
493            fee_basis_points,
494        ))
495        .instruction(migrate_arx_node_ix(&authority.pubkey(), node_offset));
496    send_tx(vec![authority], tx, config).await
497}
498
499/// Full `RecoveryPeerAccount` bind migration: grow the peer to the current layout, create a
500/// real-stake `PrimaryStakingAccount` for `authority` (`amount >= MIN_SELF_DELEGATION`), and bind
501/// it — in one transaction. `authority` must be the peer's `authority`. Run once per recovery peer.
502pub async fn migrate_recovery_peer_bind(
503    arcium_program: &Program<Arc<Keypair>>,
504    authority: Arc<Keypair>,
505    peer_offset: u32,
506    amount: u64,
507    fee_basis_points: u16,
508    config: RpcSendTransactionConfig,
509) -> Signature {
510    let tx = arcium_program
511        .request()
512        .instruction(migrate_recovery_peer_ix(&authority.pubkey(), peer_offset))
513        .instruction(add_primary_stake_to_legacy_acc_ix(
514            &authority.pubkey(),
515            amount,
516            fee_basis_points,
517        ))
518        .instruction(bind_recovery_peer_migration_ix(
519            &authority.pubkey(),
520            peer_offset,
521        ));
522    send_tx(vec![authority], tx, config).await
523}
524
525pub async fn set_migration(
526    arcium_program: &Program<Arc<Keypair>>,
527    signer: Arc<Keypair>,
528    mxe_program: &Pubkey,
529    config: RpcSendTransactionConfig,
530) -> Signature {
531    let ix = set_migration_ix(&signer.pubkey(), mxe_program);
532    let tx = arcium_program.request().instruction(ix);
533    send_tx(vec![signer.clone()], tx, config).await
534}
535
536pub async fn set_active(
537    arcium_program: &Program<Arc<Keypair>>,
538    signer: Arc<Keypair>,
539    mxe_program: &Pubkey,
540    config: RpcSendTransactionConfig,
541) -> Signature {
542    let ix = set_active_ix(&signer.pubkey(), mxe_program);
543    let tx = arcium_program.request().instruction(ix);
544    send_tx(vec![signer.clone()], tx, config).await
545}
546
547#[allow(clippy::too_many_arguments)]
548pub async fn init_key_recovery_execution(
549    arcium_program: &Program<Arc<Keypair>>,
550    payer: Arc<Keypair>,
551    original_mxe_program: &Pubkey,
552    backup_mxe_program: &Pubkey,
553    cluster_offset: u32,
554    key_recovery_finalize_offset: u64,
555    config: RpcSendTransactionConfig,
556) -> Signature {
557    let ix_pt1 = init_key_recovery_execution_part1_ix(
558        &payer.pubkey(),
559        &payer.pubkey(),
560        original_mxe_program,
561        backup_mxe_program,
562    );
563    let ix_pt2 = init_key_recovery_execution_part2_ix(
564        &payer.pubkey(),
565        original_mxe_program,
566        backup_mxe_program,
567        cluster_offset,
568        key_recovery_finalize_offset,
569    );
570    let tx = arcium_program
571        .request()
572        .instruction(ix_pt1)
573        .instruction(ix_pt2);
574    send_tx(vec![payer], tx, config).await
575}
576
577#[allow(clippy::too_many_arguments)]
578pub async fn queue_key_recovery_finalize(
579    arcium_program: &Program<Arc<Keypair>>,
580    signer: Arc<Keypair>,
581    original_mxe_program: &Pubkey,
582    backup_mxe_program: &Pubkey,
583    backup_cluster_offset: u32,
584    key_recovery_finalize_offset: u64,
585    lut_slot: u64,
586    config: RpcSendTransactionConfig,
587) -> Signature {
588    let ix = queue_key_recovery_finalize_ix(
589        &signer.pubkey(),
590        &signer.pubkey(),
591        original_mxe_program,
592        backup_mxe_program,
593        backup_cluster_offset,
594        key_recovery_finalize_offset,
595        lut_slot,
596    );
597    let tx = arcium_program.request().instruction(ix);
598    send_tx(vec![signer], tx, config).await
599}
600
601pub async fn close_successful_key_recovery(
602    arcium_program: &Program<Arc<Keypair>>,
603    signer: Arc<Keypair>,
604    original_mxe_program: &Pubkey,
605    backup_mxe_program: &Pubkey,
606    backup_cluster_offset: u32,
607    key_recovery_finalize_offset: u64,
608    config: RpcSendTransactionConfig,
609) -> Signature {
610    try_close_successful_key_recovery(
611        arcium_program,
612        signer,
613        original_mxe_program,
614        backup_mxe_program,
615        backup_cluster_offset,
616        key_recovery_finalize_offset,
617        config,
618    )
619    .await
620    .unwrap()
621}
622
623/// Closes recovery accounts after a successful key recovery computation.
624///
625/// # Errors
626///
627/// Returns the client error if the transaction cannot be sent or confirmed.
628pub async fn try_close_successful_key_recovery(
629    arcium_program: &Program<Arc<Keypair>>,
630    signer: Arc<Keypair>,
631    original_mxe_program: &Pubkey,
632    backup_mxe_program: &Pubkey,
633    backup_cluster_offset: u32,
634    key_recovery_finalize_offset: u64,
635    config: RpcSendTransactionConfig,
636) -> Result<Signature, anchor_client::ClientError> {
637    let ix = close_successful_key_recovery_ix(
638        &signer.pubkey(),
639        original_mxe_program,
640        backup_mxe_program,
641        backup_cluster_offset,
642        key_recovery_finalize_offset,
643    );
644    let tx = arcium_program.request().instruction(ix);
645    try_send_tx(vec![signer], tx, config).await
646}
647
648pub async fn close_aborted_key_recovery(
649    arcium_program: &Program<Arc<Keypair>>,
650    signer: Arc<Keypair>,
651    original_mxe_program: &Pubkey,
652    backup_mxe_program: &Pubkey,
653    backup_cluster_offset: u32,
654    key_recovery_finalize_offset: u64,
655    config: RpcSendTransactionConfig,
656) -> Signature {
657    let ix = close_aborted_key_recovery_ix(
658        &signer.pubkey(),
659        original_mxe_program,
660        backup_mxe_program,
661        backup_cluster_offset,
662        key_recovery_finalize_offset,
663    );
664    let tx = arcium_program.request().instruction(ix);
665    send_tx(vec![signer], tx, config).await
666}
667
668pub async fn requeue_mxe_keygen(
669    arcium_program: &Program<Arc<Keypair>>,
670    payer: Arc<Keypair>,
671    mxe_program: &Pubkey,
672    keygen_offset: u64,
673    cluster_offset: u32,
674    config: RpcSendTransactionConfig,
675) -> Signature {
676    let ix = requeue_mxe_keygen_ix(&payer.pubkey(), mxe_program, cluster_offset, keygen_offset);
677    let tx = arcium_program.request().instruction(ix);
678    send_tx(vec![payer], tx, config).await
679}
680
681pub async fn queue_key_recovery_init(
682    arcium_program: &Program<Arc<Keypair>>,
683    payer: Arc<Keypair>,
684    mxe_program: &Pubkey,
685    cluster_offset: u32,
686    key_recovery_init_offset: u64,
687    config: RpcSendTransactionConfig,
688) -> Signature {
689    let ix = queue_key_recovery_init_ix(
690        &payer.pubkey(),
691        mxe_program,
692        cluster_offset,
693        key_recovery_init_offset,
694    );
695    let tx = arcium_program.request().instruction(ix);
696    send_tx(vec![payer], tx, config).await
697}
698
699pub async fn claim_computation_rent(
700    arcium_program: &Program<Arc<Keypair>>,
701    signer: Arc<Keypair>,
702    computation_offset: u64,
703    cluster_offset: u32,
704    config: RpcSendTransactionConfig,
705) -> Signature {
706    let ix = claim_computation_rent_ix(&signer.pubkey(), computation_offset, cluster_offset);
707    let tx = arcium_program.request().instruction(ix);
708    send_tx(vec![signer], tx, config).await
709}
710
711pub async fn reclaim_expired_computation_fee(
712    arcium_program: &Program<Arc<Keypair>>,
713    signer: Arc<Keypair>,
714    mxe_program: &Pubkey,
715    computation_offset: u64,
716    cluster_offset: u32,
717    config: RpcSendTransactionConfig,
718) -> Signature {
719    let ix = reclaim_expired_computation_fee_ix(
720        &signer.pubkey(),
721        mxe_program,
722        cluster_offset,
723        computation_offset,
724    );
725    let tx = arcium_program.request().instruction(ix);
726    send_tx(vec![signer], tx, config).await
727}
728
729#[allow(clippy::too_many_arguments)]
730pub async fn init_computation_definition(
731    arcium_program: &Program<Arc<Keypair>>,
732    payer: Arc<Keypair>,
733    computation_def_offset: u32,
734    mxe_program: &Pubkey,
735    circuit_len: u32,
736    params: Vec<Parameter>,
737    outputs: Vec<Output>,
738    circuit_source_override: Option<CircuitSource>,
739    cu_amount: u64,
740    lut_slot: u64,
741    config: RpcSendTransactionConfig,
742) -> Signature {
743    let ix = init_computation_definition_ix(
744        &payer.pubkey(),
745        computation_def_offset,
746        mxe_program,
747        circuit_len,
748        params,
749        outputs,
750        cu_amount,
751        circuit_source_override,
752        lut_slot,
753    );
754    let tx = arcium_program.request().instruction(ix);
755    send_tx(vec![payer], tx, config).await
756}
757
758pub async fn increase_mempool_size(
759    arcium_program: &Program<Arc<Keypair>>,
760    signer: Arc<Keypair>,
761    cluster_offset: u32,
762    config: RpcSendTransactionConfig,
763) -> Signature {
764    let ix = increase_mempool_size_ix(&signer.pubkey(), cluster_offset);
765    let tx = arcium_program.request().instruction(ix);
766    send_tx(vec![signer], tx, config).await
767}
768
769pub async fn activate_cluster(
770    arcium_program: &Program<Arc<Keypair>>,
771    signer: Arc<Keypair>,
772    cluster_offset: u32,
773    config: RpcSendTransactionConfig,
774) -> Signature {
775    let ix = activate_cluster_ix(&signer.pubkey(), cluster_offset);
776    let tx = arcium_program.request().instruction(ix);
777    send_tx(vec![signer], tx, config).await
778}
779
780pub async fn set_cluster_td_info(
781    arcium_program: &Program<Arc<Keypair>>,
782    signer: Arc<Keypair>,
783    cluster_offset: u32,
784    td_info: Option<NodeMetadata>,
785    config: RpcSendTransactionConfig,
786) -> Signature {
787    let ix = set_cluster_td_info_ix(&signer.pubkey(), cluster_offset, td_info);
788    let tx = arcium_program.request().instruction(ix);
789    send_tx(vec![signer], tx, config).await
790}
791
792pub async fn propose_join_cluster(
793    arcium_program: &Program<Arc<Keypair>>,
794    cluster_auth_signer: Arc<Keypair>,
795    cluster_offset: u32,
796    node_offset: u32,
797
798    config: RpcSendTransactionConfig,
799) -> Signature {
800    // The invite is gated on the node's primary stake meeting the cluster's per-mempool-size
801    // minimum, so the instruction needs the node's bound `PrimaryStakingAccount`. Read it off the
802    // `ArxNode` rather than asking the caller to supply it.
803    let node = arcium_program
804        .account::<ArxNode>(arx_acc(node_offset))
805        .await
806        .expect("failed to fetch ArxNode for propose_join_cluster");
807    let ix = propose_join_cluster_ix(
808        &cluster_auth_signer.pubkey(),
809        cluster_offset,
810        node_offset,
811        &node.primary_staking_account,
812    );
813    let tx = arcium_program.request().instruction(ix);
814    send_tx(vec![cluster_auth_signer], tx, config).await
815}
816
817pub async fn join_cluster(
818    arcium_program: &Program<Arc<Keypair>>,
819    node_auth_signer: Arc<Keypair>,
820    cluster_offset: u32,
821    node_offset: u32,
822    join: bool,
823    config: RpcSendTransactionConfig,
824) -> Signature {
825    let ix = join_cluster_ix(
826        &node_auth_signer.pubkey(),
827        cluster_offset,
828        node_offset,
829        join,
830    );
831    let tx = arcium_program.request().instruction(ix);
832    send_tx(vec![node_auth_signer], tx, config).await
833}
834
835#[allow(clippy::too_many_arguments)]
836pub async fn queue_computation(
837    arcium_program: &Program<Arc<Keypair>>,
838    payer: Arc<Keypair>,
839    mxe_program: &Pubkey,
840    computation_offset: u64,
841    comp_def_offset: u32,
842    args: ArgumentList,
843    cu_price_micro: u64,
844    callback_cu_limit: u32,
845    callback_instructions: Vec<CallbackInstruction>,
846    callback_transactions_required: u8,
847    config: RpcSendTransactionConfig,
848) -> Result<Signature, anchor_lang::error::Error> {
849    let mxe_raw_data = arcium_program
850        .internal_rpc()
851        .get_account_data(&mxe_acc(mxe_program))
852        .await
853        .expect("Failed to fetch MXE");
854    let mxe_data = MXEAccount::try_deserialize(&mut mxe_raw_data.as_slice())
855        .expect("Failed to deserialize MXE");
856
857    let ix = queue_computation_ix(
858        &payer.pubkey(),
859        mxe_program,
860        computation_offset,
861        comp_def_offset,
862        args,
863        cu_price_micro,
864        callback_cu_limit,
865        mxe_data,
866        callback_instructions,
867        callback_transactions_required,
868    )?;
869    let tx = arcium_program.request().instruction(ix);
870    Ok(send_tx(vec![payer], tx, config).await)
871}
872
873pub async fn propose_fee(
874    arcium_program: &Program<Arc<Keypair>>,
875    node_auth_signer: Arc<Keypair>,
876    cluster_offset: u32,
877    node_offset: u32,
878    proposed_fee: u64,
879    config: RpcSendTransactionConfig,
880) -> Signature {
881    let ix = propose_fee_ix(
882        node_auth_signer.pubkey(),
883        cluster_offset,
884        node_offset,
885        proposed_fee,
886    );
887    let tx = arcium_program.request().instruction(ix);
888    send_tx(vec![node_auth_signer], tx, config).await
889}
890
891pub async fn vote_fee(
892    arcium_program: &Program<Arc<Keypair>>,
893    node_auth_signer: Arc<Keypair>,
894    cluster_offset: u32,
895    node_offset: u32,
896    proposed_fee: u64,
897    config: RpcSendTransactionConfig,
898) -> Signature {
899    let ix = vote_fee_ix(
900        node_auth_signer.pubkey(),
901        cluster_offset,
902        node_offset,
903        proposed_fee,
904    );
905    let tx = arcium_program.request().instruction(ix);
906    send_tx(vec![node_auth_signer], tx, config).await
907}
908
909/// Low-level wrapper for arcium's `claim_node_fees` — CPI-only from the staking program.
910/// Keeping this helper around means a caller can drive the instruction in e2e tests that CPI
911/// from arcium_staking. A standalone RPC call from a wallet is **not** expected to succeed
912/// (the staking pool PDA can't sign for itself outside a CPI).
913pub async fn claim_node_fees(
914    arcium_program: &Program<Arc<Keypair>>,
915    staking_pool_signer: Arc<Keypair>,
916    primary_stake_account: Pubkey,
917    cluster_offset: u32,
918    node_offset: u32,
919    config: RpcSendTransactionConfig,
920) -> Signature {
921    let ix = claim_node_fees_ix(
922        &staking_pool_signer.pubkey(),
923        &primary_stake_account,
924        cluster_offset,
925        node_offset,
926    );
927    let tx = arcium_program.request().instruction(ix);
928    send_tx(vec![staking_pool_signer], tx, config).await
929}
930
931pub async fn submit_aggregated_bls_pubkey(
932    arcium_program: &Program<Arc<Keypair>>,
933    node_authority: Arc<Keypair>,
934    cluster_offset: u32,
935    node_offset: u32,
936    aggregated_bls_pubkey: BN254G2BLSPublicKey,
937    config: RpcSendTransactionConfig,
938) -> Signature {
939    let ix = submit_aggregated_bls_pubkey_ix(
940        &node_authority.pubkey(),
941        cluster_offset,
942        node_offset,
943        aggregated_bls_pubkey,
944    );
945    let tx = arcium_program.request().instruction(ix);
946    send_tx(vec![node_authority], tx, config).await
947}
948
949#[allow(clippy::too_many_arguments)]
950pub async fn claim_failure(
951    arcium_program: &Program<Arc<Keypair>>,
952    signer: Arc<Keypair>,
953    mxe_program: &Pubkey,
954    cluster_offset: u32,
955    comp_def_offset: u32,
956    computation_offset: u64,
957    node_offset: u32,
958    callback_data: Vec<u8>,
959    failure_claim_offset: u32,
960    config: RpcSendTransactionConfig,
961) -> ClaimResult<Vec<Signature>> {
962    let mut txs = build_claim_failure_txs(
963        arcium_program,
964        &signer.pubkey(),
965        mxe_program,
966        cluster_offset,
967        comp_def_offset,
968        computation_offset,
969        node_offset,
970        callback_data,
971        failure_claim_offset,
972    )?
973    .into_iter();
974    let txs_len = txs.len();
975    let mut signatures = Vec::with_capacity(txs_len);
976    if let Some(first) = txs.next() {
977        signatures.push(send_tx(vec![signer.clone()], first, config).await);
978    }
979    // Take all but the last one (the finalize)
980    for tx in txs.by_ref().take(txs_len.saturating_sub(2)) {
981        signatures.push(send_tx(vec![signer.clone()], tx, config).await);
982    }
983    if let Some(finalize_tx) = txs.next() {
984        signatures.push(send_tx(vec![signer], finalize_tx, config).await);
985    }
986    Ok(signatures)
987}
988
989/// Closes a finalized failure claim account and returns its rent to `recipient`.
990///
991/// The on-chain instruction is idempotent: it succeeds (as a no-op) if the account is already
992/// empty, and only closes the account once the challenge period has elapsed. `recipient` must
993/// match the `poster` stored in the failure claim account. Returns the transaction signature,
994/// or the client error so callers iterating over many accounts can continue past failures.
995pub async fn reclaim_failure_rent_idempotent(
996    arcium_program: &Program<Arc<Keypair>>,
997    signer: Arc<Keypair>,
998    recipient: &Pubkey,
999    mxe_program: &Pubkey,
1000    computation_offset: u64,
1001    config: RpcSendTransactionConfig,
1002) -> Result<Signature, anchor_client::ClientError> {
1003    let ix = reclaim_failure_rent_idempotent_ix(
1004        &signer.pubkey(),
1005        recipient,
1006        mxe_program,
1007        computation_offset,
1008    );
1009    arcium_program
1010        .request()
1011        .instruction(ix)
1012        .signer(signer)
1013        .send_with_spinner_and_config(config)
1014        .await
1015}
1016
1017/// Closes several finalized failure claim accounts in a single transaction. The fee is charged per
1018/// signature (one here), not per instruction, so packing many reclaims amortizes it. `claims` is a
1019/// slice of `(mxe_program, computation_offset)`; `recipient` must match every account's `poster`.
1020///
1021/// Note: a packed transaction is all-or-nothing — if any instruction fails (e.g. an account that
1022/// isn't actually reclaimable), the whole transaction reverts. Callers should keep `claims` to
1023/// accounts they've already confirmed are reclaimable, and fall back to single-account reclaims on
1024/// failure if they want to isolate a bad entry.
1025pub async fn reclaim_failure_rent_idempotent_batch(
1026    arcium_program: &Program<Arc<Keypair>>,
1027    signer: Arc<Keypair>,
1028    recipient: &Pubkey,
1029    claims: &[(Pubkey, u64)],
1030    config: RpcSendTransactionConfig,
1031) -> Result<Signature, anchor_client::ClientError> {
1032    let mut request = arcium_program.request();
1033    for (mxe_program, computation_offset) in claims {
1034        request = request.instruction(reclaim_failure_rent_idempotent_ix(
1035            &signer.pubkey(),
1036            recipient,
1037            mxe_program,
1038            *computation_offset,
1039        ));
1040    }
1041    request
1042        .signer(signer)
1043        .send_with_spinner_and_config(config)
1044        .await
1045}
1046
1047/// Builds, signs, and submits a packed reclaim transaction WITHOUT waiting for confirmation
1048/// (`skip_preflight`, fire-and-forget). Returns as soon as the RPC accepts the transaction, so
1049/// callers can submit many concurrently. `recent_blockhash` is supplied by the caller so a single
1050/// blockhash can be reused across a burst of submissions. Because delivery isn't confirmed, callers
1051/// should treat this as best-effort and re-run to mop up anything that didn't land — safe since the
1052/// instruction is idempotent.
1053pub async fn submit_reclaim_failure_rent_batch(
1054    rpc: &AsyncRpcClient,
1055    signer: &Keypair,
1056    recipient: &Pubkey,
1057    claims: &[(Pubkey, u64)],
1058    recent_blockhash: solana_program::hash::Hash,
1059) -> Result<Signature, solana_rpc_client_api::client_error::Error> {
1060    let ixs: Vec<_> = claims
1061        .iter()
1062        .map(|(mxe_program, computation_offset)| {
1063            reclaim_failure_rent_idempotent_ix(
1064                &signer.pubkey(),
1065                recipient,
1066                mxe_program,
1067                *computation_offset,
1068            )
1069        })
1070        .collect();
1071    let tx = Transaction::new_signed_with_payer(
1072        &ixs,
1073        Some(&signer.pubkey()),
1074        &[signer],
1075        recent_blockhash,
1076    );
1077    rpc.send_transaction_with_config(
1078        &tx,
1079        RpcSendTransactionConfig {
1080            skip_preflight: true,
1081            ..Default::default()
1082        },
1083    )
1084    .await
1085}
1086
1087#[allow(clippy::too_many_arguments, clippy::type_complexity)]
1088fn build_claim_failure_txs<'a>(
1089    arcium_program: &'a Program<Arc<Keypair>>,
1090    signer_pubkey: &Pubkey,
1091    mxe_program_id: &Pubkey,
1092    cluster_offset: u32,
1093    comp_def_offset: u32,
1094    computation_offset: u64,
1095    node_offset: u32,
1096    callback_data: Vec<u8>,
1097    failure_claim_offset: u32,
1098) -> ClaimResult<Vec<RequestBuilder<'a, Arc<Keypair>, Arc<dyn ThreadSafeSigner>>>> {
1099    if callback_data.len() <= MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE {
1100        let init_ix = claim_failure_init_ix(
1101            signer_pubkey,
1102            mxe_program_id,
1103            cluster_offset,
1104            comp_def_offset,
1105            computation_offset,
1106            node_offset,
1107            callback_data.len() as u32,
1108        );
1109        let append_ix = claim_failure_append_ix(
1110            signer_pubkey,
1111            mxe_program_id,
1112            computation_offset,
1113            callback_data,
1114            failure_claim_offset,
1115        );
1116        let finalize_ix = claim_failure_finalize_ix(
1117            signer_pubkey,
1118            mxe_program_id,
1119            cluster_offset,
1120            computation_offset,
1121            node_offset,
1122        );
1123        let tx = arcium_program
1124            .request()
1125            .instruction(init_ix)
1126            .instruction(append_ix)
1127            .instruction(finalize_ix);
1128        return Ok(vec![tx]);
1129    }
1130    Err(ClaimFailureError::CallbackDataTooLarge {
1131        size: callback_data.len(),
1132        limit: MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE,
1133    })
1134}
1135
1136pub async fn deactivate_computation_definition(
1137    arcium_program: &Program<Arc<Keypair>>,
1138    payer: Arc<Keypair>,
1139    comp_offset: u32,
1140    mxe_program: &Pubkey,
1141    config: RpcSendTransactionConfig,
1142) -> Signature {
1143    let ix = crate::instruction::deactivate_computation_definition_ix(
1144        &payer.pubkey(),
1145        comp_offset,
1146        mxe_program,
1147    );
1148    let tx = arcium_program.request().instruction(ix);
1149    send_tx(vec![payer], tx, config).await
1150}
1151
1152pub async fn close_computation_definition(
1153    arcium_program: &Program<Arc<Keypair>>,
1154    payer: Arc<Keypair>,
1155    comp_offset: u32,
1156    mxe_program: &Pubkey,
1157    cluster_offset: u32,
1158    config: RpcSendTransactionConfig,
1159) -> Signature {
1160    let ix = crate::instruction::close_computation_definition_ix(
1161        &payer.pubkey(),
1162        comp_offset,
1163        mxe_program,
1164        cluster_offset,
1165    );
1166    let tx = arcium_program.request().instruction(ix);
1167    send_tx(vec![payer], tx, config).await
1168}
1169
1170pub async fn close_computation_definition_buffers(
1171    arcium_program: &Program<Arc<Keypair>>,
1172    payer: Arc<Keypair>,
1173    comp_offset: u32,
1174    mxe_program: &Pubkey,
1175    raw_circuit_index: u8,
1176    config: RpcSendTransactionConfig,
1177) -> Signature {
1178    let ix = crate::instruction::close_computation_definition_buffers_ix(
1179        &payer.pubkey(),
1180        comp_offset,
1181        mxe_program,
1182        raw_circuit_index,
1183    );
1184    let tx = arcium_program.request().instruction(ix);
1185    send_tx(vec![payer], tx, config).await
1186}
1187
1188#[allow(clippy::too_many_arguments)]
1189pub async fn close_mxe(
1190    arcium_program: &Program<Arc<Keypair>>,
1191    payer: Arc<Keypair>,
1192    mxe_program: &Pubkey,
1193    cluster_offset: u32,
1194    keygen_offset: u64,
1195    key_recovery_init_offset: u64,
1196    kr_init_comp_def_exists: bool,
1197    config: RpcSendTransactionConfig,
1198) -> Signature {
1199    let ix = crate::instruction::close_mxe_ix(
1200        &payer.pubkey(),
1201        mxe_program,
1202        cluster_offset,
1203        keygen_offset,
1204        key_recovery_init_offset,
1205        kr_init_comp_def_exists,
1206    );
1207    let tx = arcium_program.request().instruction(ix);
1208    send_tx(vec![payer], tx, config).await
1209}
1210
1211// Builds a program client for the Arcium program
1212pub fn arcium_program_client(
1213    cluster: SolanaCluster,
1214    signer: Arc<Keypair>,
1215    rpc: AsyncRpcClient,
1216) -> Program<Arc<Keypair>> {
1217    let client = Client::new(cluster, signer);
1218    client
1219        .program(ARCIUM_PROG_ID, rpc)
1220        .unwrap_or_else(|err| panic!("Failed to create program: {}", err))
1221}
1222
1223async fn send_tx<C: Deref<Target = impl Signer> + Clone>(
1224    signers: Vec<Arc<Keypair>>,
1225    ix: RequestBuilder<'_, C, Arc<dyn ThreadSafeSigner>>,
1226    config: RpcSendTransactionConfig,
1227) -> Signature {
1228    try_send_tx(signers, ix, config)
1229        .await
1230        // We do direct unwrap. Wrapping it in panic! would result in not the full error message.
1231        .unwrap()
1232}
1233
1234async fn try_send_tx<C: Deref<Target = impl Signer> + Clone>(
1235    signers: Vec<Arc<Keypair>>,
1236    ix: RequestBuilder<'_, C, Arc<dyn ThreadSafeSigner>>,
1237    config: RpcSendTransactionConfig,
1238) -> Result<Signature, anchor_client::ClientError> {
1239    let signed = signers.into_iter().fold(ix, |ix, signer| ix.signer(signer));
1240    signed.send_with_spinner_and_config(config).await
1241}
1242
1243/// Get the current unix timestamp from the cluster
1244async fn current_unix_timestamp(client: &AsyncRpcClient) -> u64 {
1245    let latest_slot = client
1246        .get_slot()
1247        .await
1248        .unwrap_or_else(|err| panic!("Failed to fetch slot: {}", err));
1249    client
1250        .get_block_time(latest_slot)
1251        .await
1252        .unwrap_or_else(|err| panic!("Failed to fetch block time: {}", err))
1253        .try_into()
1254        .unwrap_or_else(|err| panic!("Failed to convert block time to u64: {}", err))
1255}
1256
1257pub mod staking {
1258    use super::*;
1259    use crate::{
1260        idl::arcium_staking::types::RewardClaim,
1261        instruction::staking::{
1262            activate_primary_stake_acc_ix,
1263            claim_primary_stake_rewards_ix,
1264            close_delegated_stake_ix,
1265            deactivate_primary_stake_acc_ix,
1266            delegate_stake_ix,
1267            finalize_epoch_rewards_ix,
1268            init_delegated_stake_acc_ix,
1269            init_delegated_stake_master_acc_ix,
1270            init_primary_stake_acc_ix,
1271            merge_delegated_stake_account_ix,
1272            modify_primary_stake_ix,
1273            split_delegated_stake_account_ix,
1274            undelegate_stake_ix,
1275            REWARDS_MT_HEIGHT,
1276        },
1277    };
1278
1279    /// `finalize_epoch_rewards`'s catch-up loop settles up to `MAX_EPOCHS_PER_FINALIZE` (350)
1280    /// epochs in one call, which far exceeds the default 200k CU budget. We request the
1281    /// per-transaction max so a far-behind node — e.g. after an epoch-duration change snaps the
1282    /// clock forward by hundreds of epochs — can catch up in a single tx instead of running out
1283    /// of compute and rolling back. A full 350-epoch settle costs ~1.15M CU, leaving headroom
1284    /// under the 1.4M ceiling.
1285    const FINALIZE_COMPUTE_UNIT_LIMIT: u32 = 1_400_000;
1286
1287    /// ComputeBudget `SetComputeUnitLimit` instruction (discriminant `2` followed by the u32 LE
1288    /// limit), constructed by hand to avoid a compute-budget crate dependency.
1289    fn set_compute_unit_limit_ix(
1290        limit: u32,
1291    ) -> anchor_lang::solana_program::instruction::Instruction {
1292        let mut data = Vec::with_capacity(5);
1293        data.push(2u8);
1294        data.extend_from_slice(&limit.to_le_bytes());
1295        anchor_lang::solana_program::instruction::Instruction {
1296            program_id: Pubkey::from_str_const("ComputeBudget111111111111111111111111111111"),
1297            accounts: Vec::new(),
1298            data,
1299        }
1300    }
1301
1302    pub async fn init_primary_stake_acc(
1303        arcium_program: &Program<Arc<Keypair>>,
1304        signer: Arc<Keypair>,
1305        amount: u64,
1306        fee_basis_points: u16,
1307        config: RpcSendTransactionConfig,
1308    ) -> Signature {
1309        let ix = init_primary_stake_acc_ix(&signer.pubkey(), amount, fee_basis_points);
1310        let tx = arcium_program.request().instruction(ix);
1311        send_tx(vec![signer], tx, config).await
1312    }
1313
1314    pub async fn activate_primary_stake_acc(
1315        arcium_program: &Program<Arc<Keypair>>,
1316        signer: Arc<Keypair>,
1317        lockup_epochs: u64,
1318
1319        config: RpcSendTransactionConfig,
1320    ) -> Signature {
1321        let ix = activate_primary_stake_acc_ix(&signer.pubkey(), lockup_epochs);
1322        let tx = arcium_program.request().instruction(ix);
1323        send_tx(vec![signer], tx, config).await
1324    }
1325
1326    pub async fn deactivate_primary_stake_acc(
1327        arcium_program: &Program<Arc<Keypair>>,
1328        signer: Arc<Keypair>,
1329        arx_node_offset: Option<u32>,
1330
1331        config: RpcSendTransactionConfig,
1332    ) -> Signature {
1333        let ix = deactivate_primary_stake_acc_ix(&signer.pubkey(), arx_node_offset);
1334        let tx = arcium_program.request().instruction(ix);
1335        send_tx(vec![signer], tx, config).await
1336    }
1337
1338    /// Enqueue a change of `diff` base units to `signer`'s primary stake account (positive adds,
1339    /// negative withdraws). On withdrawals from a stake bound to a clustered node, `arx_node` and
1340    /// `mempool` must be supplied so the program can enforce the cluster's minimum primary stake;
1341    /// see `modify_primary_stake_ix` for the full set of on-chain constraints.
1342    pub async fn modify_primary_stake_acc(
1343        arcium_program: &Program<Arc<Keypair>>,
1344        signer: Arc<Keypair>,
1345        diff: i64,
1346        arx_node: Option<Pubkey>,
1347        mempool: Option<Pubkey>,
1348        config: RpcSendTransactionConfig,
1349    ) -> Signature {
1350        let ix = modify_primary_stake_ix(&signer.pubkey(), diff, arx_node, mempool);
1351        let tx = arcium_program.request().instruction(ix);
1352        send_tx(vec![signer], tx, config).await
1353    }
1354
1355    /// Drive `arcium_staking::finalize_epoch_rewards`. CPIs into `arcium::claim_node_fees`
1356    /// to drain the node's accumulated `current_total_rewards` out of the arcium `FeePool`
1357    /// into the staking pool PDA, then inserts an authoritative `RewardClaim` leaf into the
1358    /// primary's compressed merkle tree so stakers can claim their share later via
1359    /// `claim_primary_stake_rewards` / `claim_delegated_stake_rewards`.
1360    ///
1361    /// This is the user-callable replacement for the old direct-to-recipient
1362    /// `claim_node_fees` flow — the low-level arcium instruction is now CPI-only.
1363    pub async fn finalize_epoch_rewards(
1364        arcium_program: &Program<Arc<Keypair>>,
1365        signer: Arc<Keypair>,
1366        primary_stake_owner: &Pubkey,
1367        cluster_offset: u32,
1368        node_offset: u32,
1369        config: RpcSendTransactionConfig,
1370    ) -> Signature {
1371        let ix = finalize_epoch_rewards_ix(
1372            &signer.pubkey(),
1373            primary_stake_owner,
1374            cluster_offset,
1375            node_offset,
1376        );
1377        // Prepend the compute-unit-limit bump so a far-behind node can settle a large catch-up gap
1378        // in a single tx (the default 200k budget only covers ~40 epochs).
1379        let tx = arcium_program
1380            .request()
1381            .instruction(set_compute_unit_limit_ix(FINALIZE_COMPUTE_UNIT_LIMIT))
1382            .instruction(ix);
1383        send_tx(vec![signer], tx, config).await
1384    }
1385
1386    /// Submit one `claim_primary_stake_rewards`, transferring the primary staker's cut for a single
1387    /// settled epoch from the staking pool to `destination`. `signer` must be the primary stake
1388    /// owner. `leaf_index` + `opening` prove `claim` against the on-chain `rewards_mt.root`
1389    /// (obtained from a rewards-server or reconstructed client-side); the program re-verifies them.
1390    /// Claims must be submitted in increasing epoch order.
1391    pub async fn claim_primary_stake_rewards(
1392        arcium_program: &Program<Arc<Keypair>>,
1393        signer: Arc<Keypair>,
1394        destination: &Pubkey,
1395        leaf_index: u16,
1396        opening: [[u8; 32]; REWARDS_MT_HEIGHT],
1397        claim: RewardClaim,
1398        config: RpcSendTransactionConfig,
1399    ) -> Signature {
1400        let ix = claim_primary_stake_rewards_ix(
1401            &signer.pubkey(),
1402            destination,
1403            leaf_index,
1404            opening,
1405            claim,
1406        );
1407        let tx = arcium_program.request().instruction(ix);
1408        send_tx(vec![signer], tx, config).await
1409    }
1410
1411    pub async fn init_delegated_stake_master_acc(
1412        arcium_program: &Program<Arc<Keypair>>,
1413        signer: Arc<Keypair>,
1414        owner: &Pubkey,
1415        config: RpcSendTransactionConfig,
1416    ) -> Signature {
1417        let ix = init_delegated_stake_master_acc_ix(&signer.pubkey(), owner);
1418        let tx = arcium_program.request().instruction(ix);
1419        send_tx(vec![signer], tx, config).await
1420    }
1421
1422    pub async fn init_delegated_stake_acc(
1423        arcium_program: &Program<Arc<Keypair>>,
1424        signer: Arc<Keypair>,
1425        stake_offset: u128,
1426        amount: u64,
1427        config: RpcSendTransactionConfig,
1428    ) -> Signature {
1429        let ix = init_delegated_stake_acc_ix(&signer.pubkey(), stake_offset, amount);
1430        let tx = arcium_program.request().instruction(ix);
1431        send_tx(vec![signer], tx, config).await
1432    }
1433
1434    pub async fn delegate_stake(
1435        arcium_program: &Program<Arc<Keypair>>,
1436        signer: Arc<Keypair>,
1437        stake_offset: u128,
1438        primary_stake_owner: &Pubkey,
1439        lockup_epochs: u64,
1440        config: RpcSendTransactionConfig,
1441    ) -> Signature {
1442        let ix = delegate_stake_ix(
1443            &signer.pubkey(),
1444            stake_offset,
1445            primary_stake_owner,
1446            lockup_epochs,
1447        );
1448        let tx = arcium_program.request().instruction(ix);
1449        send_tx(vec![signer], tx, config).await
1450    }
1451
1452    pub async fn undelegate_stake(
1453        arcium_program: &Program<Arc<Keypair>>,
1454        signer: Arc<Keypair>,
1455        stake_offset: u128,
1456        primary_stake_owner: &Pubkey,
1457        config: RpcSendTransactionConfig,
1458    ) -> Signature {
1459        let ix = undelegate_stake_ix(&signer.pubkey(), stake_offset, primary_stake_owner);
1460        let tx = arcium_program.request().instruction(ix);
1461        send_tx(vec![signer], tx, config).await
1462    }
1463
1464    #[allow(clippy::too_many_arguments)]
1465    pub async fn split_delegated_stake_account(
1466        arcium_program: &Program<Arc<Keypair>>,
1467        primary_stake_owner_target: &Pubkey,
1468        delegation_authority: Arc<Keypair>,
1469        withdrawal_authority: Arc<Keypair>,
1470        stake_offset: u128,
1471        stake_offset_new: u128,
1472        new_acc_balance: u64,
1473        config: RpcSendTransactionConfig,
1474    ) -> Signature {
1475        let ix = split_delegated_stake_account_ix(
1476            primary_stake_owner_target,
1477            &delegation_authority.pubkey(),
1478            &withdrawal_authority.pubkey(),
1479            stake_offset,
1480            stake_offset_new,
1481            new_acc_balance,
1482        );
1483        let tx = arcium_program.request().instruction(ix);
1484        send_tx(vec![delegation_authority, withdrawal_authority], tx, config).await
1485    }
1486
1487    pub async fn merge_delegated_stake_account(
1488        arcium_program: &Program<Arc<Keypair>>,
1489        primary_stake_owner_target: &Pubkey,
1490        delegation_authority: Arc<Keypair>,
1491        withdrawal_authority: Arc<Keypair>,
1492        stake_offset_keep: u128,
1493        stake_offset_close: u128,
1494        config: RpcSendTransactionConfig,
1495    ) -> Signature {
1496        let ix = merge_delegated_stake_account_ix(
1497            primary_stake_owner_target,
1498            &delegation_authority.pubkey(),
1499            &withdrawal_authority.pubkey(),
1500            stake_offset_keep,
1501            stake_offset_close,
1502        );
1503        let tx = arcium_program.request().instruction(ix);
1504        send_tx(vec![delegation_authority, withdrawal_authority], tx, config).await
1505    }
1506
1507    pub async fn close_delegated_stake(
1508        arcium_program: &Program<Arc<Keypair>>,
1509        signer: Arc<Keypair>,
1510        delegation_owner: &Pubkey,
1511        stake_offset: u128,
1512        primary_stake_owner: Option<&Pubkey>,
1513        config: RpcSendTransactionConfig,
1514    ) -> Signature {
1515        let ix = close_delegated_stake_ix(
1516            &signer.pubkey(),
1517            delegation_owner,
1518            stake_offset,
1519            primary_stake_owner,
1520        );
1521        let tx = arcium_program.request().instruction(ix);
1522        send_tx(vec![signer], tx, config).await
1523    }
1524}
1525
1526#[cfg(feature = "permissioned-mainnet")]
1527pub mod permissioned_mainnet {
1528    use crate::{
1529        instruction::permissioned_mainnet::{
1530            add_permissioned_recovery_peers_ix,
1531            close_permissioned_recovery_peers_acc_ix,
1532            init_permissioned_recovery_peers_acc_ix,
1533            remove_permissioned_recovery_peers_ix,
1534        },
1535        transactions::send_tx,
1536    };
1537    use anchor_client::Program;
1538    use anchor_lang::prelude::Pubkey;
1539    use solana_keypair::Keypair;
1540    use solana_rpc_client_api::config::RpcSendTransactionConfig;
1541    use solana_signature::Signature;
1542    use solana_signer::Signer;
1543    use std::sync::Arc;
1544
1545    pub async fn init_permissioned_recovery_peers_acc(
1546        arcium_program: &Program<Arc<Keypair>>,
1547        signer: Arc<Keypair>,
1548        config: RpcSendTransactionConfig,
1549    ) -> Signature {
1550        let ix = init_permissioned_recovery_peers_acc_ix(&signer.pubkey());
1551        let tx = arcium_program.request().instruction(ix);
1552        send_tx(vec![signer], tx, config).await
1553    }
1554
1555    pub async fn close_permissioned_recovery_peers_acc(
1556        arcium_program: &Program<Arc<Keypair>>,
1557        signer: Arc<Keypair>,
1558        config: RpcSendTransactionConfig,
1559    ) -> Signature {
1560        let ix = close_permissioned_recovery_peers_acc_ix(&signer.pubkey());
1561        let tx = arcium_program.request().instruction(ix);
1562        send_tx(vec![signer], tx, config).await
1563    }
1564
1565    pub async fn add_permissioned_recovery_peers(
1566        arcium_program: &Program<Arc<Keypair>>,
1567        signer: Arc<Keypair>,
1568        peers: Vec<Pubkey>,
1569        config: RpcSendTransactionConfig,
1570    ) -> Signature {
1571        let ix = add_permissioned_recovery_peers_ix(&signer.pubkey(), peers);
1572        let tx = arcium_program.request().instruction(ix);
1573        send_tx(vec![signer], tx, config).await
1574    }
1575
1576    pub async fn remove_permissioned_recovery_peers(
1577        arcium_program: &Program<Arc<Keypair>>,
1578        signer: Arc<Keypair>,
1579        peers: Vec<Pubkey>,
1580        config: RpcSendTransactionConfig,
1581    ) -> Signature {
1582        let ix = remove_permissioned_recovery_peers_ix(&signer.pubkey(), peers);
1583        let tx = arcium_program.request().instruction(ix);
1584        send_tx(vec![signer], tx, config).await
1585    }
1586}
1587
1588#[cfg(test)]
1589mod tests {
1590    use super::*;
1591    use crate::{
1592        idl::{arcium::ID as ARCIUM_ID, arcium_staking::ID as ARCIUM_STAKING_ID},
1593        pda::staking::cluster_stake_state_acc,
1594    };
1595    use solana_keypair::Keypair;
1596    use solana_program::hash::Hash;
1597
1598    #[test]
1599    fn init_cluster_instructions_include_cluster_stake_state() {
1600        let payer = Pubkey::new_unique();
1601        let cluster_authority = Pubkey::new_unique();
1602        let cluster_offset = 42;
1603
1604        let [cluster_ix, stake_state_ix] = init_cluster_instructions(
1605            &payer,
1606            cluster_authority,
1607            cluster_offset,
1608            3,
1609            DEFAULT_CU_PRICE,
1610            MempoolSize::Tiny,
1611            None,
1612        );
1613
1614        assert_eq!(cluster_ix.program_id, ARCIUM_ID);
1615        assert_eq!(stake_state_ix.program_id, ARCIUM_STAKING_ID);
1616        assert_eq!(stake_state_ix.accounts[0].pubkey, payer);
1617        assert_eq!(
1618            stake_state_ix.accounts[1].pubkey,
1619            cluster_stake_state_acc(cluster_offset)
1620        );
1621    }
1622
1623    #[test]
1624    fn test_build_claim_failure_txs_single_tx_at_max_size() {
1625        let payer = Arc::new(Keypair::new());
1626        let rpc = AsyncRpcClient::new("http://localhost:8899".to_string());
1627        let arcium_program = arcium_program_client(SolanaCluster::Localnet, payer.clone(), rpc);
1628
1629        let signer_pubkey = payer.pubkey();
1630        let mxe_program_id = Pubkey::new_unique();
1631        let callback_data = vec![0u8; MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE];
1632
1633        let txs = build_claim_failure_txs(
1634            &arcium_program,
1635            &signer_pubkey,
1636            &mxe_program_id,
1637            0,
1638            0,
1639            0,
1640            0,
1641            callback_data,
1642            0,
1643        )
1644        .expect("Should succeed for data within single-tx limit");
1645
1646        assert_eq!(
1647            txs.len(),
1648            1,
1649            "Expected exactly 1 transaction for data at MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE"
1650        );
1651        let mut tx = txs[0].transaction();
1652        tx.sign(&[payer], Hash::new_unique());
1653        let serialized = bincode::serialize(&tx).unwrap();
1654        assert_eq!(
1655            serialized.len(),
1656            1232,
1657            "Expected max size of serialized transaction to be 1232 bytes but got {}",
1658            serialized.len()
1659        );
1660    }
1661
1662    #[test]
1663    fn test_build_claim_failure_txs_oversized_returns_error() {
1664        let payer = Arc::new(Keypair::new());
1665        let rpc = AsyncRpcClient::new("http://localhost:8899".to_string());
1666        let arcium_program = arcium_program_client(SolanaCluster::Localnet, payer.clone(), rpc);
1667
1668        let signer_pubkey = payer.pubkey();
1669        let mxe_program_id = Pubkey::new_unique();
1670        let callback_data = vec![0u8; MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE + 1];
1671
1672        let result = build_claim_failure_txs(
1673            &arcium_program,
1674            &signer_pubkey,
1675            &mxe_program_id,
1676            0,
1677            0,
1678            0,
1679            0,
1680            callback_data,
1681            0,
1682        );
1683
1684        assert!(matches!(
1685            result,
1686            Err(ClaimFailureError::CallbackDataTooLarge { size, limit })
1687                if size == MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE + 1
1688                    && limit == MAX_SINGLE_TX_FAILURE_CLAIM_DATA_SIZE
1689        ));
1690    }
1691}