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
8 changes: 8 additions & 0 deletions onchain/budgetchain_contracts/src/base/errors.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
// contract custom errors
pub const ERROR_ONLY_ADMIN: felt252 = 'ONLY ADMIN';
pub const ERROR_ZERO_ADDRESS: felt252 = 'Zero address forbidden';

// Transaction creation errors
pub const AMOUNT_CANNOT_BE_ZERO: felt252 = 'Amount must be greater than 0';
pub const RECIPIENT_CANNOT_BE_ZERO: felt252 = 'Recipient cannot be zero addr';

// Transaction query errors
pub const TRANSACTION_NOT_FOUND: felt252 = 'Transaction not found';
pub const INVALID_PAGINATION: felt252 = 'Invalid pagination parameters';
27 changes: 27 additions & 0 deletions onchain/budgetchain_contracts/src/base/types.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@ pub struct Organization {
pub created_at: u64,
}

#[derive(Drop, starknet::Store, Serde)]
pub struct Transaction {
pub id: u64,
pub project_id: u64,
pub sender: ContractAddress,
pub recipient: ContractAddress,
pub amount: u128,
pub timestamp: u64,
pub category: felt252,
pub description: felt252,
}

#[derive(Drop, starknet::Event)]
pub struct TransactionCreated {
#[key]
pub id: u256,
#[key]
pub project_id: u64,
pub sender: ContractAddress,
pub recipient: ContractAddress,
pub amount: u128,
pub timestamp: u64,
pub category: felt252,
pub description: felt252,
}


// ROLE CONSTANTS
pub const ADMIN_ROLE: felt252 = selector!("ADMIN_ROLE");
pub const ORGANIZATION_ROLE: felt252 = selector!("ORGANIZATION_ROLE");
50 changes: 12 additions & 38 deletions onchain/budgetchain_contracts/src/budgetchain/Budget.cairo
Original file line number Diff line number Diff line change
@@ -1,45 +1,41 @@
#[feature("deprecated_legacy_map")]
#[starknet::contract]
pub mod Budget {
use budgetchain_contracts::base::errors::*;
use budgetchain_contracts::base::types::{ADMIN_ROLE,ORGANIZATION_ROLE, Organization};
use budgetchain_contracts::base::errors::*;
use budgetchain_contracts::base::types::{ADMIN_ROLE, ORGANIZATION_ROLE, Organization};
use budgetchain_contracts::interfaces::IBudget::IBudget;
use core::array::{Array, ArrayTrait};
use core::option::Option;
use openzeppelin::access::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE};
use openzeppelin::introspection::src5::SRC5Component;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess,
StoragePointerReadAccess, StoragePointerWriteAccess
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use starknet::{
ContractAddress, contract_address_const, get_block_timestamp, get_caller_address,
};
component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);
component!(path: SRC5Component, storage: src5, event: SRC5Event);

// AccessControl Mixin
#[abi(embed_v0)]
impl AccessControlImpl =
AccessControlComponent::AccessControlImpl<ContractState>;
impl AccessControlInternalImpl = AccessControlComponent::InternalImpl<ContractState>;

// SRC5 Mixin

#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;


