diff --git a/contracts/account/src/tests/test_invoice_signed.rs b/contracts/account/src/tests/test_invoice_signed.rs index 2c1415e..1bc503f 100644 --- a/contracts/account/src/tests/test_invoice_signed.rs +++ b/contracts/account/src/tests/test_invoice_signed.rs @@ -116,6 +116,7 @@ fn test_create_invoice_signed_by_manager_success() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -152,6 +153,7 @@ fn test_create_invoice_signed_by_admin_success() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -189,6 +191,7 @@ fn test_create_invoice_signed_nonce_replay_fails() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -200,6 +203,7 @@ fn test_create_invoice_signed_nonce_replay_fails() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -225,6 +229,7 @@ fn test_create_invoice_signed_different_nonces_succeed() { &description, &amount, &token, + &None, &generate_nonce(&env), &generate_signature(&env), ); @@ -235,6 +240,7 @@ fn test_create_invoice_signed_different_nonces_succeed() { &description, &amount, &token, + &None, &generate_nonce_2(&env), &generate_signature(&env), ); @@ -261,6 +267,7 @@ fn test_create_invoice_signed_unauthorized_caller_fails() { &description, &500, &token, + &None, &generate_nonce(&env), &generate_signature(&env), ); @@ -286,6 +293,7 @@ fn test_create_invoice_signed_unregistered_merchant_fails() { &description, &500, &token, + &None, &generate_nonce(&env), &generate_signature(&env), ); @@ -311,6 +319,7 @@ fn test_create_invoice_signed_invalid_amount_fails() { &description, &0, &token, + &None, &generate_nonce(&env), &generate_signature(&env), ); @@ -338,6 +347,7 @@ fn test_create_invoice_signed_when_paused_fails() { &description, &500, &token, + &None, &generate_nonce(&env), &generate_signature(&env), ); diff --git a/contracts/shade/src/components/invoice.rs b/contracts/shade/src/components/invoice.rs index d87fdff..f0cdc62 100644 --- a/contracts/shade/src/components/invoice.rs +++ b/contracts/shade/src/components/invoice.rs @@ -17,6 +17,7 @@ pub fn create_invoice( description: &String, amount: i128, token: &Address, + expires_at: &Option, ) -> u64 { merchant_address.require_auth(); if amount <= 0 { @@ -46,6 +47,7 @@ pub fn create_invoice( payer: None, date_created: env.ledger().timestamp(), date_paid: None, + expires_at: *expires_at, amount_refunded: 0, }; env.storage() @@ -72,6 +74,7 @@ pub fn create_invoice_signed( description: &String, amount: i128, token: &Address, + expires_at: &Option, nonce: &BytesN<32>, signature: &BytesN<64>, ) -> u64 { @@ -100,6 +103,7 @@ pub fn create_invoice_signed( description, amount, token, + expires_at, nonce, signature, ); @@ -132,6 +136,7 @@ pub fn create_invoice_signed( payer: None, date_created: env.ledger().timestamp(), date_paid: None, + expires_at: *expires_at, amount_refunded: 0, }; @@ -315,6 +320,12 @@ pub fn pay_invoice(env: &Env, payer: &Address, invoice_id: u64) -> i128 { panic_with_error!(env, ContractError::InvalidInvoiceStatus); } + if let Some(expires_at) = invoice.expires_at { + if env.ledger().timestamp() > expires_at { + panic_with_error!(env, ContractError::InvoiceExpired); + } + } + // Validate that the invoice token is accepted if !admin::is_accepted_token(env, &invoice.token) { panic_with_error!(env, ContractError::TokenNotAccepted); diff --git a/contracts/shade/src/components/signature_util.rs b/contracts/shade/src/components/signature_util.rs index 4c44cbc..e18dbd9 100644 --- a/contracts/shade/src/components/signature_util.rs +++ b/contracts/shade/src/components/signature_util.rs @@ -13,6 +13,7 @@ fn build_message( description: &String, amount: i128, token: &Address, + expires_at: &Option, nonce: &BytesN<32>, ) -> Bytes { let mut msg = Bytes::new(env); @@ -22,6 +23,7 @@ fn build_message( msg.append(&Bytes::from_slice(env, &amount.to_be_bytes())); msg.append(&token.clone().to_xdr(env)); msg.append(&description.clone().to_xdr(env)); + msg.append(&Bytes::from_slice(env, &expires_at.unwrap_or(0).to_be_bytes())); msg } @@ -36,6 +38,7 @@ pub fn verify_invoice_signature( description: &String, amount: i128, token: &Address, + expires_at: &Option, nonce: &BytesN<32>, signature: &BytesN<64>, ) { @@ -45,7 +48,7 @@ pub fn verify_invoice_signature( .get(&DataKey::MerchantKey(merchant.clone())) .unwrap_or_else(|| panic_with_error!(env, ContractError::MerchantKeyNotFound)); - let message = build_message(env, merchant, description, amount, token, nonce); + let message = build_message(env, merchant, description, amount, token, expires_at, nonce); env.crypto().ed25519_verify(&key, &message, signature); } diff --git a/contracts/shade/src/errors.rs b/contracts/shade/src/errors.rs index 9f917d9..d64847c 100644 --- a/contracts/shade/src/errors.rs +++ b/contracts/shade/src/errors.rs @@ -24,4 +24,5 @@ pub enum ContractError { WasmHashNotSet = 18, InvoiceAlreadyPaid = 19, MerchantAccountNotSet = 20, + InvoiceExpired = 21, } diff --git a/contracts/shade/src/interface.rs b/contracts/shade/src/interface.rs index c942c1d..91afdd2 100644 --- a/contracts/shade/src/interface.rs +++ b/contracts/shade/src/interface.rs @@ -26,6 +26,7 @@ pub trait ShadeTrait { description: String, amount: i128, token: Address, + expires_at: Option, ) -> u64; #[allow(clippy::too_many_arguments)] fn create_invoice_signed( @@ -35,6 +36,7 @@ pub trait ShadeTrait { description: String, amount: i128, token: Address, + expires_at: Option, nonce: BytesN<32>, signature: BytesN<64>, ) -> u64; diff --git a/contracts/shade/src/shade.rs b/contracts/shade/src/shade.rs index b1ca6b4..80489aa 100644 --- a/contracts/shade/src/shade.rs +++ b/contracts/shade/src/shade.rs @@ -104,9 +104,10 @@ impl ShadeTrait for Shade { description: String, amount: i128, token: Address, + expires_at: Option, ) -> u64 { pausable_component::assert_not_paused(&env); - invoice_component::create_invoice(&env, &merchant, &description, amount, &token) + invoice_component::create_invoice(&env, &merchant, &description, amount, &token, &expires_at) } #[allow(clippy::too_many_arguments)] @@ -117,6 +118,7 @@ impl ShadeTrait for Shade { description: String, amount: i128, token: Address, + expires_at: Option, nonce: BytesN<32>, signature: BytesN<64>, ) -> u64 { @@ -128,6 +130,7 @@ impl ShadeTrait for Shade { &description, amount, &token, + &expires_at, &nonce, &signature, ) diff --git a/contracts/shade/src/tests/mod.rs b/contracts/shade/src/tests/mod.rs index cb8bbad..7b673b6 100644 --- a/contracts/shade/src/tests/mod.rs +++ b/contracts/shade/src/tests/mod.rs @@ -4,6 +4,7 @@ pub mod test_access_control; pub mod test_admin_payment; pub mod test_fees; pub mod test_invoice; +pub mod test_invoice_expiry; pub mod test_invoice_signed; pub mod test_invoice_void; pub mod test_merchant; diff --git a/contracts/shade/src/tests/test_admin_payment.rs b/contracts/shade/src/tests/test_admin_payment.rs index c5114d5..627cb21 100644 --- a/contracts/shade/src/tests/test_admin_payment.rs +++ b/contracts/shade/src/tests/test_admin_payment.rs @@ -55,6 +55,7 @@ fn test_invoice_state_validation() { &String::from_str(&env, "Test Invoice"), &1000, &token, + &None, ); // Verify initial state @@ -77,12 +78,14 @@ fn test_multiple_invoices_independent() { &String::from_str(&env, "Invoice 1"), &1000, &token, + &None, ); let id_2 = client.create_invoice( &merchant, &String::from_str(&env, "Invoice 2"), &2000, &token, + &None, ); // Set second to Paid via storage manipulation @@ -117,6 +120,7 @@ fn test_fee_preservation() { &String::from_str(&env, "Test Invoice"), &1000, &token, + &None, ); // Verify fee and invoice data @@ -159,6 +163,7 @@ fn test_contract_pause_and_unpause() { &String::from_str(&env, "Post-unpause invoice"), &500, &token, + &None, ); assert!(invoice_id > 0); } diff --git a/contracts/shade/src/tests/test_invoice.rs b/contracts/shade/src/tests/test_invoice.rs index 04194ab..644b2fa 100644 --- a/contracts/shade/src/tests/test_invoice.rs +++ b/contracts/shade/src/tests/test_invoice.rs @@ -92,7 +92,7 @@ fn test_create_and_get_invoice_success() { let description = String::from_str(&env, "Test Invoice"); let amount: i128 = 1000; - let invoice_id = client.create_invoice(&merchant, &description, &amount, &token); + let invoice_id = client.create_invoice(&merchant, &description, &amount, &token, &None); assert_eq!(invoice_id, 1); assert_latest_invoice_event(&env, &contract_id, invoice_id, &merchant, amount, &token); @@ -122,18 +122,21 @@ fn test_create_multiple_invoices() { &String::from_str(&env, "Invoice 1"), &1000, &token1, + &None, ); let id2 = client.create_invoice( &merchant, &String::from_str(&env, "Invoice 2"), &2000, &token2, + &None, ); let id3 = client.create_invoice( &merchant, &String::from_str(&env, "Invoice 3"), &500, &token1, + &None, ); assert_eq!(id1, 1); @@ -158,7 +161,7 @@ fn test_create_invoice_unregistered_merchant() { let description = String::from_str(&env, "Test Invoice"); let amount: i128 = 1000; - client.create_invoice(&unregistered_merchant, &description, &amount, &token); + client.create_invoice(&unregistered_merchant, &description, &amount, &token, &None); } #[should_panic(expected = "HostError: Error(Contract, #7)")] @@ -173,7 +176,7 @@ fn test_create_invoice_invalid_amount() { let description = String::from_str(&env, "Test Invoice"); let amount: i128 = 0; - client.create_invoice(&merchant, &description, &amount, &token); + client.create_invoice(&merchant, &description, &amount, &token, &None); } #[test] @@ -186,7 +189,7 @@ fn test_refund_invoice_success_within_window() { let payer = Address::generate(&env); let description = String::from_str(&env, "Refundable Invoice"); let amount = 1_000_i128; - let invoice_id = client.create_invoice(&merchant, &description, &amount, &token); + let invoice_id = client.create_invoice(&merchant, &description, &amount, &token, &None); let merchant_account_id = env.register(MerchantAccount, ()); let merchant_account = MerchantAccountClient::new(&env, &merchant_account_id); @@ -233,6 +236,7 @@ fn test_refund_invoice_fails_after_refund_window() { &String::from_str(&env, "Expired refund"), &500_i128, &token, + &None, ); let merchant_account_id = env.register(MerchantAccount, ()); @@ -265,7 +269,7 @@ fn test_void_invoice_success() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Verify invoice is Pending let invoice_before = client.get_invoice(&invoice_id); @@ -295,6 +299,7 @@ fn test_refund_invoice_fails_for_non_owner() { &String::from_str(&env, "Wrong owner"), &250_i128, &token, + &None, ); let merchant_account_id = env.register(MerchantAccount, ()); @@ -326,7 +331,7 @@ fn test_void_invoice_non_owner() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Try to void with different merchant (should panic with NotAuthorized) let other_merchant = Address::generate(&env); @@ -349,7 +354,7 @@ fn test_void_invoice_already_paid() { // Create and pay invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); let customer = Address::generate(&env); let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token); @@ -371,7 +376,7 @@ fn test_void_invoice_already_cancelled() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Void the invoice once client.void_invoice(&merchant, &invoice_id); @@ -395,7 +400,7 @@ fn test_pay_cancelled_invoice() { // Create invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Void the invoice client.void_invoice(&merchant, &invoice_id); @@ -431,7 +436,7 @@ fn test_amend_invoice_amount_success() { let token = Address::generate(&env); let description = String::from_str(&env, "Original Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Amend the amount client.amend_invoice(&merchant, &invoice_id, &Some(2000), &None); @@ -452,7 +457,7 @@ fn test_amend_invoice_description_success() { let token = Address::generate(&env); let description = String::from_str(&env, "Original Description"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Amend the description let new_description = String::from_str(&env, "Updated Description"); @@ -479,7 +484,7 @@ fn test_amend_invoice_both_fields_success() { let token = Address::generate(&env); let description = String::from_str(&env, "Original"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Amend both amount and description let new_description = String::from_str(&env, "Updated"); @@ -511,7 +516,7 @@ fn test_amend_invoice_paid_fails() { // Create and pay invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); let customer = Address::generate(&env); let token_client = token::StellarAssetClient::new(&env, &token); @@ -534,7 +539,7 @@ fn test_amend_invoice_cancelled_fails() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Void the invoice client.void_invoice(&merchant, &invoice_id); @@ -553,7 +558,7 @@ fn test_amend_invoice_non_owner_fails() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Try to amend with different merchant (should panic with NotAuthorized) let other_merchant = Address::generate(&env); @@ -571,7 +576,7 @@ fn test_amend_invoice_invalid_amount_fails() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Try to amend with invalid amount (should panic with InvalidAmount) client.amend_invoice(&merchant, &invoice_id, &Some(0), &None); @@ -587,7 +592,7 @@ fn test_amend_invoice_negative_amount_fails() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Try to amend with negative amount (should panic with InvalidAmount) client.amend_invoice(&merchant, &invoice_id, &Some(-100), &None); diff --git a/contracts/shade/src/tests/test_invoice_expiry.rs b/contracts/shade/src/tests/test_invoice_expiry.rs new file mode 100644 index 0000000..70c7fcf --- /dev/null +++ b/contracts/shade/src/tests/test_invoice_expiry.rs @@ -0,0 +1,108 @@ +#![cfg(test)] + +use crate::shade::{Shade, ShadeClient}; +use crate::types::InvoiceStatus; +use soroban_sdk::testutils::{Address as _, Ledger as _}; +use soroban_sdk::{token, Address, Env, String}; + +fn setup_test_with_payment() -> (Env, ShadeClient<'static>, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let shade_contract_id = env.register(Shade, ()); + let shade_client = ShadeClient::new(&env, &shade_contract_id); + + let admin = Address::generate(&env); + shade_client.initialize(&admin); + + let token_admin = Address::generate(&env); + let token = env.register_stellar_asset_contract_v2(token_admin.clone()); + + shade_client.add_accepted_token(&admin, &token.address()); + shade_client.set_fee(&admin, &token.address(), &0); + + (env, shade_client, shade_contract_id, admin, token.address()) +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #21)")] +fn test_pay_expired_invoice_fails() { + let (env, client, _shade_contract_id, _admin, token) = setup_test_with_payment(); + + let merchant = Address::generate(&env); + client.register_merchant(&merchant); + + let merchant_account = Address::generate(&env); + client.set_merchant_account(&merchant, &merchant_account); + + let description = String::from_str(&env, "Expiring Invoice"); + let expires_at: u64 = 1000; + let invoice_id = + client.create_invoice(&merchant, &description, &500, &token, &Some(expires_at)); + + // Advance ledger past the expiry + env.ledger().set_timestamp(1001); + + let customer = Address::generate(&env); + let token_client = token::StellarAssetClient::new(&env, &token); + token_client.mint(&customer, &500); + + // Should panic with InvoiceExpired (#21) + client.pay_invoice(&customer, &invoice_id); +} + +#[test] +fn test_pay_invoice_before_expiry_succeeds() { + let (env, client, _shade_contract_id, _admin, token) = setup_test_with_payment(); + + let merchant = Address::generate(&env); + client.register_merchant(&merchant); + + let merchant_account = Address::generate(&env); + client.set_merchant_account(&merchant, &merchant_account); + + let description = String::from_str(&env, "Expiring Invoice"); + let expires_at: u64 = 2000; + let invoice_id = + client.create_invoice(&merchant, &description, &500, &token, &Some(expires_at)); + + // Set timestamp before expiry + env.ledger().set_timestamp(1999); + + let customer = Address::generate(&env); + let token_client = token::StellarAssetClient::new(&env, &token); + token_client.mint(&customer, &500); + + client.pay_invoice(&customer, &invoice_id); + + let invoice = client.get_invoice(&invoice_id); + assert_eq!(invoice.status, InvoiceStatus::Paid); + assert_eq!(invoice.payer, Some(customer)); +} + +#[test] +fn test_invoice_no_expiry_always_payable() { + let (env, client, _shade_contract_id, _admin, token) = setup_test_with_payment(); + + let merchant = Address::generate(&env); + client.register_merchant(&merchant); + + let merchant_account = Address::generate(&env); + client.set_merchant_account(&merchant, &merchant_account); + + let description = String::from_str(&env, "No Expiry Invoice"); + let invoice_id = client.create_invoice(&merchant, &description, &500, &token, &None); + + // Set timestamp to a very large value + env.ledger().set_timestamp(999_999_999); + + let customer = Address::generate(&env); + let token_client = token::StellarAssetClient::new(&env, &token); + token_client.mint(&customer, &500); + + client.pay_invoice(&customer, &invoice_id); + + let invoice = client.get_invoice(&invoice_id); + assert_eq!(invoice.status, InvoiceStatus::Paid); + assert_eq!(invoice.payer, Some(customer)); +} diff --git a/contracts/shade/src/tests/test_invoice_signed.rs b/contracts/shade/src/tests/test_invoice_signed.rs index 41bbb0c..c2e840e 100644 --- a/contracts/shade/src/tests/test_invoice_signed.rs +++ b/contracts/shade/src/tests/test_invoice_signed.rs @@ -29,6 +29,7 @@ fn build_test_message( description: &String, amount: i128, token: &Address, + expires_at: &Option, nonce: &BytesN<32>, ) -> alloc::vec::Vec { let mut msg = Bytes::new(env); @@ -38,6 +39,7 @@ fn build_test_message( msg.append(&Bytes::from_slice(env, &amount.to_be_bytes())); msg.append(&token.clone().to_xdr(env)); msg.append(&description.clone().to_xdr(env)); + msg.append(&Bytes::from_slice(env, &expires_at.unwrap_or(0).to_be_bytes())); let mut result = alloc::vec![0u8; msg.len() as usize]; for i in 0..msg.len() { @@ -68,6 +70,7 @@ fn sign_invoice( description: &String, amount: i128, token: &Address, + expires_at: &Option, nonce: &BytesN<32>, ) -> BytesN<64> { let message = build_test_message( @@ -77,6 +80,7 @@ fn sign_invoice( description, amount, token, + expires_at, nonce, ); let sig = keypair.signing_key.sign(&message); @@ -127,6 +131,7 @@ fn test_create_invoice_signed_manager_success() { &description, amount, &token, + &None, &nonce, ); @@ -136,6 +141,7 @@ fn test_create_invoice_signed_manager_success() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -172,6 +178,7 @@ fn test_create_invoice_signed_admin_success() { &description, amount, &token, + &None, &nonce, ); @@ -181,6 +188,7 @@ fn test_create_invoice_signed_admin_success() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -216,6 +224,7 @@ fn test_create_invoice_signed_guest_unauthorized() { &description, 1000, &token, + &None, &nonce, ); @@ -225,6 +234,7 @@ fn test_create_invoice_signed_guest_unauthorized() { &description, &1000, &token, + &None, &nonce, &signature, ); @@ -257,6 +267,7 @@ fn test_create_invoice_signed_operator_unauthorized() { &description, 1000, &token, + &None, &nonce, ); @@ -266,6 +277,7 @@ fn test_create_invoice_signed_operator_unauthorized() { &description, &1000, &token, + &None, &nonce, &signature, ); @@ -300,6 +312,7 @@ fn test_create_invoice_signed_invalid_amount_zero() { &description, &0, &token, + &None, &nonce, &signature, ); @@ -333,6 +346,7 @@ fn test_create_invoice_signed_invalid_amount_negative() { &description, &-1000, &token, + &None, &nonce, &signature, ); @@ -361,6 +375,7 @@ fn test_create_invoice_signed_unregistered_merchant() { &description, &1000, &token, + &None, &nonce, &signature, ); @@ -404,6 +419,7 @@ fn test_create_invoice_signed_multiple_invoices() { &description, 1000, &token, + &None, &nonce1, ); let invoice_id_1 = client.create_invoice_signed( @@ -412,6 +428,7 @@ fn test_create_invoice_signed_multiple_invoices() { &description, &1000, &token, + &None, &nonce1, &sig1, ); @@ -425,6 +442,7 @@ fn test_create_invoice_signed_multiple_invoices() { &description, 2000, &token, + &None, &nonce2, ); let invoice_id_2 = client.create_invoice_signed( @@ -433,6 +451,7 @@ fn test_create_invoice_signed_multiple_invoices() { &description, &2000, &token, + &None, &nonce2, &sig2, ); @@ -446,6 +465,7 @@ fn test_create_invoice_signed_multiple_invoices() { &description, 3000, &token, + &None, &nonce3, ); let invoice_id_3 = client.create_invoice_signed( @@ -454,6 +474,7 @@ fn test_create_invoice_signed_multiple_invoices() { &description, &3000, &token, + &None, &nonce3, &sig3, ); diff --git a/contracts/shade/src/tests/test_invoice_void.rs b/contracts/shade/src/tests/test_invoice_void.rs index 6f7072a..86301d7 100644 --- a/contracts/shade/src/tests/test_invoice_void.rs +++ b/contracts/shade/src/tests/test_invoice_void.rs @@ -29,7 +29,7 @@ fn test_void_invoice_success() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); let amount: i128 = 1000; - let invoice_id = client.create_invoice(&merchant, &description, &amount, &token); + let invoice_id = client.create_invoice(&merchant, &description, &amount, &token, &None); // Verify invoice is Pending before voiding let invoice_before = client.get_invoice(&invoice_id); @@ -56,7 +56,7 @@ fn test_void_invoice_unauthorized_random_address() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Try to void with random address (should panic with NotAuthorized) let random_address = Address::generate(&env); @@ -79,7 +79,7 @@ fn test_void_invoice_unauthorized_different_merchant() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant1, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant1, &description, &1000, &token, &None); // Try to void with different merchant (should panic with NotAuthorized) client.void_invoice(&merchant2, &invoice_id); @@ -116,7 +116,7 @@ fn test_void_invoice_already_paid() { // Create invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Pay the invoice let customer = Address::generate(&env); @@ -159,7 +159,7 @@ fn test_pay_voided_invoice() { // Create invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Void the invoice client.void_invoice(&merchant, &invoice_id); @@ -184,7 +184,7 @@ fn test_void_invoice_already_cancelled() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); // Void the invoice once client.void_invoice(&merchant, &invoice_id); @@ -238,7 +238,7 @@ fn test_void_refunded_invoice() { client.set_merchant_account(&merchant, &merchant_account_id); let description = String::from_str(&env, "Refundable Invoice"); - let invoice_id = client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = client.create_invoice(&merchant, &description, &1000, &token, &None); let customer = Address::generate(&env); let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token); @@ -264,9 +264,9 @@ fn test_void_invoice_state_isolation() { let token = Address::generate(&env); let description = String::from_str(&env, "Test Invoice"); - let invoice_id_1 = client.create_invoice(&merchant, &description, &1000, &token); - let invoice_id_2 = client.create_invoice(&merchant, &description, &2000, &token); - let invoice_id_3 = client.create_invoice(&merchant, &description, &3000, &token); + let invoice_id_1 = client.create_invoice(&merchant, &description, &1000, &token, &None); + let invoice_id_2 = client.create_invoice(&merchant, &description, &2000, &token, &None); + let invoice_id_3 = client.create_invoice(&merchant, &description, &3000, &token, &None); // Void only the second invoice client.void_invoice(&merchant, &invoice_id_2); diff --git a/contracts/shade/src/tests/test_payment.rs b/contracts/shade/src/tests/test_payment.rs index a3ab9e9..079c4dc 100644 --- a/contracts/shade/src/tests/test_payment.rs +++ b/contracts/shade/src/tests/test_payment.rs @@ -44,7 +44,7 @@ fn test_successful_payment_with_fee() { // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Create customer and mint tokens let customer = Address::generate(&env); @@ -86,7 +86,7 @@ fn test_payment_with_zero_fee() { // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Create customer and mint tokens let customer = Address::generate(&env); @@ -122,7 +122,7 @@ fn test_payment_with_maximum_fee() { // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Create customer and mint tokens let customer = Address::generate(&env); @@ -156,7 +156,7 @@ fn test_payment_invoice_already_paid() { // Create invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Create customer and mint tokens let customer = Address::generate(&env); @@ -185,7 +185,7 @@ fn test_payment_insufficient_funds() { // Create invoice for 1000 units let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Create customer with insufficient balance (only 500) let customer = Address::generate(&env); @@ -215,7 +215,7 @@ fn test_payment_token_not_accepted() { let description = String::from_str(&env, "Test Invoice"); let invoice_id = - shade_client.create_invoice(&merchant, &description, &1000, &unaccepted_token.address()); + shade_client.create_invoice(&merchant, &description, &1000, &unaccepted_token.address(), &None); // Create customer and mint tokens let customer = Address::generate(&env); @@ -239,7 +239,7 @@ fn test_payment_merchant_account_not_set() { // Create invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Create customer and mint tokens let customer = Address::generate(&env); @@ -264,7 +264,7 @@ fn test_payment_payer_authorization() { // Create invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Create customer and mint tokens let customer = Address::generate(&env); @@ -293,7 +293,7 @@ fn test_payment_updates_invoice_timestamps() { // Create invoice let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &1000, &token, &None); // Get invoice before payment let invoice_before = shade_client.get_invoice(&invoice_id); @@ -330,7 +330,7 @@ fn test_fee_calculation_accuracy() { // Create invoice for 10000 units let description = String::from_str(&env, "Test Invoice"); - let invoice_id = shade_client.create_invoice(&merchant, &description, &10000, &token); + let invoice_id = shade_client.create_invoice(&merchant, &description, &10000, &token, &None); // Create customer and mint tokens let customer = Address::generate(&env); diff --git a/contracts/shade/src/tests/test_signatures.rs b/contracts/shade/src/tests/test_signatures.rs index 447dc20..b89251e 100644 --- a/contracts/shade/src/tests/test_signatures.rs +++ b/contracts/shade/src/tests/test_signatures.rs @@ -47,6 +47,7 @@ fn build_test_message( description: &String, amount: i128, token: &Address, + expires_at: &Option, nonce: &BytesN<32>, ) -> alloc::vec::Vec { let mut msg = Bytes::new(env); @@ -56,6 +57,7 @@ fn build_test_message( msg.append(&Bytes::from_slice(env, &amount.to_be_bytes())); msg.append(&token.clone().to_xdr(env)); msg.append(&description.clone().to_xdr(env)); + msg.append(&Bytes::from_slice(env, &expires_at.unwrap_or(0).to_be_bytes())); let mut result = alloc::vec![0u8; msg.len() as usize]; for i in 0..msg.len() { @@ -72,6 +74,7 @@ fn sign_invoice( description: &String, amount: i128, token: &Address, + expires_at: &Option, nonce: &BytesN<32>, ) -> BytesN<64> { let message = build_test_message( @@ -81,6 +84,7 @@ fn sign_invoice( description, amount, token, + expires_at, nonce, ); let sig = keypair.signing_key.sign(&message); @@ -129,6 +133,7 @@ fn test_valid_signature() { &description, amount, &token, + &None, &nonce, ); @@ -138,6 +143,7 @@ fn test_valid_signature() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -181,6 +187,7 @@ fn test_invalid_signature_tampered_amount() { &description, original_amount, &token, + &None, &nonce, ); @@ -191,6 +198,7 @@ fn test_invalid_signature_tampered_amount() { &description, &tampered_amount, &token, + &None, &nonce, &signature, ); @@ -227,6 +235,7 @@ fn test_invalid_signature_tampered_description() { &original_desc, amount, &token, + &None, &nonce, ); @@ -237,6 +246,7 @@ fn test_invalid_signature_tampered_description() { &tampered_desc, &amount, &token, + &None, &nonce, &signature, ); @@ -271,6 +281,7 @@ fn test_replay_attack_same_nonce() { &description, amount, &token, + &None, &nonce, ); @@ -281,6 +292,7 @@ fn test_replay_attack_same_nonce() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -292,6 +304,7 @@ fn test_replay_attack_same_nonce() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -336,6 +349,7 @@ fn test_wrong_merchant_signature() { &description, amount, &token, + &None, &nonce, ); @@ -346,6 +360,7 @@ fn test_wrong_merchant_signature() { &description, &amount, &token, + &None, &nonce, &signature_a, ); @@ -380,6 +395,7 @@ fn test_no_public_key() { &description, &amount, &token, + &None, &nonce, &signature, ); @@ -426,6 +442,7 @@ fn test_nonce_independence_per_merchant() { &description, amount, &token, + &None, &shared_nonce, ); let id_a = client.create_invoice_signed( @@ -434,6 +451,7 @@ fn test_nonce_independence_per_merchant() { &description, &amount, &token, + &None, &shared_nonce, &sig_a, ); @@ -447,6 +465,7 @@ fn test_nonce_independence_per_merchant() { &description, amount, &token, + &None, &shared_nonce, ); let id_b = client.create_invoice_signed( @@ -455,6 +474,7 @@ fn test_nonce_independence_per_merchant() { &description, &amount, &token, + &None, &shared_nonce, &sig_b, ); diff --git a/contracts/shade/src/types.rs b/contracts/shade/src/types.rs index 2bfa77c..78c9fe6 100644 --- a/contracts/shade/src/types.rs +++ b/contracts/shade/src/types.rs @@ -53,6 +53,7 @@ pub struct Invoice { pub payer: Option
, pub date_created: u64, pub date_paid: Option, + pub expires_at: Option, pub amount_refunded: i128, }