Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
echo "Installed via Homebrew"
else
echo "Homebrew install failed, installing via cargo..."
cargo install --locked --version 21.0.0 soroban-cli
cargo install --locked --version 21.1.0 soroban-cli
fi
fi
# Try both commands in case Homebrew installs it as 'stellar'
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ default-members = [
resolver = "2"

[dependencies]
soroban-sdk = "21.0.0"
soroban-sdk = "21.1.1"
remittance_split = { path = "./remittance_split" }
savings_goals = { path = "./savings_goals" }
bill_payments = { path = "./bill_payments" }
Expand All @@ -45,7 +45,7 @@ reporting = { path = "./reporting" }
orchestrator = { path = "./orchestrator" }

[dev-dependencies]
soroban-sdk = { version = "21.0.0", features = ["testutils"] }
soroban-sdk = { version = "21.1.1", features = ["testutils"] }
[profile.release]
opt-level = "z"
overflow-checks = true
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,21 @@ Manages family roles, spending controls, multisig approvals, and emergency trans

For full design details, see [docs/family-wallet-design.md](docs/family-wallet-design.md).

### Data Migration

Utilities for contract data export and import (JSON, Binary, CSV).

**Key Functions:**

- `export_to_csv`: Export savings goals to a tabular CSV format.
- `import_goals_from_csv`: Import goals from CSV with strict schema validation.
- `export_to_json` / `import_from_json`: Versioned snapshot migration.

**CSV Hardening:**
- Strict header validation (exact match and order).
- Row-level type and value validation (e.g., non-negative amounts).
- Rejection of ambiguous or malformed data.

## Events

All contracts emit events for important state changes, enabling real-time tracking and frontend integration. Events follow Soroban best practices and include:
Expand Down
4 changes: 2 additions & 2 deletions bill_payments/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = "21.0.0"
soroban-sdk = "21.1.1"
remitwise-common = { path = "../remitwise-common" }

[dev-dependencies]
proptest = "1.10.0"
soroban-sdk = { version = "21.0.0", features = ["testutils"] }
soroban-sdk = { version = "21.1.1", features = ["testutils"] }
testutils = { path = "../testutils" }


54 changes: 30 additions & 24 deletions bill_payments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@

use remitwise_common::{
clamp_limit, EventCategory, EventPriority, RemitwiseEvents, ARCHIVE_BUMP_AMOUNT,
ARCHIVE_LIFETIME_THRESHOLD, CONTRACT_VERSION, DEFAULT_PAGE_LIMIT, INSTANCE_BUMP_AMOUNT,
INSTANCE_LIFETIME_THRESHOLD, MAX_BATCH_SIZE, MAX_PAGE_LIMIT,
ARCHIVE_LIFETIME_THRESHOLD, CONTRACT_VERSION, INSTANCE_BUMP_AMOUNT,
INSTANCE_LIFETIME_THRESHOLD, MAX_BATCH_SIZE,
};

use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Map, String,
Symbol, Vec,
};

