From 2b8eec45ba258e8891f07e83d5287df77708a908 Mon Sep 17 00:00:00 2001 From: maxiafolabii Date: Sat, 26 Sep 2026 19:19:53 +0000 Subject: [PATCH] fix: address issues 680 through 683 --- .../contracts/compliance/src/lib.rs | 22 +- .../contracts/invoice/src/lib.rs | 225 ++++++++++++++++++ .../src/integration_dispute_lifecycle.rs | 65 ++++- .../contracts/treasury/src/lib.rs | 36 ++- abis/compliance.json | 1 + abis/invoice.json | 1 + abis/treasury.json | 4 +- comebackhere-backend/src/schemas/index.ts | 7 +- docs/api-reference.md | 1 + docs/error-codes.md | 3 + scripts/verify.sh | 33 +++ 11 files changed, 388 insertions(+), 10 deletions(-) diff --git a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs index d213625..646edd4 100644 --- a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, Symbol, Vec}; +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, String, Symbol, Vec}; #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -33,6 +33,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() @@ -61,6 +77,10 @@ const MAX_BATCH_SIZE: u32 = 50; #[contractimpl] impl ComplianceContract { + pub fn version(e: Env) -> String { + String::from_str(&e, env!("CARGO_PKG_VERSION")) + } + pub fn initialize(e: Env, admin: Address) { e.storage().instance().set(&DataKey::Admin, &admin); e.storage().instance().set(&DataKey::Paused, &false); diff --git a/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs b/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs index 614d0c3..7fd3036 100644 --- a/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs @@ -38,6 +38,8 @@ pub enum ContractError { /// (e.g. `mark_paids` called on an invoice that is `RefundRequested`, /// `Released`, `Cancelled`, or `Expired`). InvalidStateTransition = 18, + AmountPrecision = 19, + ReferenceTooLong = 20, } #[contracttype] @@ -116,6 +118,10 @@ pub struct InvoiceContract; #[contractimpl] impl InvoiceContract { + pub fn version(env: Env) -> String { + String::from_str(&env, env!("CARGO_PKG_VERSION")) + } + /// Initialises the contract, setting the admin address and default configuration. /// /// # Parameters @@ -767,6 +773,225 @@ mod tests { (env, contract_id, admin) } + #[test] + fn test_version_returns_package_version() { + let (env, contract_id, _) = setup_contract(1000); + let client = InvoiceContractClient::new(&env, &contract_id); + assert_eq!(client.version(), String::from_str(&env, env!("CARGO_PKG_VERSION"))); + } + + #[test] + fn test_reference_length_limit_is_enforced_in_bytes() { + let (env, contract_id, _) = setup_contract(1000); + let client = InvoiceContractClient::new(&env, &contract_id); + let merchant = Address::generate(&env); + let customer = Address::generate(&env); + let token = Address::generate(&env); + let valid_text = std::string::String::from("x").repeat(MAX_REFERENCE_LEN as usize); + let valid_reference = Some(String::from_str(&env, &valid_text)); + client.create_invoice( + &merchant, + &customer, + &10_000_000i128, + &token, + &5000, + &1, + &valid_reference, + ); + + let long_text = std::string::String::from("x").repeat(MAX_REFERENCE_LEN as usize + 1); + let long_reference = Some(String::from_str(&env, &long_text)); + assert_eq!( + client.try_create_invoice( + &merchant, + &customer, + &10_000_000i128, + &token, + &5000, + &2, + &long_reference, + ), + Err(Ok(ContractError::ReferenceTooLong)) + ); + } + + // Keep this transition matrix aligned with the diagram in ARCHITECTURE.md. + #[test] + fn test_state_machine_transition_matrix() { + #[derive(Clone, Copy)] + enum Action { + MarkPaid, + Cancel, + RequestRefund, + ReleaseEscrow, + Expire, + } + + let statuses = [ + InvoiceStatus::Pending, + InvoiceStatus::Paid, + InvoiceStatus::Expired, + InvoiceStatus::Cancelled, + InvoiceStatus::RefundRequested, + InvoiceStatus::Released, + ]; + let actions = [ + Action::MarkPaid, + Action::Cancel, + Action::RequestRefund, + Action::ReleaseEscrow, + Action::Expire, + ]; + + for initial_status in statuses { + for action in actions { + let (env, contract_id, admin) = setup_contract(1000); + let client = InvoiceContractClient::new(&env, &contract_id); + let merchant = Address::generate(&env); + let customer = Address::generate(&env); + let token = Address::generate(&env); + let invoice_id = client.create_invoice( + &merchant, + &customer, + &10_000_000i128, + &token, + &2000, + &1, + &None, + ); + + match initial_status.clone() { + InvoiceStatus::Pending => {} + InvoiceStatus::Paid => { + client.mark_paids(&soroban_sdk::vec![&env, invoice_id]); + } + InvoiceStatus::Expired => { + env.ledger().set_timestamp(2000); + client.batch_expire(&soroban_sdk::vec![&env, invoice_id]); + } + InvoiceStatus::Cancelled => client.cancel_invoiced(&invoice_id, &merchant), + InvoiceStatus::RefundRequested => { + client.mark_paids(&soroban_sdk::vec![&env, invoice_id]); + client.request_refund(&invoice_id, &customer); + } + InvoiceStatus::Released => { + client.mark_paids(&soroban_sdk::vec![&env, invoice_id]); + client.request_refund(&invoice_id, &customer); + client.set_grace_window(&admin, &0); + client.release_escrow(&invoice_id, &merchant); + } + } + + let expected_error = match (initial_status.clone(), action) { + (InvoiceStatus::Pending, Action::MarkPaid) + | (InvoiceStatus::Pending, Action::Cancel) + | (InvoiceStatus::Paid, Action::Cancel) + | (InvoiceStatus::Paid, Action::RequestRefund) + | (InvoiceStatus::RefundRequested, Action::ReleaseEscrow) => None, + (InvoiceStatus::Paid, Action::MarkPaid) => { + Some(ContractError::InvoiceAlreadyPaid) + } + (InvoiceStatus::Pending, Action::RequestRefund) + | (InvoiceStatus::Expired, Action::RequestRefund) + | (InvoiceStatus::Cancelled, Action::RequestRefund) + | (InvoiceStatus::RefundRequested, Action::RequestRefund) + | (InvoiceStatus::Released, Action::RequestRefund) => { + Some(ContractError::InvoiceNotFound) + } + (InvoiceStatus::RefundRequested, Action::Cancel) => { + Some(ContractError::AlreadyRefundRequested) + } + (InvoiceStatus::Pending, Action::ReleaseEscrow) + | (InvoiceStatus::Paid, Action::ReleaseEscrow) + | (InvoiceStatus::Expired, Action::ReleaseEscrow) + | (InvoiceStatus::Cancelled, Action::ReleaseEscrow) + | (InvoiceStatus::Released, Action::ReleaseEscrow) => { + Some(ContractError::RefundNotRequested) + } + (InvoiceStatus::Expired, Action::Cancel) + | (InvoiceStatus::Cancelled, Action::Cancel) + | (InvoiceStatus::Released, Action::Cancel) => { + Some(ContractError::InvoiceCancelled) + } + (InvoiceStatus::RefundRequested, Action::MarkPaid) + | (InvoiceStatus::Expired, Action::MarkPaid) + | (InvoiceStatus::Cancelled, Action::MarkPaid) + | (InvoiceStatus::Released, Action::MarkPaid) => { + Some(ContractError::InvalidStateTransition) + } + (_, Action::Expire) => None, + }; + + client.set_grace_window(&admin, &0); + if matches!(action, Action::Expire) { + env.ledger().set_timestamp(2000); + } + + match action { + Action::MarkPaid => match expected_error { + Some(error) => assert_eq!( + client.try_mark_paids(&soroban_sdk::vec![&env, invoice_id]), + Err(Ok(error)) + ), + None => assert_eq!( + client.try_mark_paids(&soroban_sdk::vec![&env, invoice_id]), + Ok(()) + ), + }, + Action::Cancel => match expected_error { + Some(error) => assert_eq!( + client.try_cancel_invoiced(&invoice_id, &merchant), + Err(Ok(error)) + ), + None => assert_eq!( + client.try_cancel_invoiced(&invoice_id, &merchant), + Ok(()) + ), + }, + Action::RequestRefund => match expected_error { + Some(error) => assert_eq!( + client.try_request_refund(&invoice_id, &customer), + Err(Ok(error)) + ), + None => assert_eq!( + client.try_request_refund(&invoice_id, &customer), + Ok(()) + ), + }, + Action::ReleaseEscrow => match expected_error { + Some(error) => assert_eq!( + client.try_release_escrow(&invoice_id, &merchant), + Err(Ok(error)) + ), + None => assert_eq!( + client.try_release_escrow(&invoice_id, &merchant), + Ok(()) + ), + }, + Action::Expire => assert_eq!( + client.try_batch_expire(&soroban_sdk::vec![&env, invoice_id]), + Ok(()) + ), + } + + let expected_status = match (initial_status.clone(), action) { + (InvoiceStatus::Pending, Action::MarkPaid) => InvoiceStatus::Paid, + (InvoiceStatus::Pending, Action::Cancel) => InvoiceStatus::Cancelled, + (InvoiceStatus::Pending, Action::Expire) => InvoiceStatus::Expired, + (InvoiceStatus::Paid, Action::Cancel) + | (InvoiceStatus::Paid, Action::RequestRefund) => { + InvoiceStatus::RefundRequested + } + (InvoiceStatus::RefundRequested, Action::ReleaseEscrow) => { + InvoiceStatus::Released + } + (status, _) => status, + }; + assert_eq!(client.get_invoice(&invoice_id).status, expected_status); + } + } + } + #[test] fn test_create_invoice_with_unique_nonce_succeeds() { let (env, cid, _admin) = setup_contract(1000); diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs b/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs index 9284ab9..3f66365 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/integration_dispute_lifecycle.rs @@ -1,7 +1,7 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::Address as _, vec, Address, Env}; +use soroban_sdk::{testutils::Address as _, vec, Address, Env, IntoVal, Symbol}; fn setup_env() -> (Env, Address) { let env = Env::default(); @@ -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_b, &settlement_id, &true); + 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)); +} + +#[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 7d276e1..f7b5a6e 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs @@ -4,7 +4,7 @@ extern crate std; use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, Address, Env, IntoVal, Symbol, Vec, + contract, contracterror, contractimpl, contracttype, Address, Env, IntoVal, String, Symbol, Vec, }; /// Status of a settlement proposal within the Treasury contract. @@ -83,6 +83,8 @@ pub enum TreasuryError { /// `daily_withdraw_limit` and the withdrawal would push cumulative /// withdrawals for the current 24h window above that limit. DailyLimitExceeded = 13, + /// `resolve_dispute` was called for a settlement that is not on hold. + NotDisputed = 14, } /// Storage keys for Treasury contract instance state. @@ -133,6 +135,10 @@ pub struct TreasuryContract; #[contractimpl] impl TreasuryContract { + pub fn version(e: Env) -> String { + String::from_str(&e, env!("CARGO_PKG_VERSION")) + } + pub fn initialize( e: Env, signers: Vec<(Address, u64)>, @@ -711,19 +717,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/abis/compliance.json b/abis/compliance.json index 2cb4d50..ab1a43e 100644 --- a/abis/compliance.json +++ b/abis/compliance.json @@ -2,6 +2,7 @@ "contract": "compliance", "version": "1.0.0", "functions": [ + "version", "initialize", "is_allowed", "get_address_status", diff --git a/abis/invoice.json b/abis/invoice.json index f289192..7ec212b 100644 --- a/abis/invoice.json +++ b/abis/invoice.json @@ -2,6 +2,7 @@ "contract": "invoice", "version": "1.1.0", "functions": [ + "version", "initialize", "create_invoice", "get_invoice", diff --git a/abis/treasury.json b/abis/treasury.json index e24e1aa..be79f63 100644 --- a/abis/treasury.json +++ b/abis/treasury.json @@ -2,6 +2,7 @@ "contract": "treasury", "version": "1.1.0", "functions": [ + "version", "initialize", "set_signer", "propose_settlement", @@ -31,6 +32,7 @@ "6": "DisputeNotRaised", "7": "AlreadyVoted", "8": "UnauthorizedSigner", - "9": "ThresholdNotMet" + "9": "ThresholdNotMet", + "14": "NotDisputed" } } diff --git a/comebackhere-backend/src/schemas/index.ts b/comebackhere-backend/src/schemas/index.ts index 9ee5257..8d11b23 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) @@ -39,7 +41,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 08f3468..8908fb5 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -126,6 +126,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 | **Response `201`** diff --git a/docs/error-codes.md b/docs/error-codes.md index 0f707aa..c91760b 100644 --- a/docs/error-codes.md +++ b/docs/error-codes.md @@ -53,6 +53,8 @@ Defined in `COMEBACKHERE-contracts/contracts/invoice/src/lib.rs`. Shares some va | 16 | `Overflow` | An internal counter (invoice ID, or `created_at + grace_window`) would overflow `u64`. | Practically unreachable outside of adversarial ledger state; not user-actionable. | | 17 | `AddressBlocked` | `mark_paids` was called for a customer that the configured compliance contract reports as not allowed. | Confirm the customer's compliance status with `ComplianceContract.is_allowed` before retrying. | | 18 | `InvalidStateTransition` | `mark_paids` was called on an invoice in `RefundRequested`, `Released`, `Cancelled`, or `Expired` status — see [ARCHITECTURE.md § Invoice state machine](../ARCHITECTURE.md#invoice-state-machine) for the full legal-transition diagram. | Fetch the current status with `get_invoice_status` first. A refund already in progress must not be overridden by a stale payment confirmation. | +| 19 | `AmountPrecision` | `create_invoice` was called with an amount below the minimum supported amount. | Use an amount of at least 10,000,000 stroops. | +| 20 | `ReferenceTooLong` | `create_invoice` was called with a `reference` longer than 64 UTF-8 bytes. | Shorten the reference to 64 bytes or fewer. | --- @@ -121,6 +123,7 @@ Defined in `COMEBACKHERE-contracts/contracts/treasury/src/lib.rs`. | 6 | `InvalidThreshold` | `update_threshold` was called with a threshold of 0. | Pass a positive `u32` threshold; the multi-sig cannot function with zero required weight. | | 7 | `DuplicateSigner` | `initialize` was called with the same signer address appearing more than once in the `signers` list. | Ensure every `(address, weight)` pair in the `signers` vector is unique before calling `initialize`. | | 8 | `InvalidWeightSum` | `initialize` was called with a `threshold` greater than the sum of all signer weights. | Lower the threshold or add signers with sufficient weight so that `sum(weights) ≥ threshold`. | +| 14 | `NotDisputed` | `resolve_dispute` was called for a settlement that is not in `OnHold` status. | Raise a dispute on the settlement before resolving it. | --- 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