diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..49242d5 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,4 @@ +{ + "projectName": "gostarkme", + "projectOwner": "web3wagers" +} \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/odhack_issue_template.md b/.github/ISSUE_TEMPLATE/odhack_issue_template.md new file mode 100644 index 0000000..dc874aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/odhack_issue_template.md @@ -0,0 +1,11 @@ +--- +name: ODHack Issue +about: Issue that will be solved during an ODHack +title: '' +labels: '' +assignees: '' + +--- + +### This issue will be part of ODHack8.0, please apply via Onlydust app +- Please read [contributors guide](https://github.com/web3wagers/gostarkme/blob/dev/CONTRIBUTORS_GUIDE.md) before asking for an issue. diff --git a/.github/workflows/contract_checks.yml b/.github/workflows/contract_checks.yml index 363999b..1649991 100644 --- a/.github/workflows/contract_checks.yml +++ b/.github/workflows/contract_checks.yml @@ -2,10 +2,19 @@ name: CI on: pull_request: + branches: + - dev + + push: + branches: + - dev + - main jobs: - check-lint: + determine-changes: runs-on: ubuntu-latest + outputs: + contracts_dir: ${{ steps.changes.outputs.contracts_dir }} steps: - uses: actions/checkout@v2 @@ -14,71 +23,76 @@ jobs: id: changes with: filters: | - contracts_dir: + contracts_dir: - 'contracts/src/**' - - name: Install scarb - if: steps.changes.outputs.contracts_dir == 'true' - run: | - cd contracts - curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.6.5 + check-lint: + runs-on: ubuntu-latest + needs: determine-changes + steps: + - uses: actions/checkout@v2 + + - name: Setup Scarb + uses: software-mansion/setup-scarb@v1 + if: needs.determine-changes.outputs.contracts_dir == 'true' + with: + scarb-version: "2.6.5" - - name: Run scarb fmt - if: steps.changes.outputs.contracts_dir == 'true' - run: | - cd contracts - scarb fmt + - name: Run Scarb formatting + if: needs.determine-changes.outputs.contracts_dir == 'true' + working-directory: contracts + run: scarb fmt + + - name: No changes detected in contracts directory + if: needs.determine-changes.outputs.contracts_dir == 'false' + run: echo "No linting perfomed since no changes have been detected in the contracts directory." check-contracts-changes: runs-on: ubuntu-latest + needs: determine-changes steps: - uses: actions/checkout@v2 - - - name: Check for changes - uses: dorny/paths-filter@v3 - id: changes - with: - filters: | - contracts_dir: - - 'contracts/src/**' - name: Install scarb - if: steps.changes.outputs.contracts_dir == 'true' - run: | - cd contracts - curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.6.5 + uses: software-mansion/setup-scarb@v1 + if: needs.determine-changes.outputs.contracts_dir == 'true' + with: + scarb-version: "2.6.5" + + - name: Run Scarb check + if: needs.determine-changes.outputs.contracts_dir == 'true' + working-directory: contracts + run: scarb check - - name: Run scarb check - if: steps.changes.outputs.contracts_dir == 'true' - run: | - cd contracts - scarb check + - name: No changes detected in contracts directory + if: needs.determine-changes.outputs.contracts_dir == 'false' + run: echo "No contracts checks done since no changes have been detected in the contracts directory." run-tests: runs-on: ubuntu-latest + needs: determine-changes steps: - name: Checkout repository uses: actions/checkout@v2 - - name: Check for changes - uses: dorny/paths-filter@v3 - id: changes + - name: Install Scarb + uses: software-mansion/setup-scarb@v1 + if: needs.determine-changes.outputs.contracts_dir == 'true' with: - filters: | - contracts_dir: - - 'contracts/src/**' + scarb-version: "2.6.5" - - name: Install scarb - if: steps.changes.outputs.contracts_dir == 'true' - run: | - cd contracts - curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v 2.6.5 - name: Install snfoundry uses: foundry-rs/setup-snfoundry@v3 + if: needs.determine-changes.outputs.contracts_dir == 'true' with: - starknet-foundry-version: "0.20.1" + starknet-foundry-version: "0.27.0" - name: Run Cairo tests + if: needs.determine-changes.outputs.contracts_dir == 'true' id: cairo_tests run: bash scripts/run_tests.sh + + - name: No changes detected in contracts directory + if: needs.determine-changes.outputs.contracts_dir == 'false' + run: echo "No tests were run since no changes have been detected in the contracts directory." diff --git a/.gitignore b/.gitignore index 6ec4261..62d7b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +.snfoundry_cache target -.DS_Store \ No newline at end of file +.DS_Store + +contracts/.snfoundry_cache/https___starknet_mainnet_public_blastapi_io_rpc_v0_7_519354_v3.json \ No newline at end of file diff --git a/CONTRIBUTORS_GUIDE.md b/CONTRIBUTORS_GUIDE.md index 75e7966..be1426e 100644 --- a/CONTRIBUTORS_GUIDE.md +++ b/CONTRIBUTORS_GUIDE.md @@ -14,6 +14,8 @@ The best way to contribute to our projects is by opening a new issue > ~/.zshrc + ``` + ```bash + echo 'export PATH="$HOME/.foundry/bin:$PATH"' >> ~/.zshrc +3. **Compile Go Stark Me Backend 🛠️** + + To build the contracts, run the command: + + ```bash + scarb build + ``` + +4. **Run Go Stark Me Unit Tests ✅** + + To run the unit tests for the contracts, run the following command: + + ```bash + scarb run test + ``` + or + ```bash + snforge test + ``` + +5. **Run Code Formatter 📝** + + To format your contracts, simply run the command: + + ```bash + scarb fmt + ``` + +## UML Diagram + +![UML Class Diagram](https://github.com/user-attachments/assets/479c9296-e3ac-4ad3-bf79-5f458c456a45) \ No newline at end of file diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index 9079da6..7b2dd57 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -16,5 +16,5 @@ source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d7 [[package]] name = "snforge_std" -version = "0.20.1" -source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.20.1#fea2db8f2b20148cc15ee34b08de12028eb42942" +version = "0.27.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.27.0#2d99b7c00678ef0363881ee0273550c44a9263de" diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index bfd50e9..c045445 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -10,10 +10,15 @@ starknet = "2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" } [dev-dependencies] -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.20.1" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.27.0" } [scripts] test = "snforge test" [[target.starknet-contract]] casm = true + +[[tool.snforge.fork]] +name = "Mainnet" +url = "https://starknet-mainnet.public.blastapi.io/rpc/v0_7" +block_id.number = "519354" diff --git a/contracts/src/constants/donator/donator_constants.cairo b/contracts/src/constants/donator/donator_constants.cairo index b5a3327..5822e99 100644 --- a/contracts/src/constants/donator/donator_constants.cairo +++ b/contracts/src/constants/donator/donator_constants.cairo @@ -3,5 +3,5 @@ // ************************************************************************* pub mod DonatorConstants { pub const INITIAL_LEVEL: u32 = 1; - pub const INITIAL_MAX_STARKS_DONATION_TO_NEXT_LEVEL: u64 = 10; + pub const INITIAL_MAX_STARKS_DONATION_TO_NEXT_LEVEL: u256 = 10; } diff --git a/contracts/src/constants/funds.cairo b/contracts/src/constants/funds.cairo index a0adbf1..77c459e 100644 --- a/contracts/src/constants/funds.cairo +++ b/contracts/src/constants/funds.cairo @@ -1,2 +1,4 @@ pub mod state_constants; pub mod fund_constants; +pub mod fund_manager_constants; +pub mod starknet_constants; diff --git a/contracts/src/constants/funds/fund_constants.cairo b/contracts/src/constants/funds/fund_constants.cairo index 7de0d07..afc1cb2 100644 --- a/contracts/src/constants/funds/fund_constants.cairo +++ b/contracts/src/constants/funds/fund_constants.cairo @@ -2,7 +2,8 @@ // FUND CONSTANTS // ************************************************************************* pub mod FundConstants { - pub const UP_VOTES_NEEDED: u32 = 100; + pub const UP_VOTES_NEEDED: u32 = 50; pub const INITIAL_UP_VOTES: u32 = 0; - pub const INITIAL_GOAL: u64 = 0; + pub const INITIAL_GOAL: u256 = 0; + pub const MINIMUM_GOAL: u256 = 500; } diff --git a/contracts/src/constants/funds/fund_manager_constants.cairo b/contracts/src/constants/funds/fund_manager_constants.cairo new file mode 100644 index 0000000..b19a40f --- /dev/null +++ b/contracts/src/constants/funds/fund_manager_constants.cairo @@ -0,0 +1,11 @@ +// ************************************************************************* +// FUND MANAGER CONSTANTS +// ************************************************************************* +pub mod FundManagerConstants { + pub const FUND_MANAGER_ADDRESS: felt252 = + 0x02bbab4d3c77dd83dfe47af99c9a3188a660e9c652a2f7af3d21df213f4882cd; + pub const VALID_ADDRESS_1: felt252 = + 0x0388012BD4385aDf3b7afDE89774249D5179841cBaB06e9E5b4045F27B327CE8; + pub const VALID_ADDRESS_2: felt252 = + 0x0528A7ba821024a8eC44dff0bFFe15443d811F233e4de7AB1a8C26f251597c4c; +} diff --git a/contracts/src/constants/funds/starknet_constants.cairo b/contracts/src/constants/funds/starknet_constants.cairo new file mode 100644 index 0000000..a04c8c0 --- /dev/null +++ b/contracts/src/constants/funds/starknet_constants.cairo @@ -0,0 +1,11 @@ +use starknet::ContractAddress; + +// ************************************************************************* +// STARKNET CONSTANTS +// ************************************************************************* +pub mod StarknetConstants { + pub const STRK_TOKEN_ADDRESS: felt252 = + 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; + pub const STRK_TOKEN_MINTER_ADDRESS: felt252 = + 0x0594c1582459ea03f77deaf9eb7e3917d6994a03c13405ba42867f83d85f085d; +} diff --git a/contracts/src/constants/funds/state_constants.cairo b/contracts/src/constants/funds/state_constants.cairo index 0da1356..0c04d65 100644 --- a/contracts/src/constants/funds/state_constants.cairo +++ b/contracts/src/constants/funds/state_constants.cairo @@ -6,4 +6,5 @@ pub mod FundStates { pub const RECOLLECTING_VOTES: u8 = 1; pub const RECOLLECTING_DONATIONS: u8 = 2; pub const CLOSED: u8 = 3; + pub const WITHDRAW: u8 = 4; } diff --git a/contracts/src/donator.cairo b/contracts/src/donator.cairo index c2c4faf..b83abce 100644 --- a/contracts/src/donator.cairo +++ b/contracts/src/donator.cairo @@ -4,9 +4,9 @@ use starknet::ContractAddress; pub trait IDonator { fn getOwner(self: @TContractState) -> ContractAddress; fn getLevel(self: @TContractState) -> u32; - fn getTotalStarkDonations(self: @TContractState) -> u64; - fn getMaxStarkDonationsToNextLevel(self: @TContractState) -> u64; - fn updateDonatorValues(ref self: TContractState, donated_starks: u64); + fn getTotalStarkDonations(self: @TContractState) -> u256; + fn getMaxStarkDonationsToNextLevel(self: @TContractState) -> u256; + fn updateDonatorValues(ref self: TContractState, donated_starks: u256); } #[starknet::contract] @@ -24,8 +24,8 @@ mod Donator { struct Storage { owner: ContractAddress, level: u32, - total_stark_donations: u64, - max_stark_donations_to_next_level: u64 + total_stark_donations: u256, + max_stark_donations_to_next_level: u256 } // ************************************************************************* @@ -52,14 +52,13 @@ mod Donator { fn getLevel(self: @ContractState) -> u32 { return self.level.read(); } - - fn getTotalStarkDonations(self: @ContractState) -> u64 { + fn getTotalStarkDonations(self: @ContractState) -> u256 { return self.total_stark_donations.read(); } - fn getMaxStarkDonationsToNextLevel(self: @ContractState) -> u64 { + fn getMaxStarkDonationsToNextLevel(self: @ContractState) -> u256 { return self.max_stark_donations_to_next_level.read(); } - fn updateDonatorValues(ref self: ContractState, donated_starks: u64) { + fn updateDonatorValues(ref self: ContractState, donated_starks: u256) { let total_donator_pod = self.total_stark_donations.read() + donated_starks; self.total_stark_donations.write(total_donator_pod); if (total_donator_pod > self.max_stark_donations_to_next_level.read()) { diff --git a/contracts/src/donatorManager.cairo b/contracts/src/donatorManager.cairo index 67fa584..2ceea43 100644 --- a/contracts/src/donatorManager.cairo +++ b/contracts/src/donatorManager.cairo @@ -4,11 +4,13 @@ use starknet::class_hash::ClassHash; #[starknet::interface] pub trait IDonatorManager { fn newDonator(ref self: TContractState); + fn getOwner(self: @TContractState) -> ContractAddress; + fn getDonatorClassHash(self: @TContractState) -> ClassHash; fn getDonatorByAddress(self: @TContractState, owner: ContractAddress) -> ContractAddress; } #[starknet::contract] -mod DonatorManager { +pub mod DonatorManager { // ************************************************************************* // IMPORT // ************************************************************************* @@ -19,10 +21,6 @@ mod DonatorManager { use starknet::class_hash::ClassHash; use starknet::get_caller_address; - // This hash will change if donator.cairo file is modified - const DONATOR_CLASS_HASH: felt252 = - 0x03ddcb5ac2ecf82627887217de833132e7252b146cce03a6e38374fc9b6d61b2; - // ************************************************************************* // STORAGE // ************************************************************************* @@ -30,14 +28,31 @@ mod DonatorManager { struct Storage { owner: ContractAddress, donators: LegacyMap::, + donator_class_hash: ClassHash, } // ************************************************************************* // CONSTRUCTOR // ************************************************************************* #[constructor] - fn constructor(ref self: ContractState) { + fn constructor(ref self: ContractState, donator_class_hash: felt252) { self.owner.write(get_caller_address()); + self.donator_class_hash.write(donator_class_hash.try_into().unwrap()); + } + + // ************************************************************************* + // EVENTS + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + DonatorContractDeployed: DonatorContractDeployed, + } + + #[derive(Drop, starknet::Event)] + pub struct DonatorContractDeployed { + pub new_donator: ContractAddress, + pub owner: ContractAddress } // ************************************************************************* @@ -47,14 +62,25 @@ mod DonatorManager { impl DonatorManagerImpl of super::IDonatorManager { fn newDonator(ref self: ContractState) { let mut calldata = ArrayTrait::::new(); - calldata.append(get_caller_address().try_into().unwrap()); - let (address_0, _) = deploy_syscall( - DONATOR_CLASS_HASH.try_into().unwrap(), 12345, calldata.span(), false + let (new_donator_address, _) = deploy_syscall( + self.donator_class_hash.read(), 12345, calldata.span(), false ) .unwrap(); - self.donators.write(get_caller_address().try_into().unwrap(), address_0); + self.donators.write(get_caller_address().try_into().unwrap(), new_donator_address); + self + .emit( + DonatorContractDeployed { + owner: get_caller_address(), new_donator: new_donator_address + } + ) + } + fn getOwner(self: @ContractState) -> ContractAddress { + return self.owner.read(); + } + fn getDonatorClassHash(self: @ContractState) -> ClassHash { + return self.donator_class_hash.read(); } fn getDonatorByAddress(self: @ContractState, owner: ContractAddress) -> ContractAddress { return self.donators.read(owner); diff --git a/contracts/src/fund.cairo b/contracts/src/fund.cairo index 94ef077..d809d38 100644 --- a/contracts/src/fund.cairo +++ b/contracts/src/fund.cairo @@ -4,32 +4,77 @@ use starknet::ContractAddress; pub trait IFund { fn getId(self: @TContractState) -> u128; fn getOwner(self: @TContractState) -> ContractAddress; - fn setName(ref self: TContractState, name: felt252); - fn getName(self: @TContractState) -> felt252; + fn setName(ref self: TContractState, name: ByteArray); + fn getName(self: @TContractState) -> ByteArray; fn setReason(ref self: TContractState, reason: ByteArray); fn getReason(self: @TContractState) -> ByteArray; fn receiveVote(ref self: TContractState); fn getUpVotes(self: @TContractState) -> u32; - fn setGoal(ref self: TContractState, goal: u64); - fn getGoal(self: @TContractState) -> u64; - fn receiveDonation(ref self: TContractState, strks: u64); - fn getCurrentGoalState(self: @TContractState) -> u64; - fn setIsActive(ref self: TContractState, state: u8); - fn getIsActive(self: @TContractState) -> u8; + fn setGoal(ref self: TContractState, goal: u256); + fn getGoal(self: @TContractState) -> u256; + fn update_receive_donation(ref self: TContractState, strks: u256); + fn get_current_goal_state(self: @TContractState) -> u256; + fn setState(ref self: TContractState, state: u8); + fn getState(self: @TContractState) -> u8; fn getVoter(self: @TContractState) -> u32; + fn withdraw(ref self: TContractState); + fn set_evidence_link(ref self: TContractState, evidence: ByteArray); + fn get_evidence_link(self: @TContractState) -> ByteArray; + fn set_contact_handle(ref self: TContractState, contact_handle: ByteArray); + fn get_contact_handle(self: @TContractState) -> ByteArray; } #[starknet::contract] -mod Fund { +pub mod Fund { // ************************************************************************* // IMPORT // ************************************************************************* use starknet::ContractAddress; use starknet::get_caller_address; + use starknet::contract_address_const; + use starknet::get_contract_address; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use gostarkme::constants::{funds::{state_constants::FundStates},}; - use gostarkme::constants::{funds::{fund_constants::FundConstants},}; + use gostarkme::constants::{ + funds::{fund_constants::FundConstants, fund_manager_constants::FundManagerConstants}, + }; + use gostarkme::constants::{funds::{starknet_constants::StarknetConstants},}; + // ************************************************************************* + // EVENTS + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + DonationWithdraw: DonationWithdraw, + NewVoteReceived: NewVoteReceived, + DonationReceived: DonationReceived, + } + + #[derive(Drop, starknet::Event)] + pub struct DonationWithdraw { + #[key] + pub owner_address: ContractAddress, + pub fund_contract_address: ContractAddress, + pub withdrawn_amount: u256 + } + #[derive(Drop, starknet::Event)] + pub struct NewVoteReceived { + #[key] + pub voter: ContractAddress, + pub fund: ContractAddress, + pub votes: u32 + } + + #[derive(Drop, starknet::Event)] + pub struct DonationReceived { + #[key] + pub donator_address: ContractAddress, + pub current_balance: u256, + pub donated_strks: u256, + pub fund_contract_address: ContractAddress, + } // ************************************************************************* // STORAGE // ************************************************************************* @@ -37,13 +82,14 @@ mod Fund { struct Storage { id: u128, owner: ContractAddress, - name: felt252, + name: ByteArray, reason: ByteArray, up_votes: u32, voters: LegacyMap::, - goal: u64, - current_goal_state: u64, - state: u8 + goal: u256, + state: u8, + evidence_link: ByteArray, + contact_handle: ByteArray } // ************************************************************************* @@ -51,16 +97,24 @@ mod Fund { // ************************************************************************* #[constructor] fn constructor( - ref self: ContractState, id: u128, owner: ContractAddress, name: felt252, goal: u64 + ref self: ContractState, + id: u128, + owner: ContractAddress, + name: ByteArray, + goal: u256, + evidence_link: ByteArray, + contact_handle: ByteArray, + reason: ByteArray ) { self.id.write(id); self.owner.write(owner); self.name.write(name); - self.reason.write(" "); + self.reason.write(reason); self.up_votes.write(FundConstants::INITIAL_UP_VOTES); self.goal.write(goal); - self.current_goal_state.write(FundConstants::INITIAL_GOAL); self.state.write(FundStates::RECOLLECTING_VOTES); + self.evidence_link.write(evidence_link); + self.contact_handle.write(contact_handle); } // ************************************************************************* @@ -74,12 +128,12 @@ mod Fund { fn getOwner(self: @ContractState) -> ContractAddress { return self.owner.read(); } - fn setName(ref self: ContractState, name: felt252) { + fn setName(ref self: ContractState, name: ByteArray) { let caller = get_caller_address(); assert!(self.owner.read() == caller, "You are not the owner"); self.name.write(name); } - fn getName(self: @ContractState) -> felt252 { + fn getName(self: @ContractState) -> ByteArray { return self.name.read(); } fn setReason(ref self: ContractState, reason: ByteArray) { @@ -100,41 +154,112 @@ mod Fund { if self.up_votes.read() >= FundConstants::UP_VOTES_NEEDED { self.state.write(FundStates::RECOLLECTING_DONATIONS); } + + self + .emit( + NewVoteReceived { + voter: get_caller_address(), + fund: get_contract_address(), + votes: self.up_votes.read() + } + ); } fn getUpVotes(self: @ContractState) -> u32 { return self.up_votes.read(); } - fn setGoal(ref self: ContractState, goal: u64) { + fn setGoal(ref self: ContractState, goal: u256) { let caller = get_caller_address(); - assert!(self.owner.read() == caller, "You are not the owner"); + let fund_manager_address = contract_address_const::< + FundManagerConstants::FUND_MANAGER_ADDRESS + >(); + assert!(fund_manager_address == caller, "You are not the fund manager"); self.goal.write(goal); } - fn getGoal(self: @ContractState) -> u64 { + fn getGoal(self: @ContractState) -> u256 { return self.goal.read(); } - // TODO: implement the logic where user actually donates starks - fn receiveDonation(ref self: ContractState, strks: u64) { - assert( - self.state.read() == FundStates::RECOLLECTING_DONATIONS, - 'Fund not recollecting dons!' - ); - self.current_goal_state.write(self.current_goal_state.read() + strks); - if self.current_goal_state.read() >= self.goal.read() { + fn update_receive_donation(ref self: ContractState, strks: u256) { + let current_balance = self.get_current_goal_state(); + if current_balance >= self.goal.read() { self.state.write(FundStates::CLOSED); } + self + .emit( + DonationReceived { + current_balance, + donated_strks: strks, + donator_address: get_caller_address(), + fund_contract_address: get_contract_address(), + } + ) } - fn getCurrentGoalState(self: @ContractState) -> u64 { - return self.current_goal_state.read(); + fn get_current_goal_state(self: @ContractState) -> u256 { + self.token_dispatcher().balance_of(get_contract_address()) } - // TODO: Validate to change method to change setState and getState - fn setIsActive(ref self: ContractState, state: u8) { + fn setState(ref self: ContractState, state: u8) { + let caller = get_caller_address(); + let valid_address_1 = contract_address_const::(); + let valid_address_2 = contract_address_const::(); + assert!(valid_address_1 == caller || valid_address_2 == caller, "Only Admins can change the fund state."); self.state.write(state); } - fn getIsActive(self: @ContractState) -> u8 { + fn getState(self: @ContractState) -> u8 { return self.state.read(); } fn getVoter(self: @ContractState) -> u32 { return self.voters.read(get_caller_address()); } + fn withdraw(ref self: ContractState) { + let caller = get_caller_address(); + assert!(self.owner.read() == caller, "You are not the owner"); + assert(self.state.read() == FundStates::CLOSED, 'Fund not close goal yet.'); + assert( + self.get_current_goal_state() >= self.getGoal(), 'Fund hasnt reached its goal yet' + ); + let valid_address = contract_address_const::(); + let withdrawn_amount = self.get_current_goal_state() * 95 / 100; + let fund_manager_amount = self.get_current_goal_state() * 5 / 100; + self.token_dispatcher().approve(self.getOwner(), withdrawn_amount); + self.token_dispatcher().transfer(self.getOwner(), withdrawn_amount); + self.token_dispatcher().approve(valid_address, fund_manager_amount); + self.token_dispatcher().transfer(valid_address, fund_manager_amount); + assert(self.get_current_goal_state() == 0, 'Pending stks to withdraw'); + self.setState(4); + self + .emit( + DonationWithdraw { + owner_address: self.getOwner(), + fund_contract_address: get_contract_address(), + withdrawn_amount + } + ); + } + fn set_evidence_link(ref self: ContractState, evidence: ByteArray) { + let caller = get_caller_address(); + assert!(self.owner.read() == caller, "You are not the owner"); + self.evidence_link.write(evidence); + } + fn get_evidence_link(self: @ContractState) -> ByteArray { + return self.evidence_link.read(); + } + fn set_contact_handle(ref self: ContractState, contact_handle: ByteArray) { + let caller = get_caller_address(); + assert!(self.owner.read() == caller, "You are not the owner"); + self.contact_handle.write(contact_handle); + } + fn get_contact_handle(self: @ContractState) -> ByteArray { + return self.contact_handle.read(); + } + } + // ************************************************************************* + // INTERNALS + // ************************************************************************* + #[generate_trait] + impl InternalImpl of InternalTrait { + fn token_dispatcher(self: @ContractState) -> IERC20Dispatcher { + IERC20Dispatcher { + contract_address: contract_address_const::() + } + } } } diff --git a/contracts/src/fundManager.cairo b/contracts/src/fundManager.cairo old mode 100644 new mode 100755 index 36426d8..ebc2bc7 --- a/contracts/src/fundManager.cairo +++ b/contracts/src/fundManager.cairo @@ -3,26 +3,38 @@ use starknet::class_hash::ClassHash; #[starknet::interface] pub trait IFundManager { - fn newFund(ref self: TContractState, name: felt252, goal: u64); + fn newFund( + ref self: TContractState, + name: ByteArray, + goal: u256, + evidence_link: ByteArray, + contact_handle: ByteArray, + reason: ByteArray + ); fn getCurrentId(self: @TContractState) -> u128; fn getFund(self: @TContractState, id: u128) -> ContractAddress; + fn getOwner(self: @TContractState) -> ContractAddress; + fn getFundClassHash(self: @TContractState) -> ClassHash; } #[starknet::contract] -mod FundManager { - // ************************************************************************* +pub mod FundManager { + // *************************************************************************************** // IMPORT - // ************************************************************************* + // *************************************************************************************** use core::array::ArrayTrait; use core::traits::TryInto; use starknet::ContractAddress; use starknet::syscalls::deploy_syscall; use starknet::class_hash::ClassHash; use starknet::get_caller_address; + use openzeppelin::utils::serde::SerializedAppend; + use gostarkme::constants::{funds::{fund_constants::FundConstants},}; - // ************************************************************************* + + // *************************************************************************************** // STORAGE - // ************************************************************************* + // *************************************************************************************** #[storage] struct Storage { owner: ContractAddress, @@ -31,32 +43,73 @@ mod FundManager { fund_class_hash: ClassHash, } - // ************************************************************************* + // *************************************************************************************** // CONSTRUCTOR - // ************************************************************************* + // *************************************************************************************** #[constructor] fn constructor(ref self: ContractState, fund_class_hash: felt252) { self.owner.write(get_caller_address()); self.fund_class_hash.write(fund_class_hash.try_into().unwrap()); - self.current_id.write(0); + self.current_id.write(1); + } + + + // *************************************************************************************** + // EVENTS + // *************************************************************************************** + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + FundDeployed: FundDeployed, + } + + #[derive(Drop, starknet::Event)] + pub struct FundDeployed { + #[key] + pub owner: ContractAddress, + pub fund_address: ContractAddress, + pub fund_id: u128, } - // ************************************************************************* + + // *************************************************************************************** // EXTERNALS - // ************************************************************************* + // *************************************************************************************** + #[abi(embed_v0)] impl FundManagerImpl of super::IFundManager { - fn newFund(ref self: ContractState, name: felt252, goal: u64) { - let mut calldata = ArrayTrait::::new(); - calldata.append(self.current_id.read().try_into().unwrap()); - calldata.append(get_caller_address().try_into().unwrap()); - calldata.append(name); - calldata.append(goal.try_into().unwrap()); - let (address_0, _) = deploy_syscall( - self.fund_class_hash.read(), 12345, calldata.span(), false + fn newFund( + ref self: ContractState, + name: ByteArray, + goal: u256, + evidence_link: ByteArray, + contact_handle: ByteArray, + reason: ByteArray, + ) { + assert(goal >= FundConstants::MINIMUM_GOAL, 'Goal must be at least 500'); + let mut call_data: Array = array![]; + Serde::serialize(@self.current_id.read(), ref call_data); + Serde::serialize(@get_caller_address(), ref call_data); + Serde::serialize(@name, ref call_data); + Serde::serialize(@goal, ref call_data); + Serde::serialize(@evidence_link, ref call_data); + Serde::serialize(@contact_handle, ref call_data); + Serde::serialize(@reason, ref call_data); + let (new_fund_address, _) = deploy_syscall( + self.fund_class_hash.read(), 12345, call_data.span(), false ) .unwrap(); - self.funds.write(self.current_id.read(), address_0); + + self.funds.write(self.current_id.read(), new_fund_address); + self + .emit( + FundDeployed { + owner: get_caller_address(), + fund_address: new_fund_address, + fund_id: self.current_id.read() + } + ); + self.current_id.write(self.current_id.read() + 1); } fn getCurrentId(self: @ContractState) -> u128 { @@ -65,5 +118,11 @@ mod FundManager { fn getFund(self: @ContractState, id: u128) -> ContractAddress { return self.funds.read(id); } + fn getOwner(self: @ContractState) -> ContractAddress { + return self.owner.read(); + } + fn getFundClassHash(self: @ContractState) -> ClassHash { + return self.fund_class_hash.read(); + } } } diff --git a/contracts/tests/test_donator.cairo b/contracts/tests/test_donator.cairo index 1402bd8..62d758e 100644 --- a/contracts/tests/test_donator.cairo +++ b/contracts/tests/test_donator.cairo @@ -14,10 +14,11 @@ fn OWNER() -> ContractAddress { } fn __setup__() -> ContractAddress { - let contract = declare("Donator"); + let contract = declare("Donator").unwrap(); let mut calldata: Array = array![]; calldata.append_serde(OWNER()); - contract.deploy(@calldata).unwrap() + let (address, _) = contract.deploy(@calldata).unwrap(); + address } // ************************************************************************* diff --git a/contracts/tests/test_donator_manager.cairo b/contracts/tests/test_donator_manager.cairo new file mode 100644 index 0000000..1187b47 --- /dev/null +++ b/contracts/tests/test_donator_manager.cairo @@ -0,0 +1,89 @@ +// ************************************************************************* +// DONATOR MANAGER TEST +// ************************************************************************* +use starknet::{ContractAddress, contract_address_const}; +use starknet::class_hash::{ClassHash}; +use starknet::syscalls::deploy_syscall; + +use snforge_std::{ + ContractClass, declare, ContractClassTrait, start_cheat_caller_address_global, get_class_hash, + spy_events, EventSpyAssertionsTrait, +}; + +use openzeppelin::utils::serde::SerializedAppend; + +use gostarkme::donatorManager::{ + DonatorManager, IDonatorManagerDispatcher, IDonatorManagerDispatcherTrait +}; + +fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +fn __setup__() -> (ContractAddress, ClassHash) { + // Donator + let donator = declare("Donator").unwrap(); + let mut donator_calldata: Array = array![]; + donator_calldata.append_serde(OWNER()); + let (donator_contract_address, _) = donator.deploy(@donator_calldata).unwrap(); + let donator_class_hash = get_class_hash(donator_contract_address); + + // Donator Manager + let donator_manager = declare("DonatorManager").unwrap(); + let mut donator_manager_calldata: Array = array![]; + donator_manager_calldata.append_serde(donator_class_hash); + let (contract_address, _) = donator_manager.deploy(@donator_manager_calldata).unwrap(); + + return (contract_address, donator_class_hash,); +} + +// ************************************************************************* +// TEST +// ************************************************************************* +#[test] +fn test_constructor() { + // Put owner address like caller + start_cheat_caller_address_global(OWNER()); + let (contract_address, donator_class_hash) = __setup__(); + let donator_manager_contract = IDonatorManagerDispatcher { contract_address }; + let expected_donator_address = donator_manager_contract.getDonatorClassHash(); + let owner = donator_manager_contract.getOwner(); + assert(owner == OWNER(), 'Invalid owner'); + assert(donator_class_hash == expected_donator_address, 'Invalid donator class hash'); +} + +#[test] +fn test_new_donator() { + start_cheat_caller_address_global(OWNER()); + let (contract_address, donator_class_hash) = __setup__(); + let donator_manager_contract = IDonatorManagerDispatcher { contract_address }; + donator_manager_contract.newDonator(); + let expected_donator_class_hash = get_class_hash( + donator_manager_contract.getDonatorByAddress(OWNER()) + ); + assert(expected_donator_class_hash == donator_class_hash, 'Invalid donator address'); +} + +#[test] +fn test_emit_event_donator_contract_deployed() { + start_cheat_caller_address_global(OWNER()); + let (contract_address, _) = __setup__(); + let donator_manager_contract = IDonatorManagerDispatcher { contract_address }; + let mut spy = spy_events(); + donator_manager_contract.newDonator(); + + spy + .assert_emitted( + @array![ + ( + contract_address, + DonatorManager::Event::DonatorContractDeployed( + DonatorManager::DonatorContractDeployed { + new_donator: donator_manager_contract.getDonatorByAddress(OWNER()), + owner: OWNER() + } + ) + ) + ] + ); +} diff --git a/contracts/tests/test_fund.cairo b/contracts/tests/test_fund.cairo index 5c25946..f0fc4df 100644 --- a/contracts/tests/test_fund.cairo +++ b/contracts/tests/test_fund.cairo @@ -1,14 +1,25 @@ -// ************************* +// *************************************************************************************** // FUND TEST -// ************************* +// *************************************************************************************** use starknet::{ContractAddress, contract_address_const}; +use starknet::syscalls::call_contract_syscall; -use snforge_std::{declare, ContractClassTrait, CheatTarget}; +use snforge_std::{ + declare, ContractClassTrait, start_cheat_caller_address_global, start_cheat_caller_address, + cheat_caller_address, CheatSpan, spy_events, EventSpyAssertionsTrait +}; use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +use gostarkme::fund::Fund; use gostarkme::fund::IFundDispatcher; use gostarkme::fund::IFundDispatcherTrait; +use gostarkme::constants::{funds::{fund_manager_constants::FundManagerConstants},}; +use gostarkme::constants::{funds::{state_constants::FundStates},}; +use gostarkme::constants::{funds::{starknet_constants::StarknetConstants},}; + fn ID() -> u128 { 1 @@ -19,28 +30,54 @@ fn OWNER() -> ContractAddress { fn OTHER_USER() -> ContractAddress { contract_address_const::<'USER'>() } -fn NAME() -> felt252 { - 'NAME_FUND_TEST' +fn FUND_MANAGER() -> ContractAddress { + contract_address_const::() +} +fn NAME() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn REASON_1() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" } -fn REASON() -> ByteArray { +fn REASON_2() -> ByteArray { "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" } -fn GOAL() -> u64 { +fn GOAL() -> u256 { 1000 } +fn EVIDENCE_LINK_1() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn EVIDENCE_LINK_2() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn CONTACT_HANDLE_1() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn CONTACT_HANDLE_2() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn VALID_ADDRESS_1() -> ContractAddress { + contract_address_const::() +} fn _setup_() -> ContractAddress { - let contract = declare("Fund"); + let contract = declare("Fund").unwrap(); let mut calldata: Array = array![]; calldata.append_serde(ID()); calldata.append_serde(OWNER()); calldata.append_serde(NAME()); calldata.append_serde(GOAL()); - contract.deploy(@calldata).unwrap() + calldata.append_serde(EVIDENCE_LINK_1()); + calldata.append_serde(CONTACT_HANDLE_1()); + calldata.append_serde(REASON_1()); + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + contract_address } -// ************************* +// *************************************************************************************** // TEST -// ************************* +// *************************************************************************************** #[test] +#[fork("Mainnet")] fn test_constructor() { let contract_address = _setup_(); let dispatcher = IFundDispatcher { contract_address }; @@ -50,12 +87,12 @@ fn test_constructor() { let reason = dispatcher.getReason(); let up_votes = dispatcher.getUpVotes(); let goal = dispatcher.getGoal(); - let current_goal_state = dispatcher.getCurrentGoalState(); - let state = dispatcher.getIsActive(); + let current_goal_state = dispatcher.get_current_goal_state(); + let state = dispatcher.getState(); assert(id == ID(), 'Invalid id'); assert(owner == OWNER(), 'Invalid owner'); assert(name == NAME(), 'Invalid name'); - assert(reason == " ", 'Invalid reason'); + assert(reason == REASON_1(), 'Invalid reason'); assert(up_votes == 0, 'Invalid up votes'); assert(goal == GOAL(), 'Invalid goal'); assert(current_goal_state == 0, 'Invalid current goal state'); @@ -68,10 +105,10 @@ fn test_set_name() { let dispatcher = IFundDispatcher { contract_address }; let name = dispatcher.getName(); assert(name == NAME(), 'Invalid name'); - snforge_std::start_prank(CheatTarget::One(contract_address), OWNER()); - dispatcher.setName('NEW_NAME'); + start_cheat_caller_address_global(OWNER()); + dispatcher.setName("NEW_NAME"); let new_name = dispatcher.getName(); - assert(new_name == 'NEW_NAME', 'Set name method not working') + assert(new_name == "NEW_NAME", 'Set name method not working') } #[test] @@ -79,11 +116,11 @@ fn test_set_reason() { let contract_address = _setup_(); let dispatcher = IFundDispatcher { contract_address }; let reason = dispatcher.getReason(); - assert(reason == " ", 'Invalid reason'); - snforge_std::start_prank(CheatTarget::One(contract_address), OWNER()); - dispatcher.setReason(REASON()); + assert(reason == REASON_1(), 'Invalid reason'); + start_cheat_caller_address_global(OWNER()); + dispatcher.setReason(REASON_2()); let new_reason = dispatcher.getReason(); - assert(new_reason == REASON(), 'Set reason method not working') + assert(new_reason == REASON_2(), 'Set reason method not working') } #[test] @@ -92,7 +129,7 @@ fn test_set_goal() { let dispatcher = IFundDispatcher { contract_address }; let goal = dispatcher.getGoal(); assert(goal == GOAL(), 'Invalid goal'); - snforge_std::start_prank(CheatTarget::One(contract_address), OWNER()); + start_cheat_caller_address_global(FUND_MANAGER()); dispatcher.setGoal(123); let new_goal = dispatcher.getGoal(); assert(new_goal == 123, 'Set goal method not working') @@ -125,32 +162,290 @@ fn test_receive_vote_unsuccessful_double_vote() { dispatcher.receiveVote(); } +// #[test] +// #[fork("Mainnet")] +// fn test_receive_donation_successful() { +// let contract_address = _setup_(); +// let dispatcher = IFundDispatcher { contract_address }; +// let goal: u256 = 10; +// let minter_address = contract_address_const::(); +// let token_address = contract_address_const::(); +// let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; +// // Put state as recollecting dons +// dispatcher.setState(2); +// // Put 10 strks as goal, only fund manager +// start_cheat_caller_address(contract_address, FUND_MANAGER()); +// dispatcher.setGoal(goal); +// // fund the manager with STRK token +// cheat_caller_address(token_address, minter_address, CheatSpan::TargetCalls(1)); +// let mut calldata = array![]; +// calldata.append_serde(FUND_MANAGER()); +// calldata.append_serde(goal); +// call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()).unwrap(); +// // approve +// cheat_caller_address(token_address, FUND_MANAGER(), CheatSpan::TargetCalls(1)); +// token_dispatcher.approve(contract_address, goal); +// // Donate 5 strks +// dispatcher.update_receive_donation(goal / 2); +// let current_goal_state = dispatcher.get_current_goal_state(); +// assert(current_goal_state == goal / 2, 'Receive donation not working'); +// // Donate 5 strks, the goal is done +// dispatcher.update_receive_donation(goal / 2); +// let state = dispatcher.getState(); +// assert(state == 3, 'State should be close'); +// } + #[test] -fn test_receive_donation_successful() { +#[should_panic(expected: ("You are not the fund manager",))] +fn test_set_goal_unauthorized() { let contract_address = _setup_(); let dispatcher = IFundDispatcher { contract_address }; - // Put state as recollecting dons - dispatcher.setIsActive(2); - // Put 10 strks as goal, only owner - snforge_std::start_prank(CheatTarget::One(contract_address), OWNER()); - dispatcher.setGoal(10); - // Donate 5 strks - dispatcher.receiveDonation(5); - let current_goal_state = dispatcher.getCurrentGoalState(); - assert(current_goal_state == 5, 'Receive donation not working'); - // Donate 5 strks, the goal is done - dispatcher.receiveDonation(5); - let state = dispatcher.getIsActive(); - assert(state == 3, 'State should be close'); + // Change the goal without being the fund manager + dispatcher.setGoal(22); } + #[test] -#[should_panic(expected: ('Fund not recollecting dons!',))] -fn test_receive_donation_unsuccessful_wrong_state() { +fn test_new_vote_received_event_emitted_successful() { let contract_address = _setup_(); let dispatcher = IFundDispatcher { contract_address }; - // Put a wrong state to receive donations - dispatcher.setIsActive(1); - // Donate - dispatcher.receiveDonation(5); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OTHER_USER()); + dispatcher.receiveVote(); + + spy + .assert_emitted( + @array![ + ( + contract_address, + Fund::Event::NewVoteReceived( + Fund::NewVoteReceived { + voter: OTHER_USER(), fund: contract_address, votes: 1 + } + ) + ) + ] + ); +} + +// #[test] +// #[fork("Mainnet")] +// fn test_emit_event_donation_withdraw() { +// //Set up contract addresses +// let contract_address = _setup_(); +// let goal: u256 = 10; + +// let dispatcher = IFundDispatcher { contract_address }; +// let minter_address = contract_address_const::(); +// let token_address = contract_address_const::(); +// let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + +// //Set up donation call +// dispatcher.setState(2); +// // Put 10 strks as goal, only fund manager +// start_cheat_caller_address(contract_address, FUND_MANAGER()); +// dispatcher.setGoal(goal); +// // fund the manager with STRK token +// cheat_caller_address(token_address, minter_address, CheatSpan::TargetCalls(1)); +// let mut calldata = array![]; +// calldata.append_serde(FUND_MANAGER()); +// calldata.append_serde(goal); +// call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()).unwrap(); +// // approve +// cheat_caller_address(token_address, FUND_MANAGER(), CheatSpan::TargetCalls(1)); +// token_dispatcher.approve(contract_address, goal); + +// dispatcher.update_receive_donation(goal); + +// start_cheat_caller_address_global(OWNER()); +// cheat_caller_address(token_address, OWNER(), CheatSpan::TargetCalls(1)); + +// // Spy on emitted events and call the withdraw function +// let mut spy = spy_events(); +// dispatcher.withdraw(); + +// // Verify the expected event was emitted with the correct values +// spy +// .assert_emitted( +// @array![ +// ( +// contract_address, +// Fund::Event::DonationWithdraw( +// Fund::DonationWithdraw { +// owner_address: OWNER(), +// fund_contract_address: contract_address, +// withdrawn_amount: 10 +// } +// ) +// ) +// ] +// ); +// } + +#[test] +#[should_panic(expected: ("You are not the owner",))] +fn test_withdraw_with_wrong_owner() { + let contract_address = _setup_(); + + // call withdraw fn with wrong owner + start_cheat_caller_address_global(OTHER_USER()); + IFundDispatcher { contract_address }.withdraw(); +} + +#[test] +#[should_panic(expected: ('Fund not close goal yet.',))] +fn test_withdraw_with_non_closed_state() { + let contract_address = _setup_(); + let fund_dispatcher = IFundDispatcher { contract_address }; + + start_cheat_caller_address_global(FUND_MANAGER()); + // set goal + fund_dispatcher.setGoal(500_u256); + + start_cheat_caller_address_global(OWNER()); + // withdraw funds + fund_dispatcher.withdraw(); +} + +// #[test] +// #[fork("Mainnet")] +// fn test_withdraw() { +// let contract_address = _setup_(); +// let goal: u256 = 500; + +// let dispatcher = IFundDispatcher { contract_address }; +// let minter_address = contract_address_const::(); +// let token_address = contract_address_const::(); +// let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + +// //Set donation state +// dispatcher.setState(2); + +// start_cheat_caller_address(contract_address, FUND_MANAGER()); +// dispatcher.setGoal(goal); + +// cheat_caller_address(token_address, minter_address, CheatSpan::TargetCalls(1)); +// let mut calldata = array![]; +// calldata.append_serde(FUND_MANAGER()); +// calldata.append_serde(goal); +// call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()).unwrap(); + +// cheat_caller_address(token_address, FUND_MANAGER(), CheatSpan::TargetCalls(1)); +// token_dispatcher.approve(contract_address, goal); + +// dispatcher.update_receive_donation(goal); + +// start_cheat_caller_address_global(OWNER()); +// cheat_caller_address(token_address, OWNER(), CheatSpan::TargetCalls(1)); + +// let owner_balance_before = token_dispatcher.balance_of(OWNER()); +// let fund_balance_before = token_dispatcher.balance_of(contract_address); + +// // withdraw +// dispatcher.withdraw(); + +// let owner_balance_after = token_dispatcher.balance_of(OWNER()); +// let fund_balance_after = token_dispatcher.balance_of(contract_address); + +// assert(owner_balance_after == (owner_balance_before + goal), 'wrong owner balance'); +// assert((fund_balance_before - goal) == fund_balance_after, 'wrong fund balance'); +// } + +#[test] +#[fork("Mainnet")] +fn test_emit_event_donation_received() { + //Initial configuration of contract addresses and donation targets + let contract_address = _setup_(); + let goal: u256 = 10; + let dispatcher = IFundDispatcher { contract_address }; + let minter_address = contract_address_const::(); + let token_address = contract_address_const::(); + let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + + //Donation target configuration in the dispatcher + start_cheat_caller_address(contract_address, VALID_ADDRESS_1()); + dispatcher.setState(2); + start_cheat_caller_address(contract_address, FUND_MANAGER()); + dispatcher.setGoal(goal); + + //Provision of STRK token to the fund manager + cheat_caller_address(token_address, minter_address, CheatSpan::TargetCalls(1)); + let mut calldata = array![]; + calldata.append_serde(FUND_MANAGER()); + calldata.append_serde(goal); + call_contract_syscall(token_address, selector!("permissioned_mint"), calldata.span()).unwrap(); + + //Approve + cheat_caller_address(token_address, FUND_MANAGER(), CheatSpan::TargetCalls(1)); + token_dispatcher.approve(contract_address, goal); + let mut spy = spy_events(); + + //Receipt of the donation at the dispatcher + dispatcher.update_receive_donation(goal); + start_cheat_caller_address_global(FUND_MANAGER()); + + //Verification of the current balance and issuance of the expected event + let current_balance = dispatcher.get_current_goal_state(); + spy + .assert_emitted( + @array![ + ( + contract_address, + Fund::Event::DonationReceived( + Fund::DonationReceived { + current_balance, + donated_strks: goal, + donator_address: FUND_MANAGER(), + fund_contract_address: contract_address, + } + ) + ) + ] + ); +} + +#[test] +fn test_set_evidence_link() { + let contract_address = _setup_(); + let dispatcher = IFundDispatcher { contract_address }; + let evidence_link = dispatcher.get_evidence_link(); + assert(evidence_link == EVIDENCE_LINK_1(), 'Invalid evidence_link'); + start_cheat_caller_address_global(OWNER()); + dispatcher.set_evidence_link(EVIDENCE_LINK_2()); + let new_evidence_link = dispatcher.get_evidence_link(); + assert(new_evidence_link == EVIDENCE_LINK_2(), 'Set evidence method not working') +} + +#[test] +#[should_panic(expected: ("You are not the owner",))] +fn test_set_evidence_link_wrong_owner() { + let contract_address = _setup_(); + + // call set_evidence_link fn with wrong owner + start_cheat_caller_address_global(OTHER_USER()); + IFundDispatcher { contract_address }.set_evidence_link(EVIDENCE_LINK_2()); +} + +#[test] +fn test_set_contact_handle() { + let contract_address = _setup_(); + let dispatcher = IFundDispatcher { contract_address }; + let contact_handle = dispatcher.get_contact_handle(); + assert(contact_handle == CONTACT_HANDLE_1(), 'Invalid contact handle'); + start_cheat_caller_address_global(OWNER()); + dispatcher.set_contact_handle(CONTACT_HANDLE_2()); + let new_contact_handle = dispatcher.get_contact_handle(); + assert(new_contact_handle == CONTACT_HANDLE_2(), 'Set contact method not working') +} + +#[test] +#[should_panic(expected: ("You are not the owner",))] +fn test_set_contact_handle_wrong_owner() { + let contract_address = _setup_(); + + // call set_contact_handle fn with wrong owner + start_cheat_caller_address_global(OTHER_USER()); + IFundDispatcher { contract_address }.set_contact_handle(CONTACT_HANDLE_2()); } diff --git a/contracts/tests/test_fund_manager.cairo b/contracts/tests/test_fund_manager.cairo new file mode 100755 index 0000000..effffd5 --- /dev/null +++ b/contracts/tests/test_fund_manager.cairo @@ -0,0 +1,136 @@ +// *************************************************************************************** +// FUND MANAGER TEST +// *************************************************************************************** +use starknet::{ContractAddress, contract_address_const}; +use starknet::class_hash::{ClassHash}; +use starknet::syscalls::deploy_syscall; + +use snforge_std::{ + ContractClass, declare, ContractClassTrait, start_cheat_caller_address_global, get_class_hash, + spy_events, EventSpyAssertionsTrait +}; + +use openzeppelin::utils::serde::SerializedAppend; + +use gostarkme::fundManager::IFundManagerDispatcher; +use gostarkme::fundManager::IFundManagerDispatcherTrait; +use gostarkme::fundManager::FundManager; + +fn ID() -> u128 { + 1 +} +fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} +fn OTHER_USER() -> ContractAddress { + contract_address_const::<'USER'>() +} +fn NAME() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn REASON() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn GOAL() -> u256 { + 1000 +} +fn BAD_GOAL() -> u256 { + 4 +} +fn EVIDENCE_LINK() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn CONTACT_HANDLE() -> ByteArray { + "Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum, Lorem impsum" +} +fn _setup_() -> (ContractAddress, ClassHash) { + // Fund + let fund = declare("Fund").unwrap(); + let mut fund_calldata: Array = array![]; + fund_calldata.append_serde(ID()); + fund_calldata.append_serde(OWNER()); + fund_calldata.append_serde(NAME()); + fund_calldata.append_serde(GOAL()); + fund_calldata.append_serde(EVIDENCE_LINK()); + fund_calldata.append_serde(CONTACT_HANDLE()); + fund_calldata.append_serde(REASON()); + + let (fund_contract_address, _) = fund.deploy(@fund_calldata).unwrap(); + let fund_class_hash = get_class_hash(fund_contract_address); + + // Fund Manager + let fund_manager = declare("FundManager").unwrap(); + let mut fund_manager_calldata: Array = array![]; + fund_manager_calldata.append_serde(fund_class_hash); + let (contract_address, _) = fund_manager.deploy(@fund_manager_calldata).unwrap(); + + return (contract_address, fund_class_hash); +} + +// ****************************************************************************** +// TEST +// ****************************************************************************** + +#[test] +fn test_constructor() { + start_cheat_caller_address_global(OWNER()); + let (contract_address, fund_class_hash) = _setup_(); + let fund_manager_contract = IFundManagerDispatcher { contract_address }; + let expected_fund_address = fund_manager_contract.getFundClassHash(); + let owner = fund_manager_contract.getOwner(); + assert(owner == OWNER(), 'Invalid owner'); + assert(fund_class_hash == expected_fund_address, 'Invalid fund class hash'); +} + +#[test] +fn test_new_fund() { + start_cheat_caller_address_global(OWNER()); + let (contract_address, fund_class_hash) = _setup_(); + let fund_manager_contract = IFundManagerDispatcher { contract_address }; + fund_manager_contract.newFund(NAME(), GOAL(), EVIDENCE_LINK(), CONTACT_HANDLE(), REASON()); + let expected_fund_class_hash = get_class_hash(fund_manager_contract.getFund(1)); + let current_id = fund_manager_contract.getCurrentId(); + assert(expected_fund_class_hash == fund_class_hash, 'Invalid fund address'); + assert(current_id == 2, 'Invalid current ID'); +} + +#[test] +#[should_panic(expected: 'Goal must be at least 500')] +fn test_new_fund_bad_goal() { + start_cheat_caller_address_global(OWNER()); + let (contract_address, _) = _setup_(); + let fund_manager_contract = IFundManagerDispatcher { contract_address }; + fund_manager_contract.newFund(NAME(), BAD_GOAL(), EVIDENCE_LINK(), CONTACT_HANDLE(), REASON()); +} + +#[test] +fn test_fund_deployed_event() { + let (contract_address, _) = _setup_(); + let fund_manager_contract = IFundManagerDispatcher { contract_address }; + + start_cheat_caller_address_global(OWNER()); + + let mut spy = spy_events(); + + let current_id = fund_manager_contract.getCurrentId(); + fund_manager_contract.newFund(NAME(), GOAL(), EVIDENCE_LINK(), CONTACT_HANDLE(), REASON()); + + let expected_fund_class_hash = fund_manager_contract.getFund(1); + + spy + .assert_emitted( + @array![ + ( + contract_address, + FundManager::Event::FundDeployed( + FundManager::FundDeployed { + fund_address: expected_fund_class_hash, + fund_id: current_id, + owner: OWNER(), + } + ) + ) + ] + ); +} + diff --git a/frontend/README.md b/frontend/README.md index c403366..1d0c386 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,36 +1,114 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -## Getting Started -First, run the development server: + +### Pre-requisites + +- Install Node.js (v20.15.1) using Node Version Manager (NVM) + + We recommend using [Node Version Manager (NVM) ](https://github.com/nvm-sh/nvm) to easily manage different versions of Node.js on your system. + +### Local configurations + + +- Rename the `frontend/gostarkme-web/.env.example` file to: `frontend/gostarkme-web/.env`. + + + +- Comment the content of the file `frontend/gostarkme-web/next.config.mjs`. Adding only one element to the ```nextConfig``` object like this: + +``` +/** @type {import('next').NextConfig} */ +const nextConfig = { + /** + * Enable static exports for the App Router. + * + * @see https://nextjs.org/docs/app/building-your-application/deploying/static-exports + */ + // output: "export", + + /** + * Set base path. This is the slug of your GitHub repository. + * + * @see https://nextjs.org/docs/app/api-reference/next-config-js/basePath + */ + // basePath: "/gostarkme", + + // assetPrefix: 'https://web3wagers.github.io/gostarkme', + + /** + * Disable server-based image optimization. Next.js does not support + * dynamic features with static exports. + * + * @see https://nextjs.org/docs/app/api-reference/components/image#unoptimized + */ + // images: { + // unoptimized: true, + // }, + + reactStrictMode: false, + }; + + export default nextConfig; +``` + + +## Local Deployment + +### Installing Dependencies + +First, install the required packages: + +```bash +cd gostarkme/frontend/gostarkme-web + +npm install +``` +### Running the Application + + +Once the dependencies are installed, start the development server: + ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` + + + Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. -## Learn More +## Production Deployment -To learn more about Next.js, take a look at the following resources: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### Installing Dependencies + +First, install the required packages: + +```bash +cd gostarkme/frontend/gostarkme-web + +npm install +``` -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! -## Deploy on Vercel +### Build the Application +To create an optimized production build, run: -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +```bash +npm run build +``` + + +### Serve the build +Start the production server: +```bash +npm run start +``` + + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/frontend/gostarkme-web/.gitignore b/frontend/gostarkme-web/.gitignore index d13968e..81093c6 100644 --- a/frontend/gostarkme-web/.gitignore +++ b/frontend/gostarkme-web/.gitignore @@ -35,4 +35,6 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -/package-lock.json \ No newline at end of file +/package-lock.json +/yarn.lock +/.env* \ No newline at end of file diff --git a/frontend/gostarkme-web/animations/StardustAnimation.tsx b/frontend/gostarkme-web/animations/StardustAnimation.tsx index 807fe5f..1a83f5a 100644 --- a/frontend/gostarkme-web/animations/StardustAnimation.tsx +++ b/frontend/gostarkme-web/animations/StardustAnimation.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react'; -export const StardustAnimation = () => { +export const StardustAnimation = ({height, width}: {height?: number, width?: number}) => { const canvasRef = useRef(null); useEffect(() => { @@ -11,8 +11,8 @@ export const StardustAnimation = () => { let animationFrameId: number; // Set canvas size - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; + canvas.width = width ?? window.innerWidth; + canvas.height = height ?? window.innerHeight; // Create stars const stars: { x: number; y: number; size: number; speed: number; }[] = []; @@ -54,7 +54,7 @@ export const StardustAnimation = () => { return () => { cancelAnimationFrame(animationFrameId); }; - }, []); + }, [height, width]); - return ; + return ; }; \ No newline at end of file diff --git a/frontend/gostarkme-web/app/app/confirmation/page.tsx b/frontend/gostarkme-web/app/app/confirmation/page.tsx new file mode 100644 index 0000000..4d82993 --- /dev/null +++ b/frontend/gostarkme-web/app/app/confirmation/page.tsx @@ -0,0 +1,11 @@ +import Confirmation from "@/components/modules/confirmation/Confirmation"; + +const ConfirmationPage = async () => { + return ( + <> + + + ); +}; + +export default ConfirmationPage; \ No newline at end of file diff --git a/frontend/gostarkme-web/app/app/fund/page.tsx b/frontend/gostarkme-web/app/app/fund/page.tsx new file mode 100644 index 0000000..308fdd6 --- /dev/null +++ b/frontend/gostarkme-web/app/app/fund/page.tsx @@ -0,0 +1,42 @@ +'use client'; +import Fund from "@/components/modules/Fund/Fund"; +import Bounded from "@/components/ui/Bounded"; +import Navbar from "@/components/ui/Navbar"; +import { navItems } from "@/constants"; +import { clickedFundState } from "@/state/nFunds"; +import { useAtomValue } from "jotai"; + +const FundDetailsPage = () => { + + const clickedFund = useAtomValue(clickedFundState); + + return ( + <> + {clickedFund && + + + + } + + {!clickedFund && + <> + +
+ Funding not found, please go back to dashboard and search for the funding again. +
+ + } + + ); +}; + +export default FundDetailsPage; diff --git a/frontend/gostarkme-web/app/app/myfunds/page.tsx b/frontend/gostarkme-web/app/app/myfunds/page.tsx new file mode 100644 index 0000000..815da41 --- /dev/null +++ b/frontend/gostarkme-web/app/app/myfunds/page.tsx @@ -0,0 +1,30 @@ +'use client' + +import UserFunds from '@/components/modules/myfunds/UserFunds'; +import Navbar from '@/components/ui/Navbar'; +import Footer from '@/components/ui/Footer'; +import { navItems } from '@/constants'; + +const MyFundsPage = () => { + + return ( +
+ +
+ +
+
+
+ ); +}; + +export default MyFundsPage; \ No newline at end of file diff --git a/frontend/gostarkme-web/app/app/myprofile/page.tsx b/frontend/gostarkme-web/app/app/myprofile/page.tsx new file mode 100644 index 0000000..fa775fb --- /dev/null +++ b/frontend/gostarkme-web/app/app/myprofile/page.tsx @@ -0,0 +1,102 @@ +'use client' + +import ProgressBar from '@/components/ui/ProgressBar'; +import Divider from '@/components/ui/Divider'; +import Image from 'next/image'; +import Footer from '@/components/ui/Footer'; +import Navbar from '@/components/ui/Navbar'; +import { walletStarknetkitLatestAtom } from '@/state/connectedWallet'; +import { useAtomValue } from 'jotai'; +import { navItems } from '@/constants'; + + +const UserProfilePage = () => { + + const wallet = useAtomValue(walletStarknetkitLatestAtom); + + // Mock data for design purposes + const totalDonations = 20000; + const currentLevel = 10; + const currentPoints = 200; + const totalPoints = 500; + + // Calculate progress percentage + const progress = (currentPoints / totalPoints) * 100; + + return ( +
+ + {wallet !== undefined ? ( +
+ {/* Profile Section */} +
+ {/* Profile Header */} +

+ + {wallet?.account?.address.slice(0, 5)}...{wallet?.account?.address.slice(-4)} + + {"'s Profile "} {'\u2728'} +

+ + + + {/* Total Donations and Current Level */} +

+ Total donations: + + {totalDonations.toLocaleString()} + + STRKs +

+ +

+ Current level: {currentLevel} +

+

+ Your progress to next level +

+ + + + + +
+ {currentPoints} / {totalPoints} + STRKs +
+
+
+ ) : ( +
+
+ Please connect your wallet to see your profile. +
+
+ )} +
+
+ ); +}; + +export default UserProfilePage; diff --git a/frontend/gostarkme-web/app/app/newfunding/page.tsx b/frontend/gostarkme-web/app/app/newfunding/page.tsx new file mode 100644 index 0000000..13e43f1 --- /dev/null +++ b/frontend/gostarkme-web/app/app/newfunding/page.tsx @@ -0,0 +1,20 @@ +"use client"; // Mark this component as a Client Component +import Bounded from "@/components/ui/Bounded"; +import Divider from "@/components/ui/Divider"; +import Stages from "@/components/modules/newfunding/Stages"; + +const NewFundingPage = () => { + return ( + <> + +

