diff --git a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs index bfa6c12..fb86c93 100644 --- a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs @@ -35,6 +35,22 @@ pub enum DataKey { #[contract] pub struct ComplianceContract; +#[cfg(test)] +mod version_tests { + use super::*; + + #[test] + fn test_version_returns_package_version() { + let env = Env::default(); + let contract_id = env.register(ComplianceContract, ()); + let client = ComplianceContractClient::new(&env, &contract_id); + assert_eq!( + client.version(), + String::from_str(&env, env!("CARGO_PKG_VERSION")) + ); + } +} + fn is_paused(e: &Env) -> bool { e.storage() .instance() diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs b/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs index 7b39614..5d53ed9 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs @@ -157,8 +157,69 @@ fn test_resolve_dispute_callable_by_signer() { client.raise_dispute(&merchant, &settlement_id, &1u32); client.resolve_dispute(&signer_a, &settlement_id, &true); + assert_eq!( + client.get_settlement(&settlement_id).unwrap().status, + SettlementStatus::Pending + ); + + let events = env.events().all(); + let (_, topics, data) = events.get(events.len() - 1).unwrap(); + assert_eq!(topics.get(0).unwrap(), Symbol::new(&env, "dispute_resolved").into_val(&env)); + assert_eq!(data, (settlement_id, true, 0u64).into_val(&env)); +} + +#[test] +fn test_resolve_dispute_against_merchant_cancels_settlement() { + let (env, contract_id) = setup_env(); + let client = make_client(&env, &contract_id); + let admin = Address::generate(&env); + let signer = Address::generate(&env); + let token = Address::generate(&env); + let merchant = Address::generate(&env); + client.initialize(&vec![&env, (signer.clone(), 1u64)], &1u64, &admin); + let settlement_id = client.propose_settlement(&signer, &token, &5_000_000u64, &merchant); + client.raise_dispute(&merchant, &settlement_id, &1u32); + + client.resolve_dispute(&signer, &settlement_id, &false); + + assert_eq!( + client.get_settlement(&settlement_id).unwrap().status, + SettlementStatus::Cancelled + ); + let events = env.events().all(); + let (_, _, data) = events.get(events.len() - 1).unwrap(); + assert_eq!(data, (settlement_id, false, 0u64).into_val(&env)); +} - client.resolve_dispute(&signer_b, &settlement_id, &true); +#[test] +fn test_resolve_non_disputed_settlement_returns_error() { + let (env, contract_id) = setup_env(); + let client = make_client(&env, &contract_id); + let admin = Address::generate(&env); + let signer = Address::generate(&env); + let token = Address::generate(&env); + let merchant = Address::generate(&env); + client.initialize(&vec![&env, (signer.clone(), 1u64)], &1u64, &admin); + let settlement_id = client.propose_settlement(&signer, &token, &5_000_000u64, &merchant); + + assert_eq!( + client.try_resolve_dispute(&signer, &settlement_id, &true), + Err(Ok(TreasuryError::NotDisputed)) + ); + assert_eq!( + client.get_settlement(&settlement_id).unwrap().status, + SettlementStatus::Pending + ); +} + +#[test] +fn test_treasury_version_returns_package_version() { + let (env, contract_id) = setup_env(); + let client = make_client(&env, &contract_id); + assert_eq!( + client.version(), + soroban_sdk::String::from_str(&env, env!("CARGO_PKG_VERSION")) + ); } #[test] diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs b/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs index e75df51..5d1e097 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs @@ -940,19 +940,39 @@ impl TreasuryContract { /// # Arguments /// * `e` - Soroban environment handle. /// * `signer` - Authorized signer address resolving the dispute (must authenticate). - /// * `_settlement_id` - ID of the disputed settlement. - /// * `_resolve_in_favor` - Resolution outcome decision flag. + /// * `settlement_id` - ID of the disputed settlement. + /// * `resolve_in_favor` - Whether the outcome favors the merchant. /// /// # Errors /// * Returns [`TreasuryError::ContractPaused`] if contract is paused. + /// * Returns [`TreasuryError::NotDisputed`] if the settlement is not on hold. pub fn resolve_dispute( e: Env, signer: Address, - _settlement_id: u64, - _resolve_in_favor: bool, + settlement_id: u64, + resolve_in_favor: bool, ) -> Result<(), TreasuryError> { check_not_paused(&e)?; signer.require_auth(); + let mut settlement = Self::get_settlement_internal(&e, settlement_id); + if settlement.status != SettlementStatus::OnHold { + return Err(TreasuryError::NotDisputed); + } + let resolution_weight = settlement.approval_weight; + settlement.status = if resolve_in_favor { + SettlementStatus::Pending + } else { + SettlementStatus::Cancelled + }; + e.storage() + .instance() + .set(&DataKey::Settlement(settlement_id), &settlement); + crate::events::dispute_resolved( + &e, + &settlement_id, + &resolve_in_favor, + &resolution_weight, + ); Ok(()) } diff --git a/comebackhere-backend/src/schemas/index.ts b/comebackhere-backend/src/schemas/index.ts index 971c34b..762ad4d 100644 --- a/comebackhere-backend/src/schemas/index.ts +++ b/comebackhere-backend/src/schemas/index.ts @@ -1,6 +1,8 @@ import { z } from "zod" import { Keypair } from "stellar-sdk" +const MAX_REFERENCE_LEN_BYTES = 64 + function isValidStellarAddress(addr: string): boolean { try { Keypair.fromPublicKey(addr) @@ -35,7 +37,10 @@ export const createInvoiceSchema = z.object({ due_date: futureTimestamp, reference: z .string() - .refine((val) => Buffer.byteLength(val, "utf8") <= 64, "reference must not exceed 64 bytes") + .refine( + (val) => Buffer.byteLength(val, "utf8") <= MAX_REFERENCE_LEN_BYTES, + `reference must not exceed ${MAX_REFERENCE_LEN_BYTES} bytes`, + ) .optional(), }) diff --git a/docs/api-reference.md b/docs/api-reference.md index 1c19c5d..eced157 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -246,6 +246,7 @@ Create a new invoice by submitting `create_invoice` to the Soroban RPC. | `token` | string | Token identifier | | `amount` | number | Positive number (in stroops / smallest unit) | | `due_date` | number | Future Unix timestamp (seconds) for the due date | +| `reference` | string | Optional invoice reference, limited to 64 UTF-8 bytes | The merchant identity is taken from the API key, never from the request body. Include `Authorization: Bearer `. diff --git a/scripts/verify.sh b/scripts/verify.sh index 635cd4d..90d8a74 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -13,6 +13,8 @@ # INVOICE_CONTRACT_ID — deployed invoice contract ID (C…) # TREASURY_CONTRACT_ID — deployed treasury contract ID (C…) # COMPLIANCE_CONTRACT_ID — deployed compliance contract ID (C…) +# SOROBAN_NETWORK_PASSPHRASE — network passphrase for contract invocation +# SOROBAN_SOURCE_ACCOUNT — public G-address used to simulate read-only calls # # Optional: # CONTRACTS_DIR — path to built WASM artifacts @@ -66,12 +68,15 @@ fi : "${TREASURY_CONTRACT_ID:?TREASURY_CONTRACT_ID is required for WASM verification}" : "${COMPLIANCE_CONTRACT_ID:?COMPLIANCE_CONTRACT_ID is required for WASM verification}" +: "${SOROBAN_NETWORK_PASSPHRASE:?SOROBAN_NETWORK_PASSPHRASE is required to read contract versions}" +: "${SOROBAN_SOURCE_ACCOUNT:?SOROBAN_SOURCE_ACCOUNT is required to simulate version getters}" CONTRACTS_DIR="${CONTRACTS_DIR:-$ROOT_DIR/../COMEBACKHERE-contracts/target/wasm32-unknown-unknown/release}" FAIL=0 MISMATCHES=() CHECKS=() # For JSON output: array of check results +VERSIONS=() # For JSON output: deployed contract versions verify_contract() { local name="$1" @@ -134,6 +139,25 @@ print(hashlib.sha256(wasm_bytes).hexdigest()) fi } +print_contract_version() { + local name="$1" + local contract_id="$2" + local version + + version=$(stellar contract invoke \ + --id "$contract_id" \ + --rpc-url "$SOROBAN_RPC_URL" \ + --network-passphrase "$SOROBAN_NETWORK_PASSPHRASE" \ + --source-account "$SOROBAN_SOURCE_ACCOUNT" \ + --send no \ + -- version) + + if [[ "$OUTPUT_FORMAT" == "text" ]]; then + echo " $name version: $version" + fi + VERSIONS+=("{\"contract\":\"$name\",\"version\":\"$version\"}") +} + echo "" echo "Verifying deployed WASM hashes against local build artifacts…" @@ -149,6 +173,10 @@ verify_contract "compliance" \ "$COMPLIANCE_CONTRACT_ID" \ "$CONTRACTS_DIR/comebackhere_compliance.wasm" +print_contract_version "invoice" "$INVOICE_CONTRACT_ID" +print_contract_version "treasury" "$TREASURY_CONTRACT_ID" +print_contract_version "compliance" "$COMPLIANCE_CONTRACT_ID" + if [[ "$OUTPUT_FORMAT" == "json" ]]; then # Output JSON result echo "{" @@ -157,6 +185,11 @@ if [[ "$OUTPUT_FORMAT" == "json" ]]; then for i in "${!CHECKS[@]}"; do echo " ${CHECKS[$i]}$([ $i -lt $((${#CHECKS[@]} - 1)) ] && echo ',' || echo '')" done + echo " ]," + echo " \"versions\": [" + for i in "${!VERSIONS[@]}"; do + echo " ${VERSIONS[$i]}$([ $i -lt $((${#VERSIONS[@]} - 1)) ] && echo ',' || echo '')" + done echo " ]" echo "}" else