Hello everyone!
We’ve built a complex system consisting of five smart contracts, and we’re currently in the testing phase. We’ve created 11 different scenarios to thoroughly test our system. However, we’ve hit a roadblock. Our current approach, where we write all the scenarios in a single main file, is causing a stack overflow error in Rust. We’re seeking guidance on a better test architecture to prevent this error.
One key challenge we face is that our scenarios must be executed step-by-step. This is because the logic in each new scenario depends on the state of the contracts, which may have changed in previous steps. Consequently, we need to run up to 20 calls in a row to ensure comprehensive testing.
Here’s a glimpse of our folder structure to provide more context:
abis
(folder containing ABIs of all smart contracts)contract1
contract2
contract3
contract4
contract5
token
tests
local-tests
utils
(folder with function wrappers that optimize smart contract calls)
Within the utils
folder, we’ve created function wrappers that allow us to efficiently manage smart contract instances and invoke their methods.
To give you a better idea of the issue, here’s a code snippet from our testing environment along with the encountered error. We’re eagerly awaiting suggestions for architectural solutions to overcome this challenge. Your insights and recommendations are greatly appreciated! Thank you.
utils file example
use fuels::{
prelude::{abigen, Contract, LoadConfiguration, TxParameters, WalletUnlocked},
programs::{call_response::FuelCallResponse, contract::SettableContract},
types::{bech32::Bech32ContractId, Address, AssetId},
};
use rand::Rng;
abigen!(Contract(
name = "AccountBalanceContract",
abi = "account-balance/out/debug/account-balance-abi.json"
));
/*
class AccountBalanceInstance{
pub instance: AccountBalanceContract<WalletUnlocked>,
pub wallet: WalletUnlocked,
constructor(this){
}
}
*/
pub struct AccountBalanceInstance {
pub instance: AccountBalanceContract<WalletUnlocked>,
pub wallet: WalletUnlocked,
}
impl AccountBalanceInstance {
// ## Pnl block
/// Modifies the owed and realized PNL (Profit and Loss) for a trader.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `amount`: [I64] - The amount to modify the owed and realized PNL.
async fn modify_owed_realized_pnl(
&self,
trader: Address,
amount: I64,
) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.modify_owed_realized_pnl(trader, amount)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
/// Settles a specified amount of quote currency to the trader's owed and realized PNL (Profit and Loss).
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
/// * `amount`: [I64] - The amount to settle from quote currency to owed and realized PNL.
async fn settle_quote_to_owed_realized_pnl(
&self,
trader: Address,
base_token: AssetId,
amount: I64,
) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.settle_quote_to_owed_realized_pnl(trader, base_token, amount)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
/// Settles the owed and realized PNL (Profit and Loss) for a trader, returning the settled amount.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
///
/// # Returns
///
/// * [I64] - The settled owed and realized PNL amount.
async fn settle_owed_realized_pnl(
&self,
trader: Address,
) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.settle_owed_realized_pnl(trader)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
/// Retrieves the PNL (Profit and Loss) information for a trader, including owed and unrealized PNL.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
///
/// # Returns
///
/// * [(I64, I64)] - A tuple containing the owed PNL and unrealized PNL for the trader.
pub async fn get_pnl(
&self,
trader: Address,
contracts: &[&dyn SettableContract],
) -> Result<FuelCallResponse<(I64, I64)>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_pnl(trader)
.tx_params(tx_params)
// .call_params(call_params)?
.with_contracts(contracts)
// .append_variable_outputs(1)
.simulate()
.await
}
// ## Balance block
/// Settles the balance for a trader, deregisters the trader from the specified market, and records realized PNL (Profit and Loss).
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
/// * `taker_base`: [I64] - The base token amount to settle.
/// * `taker_quote`: [I64] - The quote token amount to settle.
/// * `realized_pnl`: [I64] - The realized Profit and Loss amount.
async fn settle_balance_and_deregister(
&self,
trader: Address,
base_token: AssetId,
taker_base: I64,
taker_quote: I64,
realized_pnl: I64,
) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.settle_balance_and_deregister(
trader,
base_token,
taker_base,
taker_quote,
realized_pnl,
)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
/// Retrieves the account balance for a trader in a specific market.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
///
/// # Returns
///
/// * [AccountBalance] - The account balance information.
pub async fn get_account_balance(
&self,
trader: Address,
base_token: AssetId,
) -> Result<FuelCallResponse<AccountBalance>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_account_balance(trader, base_token)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.simulate()
.await
}
/// Modifies the taker's balance in a specific market.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
/// * `base`: [I64] - The base token amount to modify.
/// * `quote`: [I64] - The quote token amount to modify.
///
/// # Returns
///
/// * [(I64, I64)] - A tuple containing the modified base and quote token amounts.
async fn modify_taker_balance(
&self,
trader: Address,
base_token: AssetId,
base: I64,
quote: I64,
) -> Result<FuelCallResponse<(I64, I64)>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.modify_taker_balance(trader, base_token, base, quote)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
// ## Base token block
/// Registers a base token for a trader.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token to register.
async fn register_base_token(
&self,
trader: Address,
base_token: AssetId,
) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.register_base_token(trader, base_token)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
/// Deregisters a base token for a trader.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token to deregister.
async fn deregister_base_token(
&self,
trader: Address,
base_token: AssetId,
) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.deregister_base_token(trader, base_token)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
/// Retrieves a list of base tokens registered by a trader.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
///
/// # Returns
///
/// * [Vec<AssetId>] - A vector containing the AssetIds of the registered base tokens.
async fn get_base_tokens(
&self,
trader: Address,
) -> Result<FuelCallResponse<Vec<AssetId>>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_base_tokens(trader)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.simulate()
.await
}
// ## Position block
/// Settles a trader's position in a closed market for a specific base token.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token for which the position is settled.
///
/// # Returns
///
/// * [(I64, I64, I64, u64)] - A tuple containing the position notional, open notional, realized P&L, and the closed price.
async fn settle_position_in_closed_market(
&self,
trader: Address,
base_token: AssetId,
) -> Result<FuelCallResponse<(I64, I64, I64, u64)>, fuels::types::errors::Error> {
// -> (positionNotional, openNotional, realizedPnl, closedPrice
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.settle_position_in_closed_market(trader, base_token)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
/// Calculates the size of a liquidatable position for a trader in a specified base token based on the trader's account value.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
/// * `account_value`: [I64] - The trader's account value.
///
/// # Returns
///
/// * [I64] - The size of the liquidatable position
pub async fn get_liquidatable_position_size(
&self,
trader: Address,
base_token: AssetId,
account_value: I64,
) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_liquidatable_position_size(trader, base_token, account_value)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.simulate()
.await
}
/// Retrieves the taker position size for a trader in a specified base token.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
///
/// # Returns
///
/// * [I64] - The taker position size.
async fn get_taker_position_size(
&self,
trader: Address,
base_token: AssetId,
) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_taker_position_size(trader, base_token)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.simulate()
.await
}
/// Retrieves the total position value for a trader in a specified base token.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
///
/// # Returns
///
/// * [I64] - The total position value.
pub async fn get_total_position_value(
&self,
trader: Address,
base_token: AssetId,
contracts: &[&dyn SettableContract],
) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_total_position_value(trader, base_token)
.tx_params(tx_params)
// .call_params(call_params)?
.with_contracts(contracts)
// .append_variable_outputs(1)
.simulate()
.await
}
/// Retrieves the total absolute position value for a trader.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
///
/// # Returns
///
/// * [u64] - The total absolute position value.
pub async fn get_total_abs_position_value(
&self,
trader: Address,
contracts: &[&dyn SettableContract],
) -> Result<FuelCallResponse<u64>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_total_abs_position_value(trader)
.tx_params(tx_params)
// .call_params(call_params)?
.with_contracts(contracts)
// .append_variable_outputs(1)
.simulate()
.await
}
// ## TW Premium
/// Updates the global twap premium growth for a trader's position in a specified base token.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
/// * `last_tw_premium_growth_global`: [I64] - The last twap premium growth global value.
async fn update_tw_premium_growth_global(
&self,
trader: Address,
base_token: AssetId,
last_tw_premium_growth_global: I64,
) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.update_tw_premium_growth_global(trader, base_token, last_tw_premium_growth_global)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.call()
.await
}
// ## Open notional
/// Retrieves the taker's open notional value for a specified base token.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
/// * `base_token`: [AssetId] - The AssetId of the base token.
///
/// # Returns
///
/// * [I64] - The taker's open notional value.
async fn get_taker_open_notional(
&self,
trader: Address,
base_token: AssetId,
) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_taker_open_notional(trader, base_token)
.tx_params(tx_params)
// .call_params(call_params)?
// .with_contracts(contract_ids)
// .append_variable_outputs(1)
.simulate()
.await
}
/// Retrieves the margin requirement for a specified trader.
///
/// # Arguments
///
/// * `trader`: [Address] - The address of the trader.
///
/// # Returns
///
/// * [u64] - The margin requirement.
pub async fn get_margin_requirement(
&self,
trader: Address,
contracts: &[&dyn SettableContract],
) -> Result<FuelCallResponse<u64>, fuels::types::errors::Error> {
let tx_params = TxParameters::default().with_gas_price(1);
self.instance
.methods()
.get_margin_requirement(trader)
.tx_params(tx_params)
// .call_params(call_params)?
.with_contracts(contracts)
// .append_variable_outputs(1)
.simulate()
.await
}
pub fn new(wallet: &WalletUnlocked, id: &Bech32ContractId) -> Self {
Self {
instance: AccountBalanceContract::new(id, wallet.clone()),
wallet: wallet.clone(),
}
}
pub async fn deploy(wallet: &WalletUnlocked, proxy: Address) -> Self {
let mut rng = rand::thread_rng();
let salt = rng.gen::<[u8; 32]>();
let configurables =
AccountBalanceContractConfigurables::default().with_PROXY_ADDRESS(proxy);
let config = LoadConfiguration::default().with_configurables(configurables);
let id = Contract::load_from("account-balance/out/debug/account-balance.bin", config)
.unwrap()
.with_salt(salt)
.deploy(wallet, TxParameters::default().with_gas_price(1))
.await
.unwrap();
Self {
instance: AccountBalanceContract::new(id, wallet.clone()),
wallet: wallet.clone(),
}
}
}