diff --git a/src/component/transfer/mock.cairo b/src/component/transfer/mock.cairo new file mode 100644 index 0000000..6304b48 --- /dev/null +++ b/src/component/transfer/mock.cairo @@ -0,0 +1,33 @@ +use starknet::ContractAddress; + +#[starknet::contract] +pub mod MockTransferContract { + use starkremit_contract::component::transfer::transfer::transfer_component; + use super::*; + + component!(path: transfer_component, storage: transfer_component, event: TransferEvent); + + #[abi(embed_v0)] + impl TransferImpl = transfer_component::Transfer; + + // Event definitions + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + TransferEvent: transfer_component::Event, + } + + // Contract storage definition + #[storage] + #[allow(starknet::colliding_storage_paths)] + struct Storage { + #[substorage(v0)] + transfer_component: transfer_component::Storage, + } + + // Contract constructor + #[constructor] + fn constructor(ref self: ContractState) { // No initialization needed for transfer component + } +} diff --git a/src/component/transfer/test.cairo b/src/component/transfer/test.cairo new file mode 100644 index 0000000..bd130c8 --- /dev/null +++ b/src/component/transfer/test.cairo @@ -0,0 +1,260 @@ +#[cfg(test)] +mod transfer_tests { + use snforge_std::{ + ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, + start_cheat_block_timestamp, start_cheat_caller_address, stop_cheat_caller_address, + }; + use starknet::{ContractAddress, contract_address_const}; + use starkremit_contract::base::types::TransferStatus; + use starkremit_contract::component::transfer::transfer::{ + ITransferDispatcher, ITransferDispatcherTrait, transfer_component, + }; + + pub fn SENDER() -> ContractAddress { + contract_address_const::<'SENDER'>() + } + + pub fn RECIPIENT() -> ContractAddress { + contract_address_const::<'RECIPIENT'>() + } + + pub fn ADMIN() -> ContractAddress { + contract_address_const::<'ADMIN'>() + } + + fn deploy_transfer_contract() -> (ContractAddress, ITransferDispatcher) { + let transfer_class_hash = declare("MockTransferContract").unwrap().contract_class(); + let mut constructor_calldata = array![]; + let (contract_address, _) = transfer_class_hash.deploy(@constructor_calldata).unwrap(); + + let transfer_dispatcher = ITransferDispatcher { contract_address: contract_address }; + + (contract_address, transfer_dispatcher) + } + + #[test] + fn test_process_expired_transfers_success() { + let (contract_address, transfer_dispatcher) = deploy_transfer_contract(); + let mut spy = spy_events(); + + // Create a transfer that will expire + let transfer_amount = 1000; + let current_time = 1000000000; // Current timestamp + let expiry_time = current_time + 3600; // Expires in 1 hour + + // Set up the transfer + start_cheat_caller_address(contract_address, SENDER()); + start_cheat_block_timestamp(contract_address, current_time); + let transfer_id = transfer_dispatcher + .initiate_transfer(RECIPIENT(), transfer_amount, expiry_time, 'metadata'); + stop_cheat_caller_address(contract_address); + + // Verify transfer is initially pending + let transfer = transfer_dispatcher.get_transfer(transfer_id); + assert!(transfer.status == TransferStatus::Pending, "Transfer should be pending"); + assert!(transfer.expires_at == expiry_time, "Expiry time should match"); + + // Fast forward time to after expiry + let future_time = expiry_time + 1; + start_cheat_block_timestamp(contract_address, future_time); + + // Process expired transfers + let processed_count = transfer_dispatcher.process_expired_transfers(10); + assert!(processed_count == 1, "Should process 1 expired transfer"); + + // Verify transfer is now expired + let expired_transfer = transfer_dispatcher.get_transfer(transfer_id); + assert!(expired_transfer.status == TransferStatus::Expired, "Transfer should be expired"); + assert!(expired_transfer.updated_at == future_time, "Updated time should be future time"); + + // Verify statistics + let (total, _completed, _cancelled, expired) = transfer_dispatcher + .get_transfer_statistics(); + assert!(total == 1, "Total transfers should be 1"); + assert!(expired == 1, "Expired transfers should be 1"); + } + + #[test] + fn test_process_multiple_expired_transfers() { + let (contract_address, transfer_dispatcher) = deploy_transfer_contract(); + let mut _spy = spy_events(); + + let current_time = 1000000000; + let expiry_time = current_time + 3600; // Expires in 1 hour + + // Create multiple transfers + start_cheat_caller_address(contract_address, SENDER()); + start_cheat_block_timestamp(contract_address, current_time); + + let transfer_id1 = transfer_dispatcher + .initiate_transfer(RECIPIENT(), 1000, expiry_time, 'metadata1'); + + let transfer_id2 = transfer_dispatcher + .initiate_transfer(RECIPIENT(), 2000, expiry_time, 'metadata2'); + + let transfer_id3 = transfer_dispatcher + .initiate_transfer( + RECIPIENT(), 3000, expiry_time + 7200, // This one expires later + 'metadata3', + ); + + stop_cheat_caller_address(contract_address); + + // Fast forward time to after first two transfers expire + let future_time = expiry_time + 1; + start_cheat_block_timestamp(contract_address, future_time); + + // Process expired transfers with limit of 5 + let processed_count = transfer_dispatcher.process_expired_transfers(5); + assert!(processed_count == 2, "Should process 2 expired transfers"); + + // Verify first two transfers are expired + let transfer1 = transfer_dispatcher.get_transfer(transfer_id1); + let transfer2 = transfer_dispatcher.get_transfer(transfer_id2); + let transfer3 = transfer_dispatcher.get_transfer(transfer_id3); + + assert!(transfer1.status == TransferStatus::Expired, "Transfer 1 should be expired"); + assert!(transfer2.status == TransferStatus::Expired, "Transfer 2 should be expired"); + assert!(transfer3.status == TransferStatus::Pending, "Transfer 3 should still be pending"); + + // Verify statistics + let (total, _completed, _cancelled, expired) = transfer_dispatcher + .get_transfer_statistics(); + assert!(total == 3, "Total transfers should be 3"); + assert!(expired == 2, "Expired transfers should be 2"); + } + + #[test] + #[should_panic(expected: ('Invalid transfer status',))] + fn test_complete_transfer_after_expiry_panics() { + let (contract_address, transfer_dispatcher) = deploy_transfer_contract(); + + let current_time = 1000000000; + let expiry_time = current_time + 3600; // Expires in 1 hour + + // Create a transfer + start_cheat_caller_address(contract_address, SENDER()); + start_cheat_block_timestamp(contract_address, current_time); + let transfer_id = transfer_dispatcher + .initiate_transfer(RECIPIENT(), 1000, expiry_time, 'metadata'); + stop_cheat_caller_address(contract_address); + + // Fast forward time to after expiry + let future_time = expiry_time + 1; + start_cheat_block_timestamp(contract_address, future_time); + + // Process expired transfers to mark as expired + transfer_dispatcher.process_expired_transfers(10); + + // Try to complete the expired transfer - this should panic + start_cheat_caller_address(contract_address, RECIPIENT()); + transfer_dispatcher.complete_transfer(transfer_id); + stop_cheat_caller_address(contract_address); + } + + #[test] + #[should_panic(expected: ('Invalid transfer status',))] + fn test_partial_complete_transfer_after_expiry_panics() { + let (contract_address, transfer_dispatcher) = deploy_transfer_contract(); + + let current_time = 1000000000; + let expiry_time = current_time + 3600; // Expires in 1 hour + + // Create a transfer + start_cheat_caller_address(contract_address, SENDER()); + start_cheat_block_timestamp(contract_address, current_time); + let transfer_id = transfer_dispatcher + .initiate_transfer(RECIPIENT(), 1000, expiry_time, 'metadata'); + stop_cheat_caller_address(contract_address); + + // Fast forward time to after expiry + let future_time = expiry_time + 1; + start_cheat_block_timestamp(contract_address, future_time); + + // Process expired transfers to mark as expired + transfer_dispatcher.process_expired_transfers(10); + + // Try to partially complete the expired transfer - this should panic + start_cheat_caller_address(contract_address, RECIPIENT()); + transfer_dispatcher.partial_complete_transfer(transfer_id, 500); + stop_cheat_caller_address(contract_address); + } + + #[test] + #[should_panic(expected: ('Invalid transfer status',))] + fn test_request_cash_out_after_expiry_panics() { + let (contract_address, transfer_dispatcher) = deploy_transfer_contract(); + + let current_time = 1000000000; + let expiry_time = current_time + 3600; // Expires in 1 hour + + // Create a transfer + start_cheat_caller_address(contract_address, SENDER()); + start_cheat_block_timestamp(contract_address, current_time); + let transfer_id = transfer_dispatcher + .initiate_transfer(RECIPIENT(), 1000, expiry_time, 'metadata'); + stop_cheat_caller_address(contract_address); + + // Fast forward time to after expiry + let future_time = expiry_time + 1; + start_cheat_block_timestamp(contract_address, future_time); + + // Process expired transfers to mark as expired + transfer_dispatcher.process_expired_transfers(10); + + // Try to request cash out for the expired transfer - this should panic + start_cheat_caller_address(contract_address, RECIPIENT()); + transfer_dispatcher.request_cash_out(transfer_id); + stop_cheat_caller_address(contract_address); + } + + #[test] + fn test_process_expired_transfers_with_limit() { + let (contract_address, transfer_dispatcher) = deploy_transfer_contract(); + + let current_time = 1000000000; + let expiry_time = current_time + 3600; // Expires in 1 hour + + // Create 5 transfers that will expire + start_cheat_caller_address(contract_address, SENDER()); + start_cheat_block_timestamp(contract_address, current_time); + + let mut i: u32 = 0; + while i != 5 { + let _transfer_id = transfer_dispatcher + .initiate_transfer(RECIPIENT(), 1000, expiry_time, 'metadata'); + i += 1; + } + + stop_cheat_caller_address(contract_address); + + // Fast forward time to after expiry + let future_time = expiry_time + 1; + start_cheat_block_timestamp(contract_address, future_time); + + // Process only 3 expired transfers (limit) + let processed_count = transfer_dispatcher.process_expired_transfers(3); + assert!(processed_count == 3, "Should process exactly 3 expired transfers"); + + // Verify statistics + let (total, _completed, _cancelled, expired) = transfer_dispatcher + .get_transfer_statistics(); + assert!(total == 5, "Total transfers should be 5"); + assert!(expired == 3, "Expired transfers should be 3"); + } + + #[test] + fn test_no_transfers_to_expire() { + let (_contract_address, transfer_dispatcher) = deploy_transfer_contract(); + + // Process expired transfers when no transfers exist + let processed_count = transfer_dispatcher.process_expired_transfers(10); + assert!(processed_count == 0, "Should process 0 expired transfers"); + + // Verify statistics remain at 0 + let (total, _completed, _cancelled, expired) = transfer_dispatcher + .get_transfer_statistics(); + assert!(total == 0, "Total transfers should be 0"); + assert!(expired == 0, "Expired transfers should be 0"); + } +} diff --git a/src/component/transfer.cairo b/src/component/transfer/transfer.cairo similarity index 86% rename from src/component/transfer.cairo rename to src/component/transfer/transfer.cairo index 4e433c3..df86483 100644 --- a/src/component/transfer.cairo +++ b/src/component/transfer/transfer.cairo @@ -25,6 +25,7 @@ pub trait ITransfer { self: @TContractState, recipient: ContractAddress, limit: u32, offset: u32, ) -> Array; fn get_transfer_statistics(self: @TContractState) -> (u256, u256, u256, u256); + fn process_expired_transfers(ref self: TContractState, limit: u32) -> u32; } #[starknet::component] @@ -128,9 +129,14 @@ pub mod transfer_component { assert(expires_at > current_time, 'Expiry must be in future'); assert(expires_at <= current_time + 86400 * 30, 'Expiry too far in future'); let transfer_id = self.next_transfer_id.read(); + println!( + "Initiating transfer with ID: {} and the new id now is {}", + transfer_id, + transfer_id + 1, + ); self.next_transfer_id.write(transfer_id + 1); let transfer = TransferData { - transfer_id, + transfer_id: transfer_id + 1, sender: caller, recipient, amount, @@ -142,14 +148,14 @@ pub mod transfer_component { partial_amount: 0, metadata, }; - self.transfers.write(transfer_id, transfer); + self.transfers.write(transfer_id + 1, transfer); let sender_count = self.user_sent_count.read(caller); assert(sender_count < 4294967295, 'Max transfers per user exceeded'); - self.user_sent_transfers.write((caller, sender_count), transfer_id); + self.user_sent_transfers.write((caller, sender_count), transfer_id + 1); self.user_sent_count.write(caller, sender_count + 1); let recipient_count = self.user_received_count.read(recipient); assert(recipient_count < 4294967295, 'Max transfers per user exceeded'); - self.user_received_transfers.write((recipient, recipient_count), transfer_id); + self.user_received_transfers.write((recipient, recipient_count), transfer_id + 1); self.user_received_count.write(recipient, recipient_count + 1); let total = self.total_transfers.read(); self.total_transfers.write(total + 1); @@ -157,11 +163,15 @@ pub mod transfer_component { .emit( Event::TransferCreated( TransferCreated { - transfer_id, sender: caller, recipient, amount, expires_at, + transfer_id: transfer_id + 1, + sender: caller, + recipient, + amount, + expires_at, }, ), ); - transfer_id + transfer_id + 1 } fn cancel_transfer(ref self: ComponentState, transfer_id: u256) -> bool { let caller = get_caller_address(); @@ -357,5 +367,46 @@ pub mod transfer_component { self.total_expired_transfers.read(), ) } + + /// Process expired transfers (admin only) + fn process_expired_transfers(ref self: ComponentState, limit: u32) -> u32 { + let current_time = get_block_timestamp(); + let mut processed_count = 0; + let mut transfer_id = 1; // Start from first transfer ID + + // Iterate through transfers to find expired ones + while processed_count < limit && transfer_id <= self.next_transfer_id.read() { + let transfer = self.transfers.read(transfer_id); + + // Check if transfer exists and is still pending but expired + if transfer.transfer_id != 0 + && transfer.status == TransferStatus::Pending + && current_time > transfer.expires_at { + // Update transfer status to expired + let mut updated_transfer = transfer; + updated_transfer.status = TransferStatus::Expired; + updated_transfer.updated_at = current_time; + self.transfers.write(transfer_id, updated_transfer); + + // Update expired transfers counter + let expired_count = self.total_expired_transfers.read(); + self.total_expired_transfers.write(expired_count + 1); + + // Emit TransferExpired event + self + .emit( + Event::TransferExpired( + TransferExpired { transfer_id, expired_at: current_time }, + ), + ); + + processed_count += 1; + } + + transfer_id += 1; + } + + processed_count + } } } diff --git a/src/lib.cairo b/src/lib.cairo index 6d5dc33..e3da533 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -36,7 +36,11 @@ pub mod component { pub mod test; pub mod token_management; } - pub mod transfer; + pub mod transfer { + pub mod mock; + pub mod test; + pub mod transfer; + } pub mod user_management { pub mod mock; pub mod test; diff --git a/src/starkremit/StarkRemit.cairo b/src/starkremit/StarkRemit.cairo index 50121c5..1ba837d 100644 --- a/src/starkremit/StarkRemit.cairo +++ b/src/starkremit/StarkRemit.cairo @@ -34,7 +34,7 @@ pub mod StarkRemit { use starkremit_contract::component::loan::loan_component; use starkremit_contract::component::savings_group::savings_group_component; use starkremit_contract::component::token_management::token_management::token_management_component; - use starkremit_contract::component::transfer::transfer_component; + use starkremit_contract::component::transfer::transfer::transfer_component; use starkremit_contract::component::user_management::user_management::user_management_component; use super::*; @@ -1160,6 +1160,8 @@ pub mod StarkRemit { || transfer.status == TransferStatus::PartialComplete, TransferErrors::INVALID_TRANSFER_STATUS, ); + // Check if transfer has expired + assert(current_time <= transfer.expires_at, 'Transfer has expired'); // Only recipient or assigned agent can complete let zero_address: ContractAddress = 0.try_into().unwrap(); @@ -1236,6 +1238,8 @@ pub mod StarkRemit { || transfer.status == TransferStatus::PartialComplete, TransferErrors::INVALID_TRANSFER_STATUS, ); + // Check if transfer has expired + assert(current_time <= transfer.expires_at, 'Transfer has expired'); // Only recipient or assigned agent can complete let zero_address: ContractAddress = 0.try_into().unwrap(); @@ -1310,6 +1314,8 @@ pub mod StarkRemit { assert( transfer.status == TransferStatus::Pending, TransferErrors::INVALID_TRANSFER_STATUS, ); + // Check if transfer has expired + assert(current_time <= transfer.expires_at, 'Transfer has expired'); assert(caller == transfer.recipient, TransferErrors::UNAUTHORIZED_TRANSFER_OP); // Update transfer status @@ -1469,12 +1475,41 @@ pub mod StarkRemit { /// Process expired transfers (admin only) fn process_expired_transfers(ref self: ContractState, limit: u32) -> u32 { - let _ = get_caller_address(); + let _caller = get_caller_address(); self.accesscontrol.assert_only_role(ADMIN_ROLE); - // This is a simplified implementation - // In production, you'd iterate through transfers and mark expired ones - 0 + let current_time = get_block_timestamp(); + let mut processed_count = 0; + let mut transfer_id = 1; // Start from first transfer ID + + // Iterate through transfers to find expired ones + while processed_count < limit && transfer_id <= self.next_transfer_id.read() { + let transfer = self.transfers.read(transfer_id); + + // Check if transfer exists and is still pending but expired + if transfer.transfer_id != 0 + && transfer.status == TransferStatus::Pending + && current_time > transfer.expires_at { + // Update transfer status to expired + let mut updated_transfer = transfer; + updated_transfer.status = TransferStatus::Expired; + updated_transfer.updated_at = current_time; + self.transfers.write(transfer_id, updated_transfer); + + // Update expired transfers counter + let expired_count = self.total_expired_transfers.read(); + self.total_expired_transfers.write(expired_count + 1); + + // Emit TransferExpired event + self.emit(TransferExpired { transfer_id, timestamp: current_time }); + + processed_count += 1; + } + + transfer_id += 1; + } + + processed_count } /// Assign agent to transfer (admin only)