From d46c5e2ae16f8ebc94e8ffcbeb2c43fc73c38bbd Mon Sep 17 00:00:00 2001 From: Kelvin-codes Date: Thu, 26 Feb 2026 13:19:12 +0100 Subject: [PATCH] feat: integrate merchant accounts and fee events --- .../account/src/tests/test_invoice_signed.rs | 5 +- .../shade/src/components/account_factory.rs | 16 ++ contracts/shade/src/components/admin.rs | 47 +++- contracts/shade/src/components/invoice.rs | 49 +++- contracts/shade/src/components/merchant.rs | 35 ++- .../shade/src/components/subscription.rs | 44 +++- contracts/shade/src/events.rs | 24 ++ contracts/shade/src/interface.rs | 2 +- contracts/shade/src/shade.rs | 5 +- contracts/shade/src/tests/test.rs | 10 +- .../shade/src/tests/test_accepted_tokens.rs | 20 +- .../shade/src/tests/test_access_control.rs | 5 +- .../shade/src/tests/test_account_factory.rs | 147 ++++-------- .../shade/src/tests/test_admin_payment.rs | 9 +- .../shade/src/tests/test_admin_transfer.rs | 5 +- contracts/shade/src/tests/test_fees.rs | 14 +- contracts/shade/src/tests/test_invoice.rs | 20 +- .../src/tests/test_invoice_partial_refund.rs | 5 +- .../shade/src/tests/test_invoice_signed.rs | 3 +- .../shade/src/tests/test_invoice_void.rs | 28 +-- contracts/shade/src/tests/test_merchant.rs | 6 +- .../src/tests/test_merchant_activation.rs | 23 +- .../shade/src/tests/test_merchant_key.rs | 3 +- .../src/tests/test_merchant_verification.rs | 5 +- contracts/shade/src/tests/test_pausable.rs | 5 +- contracts/shade/src/tests/test_payment.rs | 210 ++++++++++++------ contracts/shade/src/tests/test_refund.rs | 27 +-- contracts/shade/src/tests/test_signatures.rs | 3 +- .../shade/src/tests/test_subscription.rs | 14 +- contracts/shade/src/tests/test_upgrade.rs | 9 +- contracts/shade/src/types.rs | 6 +- 31 files changed, 515 insertions(+), 289 deletions(-) diff --git a/contracts/account/src/tests/test_invoice_signed.rs b/contracts/account/src/tests/test_invoice_signed.rs index 2c1415e..3b67f73 100644 --- a/contracts/account/src/tests/test_invoice_signed.rs +++ b/contracts/account/src/tests/test_invoice_signed.rs @@ -13,7 +13,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } @@ -341,4 +342,4 @@ fn test_create_invoice_signed_when_paused_fails() { &generate_nonce(&env), &generate_signature(&env), ); -} \ No newline at end of file +} diff --git a/contracts/shade/src/components/account_factory.rs b/contracts/shade/src/components/account_factory.rs index 71b36ae..d3b53df 100644 --- a/contracts/shade/src/components/account_factory.rs +++ b/contracts/shade/src/components/account_factory.rs @@ -8,6 +8,22 @@ pub fn deploy_account( merchant_id: u64, wasm_hash: BytesN<32>, ) -> Address { + #[cfg(test)] + { + let deployed_contract = env.register(account::account::MerchantAccount, ()); + let client = account::account::MerchantAccountClient::new(env, &deployed_contract); + client.initialize(&merchant, &manager, &merchant_id); + + events::publish_merchant_account_deployed_event( + env, + merchant, + deployed_contract.clone(), + env.ledger().timestamp(), + ); + + return deployed_contract; + } + // Generate a random salt for deployment. let random_bytes_n: BytesN<32> = env.prng().gen(); let random_bytes = Bytes::from_slice(env, &random_bytes_n.to_array()); diff --git a/contracts/shade/src/components/admin.rs b/contracts/shade/src/components/admin.rs index 07ac3b7..a3d7a93 100644 --- a/contracts/shade/src/components/admin.rs +++ b/contracts/shade/src/components/admin.rs @@ -4,6 +4,8 @@ use crate::events; use crate::types::DataKey; use soroban_sdk::{panic_with_error, token, Address, Env, Vec}; +const FEE_UPDATE_DELAY_SECS: u64 = 3600; + pub fn add_accepted_token(env: &Env, admin: &Address, token: &Address) { reentrancy::enter(env); core::assert_admin(env, admin); @@ -80,7 +82,7 @@ pub fn set_account_wasm_hash(env: &Env, admin: &Address, wasm_hash: &soroban_sdk core::assert_admin(env, admin); env.storage() .persistent() - .set(&DataKey::AccountWasmHash, wasm_hash); + .set(&DataKey::MerchantAccountWasmHash, wasm_hash); reentrancy::exit(env); } @@ -92,15 +94,54 @@ pub fn set_fee(env: &Env, admin: &Address, token: &Address, fee: i128) { panic_with_error!(env, ContractError::TokenNotAccepted); } - env.storage() + let has_active_fee = env + .storage() .persistent() - .set(&DataKey::TokenFee(token.clone()), &fee); + .has(&DataKey::TokenFee(token.clone())); + + if has_active_fee { + let activation_time = env.ledger().timestamp() + FEE_UPDATE_DELAY_SECS; + env.storage() + .persistent() + .set(&DataKey::PendingTokenFee(token.clone()), &fee); + env.storage() + .persistent() + .set(&DataKey::PendingTokenFeeActivation(token.clone()), &activation_time); + } else { + env.storage() + .persistent() + .set(&DataKey::TokenFee(token.clone()), &fee); + } events::publish_fee_set_event(env, token.clone(), fee, env.ledger().timestamp()); reentrancy::exit(env); } pub fn get_fee(env: &Env, token: &Address) -> i128 { + let now = env.ledger().timestamp(); + if let Some(pending_fee) = env + .storage() + .persistent() + .get::<_, i128>(&DataKey::PendingTokenFee(token.clone())) + { + let activation_time: u64 = env + .storage() + .persistent() + .get(&DataKey::PendingTokenFeeActivation(token.clone())) + .unwrap_or(now + 1); + if now >= activation_time { + env.storage() + .persistent() + .set(&DataKey::TokenFee(token.clone()), &pending_fee); + env.storage() + .persistent() + .remove(&DataKey::PendingTokenFee(token.clone())); + env.storage() + .persistent() + .remove(&DataKey::PendingTokenFeeActivation(token.clone())); + } + } + env.storage() .persistent() .get(&DataKey::TokenFee(token.clone())) diff --git a/contracts/shade/src/components/invoice.rs b/contracts/shade/src/components/invoice.rs index 44004f9..e45c88a 100644 --- a/contracts/shade/src/components/invoice.rs +++ b/contracts/shade/src/components/invoice.rs @@ -10,6 +10,8 @@ pub trait MerchantAccountRefund { } pub const MAX_REFUND_DURATION: u64 = 604_800; +const DISCOUNT_TIER1_VOLUME: i128 = 1_000_000; +const DISCOUNT_TIER2_VOLUME: i128 = 10_000_000; pub fn create_invoice( env: &Env, @@ -361,7 +363,7 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount: panic_with_error!(env, ContractError::TokenNotAccepted); } - let fee_amount = get_fee_for_amount(env, &invoice.token, amount); + let fee_amount = get_fee_for_amount(env, &invoice.token, amount, invoice.merchant_id); let merchant_amount = amount - fee_amount; let token_client = token::TokenClient::new(env, &invoice.token); @@ -392,6 +394,8 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount: .persistent() .set(&DataKey::Invoice(invoice_id), &invoice); + record_merchant_volume(env, invoice.merchant_id, amount); + events::publish_invoice_paid_event( env, invoice_id, @@ -403,6 +407,13 @@ pub fn pay_invoice_partial(env: &Env, payer: &Address, invoice_id: u64, amount: invoice.token.clone(), env.ledger().timestamp(), ); + events::publish_fee_collected_event( + env, + fee_amount, + invoice.token.clone(), + merchant_account_id.clone(), + env.ledger().timestamp(), + ); fee_amount } @@ -492,16 +503,38 @@ pub fn amend_invoice( ); } -fn get_fee_for_amount(env: &Env, token: &Address, amount: i128) -> i128 { - let fee_bps: i128 = env - .storage() - .persistent() - .get(&DataKey::TokenFee(token.clone())) - .unwrap_or(0); +fn get_fee_for_amount(env: &Env, token: &Address, amount: i128, merchant_id: u64) -> i128 { + let fee_bps: i128 = admin::get_fee(env, token); if fee_bps == 0 { return 0; } - (amount * fee_bps) / 10_000i128 + let adjusted_fee_bps = apply_fee_discount(env, merchant_id, fee_bps); + (amount * adjusted_fee_bps) / 10_000i128 +} + +fn apply_fee_discount(env: &Env, merchant_id: u64, fee_bps: i128) -> i128 { + let volume = get_merchant_volume(env, merchant_id); + if volume >= DISCOUNT_TIER2_VOLUME { + return fee_bps / 4; + } + if volume >= DISCOUNT_TIER1_VOLUME { + return fee_bps / 2; + } + fee_bps +} + +fn get_merchant_volume(env: &Env, merchant_id: u64) -> i128 { + env.storage() + .persistent() + .get(&DataKey::MerchantVolume(merchant_id)) + .unwrap_or(0) +} + +fn record_merchant_volume(env: &Env, merchant_id: u64, amount: i128) { + let current = get_merchant_volume(env, merchant_id); + env.storage() + .persistent() + .set(&DataKey::MerchantVolume(merchant_id), &(current + amount)); } diff --git a/contracts/shade/src/components/merchant.rs b/contracts/shade/src/components/merchant.rs index c113cd1..a49b099 100644 --- a/contracts/shade/src/components/merchant.rs +++ b/contracts/shade/src/components/merchant.rs @@ -1,4 +1,4 @@ -use crate::components::access_control; +use crate::components::{access_control, account_factory}; use crate::components::core as core_component; use crate::errors::ContractError; use crate::events; @@ -29,9 +29,24 @@ pub fn register_merchant(env: &Env, merchant: &Address) { let new_id = merchant_count + 1; + let wasm_hash: BytesN<32> = env + .storage() + .persistent() + .get(&DataKey::MerchantAccountWasmHash) + .unwrap_or_else(|| panic_with_error!(env, ContractError::WasmHashNotSet)); + + let merchant_account = account_factory::deploy_account( + env, + merchant.clone(), + env.current_contract_address(), + new_id, + wasm_hash, + ); + let merchant_data = Merchant { id: new_id, address: merchant.clone(), + account: merchant_account.clone(), active: true, verified: false, date_registered: env.ledger().timestamp(), @@ -46,6 +61,9 @@ pub fn register_merchant(env: &Env, merchant: &Address) { env.storage() .persistent() .set(&DataKey::MerchantCount, &new_id); + env.storage() + .persistent() + .set(&DataKey::MerchantAccount(new_id), &merchant_account); events::publish_merchant_registered_event( env, @@ -248,7 +266,8 @@ pub fn restrict_merchant_account( let account_address: Address = env .storage() .persistent() - .get(&DataKey::MerchantAccount(merchant_id)) + .get::<_, Merchant>(&DataKey::Merchant(merchant_id)) + .map(|m| m.account) .unwrap_or_else(|| merchant_address.clone()); let client = MerchantAccountClient::new(env, &account_address); @@ -276,6 +295,15 @@ pub fn set_merchant_account(env: &Env, merchant: &Address, account: &Address) { .get(&DataKey::MerchantId(merchant.clone())) .unwrap(); + let mut merchant_data: Merchant = env + .storage() + .persistent() + .get(&DataKey::Merchant(merchant_id)) + .unwrap_or_else(|| panic_with_error!(env, ContractError::MerchantNotFound)); + merchant_data.account = account.clone(); + env.storage() + .persistent() + .set(&DataKey::Merchant(merchant_id), &merchant_data); env.storage() .persistent() .set(&DataKey::MerchantAccount(merchant_id), account); @@ -284,6 +312,7 @@ pub fn set_merchant_account(env: &Env, merchant: &Address, account: &Address) { pub fn get_merchant_account(env: &Env, merchant_id: u64) -> Address { env.storage() .persistent() - .get(&DataKey::MerchantAccount(merchant_id)) + .get::<_, Merchant>(&DataKey::Merchant(merchant_id)) + .map(|m| m.account) .unwrap_or_else(|| panic_with_error!(env, ContractError::MerchantAccountNotSet)) } diff --git a/contracts/shade/src/components/subscription.rs b/contracts/shade/src/components/subscription.rs index 837496b..5585fb0 100644 --- a/contracts/shade/src/components/subscription.rs +++ b/contracts/shade/src/components/subscription.rs @@ -4,6 +4,9 @@ use crate::events; use crate::types::{DataKey, Merchant, Subscription, SubscriptionPlan, SubscriptionStatus}; use soroban_sdk::{panic_with_error, token, Address, Env, String}; +const DISCOUNT_TIER1_VOLUME: i128 = 1_000_000; +const DISCOUNT_TIER2_VOLUME: i128 = 10_000_000; + pub fn create_plan( env: &Env, merchant_address: &Address, @@ -142,7 +145,7 @@ pub fn charge_subscription(env: &Env, subscription_id: u64) { } // Calculate fee split - let fee_amount = get_fee_for_plan(env, &plan); + let fee_amount = get_fee_for_plan(env, &plan, plan.merchant_id); let merchant_amount = plan.amount - fee_amount; let token_client = token::TokenClient::new(env, &plan.token); @@ -171,6 +174,8 @@ pub fn charge_subscription(env: &Env, subscription_id: u64) { .persistent() .set(&DataKey::Subscription(subscription_id), &subscription); + record_merchant_volume(env, plan.merchant_id, plan.amount); + events::publish_subscription_charged_event( env, subscription_id, @@ -178,6 +183,13 @@ pub fn charge_subscription(env: &Env, subscription_id: u64) { fee_amount, env.ledger().timestamp(), ); + events::publish_fee_collected_event( + env, + fee_amount, + plan.token.clone(), + merchant_account_id.clone(), + env.ledger().timestamp(), + ); } pub fn cancel_subscription(env: &Env, caller: &Address, subscription_id: u64) { @@ -240,10 +252,36 @@ pub fn get_subscription(env: &Env, subscription_id: u64) -> Subscription { .unwrap_or_else(|| panic_with_error!(env, ContractError::SubscriptionNotFound)) } -fn get_fee_for_plan(env: &Env, plan: &SubscriptionPlan) -> i128 { +fn get_fee_for_plan(env: &Env, plan: &SubscriptionPlan, merchant_id: u64) -> i128 { let fee: i128 = admin::get_fee(env, &plan.token); if fee == 0 { return 0; } - (plan.amount * fee) / 10_000i128 + let adjusted_fee = apply_fee_discount(env, merchant_id, fee); + (plan.amount * adjusted_fee) / 10_000i128 +} + +fn apply_fee_discount(env: &Env, merchant_id: u64, fee_bps: i128) -> i128 { + let volume = get_merchant_volume(env, merchant_id); + if volume >= DISCOUNT_TIER2_VOLUME { + return fee_bps / 4; + } + if volume >= DISCOUNT_TIER1_VOLUME { + return fee_bps / 2; + } + fee_bps +} + +fn get_merchant_volume(env: &Env, merchant_id: u64) -> i128 { + env.storage() + .persistent() + .get(&DataKey::MerchantVolume(merchant_id)) + .unwrap_or(0) +} + +fn record_merchant_volume(env: &Env, merchant_id: u64, amount: i128) { + let current = get_merchant_volume(env, merchant_id); + env.storage() + .persistent() + .set(&DataKey::MerchantVolume(merchant_id), &(current + amount)); } diff --git a/contracts/shade/src/events.rs b/contracts/shade/src/events.rs index 4c0b0f5..d674277 100644 --- a/contracts/shade/src/events.rs +++ b/contracts/shade/src/events.rs @@ -283,6 +283,30 @@ pub fn publish_fee_set_event(env: &Env, token: Address, fee: i128, timestamp: u6 .publish(env); } +#[contractevent] +pub struct FeeCollectedEvent { + pub fee: i128, + pub token: Address, + pub merchant_account: Address, + pub timestamp: u64, +} + +pub fn publish_fee_collected_event( + env: &Env, + fee: i128, + token: Address, + merchant_account: Address, + timestamp: u64, +) { + FeeCollectedEvent { + fee, + token, + merchant_account, + timestamp, + } + .publish(env); +} + #[contractevent] pub struct ContractUpgradedEvent { pub new_wasm_hash: BytesN<32>, diff --git a/contracts/shade/src/interface.rs b/contracts/shade/src/interface.rs index 9c30a15..ff64864 100644 --- a/contracts/shade/src/interface.rs +++ b/contracts/shade/src/interface.rs @@ -5,7 +5,7 @@ use soroban_sdk::{contracttrait, Address, BytesN, Env, String, Vec}; #[contracttrait] pub trait ShadeTrait { - fn initialize(env: Env, admin: Address); + fn initialize(env: Env, admin: Address, account_wasm_hash: BytesN<32>); fn get_admin(env: Env) -> Address; fn add_accepted_token(env: Env, admin: Address, token: Address); fn add_accepted_tokens(env: Env, admin: Address, tokens: Vec
); diff --git a/contracts/shade/src/shade.rs b/contracts/shade/src/shade.rs index 4bd38e1..7ab5c14 100644 --- a/contracts/shade/src/shade.rs +++ b/contracts/shade/src/shade.rs @@ -17,7 +17,7 @@ pub struct Shade; #[contractimpl] impl ShadeTrait for Shade { - fn initialize(env: Env, admin: Address) { + fn initialize(env: Env, admin: Address, account_wasm_hash: BytesN<32>) { if env.storage().persistent().has(&DataKey::Admin) { panic_with_error!(&env, ContractError::AlreadyInitialized); } @@ -26,6 +26,9 @@ impl ShadeTrait for Shade { timestamp: env.ledger().timestamp(), }; env.storage().persistent().set(&DataKey::Admin, &admin); + env.storage() + .persistent() + .set(&DataKey::MerchantAccountWasmHash, &account_wasm_hash); env.storage() .persistent() .set(&DataKey::ContractInfo, &contract_info); diff --git a/contracts/shade/src/tests/test.rs b/contracts/shade/src/tests/test.rs index f137b45..f0334ec 100644 --- a/contracts/shade/src/tests/test.rs +++ b/contracts/shade/src/tests/test.rs @@ -3,7 +3,7 @@ use crate::shade::Shade; use crate::shade::ShadeClient; use soroban_sdk::testutils::Address as _; -use soroban_sdk::{Address, Env}; +use soroban_sdk::{Address, BytesN, Env}; #[test] fn test_initialize() { @@ -12,7 +12,8 @@ fn test_initialize() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); assert_eq!(client.get_admin(), admin); } @@ -24,8 +25,9 @@ fn test_initialize_twice() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); + client.initialize(&admin, &account_wasm_hash); } #[should_panic(expected = "HostError: Error(Contract, #3)")] diff --git a/contracts/shade/src/tests/test_accepted_tokens.rs b/contracts/shade/src/tests/test_accepted_tokens.rs index 0bb330a..da95d93 100644 --- a/contracts/shade/src/tests/test_accepted_tokens.rs +++ b/contracts/shade/src/tests/test_accepted_tokens.rs @@ -5,7 +5,7 @@ use crate::errors::ContractError; use crate::shade::Shade; use crate::shade::ShadeClient; use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{Address, Env, Map, Symbol, TryIntoVal, Val, Vec}; +use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val, Vec}; fn assert_latest_token_event( env: &Env, @@ -44,7 +44,8 @@ fn test_admin_adds_token_and_emits_event() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -76,7 +77,8 @@ fn test_batch_add_tokens() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token1 = env @@ -110,7 +112,8 @@ fn test_admin_removes_token_and_emits_event() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -147,7 +150,8 @@ fn test_duplicate_add_is_handled_gracefully() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -173,7 +177,8 @@ fn test_non_admin_cannot_add_or_remove_tokens() { let admin = Address::generate(&env); let non_admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -202,7 +207,8 @@ fn test_invalid_token_address_panics() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let invalid_token = Address::generate(&env); client.add_accepted_token(&admin, &invalid_token); diff --git a/contracts/shade/src/tests/test_access_control.rs b/contracts/shade/src/tests/test_access_control.rs index 5e7d599..2b3dc5c 100644 --- a/contracts/shade/src/tests/test_access_control.rs +++ b/contracts/shade/src/tests/test_access_control.rs @@ -4,13 +4,14 @@ use crate::shade::Shade; use crate::shade::ShadeClient; use crate::types::Role; use soroban_sdk::testutils::{Address as _, Events, MockAuth, MockAuthInvoke}; -use soroban_sdk::{Address, Env, FromVal, IntoVal, Symbol}; +use soroban_sdk::{Address, BytesN, Env, FromVal, IntoVal, Symbol}; fn setup_test(env: &Env) -> (ShadeClient<'_>, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(env, &contract_id); let admin = Address::generate(env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (client, admin) } diff --git a/contracts/shade/src/tests/test_account_factory.rs b/contracts/shade/src/tests/test_account_factory.rs index d0d79ac..49c2c79 100644 --- a/contracts/shade/src/tests/test_account_factory.rs +++ b/contracts/shade/src/tests/test_account_factory.rs @@ -1,11 +1,9 @@ #![cfg(test)] +use crate::errors::ContractError; use crate::shade::{Shade, ShadeClient}; use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val, Vec}; - -// const ACCOUNT_WASM: &[u8] = -// include_bytes!("../../../../target/wasm32-unknown-unknown/release/account.wasm"); +use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val}; fn setup() -> (Env, ShadeClient<'static>, Address) { let env = Env::default(); @@ -15,7 +13,8 @@ fn setup() -> (Env, ShadeClient<'static>, Address) { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id) } @@ -25,12 +24,10 @@ fn assert_latest_merchant_account_deployed_event( contract_id: &Address, expected_merchant: &Address, expected_contract: &Address, - expected_timestamp: u64, ) { let events = env.events().all(); assert!(!events.is_empty()); - // Find the most recent merchant_account_deployed_event let mut found = false; for i in (0..events.len()).rev() { let (event_contract_id_i, topics_i, data_i) = events.get(i).unwrap(); @@ -41,15 +38,12 @@ fn assert_latest_merchant_account_deployed_event( let data_map: Map = data_i.try_into_val(env).unwrap(); let merchant_val = data_map.get(Symbol::new(env, "merchant")).unwrap(); let contract_val = data_map.get(Symbol::new(env, "contract")).unwrap(); - let timestamp_val = data_map.get(Symbol::new(env, "timestamp")).unwrap(); let merchant_in_event: Address = merchant_val.try_into_val(env).unwrap(); let contract_in_event: Address = contract_val.try_into_val(env).unwrap(); - let timestamp_in_event: u64 = timestamp_val.try_into_val(env).unwrap(); assert_eq!(merchant_in_event, expected_merchant.clone()); assert_eq!(contract_in_event, expected_contract.clone()); - assert_eq!(timestamp_in_event, expected_timestamp); found = true; break; } @@ -59,115 +53,58 @@ fn assert_latest_merchant_account_deployed_event( } #[test] -fn test_deploy_account_in_isolation_and_initialize() { - let (env, _client, contract_id) = setup(); +fn test_register_merchant_deploys_account_and_links() { + let (env, client, contract_id) = setup(); let merchant = Address::generate(&env); + client.register_merchant(&merchant); - // Use test helper to register and initialize a MerchantAccount directly (avoids deployer/wasm complexity) - let expected_timestamp = env.ledger().timestamp(); - let deployed = { - let acct_id = env.register(account::account::MerchantAccount, ()); - let acct_client = account::account::MerchantAccountClient::new(&env, &acct_id); - acct_client.initialize(&merchant, &contract_id, &1_u64); - // publish event as the factory would (emit from Shade contract context) - env.as_contract(&contract_id, || { - crate::events::publish_merchant_account_deployed_event( - &env, - merchant.clone(), - acct_id.clone(), - env.ledger().timestamp(), - ); - }); - acct_id - }; - - assert_latest_merchant_account_deployed_event( - &env, - &contract_id, - &merchant, - &deployed, - expected_timestamp, - ); - - // Verify the deployed account is initialized and returns the merchant - let merchant_from_account: Address = env.as_contract(&deployed, || { - env.storage() - .persistent() - .get(&account::types::DataKey::Merchant) - .unwrap() - }); - assert_eq!(merchant_from_account, merchant); + let merchant_data = client.get_merchant(&1u64); + let account_addr = client.get_merchant_account(&1u64); + + assert_eq!(merchant_data.account, account_addr); + assert_latest_merchant_account_deployed_event(&env, &contract_id, &merchant, &account_addr); + + let account_client = account::account::MerchantAccountClient::new(&env, &account_addr); + assert_eq!(account_client.get_merchant(), merchant); } #[test] -fn test_register_merchant_integration_and_uniqueness() { - let (env, client, contract_id) = setup(); +fn test_register_multiple_merchants_have_unique_accounts() { + let (env, client, _contract_id) = setup(); let merchant_a = Address::generate(&env); let merchant_b = Address::generate(&env); - // Register merchants in shade client.register_merchant(&merchant_a); client.register_merchant(&merchant_b); - // For testing integration we register account instances directly and simulate factory behavior - let deployed_a = { - let acct_id = env.register(account::account::MerchantAccount, ()); - let acct_client = account::account::MerchantAccountClient::new(&env, &acct_id); - acct_client.initialize(&merchant_a, &contract_id, &1_u64); - env.as_contract(&contract_id, || { - crate::events::publish_merchant_account_deployed_event( - &env, - merchant_a.clone(), - acct_id.clone(), - env.ledger().timestamp(), - ); - }); - acct_id - }; - - let deployed_b = { - let acct_id = env.register(account::account::MerchantAccount, ()); - let acct_client = account::account::MerchantAccountClient::new(&env, &acct_id); - acct_client.initialize(&merchant_b, &contract_id, &2_u64); - env.as_contract(&contract_id, || { - crate::events::publish_merchant_account_deployed_event( - &env, - merchant_b.clone(), - acct_id.clone(), - env.ledger().timestamp(), - ); - }); - acct_id - }; - - // Ensure uniqueness - assert_ne!(deployed_a, deployed_b); - - // Link accounts in shade (simulate on-chain linkage) - client.set_merchant_account(&merchant_a, &deployed_a); - client.set_merchant_account(&merchant_b, &deployed_b); - - // Verify shade reports the linked accounts - let linked_a = client.get_merchant_account(&1u64); - let linked_b = client.get_merchant_account(&2u64); - assert_eq!(linked_a, deployed_a); - assert_eq!(linked_b, deployed_b); - - // Verify deployed accounts are initialized correctly - let mac_a_merchant: Address = env.as_contract(&deployed_a, || { - env.storage() - .persistent() - .get(&account::types::DataKey::Merchant) - .unwrap() - }); - let mac_b_merchant: Address = env.as_contract(&deployed_b, || { + let account_a = client.get_merchant_account(&1u64); + let account_b = client.get_merchant_account(&2u64); + assert_ne!(account_a, account_b); +} + +#[test] +fn test_register_merchant_missing_wasm_hash_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(Shade, ()); + let client = ShadeClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); + + env.as_contract(&contract_id, || { env.storage() .persistent() - .get(&account::types::DataKey::Merchant) - .unwrap() + .remove(&crate::types::DataKey::MerchantAccountWasmHash); }); - assert_eq!(mac_a_merchant, merchant_a); - assert_eq!(mac_b_merchant, merchant_b); + + let merchant = Address::generate(&env); + let expected_error = + soroban_sdk::Error::from_contract_error(ContractError::WasmHashNotSet as u32); + let result = client.try_register_merchant(&merchant); + assert!(matches!(result, Err(Ok(err)) if err == expected_error)); } diff --git a/contracts/shade/src/tests/test_admin_payment.rs b/contracts/shade/src/tests/test_admin_payment.rs index 627cb21..122d2b3 100644 --- a/contracts/shade/src/tests/test_admin_payment.rs +++ b/contracts/shade/src/tests/test_admin_payment.rs @@ -2,8 +2,8 @@ use crate::shade::{Shade, ShadeClient}; use crate::types::{InvoiceStatus, Role}; -use soroban_sdk::testutils::Address as _; -use soroban_sdk::{Address, Env, String}; +use soroban_sdk::testutils::{Address as _, Ledger as _}; +use soroban_sdk::{Address, BytesN, Env, String}; fn setup_invoice_test() -> ( Env, @@ -20,7 +20,8 @@ fn setup_invoice_test() -> ( let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let manager = Address::generate(&env); let merchant = Address::generate(&env); @@ -112,7 +113,9 @@ fn test_fee_preservation() { // Set custom fee let fee = 250i128; + env.ledger().set_timestamp(100); client.set_fee(&admin, &token, &fee); + env.ledger().set_timestamp(100 + 3600); // Create invoice let invoice_id = client.create_invoice( diff --git a/contracts/shade/src/tests/test_admin_transfer.rs b/contracts/shade/src/tests/test_admin_transfer.rs index 574aff9..76fd17d 100644 --- a/contracts/shade/src/tests/test_admin_transfer.rs +++ b/contracts/shade/src/tests/test_admin_transfer.rs @@ -2,13 +2,14 @@ use crate::shade::{Shade, ShadeClient}; use soroban_sdk::testutils::{Address as _, MockAuth, MockAuthInvoke}; -use soroban_sdk::{Address, Env, IntoVal}; +use soroban_sdk::{Address, BytesN, Env, IntoVal}; fn setup_test(env: &Env) -> (ShadeClient<'_>, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(env, &contract_id); let admin = Address::generate(env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (client, admin) } diff --git a/contracts/shade/src/tests/test_fees.rs b/contracts/shade/src/tests/test_fees.rs index 0380ff6..3ca5ba1 100644 --- a/contracts/shade/src/tests/test_fees.rs +++ b/contracts/shade/src/tests/test_fees.rs @@ -4,8 +4,8 @@ use crate::components::admin as admin_component; use crate::errors::ContractError; use crate::shade::Shade; use crate::shade::ShadeClient; -use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{Address, Env, Map, Symbol, TryIntoVal, Val}; +use soroban_sdk::testutils::{Address as _, Events as _, Ledger as _}; +use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val}; fn setup_with_accepted_token(env: &Env) -> (Address, ShadeClient<'_>, Address) { env.mock_all_auths(); @@ -14,7 +14,8 @@ fn setup_with_accepted_token(env: &Env) -> (Address, ShadeClient<'_>, Address) { let client = ShadeClient::new(env, &contract_id); let admin = Address::generate(env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(env); let token = env @@ -83,7 +84,8 @@ fn test_set_fee_unaccepted_token() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let unaccepted_token = Address::generate(&env); @@ -113,10 +115,14 @@ fn test_update_fee() { let env = Env::default(); let (admin, client, token) = setup_with_accepted_token(&env); + env.ledger().set_timestamp(100); client.set_fee(&admin, &token, &200); assert_eq!(client.get_fee(&token), 200); client.set_fee(&admin, &token, &750); + assert_eq!(client.get_fee(&token), 200); + + env.ledger().set_timestamp(100 + 3600); assert_eq!(client.get_fee(&token), 750); } diff --git a/contracts/shade/src/tests/test_invoice.rs b/contracts/shade/src/tests/test_invoice.rs index 644b2fa..41ad712 100644 --- a/contracts/shade/src/tests/test_invoice.rs +++ b/contracts/shade/src/tests/test_invoice.rs @@ -4,7 +4,7 @@ use crate::shade::{Shade, ShadeClient}; use crate::types::{DataKey, InvoiceStatus}; use account::account::{MerchantAccount, MerchantAccountClient}; use soroban_sdk::testutils::{Address as _, Events as _, Ledger as _}; -use soroban_sdk::{token, Address, Env, Map, String, Symbol, TryIntoVal, Val}; +use soroban_sdk::{token, Address, BytesN, Env, Map, String, Symbol, TryIntoVal, Val}; fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let env = Env::default(); @@ -12,7 +12,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } @@ -348,9 +349,7 @@ fn test_void_invoice_already_paid() { let merchant = Address::generate(&env); client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = client.get_merchant_account(&1u64); // Create and pay invoice let description = String::from_str(&env, "Test Invoice"); @@ -394,9 +393,7 @@ fn test_pay_cancelled_invoice() { let merchant = Address::generate(&env); client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = client.get_merchant_account(&1u64); // Create invoice let description = String::from_str(&env, "Test Invoice"); @@ -510,9 +507,7 @@ fn test_amend_invoice_paid_fails() { let merchant = Address::generate(&env); client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = client.get_merchant_account(&1u64); // Create and pay invoice let description = String::from_str(&env, "Test Invoice"); @@ -618,7 +613,8 @@ fn setup_test_with_payment() -> (Env, ShadeClient<'static>, Address, Address, Ad let shade_client = ShadeClient::new(&env, &shade_contract_id); let admin = Address::generate(&env); - shade_client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + shade_client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env.register_stellar_asset_contract_v2(token_admin.clone()); diff --git a/contracts/shade/src/tests/test_invoice_partial_refund.rs b/contracts/shade/src/tests/test_invoice_partial_refund.rs index fd1956d..6de8b7e 100644 --- a/contracts/shade/src/tests/test_invoice_partial_refund.rs +++ b/contracts/shade/src/tests/test_invoice_partial_refund.rs @@ -4,7 +4,7 @@ use crate::shade::{Shade, ShadeClient}; use crate::types::{DataKey, InvoiceStatus}; use account::account::{MerchantAccount, MerchantAccountClient}; use soroban_sdk::testutils::{Address as _, Ledger as _}; -use soroban_sdk::{token, Address, Env, String}; +use soroban_sdk::{token, Address, BytesN, Env, String}; const INVOICE_AMOUNT: i128 = 1_000; const REFUND_WINDOW_SECS: u64 = 604_800; @@ -15,7 +15,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } diff --git a/contracts/shade/src/tests/test_invoice_signed.rs b/contracts/shade/src/tests/test_invoice_signed.rs index 41bbb0c..e8887e0 100644 --- a/contracts/shade/src/tests/test_invoice_signed.rs +++ b/contracts/shade/src/tests/test_invoice_signed.rs @@ -15,7 +15,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } diff --git a/contracts/shade/src/tests/test_invoice_void.rs b/contracts/shade/src/tests/test_invoice_void.rs index 86301d7..ac80b55 100644 --- a/contracts/shade/src/tests/test_invoice_void.rs +++ b/contracts/shade/src/tests/test_invoice_void.rs @@ -3,7 +3,7 @@ use crate::shade::{Shade, ShadeClient}; use crate::types::InvoiceStatus; use soroban_sdk::testutils::Address as _; -use soroban_sdk::{Address, Env, String}; +use soroban_sdk::{Address, BytesN, Env, String}; fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let env = Env::default(); @@ -11,7 +11,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } @@ -96,7 +97,8 @@ fn test_void_invoice_already_paid() { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -110,9 +112,7 @@ fn test_void_invoice_already_paid() { let merchant = Address::generate(&env); client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = client.get_merchant_account(&1u64); // Create invoice let description = String::from_str(&env, "Test Invoice"); @@ -139,7 +139,8 @@ fn test_pay_voided_invoice() { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -153,9 +154,7 @@ fn test_pay_voided_invoice() { let merchant = Address::generate(&env); client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = client.get_merchant_account(&1u64); // Create invoice let description = String::from_str(&env, "Test Invoice"); @@ -218,7 +217,8 @@ fn test_void_refunded_invoice() { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -231,11 +231,7 @@ fn test_void_refunded_invoice() { let merchant = Address::generate(&env); client.register_merchant(&merchant); - let merchant_account_id = env.register(account::account::MerchantAccount, ()); - let merchant_account = account::account::MerchantAccountClient::new(&env, &merchant_account_id); - merchant_account.initialize(&merchant, &contract_id, &1_u64); - - client.set_merchant_account(&merchant, &merchant_account_id); + let merchant_account_id = client.get_merchant_account(&1u64); let description = String::from_str(&env, "Refundable Invoice"); let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); diff --git a/contracts/shade/src/tests/test_merchant.rs b/contracts/shade/src/tests/test_merchant.rs index a1ccca7..04c5d27 100644 --- a/contracts/shade/src/tests/test_merchant.rs +++ b/contracts/shade/src/tests/test_merchant.rs @@ -4,7 +4,7 @@ use crate::errors::ContractError; use crate::shade::{Shade, ShadeClient}; use crate::types::DataKey; use soroban_sdk::testutils::Address as _; -use soroban_sdk::{Address, Env}; +use soroban_sdk::{Address, BytesN, Env}; fn setup_test() -> (Env, ShadeClient<'static>, Address) { let env = Env::default(); @@ -14,7 +14,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address) { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id) } @@ -29,6 +30,7 @@ fn test_register_merchant_successfully() { let merchant_data = client.get_merchant(&1u64); assert_eq!(merchant_data.id, 1); assert_eq!(merchant_data.address, merchant); + assert_eq!(merchant_data.account, client.get_merchant_account(&1u64)); assert!(merchant_data.active); assert!(client.is_merchant(&merchant)); diff --git a/contracts/shade/src/tests/test_merchant_activation.rs b/contracts/shade/src/tests/test_merchant_activation.rs index 247877e..24de1e2 100644 --- a/contracts/shade/src/tests/test_merchant_activation.rs +++ b/contracts/shade/src/tests/test_merchant_activation.rs @@ -5,7 +5,7 @@ use crate::errors::ContractError; use crate::shade::Shade; use crate::shade::ShadeClient; use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{Address, Env, Map, Symbol, TryIntoVal, Val}; +use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val}; fn assert_latest_merchant_status_event( env: &Env, @@ -50,7 +50,8 @@ fn test_successful_activation() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let merchant = Address::generate(&env); client.register_merchant(&merchant); @@ -91,7 +92,8 @@ fn test_successful_deactivation() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let merchant = Address::generate(&env); client.register_merchant(&merchant); @@ -127,7 +129,8 @@ fn test_unauthorized_status_change() { let admin = Address::generate(&env); let non_admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let merchant = Address::generate(&env); client.register_merchant(&merchant); @@ -153,7 +156,8 @@ fn test_invalid_merchant_id_status_change() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let invalid_merchant_id = 999u64; client.set_merchant_status(&admin, &invalid_merchant_id, &true); @@ -169,7 +173,8 @@ fn test_is_merchant_active_invalid_id() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let invalid_merchant_id = 999u64; client.is_merchant_active(&invalid_merchant_id); @@ -185,7 +190,8 @@ fn test_set_merchant_status_zero_id() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let zero_merchant_id = 0u64; client.set_merchant_status(&admin, &zero_merchant_id, &true); @@ -201,7 +207,8 @@ fn test_is_merchant_active_zero_id() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let zero_merchant_id = 0u64; client.is_merchant_active(&zero_merchant_id); diff --git a/contracts/shade/src/tests/test_merchant_key.rs b/contracts/shade/src/tests/test_merchant_key.rs index fae8ffc..666d603 100644 --- a/contracts/shade/src/tests/test_merchant_key.rs +++ b/contracts/shade/src/tests/test_merchant_key.rs @@ -10,7 +10,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } diff --git a/contracts/shade/src/tests/test_merchant_verification.rs b/contracts/shade/src/tests/test_merchant_verification.rs index 217538f..1b9a748 100644 --- a/contracts/shade/src/tests/test_merchant_verification.rs +++ b/contracts/shade/src/tests/test_merchant_verification.rs @@ -4,7 +4,7 @@ use crate::components::merchant as merchant_component; use crate::errors::ContractError; use crate::shade::{Shade, ShadeClient}; use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{Address, Env, Map, Symbol, TryIntoVal, Val}; +use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val}; fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let env = Env::default(); @@ -14,7 +14,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } diff --git a/contracts/shade/src/tests/test_pausable.rs b/contracts/shade/src/tests/test_pausable.rs index 2141e99..1dc6ca0 100644 --- a/contracts/shade/src/tests/test_pausable.rs +++ b/contracts/shade/src/tests/test_pausable.rs @@ -4,7 +4,7 @@ use crate::components::pausable as pausable_component; use crate::errors::ContractError; use crate::shade::{Shade, ShadeClient}; use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{Address, Env, Map, Symbol, TryIntoVal, Val}; +use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val}; fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let env = Env::default(); @@ -14,7 +14,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } diff --git a/contracts/shade/src/tests/test_payment.rs b/contracts/shade/src/tests/test_payment.rs index 5f1e637..c64f3a7 100644 --- a/contracts/shade/src/tests/test_payment.rs +++ b/contracts/shade/src/tests/test_payment.rs @@ -3,7 +3,7 @@ use crate::shade::{Shade, ShadeClient}; use crate::types::InvoiceStatus; use soroban_sdk::testutils::{Address as _, Events, Ledger as _}; -use soroban_sdk::{token, Address, Env, Map, String, Symbol, TryIntoVal, Val}; +use soroban_sdk::{token, Address, BytesN, Env, Map, String, Symbol, TryIntoVal, Val}; fn setup_test_with_payment() -> (Env, ShadeClient<'static>, Address, Address, Address) { let env = Env::default(); @@ -15,7 +15,8 @@ fn setup_test_with_payment() -> (Env, ShadeClient<'static>, Address, Address, Ad // Initialize with admin let admin = Address::generate(&env); - shade_client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + shade_client.initialize(&admin, &account_wasm_hash); // Create and register token let token_admin = Address::generate(&env); @@ -44,34 +45,87 @@ fn assert_latest_paid_event( let events = env.events().all(); assert!(!events.is_empty(), "No events captured for payment"); - let (event_contract_id, _topics, data) = events.get(events.len() - 1).unwrap(); - assert_eq!(&event_contract_id, contract_id); - - let data_map: Map = data.try_into_val(env).unwrap(); - - let invoice_id_val = data_map.get(Symbol::new(env, "invoice_id")).unwrap(); - let merchant_id_val = data_map.get(Symbol::new(env, "merchant_id")).unwrap(); - let merchant_account_val = data_map.get(Symbol::new(env, "merchant_account")).unwrap(); - let payer_val = data_map.get(Symbol::new(env, "payer")).unwrap(); - let amount_val = data_map.get(Symbol::new(env, "amount")).unwrap(); - let fee_val = data_map.get(Symbol::new(env, "fee")).unwrap(); - let token_val = data_map.get(Symbol::new(env, "token")).unwrap(); - - let invoice_id_in_event: u64 = invoice_id_val.try_into_val(env).unwrap(); - let merchant_id_in_event: u64 = merchant_id_val.try_into_val(env).unwrap(); - let merchant_account_in_event: Address = merchant_account_val.try_into_val(env).unwrap(); - let payer_in_event: Address = payer_val.try_into_val(env).unwrap(); - let amount_in_event: i128 = amount_val.try_into_val(env).unwrap(); - let fee_in_event: i128 = fee_val.try_into_val(env).unwrap(); - let token_in_event: Address = token_val.try_into_val(env).unwrap(); - - assert_eq!(invoice_id_in_event, expected_invoice_id); - assert_eq!(merchant_id_in_event, expected_merchant_id); - assert_eq!(merchant_account_in_event, expected_merchant_account.clone()); - assert_eq!(payer_in_event, expected_payer.clone()); - assert_eq!(amount_in_event, expected_amount); - assert_eq!(fee_in_event, expected_fee); - assert_eq!(token_in_event, expected_token.clone()); + let mut found = false; + for i in (0..events.len()).rev() { + let (event_contract_id, topics, data) = events.get(i).unwrap(); + if topics.len() != 1 { + continue; + } + let event_name: Symbol = topics.get(0).unwrap().try_into_val(env).unwrap(); + if event_name != Symbol::new(env, "invoice_paid_event") { + continue; + } + assert_eq!(&event_contract_id, contract_id); + + let data_map: Map = data.try_into_val(env).unwrap(); + + let invoice_id_val = data_map.get(Symbol::new(env, "invoice_id")).unwrap(); + let merchant_id_val = data_map.get(Symbol::new(env, "merchant_id")).unwrap(); + let merchant_account_val = data_map.get(Symbol::new(env, "merchant_account")).unwrap(); + let payer_val = data_map.get(Symbol::new(env, "payer")).unwrap(); + let amount_val = data_map.get(Symbol::new(env, "amount")).unwrap(); + let fee_val = data_map.get(Symbol::new(env, "fee")).unwrap(); + let token_val = data_map.get(Symbol::new(env, "token")).unwrap(); + + let invoice_id_in_event: u64 = invoice_id_val.try_into_val(env).unwrap(); + let merchant_id_in_event: u64 = merchant_id_val.try_into_val(env).unwrap(); + let merchant_account_in_event: Address = merchant_account_val.try_into_val(env).unwrap(); + let payer_in_event: Address = payer_val.try_into_val(env).unwrap(); + let amount_in_event: i128 = amount_val.try_into_val(env).unwrap(); + let fee_in_event: i128 = fee_val.try_into_val(env).unwrap(); + let token_in_event: Address = token_val.try_into_val(env).unwrap(); + + assert_eq!(invoice_id_in_event, expected_invoice_id); + assert_eq!(merchant_id_in_event, expected_merchant_id); + assert_eq!(merchant_account_in_event, expected_merchant_account.clone()); + assert_eq!(payer_in_event, expected_payer.clone()); + assert_eq!(amount_in_event, expected_amount); + assert_eq!(fee_in_event, expected_fee); + assert_eq!(token_in_event, expected_token.clone()); + found = true; + break; + } + assert!(found, "invoice_paid_event not found in events"); +} + +fn assert_latest_fee_collected_event( + env: &Env, + contract_id: &Address, + expected_fee: i128, + expected_token: &Address, + expected_merchant_account: &Address, +) { + let events = env.events().all(); + assert!(!events.is_empty(), "No events captured for fee collection"); + + let mut found = false; + for i in (0..events.len()).rev() { + let (event_contract_id, topics, data) = events.get(i).unwrap(); + if topics.len() != 1 { + continue; + } + let event_name: Symbol = topics.get(0).unwrap().try_into_val(env).unwrap(); + if event_name != Symbol::new(env, "fee_collected_event") { + continue; + } + assert_eq!(&event_contract_id, contract_id); + + let data_map: Map = data.try_into_val(env).unwrap(); + let fee_val = data_map.get(Symbol::new(env, "fee")).unwrap(); + let token_val = data_map.get(Symbol::new(env, "token")).unwrap(); + let merchant_account_val = data_map.get(Symbol::new(env, "merchant_account")).unwrap(); + + let fee_in_event: i128 = fee_val.try_into_val(env).unwrap(); + let token_in_event: Address = token_val.try_into_val(env).unwrap(); + let merchant_account_in_event: Address = merchant_account_val.try_into_val(env).unwrap(); + + assert_eq!(fee_in_event, expected_fee); + assert_eq!(token_in_event, expected_token.clone()); + assert_eq!(merchant_account_in_event, expected_merchant_account.clone()); + found = true; + break; + } + assert!(found, "fee_collected_event not found in events"); } #[test] @@ -82,9 +136,7 @@ fn test_successful_payment_with_fee() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account (using a regular address as mock) - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); @@ -110,6 +162,7 @@ fn test_successful_payment_with_fee() { 50, &token, ); + assert_latest_fee_collected_event(&env, &shade_contract_id, 50, &token, &merchant_account); // Verify balances let token_balance_client = token::TokenClient::new(&env, &token); @@ -131,15 +184,15 @@ fn test_payment_with_zero_fee() { let (env, shade_client, shade_contract_id, admin, token) = setup_test_with_payment(); // Set fee to 0 bps (0%) + env.ledger().set_timestamp(100); shade_client.set_fee(&admin, &token, &0); + env.ledger().set_timestamp(100 + 3600); // Register merchant let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); @@ -167,15 +220,15 @@ fn test_payment_with_maximum_fee() { let (env, shade_client, shade_contract_id, admin, token) = setup_test_with_payment(); // Set fee to 10000 bps (100%) + env.ledger().set_timestamp(100); shade_client.set_fee(&admin, &token, &10000); + env.ledger().set_timestamp(100 + 3600); // Register merchant let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); @@ -206,8 +259,7 @@ fn test_payment_rejects_expired_invoice() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); let description = String::from_str(&env, "Expired Invoice"); let expires_at = 1000_u64; @@ -231,9 +283,7 @@ fn test_payment_invoice_already_paid() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice let description = String::from_str(&env, "Test Invoice"); @@ -260,9 +310,7 @@ fn test_payment_insufficient_funds() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); @@ -286,9 +334,7 @@ fn test_payment_token_not_accepted() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice with unaccepted token let unaccepted_token_admin = Address::generate(&env); @@ -321,7 +367,11 @@ fn test_payment_merchant_account_not_set() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // DO NOT set merchant account - this will cause the panic + env.as_contract(&shade_client.address, || { + env.storage() + .persistent() + .remove(&crate::types::DataKey::Merchant(1u64)); + }); // Create invoice let description = String::from_str(&env, "Test Invoice"); @@ -344,9 +394,7 @@ fn test_payment_payer_authorization() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice let description = String::from_str(&env, "Test Invoice"); @@ -373,9 +421,7 @@ fn test_payment_updates_invoice_timestamps() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice let description = String::from_str(&env, "Test Invoice"); @@ -404,15 +450,15 @@ fn test_fee_calculation_accuracy() { let (env, shade_client, shade_contract_id, admin, token) = setup_test_with_payment(); // Test with 1% fee (100 bps) + env.ledger().set_timestamp(100); shade_client.set_fee(&admin, &token, &100); + env.ledger().set_timestamp(100 + 3600); // Register merchant let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - // Create merchant account - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); // Create invoice for 10000 units let description = String::from_str(&env, "Test Invoice"); @@ -441,8 +487,7 @@ fn test_partial_payment_two_equal_steps_reaches_paid() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); let description = String::from_str(&env, "Partial Payment Invoice"); let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); @@ -477,8 +522,7 @@ fn test_partial_payment_collects_fees_proportionally_each_step() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); let description = String::from_str(&env, "Proportional Fee Invoice"); let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); @@ -497,6 +541,41 @@ fn test_partial_payment_collects_fees_proportionally_each_step() { assert_eq!(token_balance_client.balance(&merchant_account), 950); } +#[test] +fn test_fee_discount_applies_after_volume_threshold() { + let (env, shade_client, shade_contract_id, admin, token) = setup_test_with_payment(); + + env.ledger().set_timestamp(100); + shade_client.set_fee(&admin, &token, &1000); // 10% + env.ledger().set_timestamp(100 + 3600); + + let merchant = Address::generate(&env); + shade_client.register_merchant(&merchant); + let merchant_account = shade_client.get_merchant_account(&1u64); + + let payer = Address::generate(&env); + let token_client = token::StellarAssetClient::new(&env, &token); + token_client.mint(&payer, &1_020_000); + + let invoice_id_1 = + shade_client.create_invoice(&merchant, &String::from_str(&env, "Vol 1"), &1_000_000, &token, &None); + shade_client.pay_invoice(&payer, &invoice_id_1); + + let invoice_id_2 = + shade_client.create_invoice(&merchant, &String::from_str(&env, "Vol 2"), &10_000, &token, &None); + shade_client.pay_invoice(&payer, &invoice_id_2); + + let tok = token::TokenClient::new(&env, &token); + let shade_balance = tok.balance(&shade_contract_id); + let merchant_balance = tok.balance(&merchant_account); + + // First payment at 10%: 100_000 fee, second payment at 5%: 500 fee + assert_eq!(shade_balance, 100_500); + assert_eq!(merchant_balance, 1_009_500); + + assert_latest_fee_collected_event(&env, &shade_contract_id, 500, &token, &merchant_account); +} + #[test] #[should_panic(expected = "HostError: Error(Contract, #7)")] fn test_partial_payment_cannot_exceed_requested_amount() { @@ -504,8 +583,7 @@ fn test_partial_payment_cannot_exceed_requested_amount() { let merchant = Address::generate(&env); shade_client.register_merchant(&merchant); - let merchant_account = Address::generate(&env); - shade_client.set_merchant_account(&merchant, &merchant_account); + let merchant_account = shade_client.get_merchant_account(&1u64); let description = String::from_str(&env, "Overpay Guard Invoice"); let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); diff --git a/contracts/shade/src/tests/test_refund.rs b/contracts/shade/src/tests/test_refund.rs index bfc0c82..48b86ce 100644 --- a/contracts/shade/src/tests/test_refund.rs +++ b/contracts/shade/src/tests/test_refund.rs @@ -2,9 +2,8 @@ use crate::shade::{Shade, ShadeClient}; use crate::types::InvoiceStatus; -use account::account::{MerchantAccount, MerchantAccountClient}; use soroban_sdk::testutils::{Address as _, Ledger as _}; -use soroban_sdk::{token, Address, Env, String}; +use soroban_sdk::{token, Address, BytesN, Env, String}; /// Shared setup: deploy Shade, initialize, register a token with **0 fee**, /// register a merchant, deploy + link a merchant account, create an invoice, @@ -34,7 +33,8 @@ fn setup_paid_invoice(pay_timestamp: u64) -> RefundTestContext<'static> { let shade_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &shade_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); // Register token with 0 fee – keeps full amount in merchant account let token_admin = Address::generate(&env); @@ -46,11 +46,7 @@ fn setup_paid_invoice(pay_timestamp: u64) -> RefundTestContext<'static> { // Register merchant + deploy merchant account contract let merchant = Address::generate(&env); client.register_merchant(&merchant); - - let merchant_account_id = env.register(MerchantAccount, ()); - let merchant_account = MerchantAccountClient::new(&env, &merchant_account_id); - merchant_account.initialize(&merchant, &shade_id, &1_u64); - client.set_merchant_account(&merchant, &merchant_account_id); + let merchant_account_id = client.get_merchant_account(&1u64); // Create invoice let amount = 1_000_i128; @@ -216,7 +212,8 @@ fn test_refund_pending_invoice_fails() { let shade_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &shade_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let merchant = Address::generate(&env); client.register_merchant(&merchant); @@ -243,7 +240,8 @@ fn test_refund_cancelled_invoice_fails() { let shade_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &shade_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let merchant = Address::generate(&env); client.register_merchant(&merchant); @@ -318,7 +316,8 @@ fn test_partial_refund_with_fee() { let shade_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &shade_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token_contract = env.register_stellar_asset_contract_v2(token_admin.clone()); @@ -328,11 +327,7 @@ fn test_partial_refund_with_fee() { let merchant = Address::generate(&env); client.register_merchant(&merchant); - - let merchant_account_id = env.register(MerchantAccount, ()); - let merchant_account = MerchantAccountClient::new(&env, &merchant_account_id); - merchant_account.initialize(&merchant, &shade_id, &1_u64); - client.set_merchant_account(&merchant, &merchant_account_id); + let merchant_account_id = client.get_merchant_account(&1u64); let amount = 1_000_i128; let description = String::from_str(&env, "Fee Refund"); diff --git a/contracts/shade/src/tests/test_signatures.rs b/contracts/shade/src/tests/test_signatures.rs index 447dc20..1fadb19 100644 --- a/contracts/shade/src/tests/test_signatures.rs +++ b/contracts/shade/src/tests/test_signatures.rs @@ -19,7 +19,8 @@ fn setup_test() -> (Env, ShadeClient<'static>, Address, Address) { let contract_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); (env, client, contract_id, admin) } diff --git a/contracts/shade/src/tests/test_subscription.rs b/contracts/shade/src/tests/test_subscription.rs index c4a9268..0330810 100644 --- a/contracts/shade/src/tests/test_subscription.rs +++ b/contracts/shade/src/tests/test_subscription.rs @@ -2,9 +2,8 @@ use crate::shade::{Shade, ShadeClient}; use crate::types::SubscriptionStatus; -use account::account::{MerchantAccount, MerchantAccountClient}; use soroban_sdk::testutils::{Address as _, Ledger as _}; -use soroban_sdk::{token, Address, Env, String}; +use soroban_sdk::{token, Address, BytesN, Env, String}; /// Monthly interval constant: 30 days = 2 592 000 seconds. const MONTHLY_INTERVAL: u64 = 2_592_000; @@ -28,7 +27,8 @@ fn setup_subscription_env() -> SubTestContext<'static> { let shade_id = env.register(Shade, ()); let client = ShadeClient::new(&env, &shade_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); // Register token with 5% fee let token_admin = Address::generate(&env); @@ -40,11 +40,7 @@ fn setup_subscription_env() -> SubTestContext<'static> { // Register merchant + merchant account let merchant = Address::generate(&env); client.register_merchant(&merchant); - - let merchant_account_id = env.register(MerchantAccount, ()); - let merchant_account = MerchantAccountClient::new(&env, &merchant_account_id); - merchant_account.initialize(&merchant, &shade_id, &1_u64); - client.set_merchant_account(&merchant, &merchant_account_id); + let merchant_account_id = client.get_merchant_account(&1u64); // Merchant creates a monthly plan let description = String::from_str(&env, "Monthly Pro Plan"); @@ -373,7 +369,9 @@ fn test_charge_with_zero_fee() { let ctx = setup_subscription_env(); // Set fee to 0 + ctx.env.ledger().set_timestamp(100); ctx.client.set_fee(&ctx.admin, &ctx.token, &0); + ctx.env.ledger().set_timestamp(100 + 3600); let customer = Address::generate(&ctx.env); fund_and_approve(&ctx, &customer, 5_000); diff --git a/contracts/shade/src/tests/test_upgrade.rs b/contracts/shade/src/tests/test_upgrade.rs index 0d6f42d..c544796 100644 --- a/contracts/shade/src/tests/test_upgrade.rs +++ b/contracts/shade/src/tests/test_upgrade.rs @@ -42,7 +42,8 @@ fn test_admin_can_upgrade_successfully() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let v2_hash = env.deployer().upload_contract_wasm(V2_WASM); client.upgrade(&v2_hash); @@ -57,7 +58,8 @@ fn test_state_persists_after_upgrade() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let token_admin = Address::generate(&env); let token = env @@ -98,7 +100,8 @@ fn test_upgrade_emits_contract_upgraded_event() { let client = ShadeClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.initialize(&admin); + let account_wasm_hash = BytesN::from_array(&env, &[0; 32]); + client.initialize(&admin, &account_wasm_hash); let v2_hash = env.deployer().upload_contract_wasm(V2_WASM); let expected_timestamp = env.ledger().timestamp(); diff --git a/contracts/shade/src/types.rs b/contracts/shade/src/types.rs index e7e456b..deacd78 100644 --- a/contracts/shade/src/types.rs +++ b/contracts/shade/src/types.rs @@ -14,19 +14,22 @@ pub enum DataKey { MerchantCount, MerchantId(Address), TokenFee(Address), + PendingTokenFee(Address), + PendingTokenFeeActivation(Address), MerchantTokens, MerchantBalance(Address), MerchantAccount(u64), Invoice(u64), InvoiceCount, ReentrancyStatus, - AccountWasmHash, + MerchantAccountWasmHash, Role(Address, Role), UsedNonce(Address, BytesN<32>), Plan(u64), PlanCount, Subscription(u64), SubscriptionCount, + MerchantVolume(u64), } #[contracttype] @@ -41,6 +44,7 @@ pub struct ContractInfo { pub struct Merchant { pub id: u64, pub address: Address, + pub account: Address, pub active: bool, pub verified: bool, pub date_registered: u64,