From 2daf0643f8adf06cfddf00a58e6832c60ccdb14a Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Sat, 26 Apr 2025 02:19:02 +0100 Subject: [PATCH 1/3] Feat: implemented get_project_transactions function --- src/budgetchain/Budget.cairo | 101 ++++++++++++++++++---- src/interfaces/IBudget.cairo | 19 ++-- tests/test_budgetchain.cairo | 162 +++++++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+), 26 deletions(-) diff --git a/src/budgetchain/Budget.cairo b/src/budgetchain/Budget.cairo index 65ed71f..76cbefe 100644 --- a/src/budgetchain/Budget.cairo +++ b/src/budgetchain/Budget.cairo @@ -53,6 +53,7 @@ pub mod Budget { org_milestones: Map, // org to number of milestones they have all_transactions: Vec, project_transaction_ids: Map>, + project_transaction_count: Map, // project_id -> count #[substorage(v0)] accesscontrol: AccessControlComponent::Storage, #[substorage(v0)] @@ -164,17 +165,41 @@ pub mod Budget { #[abi(embed_v0)] impl BudgetImpl of IBudget { - // fn create_transaction( - // ref self: ContractState, - // project_id: u64, - // recipient: ContractAddress, - // amount: u128, - // category: felt252, - // description: felt252, - // ) -> Result { + fn create_transaction( + ref self: ContractState, + project_id: u64, + recipient: ContractAddress, + amount: u128, + category: felt252, + description: felt252, + ) -> Result { + // Ensure the contract is not paused + self.assert_not_paused(); + + // Generate new transaction ID + let transaction_id = self.transaction_count.read(); + let sender = get_caller_address(); + let timestamp = get_block_timestamp(); + let transaction = Transaction { + id: transaction_id, + project_id: project_id, + sender: sender, + recipient: recipient, + amount: amount, + timestamp: timestamp, + category: category, + description: description, + }; + self.transactions.write(transaction_id, transaction); + self.transaction_count.write(transaction_id + 1); + + // Use category as project_id + let count = self.project_transaction_count.read(project_id); + self.project_transaction_ids.entry(project_id).append().write(transaction_id); + self.project_transaction_count.write(project_id, count + 1); - // // Ensure the contract is not paused - // self.assert_not_paused();} + Result::Ok(transaction_id) + } fn get_transaction(self: @ContractState, id: u64) -> Result { assert(id > 0 && id <= self.transaction_count.read(), ERROR_INVALID_TRANSACTION_ID); @@ -623,16 +648,56 @@ pub mod Budget { self.is_paused.read() } // fn request_funds( - // ref self: ContractState, - // requester: ContractAddress, - // project_id: u64, - // milestone_id: u64, - // request_id: u64, - // ) -> u64 { - // Ensure the contract is not paused - // self.assert_not_paused(); + // ref self: ContractState, + // requester: ContractAddress, + // project_id: u64, + // milestone_id: u64, + // request_id: u64, + // ) -> u64 { + // Ensure the contract is not paused + // self.assert_not_paused(); // } + + /// @notice Retrieves a paginated list of transactions for a specific project. + /// @param project_id The ID of the project whose transactions are to be retrieved. + /// @param page The page number (1-based). + /// @param page_size The number of transactions per page (max 100). + /// @return Result<(Array, u64), felt252> A tuple containing the paginated + /// transactions and the total count, or an error code. + fn get_project_transactions( + self: @ContractState, project_id: u64, page: u64, page_size: u64, + ) -> Result<(Array, u64), felt252> { + if page == 0 { + return Result::Err(ERROR_INVALID_PAGE); + } + if page_size == 0 || page_size > 100 { + return Result::Err(ERROR_INVALID_PAGE_SIZE); + } + let total = self.project_transaction_count.read(project_id); + if total == 0 { + return Result::Err(ERROR_NO_TRANSACTIONS); + } + let start = (page - 1) * page_size; + if start >= total { + return Result::Ok((ArrayTrait::new(), total)); + } + let end = if start + page_size > total { + total + } else { + start + page_size + }; + let mut txs = ArrayTrait::new(); + + let mut i = start; + while i < end { + let tx_id = self.project_transaction_ids.entry(project_id).at(i).read(); + let tx = self.transactions.read(tx_id); + txs.append(tx); + i += 1; + }; + Result::Ok((txs, total)) + } } #[generate_trait] diff --git a/src/interfaces/IBudget.cairo b/src/interfaces/IBudget.cairo index 611c078..f4ea49f 100644 --- a/src/interfaces/IBudget.cairo +++ b/src/interfaces/IBudget.cairo @@ -6,14 +6,14 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IBudget { // Transaction Management - // fn create_transaction( - // ref self: TContractState, - // project_id: u64, - // recipient: ContractAddress, - // amount: u128, - // category: felt252, - // description: felt252, - // ) -> Result; + fn create_transaction( + ref self: TContractState, + project_id: u64, + recipient: ContractAddress, + amount: u128, + category: felt252, + description: felt252, + ) -> Result; fn get_transaction(self: @TContractState, id: u64) -> Result; // fn get_transaction_history( // self: @TContractState, page: u64, page_size: u64, @@ -88,6 +88,9 @@ pub trait IBudget { fn check_owner(self: @TContractState, requester: ContractAddress, project_id: u64); fn set_fund_requests_counter(ref self: TContractState, value: u64) -> bool; fn get_fund_requests_counter(self: @TContractState) -> u64; + fn get_project_transactions( + self: @TContractState, project_id: u64, page: u64, page_size: u64, + ) -> Result<(Array, u64), felt252>; fn pause_contract(ref self: TContractState); fn unpause_contract(ref self: TContractState); diff --git a/tests/test_budgetchain.cairo b/tests/test_budgetchain.cairo index ac1b396..0fadcb0 100644 --- a/tests/test_budgetchain.cairo +++ b/tests/test_budgetchain.cairo @@ -1213,3 +1213,165 @@ fn test_get_project_budget_initial() { let remaining_budget = budget_dispatcher.get_project_budget(project_id); assert(remaining_budget == total_budget, 'Initial budget incorrect'); } + +#[test] +fn test_get_project_transactions_basic() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let recipient = contract_address_const::<'recipient'>(); + let project_id: u64 = 42; + let category: felt252 = project_id.into(); + let description = 'Test transaction'; + // Create 5 transactions for the project + let mut i = 0_u64; + while i < 5_u64 { + dispatcher.create_transaction(project_id, recipient, 1000, category, description).unwrap(); + i += 1_u64; + }; + // Retrieve all transactions (page 1, size 5) + let (txs, total) = dispatcher.get_project_transactions(project_id, 1, 5).unwrap(); + + assert(total == 5_u64, 'Total should be 5'); + assert(txs.len() == 5_u32, 'Should return 5 transactions'); +} + +#[test] +fn test_get_project_transactions_pagination_and_integrity() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let recipient = contract_address_const::<'recipient'>(); + let project_id: u64 = 42; + let category: felt252 = project_id.into(); + let description = 'Test transaction'; + // Create 25 transactions for the project + let mut i = 0_u64; + while i < 25_u64 { + let amount: u128 = (1000 + i).into(); + dispatcher + .create_transaction(project_id, recipient, amount, category, description) + .unwrap(); + i += 1_u64; + }; + + // Retrieve first page (page 1, size 10) + let (txs, total) = dispatcher.get_project_transactions(project_id, 1, 10).unwrap(); + assert(total == 25_u64, 'Total should be 25'); + assert(txs.len() == 10_u32, 'Should return 10 transactions'); + // Check data integrity and order + let mut j = 0_u32; + while j < 10_u32 { + let tx = txs.get(j).unwrap(); + let expected_id: u64 = j.into(); + let expected_amount: u128 = (1000 + j).into(); + + assert(tx.id == expected_id, 'ID mismatch'); + assert(tx.amount == expected_amount, 'Amount mismatch'); + assert(tx.category == category, 'Category mismatch'); + assert(tx.description == description, 'Description mismatch'); + j += 1_u32; + }; + + // Retrieve last page (page 3, size 10) + let (txs_last, _) = dispatcher.get_project_transactions(project_id, 3, 10).unwrap(); + assert(txs_last.len() == 5_u32, 'Page should have 5 transactions'); + // Retrieve out-of-range page (page 4, size 10) + let (txs_empty, _) = dispatcher.get_project_transactions(project_id, 4, 10).unwrap(); + assert(txs_empty.len() == 0_u32, 'Out-of-range, should be empty'); +} + +#[test] +#[should_panic] +fn test_get_project_transactions_no_transactions() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let project_id: u64 = 9999; + dispatcher.get_project_transactions(project_id, 1, 10).unwrap(); +} + +#[test] +#[should_panic] +fn test_get_project_transactions_invalid_page() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let project_id: u64 = 1; + dispatcher.get_project_transactions(project_id, 0, 10).unwrap(); +} + +#[test] +#[should_panic] +fn test_get_project_transactions_invalid_page_size_zero() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let project_id: u64 = 1; + dispatcher.get_project_transactions(project_id, 1, 0).unwrap(); +} + +#[test] +#[should_panic] +fn test_get_project_transactions_invalid_page_size_too_large() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let project_id: u64 = 1; + dispatcher.get_project_transactions(project_id, 1, 101).unwrap(); +} + +#[test] +fn test_get_project_transactions_single_transaction() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let recipient = contract_address_const::<'recipient'>(); + let project_id: u64 = 7; + let category: felt252 = project_id.into(); + let description = 'Single transaction'; + dispatcher.create_transaction(project_id, recipient, 1234, category, description).unwrap(); + let (txs, total) = dispatcher.get_project_transactions(project_id, 1, 10).unwrap(); + assert(total == 1_u64, 'Total should be 1'); + assert(txs.len() == 1_u32, 'Should return 1 transaction'); + let tx = txs.get(0).unwrap(); + assert(tx.amount == 1234, 'Amount mismatch'); + assert(tx.description == description, 'Description mismatch'); +} + +#[test] +fn test_get_project_transactions_multiple_projects_isolation() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let recipient = contract_address_const::<'recipient'>(); + let project_id1: u64 = 100; + let project_id2: u64 = 200; + let category1: felt252 = project_id1.into(); + let category2: felt252 = project_id2.into(); + dispatcher.create_transaction(project_id1, recipient, 1, category1, 'P1-T1').unwrap(); + dispatcher.create_transaction(project_id2, recipient, 2, category2, 'P2-T1').unwrap(); + dispatcher.create_transaction(project_id1, recipient, 3, category1, 'P1-T2').unwrap(); + let (txs1, total1) = dispatcher.get_project_transactions(project_id1, 1, 10).unwrap(); + let (txs2, total2) = dispatcher.get_project_transactions(project_id2, 1, 10).unwrap(); + assert!(total1 == 2_u64, "Project 1 should have 2 transactions"); + assert!(total2 == 1_u64, "Project 2 should have 1 transaction"); + assert!(txs1.len() == 2_u32, "Project 1 should return 2 transactions"); + assert!(txs2.len() == 1_u32, "Project 2 should return 1 transaction"); + assert(txs1.get(0).unwrap().description == 'P1-T1', 'Project 1, Tx 1 desc mismatch'); + assert(txs1.get(1).unwrap().description == 'P1-T2', 'Project 1, Tx 2 desc mismatch'); + assert(txs2.get(0).unwrap().description == 'P2-T1', 'Project 2, Tx 1 desc mismatch'); +} + +#[test] +fn test_project_transaction_count_and_storage() { + let (contract_address, _) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + let recipient = contract_address_const::<'recipient'>(); + let project_id: u64 = 55; + let category: felt252 = project_id.into(); + let description = 'Count test'; + // Add 3 transactions + dispatcher.create_transaction(project_id, recipient, 1, category, description).unwrap(); + dispatcher.create_transaction(project_id, recipient, 2, category, description).unwrap(); + dispatcher.create_transaction(project_id, recipient, 3, category, description).unwrap(); + let (txs, total) = dispatcher.get_project_transactions(project_id, 1, 10).unwrap(); + assert(total == 3_u64, 'Total should be 3'); + assert(txs.len() == 3_u32, 'Should return 3 transactions'); + // Check order and IDs + assert(txs.get(0).unwrap().id == 0_u64, 'First tx id should be 0'); + assert(txs.get(1).unwrap().id == 1_u64, 'Second tx id should be 1'); + assert(txs.get(2).unwrap().id == 2_u64, 'Third tx id should be 2'); +} From 3ffbb8e4f37ce8e56e66bf96cc7106a463e61030 Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Tue, 29 Apr 2025 19:03:18 +0100 Subject: [PATCH 2/3] implemented get_project_transactions function --- .tool-versions | 1 - 1 file changed, 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index 2ea00a0..f3c4ddf 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,2 @@ scarb 2.9.2 -snforge_std 0.36.0 starknet-foundry 0.36.0 From 83a76f28616642dfb8e9af08d137003154d5065d Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Tue, 29 Apr 2025 19:58:35 +0100 Subject: [PATCH 3/3] Feat: implemented get_project_transactions function #57 --- src/budgetchain/Budget.cairo | 38 ++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/budgetchain/Budget.cairo b/src/budgetchain/Budget.cairo index c973d8f..8215e62 100644 --- a/src/budgetchain/Budget.cairo +++ b/src/budgetchain/Budget.cairo @@ -217,10 +217,40 @@ pub mod Budget { // ) -> Result, felt252> {} // New function: Get all transactions for a specific project - // fn get_project_transactions( - // self: @ContractState, project_id: u64, page: u64, page_size: u64, - // ) -> Result<(Array, u64), felt252> { - // } + + fn get_project_transactions( + self: @ContractState, project_id: u64, page: u64, page_size: u64, + ) -> Result<(Array, u64), felt252> { + if page == 0 { + return Result::Err(ERROR_INVALID_PAGE); + } + if page_size == 0 || page_size > 100 { + return Result::Err(ERROR_INVALID_PAGE_SIZE); + } + let total = self.project_transaction_count.read(project_id); + if total == 0 { + return Result::Err(ERROR_NO_TRANSACTIONS); + } + let start = (page - 1) * page_size; + if start >= total { + return Result::Ok((ArrayTrait::new(), total)); + } + let end = if start + page_size > total { + total + } else { + start + page_size + }; + let mut txs = ArrayTrait::new(); + + let mut i = start; + while i < end { + let tx_id = self.project_transaction_ids.entry(project_id).at(i).read(); + let tx = self.transactions.read(tx_id); + txs.append(tx); + i += 1; + }; + Result::Ok((txs, total)) + } // Retrieves all fund requests for a given project ID. fn get_fund_requests(self: @ContractState, project_id: u64) -> Array {