use crate::{
chain::{client::QuantusClient, quantus_subxt},
cli::common::{ExecutionMode, SubxtAccountId32},
error::{QuantusError, Result},
wallet::{DilithiumScheme, QuantumKeyPair},
};
use rand::{rngs::StdRng, Rng};
pub struct ExerciseCtx {
pub client: QuantusClient,
pub node_url: String,
pub root: QuantumKeyPair,
pub alice: QuantumKeyPair,
pub bob: QuantumKeyPair,
pub charlie: QuantumKeyPair,
pub eph: Vec<QuantumKeyPair>,
pub root_ss58: String,
pub root_start_balance: u128,
pub budget: u128,
pub budget_exempt: u128,
pub unit: u128,
pub test_unit: u128,
pub existential_deposit: u128,
pub rng: StdRng,
pub seed: u64,
pub fuzz_iterations: u32,
}
impl ExerciseCtx {
pub fn wait_mode(&self) -> ExecutionMode {
ExecutionMode { finalized: false, wait_for_transaction: true }
}
pub async fn budget_spent(&self) -> Result<u128> {
let current = self.free_balance(&self.root_ss58).await?;
Ok(self
.root_start_balance
.saturating_sub(current)
.saturating_sub(self.budget_exempt))
}
pub async fn reserve(&self, amount: u128) -> Result<()> {
let spent = self.budget_spent().await?;
if spent.saturating_add(amount) > self.budget {
return Err(QuantusError::Generic(format!(
"--total-amount exhausted: {spent} of {} raw units already drawn from the root \
account and this step needs {amount} more; raise --total-amount or skip phases",
self.budget
)));
}
Ok(())
}
pub async fn submit_budgeted<Call>(
&self,
from: &QuantumKeyPair,
call: Call,
reserved: u128,
) -> Result<subxt::utils::H256>
where
Call: subxt::tx::Payload,
{
if from.try_to_account_id_ss58check()? == self.root_ss58 {
let fee =
crate::cli::send::estimate_transaction_partial_fee(&self.client, from, &call, None)
.await?;
self.reserve(reserved.saturating_add(fee)).await?;
}
submit_ok(self, from, call).await
}
pub async fn submit_from_root<Call>(
&self,
call: Call,
reserved: u128,
) -> Result<subxt::utils::H256>
where
Call: subxt::tx::Payload,
{
self.submit_budgeted(&self.root, call, reserved).await
}
pub async fn fund_from_root(&self, dest_ss58: &str, amount: u128) -> Result<()> {
self.batch_fund_from_root(vec![(dest_ss58.to_string(), amount)]).await
}
pub async fn batch_fund_from_root(&self, transfers: Vec<(String, u128)>) -> Result<()> {
let total = transfers.iter().map(|(_, amount)| amount).sum();
let call = crate::cli::send::build_batch_transfer_call(&transfers)?;
self.submit_from_root(call, total).await?;
Ok(())
}
pub async fn sweep_to_root(&self, who: &QuantumKeyPair) -> Result<()> {
if self.free_balance(&who.try_to_account_id_ss58check()?).await? == 0 {
return Ok(());
}
let call = quantus_subxt::api::tx().balances().transfer_all(
subxt::ext::subxt_core::utils::MultiAddress::Id(account_id_of(&self.root)?),
false,
);
submit_ok(self, who, call).await?;
Ok(())
}
pub fn fresh_keypair_with_scheme(&mut self, scheme: DilithiumScheme) -> Result<QuantumKeyPair> {
let seed: [u8; 32] = self.rng.random();
match scheme {
DilithiumScheme::MlDsa65 => {
let pair =
qp_dilithium_crypto::types::Dilithium65Pair::from_seed(&seed).map_err(|e| {
QuantusError::Generic(format!("Failed to derive keypair: {e:?}"))
})?;
Ok(QuantumKeyPair::from_dilithium65_pair(&pair))
},
DilithiumScheme::MlDsa87 => {
let pair =
qp_dilithium_crypto::types::Dilithium87Pair::from_seed(&seed).map_err(|e| {
QuantusError::Generic(format!("Failed to derive keypair: {e:?}"))
})?;
Ok(QuantumKeyPair::from_resonance_pair(&pair))
},
}
}
pub fn fresh_keypair(&mut self) -> Result<QuantumKeyPair> {
let seed: [u8; 32] = self.rng.random();
let scheme = if seed[0].is_multiple_of(2) {
DilithiumScheme::MlDsa65
} else {
DilithiumScheme::MlDsa87
};
match scheme {
DilithiumScheme::MlDsa65 => {
let pair =
qp_dilithium_crypto::types::Dilithium65Pair::from_seed(&seed).map_err(|e| {
QuantusError::Generic(format!("Failed to derive keypair: {e:?}"))
})?;
Ok(QuantumKeyPair::from_dilithium65_pair(&pair))
},
DilithiumScheme::MlDsa87 => {
let pair =
qp_dilithium_crypto::types::Dilithium87Pair::from_seed(&seed).map_err(|e| {
QuantusError::Generic(format!("Failed to derive keypair: {e:?}"))
})?;
Ok(QuantumKeyPair::from_resonance_pair(&pair))
},
}
}
pub async fn free_balance(&self, ss58: &str) -> Result<u128> {
crate::cli::send::get_balance(&self.client, ss58).await
}
}
pub fn account_id_of(keypair: &QuantumKeyPair) -> Result<SubxtAccountId32> {
let account = keypair.try_to_account_id_32()?;
let bytes: [u8; 32] = *account.as_ref();
Ok(SubxtAccountId32::from(bytes))
}
pub async fn submit_ok<Call>(
ctx: &ExerciseCtx,
from: &QuantumKeyPair,
call: Call,
) -> Result<subxt::utils::H256>
where
Call: subxt::tx::Payload,
{
crate::cli::common::submit_transaction(&ctx.client, from, call, None, ctx.wait_mode()).await
}
pub async fn submit_expect_failure<Call>(
ctx: &ExerciseCtx,
from: &QuantumKeyPair,
call: Call,
expected_fragments: &[&str],
) -> Result<String>
where
Call: subxt::tx::Payload,
{
match crate::cli::common::submit_transaction(&ctx.client, from, call, None, ctx.wait_mode())
.await
{
Ok(hash) => Err(QuantusError::Generic(format!(
"expected rejection but transaction succeeded ({hash:?})"
))),
Err(e) => {
let msg = e.to_string();
if expected_fragments.is_empty() ||
expected_fragments.iter().any(|frag| msg.contains(frag))
{
Ok(format!("rejected as expected: {}", first_line(&msg)))
} else {
Err(QuantusError::Generic(format!(
"rejected with unexpected error (wanted one of {expected_fragments:?}): {msg}"
)))
}
},
}
}
fn first_line(msg: &str) -> &str {
msg.lines().next().unwrap_or(msg)
}
#[macro_export]
macro_rules! exercise_step {
($report:expr, $phase:expr, $name:expr, $fut:expr) => {{
let __started = std::time::Instant::now();
let __result = $fut.await;
$report.record($phase, $name, __started.elapsed(), __result);
if $report.should_abort() {
return Ok(());
}
}};
}