diff --git a/.gitignore b/.gitignore index 6704566..1de5659 100644 --- a/.gitignore +++ b/.gitignore @@ -1,104 +1 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port +target \ No newline at end of file diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 0000000..3a99615 --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,18 @@ +[package] +name = "contributor_SBT2_0" +version = "0.1.0" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html +[[target.starknet-contract]] +sierra = true +casm = true +casm-add-pythonic-hints = true +allowed-libfuncs-list.name = "all" + +[workspace.dependencies] +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" } + +[dependencies] +starknet = ">=2.2.0" +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.7.1" } +openzeppelin = { git = "https://github.com/openzeppelin/cairo-contracts", tag = "v0.7.0" } diff --git a/src/GuildSBT.cairo b/src/GuildSBT.cairo new file mode 100644 index 0000000..7e40ab8 --- /dev/null +++ b/src/GuildSBT.cairo @@ -0,0 +1,499 @@ +// @title Mesh Guild SBTs Cairo 2.2 +// @author Mesh Finance +// @license MIT +// @notice SBT contract to give out to contributor + +use starknet::ContractAddress; +use array::Array; + + +#[starknet::interface] +trait IMaster { + fn get_guild_points(self: @T, contributor: ContractAddress, guild: felt252) -> u32; +} + +// +// Contract Interface +// +#[starknet::interface] +trait IGuildSBT { + // view functions + fn tokenURI(self: @TContractState, token_id: u256) -> Span; + fn tokenURI_from_contributor(self: @TContractState, contributor: ContractAddress) -> Span; + fn get_master(self: @TContractState) -> ContractAddress; + fn get_next_token_id(self: @TContractState) -> u256; + fn get_contribution_tier(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_contribution_levels(self: @TContractState) -> Array; + fn get_number_of_levels(self: @TContractState) -> u32; + fn baseURI(self: @TContractState) -> Span; + fn wallet_of_owner(self: @TContractState, account: ContractAddress) -> u256; + + // external functions + fn update_baseURI(ref self: TContractState, new_baseURI: Span); + fn update_contribution_levels(ref self: TContractState, new_conribution_levels: Array); + fn update_master(ref self: TContractState, new_master: ContractAddress); + fn safe_mint(ref self: TContractState, token_type: u8); + fn migrate_sbt(ref self: TContractState, old_address: ContractAddress, new_address: ContractAddress); + +} + +#[starknet::contract] +mod GuildSBT { + + use option::OptionTrait; + // use traits::Into; + use array::{SpanSerde, ArrayTrait}; + use clone::Clone; + use array::SpanTrait; + use box::BoxTrait; + use ecdsa::check_ecdsa_signature; + use zeroable::Zeroable; + use openzeppelin::token::erc721::ERC721; + use openzeppelin::token::erc721::ERC721::InternalTrait as ERC721InternalTrait; + + use openzeppelin::introspection::interface::ISRC5; + use openzeppelin::introspection::interface::ISRC5Camel; + use openzeppelin::token::erc721::interface::{ + IERC721, IERC721CamelOnly, IERC721Metadata, IERC721MetadataCamelOnly + }; + use contributor_SBT2_0::access::ownable::{Ownable, IOwnable}; + use contributor_SBT2_0::access::ownable::Ownable::{ + ModifierTrait as OwnableModifierTrait, InternalTrait as OwnableInternalTrait, + }; + use starknet::ContractAddress; + use starknet::get_caller_address; + + // use alexandria_storage::list::{List, ListTrait}; + use contributor_SBT2_0::storage::StoreSpanFelt252; + use contributor_SBT2_0::array::StoreU32Array; + use super::{ + IMasterDispatcher, IMasterDispatcherTrait + }; + + const IERC721_ID_LEGACY: felt252 = 0x80ac58cd; + const IERC721_METADATA_ID_LEGACY: felt252 = 0x5b5e139f; + const IERC721_RECEIVER_ID_LEGACY: felt252 = 0x150b7a02; + + #[storage] + struct Storage { + _master: ContractAddress, + _contribution_levels: Array, + _baseURI: Span, + _token_type: LegacyMap::, + _next_token_id: u256, + _wallet_of_owner: LegacyMap::, + } + + // + // Constructor + // + + // @notice Contract constructor + #[constructor] + fn constructor(ref self: ContractState, name_: felt252, symbol_: felt252, baseURI_: Span, owner_: ContractAddress, master_: ContractAddress, contribution_levels_: Array) { + let mut erc721_self = ERC721::unsafe_new_contract_state(); + erc721_self.initializer(name: name_, symbol: symbol_); + + let mut ownable_self = Ownable::unsafe_new_contract_state(); + ownable_self._transfer_ownership(new_owner: owner_); + + self._baseURI.write(baseURI_); + self._master.write(master_); + self._next_token_id.write(1); + InternalImpl::_update_contribution_levels(ref self, contribution_levels_); + } + + #[external(v0)] + impl GuildSBT of super::IGuildSBT { + // + // Getters + // + fn tokenURI(self: @ContractState, token_id: u256) -> Span { + let erc721_self = ERC721::unsafe_new_contract_state(); + let owner = erc721_self.owner_of(:token_id); + let master = self._master.read(); + let masterDispatcher = IMasterDispatcher { contract_address: master }; + // @notice this is a sample SBT contract for dev guild, update the next line before deploying other guild + let points = masterDispatcher.get_guild_points(owner, 'dev'); + let token_type = self._token_type.read(owner); + + let tier = InternalImpl::_get_contribution_tier(self, points); + + InternalImpl::_get_tokenURI(self, tier, token_type) + + } + + + + fn tokenURI_from_contributor(self: @ContractState, contributor: ContractAddress) -> Span { + let master = self._master.read(); + let masterDispatcher = IMasterDispatcher { contract_address: master }; + // @notice this is a sample SBT contract for dev guild, update the next line before deploying other guild + let points = masterDispatcher.get_guild_points(contributor, 'dev'); + let token_type = self._token_type.read(contributor); + + let tier = InternalImpl::_get_contribution_tier(self, points); + + InternalImpl::_get_tokenURI(self, tier, token_type) + + } + + + fn get_master(self: @ContractState) -> ContractAddress { + self._master.read() + } + + fn get_next_token_id(self: @ContractState) -> u256 { + self._next_token_id.read() + } + + + fn get_contribution_tier(self: @ContractState, contributor: ContractAddress) -> u32 { + let master = self._master.read(); + let masterDispatcher = IMasterDispatcher { contract_address: master }; + let points = masterDispatcher.get_guild_points(contributor, 'dev'); + InternalImpl::_get_contribution_tier(self, points) + } + + + fn get_contribution_levels(self: @ContractState) -> Array { + self._contribution_levels.read() + } + + fn get_number_of_levels(self: @ContractState) -> u32 { + self._contribution_levels.read().len() + } + + fn baseURI(self: @ContractState) -> Span { + self._baseURI.read() + } + + fn wallet_of_owner(self: @ContractState, account: ContractAddress) -> u256 { + self._wallet_of_owner.read(account) + } + + + // + // Setters + // + + fn update_baseURI(ref self: ContractState, new_baseURI: Span) { + self._only_owner(); + self._baseURI.write(new_baseURI); + } + + fn update_contribution_levels(ref self: ContractState, new_conribution_levels: Array) { + self._only_owner(); + InternalImpl::_update_contribution_levels(ref self, new_conribution_levels); + + } + fn update_master(ref self: ContractState, new_master: ContractAddress) { + self._only_owner(); + self._master.write(new_master); + } + + fn safe_mint(ref self: ContractState, token_type: u8) { + let account = get_caller_address(); + let mut erc721_self = ERC721::unsafe_new_contract_state(); + + let balance = erc721_self.balance_of(:account); + assert (balance == 0, 'ALREADY_MINTED'); + + let master = self._master.read(); + let masterDispatcher = IMasterDispatcher { contract_address: master }; + let points = masterDispatcher.get_guild_points(account, 'dev'); + let tier = InternalImpl::_get_contribution_tier(@self, points); + + assert (tier != 0, 'NOT_ENOUGH_POINTS'); + self._token_type.write(account, token_type); + let token_id = self._next_token_id.read(); + erc721_self._mint(to: account, token_id: token_id.into()); + self._wallet_of_owner.write(account, token_id); + self._next_token_id.write(token_id + 1); + + } + + fn migrate_sbt(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress) { + self._only_master(); + let mut erc721_self = ERC721::unsafe_new_contract_state(); + + let old_address_balance = erc721_self.balance_of(account: old_address); + if (old_address_balance == 0) { + return (); + } + + let new_address_balance = erc721_self.balance_of(account: new_address); + assert (new_address_balance == 0, 'SBT_ALREADY_FOUND'); + + let token_id = self._wallet_of_owner.read(old_address); + let token_type = self._token_type.read(old_address); + + erc721_self._transfer(from: old_address, to: new_address, :token_id); + + self._wallet_of_owner.write(old_address, 0); + self._wallet_of_owner.write(new_address, token_id); + self._token_type.write(new_address, token_type); + + } + + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + + fn _update_contribution_levels(ref self: ContractState, new_contribution_levels: Array) { + self._contribution_levels.write(new_contribution_levels); + } + + fn _get_contribution_tier(self: @ContractState, points: u32) -> u32 { + let mut current_index = 0_u32; + let contribution_levels = self._contribution_levels.read(); + loop { + if (current_index == contribution_levels.len()) { + break; + } + + if (points < *contribution_levels[current_index]) { + break; + } + + current_index += 1; + }; + current_index + } + + fn _get_tokenURI(self: @ContractState, tier: u32, token_type: u8) -> Span { + let baseURI = self._baseURI.read(); + let new_base_uri: Array = baseURI.snapshot.clone(); + let mut tmp: Array = InternalImpl::append_number_ascii(new_base_uri, tier.into()); + tmp = InternalImpl::append_number_ascii(tmp, token_type.into()); + tmp.append('.json'); + return tmp.span(); + } + + + + fn append_number_ascii(mut uri: Array, mut number_in: u256) -> Array { + // TODO: replace with u256 divide once it's implemented on network + let mut number: u128 = number_in.try_into().unwrap(); + let mut tmpArray: Array = ArrayTrait::new(); + loop { + if (number == 0.try_into().unwrap()) { + break; + } + let digit: u128 = number % 10; + number /= 10; + tmpArray.append(digit.into() + 48); + }; + let mut i: u32 = tmpArray.len(); + if (i == 0.try_into().unwrap()) { // deal with 0 case + uri.append(48); + } + loop { + if i == 0.try_into().unwrap() { + break; + } + i -= 1; + uri.append(*tmpArray.get(i.into()).unwrap().unbox()); + }; + return uri; + } + } + + + // + // ERC721 ABI impl + // + + #[external(v0)] + impl IERC721Impl of IERC721 { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + let erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.balance_of(:account) + } + + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + let erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.owner_of(:token_id) + } + + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + let erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.get_approved(:token_id) + } + + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + let erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.is_approved_for_all(:owner, :operator) + } + + fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + let mut erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.approve(:to, :token_id); + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + assert(false, 'SBT_NOT_TRANSFERABLE'); + // let mut erc721_self = ERC721::unsafe_new_contract_state(); + + // erc721_self.transfer_from(:from, :to, :token_id); + } + + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) { + assert(false, 'SBT_NOT_TRANSFERABLE'); + + // let mut erc721_self = ERC721::unsafe_new_contract_state(); + + // erc721_self.safe_transfer_from(:from, :to, :token_id, :data); + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool + ) { + let mut erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.set_approval_for_all(:operator, :approved); + } + } + + #[external(v0)] + impl ISRC5Impl of ISRC5 { + fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + if ((interface_id == IERC721_ID_LEGACY) + | (interface_id == IERC721_METADATA_ID_LEGACY)) { + true + } else { + let mut erc721_self = ERC721::unsafe_new_contract_state(); + erc721_self.supports_interface(:interface_id) + } + } + } + + #[external(v0)] + impl ISRC5CamelImpl of ISRC5Camel { + fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { + self.supports_interface(interface_id: interfaceId) + } + } + + #[external(v0)] + impl IERC721MetadataImpl of IERC721Metadata { + fn name(self: @ContractState) -> felt252 { + let erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.name() + } + + fn symbol(self: @ContractState) -> felt252 { + let erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.symbol() + } + + fn token_uri(self: @ContractState, token_id: u256) -> felt252 { + let erc721_self = ERC721::unsafe_new_contract_state(); + + erc721_self.token_uri(:token_id) + } + } + + #[external(v0)] + impl JediERC721CamelImpl of IERC721CamelOnly { + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + IERC721::balance_of(self, account: account) + } + + fn ownerOf(self: @ContractState, tokenId: u256) -> ContractAddress { + IERC721::owner_of(self, token_id: tokenId) + } + + fn getApproved(self: @ContractState, tokenId: u256) -> ContractAddress { + IERC721::get_approved(self, token_id: tokenId) + } + + fn isApprovedForAll( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + IERC721::is_approved_for_all(self, owner: owner, operator: operator) + } + + fn transferFrom( + ref self: ContractState, from: ContractAddress, to: ContractAddress, tokenId: u256 + ) { + assert(false, 'SBT_NOT_TRANSFERABLE'); + + // IERC721::transfer_from(ref self, :from, :to, token_id: tokenId); + } + + fn safeTransferFrom( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + data: Span + ) { + assert(false, 'SBT_NOT_TRANSFERABLE'); + + // IERC721::safe_transfer_from(ref self, :from, :to, token_id: tokenId, :data); + } + + fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { + IERC721::set_approval_for_all(ref self, :operator, :approved); + } + } + + + #[generate_trait] + impl ModifierImpl of ModifierTrait { + fn _only_owner(self: @ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.assert_only_owner(); + } + + fn _only_master(self: @ContractState) { + let master = self._master.read(); + let caller = get_caller_address(); + assert(!caller.is_zero(), 'CALLER_IS_ZERO_ADDRESS'); + assert (caller == master, 'UNAUTHORISED') + } + } + + #[external(v0)] + impl IOwnableImpl of IOwnable { + fn owner(self: @ContractState) -> ContractAddress { + let ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.owner() + } + + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.transfer_ownership(:new_owner); + } + + fn renounce_ownership(ref self: ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.renounce_ownership(); + } + } + +} + diff --git a/src/Master.cairo b/src/Master.cairo new file mode 100644 index 0000000..e5afd68 --- /dev/null +++ b/src/Master.cairo @@ -0,0 +1,509 @@ +// @title Mesh Contributor SBTs Master Cairo 2.2 +// @author Mesh Finance +// @license MIT +// @notice Master to store contribution points +// TODO:: modify the structure to add new guild in future. + +use starknet::ContractAddress; +use array::Array; + + +#[derive(Drop, Serde, starknet::Store)] +struct Contribution { + // @notice Contribution for dev guild + dev: u32, + // @notice Contribution for design guild + design: u32, + // @notice Contribution for problem solving guild + problem_solving: u32, + // @notice Contribution for marcom guild + marcom: u32, + // @notice Contribution for research guild + research: u32, + // @notice timestamp for the last update + last_timestamp: u64 +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct MonthlyContribution { + // @notice Contributor Address, used in update_contribution function + contributor: ContractAddress, + // @notice Contribution for dev guild + dev: u32, + // @notice Contribution for design guild + design: u32, + // @notice Contribution for problem solving guild + problem_solving: u32, + // @notice Contribution for marcom guild + marcom: u32, + // @notice Contribution for research guild + research: u32 + +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct TotalMonthlyContribution { + // @notice Monthly contribution for dev guild + dev: u32, + // @notice Monthly contribution for design guild + design: u32, + // @notice Monthly contribution for problem solving guild + problem_solving: u32, + // @notice Monthly contribution for marcom guild + marcom: u32, + // @notice Monthly contribution for research guild + research: u32 +} + +// +// External Interfaces +// + + +#[starknet::interface] +trait IGuild { + fn migrate_sbt(ref self: T, old_address: ContractAddress, new_address: ContractAddress); +} + + +// +// Contract Interface +// +#[starknet::interface] +trait IMaster { + // view functions + fn get_contributions_points(self: @TContractState, contributor: ContractAddress) -> Contribution; + fn get_guild_points(self: @TContractState, contributor: ContractAddress, guild: felt252) -> u32; + fn get_last_update_id(self: @TContractState) -> u32; + fn get_last_update_time(self: @TContractState) -> u64; + fn get_migartion_queued_state(self: @TContractState, hash: felt252 ) -> bool; + fn get_dev_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_design_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_marcom_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_problem_solving_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_research_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_total_contribution(self: @TContractState, month_id: u32) -> TotalMonthlyContribution; + fn get_contributions_data(self: @TContractState, contributor: ContractAddress, guild: felt252) -> Array; + fn get_guild_total_contribution(self: @TContractState, month_id: u32, guild: felt252) -> u32; + fn get_guild_contribution_for_month(self: @TContractState, contributor: ContractAddress, month_id: u32, guild: felt252) -> u32; + + + // external functions + fn update_contibutions(ref self: TContractState, month_id: u32, contributions: Array::); + fn initialise(ref self: TContractState, dev_guild: ContractAddress, design_guild: ContractAddress, marcom_guild: ContractAddress, problem_solver_guild: ContractAddress, research_guild: ContractAddress); + fn migrate_points_initiated_by_DAO(ref self: TContractState, old_addresses: Array::, new_addresses: Array::); + fn migrate_points_initiated_by_holder(ref self: TContractState, new_address: ContractAddress); + fn execute_migrate_points_initiated_by_holder(ref self: TContractState, old_address: ContractAddress, new_address: ContractAddress); + + +} + + +#[starknet::contract] +mod Master { + use traits::Into; // TODO remove intos when u256 inferred type is available + use option::OptionTrait; + use array::{ArrayTrait, SpanTrait}; + use result::ResultTrait; + use zeroable::Zeroable; + use hash::LegacyHash; + use contributor_SBT2_0::access::ownable::{Ownable, IOwnable}; + use contributor_SBT2_0::access::ownable::Ownable::{ + ModifierTrait as OwnableModifierTrait, InternalTrait as OwnableInternalTrait, + }; + use starknet::{ContractAddress, ClassHash, SyscallResult, SyscallResultTrait, get_caller_address, get_contract_address, get_block_timestamp, contract_address_const}; + use integer::{u128_try_from_felt252, u256_sqrt, u256_from_felt252}; + use starknet::syscalls::{replace_class_syscall, call_contract_syscall}; + use contributor_SBT2_0::array::StoreU32Array; + + use super::{Contribution, MonthlyContribution, TotalMonthlyContribution}; + use super::{ + IGuildDispatcher, IGuildDispatcherTrait + }; + + + // + // Storage Master + // + #[storage] + struct Storage { + _contributions: LegacyMap::, // @dev contributions points for each contributor + _contributions_data: LegacyMap::<(ContractAddress, felt252), Array>, // @dev contributions data for specific contributor and guild + _total_contribution: LegacyMap::, // @dev total contribution month wise [month_id => points] + _last_update_id: u32, // @dev contribution update id + _last_update_time: u64, // @dev timestamp for last update + _dev_guild_SBT: ContractAddress, // @dev contract address for dev guild SBTs + _design_guild_SBT: ContractAddress, // @dev contract address for design guild guild SBTs + _marcom_guild_SBT: ContractAddress, // @dev contract address for marcom guild SBTs + _problem_solving_guild_SBT: ContractAddress, // @dev contract address for problem solving guild SBTs + _research_guild_SBT: ContractAddress, // @dev contract address for research guild SBTs + _initialised: bool, // @dev Flag to store initialisation state + _queued_migrations: LegacyMap::, // @dev flag to store queued migration requests. + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + ContributionUpdated: ContributionUpdated, + MigrationQueued: MigrationQueued, + Migrated: Migrated, + } + + // @notice An event emitted whenever contribution is updated + #[derive(Drop, starknet::Event)] + struct ContributionUpdated { + update_id: u32, + contributor: ContractAddress, + month_id: u32, + points_earned: MonthlyContribution + } + + // @notice An event emitted whenever migration is queued + #[derive(Drop, starknet::Event)] + struct MigrationQueued { + old_address: ContractAddress, + new_address: ContractAddress + } + + // @notice An event emitted whenever SBT is migrated + #[derive(Drop, starknet::Event)] + struct Migrated { + old_address: ContractAddress, + new_address: ContractAddress + } + + + // + // Constructor + // + + // @notice Contract constructor + #[constructor] + fn constructor(ref self: ContractState, owner_: ContractAddress,) { + // @notice not sure if default is already zero or need to initialise. + self._last_update_id.write(0_u32); + self._last_update_time.write(0_u64); + self._initialised.write(false); + + let mut ownable_self = Ownable::unsafe_new_contract_state(); + ownable_self._transfer_ownership(new_owner: owner_); + + } + + #[external(v0)] + impl Master of super::IMaster { + // + // Getters + // + fn get_contributions_points(self: @ContractState, contributor: ContractAddress) -> Contribution { + self._contributions.read(contributor) + } + + fn get_total_contribution(self: @ContractState, month_id: u32) -> TotalMonthlyContribution { + self._total_contribution.read(month_id) + } + + fn get_guild_total_contribution(self: @ContractState, month_id: u32, guild: felt252) -> u32 { + if(guild == 'dev') { + self._total_contribution.read(month_id).dev + } + else if(guild == 'design') { + self._total_contribution.read(month_id).design + } + else if(guild == 'problem_solving') { + self._total_contribution.read(month_id).problem_solving + } + else if(guild == 'marcom') { + self._total_contribution.read(month_id).marcom + } + else if(guild == 'research') { + self._total_contribution.read(month_id).research + } + else { + 0 + } + } + + fn get_contributions_data(self: @ContractState, contributor: ContractAddress, guild: felt252) -> Array { + self._contributions_data.read((contributor, guild)) + } + + fn get_guild_points(self: @ContractState, contributor: ContractAddress, guild: felt252) -> u32 { + if(guild == 'dev') { + self._contributions.read(contributor).dev + } + else if(guild == 'design') { + self._contributions.read(contributor).design + } + else if(guild == 'problem_solving') { + self._contributions.read(contributor).problem_solving + } + else if(guild == 'marcom') { + self._contributions.read(contributor).marcom + } + else if(guild == 'research') { + self._contributions.read(contributor).research + } + else { + 0 + } + } + + fn get_guild_contribution_for_month(self: @ContractState, contributor: ContractAddress, month_id: u32, guild: felt252) -> u32 { + let contribution_data = self._contributions_data.read((contributor, guild)); + let mut current_index = contribution_data.len(); + let point = loop { + if (current_index == 0) { + break 0; + } + if(month_id == *contribution_data[current_index - 2]) { + break *contribution_data[current_index - 1]; + } + + current_index -= 2; + }; + point + } + + fn get_last_update_id(self: @ContractState) -> u32 { + self._last_update_id.read() + } + + fn get_last_update_time(self: @ContractState) -> u64 { + self._last_update_time.read() + } + + fn get_migartion_queued_state(self: @ContractState, hash: felt252 ) -> bool { + self._queued_migrations.read(hash) + } + + fn get_dev_guild_SBT(self: @ContractState) -> ContractAddress { + self._dev_guild_SBT.read() + } + + fn get_design_guild_SBT(self: @ContractState) -> ContractAddress { + self._design_guild_SBT.read() + } + + fn get_marcom_guild_SBT(self: @ContractState) -> ContractAddress { + self._marcom_guild_SBT.read() + } + + fn get_problem_solving_guild_SBT(self: @ContractState) -> ContractAddress { + self._problem_solving_guild_SBT.read() + } + + fn get_research_guild_SBT(self: @ContractState) -> ContractAddress { + self._research_guild_SBT.read() + } + + + // + // Setters + // + + fn initialise(ref self: ContractState, dev_guild: ContractAddress, design_guild: ContractAddress, marcom_guild: ContractAddress, problem_solver_guild: ContractAddress, research_guild: ContractAddress) { + self._only_owner(); + let is_initialised = self._initialised.read(); + assert (is_initialised == false, 'ALREADY_INITIALISED'); + + self._dev_guild_SBT.write(dev_guild); + self._design_guild_SBT.write(design_guild); + self._marcom_guild_SBT.write(marcom_guild); + self._problem_solving_guild_SBT.write(problem_solver_guild); + self._research_guild_SBT.write(research_guild); + self._initialised.write(true); + } + + fn update_contibutions(ref self: ContractState, month_id: u32, contributions: Array::) { + self._only_owner(); + let block_timestamp = get_block_timestamp(); + let mut id = self._last_update_id.read(); + let mut current_index = 0; + + // for keeping track of cummulative guild points for that month. + let mut dev_total_cum = 0_u32; + let mut design_total_cum = 0_u32; + let mut problem_solving_total_cum = 0_u32; + let mut marcom_total_cum = 0_u32; + let mut research_total_cum = 0_u32; + + loop { + if (current_index == contributions.len()) { + break; + } + let new_contributions: MonthlyContribution = *contributions[current_index]; + let contributor: ContractAddress = new_contributions.contributor; + let old_contribution = self._contributions.read(contributor); + + let new_dev_contribution = InternalImpl::_update_guild_data(ref self, old_contribution.dev, new_contributions.dev, month_id, contributor, 'dev'); + let new_design_contribution = InternalImpl::_update_guild_data(ref self, old_contribution.design, new_contributions.design, month_id, contributor, 'design'); + let new_problem_solving_contribution = InternalImpl::_update_guild_data(ref self, old_contribution.problem_solving, new_contributions.problem_solving, month_id, contributor, 'problem_solving'); + let new_marcom_contribution = InternalImpl::_update_guild_data(ref self, old_contribution.marcom, new_contributions.marcom, month_id, contributor, 'marcom'); + let new_research_contribution = InternalImpl::_update_guild_data(ref self, old_contribution.research, new_contributions.research, month_id, contributor, 'research'); + + dev_total_cum += new_contributions.dev; + design_total_cum += new_contributions.design; + problem_solving_total_cum += new_contributions.problem_solving; + marcom_total_cum += new_contributions.marcom; + research_total_cum += new_contributions.research; + + let updated_contribution = Contribution{dev: new_dev_contribution, design: new_design_contribution, problem_solving: new_problem_solving_contribution, marcom: new_marcom_contribution, research: new_research_contribution, last_timestamp: block_timestamp}; + self._contributions.write(contributor, updated_contribution); + + current_index += 1; + + self.emit(ContributionUpdated{update_id: id, contributor: contributor, month_id: month_id, points_earned: new_contributions}); + + }; + let total_monthy_contribution = TotalMonthlyContribution{dev: dev_total_cum, design: design_total_cum, problem_solving: problem_solving_total_cum, marcom: marcom_total_cum, research: research_total_cum}; + self._total_contribution.write(month_id, total_monthy_contribution); + + id += 1; + self._last_update_id.write(id); + self._last_update_time.write(block_timestamp); + + } + + + fn migrate_points_initiated_by_DAO(ref self: ContractState, old_addresses: Array::, new_addresses: Array:: ) { + self._only_owner(); + assert(old_addresses.len() == new_addresses.len(), 'INVALID_INPUTS'); + let mut current_index = 0; + + loop { + if (current_index == old_addresses.len()) { + break; + } + InternalImpl::_migrate_points(ref self, *old_addresses[current_index], *new_addresses[current_index]); + current_index += 1; + }; + + } + + + fn migrate_points_initiated_by_holder(ref self: ContractState, new_address: ContractAddress) { + // TODO: if new address already have any contribution points, if yes return. + let caller = get_caller_address(); + let migration_hash: felt252 = LegacyHash::hash(caller.into(), new_address); + + self._queued_migrations.write(migration_hash, true); + + self.emit(MigrationQueued { old_address: caller, new_address: new_address}); + + } + + // @Notice the function has only_owner modifier to prevent user to use this function to tranfer SBT anytime. + fn execute_migrate_points_initiated_by_holder(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress) { + self._only_owner(); + let migration_hash: felt252 = LegacyHash::hash(old_address.into(), new_address); + let is_queued = self._queued_migrations.read(migration_hash); + + assert(is_queued == true, 'NOT_QUEUED'); + + InternalImpl::_migrate_points(ref self, old_address, new_address); + self._queued_migrations.write(migration_hash, false); + + } + + + + + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + // + // Internals + // + + fn _update_guild_data(ref self: ContractState, old_guild_score: u32, new_contribution_score: u32, month_id: u32, contributor: ContractAddress, guild: felt252) -> u32 { + let new_guild_score = old_guild_score + new_contribution_score; + if(new_contribution_score != 0) { + let mut contribution_data = self._contributions_data.read((contributor, guild)); + contribution_data.append(month_id); + contribution_data.append(new_contribution_score); + + self._contributions_data.write((contributor, guild), contribution_data); + } + (new_guild_score) + } + + fn _migrate_points(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress) { + + let design_guild = self._design_guild_SBT.read(); + let dev_guild = self._dev_guild_SBT.read(); + let problem_solver_guild = self._problem_solving_guild_SBT.read(); + let marcom_guild = self._marcom_guild_SBT.read(); + let research_guild = self._research_guild_SBT.read(); + + let contribution = self._contributions.read(old_address); + let zero_contribution = Contribution{dev: 0_u32, + design: 0_u32, + problem_solving: 0_u32, + marcom: 0_u32, + research: 0_u32, + last_timestamp: 0_u64 + }; + + // updating contribution data and transfering SBTs + InternalImpl::_update_contribution_data_and_migrate(ref self, old_address, new_address, 'dev', dev_guild); + InternalImpl::_update_contribution_data_and_migrate(ref self, old_address, new_address, 'design', design_guild); + InternalImpl::_update_contribution_data_and_migrate(ref self, old_address, new_address, 'problem_solving', problem_solver_guild); + InternalImpl::_update_contribution_data_and_migrate(ref self, old_address, new_address, 'marcom', marcom_guild); + InternalImpl::_update_contribution_data_and_migrate(ref self, old_address, new_address, 'research', research_guild); + + self._contributions.write(old_address, zero_contribution); + self._contributions.write(new_address, contribution); + + self.emit(Migrated{old_address: old_address, new_address: new_address}); + + } + + fn _update_contribution_data_and_migrate(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress, guild: felt252, guild_contract: ContractAddress) { + let guild_data = self._contributions_data.read((old_address, guild)); + + self._contributions_data.write((new_address, guild), guild_data); + self._contributions_data.write((old_address, guild), ArrayTrait::new()); + + let guildDispatcher = IGuildDispatcher { contract_address: guild_contract }; + guildDispatcher.migrate_sbt(old_address, new_address); + } + + } + + + #[generate_trait] + impl ModifierImpl of ModifierTrait { + fn _only_owner(self: @ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.assert_only_owner(); + } + } + + #[external(v0)] + impl IOwnableImpl of IOwnable { + fn owner(self: @ContractState) -> ContractAddress { + let ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.owner() + } + + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.transfer_ownership(:new_owner); + } + + fn renounce_ownership(ref self: ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.renounce_ownership(); + } + } + + + +} + diff --git a/src/Master_approach1.cairo b/src/Master_approach1.cairo new file mode 100644 index 0000000..c4fe1de --- /dev/null +++ b/src/Master_approach1.cairo @@ -0,0 +1,546 @@ +// @title Mesh Contributor SBTs Master Cairo 2.2 +// @author Mesh Finance +// @license MIT +// @notice Master to store contribution points + +// ************************************ +// @notice this approach is abandoned because issue with storing of array inside struct +// Can refer it in future (if possible to implement) +// ************************************ + +use starknet::ContractAddress; +use zeroable::Zeroable; +use array::{Array, ArrayTrait, SpanTrait}; +use serde::Serde; +use traits::{Into, TryInto}; +use contributor_SBT2_0::array::StoreU32Array; + + + +#[derive(Drop, Serde, starknet::Store)] +struct GuildPoints { + // @notice the cummulative score for each contributor + cum_score: u32, + // @notice Monthly points for contribution for eg [Sept_2023 -> 250] will be written as [092023, 250] + // even index as month_id, immediate right is points in that month + data: Array +} + +#[derive(Drop, Serde, starknet::Store)] +struct Contribution { + // @notice Contribution for dev guild + dev: GuildPoints, + // @notice Contribution for design guild + design: GuildPoints, + // @notice Contribution for problem solving guild + problem_solving: GuildPoints, + // @notice Contribution for marcom guild + marcom: GuildPoints, + // @notice Contribution for research guild + research: GuildPoints, + // @notice timestamp for the last update + last_timestamp: u64 +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct MonthlyContribution { + // @notice Contributor Address, used in update_contribution function + contributor: ContractAddress, + // @notice Contribution for dev guild + dev: u32, + // @notice Contribution for design guild + design: u32, + // @notice Contribution for problem solving guild + problem_solving: u32, + // @notice Contribution for marcom guild + marcom: u32, + // @notice Contribution for research guild + research: u32 + +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct TotalMonthlyContribution { + // @notice Monthly contribution for dev guild + dev: u32, + // @notice Monthly contribution for design guild + design: u32, + // @notice Monthly contribution for problem solving guild + problem_solving: u32, + // @notice Monthly contribution for marcom guild + marcom: u32, + // @notice Monthly contribution for research guild + research: u32 +} + +// +// External Interfaces +// + + +#[starknet::interface] +trait IGuild { + fn migrate_sbt(ref self: T, old_address: ContractAddress, new_address: ContractAddress); +} + + +// +// Contract Interface +// +#[starknet::interface] +trait IMaster { + // view functions + fn get_contibutions_points(self: @TContractState, contributor: ContractAddress) -> Contribution; + fn get_dev_points(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_design_points(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_problem_solving_points(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_marcom_points(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_research_points(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_last_update_id(self: @TContractState) -> u32; + fn get_last_update_time(self: @TContractState) -> u64; + fn get_migartion_queued_state(self: @TContractState, hash: felt252 ) -> bool; + fn get_dev_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_design_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_marcom_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_problem_solving_guild_SBT(self: @TContractState) -> ContractAddress; + fn get_research_guild_SBT(self: @TContractState) -> ContractAddress; + + + // external functions + fn update_contibutions(ref self: TContractState, month_id: u32, contributions: Array::); + fn initialise(ref self: TContractState, dev_guild: ContractAddress, design_guild: ContractAddress, marcom_guild: ContractAddress, problem_solver_guild: ContractAddress, research_guild: ContractAddress); + fn migrate_points_initiated_by_DAO(ref self: TContractState, old_addresses: Array::, new_addresses: Array::); + fn migrate_points_initiated_by_holder(ref self: TContractState, new_address: ContractAddress); + fn execute_migrate_points_initiated_by_holder(ref self: TContractState, old_address: ContractAddress, new_address: ContractAddress); + + +} + + +#[starknet::contract] +mod Master { + use traits::Into; // TODO remove intos when u256 inferred type is available + use option::OptionTrait; + use array::{ArrayTrait, SpanTrait}; + use result::ResultTrait; + use zeroable::Zeroable; + use hash::LegacyHash; + use contributor_SBT2_0::access::ownable::{Ownable, IOwnable}; + use contributor_SBT2_0::access::ownable::Ownable::{ + ModifierTrait as OwnableModifierTrait, InternalTrait as OwnableInternalTrait, + }; + use starknet::{ContractAddress, ClassHash, SyscallResult, SyscallResultTrait, get_caller_address, get_contract_address, get_block_timestamp, contract_address_const}; + use integer::{u128_try_from_felt252, u256_sqrt, u256_from_felt252}; + use starknet::syscalls::{replace_class_syscall, call_contract_syscall}; + // use alexandria_storage::list::{List, ListTrait}; + use contributor_SBT2_0::array::StoreU32Array; + + use super::{GuildPoints, Contribution, MonthlyContribution, TotalMonthlyContribution}; + use super::{ + IGuildDispatcher, IGuildDispatcherTrait + }; + + + // + // Storage Master + // + #[storage] + struct Storage { + _contributions: LegacyMap::, // @dev contributions + _total_contribution: LegacyMap::, // @dev total contribution month wise [month_id => points] + _last_update_id: u32, // @dev contribution update id + _last_update_time: u64, // @dev timestamp for last update + _dev_guild_SBT: ContractAddress, // @dev contract address for dev guild SBTs + _design_guild_SBT: ContractAddress, // @dev contract address for design guild guild SBTs + _marcom_guild_SBT: ContractAddress, // @dev contract address for marcom guild SBTs + _problem_solving_guild_SBT: ContractAddress, // @dev contract address for problem solving guild SBTs + _research_guild_SBT: ContractAddress, // @dev contract address for research guild SBTs + _initialised: bool, // @dev Flag to store initialisation state + _queued_migrations: LegacyMap::, // @dev flag to store queued migration requests. + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + ContributionUpdated: ContributionUpdated, + MigrationQueued: MigrationQueued, + Migrated: Migrated, + } + + // @notice An event emitted whenever contribution is updated + #[derive(Drop, starknet::Event)] + struct ContributionUpdated { + update_id: u32, + contributor: ContractAddress, + month_id: u32, + points_earned: MonthlyContribution + } + + // @notice An event emitted whenever migration is queued + #[derive(Drop, starknet::Event)] + struct MigrationQueued { + old_address: ContractAddress, + new_address: ContractAddress, + hash: felt252 + } + + // @notice An event emitted whenever SBT is migrated + #[derive(Drop, starknet::Event)] + struct Migrated { + old_address: ContractAddress, + new_address: ContractAddress + } + + + // + // Constructor + // + + // @notice Contract constructor + #[constructor] + fn constructor(ref self: ContractState, owner_: ContractAddress,) { + // @notice not sure if default is already zero or need to initialise. + self._last_update_id.write(0_u32); + self._last_update_time.write(0_u64); + self._initialised.write(false); + + let mut ownable_self = Ownable::unsafe_new_contract_state(); + ownable_self._transfer_ownership(new_owner: owner_); + + } + + #[external(v0)] + impl Master of super::IMaster { + // + // Getters + // + fn get_contibutions_points(self: @ContractState, contributor: ContractAddress) -> Contribution { + self._contributions.read(contributor) + } + + fn get_dev_points(self: @ContractState, contributor: ContractAddress) -> u32 { + let contribution = self._contributions.read(contributor); + contribution.dev.cum_score + } + + fn get_design_points(self: @ContractState, contributor: ContractAddress) -> u32 { + let contribution = self._contributions.read(contributor); + contribution.design.cum_score + } + + fn get_problem_solving_points(self: @ContractState, contributor: ContractAddress) -> u32 { + let contribution = self._contributions.read(contributor); + contribution.problem_solving.cum_score + } + + fn get_marcom_points(self: @ContractState, contributor: ContractAddress) -> u32 { + let contribution = self._contributions.read(contributor); + contribution.marcom.cum_score + } + + fn get_research_points(self: @ContractState, contributor: ContractAddress) -> u32 { + let contribution = self._contributions.read(contributor); + contribution.research.cum_score + } + + fn get_last_update_id(self: @ContractState) -> u32 { + self._last_update_id.read() + } + + fn get_last_update_time(self: @ContractState) -> u64 { + self._last_update_time.read() + } + + fn get_migartion_queued_state(self: @ContractState, hash: felt252 ) -> bool { + self._queued_migrations.read(hash) + } + + fn get_dev_guild_SBT(self: @ContractState) -> ContractAddress { + self._dev_guild_SBT.read() + } + + fn get_design_guild_SBT(self: @ContractState) -> ContractAddress { + self._design_guild_SBT.read() + } + + fn get_marcom_guild_SBT(self: @ContractState) -> ContractAddress { + self._marcom_guild_SBT.read() + } + + fn get_problem_solving_guild_SBT(self: @ContractState) -> ContractAddress { + self._problem_solving_guild_SBT.read() + } + + fn get_research_guild_SBT(self: @ContractState) -> ContractAddress { + self._research_guild_SBT.read() + } + + + // + // Setters + // + + fn initialise(ref self: ContractState, dev_guild: ContractAddress, design_guild: ContractAddress, marcom_guild: ContractAddress, problem_solver_guild: ContractAddress, research_guild: ContractAddress) { + self._only_owner(); + let is_initialised = self._initialised.read(); + assert (is_initialised == false, 'ALREADY_INITIALISED'); + + self._dev_guild_SBT.write(dev_guild); + self._design_guild_SBT.write(design_guild); + self._marcom_guild_SBT.write(marcom_guild); + self._problem_solving_guild_SBT.write(problem_solver_guild); + self._research_guild_SBT.write(research_guild); + self._initialised.write(true); + } + + fn update_contibutions(ref self: ContractState, month_id: u32, contributions: Array::) { + self._only_owner(); + let block_timestamp = get_block_timestamp(); + let mut id = self._last_update_id.read(); + let mut current_index = 0; + + // for keeping track of cummulative guild points for that month. + let mut dev_total_cum = 0_u32; + let mut design_total_cum = 0_u32; + let mut problem_solving_total_cum = 0_u32; + let mut marcom_total_cum = 0_u32; + let mut research_total_cum = 0_u32; + + loop { + if (current_index == contributions.len() - 1) { + break true; + } + let new_contributions = *contributions[current_index]; + let contributor: ContractAddress = new_contributions.contributor; + // let points = new_contributions.points; + // let mut points_index = 0; + + // loop { + // if (points_index == 4) { + // break true; + // } + // let contribution_point = *points[points_index]; + // if (contribution_point == 0) { + // continue true; + // } + // } + + let old_contribution = self._contributions.read(contributor); + + let old_dev_contribution = old_contribution.dev; + let mut contribution_data_dev = old_dev_contribution.data; + let mut cum_score_dev = old_dev_contribution.cum_score; + if (new_contributions.dev != 0) { + + contribution_data_dev.append(month_id); + contribution_data_dev.append(new_contributions.dev); + + cum_score_dev = cum_score_dev + new_contributions.dev; + + dev_total_cum = dev_total_cum + new_contributions.dev; + } + let new_dev_contribution = GuildPoints{cum_score: cum_score_dev, data: contribution_data_dev}; + + + let old_design_contribution = old_contribution.design; + let mut contribution_data_design = old_design_contribution.data; + let mut cum_score_design = old_design_contribution.cum_score; + if (new_contributions.design != 0) { + + contribution_data_design.append(month_id); + contribution_data_design.append(new_contributions.design); + + cum_score_design = cum_score_design + new_contributions.design; + + design_total_cum = design_total_cum + new_contributions.design; + } + let new_design_contribution = GuildPoints{cum_score: cum_score_design, data: contribution_data_design}; + + + let old_problem_solving_contribution = old_contribution.problem_solving; + let mut contribution_data_problem_solving = old_problem_solving_contribution.data; + let mut cum_score_problem_solving = old_problem_solving_contribution.cum_score; + if (new_contributions.problem_solving != 0) { + + contribution_data_problem_solving.append(month_id); + contribution_data_problem_solving.append(new_contributions.problem_solving); + + cum_score_problem_solving = cum_score_problem_solving + new_contributions.problem_solving; + + problem_solving_total_cum = problem_solving_total_cum + new_contributions.problem_solving; + } + let new_problem_solving_contribution = GuildPoints{cum_score: cum_score_problem_solving, data: contribution_data_problem_solving}; + + + let old_marcom_contribution = old_contribution.marcom; + let mut contribution_data_marcom = old_marcom_contribution.data; + let mut cum_score_marcom = old_marcom_contribution.cum_score; + if (new_contributions.marcom != 0) { + + contribution_data_marcom.append(month_id); + contribution_data_marcom.append(new_contributions.marcom); + + cum_score_marcom = cum_score_marcom + new_contributions.marcom; + marcom_total_cum = marcom_total_cum + new_contributions.marcom; + } + let new_marcom_contribution = GuildPoints{cum_score: cum_score_marcom, data: contribution_data_marcom}; + + + let old_research_contribution = old_contribution.research; + let mut contribution_data_research = old_research_contribution.data; + let mut cum_score_research = old_research_contribution.cum_score; + if (new_contributions.research != 0) { + + contribution_data_research.append(month_id); + contribution_data_research.append(new_contributions.research); + + cum_score_research = cum_score_research + new_contributions.research; + + research_total_cum = research_total_cum + new_contributions.research; + } + let new_research_contribution = GuildPoints{cum_score: cum_score_research, data: contribution_data_research}; + + + let updated_contribution = Contribution{dev: new_dev_contribution, design: new_design_contribution, problem_solving: new_problem_solving_contribution, marcom: new_marcom_contribution, research: new_research_contribution, last_timestamp: block_timestamp}; + self._contributions.write(contributor, updated_contribution); + + let total_monthy_contribution = TotalMonthlyContribution{dev: dev_total_cum, design: design_total_cum, problem_solving: problem_solving_total_cum, marcom: marcom_total_cum, research: research_total_cum}; + self._total_contribution.write(month_id, total_monthy_contribution); + current_index += 1; + // } + self.emit(ContributionUpdated{update_id: id, contributor: contributor, month_id: month_id, points_earned: new_contributions}); + + }; + id += 1; + self._last_update_id.write(id); + self._last_update_time.write(block_timestamp); + + } + + + fn migrate_points_initiated_by_DAO(ref self: ContractState, old_addresses: Array::, new_addresses: Array:: ) { + self._only_owner(); + assert(old_addresses.len() == new_addresses.len(), 'INVALID_INPUTS'); + let mut current_index = 0; + + loop { + if (current_index == old_addresses.len() - 1) { + break true; + } + InternalImpl::_migrate_points(ref self, *old_addresses[current_index], *new_addresses[current_index]); + current_index += 1; + }; + + } + + + fn migrate_points_initiated_by_holder(ref self: ContractState, new_address: ContractAddress) { + let caller = get_caller_address(); + let migration_hash: felt252 = LegacyHash::hash(caller.into(), new_address); + + self._queued_migrations.write(migration_hash, true); + + self.emit(MigrationQueued { old_address: caller, new_address: new_address, hash: migration_hash}); + + } + + fn execute_migrate_points_initiated_by_holder(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress) { + self._only_owner(); + let migration_hash: felt252 = LegacyHash::hash(old_address.into(), new_address); + let is_queued = self._queued_migrations.read(migration_hash); + + assert(is_queued == true, 'NOT_QUEUED'); + + self._queued_migrations.write(migration_hash, false); + InternalImpl::_migrate_points(ref self, old_address, new_address); + // self._last_update_id.write(0); + } + + + + + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + // + // Internals + // + + fn _migrate_points(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress) { + + let design_guild = self._design_guild_SBT.read(); + let dev_guild = self._dev_guild_SBT.read(); + let problem_solver_guild = self._problem_solving_guild_SBT.read(); + let marcom_guild = self._marcom_guild_SBT.read(); + let research_guild = self._research_guild_SBT.read(); + + let contribution = self._contributions.read(old_address); + /// @notice data needs to be empty list, TODO:: figure out how to create that. + let zero_contribution = Contribution{dev: GuildPoints{ cum_score: 0_u32, data: ArrayTrait::new()}, + design: GuildPoints{ cum_score: 0_u32, data: ArrayTrait::new()}, + problem_solving: GuildPoints{ cum_score: 0_u32, data: ArrayTrait::new()}, + marcom: GuildPoints{ cum_score: 0_u32, data:ArrayTrait::new()}, + research: GuildPoints{ cum_score: 0_u32, data: ArrayTrait::new()}, + last_timestamp: 0_u64 + }; + + self._contributions.write(old_address, zero_contribution); + self._contributions.write(new_address, contribution); + + let dev_guildDispatcher = IGuildDispatcher { contract_address: dev_guild }; + dev_guildDispatcher.migrate_sbt(old_address, new_address); + + let design_guildDispatcher = IGuildDispatcher { contract_address: design_guild }; + design_guildDispatcher.migrate_sbt(old_address, new_address); + + let problem_solver_guildDispatcher = IGuildDispatcher { contract_address: problem_solver_guild }; + problem_solver_guildDispatcher.migrate_sbt(old_address, new_address); + + let marcom_guildDispatcher = IGuildDispatcher { contract_address: marcom_guild }; + marcom_guildDispatcher.migrate_sbt(old_address, new_address); + + let research_guildDispatcher = IGuildDispatcher { contract_address: research_guild }; + research_guildDispatcher.migrate_sbt(old_address, new_address); + + self.emit(Migrated{old_address: old_address, new_address: new_address}); + + } + + } + + + + #[generate_trait] + impl ModifierImpl of ModifierTrait { + fn _only_owner(self: @ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.assert_only_owner(); + } + } + + #[external(v0)] + impl IOwnableImpl of IOwnable { + fn owner(self: @ContractState) -> ContractAddress { + let ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.owner() + } + + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.transfer_ownership(:new_owner); + } + + fn renounce_ownership(ref self: ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.renounce_ownership(); + } + } + + + +} diff --git a/src/access.cairo b/src/access.cairo new file mode 100644 index 0000000..06dd03e --- /dev/null +++ b/src/access.cairo @@ -0,0 +1 @@ +mod ownable; \ No newline at end of file diff --git a/src/access/ownable.cairo b/src/access/ownable.cairo new file mode 100644 index 0000000..065e8c4 --- /dev/null +++ b/src/access/ownable.cairo @@ -0,0 +1,104 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IOwnable { + fn owner(self: @TContractState) -> ContractAddress; + + fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress); + + fn renounce_ownership(ref self: TContractState); +} + +#[starknet::contract] +mod Ownable { + use zeroable::Zeroable; + + // // locals + use contributor_SBT2_0::access::ownable; + use starknet::ContractAddress; + use starknet::get_caller_address; + + // + // Storage + // + + #[storage] + struct Storage { + _owner: ContractAddress + } + + // + // Events + // + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + OwnershipTransferred: OwnershipTransferred, + } + + #[derive(Drop, starknet::Event)] + struct OwnershipTransferred { + previous_owner: ContractAddress, + new_owner: ContractAddress, + } + + // + // Modifiers + // + + #[generate_trait] + impl ModifierImpl of ModifierTrait { + fn assert_only_owner(self: @ContractState) { + let owner = self._owner.read(); + let caller = get_caller_address(); + assert(!caller.is_zero(), 'Caller is the zero address'); + assert(caller == owner, 'Caller is not the owner'); + } + } + + // + // Ownable impl + // + + #[external(v0)] + impl IOwnableImpl of ownable::IOwnable { + fn owner(self: @ContractState) -> ContractAddress { + self._owner.read() + } + + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { + assert(!new_owner.is_zero(), 'New owner is the zero address'); + self.assert_only_owner(); + self._transfer_ownership(new_owner); + } + + fn renounce_ownership(ref self: ContractState) { + self.assert_only_owner(); + self._transfer_ownership(Zeroable::zero()); + } + } + + // + // Internals + // + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn initializer(ref self: ContractState) { + let caller = get_caller_address(); + self._transfer_ownership(caller); + } + + fn _transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { + let previous_owner = self._owner.read(); + self._owner.write(new_owner); + + // Events + self + .emit( + Event::OwnershipTransferred(OwnershipTransferred { previous_owner, new_owner }) + ); + } + } +} \ No newline at end of file diff --git a/src/array.cairo b/src/array.cairo new file mode 100644 index 0000000..3753f17 --- /dev/null +++ b/src/array.cairo @@ -0,0 +1,135 @@ +use array::{ArrayTrait, SpanTrait}; +use traits::{Into, TryInto}; +use option::OptionTrait; +use starknet::{ + Store, storage_address_from_base_and_offset, storage_read_syscall, storage_write_syscall, + SyscallResult, StorageBaseAddress, +}; + + +impl StoreU32Array of Store> { + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + StoreU32Array::read_at_offset(address_domain, base, 0) + } + + fn write( + address_domain: u32, base: StorageBaseAddress, value: Array + ) -> SyscallResult<()> { + StoreU32Array::write_at_offset(address_domain, base, 0, value) + } + + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8 + ) -> SyscallResult> { + let mut arr: Array = ArrayTrait::new(); + + // Read the stored array's length. If the length is superior to 255, the read will fail. + let len: u8 = Store::::read_at_offset(address_domain, base, offset) + .expect('Storage Span too large'); + offset += 1; + + // Sequentially read all stored elements and append them to the array. + let exit = len + offset; + loop { + if offset >= exit { + break; + } + + let value = Store::::read_at_offset(address_domain, base, offset).unwrap(); + arr.append(value); + offset += Store::::size(); + }; + + // Return the array. + Result::Ok(arr) + } + + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array + ) -> SyscallResult<()> { + // // Store the length of the array in the first storage slot. + let len: u8 = value.len().try_into().expect('Storage - Span too large'); + Store::::write_at_offset(address_domain, base, offset, len); + offset += 1; + + // Store the array elements sequentially + loop { + match value.pop_front() { + Option::Some(element) => { + Store::::write_at_offset(address_domain, base, offset, element); + offset += Store::::size(); + }, + Option::None(_) => { + break Result::Ok(()); + } + }; + } + } + + fn size() -> u8 { + 255 * Store::::size() + } +} + + +impl StoreFelt252Array of Store> { + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + StoreFelt252Array::read_at_offset(address_domain, base, 0) + } + + fn write( + address_domain: u32, base: StorageBaseAddress, value: Array + ) -> SyscallResult<()> { + StoreFelt252Array::write_at_offset(address_domain, base, 0, value) + } + + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8 + ) -> SyscallResult> { + let mut arr: Array = ArrayTrait::new(); + + // Read the stored array's length. If the length is superior to 255, the read will fail. + let len: u8 = Store::::read_at_offset(address_domain, base, offset) + .expect('Storage Span too large'); + offset += 1; + + // Sequentially read all stored elements and append them to the array. + let exit = len + offset; + loop { + if offset >= exit { + break; + } + + let value = Store::::read_at_offset(address_domain, base, offset).unwrap(); + arr.append(value); + offset += Store::::size(); + }; + + // Return the array. + Result::Ok(arr) + } + + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array + ) -> SyscallResult<()> { + // // Store the length of the array in the first storage slot. + let len: u8 = value.len().try_into().expect('Storage - Span too large'); + Store::::write_at_offset(address_domain, base, offset, len); + offset += 1; + + // Store the array elements sequentially + loop { + match value.pop_front() { + Option::Some(element) => { + Store::::write_at_offset(address_domain, base, offset, element); + offset += Store::::size(); + }, + Option::None(_) => { break Result::Ok(()); } + }; + } + } + + fn size() -> u8 { + 255 * Store::::size() + } +} \ No newline at end of file diff --git a/src/attribution.cairo b/src/attribution.cairo new file mode 100644 index 0000000..0f4da7a --- /dev/null +++ b/src/attribution.cairo @@ -0,0 +1,421 @@ +// @title Mesh Contributor SBTs Attribution Cairo 2.2 +// @author Mesh Finance +// @license MIT +// @notice Attribution to store contribution points + +use starknet::ContractAddress; +use array::Array; + + +#[derive(Drop, Serde, starknet::Store)] +struct Contribution { + // @notice cummulative Contribution points for each guild + cum_point: u32, + // @notice timestamp for the last update + last_timestamp: u64 +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +struct MonthlyContribution { + // @notice Contributor Address, used in update_contribution function + contributor: ContractAddress, + // @notice Contribution for guilds + point: u32, +} + + +// +// External Interfaces +// + +#[starknet::interface] +trait IGuild { + fn migrate_sbt(ref self: T, old_address: ContractAddress, new_address: ContractAddress); +} + + +// +// Contract Interface +// +#[starknet::interface] +trait IAttribution { + // view functions + fn get_contributions_points(self: @TContractState, contributor: ContractAddress) -> Array; + fn get_guild_points(self: @TContractState, contributor: ContractAddress, guild: felt252) -> u32; + fn get_last_update_id(self: @TContractState) -> u32; + fn get_last_update_time(self: @TContractState) -> u64; + fn get_migartion_queued_state(self: @TContractState, hash: felt252 ) -> bool; + fn get_guild_SBT(self: @TContractState, guild: felt252) -> ContractAddress; + fn get_contributions_data(self: @TContractState, contributor: ContractAddress, guild: felt252) -> Array; + fn get_guild_total_contribution(self: @TContractState, month_id: u32, guild: felt252) -> u32; + fn get_guild_contribution_for_month(self: @TContractState, contributor: ContractAddress, month_id: u32, guild: felt252) -> u32; + + + // external functions + fn update_contibutions(ref self: TContractState, month_id: u32, guild: felt252, contributions: Array::); + fn initialise(ref self: TContractState, guilds_name: Array::, guilds_address: Array::); + fn add_guild(ref self: TContractState, guild_name: felt252, guild_address: ContractAddress); + fn migrate_points_initiated_by_DAO(ref self: TContractState, old_addresses: Array::, new_addresses: Array::); + fn migrate_points_initiated_by_holder(ref self: TContractState, new_address: ContractAddress); + fn execute_migrate_points_initiated_by_holder(ref self: TContractState, old_address: ContractAddress, new_address: ContractAddress); + +} + + +#[starknet::contract] +mod Attribution { + use traits::Into; // TODO remove intos when u256 inferred type is available + use option::OptionTrait; + use array::{ArrayTrait, SpanTrait}; + use result::ResultTrait; + use zeroable::Zeroable; + use hash::LegacyHash; + use contributor_SBT2_0::access::ownable::{Ownable, IOwnable}; + use contributor_SBT2_0::access::ownable::Ownable::{ + ModifierTrait as OwnableModifierTrait, InternalTrait as OwnableInternalTrait, + }; + use starknet::{ContractAddress, ClassHash, SyscallResult, SyscallResultTrait, get_caller_address, get_contract_address, get_block_timestamp, contract_address_const}; + use integer::{u128_try_from_felt252, u256_sqrt, u256_from_felt252}; + use starknet::syscalls::{replace_class_syscall, call_contract_syscall}; + use contributor_SBT2_0::array::StoreFelt252Array; + use contributor_SBT2_0::array::StoreU32Array; + + use super::{Contribution, MonthlyContribution}; + use super::{ + IGuildDispatcher, IGuildDispatcherTrait + }; + + + // + // Storage Attribution + // + #[storage] + struct Storage { + _contributions: LegacyMap::<(ContractAddress, felt252), Contribution>, // @dev contributions points for each contributor for each guild + _contributions_data: LegacyMap::<(ContractAddress, felt252), Array>, // @dev contributions data for specific contributor and guild + _total_contribution: LegacyMap::<(u32, felt252), u32>, // @dev total contribution month wise [(month_id, guild) => points] + _last_update_id: u32, // @dev contribution update id + _last_update_time: u64, // @dev timestamp for last update + _guilds: Array, // @dev array to store all the guilds + _guild_SBT: LegacyMap::, // @dev contract address for guild SBTs + _initialised: bool, // @dev Flag to store initialisation state + _queued_migrations: LegacyMap::, // @dev flag to store queued migration requests. + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + ContributionUpdated: ContributionUpdated, + MigrationQueued: MigrationQueued, + Migrated: Migrated, + } + + // @notice An event emitted whenever contribution is updated + #[derive(Drop, starknet::Event)] + struct ContributionUpdated { + update_id: u32, + contributor: ContractAddress, + month_id: u32, + guild: felt252, + points_earned: u32 + } + + // @notice An event emitted whenever migration is queued + #[derive(Drop, starknet::Event)] + struct MigrationQueued { + old_address: ContractAddress, + new_address: ContractAddress + } + + // @notice An event emitted whenever SBT is migrated + #[derive(Drop, starknet::Event)] + struct Migrated { + old_address: ContractAddress, + new_address: ContractAddress + } + + + // + // Constructor + // + + // @notice Contract constructor + #[constructor] + fn constructor(ref self: ContractState, owner_: ContractAddress,) { + // @notice not sure if default is already zero or need to initialise. + self._last_update_id.write(0_u32); + self._last_update_time.write(0_u64); + self._initialised.write(false); + + let mut ownable_self = Ownable::unsafe_new_contract_state(); + ownable_self._transfer_ownership(new_owner: owner_); + + } + + #[external(v0)] + impl Attribution of super::IAttribution { + // + // Getters + // + fn get_contributions_points(self: @ContractState, contributor: ContractAddress) -> Array { + let guilds = self._guilds.read(); + let mut contributions = ArrayTrait::::new(); + let mut current_index = 0; + loop { + if (current_index == guilds.len()) { + break; + } + let contribution = self._contributions.read((contributor, *guilds[current_index])); + contributions.append(contribution); + current_index += 1; + }; + contributions + } + + fn get_guild_total_contribution(self: @ContractState, month_id: u32, guild: felt252) -> u32 { + self._total_contribution.read((month_id, guild)) + } + + fn get_contributions_data(self: @ContractState, contributor: ContractAddress, guild: felt252) -> Array { + self._contributions_data.read((contributor, guild)) + } + + fn get_guild_points(self: @ContractState, contributor: ContractAddress, guild: felt252) -> u32 { + self._contributions.read((contributor, guild)).cum_point + } + + fn get_guild_contribution_for_month(self: @ContractState, contributor: ContractAddress, month_id: u32, guild: felt252) -> u32 { + let contribution_data = self._contributions_data.read((contributor, guild)); + let mut current_index = contribution_data.len(); + let point = loop { + if (current_index == 0) { + break 0; + } + if(month_id == *contribution_data[current_index - 2]) { + break *contribution_data[current_index - 1]; + } + + current_index -= 2; + }; + point + } + + fn get_last_update_id(self: @ContractState) -> u32 { + self._last_update_id.read() + } + + fn get_last_update_time(self: @ContractState) -> u64 { + self._last_update_time.read() + } + + fn get_migartion_queued_state(self: @ContractState, hash: felt252 ) -> bool { + self._queued_migrations.read(hash) + } + + fn get_guild_SBT(self: @ContractState, guild: felt252) -> ContractAddress { + self._guild_SBT.read(guild) + } + + + // + // Setters + // + + fn initialise(ref self: ContractState, guilds_name: Array::, guilds_address: Array::) { + self._only_owner(); + let is_initialised = self._initialised.read(); + assert (is_initialised == false, 'ALREADY_INITIALISED'); + self._guilds.write(guilds_name.clone()); + + let mut current_index = 0; + loop { + if (current_index == guilds_name.len()) { + break; + } + self._guild_SBT.write(*guilds_name[current_index], *guilds_address[current_index]); + current_index += 1; + }; + self._initialised.write(true); + } + + fn update_contibutions(ref self: ContractState, month_id: u32, guild: felt252, contributions: Array::) { + self._only_owner(); + let block_timestamp = get_block_timestamp(); + let mut id = self._last_update_id.read(); + let mut current_index = 0; + + // for keeping track of cummulative guild points for that month. + let mut total_cum = 0_u32; + + loop { + if (current_index == contributions.len()) { + break; + } + let new_contributions: MonthlyContribution = *contributions[current_index]; + let contributor: ContractAddress = new_contributions.contributor; + let old_contribution = self._contributions.read((contributor, guild)); + + let new_cum_point = InternalImpl::_update_guild_data(ref self, old_contribution.cum_point, new_contributions.point, month_id, contributor, guild); + + total_cum += new_contributions.point; + + let updated_contribution = Contribution{cum_point: new_cum_point, last_timestamp: block_timestamp}; + self._contributions.write((contributor, guild), updated_contribution); + + current_index += 1; + + self.emit(ContributionUpdated{update_id: id, contributor: contributor, month_id: month_id, guild: guild, points_earned: new_contributions.point}); + + }; + self._total_contribution.write((month_id, guild), total_cum); + + id += 1; + self._last_update_id.write(id); + self._last_update_time.write(block_timestamp); + + } + + fn add_guild(ref self: ContractState, guild_name: felt252, guild_address: ContractAddress) { + self._only_owner(); + let mut guilds = self._guilds.read(); + guilds.append(guild_name); + self._guilds.write(guilds); + self._guild_SBT.write(guild_name, guild_address); + } + + + fn migrate_points_initiated_by_DAO(ref self: ContractState, old_addresses: Array::, new_addresses: Array:: ) { + self._only_owner(); + assert(old_addresses.len() == new_addresses.len(), 'INVALID_INPUTS'); + let mut current_index = 0; + + loop { + if (current_index == old_addresses.len()) { + break; + } + InternalImpl::_migrate_points(ref self, *old_addresses[current_index], *new_addresses[current_index]); + current_index += 1; + }; + + } + + + fn migrate_points_initiated_by_holder(ref self: ContractState, new_address: ContractAddress) { + // TODO: if new address already have any contribution points, if yes return. + let caller = get_caller_address(); + let migration_hash: felt252 = LegacyHash::hash(caller.into(), new_address); + + self._queued_migrations.write(migration_hash, true); + + self.emit(MigrationQueued { old_address: caller, new_address: new_address}); + + } + + // @Notice the function has only_owner modifier to prevent user to use this function to tranfer SBT anytime. + fn execute_migrate_points_initiated_by_holder(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress) { + self._only_owner(); + let migration_hash: felt252 = LegacyHash::hash(old_address.into(), new_address); + let is_queued = self._queued_migrations.read(migration_hash); + + assert(is_queued == true, 'NOT_QUEUED'); + + InternalImpl::_migrate_points(ref self, old_address, new_address); + self._queued_migrations.write(migration_hash, false); + + } + + + + + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + // + // Internals + // + + fn _update_guild_data(ref self: ContractState, old_guild_score: u32, new_contribution_score: u32, month_id: u32, contributor: ContractAddress, guild: felt252) -> u32 { + let new_guild_score = old_guild_score + new_contribution_score; + if(new_contribution_score != 0) { + let mut contribution_data = self._contributions_data.read((contributor, guild)); + contribution_data.append(month_id); + contribution_data.append(new_contribution_score); + + self._contributions_data.write((contributor, guild), contribution_data); + } + (new_guild_score) + } + + fn _migrate_points(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress) { + + let guilds = self._guilds.read(); + let mut contributions = ArrayTrait::::new(); + + let mut current_index = 0; + loop { + if (current_index == guilds.len()) { + break; + } + let guild_address = self._guild_SBT.read(*guilds[current_index]); + let contribution = self._contributions.read((old_address, *guilds[current_index])); + // updating contribution data and transfering SBTs + InternalImpl::_update_contribution_data_and_migrate(ref self, old_address, new_address, *guilds[current_index], guild_address); + let zero_contribution = Contribution{cum_point: 0_u32, + last_timestamp: 0_u64 + }; + self._contributions.write((old_address, *guilds[current_index]), zero_contribution); + self._contributions.write((new_address, *guilds[current_index]), contribution); + }; + + self.emit(Migrated{old_address: old_address, new_address: new_address}); + + } + + fn _update_contribution_data_and_migrate(ref self: ContractState, old_address: ContractAddress, new_address: ContractAddress, guild: felt252, guild_contract: ContractAddress) { + let guild_data = self._contributions_data.read((old_address, guild)); + + self._contributions_data.write((new_address, guild), guild_data); + self._contributions_data.write((old_address, guild), ArrayTrait::new()); + + let guildDispatcher = IGuildDispatcher { contract_address: guild_contract }; + guildDispatcher.migrate_sbt(old_address, new_address); + } + + } + + + #[generate_trait] + impl ModifierImpl of ModifierTrait { + fn _only_owner(self: @ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.assert_only_owner(); + } + } + + #[external(v0)] + impl IOwnableImpl of IOwnable { + fn owner(self: @ContractState) -> ContractAddress { + let ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.owner() + } + + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.transfer_ownership(:new_owner); + } + + fn renounce_ownership(ref self: ContractState) { + let mut ownable_self = Ownable::unsafe_new_contract_state(); + + ownable_self.renounce_ownership(); + } + } + + + +} + diff --git a/src/lib.cairo b/src/lib.cairo new file mode 100644 index 0000000..4941518 --- /dev/null +++ b/src/lib.cairo @@ -0,0 +1,6 @@ +mod master; +mod attribution; +mod access; +mod storage; +mod guildSBT; +mod array; diff --git a/src/storage.cairo b/src/storage.cairo new file mode 100644 index 0000000..8729526 --- /dev/null +++ b/src/storage.cairo @@ -0,0 +1,88 @@ +use array::{ArrayTrait, SpanTrait}; +use traits::{Into, TryInto}; +use option::OptionTrait; +use starknet::{ + Store, storage_address_from_base_and_offset, storage_read_syscall, storage_write_syscall, + SyscallResult, StorageBaseAddress, +}; + +impl StoreSpanFelt252 of Store> { + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + StoreSpanFelt252::read_at_offset(address_domain, base, 0) + } + + fn write( + address_domain: u32, base: StorageBaseAddress, mut value: Span + ) -> SyscallResult<()> { + StoreSpanFelt252::write_at_offset(address_domain, base, 0, value) + } + + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8 + ) -> SyscallResult> { + let mut arr = array![]; + + // Read span len + let len: u8 = storage_read_syscall( + :address_domain, address: storage_address_from_base_and_offset(base, offset) + )? + .try_into() + .expect('Storage - Span too large'); + + // Load span content + let mut i: u8 = offset + 1; + loop { + if (i > len) { + break (); + } + + match storage_read_syscall( + :address_domain, address: storage_address_from_base_and_offset(base, i) + ) { + Result::Ok(element) => { + arr.append(element) + }, + Result::Err(_) => panic_with_felt252('Storage - Unknown error'), + } + + i += 1; + }; + + Result::Ok(arr.span()) + } + + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8, mut value: Span + ) -> SyscallResult<()> { + // Assert span can fit in storage obj + // 1 slots for the len; 255 slots for the span content, minus the offset + let len: u8 = Into::::into(value.len() + offset.into()) + .try_into() + .expect('Storage - Span too large'); + + // Write span content + let mut i: u8 = offset + 1; + loop { + match value.pop_front() { + Option::Some(element) => { + storage_write_syscall( + :address_domain, + address: storage_address_from_base_and_offset(base, i), + value: *element + ); + i += 1; + }, + Option::None(_) => { + break (); + }, + }; + }; + + // Store span len + Store::::write(:address_domain, :base, value: len.into()) + } + + fn size() -> u8 { + 255 + } +} \ No newline at end of file diff --git a/tests/lib.cairo b/tests/lib.cairo new file mode 100644 index 0000000..fbde2ec --- /dev/null +++ b/tests/lib.cairo @@ -0,0 +1,6 @@ +mod test_deployment; +mod test_migrate_points; +mod test_mint; +mod test_attribution; +mod test_update_contribution_points; +mod utils; \ No newline at end of file diff --git a/tests/test_attribution.cairo b/tests/test_attribution.cairo new file mode 100644 index 0000000..fe5f569 --- /dev/null +++ b/tests/test_attribution.cairo @@ -0,0 +1,194 @@ +use array::{Array, ArrayTrait, SpanTrait}; +use result::ResultTrait; +use starknet::ContractAddress; +use starknet::ClassHash; +use traits::TryInto; +use option::OptionTrait; +use snforge_std::{ declare, ContractClassTrait, ContractClass, start_warp, start_prank, stop_prank, + spy_events, SpyOn, EventSpy, EventFetcher, Event, EventAssertions }; +use tests::utils::{ deployer_addr, user1, user2, user3, user4, URI}; +use contributor_SBT2_0::attribution::Contribution; +use contributor_SBT2_0::attribution::MonthlyContribution; + +#[starknet::interface] +trait IAttribution { + fn get_last_update_id(self: @TContractState) -> u32; + fn get_contributions_points(self: @TContractState, contributor: ContractAddress) -> Array; + fn get_contributions_data(self: @TContractState, contributor: ContractAddress, guild: felt252) -> Array; + fn get_guild_total_contribution(self: @TContractState, month_id: u32, guild: felt252) -> u32; + + fn update_contibutions(ref self: TContractState, month_id: u32, guild: felt252, contributions: Array::); + fn initialise(ref self: TContractState, guilds_name: Array::, guilds_address: Array::); + + +/////// + +} + +#[starknet::interface] +trait IGuildSBT { + fn get_contribution_tier(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_master(self: @TContractState) -> ContractAddress; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn wallet_of_owner(self: @TContractState, account: ContractAddress) -> u256; + fn tokenURI(self: @TContractState, token_id: u256) -> Span; + fn get_next_token_id(self: @TContractState) -> u256; + + fn safe_mint(ref self: TContractState, token_type: u8); +} + + +fn deploy_contracts() -> (ContractAddress, ContractAddress) { + let mut attribution_constructor_calldata = Default::default(); + Serde::serialize(@deployer_addr(), ref attribution_constructor_calldata); + let attribution_class = declare('Attribution'); + let attribution_address = attribution_class.deploy(@attribution_constructor_calldata).unwrap(); + + let name = 'Jedi Dev Guild SBT'; + let symbol = 'JEDI-DEV'; + let mut contribution_levels: Array = ArrayTrait::new(); + contribution_levels.append(100); + contribution_levels.append(200); + contribution_levels.append(500); + contribution_levels.append(1000); + + let mut guildSBT_constructor_calldata = Default::default(); + Serde::serialize(@name, ref guildSBT_constructor_calldata); + Serde::serialize(@symbol, ref guildSBT_constructor_calldata); + Serde::serialize(@URI(), ref guildSBT_constructor_calldata); + Serde::serialize(@deployer_addr(), ref guildSBT_constructor_calldata); + Serde::serialize(@attribution_address, ref guildSBT_constructor_calldata); + Serde::serialize(@contribution_levels, ref guildSBT_constructor_calldata); + + let guildSBT_class = declare('GuildSBT'); + let guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + + let attribution_dispatcher = IAttributionDispatcher { contract_address: attribution_address }; + + let mut guilds_name: Array = ArrayTrait::new(); + guilds_name.append('dev'); + guilds_name.append('design'); + + let mut guilds_address: Array = ArrayTrait::new(); + guilds_address.append(guildSBT_address); + guilds_address.append(guildSBT_address); + + start_prank(attribution_address, deployer_addr()); + attribution_dispatcher.initialise(guilds_name, guilds_address); + stop_prank(attribution_address); + (attribution_address, guildSBT_address) +} + +#[test] +fn test_update_contribution_points() { + let (attribution_address, guildSBT_address) = deploy_contracts(); + let attribution_dispatcher = IAttributionDispatcher { contract_address: attribution_address }; + let guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: guildSBT_address }; + + let id1 = attribution_dispatcher.get_last_update_id(); + assert (id1 == 0, 'Invalid id initialisation'); + + let month_id: u32 = 092023; + let dev_guild = 'dev'; + let design_guild = 'design'; + + let user1_contribution_dev = MonthlyContribution{ contributor: user1(), point: 120}; + let user2_contribution_dev = MonthlyContribution{ contributor: user2(), point: 200}; + let user3_contribution_dev = MonthlyContribution{ contributor: user3(), point: 30}; + let mut contributions_dev: Array = ArrayTrait::new(); + contributions_dev.append(user1_contribution_dev); + contributions_dev.append(user2_contribution_dev); + contributions_dev.append(user3_contribution_dev); + + let user1_contribution_design = MonthlyContribution{ contributor: user1(), point: 10}; + let user2_contribution_design = MonthlyContribution{ contributor: user2(), point: 105}; + let user3_contribution_design = MonthlyContribution{ contributor: user3(), point: 250}; + let mut contributions_design: Array = ArrayTrait::new(); + contributions_design.append(user1_contribution_design); + contributions_design.append(user2_contribution_design); + contributions_design.append(user3_contribution_design); + + + // let mut spy = spy_events(SpyOn::One(attribution_address)); + + start_prank(attribution_address, deployer_addr()); + attribution_dispatcher.update_contibutions(month_id, dev_guild, contributions_dev); + attribution_dispatcher.update_contibutions(month_id, design_guild, contributions_design); + stop_prank(attribution_address); + + // let mut user1_event_data = Default::default(); + // Serde::serialize(@id1, ref user1_event_data); + // Serde::serialize(@user1(), ref user1_event_data); + // Serde::serialize(@month_id, ref user1_event_data); + // Serde::serialize(@dev_guild, ref user1_event_data); + // Serde::serialize(@user1_contribution, ref user1_event_data); + // spy.assert_emitted(@array![ + // Event { from: attribution_address, name: 'ContributionUpdated', keys: array![], data: user1_event_data } + // ]); + + // let mut user2_event_data = Default::default(); + // Serde::serialize(@id1, ref user2_event_data); + // Serde::serialize(@user2(), ref user2_event_data); + // Serde::serialize(@month_id, ref user2_event_data); + // Serde::serialize(@dev_guild, ref user1_event_data); + // Serde::serialize(@user2_contribution, ref user2_event_data); + // spy.assert_emitted(@array![ + // Event { from: attribution_address, name: 'ContributionUpdated', keys: array![], data: user2_event_data } + // ]); + + // let mut user3_event_data = Default::default(); + // Serde::serialize(@id1, ref user3_event_data); + // Serde::serialize(@user3(), ref user3_event_data); + // Serde::serialize(@month_id, ref user3_event_data); + // Serde::serialize(@dev_guild, ref user1_event_data); + // Serde::serialize(@user3_contribution, ref user3_event_data); + // spy.assert_emitted(@array![ + // Event { from: attribution_address, name: 'ContributionUpdated', keys: array![], data: user3_event_data } + // ]); + + let id2 = attribution_dispatcher.get_last_update_id(); + assert (id2 == 2, 'invalid id'); + + // verifying points for user 1 is updated + let user1_points = attribution_dispatcher.get_contributions_points(user1()); + assert(user1_points.len() == 2, 'invalid point length'); + assert(*user1_points.at(0).cum_point == 120, 'invalid dev point'); + assert(*user1_points.at(1).cum_point == 10, 'invalid design point'); + + + // verifying points for user 2 is updated + let user2_points = attribution_dispatcher.get_contributions_points(user2()); + assert(*user2_points.at(0).cum_point == 200, 'invalid dev point'); + assert(*user2_points.at(1).cum_point == 105, 'invalid design point'); + + // verifying contribution data(Montly) is updated + let mut dev_data = attribution_dispatcher.get_contributions_data(user1(), 'dev'); + assert(dev_data.len() == 2, 'invalid length'); + assert(*dev_data[0] == month_id, 'invalid month id'); + assert(*dev_data[1] == 120, 'invalid month points'); + + let mut design_data = attribution_dispatcher.get_contributions_data(user1(), 'design'); + assert(design_data.len() == 2, 'invalid length'); + assert(*design_data[0] == month_id, 'invalid month id'); + assert(*design_data[1] == 10, 'invalid month points'); + + + // verifying tier for each levels + let tier_user1 = guildSBT_dispatcher.get_contribution_tier(user1()); + assert (tier_user1 == 1, 'invalid tier_user1'); + + let tier_user2 = guildSBT_dispatcher.get_contribution_tier(user2()); + assert (tier_user2 == 2, 'invalid tier_user2'); + + + // verifying total monthly contribution + let total_contribution_point_dev = attribution_dispatcher.get_guild_total_contribution(month_id, dev_guild); + assert(total_contribution_point_dev == 350, 'incorrect total dev'); + + // verifying total monthly contribution + let total_contribution_point_design = attribution_dispatcher.get_guild_total_contribution(month_id, design_guild); + assert(total_contribution_point_design == 365, 'incorrect total design'); + + + +} \ No newline at end of file diff --git a/tests/test_deployment.cairo b/tests/test_deployment.cairo new file mode 100644 index 0000000..ec44dfa --- /dev/null +++ b/tests/test_deployment.cairo @@ -0,0 +1,90 @@ +use array::{Array, ArrayTrait, SpanTrait}; +use result::ResultTrait; +use starknet::ContractAddress; +use starknet::ClassHash; +use traits::TryInto; +use option::OptionTrait; +use snforge_std::{ declare, ContractClassTrait }; +use tests::utils::{ deployer_addr, user1, URI}; + + +#[starknet::interface] +trait IMaster { + fn owner(self: @T) -> ContractAddress; +} + +#[starknet::interface] +trait IGuildSBT { + fn name(self: @T) -> felt252; + fn symbol(self: @T) -> felt252; + fn get_master(self: @T) -> ContractAddress; + fn owner(self: @T) -> ContractAddress; + fn baseURI(self: @T) -> Span; + fn get_contribution_levels(self: @T) -> Array; + fn get_number_of_levels(self: @T) -> u32; + +} + + +#[test] +fn test_deployment_master_guildSBT() { + let mut master_constructor_calldata = Default::default(); + Serde::serialize(@deployer_addr(), ref master_constructor_calldata); + let master_class = declare('Master'); + let master_address = master_class.deploy(@master_constructor_calldata).unwrap(); + + let name = 'Jedi Dev Guild SBT'; + let symbol = 'JEDI-DEV'; + let mut contribution_levels = ArrayTrait::new(); + contribution_levels.append(100); + contribution_levels.append(200); + contribution_levels.append(500); + contribution_levels.append(1000); + + let mut guildSBT_constructor_calldata = Default::default(); + Serde::serialize(@name, ref guildSBT_constructor_calldata); + Serde::serialize(@symbol, ref guildSBT_constructor_calldata); + Serde::serialize(@URI(), ref guildSBT_constructor_calldata); + Serde::serialize(@deployer_addr(), ref guildSBT_constructor_calldata); + Serde::serialize(@master_address, ref guildSBT_constructor_calldata); + Serde::serialize(@contribution_levels, ref guildSBT_constructor_calldata); + + let guildSBT_class = declare('GuildSBT'); + let guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + + // Create a Dispatcher object that will allow interacting with the deployed contract + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + + let result = master_dispatcher.owner(); + assert(result == deployer_addr(), 'Invalid Owner'); + + let guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: guildSBT_address }; + + let result = guildSBT_dispatcher.get_master(); + assert(result == master_address, 'Invalid Master'); + + let name: felt252 = guildSBT_dispatcher.name(); + assert(name == 'Jedi Dev Guild SBT', 'Invalid name'); + + let symbol: felt252 = guildSBT_dispatcher.symbol(); + assert(symbol == 'JEDI-DEV', 'Invalid symbol'); + + let baseURI: Span = guildSBT_dispatcher.baseURI(); + assert(*baseURI[0] == 'api.jediswap/', 'Invlalid item 0'); + assert(*baseURI[1] == 'guildSBT/', 'Invlalid item 1'); + assert(*baseURI[2] == 'dev/', 'Invlalid item 2'); + assert(baseURI.len() == 3, 'should be 3'); + + let owner = guildSBT_dispatcher.owner(); + assert(owner == deployer_addr(), 'Invalid Owner'); + + let levels = guildSBT_dispatcher.get_contribution_levels(); + assert(*levels[0] == 100 , 'Invlalid level 0'); + assert(*levels[1] == 200 , 'Invlalid level 1'); + assert(*levels[2] == 500 , 'Invlalid level 2'); + assert(*levels[3] == 1000 , 'Invlalid level 3'); + + let number_of_levels = guildSBT_dispatcher.get_number_of_levels(); + assert(number_of_levels == 4, 'Invalid levels'); + +} \ No newline at end of file diff --git a/tests/test_migrate_points.cairo b/tests/test_migrate_points.cairo new file mode 100644 index 0000000..1f7a052 --- /dev/null +++ b/tests/test_migrate_points.cairo @@ -0,0 +1,367 @@ +use array::{Array, ArrayTrait, SpanTrait}; +use result::ResultTrait; +use starknet::ContractAddress; +use starknet::ClassHash; +use traits::TryInto; +use option::OptionTrait; +use snforge_std::{ declare, ContractClassTrait, ContractClass, start_warp, start_prank, stop_prank, + spy_events, SpyOn, EventSpy, EventFetcher, Event, EventAssertions }; +use tests::utils::{ deployer_addr, user1, user2, user3, user4, URI}; +use contributor_SBT2_0::master::MonthlyContribution; +use contributor_SBT2_0::master::Contribution; + +#[starknet::interface] +trait IMaster { + fn get_last_update_id(self: @TContractState) -> u32; + fn get_contributions_points(self: @TContractState, contributor: ContractAddress) -> Contribution; + + fn update_contibutions(ref self: TContractState, month_id: u32, contributions: Array::); + fn migrate_points_initiated_by_DAO(ref self: TContractState, old_addresses: Array::, new_addresses: Array:: ); + fn initialise(ref self: TContractState, dev_guild: ContractAddress, design_guild: ContractAddress, marcom_guild: ContractAddress, problem_solver_guild: ContractAddress, research_guild: ContractAddress); + fn migrate_points_initiated_by_holder(ref self: TContractState, new_address: ContractAddress); + fn execute_migrate_points_initiated_by_holder(ref self: TContractState, old_address: ContractAddress, new_address: ContractAddress); + +} + +#[starknet::interface] +trait IGuildSBT { + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn wallet_of_owner(self: @TContractState, account: ContractAddress) -> u256; + fn get_contribution_tier(self: @TContractState, contributor: ContractAddress) -> u32; + fn tokenURI(self: @TContractState, token_id: u256) -> Span; + + fn safe_mint(ref self: TContractState, token_type: u8); + +} + + + + +fn deploy_contracts_and_initialise() -> (ContractAddress, ContractAddress, ContractAddress, ContractAddress, ContractAddress, ContractAddress) { + let mut master_constructor_calldata = Default::default(); + Serde::serialize(@deployer_addr(), ref master_constructor_calldata); + let master_class = declare('Master'); + let master_address = master_class.deploy(@master_constructor_calldata).unwrap(); + + // for simplicity deploying all five guild SBTs with same constructors args. + let name = 'Jedi Dev Guild SBT'; + let symbol = 'JEDI-DEV'; + let mut contribution_levels: Array = ArrayTrait::new(); + contribution_levels.append(100); + contribution_levels.append(200); + contribution_levels.append(500); + contribution_levels.append(1000); + + let mut guildSBT_constructor_calldata = Default::default(); + Serde::serialize(@name, ref guildSBT_constructor_calldata); + Serde::serialize(@symbol, ref guildSBT_constructor_calldata); + Serde::serialize(@URI(), ref guildSBT_constructor_calldata); + Serde::serialize(@deployer_addr(), ref guildSBT_constructor_calldata); + Serde::serialize(@master_address, ref guildSBT_constructor_calldata); + Serde::serialize(@contribution_levels, ref guildSBT_constructor_calldata); + + let guildSBT_class = declare('GuildSBT'); + + let dev_guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + let design_guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + let marcom_guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + let problem_solving_guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + let research_guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + + start_prank(master_address, deployer_addr()); + master_dispatcher.initialise(dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address); + stop_prank(master_address); + + (master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address) +} + + +fn update_contribution_and_minting_sbt(master_address: ContractAddress, dev_guildSBT_address: ContractAddress, design_guildSBT_address: ContractAddress, marcom_guildSBT_address: ContractAddress, problem_solving_guildSBT_address: ContractAddress, research_guildSBT_address: ContractAddress) -> (MonthlyContribution, MonthlyContribution) { + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + + let user1_contribution = MonthlyContribution{ contributor: user1(), dev: 120, design: 250, marcom: 20, problem_solving: 30, research: 10}; + let user2_contribution = MonthlyContribution{ contributor: user2(), dev: 200, design: 30, marcom: 160, problem_solving: 0, research: 50}; + + let mut contributions: Array = ArrayTrait::new(); + contributions.append(user1_contribution); + contributions.append(user2_contribution); + + start_prank(master_address, deployer_addr()); + master_dispatcher.update_contibutions(092023, contributions); + stop_prank(master_address); + + let dev_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: dev_guildSBT_address }; + let design_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: design_guildSBT_address }; + let marcom_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: marcom_guildSBT_address }; + + // minting dev and design SBT for user1 + start_prank(dev_guildSBT_address, user1()); + dev_guildSBT_dispatcher.safe_mint(1); + stop_prank(dev_guildSBT_address); + + start_prank(design_guildSBT_address, user1()); + design_guildSBT_dispatcher.safe_mint(1); + stop_prank(design_guildSBT_address); + + + // minting dev and marcom SBT for user2 + start_prank(dev_guildSBT_address, user2()); + dev_guildSBT_dispatcher.safe_mint(2); + stop_prank(dev_guildSBT_address); + + start_prank(marcom_guildSBT_address, user2()); + marcom_guildSBT_dispatcher.safe_mint(2); + stop_prank(marcom_guildSBT_address); + + // checking the sbt balance for users + let mut result = dev_guildSBT_dispatcher.balance_of(user1()); + assert(result == 1, ''); + result = design_guildSBT_dispatcher.balance_of(user1()); + assert(result == 1, ''); + result = dev_guildSBT_dispatcher.balance_of(user2()); + assert(result == 1, ''); + result = marcom_guildSBT_dispatcher.balance_of(user2()); + assert(result == 1, ''); + + (user1_contribution, user2_contribution) +} + +#[test] +fn test_migrate_points_initiated_by_DAO() { + let (master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address) = deploy_contracts_and_initialise(); + let (user1_contribution, user2_contribution) = update_contribution_and_minting_sbt(master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address); + + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + let dev_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: dev_guildSBT_address }; + let design_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: design_guildSBT_address }; + let marcom_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: marcom_guildSBT_address }; + + // noting the tokenID for user_1 + let user_1_dev_token_id = dev_guildSBT_dispatcher.wallet_of_owner(user1()); + let user_1_design_token_id = design_guildSBT_dispatcher.wallet_of_owner(user1()); + + // noting the tokenID for user_2 + let user_2_dev_token_id = dev_guildSBT_dispatcher.wallet_of_owner(user2()); + let user_2_marcom_token_id = marcom_guildSBT_dispatcher.wallet_of_owner(user2()); + + // migrating user1 -> user3 and user2 -> user4 + let mut old_addresses = ArrayTrait::new(); + old_addresses.append(user1()); + old_addresses.append(user2()); + + let mut new_addresses = ArrayTrait::new(); + new_addresses.append(user3()); + new_addresses.append(user4()); + + start_prank(master_address, deployer_addr()); + master_dispatcher.migrate_points_initiated_by_DAO(old_addresses, new_addresses); + stop_prank(master_address); + + + // verifying points are successfully migrated + let user3_contribution: Contribution = master_dispatcher.get_contributions_points(user3()); + assert(user3_contribution.dev == user1_contribution.dev, ''); + assert(user3_contribution.design == user1_contribution.design, ''); + assert(user3_contribution.marcom == user1_contribution.marcom, ''); + assert(user3_contribution.problem_solving == user1_contribution.problem_solving, ''); + assert(user3_contribution.research == user1_contribution.research, ''); + + let user4_contribution: Contribution = master_dispatcher.get_contributions_points(user4()); + assert(user4_contribution.dev == user2_contribution.dev, ''); + assert(user4_contribution.design == user2_contribution.design, ''); + assert(user4_contribution.marcom == user2_contribution.marcom, ''); + assert(user4_contribution.problem_solving == user2_contribution.problem_solving, ''); + assert(user4_contribution.research == user2_contribution.research, ''); + + + // verfying points of old addresses is resetted to zero + let user1_contribution_updated: Contribution = master_dispatcher.get_contributions_points(user1()); + assert(user1_contribution_updated.dev == 0, ''); + assert(user1_contribution_updated.design == 0, ''); + assert(user1_contribution_updated.marcom == 0, ''); + assert(user1_contribution_updated.problem_solving == 0, ''); + assert(user1_contribution_updated.research == 0, ''); + + let user2_contribution_updated: Contribution = master_dispatcher.get_contributions_points(user2()); + assert(user2_contribution_updated.dev == 0, ''); + assert(user2_contribution_updated.design == 0, ''); + assert(user2_contribution_updated.marcom == 0, ''); + assert(user2_contribution_updated.problem_solving == 0, ''); + assert(user2_contribution_updated.research == 0, ''); + + // verifying SBTs are transfered + let mut result = dev_guildSBT_dispatcher.balance_of(user1()); + assert(result == 0, ''); + result = design_guildSBT_dispatcher.balance_of(user1()); + assert(result == 0, ''); + result = dev_guildSBT_dispatcher.balance_of(user2()); + assert(result == 0, ''); + result = marcom_guildSBT_dispatcher.balance_of(user2()); + assert(result == 0, ''); + + result = dev_guildSBT_dispatcher.balance_of(user3()); + assert(result == 1, ''); + result = design_guildSBT_dispatcher.balance_of(user3()); + assert(result == 1, ''); + result = dev_guildSBT_dispatcher.balance_of(user4()); + assert(result == 1, ''); + result = marcom_guildSBT_dispatcher.balance_of(user4()); + assert(result == 1, ''); + + // verifying correct token id is transfered + let user_3_dev_token_id = dev_guildSBT_dispatcher.wallet_of_owner(user3()); + let user_3_design_token_id = design_guildSBT_dispatcher.wallet_of_owner(user3()); + + let user_4_dev_token_id = dev_guildSBT_dispatcher.wallet_of_owner(user4()); + let user_4_marcom_token_id = marcom_guildSBT_dispatcher.wallet_of_owner(user4()); + + assert(user_3_dev_token_id == user_1_dev_token_id, ''); + assert(user_3_design_token_id == user_1_design_token_id, ''); + assert(user_4_dev_token_id == user_4_dev_token_id, ''); + assert(user_4_marcom_token_id == user_4_marcom_token_id, ''); + +} + +#[test] +fn test_migrate_points_initiated_by_DAO_not_owner() { + let (master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address) = deploy_contracts_and_initialise(); + let (user1_contribution, user2_contribution) = update_contribution_and_minting_sbt(master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address); + + let safe_master_dispatcher = IMasterSafeDispatcher { contract_address: master_address }; + + // migrating user1 -> user3 and user2 -> user4 + let mut old_addresses = ArrayTrait::new(); + old_addresses.append(user1()); + old_addresses.append(user2()); + + let mut new_addresses = ArrayTrait::new(); + new_addresses.append(user3()); + new_addresses.append(user4()); + + let mut spy = spy_events(SpyOn::One(master_address)); + + start_prank(master_address, user1()); + match safe_master_dispatcher.migrate_points_initiated_by_DAO(old_addresses, new_addresses) { + Result::Ok(_) => panic_with_felt252('shouldve panicked'), + Result::Err(panic_data) => { + assert(*panic_data.at(0) == 'Caller is not the owner', *panic_data.at(0)); + } + }; + stop_prank(master_address); + +} + +#[test] +#[should_panic(expected: ('INVALID_INPUTS', ))] +fn test_migrate_points_initiated_by_DAO_length_mismatch() { + let (master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address) = deploy_contracts_and_initialise(); + let (user1_contribution, user2_contribution) = update_contribution_and_minting_sbt(master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address); + + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + + // migrating user1 -> user3 and user2 -> X (length mismatch) + let mut old_addresses = ArrayTrait::new(); + old_addresses.append(user1()); + old_addresses.append(user2()); + + let mut new_addresses = ArrayTrait::new(); + new_addresses.append(user3()); + + start_prank(master_address, deployer_addr()); + master_dispatcher.migrate_points_initiated_by_DAO(old_addresses, new_addresses); + stop_prank(master_address); +} + +#[test] +fn test_migrate_points_initiated_by_holder() { + let (master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address) = deploy_contracts_and_initialise(); + let (user1_contribution, user2_contribution) = update_contribution_and_minting_sbt(master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address); + + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + let dev_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: dev_guildSBT_address }; + let design_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: design_guildSBT_address }; + let marcom_guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: marcom_guildSBT_address }; + + // noting the tokenID for user_1 + let user_1_dev_token_id = dev_guildSBT_dispatcher.wallet_of_owner(user1()); + let user_1_design_token_id = design_guildSBT_dispatcher.wallet_of_owner(user1()); + + let mut spy = spy_events(SpyOn::One(master_address)); + + // initiating migration request + start_prank(master_address, user1()); + master_dispatcher.migrate_points_initiated_by_holder(user3()); + stop_prank(master_address); + + let mut event_data_1 = Default::default(); + Serde::serialize(@user1(), ref event_data_1); + Serde::serialize(@user3(), ref event_data_1); + spy.assert_emitted(@array![ + Event { from: master_address, name: 'MigrationQueued', keys: array![], data: event_data_1 } + ]); + + // executing migration (by DAO) + start_prank(master_address, deployer_addr()); + master_dispatcher.execute_migrate_points_initiated_by_holder(user1(), user3()); + stop_prank(master_address); + + let mut event_data_2 = Default::default(); + Serde::serialize(@user1(), ref event_data_2); + Serde::serialize(@user3(), ref event_data_2); + spy.assert_emitted(@array![ + Event { from: master_address, name: 'Migrated', keys: array![], data: event_data_2 } + ]); + + // verifying points are successfully migrated + let user3_contribution: Contribution = master_dispatcher.get_contributions_points(user3()); + assert(user3_contribution.dev == user1_contribution.dev, ''); + assert(user3_contribution.design == user1_contribution.design, ''); + assert(user3_contribution.marcom == user1_contribution.marcom, ''); + assert(user3_contribution.problem_solving == user1_contribution.problem_solving, ''); + assert(user3_contribution.research == user1_contribution.research, ''); + + // verfying points of old addresses is resetted to zero + let user1_contribution_updated: Contribution = master_dispatcher.get_contributions_points(user1()); + assert(user1_contribution_updated.dev == 0, ''); + assert(user1_contribution_updated.design == 0, ''); + assert(user1_contribution_updated.marcom == 0, ''); + assert(user1_contribution_updated.problem_solving == 0, ''); + assert(user1_contribution_updated.research == 0, ''); + + + // verifying SBTs are transfered + let mut result = dev_guildSBT_dispatcher.balance_of(user1()); + assert(result == 0, ''); + result = design_guildSBT_dispatcher.balance_of(user1()); + assert(result == 0, ''); + + result = dev_guildSBT_dispatcher.balance_of(user3()); + assert(result == 1, ''); + result = design_guildSBT_dispatcher.balance_of(user3()); + assert(result == 1, ''); + + // verifying correct token id is transfered + let user_3_dev_token_id = dev_guildSBT_dispatcher.wallet_of_owner(user3()); + let user_3_design_token_id = design_guildSBT_dispatcher.wallet_of_owner(user3()); + + assert(user_3_dev_token_id == user_1_dev_token_id, ''); + assert(user_3_design_token_id == user_1_design_token_id, ''); + + +} + +#[should_panic(expected: ('NOT_QUEUED', ))] +#[test] +fn test_execute_migrate_points_without_initiating() { + let (master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address) = deploy_contracts_and_initialise(); + let (user1_contribution, user2_contribution) = update_contribution_and_minting_sbt(master_address, dev_guildSBT_address, design_guildSBT_address, marcom_guildSBT_address, problem_solving_guildSBT_address, research_guildSBT_address); + + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + + // executing migration (by DAO) without initiated by holder. + start_prank(master_address, deployer_addr()); + master_dispatcher.execute_migrate_points_initiated_by_holder(user1(), user3()); + stop_prank(master_address); +} \ No newline at end of file diff --git a/tests/test_mint.cairo b/tests/test_mint.cairo new file mode 100644 index 0000000..ed50f7e --- /dev/null +++ b/tests/test_mint.cairo @@ -0,0 +1,154 @@ +use array::{Array, ArrayTrait, SpanTrait}; +use result::ResultTrait; +use starknet::ContractAddress; +use starknet::ClassHash; +use traits::TryInto; +use option::OptionTrait; +use snforge_std::{ declare, ContractClassTrait, ContractClass, start_warp, start_prank, stop_prank, + spy_events, SpyOn, EventSpy, EventFetcher, Event, EventAssertions }; +use tests::utils::{ deployer_addr, user1, user2, URI}; +use contributor_SBT2_0::master::MonthlyContribution; + +#[starknet::interface] +trait IMaster { + fn get_last_update_id(self: @TContractState) -> u32; + + fn update_contibutions(ref self: TContractState, month_id: u32, contributions: Array::); + +} + +#[starknet::interface] +trait IGuildSBT { + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn wallet_of_owner(self: @TContractState, account: ContractAddress) -> u256; + fn get_contribution_tier(self: @TContractState, contributor: ContractAddress) -> u32; + fn tokenURI(self: @TContractState, token_id: u256) -> Span; + fn get_next_token_id(self: @TContractState) -> u256; + + + fn safe_mint(ref self: TContractState, token_type: u8); +} + + +fn deploy_contracts() -> (ContractAddress, ContractAddress) { + let mut master_constructor_calldata = Default::default(); + Serde::serialize(@deployer_addr(), ref master_constructor_calldata); + let master_class = declare('Master'); + let master_address = master_class.deploy(@master_constructor_calldata).unwrap(); + + let name = 'Jedi Dev Guild SBT'; + let symbol = 'JEDI-DEV'; + let mut contribution_levels: Array = ArrayTrait::new(); + contribution_levels.append(100); + contribution_levels.append(200); + contribution_levels.append(500); + contribution_levels.append(1000); + + let mut guildSBT_constructor_calldata = Default::default(); + Serde::serialize(@name, ref guildSBT_constructor_calldata); + Serde::serialize(@symbol, ref guildSBT_constructor_calldata); + Serde::serialize(@URI(), ref guildSBT_constructor_calldata); + Serde::serialize(@deployer_addr(), ref guildSBT_constructor_calldata); + Serde::serialize(@master_address, ref guildSBT_constructor_calldata); + Serde::serialize(@contribution_levels, ref guildSBT_constructor_calldata); + + let guildSBT_class = declare('GuildSBT'); + let guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + + (master_address, guildSBT_address) +} + +#[test] +#[should_panic(expected: ('NOT_ENOUGH_POINTS', ))] +fn test_mint_not_enough_contribution_points() { + let (_, guildSBT_address) = deploy_contracts(); + let guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: guildSBT_address }; + + let balance = guildSBT_dispatcher.balance_of(user1()); + assert(balance == 0, 'invlaid initialisation'); + + start_prank(guildSBT_address, user1()); + guildSBT_dispatcher.safe_mint(1); + stop_prank(guildSBT_address); + +} + +#[test] +fn test_mint() { + let (master_address, guildSBT_address) = deploy_contracts(); + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + let guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: guildSBT_address }; + + let balance = guildSBT_dispatcher.balance_of(user1()); + assert(balance == 0, 'invlaid initialisation'); + + let mut contributions: Array = ArrayTrait::new(); + contributions.append(MonthlyContribution{ contributor: user1(), dev: 240, design: 250, marcom: 20, problem_solving: 30, research: 10}); + contributions.append(MonthlyContribution{ contributor: user2(), dev: 200, design: 30, marcom: 0, problem_solving: 0, research: 50}); + + start_prank(master_address, deployer_addr()); + master_dispatcher.update_contibutions(092023, contributions); + stop_prank(master_address); + + let expected_token_id = guildSBT_dispatcher.get_next_token_id(); + + start_prank(guildSBT_address, user1()); + guildSBT_dispatcher.safe_mint(1); + stop_prank(guildSBT_address); + + let new_balance = guildSBT_dispatcher.balance_of(user1()); + assert(new_balance == 1, 'invalid balance'); + + let user1_token_id = guildSBT_dispatcher.wallet_of_owner(user1()); + assert(user1_token_id == expected_token_id, 'Incorrect token id'); + + let tokenURI = guildSBT_dispatcher.tokenURI(user1_token_id); + assert(*tokenURI[0] == 'api.jediswap/', 'Invlalid item 0'); + assert(*tokenURI[1] == 'guildSBT/', 'Invlalid item 1'); + assert(*tokenURI[2] == 'dev/', 'Invlalid item 2'); + assert(*tokenURI[3] == '2', 'Invlalid tier (item 3)'); + assert(*tokenURI[4] == '1', 'Invlalid type (item 4)'); + assert(*tokenURI[5] == '.json', 'Invlalid item 5'); + assert(tokenURI.len() == 6, 'should be 6'); + + //verifying token id is updated + let updated_token_id = guildSBT_dispatcher.get_next_token_id(); + assert(updated_token_id == expected_token_id + 1, 'token id not updated'); + +} + +#[test] +fn test_mint_second_sbt() { + let (master_address, guildSBT_address) = deploy_contracts(); + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + let guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: guildSBT_address }; + let safe_guildSBT_dispatcher = IGuildSBTSafeDispatcher { contract_address: guildSBT_address }; + + let balance = guildSBT_dispatcher.balance_of(user1()); + assert(balance == 0, 'invalid initialisation'); + + let mut contributions: Array = ArrayTrait::new(); + contributions.append(MonthlyContribution{ contributor: user1(), dev: 240, design: 250, marcom: 20, problem_solving: 30, research: 10}); + contributions.append(MonthlyContribution{ contributor: user2(), dev: 200, design: 30, marcom: 0, problem_solving: 0, research: 50}); + + start_prank(master_address, deployer_addr()); + master_dispatcher.update_contibutions(092023, contributions); + stop_prank(master_address); + + start_prank(guildSBT_address, user1()); + safe_guildSBT_dispatcher.safe_mint(1); + stop_prank(guildSBT_address); + + let new_balance = guildSBT_dispatcher.balance_of(user1()); + assert(new_balance == 1, 'invalid balance'); + + start_prank(guildSBT_address, user1()); + match safe_guildSBT_dispatcher.safe_mint(1) { + Result::Ok(_) => panic_with_felt252('shouldve panicked'), + Result::Err(panic_data) => { + assert(*panic_data.at(0) == 'ALREADY_MINTED', *panic_data.at(0)); + } + }; + stop_prank(guildSBT_address); + +} \ No newline at end of file diff --git a/tests/test_update_contribution_points.cairo b/tests/test_update_contribution_points.cairo new file mode 100644 index 0000000..df6ac69 --- /dev/null +++ b/tests/test_update_contribution_points.cairo @@ -0,0 +1,192 @@ +use array::{Array, ArrayTrait, SpanTrait}; +use result::ResultTrait; +use starknet::ContractAddress; +use starknet::ClassHash; +use traits::TryInto; +use option::OptionTrait; +use snforge_std::{ declare, ContractClassTrait, ContractClass, start_warp, start_prank, stop_prank, + spy_events, SpyOn, EventSpy, EventFetcher, Event, EventAssertions }; +use tests::utils::{ deployer_addr, user1, user2, user3, user4, URI}; +use contributor_SBT2_0::master::MonthlyContribution; +use contributor_SBT2_0::master::Contribution; +use contributor_SBT2_0::master::TotalMonthlyContribution; + +#[starknet::interface] +trait IMaster { + fn get_last_update_id(self: @TContractState) -> u32; + fn get_contributions_points(self: @TContractState, contributor: ContractAddress) -> Contribution; + fn get_contributions_data(self: @TContractState, contributor: ContractAddress, guild: felt252) -> Array; + fn get_total_contribution(self: @TContractState, month_id: u32) -> TotalMonthlyContribution; + + + fn update_contibutions(ref self: TContractState, month_id: u32, contributions: Array::); + +/////// + +} + +#[starknet::interface] +trait IGuildSBT { + fn get_contribution_tier(self: @TContractState, contributor: ContractAddress) -> u32; + fn get_master(self: @TContractState) -> ContractAddress; +} + + +fn deploy_contracts() -> (ContractAddress, ContractAddress) { + let mut master_constructor_calldata = Default::default(); + Serde::serialize(@deployer_addr(), ref master_constructor_calldata); + let master_class = declare('Master'); + let master_address = master_class.deploy(@master_constructor_calldata).unwrap(); + + let name = 'Jedi Dev Guild SBT'; + let symbol = 'JEDI-DEV'; + let mut contribution_levels: Array = ArrayTrait::new(); + contribution_levels.append(100); + contribution_levels.append(200); + contribution_levels.append(500); + contribution_levels.append(1000); + + let mut guildSBT_constructor_calldata = Default::default(); + Serde::serialize(@name, ref guildSBT_constructor_calldata); + Serde::serialize(@symbol, ref guildSBT_constructor_calldata); + Serde::serialize(@URI(), ref guildSBT_constructor_calldata); + Serde::serialize(@deployer_addr(), ref guildSBT_constructor_calldata); + Serde::serialize(@master_address, ref guildSBT_constructor_calldata); + Serde::serialize(@contribution_levels, ref guildSBT_constructor_calldata); + + let guildSBT_class = declare('GuildSBT'); + let guildSBT_address = guildSBT_class.deploy(@guildSBT_constructor_calldata).unwrap(); + + (master_address, guildSBT_address) +} + +#[test] +fn test_update_contribution_points() { + let (master_address, guildSBT_address) = deploy_contracts(); + let master_dispatcher = IMasterDispatcher { contract_address: master_address }; + let guildSBT_dispatcher = IGuildSBTDispatcher { contract_address: guildSBT_address }; + + let id1 = master_dispatcher.get_last_update_id(); + assert (id1 == 0, 'Invalid id initialisation'); + + let month_id: u32 = 092023; + + let user1_contribution = MonthlyContribution{ contributor: user1(), dev: 120, design: 250, marcom: 20, problem_solving: 0, research: 10}; + let user2_contribution = MonthlyContribution{ contributor: user2(), dev: 200, design: 30, marcom: 0, problem_solving: 0, research: 50}; + let user3_contribution = MonthlyContribution{ contributor: user3(), dev: 30, design: 100, marcom: 0, problem_solving: 0, research: 50}; + let user4_contribution = MonthlyContribution{ contributor: user4(), dev: 1500, design: 0, marcom: 25, problem_solving: 0, research: 100}; + let mut contributions: Array = ArrayTrait::new(); + contributions.append(user1_contribution); + contributions.append(user2_contribution); + contributions.append(user3_contribution); + contributions.append(user4_contribution); + + let mut spy = spy_events(SpyOn::One(master_address)); + + start_prank(master_address, deployer_addr()); + master_dispatcher.update_contibutions(month_id, contributions); + stop_prank(master_address); + + let mut user1_event_data = Default::default(); + Serde::serialize(@id1, ref user1_event_data); + Serde::serialize(@user1(), ref user1_event_data); + Serde::serialize(@month_id, ref user1_event_data); + Serde::serialize(@user1_contribution, ref user1_event_data); + spy.assert_emitted(@array![ + Event { from: master_address, name: 'ContributionUpdated', keys: array![], data: user1_event_data } + ]); + + let mut user2_event_data = Default::default(); + Serde::serialize(@id1, ref user2_event_data); + Serde::serialize(@user2(), ref user2_event_data); + Serde::serialize(@month_id, ref user2_event_data); + Serde::serialize(@user2_contribution, ref user2_event_data); + spy.assert_emitted(@array![ + Event { from: master_address, name: 'ContributionUpdated', keys: array![], data: user2_event_data } + ]); + + let mut user3_event_data = Default::default(); + Serde::serialize(@id1, ref user3_event_data); + Serde::serialize(@user3(), ref user3_event_data); + Serde::serialize(@month_id, ref user3_event_data); + Serde::serialize(@user3_contribution, ref user3_event_data); + spy.assert_emitted(@array![ + Event { from: master_address, name: 'ContributionUpdated', keys: array![], data: user3_event_data } + ]); + + let mut user4_event_data = Default::default(); + Serde::serialize(@id1, ref user4_event_data); + Serde::serialize(@user4(), ref user4_event_data); + Serde::serialize(@month_id, ref user4_event_data); + Serde::serialize(@user4_contribution, ref user4_event_data); + spy.assert_emitted(@array![ + Event { from: master_address, name: 'ContributionUpdated', keys: array![], data: user4_event_data } + ]); + + let id2 = master_dispatcher.get_last_update_id(); + assert (id2 == 1, 'invalid id'); + + // verifying points for user 1 is updated + let mut points = master_dispatcher.get_contributions_points(user1()); + assert(points.dev == 120, 'invalid dev point'); + assert(points.design == 250, 'invalid design point'); + assert(points.marcom == 20, 'invalid marcom point'); + assert(points.problem_solving == 0, 'invalid problem solving point'); + assert(points.research == 10, 'invalid research point'); + + // verifying points for user 4 is updated + points = master_dispatcher.get_contributions_points(user4()); + assert(points.dev == 1500, 'invalid dev point'); + assert(points.design == 0, 'invalid design point'); + assert(points.marcom == 25, 'invalid marcom point'); + assert(points.problem_solving == 0, 'invalid problem solving point'); + assert(points.research == 100, 'invalid research point'); + + // verifying contribution data(Montly) is updated + let mut dev_data = master_dispatcher.get_contributions_data(user1(), 'dev'); + assert(dev_data.len() == 2, 'invalid length'); + assert(*dev_data[0] == month_id, 'invalid month id'); + assert(*dev_data[1] == 120, 'invalid month points'); + + let mut design_data = master_dispatcher.get_contributions_data(user1(), 'design'); + assert(design_data.len() == 2, 'invalid length'); + assert(*design_data[0] == month_id, 'invalid month id'); + assert(*design_data[1] == 250, 'invalid month points'); + + let mut marcom_data = master_dispatcher.get_contributions_data(user1(), 'marcom'); + assert(marcom_data.len() == 2, 'invalid length'); + assert(*marcom_data[0] == month_id, 'invalid month id'); + assert(*marcom_data[1] == 20, 'invalid month points'); + + let mut problem_solving_data = master_dispatcher.get_contributions_data(user1(), 'problem_solving'); + assert(problem_solving_data.len() == 0, 'invalid length'); + + let mut research_data = master_dispatcher.get_contributions_data(user1(), 'research'); + assert(research_data.len() == 2, 'invalid length'); + assert(*research_data[0] == month_id, 'invalid month id'); + assert(*research_data[1] == 10, 'invalid month points'); + + + // verifying tier for each levels + let tier_user1 = guildSBT_dispatcher.get_contribution_tier(user1()); + assert (tier_user1 == 1, 'invalid tier_user1'); + + let tier_user2 = guildSBT_dispatcher.get_contribution_tier(user2()); + assert (tier_user2 == 2, 'invalid tier_user2'); + + let tier_user3 = guildSBT_dispatcher.get_contribution_tier(user3()); + assert (tier_user3 == 0, 'invalid tier_user3'); + + let tier_user4 = guildSBT_dispatcher.get_contribution_tier(user4()); + assert (tier_user4 == 4, 'invalid tier_user4'); + + // verifying total monthly contribution + let total_contribution_point = master_dispatcher.get_total_contribution(month_id); + assert(total_contribution_point.dev == 1850, 'incorrect total dev'); + assert(total_contribution_point.design == 380, 'incorrect total design'); + assert(total_contribution_point.marcom == 45, 'incorrect total marcom'); + assert(total_contribution_point.problem_solving == 0, 'incorrect total problem solving'); + assert(total_contribution_point.research == 210, 'incorrect total research'); + + +} \ No newline at end of file diff --git a/tests/utils.cairo b/tests/utils.cairo new file mode 100644 index 0000000..41838e7 --- /dev/null +++ b/tests/utils.cairo @@ -0,0 +1,37 @@ +use starknet:: { ContractAddress, contract_address_try_from_felt252, contract_address_const }; + + +fn deployer_addr() -> ContractAddress { + contract_address_try_from_felt252('deployer').unwrap() +} + + +fn zero_addr() -> ContractAddress { + contract_address_const::<0>() +} + +fn user1() -> ContractAddress { + contract_address_try_from_felt252('user1').unwrap() +} + +fn user2() -> ContractAddress { + contract_address_try_from_felt252('user2').unwrap() +} + +fn user3() -> ContractAddress { + contract_address_try_from_felt252('user3').unwrap() +} + +fn user4() -> ContractAddress { + contract_address_try_from_felt252('user4').unwrap() +} + +fn URI() -> Span { + let mut uri = ArrayTrait::new(); + + uri.append('api.jediswap/'); + uri.append('guildSBT/'); + uri.append('dev/'); + + uri.span() +} \ No newline at end of file diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..c3e75aa --- /dev/null +++ b/version.txt @@ -0,0 +1,3 @@ +scarb 0.7.0 (58cc88efb 2023-08-23) +cairo: 2.2.0 (https://crates.io/crates/cairo-lang-compiler/2.2.0) +sierra: 1.3.0