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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ cargo test
- The suite covers minting the payer account, splitting across spending/savings/bills/insurance, and asserting balances along with the new allocation metadata helper.
- The same command is intended for CI so it runs without manual setup; re-run locally whenever split logic changes or new USDC paths are added.

## Gas Benchmarks

See `docs/gas-optimization.md` for methodology, before/after results, and assumptions.

Run the deterministic gas benchmarks:

```bash
RUST_TEST_THREADS=1 cargo test -p bill_payments --test gas_bench -- --nocapture
RUST_TEST_THREADS=1 cargo test -p savings_goals --test gas_bench -- --nocapture
RUST_TEST_THREADS=1 cargo test -p insurance --test gas_bench -- --nocapture
RUST_TEST_THREADS=1 cargo test -p family_wallet --test gas_bench -- --nocapture
RUST_TEST_THREADS=1 cargo test -p remittance_split --test gas_bench -- --nocapture
```

## Deployment

See the [Deployment Guide](DEPLOYMENT.md) for comprehensive deployment instructions.
Expand Down
53 changes: 18 additions & 35 deletions bill_payments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,17 +244,9 @@ impl BillPayments {
.unwrap_or_else(|| Map::new(&env));

let mut result = Vec::new(&env);
let max_id = env
.storage()
.instance()
.get(&symbol_short!("NEXT_ID"))
.unwrap_or(0u32);

for i in 1..=max_id {
if let Some(bill) = bills.get(i) {
if !bill.paid && bill.owner == owner {
result.push_back(bill);
}
for (_, bill) in bills.iter() {
if !bill.paid && bill.owner == owner {
result.push_back(bill);
}
}
result
Expand All @@ -273,17 +265,9 @@ impl BillPayments {
.unwrap_or_else(|| Map::new(&env));

let mut result = Vec::new(&env);
let max_id = env
.storage()
.instance()
.get(&symbol_short!("NEXT_ID"))
.unwrap_or(0u32);

for i in 1..=max_id {
if let Some(bill) = bills.get(i) {
if !bill.paid && bill.due_date < current_time {
result.push_back(bill);
}
for (_, bill) in bills.iter() {
if !bill.paid && bill.due_date < current_time {
result.push_back(bill);
}
}
result
Expand All @@ -297,10 +281,17 @@ impl BillPayments {
/// # Returns
/// Total amount of all unpaid bills belonging to the owner
pub fn get_total_unpaid(env: Env, owner: Address) -> i128 {
let unpaid = Self::get_unpaid_bills(env, owner);
let mut total = 0i128;
for bill in unpaid.iter() {
total += bill.amount;
let bills: Map<u32, Bill> = env
.storage()
.instance()
.get(&symbol_short!("BILLS"))
.unwrap_or_else(|| Map::new(&env));

for (_, bill) in bills.iter() {
if !bill.paid && bill.owner == owner {
total += bill.amount;
}
}
total
}
Expand Down Expand Up @@ -346,16 +337,8 @@ impl BillPayments {
.unwrap_or_else(|| Map::new(&env));

let mut result = Vec::new(&env);
let max_id = env
.storage()
.instance()
.get(&symbol_short!("NEXT_ID"))
.unwrap_or(0u32);

for i in 1..=max_id {
if let Some(bill) = bills.get(i) {
result.push_back(bill);
}
for (_, bill) in bills.iter() {
result.push_back(bill);
}
result
}
Expand Down
64 changes: 64 additions & 0 deletions bill_payments/tests/gas_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use bill_payments::{BillPayments, BillPaymentsClient};
use soroban_sdk::testutils::{Address as AddressTrait, EnvTestConfig, Ledger, LedgerInfo};
use soroban_sdk::{Address, Env, String};

fn bench_env() -> Env {
let env = Env::new_with_config(EnvTestConfig {
capture_snapshot_at_drop: false,
});
env.mock_all_auths();
let proto = env.ledger().protocol_version();
env.ledger().set(LedgerInfo {
protocol_version: proto,
sequence_number: 1,
timestamp: 1_700_000_000,
network_id: [0; 32],
base_reserve: 10,
min_temp_entry_ttl: 1,
min_persistent_entry_ttl: 1,
max_entry_ttl: 100_000,
});
let mut budget = env.budget();
budget.reset_unlimited();
env
}

fn measure<F, R>(env: &Env, f: F) -> (u64, u64, R)
where
F: FnOnce() -> R,
{
let mut budget = env.budget();
budget.reset_unlimited();
budget.reset_tracker();
let result = f();
let cpu = budget.cpu_instruction_cost();
let mem = budget.memory_bytes_cost();
(cpu, mem, result)
}

#[test]
fn bench_get_total_unpaid_worst_case() {
let env = bench_env();
let contract_id = env.register_contract(None, BillPayments);
let client = BillPaymentsClient::new(&env, &contract_id);
let owner = <Address as AddressTrait>::generate(&env);

let name = String::from_str(&env, "BenchBill");
for _ in 0..100 {
client.create_bill(&owner, &name, &100i128, &1_000_000u64, &false, &0u32);
}

// Create gaps to simulate worst-case scan behavior in previous implementation.
for id in (2u32..=100u32).step_by(2) {
client.cancel_bill(&id);
}

let expected_total = 50i128 * 100i128;
let (cpu, mem, total) = measure(&env, || client.get_total_unpaid(&owner));
assert_eq!(total, expected_total);

println!(
r#"{{"contract":"bill_payments","method":"get_total_unpaid","scenario":"100_bills_50_cancelled","cpu":{},"mem":{}}}"#,
cpu, mem
);
}
21 changes: 14 additions & 7 deletions family_wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,14 @@ impl FamilyWallet {
) -> bool {
caller.require_auth();

let members: Map<Address, FamilyMember> = env
.storage()
.instance()
.get(&symbol_short!("MEMBERS"))
.expect("Wallet not initialized");

// Verify caller is Owner or Admin
if !Self::is_owner_or_admin(&env, &caller) {
if !Self::is_owner_or_admin_in_members(&members, &caller) {
panic!("Only Owner or Admin can configure multi-sig");
}

Expand All @@ -250,12 +256,6 @@ impl FamilyWallet {
}

// Validate signers are family members
let members: Map<Address, FamilyMember> = env
.storage()
.instance()
.get(&symbol_short!("MEMBERS"))
.expect("Wallet not initialized");

for signer in signers.iter() {
if members.get(signer.clone()).is_none() {
panic!("Signer must be a family member");
Expand Down Expand Up @@ -950,6 +950,13 @@ impl FamilyWallet {
.get(&symbol_short!("MEMBERS"))
.unwrap_or_else(|| Map::new(env));

Self::is_owner_or_admin_in_members(&members, address)
}

fn is_owner_or_admin_in_members(
members: &Map<Address, FamilyMember>,
address: &Address,
) -> bool {
if let Some(member) = members.get(address.clone()) {
matches!(member.role, FamilyRole::Owner | FamilyRole::Admin)
} else {
Expand Down
76 changes: 76 additions & 0 deletions family_wallet/tests/gas_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use family_wallet::{FamilyWallet, FamilyWalletClient, TransactionType};
use soroban_sdk::testutils::{Address as AddressTrait, EnvTestConfig, Ledger, LedgerInfo};
use soroban_sdk::{Address, Env, Vec};

fn bench_env() -> Env {
let env = Env::new_with_config(EnvTestConfig {
capture_snapshot_at_drop: false,
});
env.mock_all_auths();
let proto = env.ledger().protocol_version();
env.ledger().set(LedgerInfo {
protocol_version: proto,
sequence_number: 1,
timestamp: 1_700_000_000,
network_id: [0; 32],
base_reserve: 10,
min_temp_entry_ttl: 1,
min_persistent_entry_ttl: 1,
max_entry_ttl: 100_000,
});
let mut budget = env.budget();
budget.reset_unlimited();
env
}

fn measure<F, R>(env: &Env, f: F) -> (u64, u64, R)
where
F: FnOnce() -> R,
{
let mut budget = env.budget();
budget.reset_unlimited();
budget.reset_tracker();
let result = f();
let cpu = budget.cpu_instruction_cost();
let mem = budget.memory_bytes_cost();
(cpu, mem, result)
}

#[test]
fn bench_configure_multisig_worst_case() {
let env = bench_env();
let contract_id = env.register_contract(None, FamilyWallet);
let client = FamilyWalletClient::new(&env, &contract_id);

let owner = <Address as AddressTrait>::generate(&env);
let mut initial_members = Vec::new(&env);
let mut signers = Vec::new(&env);

for _ in 0..8 {
let member = <Address as AddressTrait>::generate(&env);
initial_members.push_back(member.clone());
signers.push_back(member);
}

client.init(&owner, &initial_members);

// Include owner as an authorized signer too.
signers.push_back(owner.clone());
let threshold = signers.len();

let (cpu, mem, configured) = measure(&env, || {
client.configure_multisig(
&owner,
&TransactionType::LargeWithdrawal,
&threshold,
&signers,
&5_000i128,
)
});
assert!(configured);

println!(
r#"{{"contract":"family_wallet","method":"configure_multisig","scenario":"9_signers_threshold_all","cpu":{},"mem":{}}}"#,
cpu, mem
);
}
27 changes: 13 additions & 14 deletions insurance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,9 @@ impl Insurance {
.unwrap_or_else(|| Map::new(&env));

let mut result = Vec::new(&env);
let max_id = env
.storage()
.instance()
.get(&symbol_short!("NEXT_ID"))
.unwrap_or(0u32);

for i in 1..=max_id {
if let Some(policy) = policies.get(i) {
if policy.active && policy.owner == owner {
result.push_back(policy);
}
for (_, policy) in policies.iter() {
if policy.active && policy.owner == owner {
result.push_back(policy);
}
}
result
Expand All @@ -228,10 +220,17 @@ impl Insurance {
/// # Returns
/// Total monthly premium amount for the owner's active policies
pub fn get_total_monthly_premium(env: Env, owner: Address) -> i128 {
let active = Self::get_active_policies(env, owner);
let mut total = 0i128;
for policy in active.iter() {
total += policy.monthly_premium;
let policies: Map<u32, InsurancePolicy> = env
.storage()
.instance()
.get(&symbol_short!("POLICIES"))
.unwrap_or_else(|| Map::new(&env));

for (_, policy) in policies.iter() {
if policy.active && policy.owner == owner {
total += policy.monthly_premium;
}
}
total
}
Expand Down
Loading
Loading