Skip to content
Closed

98 #63

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: 2 additions & 1 deletion src/base/types.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ pub struct Organization {

#[derive(Copy, Drop, Serde, starknet::Store)]
pub struct Milestone {
pub organization: ContractAddress,
pub project_id: u64,
pub milestone_id: u64,
pub organization: ContractAddress,
pub milestone_description: felt252,
pub milestone_amount: u256,
pub created_at: u64,
Expand Down
7 changes: 5 additions & 2 deletions src/budgetchain/Budget.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ pub mod Budget {
Milestone {
organization: org,
project_id: project_id,
milestone_id: j.into() + 1,
milestone_description: *milestone_descriptions.at(j),
milestone_amount: *milestone_amounts.at(j),
created_at: get_block_timestamp(),
Expand Down Expand Up @@ -526,17 +527,19 @@ pub mod Budget {

let created_at = get_block_timestamp();

// Read the number of the current milestones the organization has
let current_milestone = self.org_milestones.read(org);

let new_milestone: Milestone = Milestone {
organization: org,
project_id: project_id,
milestone_id: current_milestone + 1,
milestone_description: milestone_description,
milestone_amount: milestone_amount,
created_at: created_at,
completed: false,
released: false,
};
// // read the number of the current milestones the organization has
let current_milestone = self.org_milestones.read(org);

self.milestones.write((project_id, current_milestone + 1), new_milestone);
self.org_milestones.write(org, current_milestone + 1);
Expand Down
201 changes: 201 additions & 0 deletions src/budgetchain/MilestoneManager.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#[starknet::contract]
pub mod MilestoneManager {
use budgetchain_contracts::base::errors::*;
use budgetchain_contracts::base::types::{ADMIN_ROLE, Milestone, ORGANIZATION_ROLE, Project};
use budgetchain_contracts::interfaces::IMilestoneManager::IMilestoneManager;
use core::array::{Array, ArrayTrait};
use openzeppelin::access::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE};
use openzeppelin::introspection::src5::SRC5Component;
use starknet::storage::{
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,
projects: Map<u64, Project>,
milestones: Map<(u64, u64), Milestone>, // (project_id, milestone_id) -> Milestone
project_milestone_count: Map<u64, u64>, // project_id -> count of milestones
is_paused: bool,
#[substorage(v0)]
accesscontrol: AccessControlComponent::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage,
}

#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
MilestoneCreated: MilestoneCreated,
MilestoneCompleted: MilestoneCompleted,
#[flat]
AccessControlEvent: AccessControlComponent::Event,
#[flat]
SRC5Event: SRC5Component::Event,
}

#[derive(Drop, starknet::Event)]
pub struct MilestoneCreated {
pub organization: ContractAddress,
pub project_id: u64,
pub milestone_id: u64,
pub milestone_description: felt252,
pub milestone_amount: u256,
pub created_at: u64,
}

#[derive(Drop, starknet::Event)]
pub struct MilestoneCompleted {
pub project_id: u64,
pub milestone_id: u64,
}

#[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);
self.is_paused.write(false);
}

#[abi(embed_v0)]
impl MilestoneManagerImpl of IMilestoneManager<ContractState> {
fn create_milestone(
ref self: ContractState,
organization: ContractAddress,
project_id: u64,
milestone_description: felt252,
milestone_amount: u256,
) -> u64 {
// Ensure the contract is not paused
self.assert_not_paused();

// Verify caller's authorization
let caller = get_caller_address();
let admin = self.admin.read();

assert(
caller == admin
|| self.accesscontrol.has_role(ORGANIZATION_ROLE, caller)
|| caller == organization,
ERROR_UNAUTHORIZED,
);

// Verify project exists with a valid ID
assert(project_id > 0, ERROR_INVALID_PROJECT_ID);

// Generate new milestone ID
let milestone_id = self.project_milestone_count.read(project_id) + 1;

// Create new milestone
let created_at = get_block_timestamp();
let new_milestone = Milestone {
project_id,
milestone_id,
organization,
milestone_description,
milestone_amount,
created_at,
completed: false,
released: false,
};
self.milestones.write((project_id, milestone_id), new_milestone);
self.project_milestone_count.write(project_id, milestone_id);
self
.emit(
Event::MilestoneCreated(
MilestoneCreated {
organization,
project_id,
milestone_id,
milestone_description,
milestone_amount,
created_at,
},
),
);
milestone_id
}

fn set_milestone_complete(ref self: ContractState, project_id: u64, milestone_id: u64) {
// Ensure the contract is not paused
self.assert_not_paused();
let mut milestone = self.milestones.read((project_id, milestone_id));
assert(milestone.project_id == project_id, ERROR_INVALID_MILESTONE);
assert(milestone.milestone_id == milestone_id, ERROR_INVALID_MILESTONE);
assert(milestone.completed != true, ERROR_MILESTONE_ALREADY_COMPLETED);
milestone.completed = true;
self.milestones.write((project_id, milestone_id), milestone);
self.emit(Event::MilestoneCompleted(MilestoneCompleted { project_id, milestone_id }));
}

fn get_milestone(self: @ContractState, project_id: u64, milestone_id: u64) -> Milestone {
self.milestones.read((project_id, milestone_id))
}

fn get_project_milestones(self: @ContractState, project_id: u64) -> Array<Milestone> {
let mut milestones = ArrayTrait::new();
let milestone_count = self.project_milestone_count.read(project_id);
let mut i: u64 = 1;
while i <= milestone_count {
let milestone = self.milestones.read((project_id, i));
milestones.append(milestone);
i += 1;
};
milestones
}

fn get_admin(self: @ContractState) -> ContractAddress {
self.admin.read()
}

fn is_paused(self: @ContractState) -> bool {
self.is_paused.read()
}

fn pause_contract(ref self: ContractState) {
let caller = get_caller_address();
assert(caller == self.admin.read(), ERROR_ONLY_ADMIN);
assert(!self.is_paused.read(), ERROR_ALREADY_PAUSED);
self.is_paused.write(true);
}

fn unpause_contract(ref self: ContractState) {
let caller = get_caller_address();
assert(caller == self.admin.read(), ERROR_ONLY_ADMIN);
self.is_paused.write(false);
}
}

#[generate_trait]
pub impl Internal of InternalTrait {
// Internal view function
// - Takes `@self` as it only needs to read state
// - Can only be called by other functions within the contract
fn assert_not_paused(self: @ContractState) {
assert(!self.is_paused.read(), ERROR_CONTRACT_PAUSED);
}
}
}
26 changes: 26 additions & 0 deletions src/interfaces/IMilestoneManager.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use budgetchain_contracts::base::types::Milestone;
use starknet::ContractAddress;

#[starknet::interface]
pub trait IMilestoneManager<TContractState> {
// Milestone Management
fn create_milestone(
ref self: TContractState,
organization: ContractAddress,
project_id: u64,
milestone_description: felt252,
milestone_amount: u256,
) -> u64;

fn set_milestone_complete(ref self: TContractState, project_id: u64, milestone_id: u64);

fn get_milestone(self: @TContractState, project_id: u64, milestone_id: u64) -> Milestone;

fn get_project_milestones(self: @TContractState, project_id: u64) -> Array<Milestone>;

// Admin functions
fn get_admin(self: @TContractState) -> ContractAddress;
fn is_paused(self: @TContractState) -> bool;
fn pause_contract(ref self: TContractState);
fn unpause_contract(ref self: TContractState);
}
4 changes: 4 additions & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ pub mod base {

pub mod interfaces {
pub mod IBudget;
pub mod IMilestoneManager;
}

pub mod budgetchain {
pub mod Budget;
pub mod MilestoneManager;
}

// Re-export the main modules for easier access
pub use budgetchain::Budget;
pub use budgetchain::MilestoneManager;
pub use interfaces::IBudget;
pub use interfaces::IMilestoneManager;
Loading
Loading