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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ All manifests follow the [Contract Manifest Schema](contracts/contract-manifest-

```bash
# Validate manifests locally
npm install -g ajv-cli
./scripts/validate-manifests.sh
npm install -g ajv-cli ajv-formats
./contracts/scripts/validate-manifests.sh

# View contract information
jq '.contract_name, .version.current' contracts/bounty-escrow-manifest.json
Expand Down Expand Up @@ -342,10 +342,10 @@ You can also validate locally:

```bash
# Using the provided script
./scripts/validate-manifests.sh
./contracts/scripts/validate-manifests.sh

# Or manually with ajv
ajv validate -s contracts/contract-manifest-schema.json -d contracts/bounty-escrow-manifest.json
ajv validate --spec=draft2020 -c ajv-formats -s contracts/contract-manifest-schema.json -d contracts/bounty-escrow-manifest.json
```

#### Contributing
Expand Down
2 changes: 1 addition & 1 deletion contracts/bounty_escrow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ members = [
]

[workspace.dependencies]
soroban-sdk = "21.0.0"
soroban-sdk = "21.7.7"

[profile.release]
opt-level = "z"
Expand Down
2 changes: 1 addition & 1 deletion contracts/bounty_escrow/contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8270,4 +8270,4 @@ mod test_escrow_expiry;
#[cfg(test)]
mod test_max_counts;
#[cfg(test)]
mod test_recurring_locks;
mod test_recurring_locks;
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,25 @@ fn test_aggregate_stats_initial_state_is_zeroed() {
#[test]
fn test_query_pagination_boundary() {
let setup = TestEnv::new(); // Assuming your existing test helper
let (admin, depositor) = (setup.admin, setup.depositor);
let (admin, depositor) = (setup.admin, setup.depositor);

// Create 3 escrows
for i in 1..=3 {
setup.client.lock_funds(&depositor, &i, &1000, &2000);
}
for i in 1..=3 {
setup.client.lock_funds(&depositor, &i, &1000, &2000);
}

// Offset 1, Limit 1 should return exactly the 2nd escrow created
let results = setup.client.query_escrows_by_status(&EscrowStatus::Locked, &1, &1);
assert_eq!(results.len(), 1);
let results = setup
.client
.query_escrows_by_status(&EscrowStatus::Locked, &1, &1);
assert_eq!(results.len(), 1);

// Offset 3 (out of bounds) should return empty vector, not panic
let oob_results = setup.client.query_escrows_by_status(&EscrowStatus::Locked, &3, &1);
assert_eq!(oob_results.len(), 0);
}
let oob_results = setup
.client
.query_escrows_by_status(&EscrowStatus::Locked, &3, &1);
assert_eq!(oob_results.len(), 0);
}

#[test]
fn test_aggregate_stats_reflects_single_lock() {
Expand Down
13 changes: 8 additions & 5 deletions contracts/bounty_escrow/contracts/escrow/src/test_audit_trail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn test_audit_trail_disabled_by_default() {
let deadline = env.ledger().timestamp() + 3600;

client.lock_funds(&depositor, &1, &1000, &deadline);

let tail = client.get_audit_tail(&10);
assert_eq!(tail.len(), 0, "Audit log should be empty when disabled");
}
Expand All @@ -56,16 +56,19 @@ fn test_audit_trail_logs_actions_and_maintains_hash_chain() {

// Fetch the tail
let tail = client.get_audit_tail(&10);

assert_eq!(tail.len(), 2, "Should have 2 audit records");

let record_0 = tail.get(0).unwrap();
let record_1 = tail.get(1).unwrap();

assert_eq!(record_0.sequence, 0);
assert_eq!(record_1.sequence, 1);

// Integrity Check: Record 1's "previous_hash" MUST equal the computed hash of Record 0.
// In our implementation, the head_hash gets updated, so Record 1 inherently contains the hash of Record 0's state.
assert_ne!(record_0.previous_hash, record_1.previous_hash, "Hash chain must progress");
}
assert_ne!(
record_0.previous_hash, record_1.previous_hash,
"Hash chain must progress"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@
// before the call, and subsequent single-item or batch operations behave
// as if the failed call never happened.
//
// ## Error Code Semantics
//
// | Condition | Error code |
// |---------------------------------------|----------------------|
// | Batch size == 0 | InvalidBatchSize |
// | Batch size > MAX_BATCH_SIZE | InvalidBatchSize |
// | Same bounty_id twice in one batch | DuplicateBountyId |
// | bounty_id already in persistent store | BountyExists |
// | amount ≤ 0 | InvalidAmount |
// | bounty_id not found (release) | BountyNotFound |
// | escrow not in Locked status (release) | FundsNotLocked |
// | lock_paused flag set | FundsPaused |
// | release_paused flag set | FundsPaused |
// | contract not initialised | NotInitialized |
// | contract deprecated | ContractDeprecated |
//
// ## Coverage
//
// BATCH LOCK
Expand Down Expand Up @@ -59,12 +75,12 @@

use soroban_sdk::{
testutils::{Address as _, Ledger},
token, vec, Address, Env, Vec,
token, Address, Env, Vec,
};

use crate::{
BountyEscrowContract, BountyEscrowContractClient, DataKey, Error, Escrow, EscrowStatus,
LockFundsItem, ReleaseFundsItem,
BountyEscrowContract, BountyEscrowContractClient, DataKey, Error, EscrowStatus, LockFundsItem,
ReleaseFundsItem,
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -95,7 +111,8 @@ impl<'a> TestCtx<'a> {
let depositor = Address::generate(&env);
let contributor = Address::generate(&env);

let token_id = env.register_stellar_asset_contract(admin.clone());
let token_sac_contract = env.register_stellar_asset_contract_v2(admin.clone());
let token_id = token_sac_contract.address();
let token_sac = token::StellarAssetClient::new(&env, &token_id);

let contract_id = env.register_contract(None, BountyEscrowContract);
Expand Down Expand Up @@ -182,7 +199,7 @@ impl<'a> TestCtx<'a> {

/// Assert that bounty `id` exists and has status `status`.
fn assert_escrow_status(&self, id: u64, status: EscrowStatus) {
let escrow = self.client.get_escrow_info(&id);
let escrow = self.client.get_escrow(&id);
assert_eq!(
escrow.status, status,
"bounty {id} status mismatch: expected {status:?}"
Expand All @@ -204,7 +221,7 @@ fn batch_lock_empty_batch_is_rejected() {
let ctx = TestCtx::new();
let empty: soroban_sdk::Vec<LockFundsItem> = Vec::new(&ctx.env);
let result = ctx.client.try_batch_lock_funds(&empty);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidAmount);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidBatchSize);
}

#[test]
Expand Down Expand Up @@ -234,7 +251,7 @@ fn batch_lock_exceeds_max_batch_size_is_rejected() {
.mint(&ctx.depositor, &(AMOUNT * (MAX_BATCH as i128 + 1)));
let items = ctx.build_lock_batch(MAX_BATCH + 1);
let result = ctx.client.try_batch_lock_funds(&items);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidAmount);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidBatchSize);
}

// ===========================================================================
Expand All @@ -249,7 +266,9 @@ fn batch_lock_duplicate_bounty_id_in_batch_is_rejected() {
items.push_back(ctx.lock_item(2));
items.push_back(ctx.lock_item(1)); // duplicate
let result = ctx.client.try_batch_lock_funds(&items);
assert_eq!(result.unwrap_err().unwrap(), Error::BountyExists);
// Within-batch duplicate returns DuplicateBountyId (distinct from a
// pre-existing storage entry which returns BountyExists).
assert_eq!(result.unwrap_err().unwrap(), Error::DuplicateBountyId);
}

#[test]
Expand Down Expand Up @@ -390,7 +409,7 @@ fn batch_lock_last_item_duplicate_causes_full_rollback() {
items.push_back(ctx.lock_item(1)); // duplicate of first, placed last

let result = ctx.client.try_batch_lock_funds(&items);
assert_eq!(result.unwrap_err().unwrap(), Error::BountyExists);
assert_eq!(result.unwrap_err().unwrap(), Error::DuplicateBountyId);

for id in [1u64, 2] {
ctx.assert_no_escrow(id);
Expand Down Expand Up @@ -428,7 +447,7 @@ fn batch_release_empty_batch_is_rejected() {
let ctx = TestCtx::new();
let empty: soroban_sdk::Vec<ReleaseFundsItem> = Vec::new(&ctx.env);
let result = ctx.client.try_batch_release_funds(&empty);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidAmount);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidBatchSize);
}

#[test]
Expand Down Expand Up @@ -460,7 +479,7 @@ fn batch_release_exceeds_max_batch_size_is_rejected() {
ctx.lock_n(MAX_BATCH as u64 + 1);
let items = ctx.build_release_batch(MAX_BATCH + 1);
let result = ctx.client.try_batch_release_funds(&items);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidAmount);
assert_eq!(result.unwrap_err().unwrap(), Error::InvalidBatchSize);
}

// ===========================================================================
Expand All @@ -479,7 +498,7 @@ fn batch_release_duplicate_bounty_id_in_batch_is_rejected() {
items.push_back(ctx.release_item(1)); // duplicate

let result = ctx.client.try_batch_release_funds(&items);
assert_eq!(result.unwrap_err().unwrap(), Error::BountyExists);
assert_eq!(result.unwrap_err().unwrap(), Error::DuplicateBountyId);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,22 @@
// written, no token transfer is made, and every "sibling" row in the
// same batch is left completely unaffected.
//
// This file exercises that guarantee from a second, independent angle:
// it uses a functional-style setup helper instead of the `TestCtx` struct
// found in `test_batch_failure_mode.rs`, providing complementary coverage
// with a different test harness.
// ## Error code contract
//
// | Condition | Error |
// |----------------------------------------|-------------------|
// | batch size == 0 | InvalidBatchSize |
// | batch size > MAX_BATCH_SIZE | InvalidBatchSize |
// | same bounty_id twice within this batch | DuplicateBountyId |
// | bounty_id already in persistent store | BountyExists |
// | amount ≤ 0 | InvalidAmount |
// | bounty_id missing (release) | BountyNotFound |
// | escrow not Locked (release) | FundsNotLocked |
// | contract not initialised | NotInitialized |
//
// This file uses a functional-style setup helper instead of the `TestCtx`
// struct found in `test_batch_failure_mode.rs`, providing complementary
// coverage with a different test harness.
//
// ## Coverage (this file)
//
Expand Down Expand Up @@ -84,22 +96,8 @@ fn setup() -> Ctx<'static> {

let admin = Address::generate(&env);
let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract(token_admin.clone());
/// Convenience: build a single `LockFundsItem`.
fn lock_item(
env: &Env,
bounty_id: u64,
depositor: Address,
amount: i128,
deadline: u64,
) -> LockFundsItem {
LockFundsItem {
bounty_id,
depositor,
amount,
deadline,
}
}
let token_sac_contract = env.register_stellar_asset_contract_v2(token_admin.clone());
let token_id = token_sac_contract.address();

let contract_id = env.register_contract(None, BountyEscrowContract);
let client = BountyEscrowContractClient::new(&env, &contract_id);
Expand Down Expand Up @@ -235,6 +233,8 @@ fn batch_lock_duplicate_bounty_id_within_batch_fails() {
.try_batch_lock_funds(&items)
.unwrap_err()
.unwrap(),
// Within-batch duplicate is distinct from a pre-existing storage entry;
// it returns DuplicateBountyId rather than BountyExists.
Error::DuplicateBountyId
);
}
Expand Down Expand Up @@ -283,13 +283,13 @@ fn batch_lock_invalid_second_item_rolls_back_first_sibling() {
.try_batch_lock_funds(&items)
.unwrap_err()
.unwrap(),
// Zero amount returns InvalidAmount, not ActionNotFound.
Error::InvalidAmount
);

// Sibling bounty 1 must NOT have been committed
assert_eq!(
ctx.client.try_get_escrow_info(&1).unwrap_err().unwrap(),
Error::BountyNotFound,
// Sibling bounty 1 must NOT have been committed.
assert!(
ctx.client.try_get_escrow(&1).is_err(),
"sibling bounty 1 must not be stored when a later item fails"
);
}
Expand Down Expand Up @@ -319,14 +319,12 @@ fn batch_lock_duplicate_last_item_rolls_back_all_previous_siblings() {
Error::DuplicateBountyId
);

assert_eq!(
ctx.client.try_get_escrow_info(&10).unwrap_err().unwrap(),
Error::BountyNotFound,
assert!(
ctx.client.try_get_escrow(&10).is_err(),
"sibling bounty 10 must not be stored"
);
assert_eq!(
ctx.client.try_get_escrow_info(&11).unwrap_err().unwrap(),
Error::BountyNotFound,
assert!(
ctx.client.try_get_escrow(&11).is_err(),
"sibling bounty 11 must not be stored"
);
}
Expand Down Expand Up @@ -519,6 +517,8 @@ fn batch_release_duplicate_bounty_id_within_batch_fails() {
.try_batch_release_funds(&items)
.unwrap_err()
.unwrap(),
// Within-batch duplicate returns DuplicateBountyId (not BountyExists,
// which is reserved for a bounty_id already present in storage).
Error::DuplicateBountyId
);
}
Expand Down Expand Up @@ -579,7 +579,7 @@ fn batch_release_nonexistent_second_item_rolls_back_first_sibling() {
);

assert_eq!(
ctx.client.get_escrow_info(&1).status,
ctx.client.get_escrow(&1).status,
crate::EscrowStatus::Locked,
"sibling bounty 1 must remain Locked after its neighbour caused a rollback"
);
Expand Down Expand Up @@ -659,7 +659,7 @@ fn batch_release_mixed_locked_and_refunded_is_atomic() {
);

assert_eq!(
ctx.client.get_escrow_info(&20).status,
ctx.client.get_escrow(&20).status,
crate::EscrowStatus::Locked,
"locked sibling must not be released when a refunded sibling fails"
);
Expand Down Expand Up @@ -732,7 +732,7 @@ fn batch_release_partial_failure_leaves_all_siblings_locked() {
);

assert_eq!(
ctx.client.get_escrow_info(&32).status,
ctx.client.get_escrow(&32).status,
crate::EscrowStatus::Locked,
"bounty 32 must remain Locked; its sibling's failure must not release it"
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,7 @@ fn test_issue_and_use_release_capability() {
&setup.delegate,
&capability_id,
);
assert_eq!(
too_large.unwrap_err().unwrap(),
Error::CapAmountExceeded
);
assert_eq!(too_large.unwrap_err().unwrap(), Error::CapAmountExceeded);
}

#[test]
Expand Down
Loading
Loading