use eyre::{Result, eyre};
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use crate::chain_client::ARCH_SOLANA;
use crate::commands::config::config_pb::GetConfigResponse;
use crate::solana::client::WithdrawEpochState;
use crate::wallet::Wallet;
pub const WITHDRAW_EPOCH_SLOTS: u64 = 9_000;
const MIN_SOL_LAMPORTS: u64 = 3_000_000;
#[derive(Debug, Clone)]
pub struct SetWithdrawEpochCapOutcome {
pub signature: String,
pub program_id: Pubkey,
pub instance: Pubkey,
pub mint: Pubkey,
pub withdraw_epoch: Pubkey,
pub state: Option<WithdrawEpochState>,
pub admin_is_tee_signer: bool,
}
pub async fn set_withdraw_epoch_cap(
config: &GetConfigResponse,
network: &str,
token_symbol: &str,
cap: u64,
wallet: &Wallet,
) -> Result<SetWithdrawEpochCapOutcome> {
let chain = config
.get_chain(network)
.ok_or_else(|| eyre!("Chain '{}' not found in server configuration", network))?;
if !chain.architecture.eq_ignore_ascii_case(ARCH_SOLANA) {
return Err(eyre!(
"chain '{}' has architecture '{}' — the per-epoch withdrawal cap \
command targets Solana instances only. On EVM the equivalent is \
`MidribV3.setWithdrawEpochCap`, whose authority is the same address \
that signs withdrawal vouchers.",
network,
chain.architecture
));
}
let token = chain.tokens.get(token_symbol).ok_or_else(|| {
eyre!(
"Token '{}' not found on Solana chain '{}'. Run `config` to see \
available tokens.",
token_symbol,
network
)
})?;
let keypair = wallet.as_solana().ok_or_else(|| {
eyre!(
"Solana chain '{}' requires an Ed25519 operator-admin keypair ({})",
network,
crate::wallet::OPERATOR_ADMIN_PRIVKEY_SOLANA_ENV
)
})?;
let operator_admin = solana_sdk::signer::Signer::pubkey(keypair);
let (program_id, instance) = crate::solana::client::resolve_program_and_instance(chain)?;
let mint = Pubkey::from_str(&token.address)
.map_err(|e| eyre!("invalid Solana mint '{}': {}", token.address, e))?;
let (withdraw_epoch, _) =
crate::solana::derive_withdraw_epoch_pda(&instance, &mint, &program_id);
let rpc_url = crate::chain_client::chain_rpc_url(chain)?;
let authorities =
crate::solana::client::fetch_instance_authorities(&rpc_url, &instance).await?;
if authorities.operator_admin != operator_admin {
return Err(eyre!(
"{} holds {}, but instance {} has operator_admin {} — the program \
would reject this with `Unauthorized`. Load the instance's \
operator-admin keypair.",
crate::wallet::OPERATOR_ADMIN_PRIVKEY_SOLANA_ENV,
operator_admin,
instance,
authorities.operator_admin
));
}
let admin_is_tee_signer = authorities.operator_admin == authorities.signer;
if admin_is_tee_signer {
tracing::warn!(
instance = %instance,
"instance operator_admin == its TEE signer ({}) — the cap you are \
arming bounds bugs and operator error, but a compromised signer can \
raise it again and drain. Containment requires a distinct \
operator-admin key.",
authorities.signer
);
}
{
use solana_client::nonblocking::rpc_client::RpcClient;
let rpc = RpcClient::new(rpc_url.clone());
let lamports = rpc.get_balance(&operator_admin).await.unwrap_or(0);
if lamports < MIN_SOL_LAMPORTS {
return Err(eyre!(
"insufficient SOL: operator-admin {operator_admin} has {lamports} \
lamports, need >= {MIN_SOL_LAMPORTS} to cover the fee plus rent \
for the WithdrawEpoch PDA this instruction may create."
));
}
}
let before =
crate::solana::client::fetch_withdraw_epoch(&rpc_url, &instance, &mint, &program_id)
.await
.unwrap_or(None);
if let Some(state) = before {
tracing::info!(
"current cap for {token_symbol}: {} (epoch {}, {} base units already \
withdrawn this epoch)",
state.cap,
state.epoch,
state.withdrawn
);
} else {
tracing::info!(
"no WithdrawEpoch account yet for {token_symbol} — cap is unlimited; \
this call creates the account (rent paid by {operator_admin})"
);
}
let ix = crate::solana::set_withdraw_epoch_cap_ix(
&program_id,
&instance,
&mint,
&operator_admin,
cap,
)?;
let signature = crate::solana::client::submit_user_signed(&rpc_url, keypair, ix).await?;
let state =
crate::solana::client::fetch_withdraw_epoch(&rpc_url, &instance, &mint, &program_id)
.await
.unwrap_or(None);
Ok(SetWithdrawEpochCapOutcome {
signature,
program_id,
instance,
mint,
withdraw_epoch,
state,
admin_is_tee_signer,
})
}