Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
scarb 2.11.3
snforge_std 0.36.0
scarb 2.9.2
starknet-foundry 0.36.0
86 changes: 71 additions & 15 deletions src/budgetchain/Budget.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub mod Budget {
org_milestones: Map<ContractAddress, u64>, // org to number of milestones they have
all_transactions: Vec<Transaction>,
project_transaction_ids: Map<u64, Vec<u64>>,
project_transaction_count: Map<u64, u64>, // project_id -> count
#[substorage(v0)]
accesscontrol: AccessControlComponent::Storage,
#[substorage(v0)]
Expand Down Expand Up @@ -173,17 +174,41 @@ pub mod Budget {

#[abi(embed_v0)]
impl BudgetImpl of IBudget<ContractState> {
// fn create_transaction(
// ref self: ContractState,
// project_id: u64,
// recipient: ContractAddress,
// amount: u128,
// category: felt252,
// description: felt252,
// ) -> Result<u64, felt252> {

// // Ensure the contract is not paused
// self.assert_not_paused();}
fn create_transaction(
ref self: ContractState,
project_id: u64,
recipient: ContractAddress,
amount: u128,
category: felt252,
description: felt252,
) -> Result<u64, felt252> {
// 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);

Result::Ok(transaction_id)
}

fn get_transaction(self: @ContractState, id: u64) -> Result<Transaction, felt252> {
assert(id > 0 && id <= self.transaction_count.read(), ERROR_INVALID_TRANSACTION_ID);
Expand All @@ -201,10 +226,40 @@ pub mod Budget {
// ) -> Result<Array<Transaction>, 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<Transaction>, u64), felt252> {
// }

fn get_project_transactions(
self: @ContractState, project_id: u64, page: u64, page_size: u64,
) -> Result<(Array<Transaction>, 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<FundRequest> {
Expand Down Expand Up @@ -680,6 +735,7 @@ pub mod Budget {
fn is_paused(self: @ContractState) -> bool {
self.is_paused.read()
}

fn request_funds(
ref self: ContractState,
requester: ContractAddress,
Expand Down
19 changes: 11 additions & 8 deletions src/interfaces/IBudget.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ use starknet::ContractAddress;
#[starknet::interface]
pub trait IBudget<TContractState> {
// Transaction Management
// fn create_transaction(
// ref self: TContractState,
// project_id: u64,
// recipient: ContractAddress,
// amount: u128,
// category: felt252,
// description: felt252,
// ) -> Result<u64, felt252>;
fn create_transaction(
ref self: TContractState,
project_id: u64,
recipient: ContractAddress,
amount: u128,
category: felt252,
description: felt252,
) -> Result<u64, felt252>;
fn get_transaction(self: @TContractState, id: u64) -> Result<Transaction, felt252>;
// fn get_transaction_history(
// self: @TContractState, page: u64, page_size: u64,
Expand Down Expand Up @@ -91,6 +91,9 @@ pub trait IBudget<TContractState> {
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<Transaction>, u64), felt252>;

fn pause_contract(ref self: TContractState);
fn unpause_contract(ref self: TContractState);
Expand Down
162 changes: 162 additions & 0 deletions tests/test_budgetchain.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -1278,3 +1278,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');
}
Loading