Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 60 additions & 8 deletions contracts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions contracts/utility_contracts/src/batch_executor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use soroban_sdk::{contracttype, Address, Env, Symbol, Val, Vec};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BatchOperation {
pub contract: Address,
pub function: Symbol,
pub args: Vec<Val>,
}

pub fn execute_batch(env: &Env, ops: Vec<BatchOperation>) -> Vec<Val> {
if ops.len() > 20 {
panic!("Batch exceeds maximum of 20 operations");
}

let mut results: Vec<Val> = Vec::new(env);

for op in ops.iter() {
// Any failure here will bubble up and panic, reverting the entire transaction.
// This satisfies "Atomic execution (all or nothing)" and "rollback on failure".
let res: Val = env.invoke_contract(&op.contract, &op.function, op.args);
results.push_back(res);
}

results
}

pub fn estimate_batch_gas(_env: &Env, ops: Vec<BatchOperation>) -> u64 {
// Basic heuristic for estimating gas usage for batch operations off-chain.
// Base cost + (per operation overhead * number of operations).
let base_cost = 10_000;
let per_op_cost = 5_000;

let total_cost = base_cost + (ops.len() as u64 * per_op_cost);
total_cost
}
122 changes: 122 additions & 0 deletions contracts/utility_contracts/src/batch_executor_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#![cfg(test)]

use crate::batch_executor::{BatchOperation, estimate_batch_gas};
use crate::{UtilityContract, UtilityContractClient};
use soroban_sdk::{testutils::Address as _, Address, Env, IntoVal, Symbol, Vec, symbol_short};

// A dummy contract to test the batch executor.
#[soroban_sdk::contract]
pub struct DummyContract;

#[soroban_sdk::contractimpl]
impl DummyContract {
pub fn add(env: Env, a: u32, b: u32) -> u32 {
a + b
}

pub fn fail(_env: Env) {
panic!("Intended failure");
}
}

#[test]
fn test_batch_execution_success() {
let env = Env::default();

let utility_id = env.register_contract(None, UtilityContract);
let utility_client = UtilityContractClient::new(&env, &utility_id);

let dummy_id = env.register_contract(None, DummyContract);

let mut ops = Vec::new(&env);

ops.push_back(BatchOperation {
contract: dummy_id.clone(),
function: symbol_short!("add"),
args: (2u32, 3u32).into_val(&env),
});

ops.push_back(BatchOperation {
contract: dummy_id.clone(),
function: symbol_short!("add"),
args: (10u32, 20u32).into_val(&env),
});

let results = utility_client.execute_batch(&ops);

assert_eq!(results.len(), 2);
let res1: u32 = results.get(0).unwrap().into_val(&env);
let res2: u32 = results.get(1).unwrap().into_val(&env);

assert_eq!(res1, 5);
assert_eq!(res2, 30);
}

#[test]
#[should_panic(expected = "Intended failure")]
fn test_batch_execution_partial_failure() {
let env = Env::default();
let utility_id = env.register_contract(None, UtilityContract);
let utility_client = UtilityContractClient::new(&env, &utility_id);
let dummy_id = env.register_contract(None, DummyContract);

let mut ops = Vec::new(&env);

ops.push_back(BatchOperation {
contract: dummy_id.clone(),
function: symbol_short!("add"),
args: (2u32, 3u32).into_val(&env),
});

ops.push_back(BatchOperation {
contract: dummy_id.clone(),
function: symbol_short!("fail"),
args: ().into_val(&env),
});

utility_client.execute_batch(&ops);
}

#[test]
#[should_panic(expected = "Batch exceeds maximum of 20 operations")]
fn test_batch_execution_limit() {
let env = Env::default();
let utility_id = env.register_contract(None, UtilityContract);
let utility_client = UtilityContractClient::new(&env, &utility_id);
let dummy_id = env.register_contract(None, DummyContract);

let mut ops = Vec::new(&env);

for _ in 0..21 {
ops.push_back(BatchOperation {
contract: dummy_id.clone(),
function: symbol_short!("add"),
args: (1u32, 1u32).into_val(&env),
});
}

utility_client.execute_batch(&ops);
}

#[test]
fn test_gas_estimation() {
let env = Env::default();
let dummy_id = Address::generate(&env);

let mut ops = Vec::new(&env);
for _ in 0..5 {
ops.push_back(BatchOperation {
contract: dummy_id.clone(),
function: symbol_short!("add"),
args: (1u32, 1u32).into_val(&env),
});
}

let utility_id = env.register_contract(None, UtilityContract);
let utility_client = UtilityContractClient::new(&env, &utility_id);

let estimated_gas = utility_client.estimate_batch_gas(&ops);

// Base 10000 + 5 * 5000 = 35000
assert_eq!(estimated_gas, 35000);
}
16 changes: 16 additions & 0 deletions contracts/utility_contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use soroban_sdk::{
symbol_short, token, Address, Bytes, BytesN, Env, String, Symbol, Vec,
};

pub mod batch_executor;
#[cfg(test)]
pub mod batch_executor_tests;

#[contractclient(name = "PriceOracleClient")]
pub trait PriceOracle {
fn xlm_to_usd_cents(env: Env, xlm_amount: i128) -> i128;
Expand Down Expand Up @@ -3429,6 +3433,18 @@ pub struct UtilityContract;

#[contractimpl]
impl UtilityContract {
/// Executes a batch of operations atomically to save gas.
/// Fails the entire batch if any single operation fails.
/// Limited to 20 operations per batch.
pub fn execute_batch(env: Env, ops: Vec<crate::batch_executor::BatchOperation>) -> Vec<Val> {
crate::batch_executor::execute_batch(&env, ops)
}

/// Estimates the gas required for a batch of operations.
pub fn estimate_batch_gas(env: Env, ops: Vec<crate::batch_executor::BatchOperation>) -> u64 {
crate::batch_executor::estimate_batch_gas(&env, ops)
}

/// Assigns a reseller to a specific meter with a defined fee percentage.
///
/// # Arguments
Expand Down
Loading