From 30a807a98e4af9106fabef25eb5bd32b98ed2e6f Mon Sep 17 00:00:00 2001 From: sadeeq6400 Date: Fri, 29 May 2026 09:23:44 +0100 Subject: [PATCH 1/2] Complete insurance claim workflow in assetsup/insurance.rs in opsce crate --- contracts/opsce/Cargo.toml | 14 ++ contracts/opsce/src/insurance_claim.rs | 184 ++++++++++++++++++++ contracts/opsce/src/insurance_claim_test.rs | 104 +++++++++++ contracts/opsce/src/lib.rs | 5 + 4 files changed, 307 insertions(+) create mode 100644 contracts/opsce/Cargo.toml create mode 100644 contracts/opsce/src/insurance_claim.rs create mode 100644 contracts/opsce/src/insurance_claim_test.rs create mode 100644 contracts/opsce/src/lib.rs diff --git a/contracts/opsce/Cargo.toml b/contracts/opsce/Cargo.toml new file mode 100644 index 00000000..7108921c --- /dev/null +++ b/contracts/opsce/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "opsce" +version = "0.1.0" +edition = "2021" + +t[lib] +crate-type = ["lib", "cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/opsce/src/insurance_claim.rs b/contracts/opsce/src/insurance_claim.rs new file mode 100644 index 00000000..4d418bde --- /dev/null +++ b/contracts/opsce/src/insurance_claim.rs @@ -0,0 +1,184 @@ +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, symbol_short, Address, BytesN, Env, String, Vec}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ClaimStatus { + Pending, + Approved, + Rejected, + Paid, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct InsuranceClaim { + pub claim_id: BytesN<32>, + pub asset_id: BytesN<32>, + pub policy_id: BytesN<32>, + pub claimant: Address, + pub amount: i128, + pub description: String, + pub status: ClaimStatus, + pub payout_amount: i128, + pub filed_at: u64, +} + +#[contracttype] +pub enum DataKey { + Admin, + Claim(BytesN<32>), + AssetClaims(BytesN<32>), +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum ContractError { + ClaimNotFound = 1, + Unauthorized = 2, + InvalidClaimAmount = 3, + ClaimAlreadyExists = 4, + InvalidClaimStatus = 5, +} + +#[contract] +pub struct InsuranceClaimContract; + +#[contractimpl] +impl InsuranceClaimContract { + pub fn init(env: Env, admin: Address) { + let store = env.storage().persistent(); + if store.has(&DataKey::Admin) { + panic!("contract already initialized"); + } + admin.require_auth(); + store.set(&DataKey::Admin, &admin); + } + + pub fn file_insurance_claim( + env: Env, + asset_id: BytesN<32>, + policy_id: BytesN<32>, + claim_amount: i128, + description: String, + ) -> Result, ContractError> { + let claimant = env.invoker(); + claimant.require_auth(); + + if claim_amount <= 0 { + return Err(ContractError::InvalidClaimAmount); + } + + let store = env.storage().persistent(); + let mut claim_id_bytes = env.crypto().sha256(&policy_id.clone().into()); + let mut claim_id: BytesN<32> = claim_id_bytes.into(); + let mut attempt = 0u32; + while store.has(&DataKey::Claim(claim_id.clone())) { + attempt = attempt.checked_add(1).unwrap(); + if attempt > 16 { + panic!("unable to generate unique claim id"); + } + claim_id_bytes = env.crypto().sha256(&claim_id.clone().into()); + claim_id = claim_id_bytes.into(); + } + + let claim = InsuranceClaim { + claim_id: claim_id.clone(), + asset_id: asset_id.clone(), + policy_id: policy_id.clone(), + claimant: claimant.clone(), + amount: claim_amount, + description: description.clone(), + status: ClaimStatus::Pending, + payout_amount: 0, + filed_at: env.ledger().timestamp(), + }; + + store.set(&DataKey::Claim(claim_id.clone()), &claim); + + let mut asset_claims: Vec> = store + .get(&DataKey::AssetClaims(asset_id.clone())) + .unwrap_or_else(|| Vec::new(&env)); + asset_claims.push_back(claim_id.clone()); + store.set(&DataKey::AssetClaims(asset_id.clone()), &asset_claims); + + env.events().publish( + (symbol_short!("claim_filed"), claim_id.clone()), + ( + claim.asset_id.clone(), + claim.policy_id.clone(), + claim.claimant.clone(), + claim.amount, + claim.description.clone(), + ), + ); + + Ok(claim_id) + } + + pub fn settle_claim( + env: Env, + claim_id: BytesN<32>, + approved: bool, + payout_amount: i128, + ) -> Result<(), ContractError> { + if payout_amount < 0 { + return Err(ContractError::InvalidClaimAmount); + } + + let store = env.storage().persistent(); + let admin: Address = store + .get(&DataKey::Admin) + .expect("contract not initialized"); + admin.require_auth(); + + let claim_key = DataKey::Claim(claim_id.clone()); + let mut claim: InsuranceClaim = store + .get(&claim_key) + .ok_or(ContractError::ClaimNotFound)?; + + if claim.status != ClaimStatus::Pending { + return Err(ContractError::InvalidClaimStatus); + } + + if approved { + claim.payout_amount = payout_amount; + claim.status = if payout_amount > 0 { + ClaimStatus::Paid + } else { + ClaimStatus::Approved + }; + } else { + if payout_amount != 0 { + return Err(ContractError::InvalidClaimAmount); + } + claim.status = ClaimStatus::Rejected; + } + + store.set(&claim_key, &claim); + + env.events().publish( + (symbol_short!("claim_settled"), claim_id.clone()), + (approved, claim.payout_amount, claim.status.clone()), + ); + + Ok(()) + } + + pub fn get_insurance_claim( + env: Env, + claim_id: BytesN<32>, + ) -> Result { + env.storage() + .persistent() + .get(&DataKey::Claim(claim_id)) + .ok_or(ContractError::ClaimNotFound) + } + + pub fn get_asset_claims(env: Env, asset_id: BytesN<32>) -> Vec> { + env.storage() + .persistent() + .get(&DataKey::AssetClaims(asset_id)) + .unwrap_or_else(|| Vec::new(&env)) + } +} diff --git a/contracts/opsce/src/insurance_claim_test.rs b/contracts/opsce/src/insurance_claim_test.rs new file mode 100644 index 00000000..32bf4b9d --- /dev/null +++ b/contracts/opsce/src/insurance_claim_test.rs @@ -0,0 +1,104 @@ +#![cfg(test)] +extern crate std; + +use super::*; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{Address, Env, String}; + +#[test] +fn test_file_insurance_claim() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(InsuranceClaimContract, ()); + let client = InsuranceClaimContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.init(&admin); + + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + let description = String::from_str(&env, "Broken window"); + + let claim_id = client + .file_insurance_claim(&asset_id, &policy_id, &100, &description) + .unwrap(); + + let claim = client.get_insurance_claim(&claim_id).unwrap(); + assert_eq!(claim.asset_id, asset_id); + assert_eq!(claim.policy_id, policy_id); + assert_eq!(claim.amount, 100); + assert_eq!(claim.description, description); + assert_eq!(claim.status, ClaimStatus::Pending); +} + +#[test] +fn test_settle_claim_approve() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(InsuranceClaimContract, ()); + let client = InsuranceClaimContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.init(&admin); + + let asset_id = BytesN::from_array(&env, &[3u8; 32]); + let policy_id = BytesN::from_array(&env, &[4u8; 32]); + let claim_id = client + .file_insurance_claim(&asset_id, &policy_id, &200, &String::from_str(&env, "Theft")) + .unwrap(); + + client.settle_claim(&claim_id, &true, &150).unwrap(); + + let claim = client.get_insurance_claim(&claim_id).unwrap(); + assert_eq!(claim.status, ClaimStatus::Paid); + assert_eq!(claim.payout_amount, 150); +} + +#[test] +fn test_settle_claim_reject() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(InsuranceClaimContract, ()); + let client = InsuranceClaimContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.init(&admin); + + let asset_id = BytesN::from_array(&env, &[5u8; 32]); + let policy_id = BytesN::from_array(&env, &[6u8; 32]); + let claim_id = client + .file_insurance_claim(&asset_id, &policy_id, &50, &String::from_str(&env, "Damage")) + .unwrap(); + + client.settle_claim(&claim_id, &false, &0).unwrap(); + + let claim = client.get_insurance_claim(&claim_id).unwrap(); + assert_eq!(claim.status, ClaimStatus::Rejected); + assert_eq!(claim.payout_amount, 0); +} + +#[test] +#[should_panic(expected = "Error(Contract, #2)")] +fn test_settle_claim_invalid_caller() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(InsuranceClaimContract, ()); + let client = InsuranceClaimContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.init(&admin); + + let asset_id = BytesN::from_array(&env, &[7u8; 32]); + let policy_id = BytesN::from_array(&env, &[8u8; 32]); + let claim_id = client + .file_insurance_claim(&asset_id, &policy_id, &75, &String::from_str(&env, "Water damage")) + .unwrap(); + + // Clear auths so the settle_claim call fails for admin authorization + env.reset_auths(); + client.settle_claim(&claim_id, &true, &75).unwrap(); +} diff --git a/contracts/opsce/src/lib.rs b/contracts/opsce/src/lib.rs new file mode 100644 index 00000000..b00c9cc1 --- /dev/null +++ b/contracts/opsce/src/lib.rs @@ -0,0 +1,5 @@ +#![no_std] + +mod insurance_claim; + +pub use insurance_claim::*; From 28edebc4cad13fedc02bd2b978d73fdaf8fe178e Mon Sep 17 00:00:00 2001 From: sadeeq6400 Date: Fri, 29 May 2026 09:33:11 +0100 Subject: [PATCH 2/2] complete implementation --- contracts/Cargo.toml | 1 + contracts/assetsup/src/lib.rs | 2 +- contracts/opsce/Cargo.toml | 1 + contracts/opsce/src/batch_transfer.rs | 58 +++++++ contracts/opsce/src/batch_transfer_test.rs | 174 ++++++++++++++++++++ contracts/opsce/src/insurance_claim_test.rs | 4 +- contracts/opsce/src/lib.rs | 8 + 7 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 contracts/opsce/src/batch_transfer.rs create mode 100644 contracts/opsce/src/batch_transfer_test.rs diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 62be99f9..1a790a3d 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -6,6 +6,7 @@ members = [ "contrib", "multisig-wallet", "multisig_transfer", + "./opsce", ] [workspace.dependencies] diff --git a/contracts/assetsup/src/lib.rs b/contracts/assetsup/src/lib.rs index 7c220273..ebc35f74 100644 --- a/contracts/assetsup/src/lib.rs +++ b/contracts/assetsup/src/lib.rs @@ -14,7 +14,7 @@ pub(crate) mod dividends; pub(crate) mod error; pub(crate) mod insurance; pub(crate) mod lease; -pub(crate) mod tokenization; +pub mod tokenization; pub(crate) mod transfer_restrictions; pub(crate) mod types; pub(crate) mod voting; diff --git a/contracts/opsce/Cargo.toml b/contracts/opsce/Cargo.toml index 7108921c..475c8f32 100644 --- a/contracts/opsce/Cargo.toml +++ b/contracts/opsce/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["lib", "cdylib"] doctest = false [dependencies] +assetsup = { path = "../assetsup" } soroban-sdk = { workspace = true } [dev-dependencies] diff --git a/contracts/opsce/src/batch_transfer.rs b/contracts/opsce/src/batch_transfer.rs new file mode 100644 index 00000000..ea9a2370 --- /dev/null +++ b/contracts/opsce/src/batch_transfer.rs @@ -0,0 +1,58 @@ +use assetsup::tokenization::{get_token_balance, transfer_tokens}; +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Vec}; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum ContractError { + InsufficientBalance = 1, + BatchLimitExceeded = 2, + InvalidTransferAmount = 3, + TransferFailed = 4, +} + +#[contract] +pub struct BatchTokenTransferContract; + +#[contractimpl] +impl BatchTokenTransferContract { + pub fn batch_transfer_tokens( + env: Env, + asset_id: u64, + transfers: Vec<(Address, i128)>, + ) -> Result<(), ContractError> { + if transfers.len() > 50 { + return Err(ContractError::BatchLimitExceeded); + } + + let sender = env.invoker(); + sender.require_auth(); + + let mut total_amount: i128 = 0; + for (_, amount) in transfers.iter() { + if *amount <= 0 { + return Err(ContractError::InvalidTransferAmount); + } + total_amount = total_amount + .checked_add(*amount) + .ok_or(ContractError::InvalidTransferAmount)?; + } + + let balance = get_token_balance(&env, asset_id, sender.clone()) + .map_err(|_| ContractError::InsufficientBalance)?; + if balance < total_amount { + return Err(ContractError::InsufficientBalance); + } + + for (recipient, amount) in transfers.iter() { + transfer_tokens(&env, asset_id, sender.clone(), recipient.clone(), *amount) + .map_err(|_| ContractError::TransferFailed)?; + env.events().publish( + ("token", "token_transferred"), + (asset_id, sender.clone(), recipient.clone(), *amount), + ); + } + + Ok(()) + } +} diff --git a/contracts/opsce/src/batch_transfer_test.rs b/contracts/opsce/src/batch_transfer_test.rs new file mode 100644 index 00000000..814f7443 --- /dev/null +++ b/contracts/opsce/src/batch_transfer_test.rs @@ -0,0 +1,174 @@ +#![cfg(test)] +extern crate std; + +use super::*; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{vec, Address, Env, String, Vec}; + +#[test] +fn test_batch_transfer_successful() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(BatchTokenTransferContract, ()); + let client = BatchTokenTransferContractClient::new(&env, &contract_id); + + let sender = Address::generate(&env); + let asset_id = 1u64; + let metadata = assetsup::TokenMetadata { + name: String::from_str(&env, "Batch Asset"), + description: String::from_str(&env, "Batch transfer test asset"), + asset_type: assetsup::AssetType::Digital, + ipfs_uri: None, + legal_docs_hash: None, + valuation_report_hash: None, + accredited_investor_required: false, + geographic_restrictions: Vec::new(&env), + }; + + assetsup::tokenization::tokenize_asset( + &env, + asset_id, + String::from_str(&env, "BATCH"), + 1000, + 2, + 1, + sender.clone(), + metadata, + ) + .unwrap(); + + let recipient1 = Address::generate(&env); + let recipient2 = Address::generate(&env); + let transfers: Vec<(Address, i128)> = vec![&env, (recipient1.clone(), 100_i128), (recipient2.clone(), 200_i128)]; + + client.batch_transfer_tokens(&asset_id, &transfers).unwrap(); + + let sender_balance = assetsup::tokenization::get_token_balance(&env, asset_id, sender.clone()).unwrap(); + assert_eq!(sender_balance, 700); + + let recipient1_balance = assetsup::tokenization::get_token_balance(&env, asset_id, recipient1).unwrap(); + assert_eq!(recipient1_balance, 100); + + let recipient2_balance = assetsup::tokenization::get_token_balance(&env, asset_id, recipient2).unwrap(); + assert_eq!(recipient2_balance, 200); +} + +#[test] +fn test_batch_transfer_insufficient_balance() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(BatchTokenTransferContract, ()); + let client = BatchTokenTransferContractClient::new(&env, &contract_id); + + let sender = Address::generate(&env); + let asset_id = 2u64; + let metadata = assetsup::TokenMetadata { + name: String::from_str(&env, "Batch Asset"), + description: String::from_str(&env, "Insufficient balance asset"), + asset_type: assetsup::AssetType::Digital, + ipfs_uri: None, + legal_docs_hash: None, + valuation_report_hash: None, + accredited_investor_required: false, + geographic_restrictions: Vec::new(&env), + }; + + assetsup::tokenization::tokenize_asset( + &env, + asset_id, + String::from_str(&env, "BATCH2"), + 100, + 2, + 1, + sender.clone(), + metadata, + ) + .unwrap(); + + let recipient = Address::generate(&env); + let transfers: Vec<(Address, i128)> = vec![&env, (recipient.clone(), 60_i128), (recipient.clone(), 50_i128)]; + + let result = client.batch_transfer_tokens(&asset_id, &transfers); + assert_eq!(result, Err(ContractError::InsufficientBalance)); +} + +#[test] +fn test_batch_transfer_limit_exceeded() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(BatchTokenTransferContract, ()); + let client = BatchTokenTransferContractClient::new(&env, &contract_id); + + let sender = Address::generate(&env); + let asset_id = 3u64; + let metadata = assetsup::TokenMetadata { + name: String::from_str(&env, "Batch Limit Asset"), + description: String::from_str(&env, "Limit exceeded asset"), + asset_type: assetsup::AssetType::Digital, + ipfs_uri: None, + legal_docs_hash: None, + valuation_report_hash: None, + accredited_investor_required: false, + geographic_restrictions: Vec::new(&env), + }; + + assetsup::tokenization::tokenize_asset( + &env, + asset_id, + String::from_str(&env, "BATCH3"), + 10000, + 2, + 1, + sender.clone(), + metadata, + ) + .unwrap(); + + let mut transfers: Vec<(Address, i128)> = Vec::new(&env); + for _ in 0..51 { + transfers.push_back((Address::generate(&env), 1_i128)); + } + + let result = client.batch_transfer_tokens(&asset_id, &transfers); + assert_eq!(result, Err(ContractError::BatchLimitExceeded)); +} + +#[test] +fn test_batch_transfer_empty_batch() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(BatchTokenTransferContract, ()); + let client = BatchTokenTransferContractClient::new(&env, &contract_id); + + let asset_id = 4u64; + let sender = Address::generate(&env); + let metadata = assetsup::TokenMetadata { + name: String::from_str(&env, "Batch Empty Asset"), + description: String::from_str(&env, "Empty transfer asset"), + asset_type: assetsup::AssetType::Digital, + ipfs_uri: None, + legal_docs_hash: None, + valuation_report_hash: None, + accredited_investor_required: false, + geographic_restrictions: Vec::new(&env), + }; + + assetsup::tokenization::tokenize_asset( + &env, + asset_id, + String::from_str(&env, "BATCH4"), + 500, + 2, + 1, + sender, + metadata, + ) + .unwrap(); + + let transfers: Vec<(Address, i128)> = Vec::new(&env); + client.batch_transfer_tokens(&asset_id, &transfers).unwrap(); +} diff --git a/contracts/opsce/src/insurance_claim_test.rs b/contracts/opsce/src/insurance_claim_test.rs index 32bf4b9d..b1e8bdd7 100644 --- a/contracts/opsce/src/insurance_claim_test.rs +++ b/contracts/opsce/src/insurance_claim_test.rs @@ -3,7 +3,7 @@ extern crate std; use super::*; use soroban_sdk::testutils::Address as _; -use soroban_sdk::{Address, Env, String}; +use soroban_sdk::{Address, BytesN, Env, String}; #[test] fn test_file_insurance_claim() { @@ -99,6 +99,6 @@ fn test_settle_claim_invalid_caller() { .unwrap(); // Clear auths so the settle_claim call fails for admin authorization - env.reset_auths(); + env.set_auths(&[]); client.settle_claim(&claim_id, &true, &75).unwrap(); } diff --git a/contracts/opsce/src/lib.rs b/contracts/opsce/src/lib.rs index b00c9cc1..bbf28826 100644 --- a/contracts/opsce/src/lib.rs +++ b/contracts/opsce/src/lib.rs @@ -1,5 +1,13 @@ #![no_std] mod insurance_claim; +mod batch_transfer; pub use insurance_claim::*; +pub use batch_transfer::*; + +#[cfg(test)] +mod insurance_claim_test; + +#[cfg(test)] +mod batch_transfer_test;