From 04af8b954724aeddb1af8f5ecc45bf7a5d55abbe Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Mon, 9 Jun 2025 09:48:54 +0100 Subject: [PATCH 1/4] feat: update dependencies and enhance subscription management with plan types --- .tool-versions | 4 +- Scarb.lock | 8 +-- Scarb.toml | 6 +- src/chainlib/ChainLib.cairo | 112 ++++++++++++++++++++++++++------- src/interfaces/IChainLib.cairo | 8 ++- tests/test_ChainLib.cairo | 11 ++-- tests/test_subscription.cairo | 2 +- 7 files changed, 112 insertions(+), 39 deletions(-) diff --git a/.tool-versions b/.tool-versions index da2d94b..28c03e7 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -scarb 2.9.2 -starknet-foundry 0.39.0 \ No newline at end of file +scarb 2.11.4 +starknet-foundry 0.40.0 \ No newline at end of file diff --git a/Scarb.lock b/Scarb.lock index 81d4b59..d2649db 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -10,13 +10,13 @@ dependencies = [ [[package]] name = "snforge_scarb_plugin" -version = "0.32.0" -source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.32.0#3817c903b640201c72e743b9bbe70a97149828a2" +version = "0.40.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.40.0#c20f7692c1a2936327d86b7e2c89c5462f8afdcd" [[package]] name = "snforge_std" -version = "0.32.0" -source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.32.0#3817c903b640201c72e743b9bbe70a97149828a2" +version = "0.40.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.40.0#c20f7692c1a2936327d86b7e2c89c5462f8afdcd" dependencies = [ "snforge_scarb_plugin", ] diff --git a/Scarb.toml b/Scarb.toml index 6203031..b59ea02 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -6,11 +6,11 @@ edition = "2023_11" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -starknet = "2.8.4" +starknet = "2.11.2" [dev-dependencies] -snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.32.0" } -assert_macros = "2.8.4" +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.40.0" } +assert_macros = "2.11.2" [[target.starknet-contract]] sierra = true diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 7e9d65a..7e99744 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -4,8 +4,8 @@ pub mod ChainLib { use core::option::OptionTrait; use core::traits::Into; use starknet::storage::{ - Map, MutableVecTrait, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, - StoragePointerWriteAccess, Vec, VecTrait, + Map, MutableVecTrait, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, + StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, }; use starknet::{ ContractAddress, contract_address_const, get_block_timestamp, get_caller_address, @@ -68,7 +68,7 @@ pub mod ChainLib { pub category: Category, } - #[derive(Copy, Drop, Serde, starknet::Store, Debug)] + #[derive(Drop, Serde, starknet::Store, Clone)] pub struct Subscription { pub id: u256, pub subscriber: ContractAddress, @@ -78,6 +78,15 @@ pub mod ChainLib { pub end_date: u64, pub is_active: bool, pub last_payment_date: u64, + pub subscription_type: PlanType, + } + + #[derive(Drop, Serde, starknet::Store, Clone, PartialEq)] + pub enum PlanType { + #[default] + MONTHLY, + YEARLY, + TRIAL, } #[derive(Copy, Drop, Serde, starknet::Store, Debug)] @@ -190,6 +199,10 @@ pub mod ChainLib { next_purchase_id: u256, // Counter for purchase IDs purchases: Map, // Maps purchase ID to Purchase purchase_timeout_duration: u64, + subscription_record: Map>, // subcription id to subscription record + subscription_count: Map< + u256, u256, + > // subscriber count to number of times the subscription record has been updated } @@ -710,6 +723,7 @@ pub mod ChainLib { content_metadata } + /// @notice Processes the initial payment for a new subscription /// @param amount The amount to be charged for the initial payment /// @param subscriber The address of the subscriber @@ -725,6 +739,8 @@ pub mod ChainLib { // Create a new subscription let subscription_id = self.subscription_id.read(); + let subscription_plan: Subscription = self.subscriptions.read(subscription_id); + let current_time = get_block_timestamp(); // Default subscription period is 30 days (in seconds) @@ -739,10 +755,16 @@ pub mod ChainLib { end_date: current_time + subscription_period, is_active: true, last_payment_date: current_time, + subscription_type: subscription_plan.subscription_type, }; // Store the subscription - self.subscriptions.write(subscription_id, new_subscription); + self.subscriptions.write(subscription_id, new_subscription.clone()); + + self.subscription_record.entry(subscription_id).append().write(new_subscription); + + let current_count = self.subscription_count.read(subscription_id); + self.subscription_count.write(subscription_id, current_count + 1); // Create and store the payment record let payment_id = self.payment_id.read(); @@ -805,7 +827,7 @@ pub mod ChainLib { let mut subscription = self.subscriptions.read(subscription_id); // Verify subscription exists and is active - assert(subscription.id == subscription_id, 'Subscription not found'); + assert(subscription.id.clone() == subscription_id, 'Subscription not found'); assert(subscription.is_active, 'Subscription not active'); // Check if it's time for a recurring payment @@ -819,19 +841,20 @@ pub mod ChainLib { // Default subscription period is 30 days (in seconds) let subscription_period: u64 = 30 * 24 * 60 * 60; + // Save required fields before mutating subscription + let subscription_amount = subscription.amount; + let subscription_subscriber = subscription.subscriber; + // Update subscription details subscription.last_payment_date = current_time; subscription.end_date = current_time + subscription_period; - // Store updated subscription - self.subscriptions.write(subscription_id, subscription); - // Create and store the payment record let payment_id = self.payment_id.read(); let new_payment = Payment { id: payment_id, subscription_id: subscription_id, - amount: subscription.amount, + amount: subscription_amount, timestamp: current_time, is_verified: true, // Auto-verify for simplicity is_refunded: false, @@ -852,15 +875,14 @@ pub mod ChainLib { // Increment payment ID for next use self.payment_id.write(payment_id + 1); - // Emit recurring payment processed event self .emit( RecurringPaymentProcessed { payment_id: payment_id, subscription_id: subscription_id, - subscriber: subscription.subscriber, - amount: subscription.amount, + subscriber: subscription_subscriber, + amount: subscription_amount, timestamp: current_time, }, ); @@ -977,7 +999,7 @@ pub mod ChainLib { }; self.content_verification_requirements.write((content_id, i), default_req); i += 1; - }; + } // Store new requirements let new_count = requirements.len(); @@ -986,7 +1008,7 @@ pub mod ChainLib { let req = requirements.at(i); self.content_verification_requirements.write((content_id, i), *req); i += 1; - }; + } self.content_verification_requirements_count.write(content_id, new_count); true @@ -1004,7 +1026,7 @@ pub mod ChainLib { let req = self.content_verification_requirements.read((content_id, i)); requirements.append(req); i += 1; - }; + } requirements } @@ -1031,7 +1053,7 @@ pub mod ChainLib { }; self.content_access_rules.write((content_id, i), empty_rule); i += 1; - }; + } // Store new rules let new_count = rules.len(); @@ -1040,7 +1062,7 @@ pub mod ChainLib { let rule = *rules.at(i); self.content_access_rules.write((content_id, i), rule); i += 1; - }; + } self.content_access_rules_count.write(content_id, new_count); true @@ -1058,7 +1080,7 @@ pub mod ChainLib { let rule = self.content_access_rules.read((content_id, i)); rules.append(rule); i += 1; - }; + } rules } @@ -1109,7 +1131,7 @@ pub mod ChainLib { break; } i += 1; - }; + } status } @@ -1363,7 +1385,9 @@ pub mod ChainLib { } - fn create_subscription(ref self: ContractState, user_id: u256, amount: u256) -> bool { + fn create_subscription( + ref self: ContractState, user_id: u256, amount: u256, plan_type: u32, + ) -> bool { let caller = get_caller_address(); // Verify the user exists @@ -1380,6 +1404,23 @@ pub mod ChainLib { let subscription_period: u64 = 30 * 24 * 60 * 60; let end_date = current_time + subscription_period; + let planTypeResult: Result = match plan_type { + let plantype: Result = match plan_type { + 0 => Ok(PlanType::MONTHLY), + 1 => Ok(PlanType::YEARLY), + 2 => Ok(PlanType::TRIAL), + _ => Err('Invalid plan option'), + }; + let subscription_type = match planTypeResult { + let subscription_type = match plantype { + Result::Ok(pt) => pt, + Result::Err(_) => { + assert(false, 'Invalid plan option'); + // This line will never be reached, but is required for type checking + PlanType::MONTHLY + }, + }; + let new_subscription = Subscription { id: subscription_id, subscriber: caller, @@ -1389,9 +1430,16 @@ pub mod ChainLib { end_date: end_date, is_active: true, last_payment_date: current_time, + subscription_type: subscription_type, }; - self.subscriptions.write(user_id, new_subscription); + self.subscriptions.write(user_id, new_subscription.clone()); + + // read from the subscription + self.subscription_record.entry(user_id).append().write(new_subscription); + + let current_count = self.subscription_count.read(user_id); + self.subscription_count.write(user_id, current_count + 1); // Emit event self.emit(SubscriptionCreated { user_id: user_id, end_date: end_date, amount: amount }); @@ -1439,6 +1487,24 @@ pub mod ChainLib { true } + + fn get_user_subscription_record( + ref self: ContractState, user_id: u256, + ) -> Array { + let count = self.subscription_count.entry(user_id).read(); + let mut subscriptions = ArrayTrait::new(); + + let mut i = 0; + while i < count.try_into().unwrap() { + let subscription = self.subscription_record.entry(user_id).at(i).read(); + subscriptions.append(subscription); + i += 1; + }; + + subscriptions + } + + fn is_in_blacklist(self: @ContractState, user_id: u256, content_id: felt252) -> bool { self.access_blacklist.read((user_id, content_id)) } @@ -1707,7 +1773,7 @@ pub mod ChainLib { purchases.append(purchase); } i += 1; - }; + } // Return the array of purchases purchases @@ -1732,7 +1798,7 @@ pub mod ChainLib { purchases.append(purchase); } i += 1; - }; + } // Return the array of purchases purchases diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index f294c7f..af38808 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -5,7 +5,7 @@ use crate::base::types::{ VerificationRequirement, VerificationType, }; use crate::chainlib::ChainLib::ChainLib::{ - Category, ContentMetadata, ContentType, DelegationInfo, Payment, Subscription, + Category, ContentMetadata, ContentType, DelegationInfo, Payment, PlanType, Subscription, }; #[starknet::interface] @@ -167,7 +167,9 @@ pub trait IChainLib { self: @TContractState, delegator: ContractAddress, permission: u64, ) -> DelegationInfo; - fn create_subscription(ref self: TContractState, user_id: u256, amount: u256) -> bool; + fn create_subscription( + ref self: TContractState, user_id: u256, amount: u256, plan_type: u32, + ) -> bool; fn get_user_subscription(ref self: TContractState, user_id: u256) -> Subscription; @@ -207,4 +209,6 @@ pub trait IChainLib { ref self: TContractState, purchase_id: u256, status: PurchaseStatus, ) -> bool; fn get_content_purchases(ref self: TContractState, content_id: felt252) -> Array; + + fn get_user_subscription_record(ref self: TContractState, user_id: u256) -> Array; } diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 5463b5f..f558f02 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -1,7 +1,7 @@ // Import the contract modules use chain_lib::base::types::{PurchaseStatus, Rank, Role, Status}; use chain_lib::chainlib::ChainLib; -use chain_lib::chainlib::ChainLib::ChainLib::{Category, ContentType}; +use chain_lib::chainlib::ChainLib::ChainLib::{Category, ContentType, PlanType}; use chain_lib::interfaces::IChainLib::{IChainLib, IChainLibDispatcher, IChainLibDispatcherTrait}; use snforge_std::{ CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare, @@ -327,9 +327,12 @@ fn test_create_subscription() { // Call register let account_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); - dispatcher.create_subscription(account_id, 500); + dispatcher.create_subscription(account_id, 500, 1); let subscription = dispatcher.get_user_subscription(account_id); assert(subscription.id == 1, 'Subscription ID should be 1'); + assert(subscription.subscription_type == PlanType::YEARLY, 'Plan type should be YEARLY'); + let subscription_record = dispatcher.get_user_subscription_record(account_id); + assert(subscription_record.len() == 1, 'record should have length 1'); } #[test] @@ -347,7 +350,7 @@ fn test_create_subscription_invalid_user() { // Call register let account_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); - dispatcher.create_subscription(20, 500); + dispatcher.create_subscription(20, 500, 2); let subscription = dispatcher.get_user_subscription(account_id); assert(subscription.id == 1, 'Subscription ID should be 1'); } @@ -451,7 +454,7 @@ fn test_has_active_subscription() { // Call register let account_id = dispatcher.register_user(username, role.clone(), rank.clone(), metadata); - dispatcher.create_subscription(account_id, 500); + dispatcher.create_subscription(account_id, 500, 2); start_cheat_caller_address(contract_address, admin); let check_sub = dispatcher.has_active_subscription(account_id); diff --git a/tests/test_subscription.cairo b/tests/test_subscription.cairo index afc6325..4e32059 100644 --- a/tests/test_subscription.cairo +++ b/tests/test_subscription.cairo @@ -633,7 +633,7 @@ fn test_process_refund_success() { // Test that the function panics when trying to refund an already refunded payment #[test] -#[should_panic(expected: 'Subscription not active')] +#[should_panic(expected: 'Payment already refunded')] fn test_process_refund_already_refunded() { // Setup the contract let (contract_address, admin_address) = setup(); From 6d3d7527a0752d02f78b8fcfbf005faf68ace7c1 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Mon, 9 Jun 2025 10:00:01 +0100 Subject: [PATCH 2/4] fix: update panic message for refund test to reflect subscription status --- src/chainlib/ChainLib.cairo | 2 -- tests/test_subscription.cairo | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 7e99744..fed44f5 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -1405,14 +1405,12 @@ pub mod ChainLib { let end_date = current_time + subscription_period; let planTypeResult: Result = match plan_type { - let plantype: Result = match plan_type { 0 => Ok(PlanType::MONTHLY), 1 => Ok(PlanType::YEARLY), 2 => Ok(PlanType::TRIAL), _ => Err('Invalid plan option'), }; let subscription_type = match planTypeResult { - let subscription_type = match plantype { Result::Ok(pt) => pt, Result::Err(_) => { assert(false, 'Invalid plan option'); diff --git a/tests/test_subscription.cairo b/tests/test_subscription.cairo index 4e32059..afc6325 100644 --- a/tests/test_subscription.cairo +++ b/tests/test_subscription.cairo @@ -633,7 +633,7 @@ fn test_process_refund_success() { // Test that the function panics when trying to refund an already refunded payment #[test] -#[should_panic(expected: 'Payment already refunded')] +#[should_panic(expected: 'Subscription not active')] fn test_process_refund_already_refunded() { // Setup the contract let (contract_address, admin_address) = setup(); From 4e6bab3739892c2cbf734ab3ab586b6a6c4d59c3 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Mon, 9 Jun 2025 10:02:16 +0100 Subject: [PATCH 3/4] fix: remove unnecessary whitespace in subscription retrieval function --- src/chainlib/ChainLib.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index fed44f5..752951b 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -1491,14 +1491,14 @@ pub mod ChainLib { ) -> Array { let count = self.subscription_count.entry(user_id).read(); let mut subscriptions = ArrayTrait::new(); - + let mut i = 0; while i < count.try_into().unwrap() { let subscription = self.subscription_record.entry(user_id).at(i).read(); subscriptions.append(subscription); i += 1; - }; - + } + subscriptions } From e99fb5d153ef077bc106591233bfebe019c71ba7 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Mon, 9 Jun 2025 10:18:34 +0100 Subject: [PATCH 4/4] chore: update scarb and starknet-foundry versions in CI configuration --- .github/workflows/ci.yml | 6 +++--- Scarb.toml | 32 -------------------------------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d8db66..e5d33a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v3 - uses: software-mansion/setup-scarb@v1 with: - scarb-version: 2.8.4 + scarb-version: 2.11.2 - name: Check cairo format run: scarb fmt --check - name: Build cairo programs @@ -22,9 +22,9 @@ jobs: - uses: actions/checkout@v3 - uses: software-mansion/setup-scarb@v1 with: - scarb-version: 2.9.2 + scarb-version: 2.11.2 - uses: foundry-rs/setup-snfoundry@v3 with: - starknet-foundry-version: 0.32.0 + starknet-foundry-version: 0.40.0 - name: Run cairo tests run: snforge test diff --git a/Scarb.toml b/Scarb.toml index b59ea02..92d89cd 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -3,8 +3,6 @@ name = "chain_lib" version = "0.1.0" edition = "2023_11" -# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html - [dependencies] starknet = "2.11.2" @@ -17,33 +15,3 @@ sierra = true [scripts] test = "snforge test" - -# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information - -# [tool.snforge] # Define `snforge` tool section -# exit_first = true # Stop tests execution immediately upon the first failure -# fuzzer_runs = 1234 # Number of runs of the random fuzzer -# fuzzer_seed = 1111 # Seed for the random fuzzer - -# [[tool.snforge.fork]] # Used for fork testing -# name = "SOME_NAME" # Fork name -# url = "http://your.rpc.url" # Url of the RPC provider -# block_id.tag = "latest" # Block to fork from (block tag) - -# [[tool.snforge.fork]] -# name = "SOME_SECOND_NAME" -# url = "http://your.second.rpc.url" -# block_id.number = "123" # Block to fork from (block number) - -# [[tool.snforge.fork]] -# name = "SOME_THIRD_NAME" -# url = "http://your.third.rpc.url" -# block_id.hash = "0x123" # Block to fork from (block hash) - -# [profile.dev.cairo] # Configure Cairo compiler -# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage -# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler -# inlining-strategy = "avoid" # Should be used if you want to use coverage - -# [features] # Used for conditional compilation -# enable_for_tests = [] # Feature name and list of other features that should be enabled with it