+ Your new fund! +

+ + +
+ + ); +}; + +export default NewFundingPage; diff --git a/frontend/gostarkme-web/app/app/page.tsx b/frontend/gostarkme-web/app/app/page.tsx new file mode 100644 index 0000000..36db824 --- /dev/null +++ b/frontend/gostarkme-web/app/app/page.tsx @@ -0,0 +1,108 @@ +'use client'; +import FundCards from "@/components/dashboard/fundCard"; +import Footer from "@/components/ui/Footer"; +import Navbar from "@/components/ui/Navbar"; +import { FUND_MANAGER_ADDR } from "@/constants"; +import { fundAbi } from "@/contracts/abis/fund"; +import { fundManager } from "@/contracts/abis/fundManager"; +import { walletStarknetkitLatestAtom } from "@/state/connectedWallet"; +import { useAtomValue } from "jotai"; +import React, { useEffect, useState } from "react"; +import { Contract } from "starknet"; +import { navItems } from "@/constants"; +import LoadingSpinner from "@/components/ui/LoadingSpinner"; + +const Dashboard = () => { + + const wallet = useAtomValue(walletStarknetkitLatestAtom); + + const [funds, setFunds] = useState([]); + + const [loading, setLoading] = useState(true); + + async function getFunds() { + const fundManagerContract = new Contract(fundManager, FUND_MANAGER_ADDR, wallet?.account); + const id = await fundManagerContract.getCurrentId(); + let fundings = []; + for (let i = 1; i < id; i++) { + // GET FUND ADDRESS + let fundaddr = await fundManagerContract.getFund(i); + fundaddr = "0x" + fundaddr.toString(16); + const fundContract = new Contract(fundAbi, fundaddr, wallet?.account); + // GET FUND STATE + let state = await fundContract.getState(); + if (state == 4 || state == 0) { + continue; + } + // GET FUND NAME + let name = await fundContract.getName(); + // GET FUND DESCRIPTION + let desc = await fundContract.getReason(); + let desclen = desc.length; + if (desclen > 50) { + desc = desc.substring(0, 50) + "..."; + } + // GET FUND ID + let fund_id = await fundContract.getId(); + fundings.push({ + type: "Project", + title: name, + description: desc, + fund_id: fund_id.toString(), + }); + } + setFunds(fundings); + setLoading(false); + } + + useEffect(() => { + getFunds(); + }, []); + + return ( +
+ + {!wallet && +
+ Please connect your wallet to see funding dashboard. +
+ } + + {loading && wallet &&
+ +
+ Loading funds... +
+
} + + {funds.length !== 0 && !loading && wallet && +
+ {funds.map((fund: { type: string; title: string; description: string; fund_id: string }, index: number) => ( + + ))} +
+ } + + {funds.length === 0 && !loading && wallet && +
+
+ There is no fundings to display. +
+
+ } + + +
+ ); +}; + +export default Dashboard; diff --git a/frontend/gostarkme-web/app/page.tsx b/frontend/gostarkme-web/app/page.tsx index c0b2ac4..4bd231f 100644 --- a/frontend/gostarkme-web/app/page.tsx +++ b/frontend/gostarkme-web/app/page.tsx @@ -2,15 +2,20 @@ import { LinkButton } from "@/components/ui/LinkButton"; import { WelcomeBar } from "@/components/welcomepage/WelcomeBar"; import { WelcomeItems } from "@/components/welcomepage/WelcomeItems"; +import Footer from "@/components/ui/Footer"; import Image from "next/image"; import { StardustAnimation } from "@/animations/StardustAnimation"; +import useComponentSize from "@/hooks/useComponentSize.hook"; export default function Home() { const ROOT = process.env.NEXT_PUBLIC_APP_ROOT; - + + const [ref, width, height] = useComponentSize(); + return ( -
+
+

Upload your cause

@@ -18,8 +23,7 @@ export default function Home() { -
- +
stark logo - + +
{/* */}
@@ -102,6 +107,7 @@ export default function Home() {
+
); } diff --git a/frontend/gostarkme-web/app/utils/index.ts b/frontend/gostarkme-web/app/utils/index.ts new file mode 100644 index 0000000..7b3a813 --- /dev/null +++ b/frontend/gostarkme-web/app/utils/index.ts @@ -0,0 +1,11 @@ +export default function hex2ascii(hexx: string) { + var hex = hexx.toString();//force conversion + var str = ''; + for (var i = 0; i < hex.length; i += 2) + str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); + return str; +} + +export function calculatePorcentage(qty: number, goal: number): number { + return (Number(qty) / Number(goal)) * 100; +} \ No newline at end of file diff --git a/frontend/gostarkme-web/components/dashboard/fundCard.tsx b/frontend/gostarkme-web/components/dashboard/fundCard.tsx new file mode 100644 index 0000000..5b4ad4d --- /dev/null +++ b/frontend/gostarkme-web/components/dashboard/fundCard.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { StardustAnimation } from "@/animations/StardustAnimation"; +import useComponentSize from "@/hooks/useComponentSize.hook"; +import { clickedFundState } from "@/state/nFunds"; +import { useSetAtom } from "jotai"; +import Link from "next/link"; + +interface FundCardProps { + fund: { + type: string; + title: string; + description: string; + fund_id: string + }; + index: number; +} + +const FundCards = ({ fund, index }: FundCardProps) => { + const [ref, width, height] = useComponentSize(); + + const setClickedFund = useSetAtom(clickedFundState); + + function handleNav() { + setClickedFund({id: Number(fund.fund_id), name: fund.title}); + } + + return ( +
+ +
+
+

+ {fund.type} {fund.type === "Project" ? 🚀 : 🫀} +

+

+ {fund.title} +

+
+
+ {fund.description !== " " ? ( +

{fund.description}

+ ) : + ( +

No description provided

+ )} +
+ +
+ +
+ ); +}; + +export default FundCards; diff --git a/frontend/gostarkme-web/components/modules/Fund/Fund.tsx b/frontend/gostarkme-web/components/modules/Fund/Fund.tsx new file mode 100644 index 0000000..e21fcc5 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/Fund/Fund.tsx @@ -0,0 +1,110 @@ +"use client"; + +import FundDonate from "./FundDonate"; +import starknetlogo from "@/public/icons/starklogo.png"; +import { FundVote } from "./FundVote"; + +import { useEffect, useState } from "react"; +import { FUND_MANAGER_ADDR, upVotesNeeded } from "@/constants"; +import Divider from "@/components/ui/Divider"; +import { fundAbi } from "@/contracts/abis/fund"; +import { fundManager } from "@/contracts/abis/fundManager"; +import { walletStarknetkitLatestAtom } from "@/state/connectedWallet"; +import { useAtomValue } from "jotai"; +import { Contract } from "starknet"; +import { clickedFundState } from "@/state/nFunds"; +import LoadingSpinner from "@/components/ui/LoadingSpinner"; +import { FundWithdraw } from "./FundWithdraw"; + +const Fund = () => { + + const wallet = useAtomValue(walletStarknetkitLatestAtom); + + const [fundManagerContract, _setFundManagerContract] = useState(new Contract(fundManager, FUND_MANAGER_ADDR, wallet?.account)); + + const [fund, setFund] = useState({}); + + const [loading, setLoading] = useState(true); + + const clickedFund = useAtomValue(clickedFundState); + + async function getDetails() { + let addr = await fundManagerContract.getFund(clickedFund?.id); + addr = "0x" + addr.toString(16); + const fundContract = new Contract(fundAbi, addr, wallet?.account); + + // GET FUND NAME + let name = await fundContract.getName(); + // GET FUND DESCRIPTION + + let desc = await fundContract.getReason(); + if (desc == " ") { + desc = "No description provided"; + } + let state = await fundContract.getState(); + + let currentBalance = await fundContract.get_current_goal_state(); + + currentBalance = BigInt(currentBalance) / BigInt(10 ** 18); + + let goal = await fundContract.getGoal(); + goal = BigInt(goal) / BigInt(10 ** 18); + + let upVotes = await fundContract.getUpVotes(); + + let evidenceLink = await fundContract.get_evidence_link(); + + let contactHandle = await fundContract.get_contact_handle(); + + setFund({ + name: name, + desc: desc, + state: state, + currentBalance: currentBalance, + goal: goal, + upVotes: upVotes, + addr: addr, + evidenceLink: evidenceLink, + contactHandle: contactHandle + }); + setLoading(false); + } + + useEffect(() => { + getDetails(); + }, []); + + return ( + <> + {loading && +
+ +
+ Loading funding... +
+
+ } + {!loading && +
+

{fund.name}

+ +

Description

+

{fund.desc}

+ +

Evidence

+
{fund.evidenceLink} + +

Contact handle

+ {fund.contactHandle} + {Number(fund.state) === 0 &&

Fund is currently innactive.

} + {Number(fund.state) === 1 && } + {Number(fund.state) === 2 && } + {Number(fund.state) === 3 && } + {Number(fund.state) === 4 &&

Fund was already withdrawed.

} +
+ } + + ); +}; + +export default Fund; diff --git a/frontend/gostarkme-web/components/modules/Fund/FundDonate.tsx b/frontend/gostarkme-web/components/modules/Fund/FundDonate.tsx new file mode 100644 index 0000000..4c5d5bd --- /dev/null +++ b/frontend/gostarkme-web/components/modules/Fund/FundDonate.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { calculatePorcentage } from "@/app/utils"; +import ProgressBar from "@/components/ui/ProgressBar"; +import { provider } from "@/constants"; +import { strkAbi } from "@/contracts/abis/strk"; +import { addrSTRK } from "@/contracts/addresses"; +import { walletStarknetkitLatestAtom } from "@/state/connectedWallet"; +import { useAtomValue, useSetAtom } from "jotai"; +import Image, { StaticImageData } from "next/image"; +import { useState } from "react"; +import { CallData, Contract, InvokeFunctionResponse, cairo } from "starknet"; +import { useRouter } from "next/navigation"; +import { latestTxAtom } from "@/state/latestTx"; + +interface FundDonateProps { + currentBalance: number; + goal: number; + addr: string; + icon?: StaticImageData; +} + +const FundDonate = ({ currentBalance, goal, addr, icon }: FundDonateProps) => { + const [amount, setAmount] = useState(""); + const [error, setError] = useState(""); + const setLatestTx = useSetAtom(latestTxAtom); + const wallet = useAtomValue(walletStarknetkitLatestAtom); + const progress = calculatePorcentage(currentBalance, goal); + const router = useRouter(); + + const handleAmountChange = (e: React.ChangeEvent) => { + const value = e.target.value === "" ? "" : Number(e.target.value); + setAmount(value); + setError(""); + }; + + const handleDonateClick = async (e: React.MouseEvent) => { + e.preventDefault(); + if (amount === "") { + setError("This field is required."); + } else if (typeof amount === "number" && amount < 0) { + setError("The amount cannot be negative."); + } else { + setError(""); + await wallet?.account.execute([ + { + contractAddress: addrSTRK, + entrypoint: 'transfer', + calldata: CallData.compile({ + recipient: addr, + amount: cairo.uint256(amount * 10 ** 18 ) + }), + }, + { + contractAddress: addr, + entrypoint: 'update_receive_donation', + calldata: CallData.compile({ + strks: cairo.uint256(amount * 10 ** 18 ) + }), + }, + ]).then(async (resp: InvokeFunctionResponse) => { + setLatestTx({ txHash: resp.transaction_hash, type: "donation" }); + router.push("/app/confirmation"); + }) + .catch((e: any) => { }); + } + }; + + + return ( +
+ +
+

{currentBalance.toString()} / {goal.toString()}

+ icon +
+
+ +
+ {error && ( +

{error}

+ )} + +
+ ); +}; + +export default FundDonate; diff --git a/frontend/gostarkme-web/components/modules/Fund/FundVote.tsx b/frontend/gostarkme-web/components/modules/Fund/FundVote.tsx new file mode 100644 index 0000000..35d91a0 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/Fund/FundVote.tsx @@ -0,0 +1,64 @@ +import { calculatePorcentage } from "@/app/utils"; +import { Button } from "@/components/ui/Button"; +import ProgressBar from "@/components/ui/ProgressBar"; +import { fundAbi } from "@/contracts/abis/fund"; +import { walletStarknetkitLatestAtom } from "@/state/connectedWallet"; +import { latestTxAtom } from "@/state/latestTx"; +import { useAtomValue, useSetAtom } from "jotai"; +import { Contract, InvokeFunctionResponse } from "starknet"; +import { useRouter } from "next/navigation"; + +interface FundVoteProps { + upVotes: number, + upVotesNeeded: number, + addr: string, + setLoading: (load: boolean) => void, + getDetails: () => void, +} + +export const FundVote = ({ upVotes, upVotesNeeded, addr, setLoading, getDetails }: FundVoteProps) => { + + const wallet = useAtomValue(walletStarknetkitLatestAtom); + + const progress = calculatePorcentage(upVotes, upVotesNeeded); + + const setLatestTx = useSetAtom(latestTxAtom); + + const router = useRouter(); + + async function vote() { + setLoading(true); + const fundContract = new Contract(fundAbi, addr, wallet?.account); + fundContract.receiveVote() + .then(async (resp: InvokeFunctionResponse) => { + setLatestTx({ txHash: resp.transaction_hash, type: "vote" }); + router.push("/app/confirmation"); + }) + .catch((e: any) => { getDetails() }); + } + + return ( +
+ +
+

{upVotes.toString()} / {upVotesNeeded.toString()}

+

🌟

+
+ {wallet ? ( // Check if a wallet is connected by evaluating 'wallet' condition +
+ )} + + ); +}; diff --git a/frontend/gostarkme-web/components/modules/Fund/FundWithdraw.tsx b/frontend/gostarkme-web/components/modules/Fund/FundWithdraw.tsx new file mode 100644 index 0000000..cc2c592 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/Fund/FundWithdraw.tsx @@ -0,0 +1,50 @@ +import { calculatePorcentage } from "@/app/utils"; +import { Button } from "@/components/ui/Button"; +import ProgressBar from "@/components/ui/ProgressBar"; +import { fundAbi } from "@/contracts/abis/fund"; +import { walletStarknetkitLatestAtom } from "@/state/connectedWallet"; +import { latestTxAtom } from "@/state/latestTx"; +import { useAtomValue, useSetAtom } from "jotai"; +import { Contract, InvokeFunctionResponse } from "starknet"; +import { useRouter } from "next/navigation"; + +interface FundWithdrawProps { + addr: string, + setLoading: (load: boolean) => void, + getDetails: () => void, + currentBalance: number; + goal: number; +} + +export const FundWithdraw = ({ currentBalance, goal, addr, setLoading, getDetails }: FundWithdrawProps) => { + + const wallet = useAtomValue(walletStarknetkitLatestAtom); + + const progress = calculatePorcentage(currentBalance, goal); + + const setLatestTx = useSetAtom(latestTxAtom); + + const router = useRouter(); + + async function withdraw() { + setLoading(true); + const fundContract = new Contract(fundAbi, addr, wallet?.account); + fundContract.withdraw() + .then(async (resp: InvokeFunctionResponse) => { + setLatestTx({ txHash: resp.transaction_hash, type: "withdrawn" }); + router.push("/app/confirmation"); + }) + .catch((e: any) => { getDetails() }); + } + + return ( +
+ +
+

{currentBalance.toString()} / {goal.toString()}

+

🌟

+
+ +
+ ); +}; diff --git a/frontend/gostarkme-web/components/modules/confirmation/Confirmation.tsx b/frontend/gostarkme-web/components/modules/confirmation/Confirmation.tsx new file mode 100644 index 0000000..563c5f5 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/confirmation/Confirmation.tsx @@ -0,0 +1,64 @@ +'use client'; +import React, { useEffect } from "react"; +import CreationConfirmation from "./CreationConfirmation"; +import VoteConfirmation from "./VoteConfirmation"; +import DonationConfirmation from "./DonationConfirmation"; +import { useAtom, useAtomValue } from "jotai"; +import { latestTxAtom } from "@/state/latestTx"; +import Navbar from "@/components/ui/Navbar"; +import { navItems } from "@/constants"; +import { clickedFundState } from "@/state/nFunds"; +import { walletStarknetkitLatestAtom } from "@/state/connectedWallet"; +import WithdrawConfirmation from "./WithdrawConfirmation"; + +const Confirmation = () => { + const tx = useAtomValue(latestTxAtom); + const actualFund = useAtomValue(clickedFundState); + const voteMessage = ` 🗳️ Just cast my vote for an amazing cause called ${actualFund?.name} on Go Stark Me! This fund needs more votes to start raising funds—every vote counts! Let’s support projects that make a difference at https://web3wagers.github.io/gostarkme/ @undefined_org_ 🙌💫 #GoStarkMe #Starknet #CommunityPower`; + const donationMessage = `🙌 Proud to support ${actualFund?.name} on Go Stark Me! Donations make a difference. 💪 Go ahead and donate at https://web3wagers.github.io/gostarkme/ @undefined_org_ #Starknet #GoStarkMe #Web3Wagers`; + const newFundMessage = `🚀 Just launched a new fund on Go Stark Me called ${actualFund?.name}! I’m raising support for an important cause, and every contribution makes a difference. Join me in making an impact at https://web3wagers.github.io/gostarkme/! 💪🌍 Check it out on @undefined_org_ #GoStarkMe #Starknet #BlockchainForGood`; + const withdrawnMessage = `🎉 We did it! The goal for ${actualFund?.name} on Go Stark Me has been reached, and funds have been successfully withdrawn! 🙌 Huge thanks to everyone who contributed and made this possible. Let’s keep making an impact! 🌍💪 Check it out at https://web3wagers.github.io/gostarkme/ #GoStarkMe #Starknet #CommunitySuccess`; + + return ( + <> + + {tx === undefined && +
+ The place you are trying to reach is not enabled yet. +
+ } + + {tx !== undefined && +
+

Success 🚀

+ {tx?.type === "newfund" && + + } + + {tx?.type === "vote" && + + } + + {tx?.type === "donation" && + + } + + {tx?.type === "withdrawn" && + + } +
+ } + + ) +}; + +export default Confirmation; diff --git a/frontend/gostarkme-web/components/modules/confirmation/CreationConfirmation.tsx b/frontend/gostarkme-web/components/modules/confirmation/CreationConfirmation.tsx new file mode 100644 index 0000000..fca5460 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/confirmation/CreationConfirmation.tsx @@ -0,0 +1,22 @@ +import ShareXButton from "@/components/ui/ShareOnX"; +import React from "react"; + +interface CreationConfirmationProps { + txHash: String; + message: String; +} + +const CreationConfirmation: React.FC = ({ + txHash, + message, +}) => ( + <> +
+

Your funding was created, take a look at the transaction here.

+

Share your contribution via X to tell everyone how cool you are

+ +
+ +); + +export default CreationConfirmation; diff --git a/frontend/gostarkme-web/components/modules/confirmation/DonationConfirmation.tsx b/frontend/gostarkme-web/components/modules/confirmation/DonationConfirmation.tsx new file mode 100644 index 0000000..6641179 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/confirmation/DonationConfirmation.tsx @@ -0,0 +1,22 @@ +import ShareXButton from "@/components/ui/ShareOnX"; +import React from "react"; + +interface DonationConfirmationProps { + txHash: String; + message: String; +} + +const DonationConfirmation: React.FC = ({ + txHash, + message, +}) => ( + <> +
+

Your donation was sent to the funding, take a look at the transaction here.

+

Share your contribution via X to tell everyone how cool you are

+ +
+ +); + +export default DonationConfirmation; diff --git a/frontend/gostarkme-web/components/modules/confirmation/VoteConfirmation.tsx b/frontend/gostarkme-web/components/modules/confirmation/VoteConfirmation.tsx new file mode 100644 index 0000000..2dfc05d --- /dev/null +++ b/frontend/gostarkme-web/components/modules/confirmation/VoteConfirmation.tsx @@ -0,0 +1,22 @@ +import ShareXButton from "@/components/ui/ShareOnX"; +import React from "react"; + +interface VoteConfirmationProps { + txHash: String; + message: String; +} + +const VoteConfirmation: React.FC = ({ + txHash, + message, +}) => ( + <> +
+

Your vote was submitted, take a look at the transaction here.

+

Share your contribution via X to tell everyone how cool you are

+ +
+ +); + +export default VoteConfirmation; diff --git a/frontend/gostarkme-web/components/modules/confirmation/WithdrawConfirmation.tsx b/frontend/gostarkme-web/components/modules/confirmation/WithdrawConfirmation.tsx new file mode 100644 index 0000000..cf1f07e --- /dev/null +++ b/frontend/gostarkme-web/components/modules/confirmation/WithdrawConfirmation.tsx @@ -0,0 +1,22 @@ +import ShareXButton from "@/components/ui/ShareOnX"; +import React from "react"; + +interface WithdrawConfirmationProps { + txHash: String; + message: String; +} + +const WithdrawConfirmation: React.FC = ({ + txHash, + message, +}) => ( + <> +
+

🎉 Success! Your funds have been withdrawn. Check out the transaction details and celebrate! 💸✨ here.

+

Goal Achieved! Your funds have been withdrawn, and the campaign is a success! Share your contribution on X and let everyone know how awesome you are!

+ +
+ +); + +export default WithdrawConfirmation; diff --git a/frontend/gostarkme-web/components/modules/myfunds/FundCard.tsx b/frontend/gostarkme-web/components/modules/myfunds/FundCard.tsx new file mode 100644 index 0000000..52e0439 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/myfunds/FundCard.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { StardustAnimation } from '@/animations/StardustAnimation'; + +type FundType = 'Project' | 'Fund'; + +interface FundCardProps { + type: FundType; + title: string; + description: string; + onClick?: () => void; +} + +const FundCard: React.FC = ({ type, title, description, onClick }) => { + const fundEmoji = type === "Project" ? '🧠' : "🫀" + + return ( +
+
+ +
+
+
+ + {type} + + + {onClick && ( + + )} +
+

{title}

+
+

{description}

+
+
+
+ ); +}; + +export default FundCard; \ No newline at end of file diff --git a/frontend/gostarkme-web/components/modules/myfunds/UserFunds.tsx b/frontend/gostarkme-web/components/modules/myfunds/UserFunds.tsx new file mode 100644 index 0000000..49951bf --- /dev/null +++ b/frontend/gostarkme-web/components/modules/myfunds/UserFunds.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import FundCard from '@/components/modules/myfunds/FundCard'; +import { LinkButton } from '@/components/ui/LinkButton'; +import { useAtomValue } from 'jotai'; +import { walletStarknetkitLatestAtom } from '@/state/connectedWallet'; + +const UserFunds = () => { + const wallet = useAtomValue(walletStarknetkitLatestAtom); + + const [funds, setFunds] = useState([]); + + useEffect(() => { + // TODO: Implement funds fetching using 'userAddress' + setFunds([ + { + id: 1, + type: "Project", + title: "Batman's fund", + description: "Example of card without delete button", + }, + { + id: 2, + type: "Project", + title: "Deadpool's fund", + description: "I need help with my project to develop an awesome project like Go Stark Me", + onClick: handleDeleteFund + }, + { + id: 3, + type: "Fund", + title: "Spider-Man's fund", + description: "Text to prove that we add elipsis when text exceds card width and we don't break the layout. asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf", + onClick: handleDeleteFund + } + ]); + }, []); + + const handleDeleteFund = (fundId: number) => { + // TODO: Implement fund deletion based on a unique id or receive the fund object and delete it + alert(`Deleting fund with id: ${fundId}`); + } + + return ( +
+
+
+

My Funds ✨

+
+ {wallet !== undefined ? ( + + ) : null} +
+ + {wallet === undefined ? ( +
+
+ Please connect your wallet to see your funds. +
+
+ ) : null} + + {funds.length === 0 && wallet !== null ? ( +
+
+ No funds found for address {wallet?.account?.address.slice(0, 5)}...{wallet?.account?.address.slice(-4)} +
+
+ ) : null} + + {funds.length !== 0 && wallet !== undefined ? ( +
+ {funds.map((fund: any, index: number) => ( + fund.onClick(fund.id) }} + /> + ))} +
+ ) : null} +
+ ); +}; + +export default UserFunds; diff --git a/frontend/gostarkme-web/components/modules/newfunding/DescriptionStep.tsx b/frontend/gostarkme-web/components/modules/newfunding/DescriptionStep.tsx new file mode 100644 index 0000000..0cd65d8 --- /dev/null +++ b/frontend/gostarkme-web/components/modules/newfunding/DescriptionStep.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +interface DescriptionStepProps { + fundingDescription: string; + setFundingDescription: (description: string) => void; +} + +const DescriptionStep: React.FC = ({ + fundingDescription, + setFundingDescription, +}) => ( +
+ Note: You can always edit your funding description later ;) + +