From 372e876601fa1cdeee762382009dc39fe18fbf92 Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Sun, 6 Jul 2025 22:31:58 +0100 Subject: [PATCH 01/17] Feat: Implement Creator Payout Workflow --- .tool-versions | 4 +- src/base/types.cairo | 57 ++++++ src/chainlib/ChainLib.cairo | 310 ++++++++++++++++++++++++++++++++- src/interfaces/IChainLib.cairo | 14 +- 4 files changed, 373 insertions(+), 12 deletions(-) diff --git a/.tool-versions b/.tool-versions index 37dc27f..3acc460 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -scarb 2.11.4 -starknet-foundry 0.43.1 +scarb 2.11.2 +sstarknet-foundry 0.40.0 diff --git a/src/base/types.cairo b/src/base/types.cairo index aac0e03..818e95f 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -148,6 +148,63 @@ pub struct Purchase { pub timeout_expiry: u64, } +#[derive(Copy, Serde, Drop, PartialEq, Debug, starknet::Store)] +pub enum PayoutStatus { + #[default] + PENDING, + PAID, + CANCELLED, +} + +#[derive(Copy, Drop, Serde, starknet::Store, PartialEq, Debug)] +pub struct Payout { + pub id: u64, + pub purchase_id: u256, + pub recipient: ContractAddress, + pub amount: u256, + pub timestamp: u64, + pub status: PayoutStatus, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct PayoutSchedule { + pub interval: u64, //interval between payouts, same type as block_timestamp + pub start_time: u64, + pub last_execution: u64, + // pub schedule_id: u256, +} + +#[allow(starknet::store_no_default_variant)] +#[derive(Copy, Drop, Serde, starknet::Store, PartialEq)] +pub enum RefundRequestReason { + CONTENT_NOT_RECEIVED, + DUPLICATE_PURCHASE, + UNABLE_TO_ACCESS, + MISREPRESENTED_CONTENT, + OTHER: felt252, +} + +#[derive(Copy, Drop, Serde, starknet::Store, PartialEq)] +pub enum RefundStatus { + #[default] + PENDING, + TIMED_OUT, + DECLINED, + APPROVED, + PAID +} + +#[derive(Copy, Drop, Serde, starknet::Store, PartialEq)] +pub struct Refund { + pub refund_id: u64, + pub purchase_id: u256, + pub reason: RefundRequestReason, + pub user: ContractAddress, + pub status: RefundStatus, + pub request_timestamp: u64, + pub refund_amount: Option, +} + #[derive(Drop, Serde, starknet::Store, Debug)] pub enum ReceiptStatus { diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 7fb6247..04085e0 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -1,6 +1,7 @@ #[starknet::contract] pub mod ChainLib { use core::array::{Array, ArrayTrait}; + use core::num::traits::Zero; use core::option::OptionTrait; use core::traits::Into; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -12,11 +13,11 @@ pub mod ChainLib { ContractAddress, contract_address_const, get_block_timestamp, get_caller_address, get_contract_address, }; - use crate::base::errors::payment_errors; + use crate::base::errors::{payment_errors, permission_errors}; use crate::base::types::{ - AccessRule, AccessType, Permissions, Purchase, PurchaseStatus, Rank, Receipt, ReceiptStatus, - Role, Status, TokenBoundAccount, User, VerificationRequirement, VerificationType, - permission_flags, + AccessRule, AccessType, Payout, PayoutSchedule, PayoutStatus, Permissions, Purchase, + PurchaseStatus, Rank, Receipt, ReceiptStatus, Role, Status, TokenBoundAccount, User, + VerificationRequirement, VerificationType, permission_flags, RefundRequestReason, Refund, RefundStatus }; use crate::interfaces::IChainLib::IChainLib; @@ -222,12 +223,24 @@ pub mod ChainLib { creator_sales: Map, total_sales_for_content: Map, token_address: ContractAddress, + platform_fee: u256, //basis points; 1000 = 10% + platform_fee_recipient: ContractAddress, + payout_schedule: PayoutSchedule, + payout_history: Map>, // map a creator's address to a Vec of his payouts + user_refunds: Map>, + refund_window: u64, } #[constructor] fn constructor( - ref self: ContractState, admin: ContractAddress, token_address: ContractAddress, + ref self: ContractState, + admin: ContractAddress, + token_address: ContractAddress, + platform_fee: u256, + platform_fee_recipient: ContractAddress, + payout_schedule_interval: u64, + refund_window: u64, ) { // Store the values in contract state self.admin.write(admin); @@ -235,6 +248,15 @@ pub mod ChainLib { // Initialize purchase ID counter self.next_purchase_id.write(1_u256); self.purchase_timeout_duration.write(3600); + self.platform_fee.write(platform_fee); + self.platform_fee_recipient.write(platform_fee_recipient); + let payout_schedule = PayoutSchedule { + interval: payout_schedule_interval, + start_time: get_block_timestamp(), + last_execution: 0, + }; + self.payout_schedule.write(payout_schedule); + self.refund_window.write(refund_window); } #[event] @@ -266,6 +288,7 @@ pub mod ChainLib { SubscriptionCancelled: SubscriptionCancelled, SubscriptionRenewed: SubscriptionRenewed, ReceiptGenerated: ReceiptGenerated, + PayoutExecuted: PayoutExecuted, } #[derive(Drop, starknet::Event)] @@ -438,6 +461,14 @@ pub mod ChainLib { pub timestamp: u64, } + // Payout and Refunds + #[derive(Drop, starknet::Event)] + pub struct PayoutExecuted { + pub recipients: Array, + pub timestamp: u64, + pub amount_paid: u256, + } + #[abi(embed_v0)] impl ChainLibNetImpl of IChainLib { fn create_token_account( @@ -1445,7 +1476,7 @@ pub mod ChainLib { let user = self.users.read(user_id); assert(user.id == user_id, 'User does not exist'); - let current_time = get_block_timestamp(); + // let current_time = get_block_timestamp(); // Create a new subscription let subscription_id = self.subscription_id.read() + 1; @@ -1486,7 +1517,7 @@ pub mod ChainLib { self.subscriptions.write(user_id, new_subscription.clone()); // read from the subscription - self.subscription_record.entry(user_id).append().write(new_subscription); + self.subscription_record.entry(user_id).push(new_subscription); let current_count = self.subscription_count.read(user_id); self.subscription_count.write(user_id, current_count + 1); @@ -1869,9 +1900,27 @@ pub mod ChainLib { let total_content_sales = self.total_sales_for_content.read(purchase.content_id) + purchase.price; - let total_creator_sales = self.creator_sales.read(content.creator) + purchase.price; + let platform_fee_percentage = self.platform_fee.read(); + let platform_fee_recipient = self.platform_fee_recipient.read(); + + let actual_platform_fee = (platform_fee_percentage * purchase.price) / 10000; + let creators_fraction = purchase.price - actual_platform_fee; + + let total_creator_sales = self.creator_sales.read(content.creator) + creators_fraction; self.creator_sales.write(content.creator, total_creator_sales); + self._single_payout(platform_fee_recipient, actual_platform_fee); + let creator_payout_history_id = self.payout_history.entry(content.creator).len(); + let creator_payout = Payout { + id: creator_payout_history_id, + purchase_id, + recipient: content.creator, + amount: creators_fraction, + timestamp: get_block_timestamp(), + status: PayoutStatus::PENDING + }; + self.payout_history.entry(content.creator).push(creator_payout); + self.total_sales_for_content.write(purchase.content_id, total_content_sales); self .issue_receipt( @@ -2067,6 +2116,230 @@ pub mod ChainLib { let total_content_sales = self.total_sales_for_content.read(content_id); total_content_sales } + + fn batch_payout_creators(ref self: ContractState) { + let current_user_count = self.user_id.read(); + let mut creators_array: Array = array![]; + let mut amount_paid_out: u256 = 0; + for i in 0..current_user_count { + if self.users.read(i).role == Role::WRITER { + creators_array.append(self.users.read(i)); + } + } + let mut recipients_array: Array = array![]; + + for i in 0..creators_array.len() { + let current_creator = creators_array.at(i); + let current_creator_address = *current_creator.wallet_address; + recipients_array.append(current_creator_address); + + let current_creator_payout_history_vec = self.payout_history.entry(current_creator_address); + let mut pending_payouts: Array = array![]; + let mut amt_to_be_paid_creator = 0_u256; + + for i in 0..current_creator_payout_history_vec.len() { + let mut payout = current_creator_payout_history_vec.at(i).read(); + if payout.status == PayoutStatus::PENDING { + pending_payouts.append(payout); + amt_to_be_paid_creator += payout.amount; + payout.status == PayoutStatus::PAID; + } + current_creator_payout_history_vec.at(i).write(payout); + } + + let transfer = self + ._single_payout(current_creator_address, amt_to_be_paid_creator); + assert(transfer, 'Transfer failed'); + amount_paid_out += amt_to_be_paid_creator; + self.creator_sales.write(current_creator_address, 0); + } + + self.emit( + PayoutExecuted { + recipients: recipients_array, + timestamp: get_block_timestamp(), + amount_paid: amount_paid_out + } + ); + } + + fn set_payout_schedule(ref self: ContractState, interval: u64) { + let caller = get_caller_address(); + assert(self.admin.read() == caller, 'Only admin can verify payments'); + let current_payout_schedule = self.payout_schedule.read(); + let last_execution_date = current_payout_schedule.last_execution; + let new_schedule = PayoutSchedule { + interval, + start_time: last_execution_date, + last_execution: 0 + }; + self.payout_schedule.write(new_schedule); + } + + fn get_payout_schedule(self: @ContractState) -> (u64, u64) { // interval and last execution time + let payout_schedule = self.payout_schedule.read(); + (payout_schedule.interval, payout_schedule.last_execution) + } + + fn request_refund(ref self: ContractState, purchase_id: u256, refund_reason: RefundRequestReason) { + let caller = get_caller_address(); + // let user = self.user_by_address.read(caller); + let user_refunds_vec = self.user_refunds.entry(caller); + let refund_id = user_refunds_vec.len(); + let refund_request = Refund { + refund_id, + purchase_id, + reason: refund_reason, + user: caller, + status: RefundStatus::PENDING, + request_timestamp: get_block_timestamp(), + refund_amount: Option::None + }; + self.user_refunds.entry(caller).push(refund_request); + } + + fn approve_refund(ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: Option) { + let caller = get_caller_address(); + // Ensure that only an admin can verify users. + assert((self.admin.read() == caller), 'Only admin can approve refunds'); + let user = self.users.read(user_id); + let user_address = user.wallet_address; + let mut refund = self.user_refunds.entry(user_address).at(refund_id).read(); + assert(refund.status == RefundStatus::PENDING, 'Request already processed'); + + let mut refund_amount = 0; + + let refund_reason = refund.reason; + let mut refund_percent = self.get_refund_percentage(refund_reason); + if refund_percent == 0 { + assert(refund_percentage.is_some(), 'Choose custom percentage'); + refund_percent = refund_percentage.unwrap(); + } + // We'll take the refund percent later in the contract from the creator payout + + let request_timestamp = refund.request_timestamp; + let time_since_request = get_block_timestamp() - request_timestamp; + if time_since_request >= self.refund_window.read() { + refund.status = RefundStatus::TIMED_OUT; + } else { + refund.status = RefundStatus::APPROVED; + } + + // This should affect the creator payout for that purchase + + let purchase_id = refund.purchase_id; + let purchase = self.purchases.read(purchase_id); + let content_id = purchase.content_id; + let content = self.content.read(content_id); + let content_creator = content.creator; + + let creator_payout_vec = self.payout_history.entry(content_creator); + let mut dummy_payout_array = array![]; + // Dummy array that will help retrieve the payout we need to edit + for i in 0..creator_payout_vec.len() { + let specific_payout = creator_payout_vec.at(i).read(); + if specific_payout.purchase_id == purchase_id { + dummy_payout_array.append(specific_payout); + } + } + // Retrieve the payout, the length will be one, but we can assert too + assert(dummy_payout_array.len() == 1, 'Double Payout-purchase entry'); + let mut specific_payout = *dummy_payout_array.at(0); + + let refund_amount = refund_percent * specific_payout.amount / 100; + specific_payout.amount -= refund_amount; + if refund_amount == specific_payout.amount { + specific_payout.status = PayoutStatus::CANCELLED; + } + + + self.payout_history.entry(content_creator).at(specific_payout.id).write(specific_payout); + self.user_refunds.entry(user_address).at(refund_id).write(refund); + + } + + fn decline_refund(ref self: ContractState, refund_id: u64, user_id: u256) { + let caller = get_caller_address(); + // Ensure that only an admin can verify users. + assert((self.admin.read() == caller), 'Only admin can approve refunds'); + let user = self.users.read(user_id); + let user_address = user.wallet_address; + let mut refund = self.user_refunds.entry(user_address).at(refund_id).read(); + assert(refund.status == RefundStatus::PENDING, 'Request already processed'); + let request_timestamp = refund.request_timestamp; + let time_since_request = get_block_timestamp() - request_timestamp; + if time_since_request >= self.refund_window.read() { + refund.status = RefundStatus::TIMED_OUT; + } else { + refund.status = RefundStatus::DECLINED; + } + self.user_refunds.entry(user_address).at(refund_id).write(refund); + + // Last thought: implement refund windows differently for different reasons. + // Also check the access when you refund a user + } + + fn refund_user(ref self: ContractState, refund_id: u64, user_id: u256) { + let caller = get_caller_address(); + // Ensure that only an admin can verify users. + assert((self.admin.read() == caller), 'Only admin can approve refunds'); + let user = self.users.read(user_id); + let user_address = user.wallet_address; + let mut refund = self.user_refunds.entry(user_address).at(refund_id).read(); + assert(refund.status == RefundStatus::APPROVED, 'Request already processed'); + refund.status = RefundStatus::PAID; + self.user_refunds.entry(user_address).at(refund_id).write(refund); + + let refund_amount = refund.refund_amount.unwrap(); + + self._process_refund(refund_amount, user_address); + + // Emit event + } + + fn get_user_refunds(self: @ContractState, user_id: u256) -> Array { + let user = self.users.read(user_id); + let user_address = user.wallet_address; + let user_refunds_vec = self.user_refunds.entry(user_address); + let mut user_refunds_arr = array![]; + + for i in 0..user_refunds_vec.len() { + let current_refund = user_refunds_vec.at(i).read(); + user_refunds_arr.append(current_refund); + } + + user_refunds_arr + } + + fn get_all_pending_refunds(self: @ContractState) -> Array { + let caller = get_caller_address(); + // Ensure that only an admin can verify users. + assert((self.admin.read() == caller), 'Only admin can approve refunds'); + + let current_user_id = self.user_id.read(); + let mut all_pending_refunds_arr = array![]; + let mut all_users_array = array![]; + + for i in 0..current_user_id { + let current_user = self.users.read(i); + all_users_array.append(current_user); + } + + for i in 0..all_users_array.len() { + let current_user = all_users_array.at(i); + let current_user_address = current_user.wallet_address; + let current_user_refunds_vec = self.user_refunds.entry(*current_user_address); + + for i in 0..current_user_refunds_vec.len() { + let current_user_refund = current_user_refunds_vec.at(i).read(); + if current_user_refund.status == RefundStatus::PENDING { + all_pending_refunds_arr.append(current_user_refund); + } + } + } + + all_pending_refunds_arr + } } #[generate_trait] @@ -2109,11 +2382,32 @@ pub mod ChainLib { assert(balance >= amount, payment_errors::INSUFFICIENT_BALANCE); } + fn _single_payout( + ref self: ContractState, recipient_address: ContractAddress, amount: u256, + ) -> bool { + assert(!recipient_address.is_zero(), permission_errors::ZERO_ADDRESS); + let token_dispatcher = IERC20Dispatcher { contract_address: self.token_address.read() }; + let contract_address = get_contract_address(); + self._check_token_balance(contract_address, amount); + let transfer = token_dispatcher.transfer(recipient_address, amount); + transfer + } + fn _process_refund(ref self: ContractState, amount: u256, refund_address: ContractAddress) { let token = IERC20Dispatcher { contract_address: self.token_address.read() }; let contract_address = get_contract_address(); self._check_token_balance(contract_address, amount); token.transfer(refund_address, amount); } + + fn get_refund_percentage(ref self: ContractState, refund_reason: RefundRequestReason) -> u256 { + match refund_reason { + RefundRequestReason::CONTENT_NOT_RECEIVED => 100, + RefundRequestReason::DUPLICATE_PURCHASE => 80, + RefundRequestReason::UNABLE_TO_ACCESS => 100, + RefundRequestReason::MISREPRESENTED_CONTENT => 65, + _ => 0 + } + } } } diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index 95d9c05..41a1b34 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -2,7 +2,7 @@ use core::array::Array; use starknet::ContractAddress; use crate::base::types::{ AccessRule, Permissions, Purchase, PurchaseStatus, Rank, Receipt, Role, TokenBoundAccount, User, - VerificationRequirement, VerificationType, + VerificationRequirement, VerificationType, RefundRequestReason, Refund }; use crate::chainlib::ChainLib::ChainLib::{ Category, ContentMetadata, ContentType, DelegationInfo, Payment, PlanType, Subscription, @@ -228,5 +228,15 @@ pub trait IChainLib { fn get_total_sales_by_creator(self: @TContractState, creator: ContractAddress) -> u256; fn get_total_sales_for_content(self: @TContractState, content_id: felt252) -> u256; // fn get_daily_sales(self: @TContractState, day: u64) -> u256; -// fn get_unique_buyers_count(self: @TContractState) -> u256; + // fn get_unique_buyers_count(self: @TContractState) -> u256; + + fn batch_payout_creators(ref self: TContractState); + fn set_payout_schedule(ref self: TContractState, interval: u64); + fn get_payout_schedule(self: @TContractState) -> (u64, u64); // interval and last execution time + fn request_refund(ref self: TContractState, purchase_id: u256, refund_reason: RefundRequestReason); + fn approve_refund(ref self: TContractState, refund_id: u64, user_id: u256, refund_percentage: Option); + fn decline_refund(ref self: TContractState, refund_id: u64, user_id: u256); + fn refund_user(ref self: TContractState, refund_id: u64, user_id: u256); + fn get_user_refunds(self: @TContractState, user_id: u256) -> Array; + fn get_all_pending_refunds(self: @TContractState) -> Array; } From e8dfdf806e3d0416b3b00ab6f2ddee957ccf5595 Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 06:33:11 +0100 Subject: [PATCH 02/17] emitted event --- src/base/types.cairo | 2 +- src/chainlib/ChainLib.cairo | 169 +++++++++++++++++++++++++++++---- src/interfaces/IChainLib.cairo | 2 + tests/test_utils.cairo | 2 +- 4 files changed, 154 insertions(+), 21 deletions(-) diff --git a/src/base/types.cairo b/src/base/types.cairo index 818e95f..c9faa16 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -166,7 +166,7 @@ pub struct Payout { pub status: PayoutStatus, } -#[derive(Copy, Drop, Serde, starknet::Store)] +#[derive(Copy, Drop, Serde, Default, PartialEq, starknet::Store)] pub struct PayoutSchedule { pub interval: u64, //interval between payouts, same type as block_timestamp pub start_time: u64, diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 04085e0..7570fb9 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -231,16 +231,19 @@ pub mod ChainLib { refund_window: u64, } + const REFUND_WINDOW: u64 = 86400; + const PLATFORM_FEE: u256 = 900; //9% + const PAYOUT_SCHEDULE_INTERVAL: u64 = 86400 * 7; #[constructor] fn constructor( ref self: ContractState, admin: ContractAddress, token_address: ContractAddress, - platform_fee: u256, - platform_fee_recipient: ContractAddress, - payout_schedule_interval: u64, - refund_window: u64, + // platform_fee: u256, + // platform_fee_recipient: ContractAddress, + // payout_schedule_interval: u64, + // refund_window: u64, ) { // Store the values in contract state self.admin.write(admin); @@ -248,15 +251,15 @@ pub mod ChainLib { // Initialize purchase ID counter self.next_purchase_id.write(1_u256); self.purchase_timeout_duration.write(3600); - self.platform_fee.write(platform_fee); - self.platform_fee_recipient.write(platform_fee_recipient); - let payout_schedule = PayoutSchedule { - interval: payout_schedule_interval, - start_time: get_block_timestamp(), - last_execution: 0, - }; - self.payout_schedule.write(payout_schedule); - self.refund_window.write(refund_window); + // self.platform_fee.write(PLATFORM_FEE); + // self.platform_fee_recipient.write(get_contract_address()); + // let payout_schedule = PayoutSchedule { + // interval: PAYOUT_SCHEDULE_INTERVAL, + // start_time: get_block_timestamp(), + // last_execution: 0, + // }; + // self.payout_schedule.write(payout_schedule); + // self.refund_window.write(REFUND_WINDOW); } #[event] @@ -289,6 +292,13 @@ pub mod ChainLib { SubscriptionRenewed: SubscriptionRenewed, ReceiptGenerated: ReceiptGenerated, PayoutExecuted: PayoutExecuted, + PayoutScheduleSet: PayoutScheduleSet, + RefundRequested: RefundRequested, + RefundApproved: RefundApproved, + RefundDeclined: RefundDeclined, + RefundPaid: RefundPaid, + PlatformFeeChanged: PlatformFeeChanged, + RefundWindowChanged: RefundWindowChanged, } #[derive(Drop, starknet::Event)] @@ -469,6 +479,58 @@ pub mod ChainLib { pub amount_paid: u256, } + #[derive(Drop, starknet::Event)] + pub struct PayoutScheduleSet { + pub start_time: u64, + pub setter: ContractAddress, + pub interval: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RefundRequested { + pub user: ContractAddress, + pub content_id: felt252, + pub purchase_id: u256, + pub reason: RefundRequestReason, + } + + #[derive(Drop, starknet::Event)] + pub struct RefundApproved { + pub approver: ContractAddress, + pub user_id: u256, + pub content_id: felt252, + pub refund_id: u64 + } + + #[derive(Drop, starknet::Event)] + pub struct RefundDeclined { + pub decliner: ContractAddress, + pub user_id: u256, + pub content_id: felt252, + pub refund_id: u64 + } + + #[derive(Drop, starknet::Event)] + pub struct RefundPaid { + pub refund_id: u64, + pub content_id: felt252, + pub purchase_id: u256, + pub executor: ContractAddress, + pub user_id: u256 + } + + #[derive(Drop, starknet::Event)] + pub struct PlatformFeeChanged { + pub new_fee: u256, + pub timestamp: u64 + } + + #[derive(Drop, starknet::Event)] + pub struct RefundWindowChanged { + pub new_window: u64, + pub timestamp: u64 + } + #[abi(embed_v0)] impl ChainLibNetImpl of IChainLib { fn create_token_account( @@ -2167,13 +2229,23 @@ pub mod ChainLib { let caller = get_caller_address(); assert(self.admin.read() == caller, 'Only admin can verify payments'); let current_payout_schedule = self.payout_schedule.read(); - let last_execution_date = current_payout_schedule.last_execution; + let mut last_execution_date = get_block_timestamp(); + if current_payout_schedule != Default::default() { + last_execution_date = current_payout_schedule.last_execution; + } let new_schedule = PayoutSchedule { interval, start_time: last_execution_date, last_execution: 0 }; self.payout_schedule.write(new_schedule); + self.emit( + PayoutScheduleSet { + start_time: last_execution_date, + setter: caller, + interval, + } + ); } fn get_payout_schedule(self: @ContractState) -> (u64, u64) { // interval and last execution time @@ -2196,6 +2268,15 @@ pub mod ChainLib { refund_amount: Option::None }; self.user_refunds.entry(caller).push(refund_request); + let content_id = self.purchases.read(purchase_id).content_id; + self.emit( + RefundRequested { + user: caller, + content_id, + purchase_id, + reason: refund_reason + } + ); } fn approve_refund(ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: Option) { @@ -2255,7 +2336,14 @@ pub mod ChainLib { self.payout_history.entry(content_creator).at(specific_payout.id).write(specific_payout); self.user_refunds.entry(user_address).at(refund_id).write(refund); - + self.emit( + RefundApproved { + approver: caller, + user_id, + content_id, + refund_id + } + ); } fn decline_refund(ref self: ContractState, refund_id: u64, user_id: u256) { @@ -2265,6 +2353,8 @@ pub mod ChainLib { let user = self.users.read(user_id); let user_address = user.wallet_address; let mut refund = self.user_refunds.entry(user_address).at(refund_id).read(); + let purchase = refund.purchase_id; + let content_id = self.purchases.read(purchase).content_id; assert(refund.status == RefundStatus::PENDING, 'Request already processed'); let request_timestamp = refund.request_timestamp; let time_since_request = get_block_timestamp() - request_timestamp; @@ -2275,8 +2365,14 @@ pub mod ChainLib { } self.user_refunds.entry(user_address).at(refund_id).write(refund); - // Last thought: implement refund windows differently for different reasons. - // Also check the access when you refund a user + self.emit( + RefundDeclined { + decliner: caller, + user_id, + content_id, + refund_id + } + ); } fn refund_user(ref self: ContractState, refund_id: u64, user_id: u256) { @@ -2289,12 +2385,21 @@ pub mod ChainLib { assert(refund.status == RefundStatus::APPROVED, 'Request already processed'); refund.status = RefundStatus::PAID; self.user_refunds.entry(user_address).at(refund_id).write(refund); - + let purchase_id = refund.purchase_id; + let content_id = self.purchases.read(purchase_id).content_id; let refund_amount = refund.refund_amount.unwrap(); self._process_refund(refund_amount, user_address); - // Emit event + self.emit( + RefundPaid { + refund_id, + content_id, + purchase_id: purchase_id, + executor: caller, + user_id + } + ); } fn get_user_refunds(self: @ContractState, user_id: u256) -> Array { @@ -2340,6 +2445,32 @@ pub mod ChainLib { all_pending_refunds_arr } + + fn set_platform_fee(ref self: ContractState, platform_fee: u256) { + let caller = get_caller_address(); + // Ensure that only an admin can verify users. + assert((self.admin.read() == caller), 'Only admin can execute refunds'); + self.platform_fee.write(platform_fee); + self.emit( + PlatformFeeChanged { + new_fee: platform_fee, + timestamp: get_block_timestamp() + } + ); + } + + fn set_refund_window(ref self: ContractState, window: u64) { + let caller = get_caller_address(); + // Ensure that only an admin can verify users. + assert((self.admin.read() == caller), 'Only admin can execute refunds'); + self.refund_window.write(window); + self.emit( + RefundWindowChanged { + new_window: window, + timestamp: get_block_timestamp() + } + ); + } } #[generate_trait] diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index 41a1b34..cad2250 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -239,4 +239,6 @@ pub trait IChainLib { fn refund_user(ref self: TContractState, refund_id: u64, user_id: u256); fn get_user_refunds(self: @TContractState, user_id: u256) -> Array; fn get_all_pending_refunds(self: @TContractState) -> Array; + fn set_platform_fee(ref self: TContractState, platform_fee: u256); + fn set_refund_window(ref self: TContractState, window: u64); } diff --git a/tests/test_utils.cairo b/tests/test_utils.cairo index e893ecf..37a265c 100644 --- a/tests/test_utils.cairo +++ b/tests/test_utils.cairo @@ -71,4 +71,4 @@ pub fn setup_content_with_price( // Use the new set_content_price function to set the price dispatcher.set_content_price(content_id, price); -} +} \ No newline at end of file From 46ed2cb32045e4072e3f2a4d6d345cf3ad937bcb Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 06:47:24 +0100 Subject: [PATCH 03/17] ran scarb fmt --- src/base/types.cairo | 2 +- src/chainlib/ChainLib.cairo | 177 +++++++++++++++------------------ src/interfaces/IChainLib.cairo | 14 ++- tests/test_utils.cairo | 2 +- 4 files changed, 91 insertions(+), 104 deletions(-) diff --git a/src/base/types.cairo b/src/base/types.cairo index c9faa16..8768731 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -191,7 +191,7 @@ pub enum RefundStatus { TIMED_OUT, DECLINED, APPROVED, - PAID + PAID, } #[derive(Copy, Drop, Serde, starknet::Store, PartialEq)] diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 7570fb9..1762482 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -16,8 +16,9 @@ pub mod ChainLib { use crate::base::errors::{payment_errors, permission_errors}; use crate::base::types::{ AccessRule, AccessType, Payout, PayoutSchedule, PayoutStatus, Permissions, Purchase, - PurchaseStatus, Rank, Receipt, ReceiptStatus, Role, Status, TokenBoundAccount, User, - VerificationRequirement, VerificationType, permission_flags, RefundRequestReason, Refund, RefundStatus + PurchaseStatus, Rank, Receipt, ReceiptStatus, Refund, RefundRequestReason, RefundStatus, + Role, Status, TokenBoundAccount, User, VerificationRequirement, VerificationType, + permission_flags, }; use crate::interfaces::IChainLib::IChainLib; @@ -226,7 +227,9 @@ pub mod ChainLib { platform_fee: u256, //basis points; 1000 = 10% platform_fee_recipient: ContractAddress, payout_schedule: PayoutSchedule, - payout_history: Map>, // map a creator's address to a Vec of his payouts + payout_history: Map< + ContractAddress, Vec, + >, // map a creator's address to a Vec of his payouts user_refunds: Map>, refund_window: u64, } @@ -237,13 +240,11 @@ pub mod ChainLib { #[constructor] fn constructor( - ref self: ContractState, - admin: ContractAddress, - token_address: ContractAddress, + ref self: ContractState, admin: ContractAddress, token_address: ContractAddress, // platform_fee: u256, - // platform_fee_recipient: ContractAddress, - // payout_schedule_interval: u64, - // refund_window: u64, + // platform_fee_recipient: ContractAddress, + // payout_schedule_interval: u64, + // refund_window: u64, ) { // Store the values in contract state self.admin.write(admin); @@ -252,14 +253,14 @@ pub mod ChainLib { self.next_purchase_id.write(1_u256); self.purchase_timeout_duration.write(3600); // self.platform_fee.write(PLATFORM_FEE); - // self.platform_fee_recipient.write(get_contract_address()); - // let payout_schedule = PayoutSchedule { - // interval: PAYOUT_SCHEDULE_INTERVAL, - // start_time: get_block_timestamp(), - // last_execution: 0, - // }; - // self.payout_schedule.write(payout_schedule); - // self.refund_window.write(REFUND_WINDOW); + // self.platform_fee_recipient.write(get_contract_address()); + // let payout_schedule = PayoutSchedule { + // interval: PAYOUT_SCHEDULE_INTERVAL, + // start_time: get_block_timestamp(), + // last_execution: 0, + // }; + // self.payout_schedule.write(payout_schedule); + // self.refund_window.write(REFUND_WINDOW); } #[event] @@ -499,7 +500,7 @@ pub mod ChainLib { pub approver: ContractAddress, pub user_id: u256, pub content_id: felt252, - pub refund_id: u64 + pub refund_id: u64, } #[derive(Drop, starknet::Event)] @@ -507,7 +508,7 @@ pub mod ChainLib { pub decliner: ContractAddress, pub user_id: u256, pub content_id: felt252, - pub refund_id: u64 + pub refund_id: u64, } #[derive(Drop, starknet::Event)] @@ -516,19 +517,19 @@ pub mod ChainLib { pub content_id: felt252, pub purchase_id: u256, pub executor: ContractAddress, - pub user_id: u256 + pub user_id: u256, } #[derive(Drop, starknet::Event)] pub struct PlatformFeeChanged { pub new_fee: u256, - pub timestamp: u64 + pub timestamp: u64, } #[derive(Drop, starknet::Event)] pub struct RefundWindowChanged { pub new_window: u64, - pub timestamp: u64 + pub timestamp: u64, } #[abi(embed_v0)] @@ -1979,7 +1980,7 @@ pub mod ChainLib { recipient: content.creator, amount: creators_fraction, timestamp: get_block_timestamp(), - status: PayoutStatus::PENDING + status: PayoutStatus::PENDING, }; self.payout_history.entry(content.creator).push(creator_payout); @@ -2195,7 +2196,9 @@ pub mod ChainLib { let current_creator_address = *current_creator.wallet_address; recipients_array.append(current_creator_address); - let current_creator_payout_history_vec = self.payout_history.entry(current_creator_address); + let current_creator_payout_history_vec = self + .payout_history + .entry(current_creator_address); let mut pending_payouts: Array = array![]; let mut amt_to_be_paid_creator = 0_u256; @@ -2209,20 +2212,20 @@ pub mod ChainLib { current_creator_payout_history_vec.at(i).write(payout); } - let transfer = self - ._single_payout(current_creator_address, amt_to_be_paid_creator); + let transfer = self._single_payout(current_creator_address, amt_to_be_paid_creator); assert(transfer, 'Transfer failed'); amount_paid_out += amt_to_be_paid_creator; self.creator_sales.write(current_creator_address, 0); } - self.emit( - PayoutExecuted { - recipients: recipients_array, - timestamp: get_block_timestamp(), - amount_paid: amount_paid_out - } - ); + self + .emit( + PayoutExecuted { + recipients: recipients_array, + timestamp: get_block_timestamp(), + amount_paid: amount_paid_out, + }, + ); } fn set_payout_schedule(ref self: ContractState, interval: u64) { @@ -2234,26 +2237,25 @@ pub mod ChainLib { last_execution_date = current_payout_schedule.last_execution; } let new_schedule = PayoutSchedule { - interval, - start_time: last_execution_date, - last_execution: 0 + interval, start_time: last_execution_date, last_execution: 0, }; self.payout_schedule.write(new_schedule); - self.emit( - PayoutScheduleSet { - start_time: last_execution_date, - setter: caller, - interval, - } - ); + self + .emit( + PayoutScheduleSet { start_time: last_execution_date, setter: caller, interval }, + ); } - fn get_payout_schedule(self: @ContractState) -> (u64, u64) { // interval and last execution time + fn get_payout_schedule( + self: @ContractState, + ) -> (u64, u64) { // interval and last execution time let payout_schedule = self.payout_schedule.read(); (payout_schedule.interval, payout_schedule.last_execution) } - fn request_refund(ref self: ContractState, purchase_id: u256, refund_reason: RefundRequestReason) { + fn request_refund( + ref self: ContractState, purchase_id: u256, refund_reason: RefundRequestReason, + ) { let caller = get_caller_address(); // let user = self.user_by_address.read(caller); let user_refunds_vec = self.user_refunds.entry(caller); @@ -2265,21 +2267,21 @@ pub mod ChainLib { user: caller, status: RefundStatus::PENDING, request_timestamp: get_block_timestamp(), - refund_amount: Option::None + refund_amount: Option::None, }; self.user_refunds.entry(caller).push(refund_request); let content_id = self.purchases.read(purchase_id).content_id; - self.emit( - RefundRequested { - user: caller, - content_id, - purchase_id, - reason: refund_reason - } - ); + self + .emit( + RefundRequested { + user: caller, content_id, purchase_id, reason: refund_reason, + }, + ); } - fn approve_refund(ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: Option) { + fn approve_refund( + ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: Option, + ) { let caller = get_caller_address(); // Ensure that only an admin can verify users. assert((self.admin.read() == caller), 'Only admin can approve refunds'); @@ -2333,17 +2335,13 @@ pub mod ChainLib { specific_payout.status = PayoutStatus::CANCELLED; } - - self.payout_history.entry(content_creator).at(specific_payout.id).write(specific_payout); + self + .payout_history + .entry(content_creator) + .at(specific_payout.id) + .write(specific_payout); self.user_refunds.entry(user_address).at(refund_id).write(refund); - self.emit( - RefundApproved { - approver: caller, - user_id, - content_id, - refund_id - } - ); + self.emit(RefundApproved { approver: caller, user_id, content_id, refund_id }); } fn decline_refund(ref self: ContractState, refund_id: u64, user_id: u256) { @@ -2365,14 +2363,7 @@ pub mod ChainLib { } self.user_refunds.entry(user_address).at(refund_id).write(refund); - self.emit( - RefundDeclined { - decliner: caller, - user_id, - content_id, - refund_id - } - ); + self.emit(RefundDeclined { decliner: caller, user_id, content_id, refund_id }); } fn refund_user(ref self: ContractState, refund_id: u64, user_id: u256) { @@ -2391,15 +2382,12 @@ pub mod ChainLib { self._process_refund(refund_amount, user_address); - self.emit( - RefundPaid { - refund_id, - content_id, - purchase_id: purchase_id, - executor: caller, - user_id - } - ); + self + .emit( + RefundPaid { + refund_id, content_id, purchase_id: purchase_id, executor: caller, user_id, + }, + ); } fn get_user_refunds(self: @ContractState, user_id: u256) -> Array { @@ -2420,7 +2408,7 @@ pub mod ChainLib { let caller = get_caller_address(); // Ensure that only an admin can verify users. assert((self.admin.read() == caller), 'Only admin can approve refunds'); - + let current_user_id = self.user_id.read(); let mut all_pending_refunds_arr = array![]; let mut all_users_array = array![]; @@ -2451,12 +2439,10 @@ pub mod ChainLib { // Ensure that only an admin can verify users. assert((self.admin.read() == caller), 'Only admin can execute refunds'); self.platform_fee.write(platform_fee); - self.emit( - PlatformFeeChanged { - new_fee: platform_fee, - timestamp: get_block_timestamp() - } - ); + self + .emit( + PlatformFeeChanged { new_fee: platform_fee, timestamp: get_block_timestamp() }, + ); } fn set_refund_window(ref self: ContractState, window: u64) { @@ -2464,12 +2450,7 @@ pub mod ChainLib { // Ensure that only an admin can verify users. assert((self.admin.read() == caller), 'Only admin can execute refunds'); self.refund_window.write(window); - self.emit( - RefundWindowChanged { - new_window: window, - timestamp: get_block_timestamp() - } - ); + self.emit(RefundWindowChanged { new_window: window, timestamp: get_block_timestamp() }); } } @@ -2531,13 +2512,15 @@ pub mod ChainLib { token.transfer(refund_address, amount); } - fn get_refund_percentage(ref self: ContractState, refund_reason: RefundRequestReason) -> u256 { + fn get_refund_percentage( + ref self: ContractState, refund_reason: RefundRequestReason, + ) -> u256 { match refund_reason { RefundRequestReason::CONTENT_NOT_RECEIVED => 100, RefundRequestReason::DUPLICATE_PURCHASE => 80, RefundRequestReason::UNABLE_TO_ACCESS => 100, RefundRequestReason::MISREPRESENTED_CONTENT => 65, - _ => 0 + _ => 0, } } } diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index cad2250..5b89feb 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -1,8 +1,8 @@ use core::array::Array; use starknet::ContractAddress; use crate::base::types::{ - AccessRule, Permissions, Purchase, PurchaseStatus, Rank, Receipt, Role, TokenBoundAccount, User, - VerificationRequirement, VerificationType, RefundRequestReason, Refund + AccessRule, Permissions, Purchase, PurchaseStatus, Rank, Receipt, Refund, RefundRequestReason, + Role, TokenBoundAccount, User, VerificationRequirement, VerificationType, }; use crate::chainlib::ChainLib::ChainLib::{ Category, ContentMetadata, ContentType, DelegationInfo, Payment, PlanType, Subscription, @@ -233,10 +233,14 @@ pub trait IChainLib { fn batch_payout_creators(ref self: TContractState); fn set_payout_schedule(ref self: TContractState, interval: u64); fn get_payout_schedule(self: @TContractState) -> (u64, u64); // interval and last execution time - fn request_refund(ref self: TContractState, purchase_id: u256, refund_reason: RefundRequestReason); - fn approve_refund(ref self: TContractState, refund_id: u64, user_id: u256, refund_percentage: Option); + fn request_refund( + ref self: TContractState, purchase_id: u256, refund_reason: RefundRequestReason, + ); + fn approve_refund( + ref self: TContractState, refund_id: u64, user_id: u256, refund_percentage: Option, + ); fn decline_refund(ref self: TContractState, refund_id: u64, user_id: u256); - fn refund_user(ref self: TContractState, refund_id: u64, user_id: u256); + fn refund_user(ref self: TContractState, refund_id: u64, user_id: u256); fn get_user_refunds(self: @TContractState, user_id: u256) -> Array; fn get_all_pending_refunds(self: @TContractState) -> Array; fn set_platform_fee(ref self: TContractState, platform_fee: u256); diff --git a/tests/test_utils.cairo b/tests/test_utils.cairo index 37a265c..e893ecf 100644 --- a/tests/test_utils.cairo +++ b/tests/test_utils.cairo @@ -71,4 +71,4 @@ pub fn setup_content_with_price( // Use the new set_content_price function to set the price dispatcher.set_content_price(content_id, price); -} \ No newline at end of file +} From e310b54e7b6700e7bfd034e9fdb581bced47c90a Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 11:17:26 +0100 Subject: [PATCH 04/17] verify_purchase test --- src/chainlib/ChainLib.cairo | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 1762482..5f3a2bb 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -240,11 +240,13 @@ pub mod ChainLib { #[constructor] fn constructor( - ref self: ContractState, admin: ContractAddress, token_address: ContractAddress, + ref self: ContractState, + admin: ContractAddress, + token_address: ContractAddress, // platform_fee: u256, - // platform_fee_recipient: ContractAddress, - // payout_schedule_interval: u64, - // refund_window: u64, + // platform_fee_recipient: ContractAddress, + // payout_schedule_interval: u64, + refund_window: u64, ) { // Store the values in contract state self.admin.write(admin); @@ -252,15 +254,15 @@ pub mod ChainLib { // Initialize purchase ID counter self.next_purchase_id.write(1_u256); self.purchase_timeout_duration.write(3600); - // self.platform_fee.write(PLATFORM_FEE); - // self.platform_fee_recipient.write(get_contract_address()); - // let payout_schedule = PayoutSchedule { - // interval: PAYOUT_SCHEDULE_INTERVAL, - // start_time: get_block_timestamp(), - // last_execution: 0, - // }; - // self.payout_schedule.write(payout_schedule); - // self.refund_window.write(REFUND_WINDOW); + self.platform_fee_recipient.write(admin); + self.platform_fee.write(PLATFORM_FEE); + let payout_schedule = PayoutSchedule { + interval: PAYOUT_SCHEDULE_INTERVAL, + start_time: get_block_timestamp(), + last_execution: 0, + }; + self.payout_schedule.write(payout_schedule); + self.refund_window.write(REFUND_WINDOW); } #[event] From 043d6fa7109da358cc75a077dccaabaee706285e Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 11:21:48 +0100 Subject: [PATCH 05/17] .. --- src/chainlib/ChainLib.cairo | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 5f3a2bb..af10076 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -240,13 +240,11 @@ pub mod ChainLib { #[constructor] fn constructor( - ref self: ContractState, - admin: ContractAddress, - token_address: ContractAddress, + ref self: ContractState, admin: ContractAddress, token_address: ContractAddress, // platform_fee: u256, - // platform_fee_recipient: ContractAddress, - // payout_schedule_interval: u64, - refund_window: u64, + // platform_fee_recipient: ContractAddress, + // payout_schedule_interval: u64, + // refund_window: u64, ) { // Store the values in contract state self.admin.write(admin); From 75d1fa612e72eee1adc6a223eadabd46dda01a8b Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 11:50:41 +0100 Subject: [PATCH 06/17] more testing --- src/chainlib/ChainLib.cairo | 21 +++++++++++++-------- tests/test_utils.cairo | 6 ++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index af10076..630f88a 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -240,11 +240,13 @@ pub mod ChainLib { #[constructor] fn constructor( - ref self: ContractState, admin: ContractAddress, token_address: ContractAddress, - // platform_fee: u256, - // platform_fee_recipient: ContractAddress, - // payout_schedule_interval: u64, - // refund_window: u64, + ref self: ContractState, + admin: ContractAddress, + token_address: ContractAddress, + platform_fee: u256, + // platform_fee_recipient: ContractAddress, + payout_schedule_interval: u64, + refund_window: u64, ) { // Store the values in contract state self.admin.write(admin); @@ -253,14 +255,17 @@ pub mod ChainLib { self.next_purchase_id.write(1_u256); self.purchase_timeout_duration.write(3600); self.platform_fee_recipient.write(admin); - self.platform_fee.write(PLATFORM_FEE); + // self.platform_fee.write(PLATFORM_FEE); + self.platform_fee.write(platform_fee); let payout_schedule = PayoutSchedule { - interval: PAYOUT_SCHEDULE_INTERVAL, + // interval: PAYOUT_SCHEDULE_INTERVAL, + interval: payout_schedule_interval, start_time: get_block_timestamp(), last_execution: 0, }; self.payout_schedule.write(payout_schedule); - self.refund_window.write(REFUND_WINDOW); + // self.refund_window.write(REFUND_WINDOW); + self.refund_window.write(refund_window); } #[event] diff --git a/tests/test_utils.cairo b/tests/test_utils.cairo index e893ecf..9cfa184 100644 --- a/tests/test_utils.cairo +++ b/tests/test_utils.cairo @@ -17,9 +17,15 @@ pub fn setup() -> (ContractAddress, ContractAddress, ContractAddress) { // Deploy the ChainLib contract let declare_result = declare("ChainLib"); assert(declare_result.is_ok(), 'Contract declaration failed'); + let platform_fee: u256 = 900; + let payout_schedule_interval: u64 = 86400 * 7; + let refund_window: u64 = 86400 * 2; let contract_class = declare_result.unwrap().contract_class(); let mut calldata = array![admin_address.into(), erc20_address.into()]; + platform_fee.serialize(ref calldata); + payout_schedule_interval.serialize(ref calldata); + refund_window.serialize(ref calldata); let deploy_result = contract_class.deploy(@calldata); assert(deploy_result.is_ok(), 'Contract deployment failed'); From 23d745fffe1eeaafde053dea0477f35d4435c8d7 Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 13:41:30 +0100 Subject: [PATCH 07/17] added batch_payout test --- src/chainlib/ChainLib.cairo | 10 ++++ tests/test_ChainLib.cairo | 98 ++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 630f88a..9e08c81 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -2186,6 +2186,16 @@ pub mod ChainLib { } fn batch_payout_creators(ref self: ContractState) { + let caller = get_caller_address(); + assert(self.admin.read() == caller, 'Only admin can execute payout'); + + let payout_schedule = self.payout_schedule.read(); + let (last_execution, interval) = ( + payout_schedule.last_execution, payout_schedule.interval, + ); + let current_time = get_block_timestamp(); + assert(current_time >= (last_execution + interval), 'Payout period not reached'); + let current_user_count = self.user_id.read(); let mut creators_array: Array = array![]; let mut amount_paid_out: u256 = 0; diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index e16f6a0..499d53d 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -5,8 +5,9 @@ use chain_lib::chainlib::ChainLib::ChainLib::{Category, ContentType, PlanType, S use chain_lib::interfaces::IChainLib::{IChainLib, IChainLibDispatcher, IChainLibDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ - CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare, - start_cheat_caller_address, stop_cheat_caller_address, + CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_block_timestamp, cheat_caller_address, + declare, start_cheat_block_timestamp, start_cheat_caller_address, stop_cheat_block_timestamp, + stop_cheat_caller_address, }; use starknet::ContractAddress; use starknet::class_hash::ClassHash; @@ -888,3 +889,96 @@ fn test_renew_subscription() { let subscription_record = dispatcher.get_user_subscription_record(account_id); assert(subscription_record.len() == 1, 'record should have length 1'); } + +#[test] +fn test_batch_payout_creators() { + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + let user_address = contract_address_const::<'user'>(); + let creator_1 = contract_address_const::<'creator_1'>(); + let creator_2 = contract_address_const::<'creator_2'>(); + let creator_3 = contract_address_const::<'creator_3'>(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let creator_1_init_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_init_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_init_bal = erc20_dispatcher.balance_of(creator_3); + + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); + // Set up test data + let creator1_content_id: felt252 = 'content1'; + let creator2_content_id: felt252 = 'content2'; + let creator3_content_id: felt252 = 'content3'; + let price_1: u256 = 1000_u256; + let price_2: u256 = 2000_u256; + let price_3: u256 = 1500_u256; + + // Set creator_1 as caller to set up content price + cheat_caller_address(contract_address, creator_1, CheatSpan::Indefinite); + // Set up content with price + setup_content_with_price(dispatcher, creator_1, contract_address, creator1_content_id, price_1); + + // Set creatpr_2 as caller to setup for another piece of content + cheat_caller_address(contract_address, creator_2, CheatSpan::Indefinite); + // Set up content with price + setup_content_with_price(dispatcher, creator_2, contract_address, creator2_content_id, price_2); + + // Set creatpr_2 as caller to setup for another piece of content + cheat_caller_address(contract_address, creator_3, CheatSpan::Indefinite); + // Set up content with price + setup_content_with_price(dispatcher, creator_3, contract_address, creator3_content_id, price_3); + + // Set user as caller + cheat_caller_address(contract_address, user_address, CheatSpan::Indefinite); + + // Purchase the content + let purchase_id_1 = dispatcher.purchase_content(creator1_content_id, 'tx1'); + let purchase_id_2 = dispatcher.purchase_content(creator2_content_id, 'tx2'); + let purchase_id_3 = dispatcher.purchase_content(creator3_content_id, 'tx3'); + + // Initially, purchase should not be verified (status is Pending) + let is_1_verified = dispatcher.verify_purchase(purchase_id_1); + let is_2_verified = dispatcher.verify_purchase(purchase_id_2); + let is_3_verified = dispatcher.verify_purchase(purchase_id_3); + assert(!is_1_verified, '1 should not be verified'); + assert(!is_2_verified, '2 should not be verified'); + assert(!is_3_verified, '3 should not be verified'); + + // Set admin as caller to update the purchase status + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + + // Update purchase status to Completed + let update_result_1 = dispatcher + .update_purchase_status(purchase_id_1, PurchaseStatus::Completed); + assert(update_result_1, 'Failed to update status1'); + let update_result_2 = dispatcher + .update_purchase_status(purchase_id_2, PurchaseStatus::Completed); + assert(update_result_2, 'Failed to update status1'); + let update_result_3 = dispatcher + .update_purchase_status(purchase_id_3, PurchaseStatus::Completed); + assert(update_result_3, 'Failed to update status1'); + + // Now the purchase should be verified + let is_now_verified1 = dispatcher.verify_purchase(purchase_id_1); + assert(is_now_verified1, 'Purchase should be verified'); + let is_now_verified2 = dispatcher.verify_purchase(purchase_id_2); + assert(is_now_verified2, 'Purchase should be verified'); + let is_now_verified3 = dispatcher.verify_purchase(purchase_id_3); + assert(is_now_verified3, 'Purchase should be verified'); + + let receipt = dispatcher.get_receipt(1); + + assert(receipt.purchase_id == purchase_id_1, 'receipt error'); + + // Still admin calling + + // start_cheat_block_timestamp(contract_address); + dispatcher.batch_payout_creators(); + + let creator_1_new_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_new_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_new_bal = erc20_dispatcher.balance_of(creator_3); + + assert(creator_1_new_bal > creator_1_init_bal, 'Failed to credit creator1'); + assert(creator_2_new_bal > creator_2_init_bal, 'Failed to credit creator2'); + assert(creator_3_new_bal > creator_3_init_bal, 'Failed to credit creator3'); +} From c6d66a19013d6a20f9216df10dcc293e687d0a80 Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 14:14:43 +0100 Subject: [PATCH 08/17] .. --- tests/test_ChainLib.cairo | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 499d53d..736c7e1 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -905,29 +905,42 @@ fn test_batch_payout_creators() { token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); // Set up test data - let creator1_content_id: felt252 = 'content1'; - let creator2_content_id: felt252 = 'content2'; - let creator3_content_id: felt252 = 'content3'; + let title1: felt252 = 'Creator 1 content'; + let title2: felt252 = 'Creator 2 content'; + let title3: felt252 = 'Creator 3 content'; + + let description: felt252 = 'This is a test content'; + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Education; + + let creator1_content_id: felt252 = dispatcher + .register_content(title1, description, content_type, category); + let creator2_content_id: felt252 = dispatcher + .register_content(title2, description, content_type, category); + let creator3_content_id: felt252 = dispatcher + .register_content(title3, description, content_type, category); let price_1: u256 = 1000_u256; let price_2: u256 = 2000_u256; let price_3: u256 = 1500_u256; // Set creator_1 as caller to set up content price - cheat_caller_address(contract_address, creator_1, CheatSpan::Indefinite); + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); // Set up content with price - setup_content_with_price(dispatcher, creator_1, contract_address, creator1_content_id, price_1); + setup_content_with_price( + dispatcher, admin_address, contract_address, creator1_content_id, price_1, + ); - // Set creatpr_2 as caller to setup for another piece of content - cheat_caller_address(contract_address, creator_2, CheatSpan::Indefinite); // Set up content with price - setup_content_with_price(dispatcher, creator_2, contract_address, creator2_content_id, price_2); + setup_content_with_price( + dispatcher, admin_address, contract_address, creator2_content_id, price_2, + ); - // Set creatpr_2 as caller to setup for another piece of content - cheat_caller_address(contract_address, creator_3, CheatSpan::Indefinite); // Set up content with price - setup_content_with_price(dispatcher, creator_3, contract_address, creator3_content_id, price_3); + setup_content_with_price( + dispatcher, admin_address, contract_address, creator3_content_id, price_3, + ); - // Set user as caller + // Set user as content consumer cheat_caller_address(contract_address, user_address, CheatSpan::Indefinite); // Purchase the content From 47e730d840d1bf40e32e6ab91adfde9d4b6f5b5c Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 14:26:23 +0100 Subject: [PATCH 09/17] .. --- tests/test_ChainLib.cairo | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 736c7e1..a04f018 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -898,6 +898,27 @@ fn test_batch_payout_creators() { let creator_1 = contract_address_const::<'creator_1'>(); let creator_2 = contract_address_const::<'creator_2'>(); let creator_3 = contract_address_const::<'creator_3'>(); + + let username1: felt252 = 'creator_1'; + let username2: felt252 = 'creator_2'; + let username3: felt252 = 'creator_3'; + + let role: Role = Role::WRITER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'john is a boy'; + + start_cheat_caller_address(contract_address, creator_1); + let creator1_id = dispatcher.register_user(username1, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); + let creator2_id = dispatcher.register_user(username2, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); + let creator3_id = dispatcher.register_user(username3, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; let creator_1_init_bal = erc20_dispatcher.balance_of(creator_1); let creator_2_init_bal = erc20_dispatcher.balance_of(creator_2); From e619d96915298467d4e230f6e9a16ca8f4c2cc0a Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 14:51:01 +0100 Subject: [PATCH 10/17] .. --- src/chainlib/ChainLib.cairo | 4 +++- tests/test_ChainLib.cairo | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 9e08c81..ddf52db 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -698,7 +698,9 @@ pub mod ChainLib { assert((self.admin.read() == caller), 'Only admin can verify users'); let mut user = self.users.read(user_id); user.verified = true; - self.users.write(user.id, user); + let user_address = user.wallet_address; + self.users.write(user.id, user.clone()); + self.user_by_address.write(user_address, user.clone()); true } fn retrieve_user_profile(ref self: ContractState, user_id: u256) -> User { diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index a04f018..58dcaf2 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -909,6 +909,7 @@ fn test_batch_payout_creators() { start_cheat_caller_address(contract_address, creator_1); let creator1_id = dispatcher.register_user(username1, role.clone(), rank.clone(), metadata); + println!("successfully registered creator_1 as writer"); stop_cheat_caller_address(contract_address); start_cheat_caller_address(contract_address, creator_2); @@ -919,6 +920,12 @@ fn test_batch_payout_creators() { let creator3_id = dispatcher.register_user(username3, role.clone(), rank.clone(), metadata); stop_cheat_caller_address(contract_address); + start_cheat_caller_address(contract_address, admin_address); + let is_creator_1_verified = dispatcher.verify_user(creator1_id); + let is_creator_2_verified = dispatcher.verify_user(creator2_id); + let is_creator_3_verified = dispatcher.verify_user(creator3_id); + stop_cheat_caller_address(contract_address); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; let creator_1_init_bal = erc20_dispatcher.balance_of(creator_1); let creator_2_init_bal = erc20_dispatcher.balance_of(creator_2); @@ -934,12 +941,20 @@ fn test_batch_payout_creators() { let content_type: ContentType = ContentType::Text; let category: Category = Category::Education; + start_cheat_caller_address(contract_address, creator_1); let creator1_content_id: felt252 = dispatcher .register_content(title1, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); let creator2_content_id: felt252 = dispatcher .register_content(title2, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); let creator3_content_id: felt252 = dispatcher .register_content(title3, description, content_type, category); + stop_cheat_caller_address(contract_address); let price_1: u256 = 1000_u256; let price_2: u256 = 2000_u256; let price_3: u256 = 1500_u256; From fa15f82e2bdb061b7f8c53805838dd08a921a751 Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 15:01:37 +0100 Subject: [PATCH 11/17] .. --- src/chainlib/ChainLib.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index ddf52db..4464c41 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -253,6 +253,7 @@ pub mod ChainLib { self.token_address.write(token_address); // Initialize purchase ID counter self.next_purchase_id.write(1_u256); + self.next_content_id.write(1_felt252); self.purchase_timeout_duration.write(3600); self.platform_fee_recipient.write(admin); // self.platform_fee.write(PLATFORM_FEE); From 1d4af1151f86aa4b5a37148a2779c42462ae478d Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 17:32:11 +0100 Subject: [PATCH 12/17] .. --- src/chainlib/ChainLib.cairo | 41 +++++++++++++++++++++++++++++++++---- tests/test_ChainLib.cairo | 15 +++++++------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 4464c41..cdb2891 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -253,7 +253,7 @@ pub mod ChainLib { self.token_address.write(token_address); // Initialize purchase ID counter self.next_purchase_id.write(1_u256); - self.next_content_id.write(1_felt252); + self.next_content_id.write(0_felt252); self.purchase_timeout_duration.write(3600); self.platform_fee_recipient.write(admin); // self.platform_fee.write(PLATFORM_FEE); @@ -1862,7 +1862,9 @@ pub mod ChainLib { fn purchase_content( ref self: ContractState, content_id: felt252, transaction_hash: felt252, ) -> u256 { - assert!(content_id != 0, "Content ID cannot be empty"); + // assert!(content_id != 0, "Content ID cannot be empty"); + // I commented the above line out because in other parts of the project, it is + // explicitly stated that first content id should be 0 assert!(transaction_hash != 0, "Transaction hash cannot be empty"); let price = self.content_prices.read(content_id); @@ -1967,6 +1969,7 @@ pub mod ChainLib { fn verify_purchase(ref self: ContractState, purchase_id: u256) -> bool { // Get the purchase details let purchase = self.purchases.read(purchase_id); + // assert() let content = self.content.read(purchase.content_id); let total_content_sales = self.total_sales_for_content.read(purchase.content_id) + purchase.price; @@ -1981,8 +1984,11 @@ pub mod ChainLib { self.creator_sales.write(content.creator, total_creator_sales); self._single_payout(platform_fee_recipient, actual_platform_fee); + + let mut all_payouts = self.payout_history.entry(content.creator); + let creator_payout_history_id = self.payout_history.entry(content.creator).len(); - let creator_payout = Payout { + let mut creator_payout = Payout { id: creator_payout_history_id, purchase_id, recipient: content.creator, @@ -1990,7 +1996,34 @@ pub mod ChainLib { timestamp: get_block_timestamp(), status: PayoutStatus::PENDING, }; - self.payout_history.entry(content.creator).push(creator_payout); + + for i in 0..all_payouts.len() { + let current_payout = all_payouts.at(i).read(); + if current_payout.purchase_id == purchase_id { + creator_payout.id = current_payout.id; + creator_payout.amount = current_payout.amount; + if purchase.status == PurchaseStatus::Completed { + creator_payout.timestamp = current_payout.timestamp; + } else { + creator_payout.timestamp = get_block_timestamp(); + } + creator_payout.status = current_payout.status; + // I left out the timestamp so that the time starts counting from when true is + // returned from this function. In other words, when the verify purchase returns + // true, the content time in the hands of the user starts counting. This is for + // the refund window calculation. + } + } + + if creator_payout.id >= all_payouts.len() { + self.payout_history.entry(content.creator).push(creator_payout); + } else { + self + .payout_history + .entry(content.creator) + .at(creator_payout.id) + .write(creator_payout); + } self.total_sales_for_content.write(purchase.content_id, total_content_sales); self diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 58dcaf2..f4b5250 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -985,15 +985,16 @@ fn test_batch_payout_creators() { let purchase_id_3 = dispatcher.purchase_content(creator3_content_id, 'tx3'); // Initially, purchase should not be verified (status is Pending) - let is_1_verified = dispatcher.verify_purchase(purchase_id_1); - let is_2_verified = dispatcher.verify_purchase(purchase_id_2); - let is_3_verified = dispatcher.verify_purchase(purchase_id_3); - assert(!is_1_verified, '1 should not be verified'); - assert(!is_2_verified, '2 should not be verified'); - assert(!is_3_verified, '3 should not be verified'); + let is_purchase_1_verified = dispatcher.verify_purchase(purchase_id_1); + let is_purchase_2_verified = dispatcher.verify_purchase(purchase_id_2); + let is_purchase_3_verified = dispatcher.verify_purchase(purchase_id_3); + assert(!is_purchase_1_verified, '1 should not be verified'); + assert(!is_purchase_2_verified, '2 should not be verified'); + assert(!is_purchase_3_verified, '3 should not be verified'); // Set admin as caller to update the purchase status cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + cheat_block_timestamp(contract_address, 0, CheatSpan::Indefinite); // Update purchase status to Completed let update_result_1 = dispatcher @@ -1019,7 +1020,7 @@ fn test_batch_payout_creators() { assert(receipt.purchase_id == purchase_id_1, 'receipt error'); // Still admin calling - + cheat_block_timestamp(contract_address, 86400 * 8, CheatSpan::Indefinite); // start_cheat_block_timestamp(contract_address); dispatcher.batch_payout_creators(); From d8cf2095edcfb62d7da18b93687ea37b70777b3a Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 17:37:45 +0100 Subject: [PATCH 13/17] .. --- src/chainlib/ChainLib.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index cdb2891..1bdc0a0 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -255,7 +255,7 @@ pub mod ChainLib { self.next_purchase_id.write(1_u256); self.next_content_id.write(0_felt252); self.purchase_timeout_duration.write(3600); - self.platform_fee_recipient.write(admin); + self.platform_fee_recipient.write(get_contract_address()); // self.platform_fee.write(PLATFORM_FEE); self.platform_fee.write(platform_fee); let payout_schedule = PayoutSchedule { From c4c6b56110e90c1e100fcf4622702cd65be6803c Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 19:05:35 +0100 Subject: [PATCH 14/17] .. --- src/chainlib/ChainLib.cairo | 100 ++++--- tests/test_ChainLib.cairo | 504 +++++++++++++++++++++++++++++++++++- 2 files changed, 565 insertions(+), 39 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 1bdc0a0..2e8aa24 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -306,6 +306,7 @@ pub mod ChainLib { RefundPaid: RefundPaid, PlatformFeeChanged: PlatformFeeChanged, RefundWindowChanged: RefundWindowChanged, + RefundTimedOut: RefundTimedOut, } #[derive(Drop, starknet::Event)] @@ -517,6 +518,13 @@ pub mod ChainLib { pub refund_id: u64, } + #[derive(Drop, starknet::Event)] + pub struct RefundTimedOut { + pub user_id: u256, + pub content_id: felt252, + pub refund_id: u64, + } + #[derive(Drop, starknet::Event)] pub struct RefundPaid { pub refund_id: u64, @@ -2322,6 +2330,11 @@ pub mod ChainLib { }; self.user_refunds.entry(caller).push(refund_request); let content_id = self.purchases.read(purchase_id).content_id; + let purchased_at = self.purchases.read(purchase_id).timestamp; + assert( + (get_block_timestamp() - purchased_at) < self.refund_window.read(), + 'Refund window already closed', + ); self .emit( RefundRequested { @@ -2339,6 +2352,7 @@ pub mod ChainLib { let user = self.users.read(user_id); let user_address = user.wallet_address; let mut refund = self.user_refunds.entry(user_address).at(refund_id).read(); + assert(refund.status != RefundStatus::TIMED_OUT, 'Request already timed out'); assert(refund.status == RefundStatus::PENDING, 'Request already processed'); let mut refund_amount = 0; @@ -2355,44 +2369,49 @@ pub mod ChainLib { let time_since_request = get_block_timestamp() - request_timestamp; if time_since_request >= self.refund_window.read() { refund.status = RefundStatus::TIMED_OUT; + let purchase_id = refund.purchase_id; + let purchase = self.purchases.read(purchase_id); + let content_id = purchase.content_id; + // let content = self.content.read(content_id); + self.user_refunds.entry(user_address).at(refund_id).write(refund); + self.emit(RefundTimedOut { user_id, content_id, refund_id }) } else { refund.status = RefundStatus::APPROVED; - } - - // This should affect the creator payout for that purchase - - let purchase_id = refund.purchase_id; - let purchase = self.purchases.read(purchase_id); - let content_id = purchase.content_id; - let content = self.content.read(content_id); - let content_creator = content.creator; - - let creator_payout_vec = self.payout_history.entry(content_creator); - let mut dummy_payout_array = array![]; - // Dummy array that will help retrieve the payout we need to edit - for i in 0..creator_payout_vec.len() { - let specific_payout = creator_payout_vec.at(i).read(); - if specific_payout.purchase_id == purchase_id { - dummy_payout_array.append(specific_payout); + // This should affect the creator payout for that purchase + + let purchase_id = refund.purchase_id; + let purchase = self.purchases.read(purchase_id); + let content_id = purchase.content_id; + let content = self.content.read(content_id); + let content_creator = content.creator; + + let creator_payout_vec = self.payout_history.entry(content_creator); + let mut dummy_payout_array = array![]; + // Dummy array that will help retrieve the payout we need to edit + for i in 0..creator_payout_vec.len() { + let specific_payout = creator_payout_vec.at(i).read(); + if specific_payout.purchase_id == purchase_id { + dummy_payout_array.append(specific_payout); + } + } + // Retrieve the payout, the length will be one, but we can assert too + assert(dummy_payout_array.len() == 1, 'Double Payout-purchase entry'); + let mut specific_payout = *dummy_payout_array.at(0); + + let refund_amount = refund_percent * specific_payout.amount / 100; + specific_payout.amount -= refund_amount; + if refund_amount == specific_payout.amount { + specific_payout.status = PayoutStatus::CANCELLED; } - } - // Retrieve the payout, the length will be one, but we can assert too - assert(dummy_payout_array.len() == 1, 'Double Payout-purchase entry'); - let mut specific_payout = *dummy_payout_array.at(0); - - let refund_amount = refund_percent * specific_payout.amount / 100; - specific_payout.amount -= refund_amount; - if refund_amount == specific_payout.amount { - specific_payout.status = PayoutStatus::CANCELLED; - } - self - .payout_history - .entry(content_creator) - .at(specific_payout.id) - .write(specific_payout); - self.user_refunds.entry(user_address).at(refund_id).write(refund); - self.emit(RefundApproved { approver: caller, user_id, content_id, refund_id }); + self + .payout_history + .entry(content_creator) + .at(specific_payout.id) + .write(specific_payout); + self.user_refunds.entry(user_address).at(refund_id).write(refund); + self.emit(RefundApproved { approver: caller, user_id, content_id, refund_id }); + } } fn decline_refund(ref self: ContractState, refund_id: u64, user_id: u256) { @@ -2409,12 +2428,17 @@ pub mod ChainLib { let time_since_request = get_block_timestamp() - request_timestamp; if time_since_request >= self.refund_window.read() { refund.status = RefundStatus::TIMED_OUT; + let purchase_id = refund.purchase_id; + let purchase = self.purchases.read(purchase_id); + let content_id = purchase.content_id; + // let content = self.content.read(content_id); + self.user_refunds.entry(user_address).at(refund_id).write(refund); + self.emit(RefundTimedOut { user_id, content_id, refund_id }) } else { refund.status = RefundStatus::DECLINED; + self.emit(RefundDeclined { decliner: caller, user_id, content_id, refund_id }); + self.user_refunds.entry(user_address).at(refund_id).write(refund); } - self.user_refunds.entry(user_address).at(refund_id).write(refund); - - self.emit(RefundDeclined { decliner: caller, user_id, content_id, refund_id }); } fn refund_user(ref self: ContractState, refund_id: u64, user_id: u256) { @@ -2424,7 +2448,7 @@ pub mod ChainLib { let user = self.users.read(user_id); let user_address = user.wallet_address; let mut refund = self.user_refunds.entry(user_address).at(refund_id).read(); - assert(refund.status == RefundStatus::APPROVED, 'Request already processed'); + assert(refund.status == RefundStatus::APPROVED, 'Refund request declined'); refund.status = RefundStatus::PAID; self.user_refunds.entry(user_address).at(refund_id).write(refund); let purchase_id = refund.purchase_id; diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index f4b5250..78bf45d 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -1,5 +1,7 @@ // Import the contract modules -use chain_lib::base::types::{PurchaseStatus, Rank, Role, Status}; +use chain_lib::base::types::{ + PurchaseStatus, Rank, Refund, RefundRequestReason, RefundStatus, Role, Status, +}; use chain_lib::chainlib::ChainLib; use chain_lib::chainlib::ChainLib::ChainLib::{Category, ContentType, PlanType, SubscriptionStatus}; use chain_lib::interfaces::IChainLib::{IChainLib, IChainLibDispatcher, IChainLibDispatcherTrait}; @@ -1032,3 +1034,503 @@ fn test_batch_payout_creators() { assert(creator_2_new_bal > creator_2_init_bal, 'Failed to credit creator2'); assert(creator_3_new_bal > creator_3_init_bal, 'Failed to credit creator3'); } + +#[test] +fn test_refund_flow_approve_refund_request() { + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + let user_address = contract_address_const::<'user'>(); + let creator_1 = contract_address_const::<'creator_1'>(); + let creator_2 = contract_address_const::<'creator_2'>(); + let creator_3 = contract_address_const::<'creator_3'>(); + + let username1: felt252 = 'creator_1'; + let username2: felt252 = 'creator_2'; + let username3: felt252 = 'creator_3'; + let user_name: felt252 = 'user'; + + let role: Role = Role::WRITER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'john is a boy'; + + start_cheat_caller_address(contract_address, user_address); + let user_id = dispatcher.register_user(user_name, Role::READER, Rank::BEGINNER, metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_1); + let creator1_id = dispatcher.register_user(username1, role.clone(), rank.clone(), metadata); + println!("successfully registered creator_1 as writer"); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); + let creator2_id = dispatcher.register_user(username2, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); + let creator3_id = dispatcher.register_user(username3, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, admin_address); + let is_creator_1_verified = dispatcher.verify_user(creator1_id); + let is_creator_2_verified = dispatcher.verify_user(creator2_id); + let is_creator_3_verified = dispatcher.verify_user(creator3_id); + stop_cheat_caller_address(contract_address); + + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let creator_1_init_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_init_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_init_bal = erc20_dispatcher.balance_of(creator_3); + + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); + // Set up test data + let title1: felt252 = 'Creator 1 content'; + let title2: felt252 = 'Creator 2 content'; + let title3: felt252 = 'Creator 3 content'; + + let description: felt252 = 'This is a test content'; + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Education; + + start_cheat_caller_address(contract_address, creator_1); + let creator1_content_id: felt252 = dispatcher + .register_content(title1, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); + let creator2_content_id: felt252 = dispatcher + .register_content(title2, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); + let creator3_content_id: felt252 = dispatcher + .register_content(title3, description, content_type, category); + stop_cheat_caller_address(contract_address); + let price_1: u256 = 1000_u256; + let price_2: u256 = 2000_u256; + let price_3: u256 = 1500_u256; + + // Set creator_1 as caller to set up content price + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator1_content_id, price_1, + ); + + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator2_content_id, price_2, + ); + + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator3_content_id, price_3, + ); + + // Set user as content consumer + cheat_caller_address(contract_address, user_address, CheatSpan::Indefinite); + + // Purchase the content + let purchase_id_1 = dispatcher.purchase_content(creator1_content_id, 'tx1'); + let purchase_id_2 = dispatcher.purchase_content(creator2_content_id, 'tx2'); + let purchase_id_3 = dispatcher.purchase_content(creator3_content_id, 'tx3'); + + // Initially, purchase should not be verified (status is Pending) + let is_purchase_1_verified = dispatcher.verify_purchase(purchase_id_1); + let is_purchase_2_verified = dispatcher.verify_purchase(purchase_id_2); + let is_purchase_3_verified = dispatcher.verify_purchase(purchase_id_3); + assert(!is_purchase_1_verified, '1 should not be verified'); + assert(!is_purchase_2_verified, '2 should not be verified'); + assert(!is_purchase_3_verified, '3 should not be verified'); + + // Set admin as caller to update the purchase status + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + cheat_block_timestamp(contract_address, 0, CheatSpan::Indefinite); + + // Update purchase status to Completed + let update_result_1 = dispatcher + .update_purchase_status(purchase_id_1, PurchaseStatus::Completed); + assert(update_result_1, 'Failed to update status1'); + let update_result_2 = dispatcher + .update_purchase_status(purchase_id_2, PurchaseStatus::Completed); + assert(update_result_2, 'Failed to update status1'); + let update_result_3 = dispatcher + .update_purchase_status(purchase_id_3, PurchaseStatus::Completed); + assert(update_result_3, 'Failed to update status1'); + + // Now the purchase should be verified + let is_now_verified1 = dispatcher.verify_purchase(purchase_id_1); + assert(is_now_verified1, 'Purchase should be verified'); + let is_now_verified2 = dispatcher.verify_purchase(purchase_id_2); + assert(is_now_verified2, 'Purchase should be verified'); + let is_now_verified3 = dispatcher.verify_purchase(purchase_id_3); + assert(is_now_verified3, 'Purchase should be verified'); + + let receipt = dispatcher.get_receipt(1); + + assert(receipt.purchase_id == purchase_id_1, 'receipt error'); + + start_cheat_caller_address(contract_address, user_address); + dispatcher.request_refund(purchase_id_1, RefundRequestReason::MISREPRESENTED_CONTENT); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, admin_address); + let refunds_array = dispatcher.get_user_refunds(user_id); + let refund_request = refunds_array.at(0); + assert(*refund_request.status == RefundStatus::PENDING, 'Wrong refund status'); + + dispatcher.approve_refund(*refund_request.refund_id, user_id, Option::None); + let new_refunds_array = dispatcher.get_user_refunds(user_id); + let new_refund_request = new_refunds_array.at(0); + assert(*new_refund_request.status == RefundStatus::APPROVED, 'Failed to change refund status'); + + dispatcher.refund_user(*refund_request.refund_id, user_id); + + let paid_refunds_array = dispatcher.get_user_refunds(user_id); + let paid_refund = paid_refunds_array.at(0); + assert(*paid_refund.status == RefundStatus::PAID, 'Failed to change refund status'); + + // Still admin calling + cheat_block_timestamp(contract_address, 86400 * 8, CheatSpan::Indefinite); + // start_cheat_block_timestamp(contract_address); + dispatcher.batch_payout_creators(); + + let creator_1_new_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_new_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_new_bal = erc20_dispatcher.balance_of(creator_3); + + assert(creator_1_new_bal > creator_1_init_bal, 'Failed to credit creator1'); + assert(creator_2_new_bal > creator_2_init_bal, 'Failed to credit creator2'); + assert(creator_3_new_bal > creator_3_init_bal, 'Failed to credit creator3'); +} + +#[test] +#[should_panic(expected: 'Refund request declined')] +fn test_refund_flow_decline_refund_request() { + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + let user_address = contract_address_const::<'user'>(); + let creator_1 = contract_address_const::<'creator_1'>(); + let creator_2 = contract_address_const::<'creator_2'>(); + let creator_3 = contract_address_const::<'creator_3'>(); + + let username1: felt252 = 'creator_1'; + let username2: felt252 = 'creator_2'; + let username3: felt252 = 'creator_3'; + let user_name: felt252 = 'user'; + + let role: Role = Role::WRITER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'john is a boy'; + + start_cheat_caller_address(contract_address, user_address); + let user_id = dispatcher.register_user(user_name, Role::READER, Rank::BEGINNER, metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_1); + let creator1_id = dispatcher.register_user(username1, role.clone(), rank.clone(), metadata); + println!("successfully registered creator_1 as writer"); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); + let creator2_id = dispatcher.register_user(username2, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); + let creator3_id = dispatcher.register_user(username3, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, admin_address); + let is_creator_1_verified = dispatcher.verify_user(creator1_id); + let is_creator_2_verified = dispatcher.verify_user(creator2_id); + let is_creator_3_verified = dispatcher.verify_user(creator3_id); + stop_cheat_caller_address(contract_address); + + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let creator_1_init_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_init_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_init_bal = erc20_dispatcher.balance_of(creator_3); + + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); + // Set up test data + let title1: felt252 = 'Creator 1 content'; + let title2: felt252 = 'Creator 2 content'; + let title3: felt252 = 'Creator 3 content'; + + let description: felt252 = 'This is a test content'; + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Education; + + start_cheat_caller_address(contract_address, creator_1); + let creator1_content_id: felt252 = dispatcher + .register_content(title1, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); + let creator2_content_id: felt252 = dispatcher + .register_content(title2, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); + let creator3_content_id: felt252 = dispatcher + .register_content(title3, description, content_type, category); + stop_cheat_caller_address(contract_address); + let price_1: u256 = 1000_u256; + let price_2: u256 = 2000_u256; + let price_3: u256 = 1500_u256; + + // Set creator_1 as caller to set up content price + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator1_content_id, price_1, + ); + + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator2_content_id, price_2, + ); + + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator3_content_id, price_3, + ); + + // Set user as content consumer + cheat_caller_address(contract_address, user_address, CheatSpan::Indefinite); + + // Purchase the content + let purchase_id_1 = dispatcher.purchase_content(creator1_content_id, 'tx1'); + let purchase_id_2 = dispatcher.purchase_content(creator2_content_id, 'tx2'); + let purchase_id_3 = dispatcher.purchase_content(creator3_content_id, 'tx3'); + + // Initially, purchase should not be verified (status is Pending) + let is_purchase_1_verified = dispatcher.verify_purchase(purchase_id_1); + let is_purchase_2_verified = dispatcher.verify_purchase(purchase_id_2); + let is_purchase_3_verified = dispatcher.verify_purchase(purchase_id_3); + assert(!is_purchase_1_verified, '1 should not be verified'); + assert(!is_purchase_2_verified, '2 should not be verified'); + assert(!is_purchase_3_verified, '3 should not be verified'); + + // Set admin as caller to update the purchase status + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + cheat_block_timestamp(contract_address, 0, CheatSpan::Indefinite); + + // Update purchase status to Completed + let update_result_1 = dispatcher + .update_purchase_status(purchase_id_1, PurchaseStatus::Completed); + assert(update_result_1, 'Failed to update status1'); + let update_result_2 = dispatcher + .update_purchase_status(purchase_id_2, PurchaseStatus::Completed); + assert(update_result_2, 'Failed to update status1'); + let update_result_3 = dispatcher + .update_purchase_status(purchase_id_3, PurchaseStatus::Completed); + assert(update_result_3, 'Failed to update status1'); + + // Now the purchase should be verified + let is_now_verified1 = dispatcher.verify_purchase(purchase_id_1); + assert(is_now_verified1, 'Purchase should be verified'); + let is_now_verified2 = dispatcher.verify_purchase(purchase_id_2); + assert(is_now_verified2, 'Purchase should be verified'); + let is_now_verified3 = dispatcher.verify_purchase(purchase_id_3); + assert(is_now_verified3, 'Purchase should be verified'); + + let receipt = dispatcher.get_receipt(1); + + assert(receipt.purchase_id == purchase_id_1, 'receipt error'); + + start_cheat_caller_address(contract_address, user_address); + dispatcher.request_refund(purchase_id_1, RefundRequestReason::MISREPRESENTED_CONTENT); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, admin_address); + let refunds_array = dispatcher.get_user_refunds(user_id); + let refund_request = refunds_array.at(0); + + dispatcher.decline_refund(*refund_request.refund_id, user_id); + + let new_refunds_array = dispatcher.get_user_refunds(user_id); + let new_refund_request = new_refunds_array.at(0); + assert(*new_refund_request.status == RefundStatus::DECLINED, 'Failed to change refund status'); + + dispatcher.refund_user(*refund_request.refund_id, user_id); + + // Still admin calling + cheat_block_timestamp(contract_address, 86400 * 8, CheatSpan::Indefinite); + // start_cheat_block_timestamp(contract_address); + dispatcher.batch_payout_creators(); + + let creator_1_new_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_new_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_new_bal = erc20_dispatcher.balance_of(creator_3); + + assert(creator_1_new_bal > creator_1_init_bal, 'Failed to credit creator1'); + assert(creator_2_new_bal > creator_2_init_bal, 'Failed to credit creator2'); + assert(creator_3_new_bal > creator_3_init_bal, 'Failed to credit creator3'); +} + +#[test] +#[should_panic(expected: 'Refund window already closed')] +fn test_refund_flow_refund_request_timed_out() { + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + let user_address = contract_address_const::<'user'>(); + let creator_1 = contract_address_const::<'creator_1'>(); + let creator_2 = contract_address_const::<'creator_2'>(); + let creator_3 = contract_address_const::<'creator_3'>(); + + let username1: felt252 = 'creator_1'; + let username2: felt252 = 'creator_2'; + let username3: felt252 = 'creator_3'; + let user_name: felt252 = 'user'; + + let role: Role = Role::WRITER; + let rank: Rank = Rank::BEGINNER; + let metadata: felt252 = 'john is a boy'; + + start_cheat_caller_address(contract_address, user_address); + let user_id = dispatcher.register_user(user_name, Role::READER, Rank::BEGINNER, metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_1); + let creator1_id = dispatcher.register_user(username1, role.clone(), rank.clone(), metadata); + println!("successfully registered creator_1 as writer"); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); + let creator2_id = dispatcher.register_user(username2, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); + let creator3_id = dispatcher.register_user(username3, role.clone(), rank.clone(), metadata); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, admin_address); + let is_creator_1_verified = dispatcher.verify_user(creator1_id); + let is_creator_2_verified = dispatcher.verify_user(creator2_id); + let is_creator_3_verified = dispatcher.verify_user(creator3_id); + stop_cheat_caller_address(contract_address); + + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let creator_1_init_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_init_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_init_bal = erc20_dispatcher.balance_of(creator_3); + + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); + // Set up test data + let title1: felt252 = 'Creator 1 content'; + let title2: felt252 = 'Creator 2 content'; + let title3: felt252 = 'Creator 3 content'; + + let description: felt252 = 'This is a test content'; + let content_type: ContentType = ContentType::Text; + let category: Category = Category::Education; + + start_cheat_caller_address(contract_address, creator_1); + let creator1_content_id: felt252 = dispatcher + .register_content(title1, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_2); + let creator2_content_id: felt252 = dispatcher + .register_content(title2, description, content_type, category); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, creator_3); + let creator3_content_id: felt252 = dispatcher + .register_content(title3, description, content_type, category); + stop_cheat_caller_address(contract_address); + let price_1: u256 = 1000_u256; + let price_2: u256 = 2000_u256; + let price_3: u256 = 1500_u256; + + // Set creator_1 as caller to set up content price + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator1_content_id, price_1, + ); + + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator2_content_id, price_2, + ); + + // Set up content with price + setup_content_with_price( + dispatcher, admin_address, contract_address, creator3_content_id, price_3, + ); + + // Set user as content consumer + cheat_caller_address(contract_address, user_address, CheatSpan::Indefinite); + + // Purchase the content + let purchase_id_1 = dispatcher.purchase_content(creator1_content_id, 'tx1'); + let purchase_id_2 = dispatcher.purchase_content(creator2_content_id, 'tx2'); + let purchase_id_3 = dispatcher.purchase_content(creator3_content_id, 'tx3'); + + // Initially, purchase should not be verified (status is Pending) + let is_purchase_1_verified = dispatcher.verify_purchase(purchase_id_1); + let is_purchase_2_verified = dispatcher.verify_purchase(purchase_id_2); + let is_purchase_3_verified = dispatcher.verify_purchase(purchase_id_3); + assert(!is_purchase_1_verified, '1 should not be verified'); + assert(!is_purchase_2_verified, '2 should not be verified'); + assert(!is_purchase_3_verified, '3 should not be verified'); + + // Set admin as caller to update the purchase status + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + cheat_block_timestamp(contract_address, 0, CheatSpan::Indefinite); + + // Update purchase status to Completed + let update_result_1 = dispatcher + .update_purchase_status(purchase_id_1, PurchaseStatus::Completed); + assert(update_result_1, 'Failed to update status1'); + let update_result_2 = dispatcher + .update_purchase_status(purchase_id_2, PurchaseStatus::Completed); + assert(update_result_2, 'Failed to update status1'); + let update_result_3 = dispatcher + .update_purchase_status(purchase_id_3, PurchaseStatus::Completed); + assert(update_result_3, 'Failed to update status1'); + + // Now the purchase should be verified + let is_now_verified1 = dispatcher.verify_purchase(purchase_id_1); + assert(is_now_verified1, 'Purchase should be verified'); + let is_now_verified2 = dispatcher.verify_purchase(purchase_id_2); + assert(is_now_verified2, 'Purchase should be verified'); + let is_now_verified3 = dispatcher.verify_purchase(purchase_id_3); + assert(is_now_verified3, 'Purchase should be verified'); + + let receipt = dispatcher.get_receipt(1); + + assert(receipt.purchase_id == purchase_id_1, 'receipt error'); + + start_cheat_caller_address(contract_address, user_address); + start_cheat_block_timestamp(contract_address, 86400 * 3); + dispatcher.request_refund(purchase_id_1, RefundRequestReason::MISREPRESENTED_CONTENT); + stop_cheat_block_timestamp(contract_address); + stop_cheat_caller_address(contract_address); + + start_cheat_caller_address(contract_address, admin_address); + let refunds_array = dispatcher.get_user_refunds(user_id); + let refund_request = refunds_array.at(0); + + dispatcher.decline_refund(*refund_request.refund_id, user_id); + + let new_refunds_array = dispatcher.get_user_refunds(user_id); + let new_refund_request = new_refunds_array.at(0); + assert(*new_refund_request.status == RefundStatus::DECLINED, 'Failed to change refund status'); + + dispatcher.refund_user(*refund_request.refund_id, user_id); + + // Still admin calling + cheat_block_timestamp(contract_address, 86400 * 8, CheatSpan::Indefinite); + // start_cheat_block_timestamp(contract_address); + dispatcher.batch_payout_creators(); + + let creator_1_new_bal = erc20_dispatcher.balance_of(creator_1); + let creator_2_new_bal = erc20_dispatcher.balance_of(creator_2); + let creator_3_new_bal = erc20_dispatcher.balance_of(creator_3); + + assert(creator_1_new_bal > creator_1_init_bal, 'Failed to credit creator1'); + assert(creator_2_new_bal > creator_2_init_bal, 'Failed to credit creator2'); + assert(creator_3_new_bal > creator_3_init_bal, 'Failed to credit creator3'); +} From 4a59ce21db85199584720b03b4798a57e0349aec Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 19:31:33 +0100 Subject: [PATCH 15/17] should all pass now --- src/chainlib/ChainLib.cairo | 12 ++++++------ src/interfaces/IChainLib.cairo | 2 +- tests/test_ChainLib.cairo | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 2e8aa24..756c1ba 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -2343,8 +2343,9 @@ pub mod ChainLib { ); } + // The refund_percentage will only be used if the reason is OTHER fn approve_refund( - ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: Option, + ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: u256, ) { let caller = get_caller_address(); // Ensure that only an admin can verify users. @@ -2358,10 +2359,9 @@ pub mod ChainLib { let mut refund_amount = 0; let refund_reason = refund.reason; - let mut refund_percent = self.get_refund_percentage(refund_reason); + let mut refund_percent = self._get_refund_percentage(refund_reason); if refund_percent == 0 { - assert(refund_percentage.is_some(), 'Choose custom percentage'); - refund_percent = refund_percentage.unwrap(); + refund_percent = refund_percentage; } // We'll take the refund percent later in the contract from the creator payout @@ -2587,7 +2587,7 @@ pub mod ChainLib { token.transfer(refund_address, amount); } - fn get_refund_percentage( + fn _get_refund_percentage( ref self: ContractState, refund_reason: RefundRequestReason, ) -> u256 { match refund_reason { @@ -2595,7 +2595,7 @@ pub mod ChainLib { RefundRequestReason::DUPLICATE_PURCHASE => 80, RefundRequestReason::UNABLE_TO_ACCESS => 100, RefundRequestReason::MISREPRESENTED_CONTENT => 65, - _ => 0, + RefundRequestReason::OTHER => 0, } } } diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index 5b89feb..f7a33cf 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -237,7 +237,7 @@ pub trait IChainLib { ref self: TContractState, purchase_id: u256, refund_reason: RefundRequestReason, ); fn approve_refund( - ref self: TContractState, refund_id: u64, user_id: u256, refund_percentage: Option, + ref self: TContractState, refund_id: u64, user_id: u256, refund_percentage: u256, ); fn decline_refund(ref self: TContractState, refund_id: u64, user_id: u256); fn refund_user(ref self: TContractState, refund_id: u64, user_id: u256); diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 78bf45d..009f9cd 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -1178,7 +1178,7 @@ fn test_refund_flow_approve_refund_request() { let refund_request = refunds_array.at(0); assert(*refund_request.status == RefundStatus::PENDING, 'Wrong refund status'); - dispatcher.approve_refund(*refund_request.refund_id, user_id, Option::None); + dispatcher.approve_refund(*refund_request.refund_id, user_id, 20); let new_refunds_array = dispatcher.get_user_refunds(user_id); let new_refund_request = new_refunds_array.at(0); assert(*new_refund_request.status == RefundStatus::APPROVED, 'Failed to change refund status'); From 1849ce7328704048e3c8995b03fdcf1e00009dae Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 19:43:59 +0100 Subject: [PATCH 16/17] should pass now --- src/chainlib/ChainLib.cairo | 9 +++++---- src/interfaces/IChainLib.cairo | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 756c1ba..a62b810 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -2345,7 +2345,7 @@ pub mod ChainLib { // The refund_percentage will only be used if the reason is OTHER fn approve_refund( - ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: u256, + ref self: ContractState, refund_id: u64, user_id: u256, refund_percentage: Option, ) { let caller = get_caller_address(); // Ensure that only an admin can verify users. @@ -2356,12 +2356,11 @@ pub mod ChainLib { assert(refund.status != RefundStatus::TIMED_OUT, 'Request already timed out'); assert(refund.status == RefundStatus::PENDING, 'Request already processed'); - let mut refund_amount = 0; - let refund_reason = refund.reason; let mut refund_percent = self._get_refund_percentage(refund_reason); if refund_percent == 0 { - refund_percent = refund_percentage; + assert(refund_percentage.is_some(), 'Custom percentage for other'); + refund_percent = refund_percentage.unwrap(); } // We'll take the refund percent later in the contract from the creator payout @@ -2403,6 +2402,7 @@ pub mod ChainLib { if refund_amount == specific_payout.amount { specific_payout.status = PayoutStatus::CANCELLED; } + refund.refund_amount = Option::Some(refund_amount); self .payout_history @@ -2453,6 +2453,7 @@ pub mod ChainLib { self.user_refunds.entry(user_address).at(refund_id).write(refund); let purchase_id = refund.purchase_id; let content_id = self.purchases.read(purchase_id).content_id; + assert(refund.refund_amount.is_some(), 'Refund amount is none'); let refund_amount = refund.refund_amount.unwrap(); self._process_refund(refund_amount, user_address); diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index f7a33cf..5b89feb 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -237,7 +237,7 @@ pub trait IChainLib { ref self: TContractState, purchase_id: u256, refund_reason: RefundRequestReason, ); fn approve_refund( - ref self: TContractState, refund_id: u64, user_id: u256, refund_percentage: u256, + ref self: TContractState, refund_id: u64, user_id: u256, refund_percentage: Option, ); fn decline_refund(ref self: TContractState, refund_id: u64, user_id: u256); fn refund_user(ref self: TContractState, refund_id: u64, user_id: u256); From c13dacc213a22ef27495b3e64542a2792b655dee Mon Sep 17 00:00:00 2001 From: OWK50GA Date: Tue, 8 Jul 2025 19:46:51 +0100 Subject: [PATCH 17/17] .. --- tests/test_ChainLib.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 009f9cd..78bf45d 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -1178,7 +1178,7 @@ fn test_refund_flow_approve_refund_request() { let refund_request = refunds_array.at(0); assert(*refund_request.status == RefundStatus::PENDING, 'Wrong refund status'); - dispatcher.approve_refund(*refund_request.refund_id, user_id, 20); + dispatcher.approve_refund(*refund_request.refund_id, user_id, Option::None); let new_refunds_array = dispatcher.get_user_refunds(user_id); let new_refund_request = new_refunds_array.at(0); assert(*new_refund_request.status == RefundStatus::APPROVED, 'Failed to change refund status');