#[storage]
struct Storage {
admin: ContractAddress, // Admin address
// Transaction storage
owner: ContractAddress, // Owner of the contract
org_count: u256, // Unique organization ID counter
organizations: Map<u256, Organization>, // Map of organizations by ID
org_addresses: Map<
ContractAddress, bool,
>, // Map of organization addresses to their active status
org_list: Array<Organization>, // List of all organizations
admin: ContractAddress,
owner: ContractAddress,
org_count: u256,
organizations: Map<u256, Organization>,
org_addresses: Map<ContractAddress, bool>,
org_list: Array<Organization>,
#[substorage(v0)]
accesscontrol: AccessControlComponent::Storage,
#[substorage(v0)]
Expand Down Expand Up @@ -79,13 +75,9 @@ pub mod Budget {
#[constructor]
fn constructor(ref self: ContractState, default_admin: ContractAddress) {
assert(default_admin != contract_address_const::<0>(), ERROR_ZERO_ADDRESS);

// Initialize access control
self.accesscontrol.initializer();
self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin);
self.accesscontrol._grant_role(ADMIN_ROLE, default_admin);

// Initialize contract storage
self.admin.write(default_admin);
}

Expand All @@ -94,15 +86,11 @@ pub mod Budget {
fn create_organization(
ref self: ContractState, name: felt252, org_address: ContractAddress, mission: felt252,
) -> u256 {
// Ensure only the admin can add an organization
let admin = self.admin.read();
assert(admin == get_caller_address(), ERROR_ONLY_ADMIN);

let created_at = get_block_timestamp();
// // Generate a unique organization ID
let org_id: u256 = self.org_count.read();

// Create and store the organization
let organization = Organization {
id: org_id,
address: org_address,
Expand All @@ -111,40 +99,26 @@ pub mod Budget {
mission,
created_at: created_at,
};

// Emit an event
self.emit(OrganizationAdded { id: org_id, address: org_address, name: name });

self.org_count.write(org_id + 1);
self.organizations.write(org_id, organization);
self.org_addresses.write(org_address, true);

// Grant organization role
self.accesscontrol._grant_role(ORGANIZATION_ROLE, organization.address);

// Emit an event
self.emit(OrganizationAdded { id: org_id, address: org_address, name: name });

org_id
}

fn update_organization(
ref self: ContractState,
name: felt252,
org_id: u256, // org_address: ContractAddress,
mission: felt252,
ref self: ContractState, name: felt252, org_id: u256, mission: felt252,
) {
// Ensure only the admin can add an organization
let admin = self.admin.read();
assert(admin == get_caller_address(), ERROR_ONLY_ADMIN);

let mut org = self.organizations.read(org_id);
// org.is_active = false;
org.name = name;
// org.address = org_address;
org.mission = mission;
// org.created_at = get_block_timestamp();
// org.is_active = true;

self.organizations.write(org_id, org);
}
Expand Down
162 changes: 162 additions & 0 deletions onchain/budgetchain_contracts/src/budgetchain/Ledger.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/// Ledger smart contract implementation
#[starknet::contract]
pub mod Ledger {
use core::num::traits::Zero;
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
use crate::base::errors::{AMOUNT_CANNOT_BE_ZERO, RECIPIENT_CANNOT_BE_ZERO};
use crate::base::types::{Transaction, TransactionCreated};
use crate::interfaces::ILedger::ILedger;
use core::starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, Map, Vec, VecTrait, MutableVecTrait,
StorageMapReadAccess, StorageMapWriteAccess,
};
use core::array::ArrayTrait;
use core::array::Array;

#[storage]
struct Storage {
transactions: Map<u64, Transaction>,
transaction_count: u64,
all_transaction_ids: Vec<u64>,
}

#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
TransactionCreated: TransactionCreated,
}

#[constructor]
fn constructor(ref self: ContractState) {
self.transaction_count.write(0);
}

#[abi(embed_v0)]
impl LedgerImpl of ILedger<ContractState> {
fn create_transaction(
ref self: ContractState,
project_id: u64,
recipient: ContractAddress,
amount: u128,
category: felt252,
description: felt252,
) -> u64 {
assert(amount > 0, AMOUNT_CANNOT_BE_ZERO);
// Check if recipient is zero address
let zero_address: ContractAddress = Zero::zero();
assert(recipient != zero_address, RECIPIENT_CANNOT_BE_ZERO);

// Get current transaction count and increment
let current_count = self.transaction_count.read();
let mut transaction_id = current_count + 1;
self.transaction_count.write(transaction_id);

// Get sender and timestamp
let sender = get_caller_address();
let timestamp = get_block_timestamp();

// Create transaction
let transaction = Transaction {
id: transaction_id,
project_id,
sender,
recipient,
amount,
timestamp,
category,
description,
};

// Store transaction
self.transactions.write(transaction_id, transaction);

// Add to all transactions list
self.all_transaction_ids.append().write(transaction_id);

// Emit event
self
.emit(
TransactionCreated {
id: transaction_id.into(),
project_id,
sender,
recipient,
amount,
timestamp,
category,
description,
},
);

transaction_id
}

fn get_transaction_history(
self: @ContractState, project_id: Option<u64>, offset: u64, limit: u64,
) -> Array<Transaction> {
let mut transactions = ArrayTrait::new();
let total_count = self.all_transaction_ids.len();

match project_id {
Option::Some(pid) => {
// Filter transactions by project_id
let mut found_count = 0_u64;
let mut added_count = 0_u64;

let mut i = 0_u64;
while i < total_count && added_count < limit {
let tx_id = self.all_transaction_ids.at(i).read();
let transaction = self.transactions.read(tx_id);

if transaction.project_id == pid {
if found_count >= offset {
transactions.append(transaction);
added_count += 1;
}
found_count += 1;
}
i += 1;
};
},
Option::None => {
// Get all transactions with pagination
let start_idx = offset;
let end_idx = core::cmp::min(start_idx + limit, total_count);

let mut i = start_idx;
while i < end_idx {
let tx_id = self.all_transaction_ids.at(i).read();
let transaction = self.transactions.read(tx_id);
transactions.append(transaction);
i += 1;
};
},
}

transactions
}

fn get_transaction(self: @ContractState, transaction_id: u64) -> Option<Transaction> {
if transaction_id == 0 || transaction_id > self.transaction_count.read() {
Option::None
} else {
let transaction = self.transactions.read(transaction_id);
if transaction.id == 0 {
Option::None
} else {
Option::Some(transaction)
}
}
}

fn get_transaction_count(self: @ContractState) -> u64 {
self.transaction_count.read()
}

fn get_project_transactions(
self: @ContractState, project_id: u64, offset: u64, limit: u64,
) -> Array<Transaction> {
self.get_transaction_history(Option::Some(project_id), offset, limit)
}
}
}
22 changes: 22 additions & 0 deletions onchain/budgetchain_contracts/src/interfaces/ILedger.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use starknet::ContractAddress;
use crate::base::types::Transaction;

#[starknet::interface]
pub trait ILedger<TContractState> {
fn create_transaction(
ref self: TContractState,
project_id: u64,
recipient: ContractAddress,
amount: u128,
category: felt252,
description: felt252,
) -> u64;
fn get_transaction_history(
self: @TContractState, project_id: Option<u64>, offset: u64, limit: u64,
) -> Array<Transaction>;
fn get_transaction(self: @TContractState, transaction_id: u64) -> Option<Transaction>;
fn get_transaction_count(self: @TContractState) -> u64;
fn get_project_transactions(
self: @TContractState, project_id: u64, offset: u64, limit: u64,
) -> Array<Transaction>;
}
7 changes: 5 additions & 2 deletions onchain/budgetchain_contracts/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ pub mod base {

pub mod interfaces {
pub mod IBudget;
pub mod ILedger;
}

pub mod budgetchain {
pub mod Budget;
pub mod Ledger;
}

// Re-export the main modules for easier access
pub use budgetchain::Budget;
pub use interfaces::IBudget;
pub use budgetchain::{Budget, Ledger};
pub use interfaces::{IBudget, ILedger};
pub use base::types::Transaction;
Loading
Loading