#[derive(Clone, Debug)]
#[contracttype]
#[derive(Clone, Debug)]
#[contracttype]
pub struct Bill {
pub id: u32,
pub owner: Address,
Expand Down Expand Up @@ -57,8 +55,6 @@ pub mod pause_functions {
pub const RESTORE: soroban_sdk::Symbol = symbol_short!("restore");
}

const CONTRACT_VERSION: u32 = 1;
const MAX_BATCH_SIZE: u32 = 50;
const STORAGE_UNPAID_TOTALS: Symbol = symbol_short!("UNPD_TOT");

#[contracterror]
Expand All @@ -77,14 +73,12 @@ pub enum Error {
BatchValidationFailed = 10,
InvalidLimit = 11,
InvalidDueDate = 12,
InvalidTag = 12,
EmptyTags = 13,
InvalidTag = 13,
EmptyTags = 14,
}

#[derive(Clone)]
#[contracttype]
#[derive(Clone)]
#[contracttype]
pub struct ArchivedBill {
pub id: u32,
pub owner: Address,
Expand All @@ -100,20 +94,25 @@ pub struct ArchivedBill {

/// Paginated result for archived bill queries
#[contracttype]
#[derive(Clone)]
pub struct ArchivedBillPage {
pub items: Vec<ArchivedBill>,
/// 0 means no more pages
pub next_cursor: u32,
pub count: u32,
}

#[contracttype]
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum BillEvent {
Created,
Paid,
Cancelled,
Archived,
Restored,
ExternalRefUpdated,
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct StorageStats {
pub active_bills: u32,
pub archived_bills: u32,
Expand Down Expand Up @@ -436,7 +435,7 @@ impl BillPayments {
};

let bill_owner = bill.owner.clone();
let bill_external_ref = bill.external_ref.clone();
let _bill_external_ref = bill.external_ref.clone();
bills.set(next_id, bill);
env.storage()
.instance()
Expand All @@ -447,9 +446,6 @@ impl BillPayments {
Self::adjust_unpaid_total(&env, &bill_owner, amount);

// Emit event for audit trail
env.events().publish(
(symbol_short!("bill"), BillEvent::Created),
(next_id, bill_owner, bill_external_ref),
RemitwiseEvents::emit(
&env,
EventCategory::State,
Expand Down Expand Up @@ -516,7 +512,7 @@ impl BillPayments {
.set(&symbol_short!("NEXT_ID"), &next_id);
}

let bill_external_ref = bill.external_ref.clone();
let _bill_external_ref = bill.external_ref.clone();
let paid_amount = bill.amount;
let was_recurring = bill.recurring;
bills.set(bill_id, bill);
Expand All @@ -528,9 +524,6 @@ impl BillPayments {
}

// Emit event for audit trail
env.events().publish(
(symbol_short!("bill"), BillEvent::Paid),
(bill_id, caller, bill_external_ref),
RemitwiseEvents::emit(
&env,
EventCategory::Transaction,
Expand Down Expand Up @@ -766,7 +759,18 @@ impl BillPayments {
///
/// # Returns
/// Vec of all Bill structs
pub fn get_all_bills(env: Env) -> Vec<Bill> {
pub fn get_all_bills_deprecated(env: Env) -> Vec<Bill> {
let bills: Map<u32, Bill> = env
.storage()
.instance()
.get(&symbol_short!("BILLS"))
.unwrap_or_else(|| Map::new(&env));
let mut result = Vec::new(&env);
for (_, bill) in bills.iter() {
result.push_back(bill);
}
result
}
// -----------------------------------------------------------------------
// Backward-compat helpers
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -986,6 +990,7 @@ impl BillPayments {
id: archived_bill.id,
owner: archived_bill.owner.clone(),
name: archived_bill.name.clone(),
external_ref: None, // Or logic to recover if stored elsewhere
amount: archived_bill.amount,
due_date: env.ledger().timestamp() + 2592000,
recurring: false,
Expand Down Expand Up @@ -1111,6 +1116,7 @@ impl BillPayments {
id: next_id,
owner: bill.owner.clone(),
name: bill.name.clone(),
external_ref: bill.external_ref.clone(),
amount: bill.amount,
due_date: next_due_date,
recurring: true,
Expand Down Expand Up @@ -1211,7 +1217,7 @@ impl BillPayments {
cursor: u32,
limit: u32,
) -> BillPage {
let limit = Self::clamp_limit(limit);
let limit = clamp_limit(limit);
let bills: Map<u32, Bill> = env
.storage()
.instance()
Expand Down Expand Up @@ -1245,7 +1251,7 @@ impl BillPayments {
cursor: u32,
limit: u32,
) -> BillPage {
let limit = Self::clamp_limit(limit);
let limit = clamp_limit(limit);
let bills: Map<u32, Bill> = env
.storage()
.instance()
Expand Down
21 changes: 16 additions & 5 deletions bill_payments/tests/stress_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ fn stress_200_bills_single_user() {

// Verify aggregate total
let total = client.get_total_unpaid(&owner);
assert_eq!(total, 200 * 100i128, "get_total_unpaid must sum all 200 bills");
assert_eq!(
total,
200 * 100i128,
"get_total_unpaid must sum all 200 bills"
);

// Exhaust all pages with MAX_PAGE_LIMIT (50) — should take exactly 4 pages
let mut collected = 0u32;
Expand Down Expand Up @@ -328,8 +332,14 @@ fn stress_archive_100_paid_bills() {

// Verify storage stats
let stats = client.get_storage_stats();
assert_eq!(stats.active_bills, 0, "No active bills should remain after full archive");
assert_eq!(stats.archived_bills, 100, "Storage stats must show 100 archived bills");
assert_eq!(
stats.active_bills, 0,
"No active bills should remain after full archive"
);
assert_eq!(
stats.archived_bills, 100,
"Storage stats must show 100 archived bills"
);

// Verify paginated access to archived bills
let mut archived_seen = 0u32;
Expand Down Expand Up @@ -487,8 +497,9 @@ fn bench_archive_paid_bills_100() {
client.pay_bill(&owner, &id);
}

let (cpu, mem, result) =
measure(&env, || client.archive_paid_bills(&owner, &2_000_000_000u64));
let (cpu, mem, result) = measure(&env, || {
client.archive_paid_bills(&owner, &2_000_000_000u64)
});
assert_eq!(result, 100);

println!(
Expand Down
Loading
Loading