diff --git a/Scarb.lock b/Scarb.lock index d2649db..67416b6 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -5,9 +5,129 @@ version = 1 name = "chain_lib" version = "0.1.0" dependencies = [ + "openzeppelin", "snforge_std", ] +[[package]] +name = "openzeppelin" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:320185f3e17cf9fafda88b1ce490f5eaed0bfcc273036b56cd22ce4fb8de628f" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_merkle_tree", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:a39a4ea1582916c637bf7e3aee0832c3fe1ea3a3e39191955e8dc39d08327f9b" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:7e943a2de32ddca4d48e467e52790e380ab1f49c4daddbbbc4634dd930d0243f" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_finance" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:9fa9e91d39b6ccdfa31eef32fdc087cd06c0269cc9c6b86e32d57f5a6997d98b" +dependencies = [ + "openzeppelin_access", + "openzeppelin_token", +] + +[[package]] +name = "openzeppelin_governance" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:c05add2974b3193c3a5c022b9586a84cf98c5970cdb884dcf201c77dbe359f55" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_introspection" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:34e088ecf19e0b3012481a29f1fbb20e600540cb9a5db1c3002a97ebb7f5a32a" + +[[package]] +name = "openzeppelin_merkle_tree" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:a5341705514a3d9beeeb39cf11464111f7355be621639740d2c5006786aa63dc" + +[[package]] +name = "openzeppelin_presets" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:4eb098e2ee3ac0e67b6828115a7de62f781418beab767d4e80b54e176808369d" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_security" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:1deb811a239c4f9cc28fc302039e2ffcb19911698a8c612487207448d70d2e6e" + +[[package]] +name = "openzeppelin_token" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:33fcb84a1a76d2d3fff9302094ff564f78d45b743548fd7568c130b272473f66" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:36f7a03e7e7111577916aacf31f88ad0053de20f33ee10b0ab3804849c3aa373" + +[[package]] +name = "openzeppelin_utils" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:fd348b31c4a4407add33adc3c2b8f26dca71dbd7431faaf726168f37a91db0c1" + [[package]] name = "snforge_scarb_plugin" version = "0.40.0" diff --git a/Scarb.toml b/Scarb.toml index 92d89cd..805dec4 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -5,6 +5,7 @@ edition = "2023_11" [dependencies] starknet = "2.11.2" +openzeppelin = "1.0.0" [dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.40.0" } diff --git a/src/base/errors.cairo b/src/base/errors.cairo index de7d923..c95eb45 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -6,3 +6,8 @@ pub mod permission_errors { pub const INVALID_PERMISSION: felt252 = 'Invalid permission value'; pub const ZERO_ADDRESS: felt252 = 'Zero address'; } + +pub mod payment_errors { + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Insufficient token allowance'; + pub const INSUFFICIENT_BALANCE: felt252 = 'Insufficient token balance'; +} diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 9bc077d..5849618 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -3,13 +3,16 @@ pub mod ChainLib { use core::array::{Array, ArrayTrait}; use core::option::OptionTrait; use core::traits::Into; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ Map, MutableVecTrait, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, }; use starknet::{ ContractAddress, contract_address_const, get_block_timestamp, get_caller_address, + get_contract_address, }; + use crate::base::errors::payment_errors; use crate::base::types::{ AccessRule, AccessType, Permissions, Purchase, PurchaseStatus, Rank, Role, Status, TokenBoundAccount, User, VerificationRequirement, VerificationType, permission_flags, @@ -211,14 +214,18 @@ pub mod ChainLib { 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 + >, // subscriber count to number of times the subscription record has been updated + token_address: ContractAddress // Address of the token contract used for payments } #[constructor] - fn constructor(ref self: ContractState, admin: ContractAddress) { + fn constructor( + ref self: ContractState, admin: ContractAddress, token_address: ContractAddress, + ) { // Store the values in contract state self.admin.write(admin); + self.token_address.write(token_address); // Initialize purchase ID counter self.next_purchase_id.write(1_u256); self.purchase_timeout_duration.write(3600); @@ -760,6 +767,8 @@ pub mod ChainLib { // Only allow the subscriber themselves to create a subscription assert(caller == subscriber, 'Only subscriber can call'); + self._process_payment(amount); + // Create a new subscription let subscription_id = self.subscription_id.read(); let subscription_plan: Subscription = self.subscriptions.read(subscription_id); @@ -862,6 +871,9 @@ pub mod ChainLib { // For simplicity, we'll allow any recurring payment after the initial payment assert(current_time > subscription.last_payment_date, 'Payment not due yet'); + // Process the payment + self._process_payment(subscription.amount); + // Default subscription period is 30 days (in seconds) let subscription_period: u64 = 30 * 24 * 60 * 60; @@ -979,6 +991,9 @@ pub mod ChainLib { // Verify payment exists and is not already refunded assert(!payment.is_refunded, 'Payment already refunded'); + // process the refund + self._process_refund(payment.amount, get_caller_address()); + // Mark payment as refunded payment.is_refunded = true; self.payments.write(payment_id, payment); @@ -1739,6 +1754,9 @@ pub mod ChainLib { let price = self.content_prices.read(content_id); assert!(price > 0, "Content either doesn't exist or has no price"); + // process the purchase + self._process_payment(price); + let buyer = get_caller_address(); let current_time = get_block_timestamp(); @@ -1967,4 +1985,52 @@ pub mod ChainLib { true } } + + #[generate_trait] + impl internal of InternalTraits { + /// @notice Processes a payment for a subscription or content purchase. + /// @dev Checks the token allowance and balance before transferring tokens. + /// @param self The contract state reference. + /// @param amount The amount of tokens to transfer. + /// @require The caller must have sufficient token allowance and balance. + fn _process_payment(ref self: ContractState, amount: u256) { + let strk_token = IERC20Dispatcher { contract_address: self.token_address.read() }; + let caller = get_caller_address(); + let contract_address = get_contract_address(); + self._check_token_allowance(caller, amount); + self._check_token_balance(caller, amount); + strk_token.transfer_from(caller, contract_address, amount); + } + + /// @notice Checks if the caller has sufficient token allowance. + /// @dev Asserts that the caller has enough allowance to transfer the specified amount. + /// @param self The contract state reference. + /// @param spender The address of the spender (usually the contract itself). + /// @param amount The amount of tokens to check allowance for. + /// @require The caller must have sufficient token allowance. + fn _check_token_allowance(ref self: ContractState, spender: ContractAddress, amount: u256) { + let token = IERC20Dispatcher { contract_address: self.token_address.read() }; + let allowance = token.allowance(spender, starknet::get_contract_address()); + assert(allowance >= amount, payment_errors::INSUFFICIENT_ALLOWANCE); + } + + /// @notice Checks if the caller has sufficient token balance. + /// @dev Asserts that the caller has enough balance to transfer the specified amount. + /// @param self The contract state reference. + /// @param caller The address of the caller (usually the user). + /// @param amount The amount of tokens to check balance for. + /// @require The caller must have sufficient token balance. + fn _check_token_balance(ref self: ContractState, caller: ContractAddress, amount: u256) { + let token = IERC20Dispatcher { contract_address: self.token_address.read() }; + let balance = token.balance_of(caller); + assert(balance >= amount, payment_errors::INSUFFICIENT_BALANCE); + } + + 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); + } + } } diff --git a/src/lib.cairo b/src/lib.cairo index f727319..cb802eb 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -9,3 +9,7 @@ pub mod interfaces { pub mod IChainLib; } +pub mod presets { + pub mod mock_erc20; +} + diff --git a/src/presets/mock_erc20.cairo b/src/presets/mock_erc20.cairo new file mode 100644 index 0000000..509ef76 --- /dev/null +++ b/src/presets/mock_erc20.cairo @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IExternal { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256); +} +#[starknet::contract] +pub mod mock_erc20 { + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::interface::IERC20Metadata; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc20: ERC20Component::Storage, + #[substorage(v0)] + pub ownable: OwnableComponent::Storage, + custom_decimals: u8 // Add custom decimals storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, recipient: ContractAddress, owner: ContractAddress, decimals: u8, + ) { + self.erc20.initializer(format!("USDC"), format!("USDC")); + self.ownable.initializer(owner); + self.custom_decimals.write(decimals); + + self.erc20.mint(recipient, core::num::traits::Bounded::::MAX); + } + + #[abi(embed_v0)] + impl CustomERC20MetadataImpl of IERC20Metadata { + fn name(self: @ContractState) -> ByteArray { + self.erc20.name() + } + + fn symbol(self: @ContractState) -> ByteArray { + self.erc20.symbol() + } + + fn decimals(self: @ContractState) -> u8 { + self.custom_decimals.read() // Return custom value + } + } + + // Keep existing implementations + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl InternalImpl = ERC20Component::InternalImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[abi(embed_v0)] + impl ExternalImpl of super::IExternal { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20.mint(recipient, amount); + } + } +} diff --git a/tests/lib.cairo b/tests/lib.cairo index 6da5df4..dc89ff1 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -6,3 +6,4 @@ pub mod test_contentpost; pub mod test_permissions; pub mod test_subscription; +pub mod test_utils; diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index e368833..067a439 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -3,6 +3,7 @@ use chain_lib::base::types::{PurchaseStatus, Rank, 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}; +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, @@ -11,42 +12,11 @@ use starknet::ContractAddress; use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::testing::{set_caller_address, set_contract_address}; - -/// Helper function to create a content item with a price -/// We'll use the set_content_price function implemented in the contract -fn setup_content_with_price( - dispatcher: IChainLibDispatcher, - admin_address: ContractAddress, - contract_address: ContractAddress, - content_id: felt252, - price: u256, -) { - // Set admin as caller for setting content price - cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); - - // Use the new set_content_price function to set the price - dispatcher.set_content_price(content_id, price); -} - -fn setup() -> (ContractAddress, ContractAddress) { - let declare_result = declare("ChainLib"); - assert(declare_result.is_ok(), 'Contract declaration failed'); - let admin_address: ContractAddress = contract_address_const::<'admin'>(); - - let contract_class = declare_result.unwrap().contract_class(); - let mut calldata = array![admin_address.into()]; - - let deploy_result = contract_class.deploy(@calldata); - assert(deploy_result.is_ok(), 'Contract deployment failed'); - - let (contract_address, _) = deploy_result.unwrap(); - - (contract_address, admin_address) -} +use crate::test_utils::{setup, setup_content_with_price, token_faucet_and_allowance}; #[test] fn test_initial_data() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; @@ -59,7 +29,7 @@ fn test_initial_data() { #[test] fn test_create_token_bount_account() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -83,7 +53,7 @@ fn test_create_token_bount_account() { #[test] fn test_create_user() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -109,7 +79,7 @@ fn test_create_user() { #[test] fn test_verify_user() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -134,7 +104,7 @@ fn test_verify_user() { #[test] fn test_update_user() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -185,7 +155,7 @@ fn test_update_user() { #[test] fn test_deactivate_user() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -216,7 +186,7 @@ fn test_deactivate_user() { #[test] #[should_panic] fn test_deactivate_user_should_panic_if_diffrent_Address() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -248,7 +218,7 @@ fn test_deactivate_user_should_panic_if_diffrent_Address() { #[test] #[should_panic] fn test_update_user_should_panic_when_address_is_0() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -291,7 +261,7 @@ fn test_update_user_should_panic_when_address_is_0() { #[test] #[should_panic(expected: 'Only admin can verify users')] fn test_verify_user_not_admin() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -315,7 +285,7 @@ fn test_verify_user_not_admin() { #[test] fn test_create_subscription() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -339,7 +309,7 @@ fn test_create_subscription() { #[test] #[should_panic(expected: 'User does not exist')] fn test_create_subscription_invalid_user() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -359,7 +329,7 @@ fn test_create_subscription_invalid_user() { #[test] #[should_panic(expected: "Only WRITER can post content")] fn test_grant_premium_access_test_admin() { - let (contract_address, admin) = setup(); + let (contract_address, admin, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -386,7 +356,7 @@ fn test_grant_premium_access_test_admin() { #[test] #[should_panic(expected: "Only WRITER can post content")] fn test_is_in_blacklist() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let creator = contract_address_const::<'creator'>(); @@ -416,7 +386,7 @@ fn test_is_in_blacklist() { #[test] #[should_panic(expected: "Only WRITER can post content")] fn test_revoke_access_by_admin() { - let (contract_address, admin) = setup(); + let (contract_address, admin, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -443,7 +413,7 @@ fn test_revoke_access_by_admin() { #[test] fn test_has_active_subscription() { - let (contract_address, admin) = setup(); + let (contract_address, admin, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -464,7 +434,7 @@ fn test_has_active_subscription() { #[test] fn test_set_cache_ttl() { - let (contract_address, admin) = setup(); + let (contract_address, admin, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; start_cheat_caller_address(contract_address, admin); @@ -476,7 +446,7 @@ fn test_set_cache_ttl() { #[test] #[should_panic(expected: "Only WRITER can post content")] fn test_verify_access() { - let (contract_address, admin) = setup(); + let (contract_address, admin, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -502,7 +472,7 @@ fn test_verify_access() { #[test] #[should_panic(expected: "Only WRITER can post content")] fn test_determine_access() { - let (contract_address, admin) = setup(); + let (contract_address, admin, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -527,10 +497,13 @@ fn test_determine_access() { #[test] fn test_purchase_content() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let user_address = contract_address_const::<'user'>(); + // Token faucet and allowance setup + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); + // Set up test data let content_id: felt252 = 'content1'; let price: u256 = 1000_u256; @@ -555,10 +528,97 @@ fn test_purchase_content() { assert(purchase.transaction_hash == transaction_hash, 'Transaction hash mismatch'); } + +#[test] +#[should_panic(expected: 'Insufficient token balance')] +fn test_purchase_content_should_fail_if_insufficient_balance() { + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + let user_address = contract_address_const::<'user'>(); + + let token_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // Transfer tokens from admin to user + start_cheat_caller_address(erc20_address, admin_address); + token_dispatcher.transfer(user_address, 10); + stop_cheat_caller_address(erc20_address); + + // Set user as caller to approve the contract + start_cheat_caller_address(erc20_address, user_address); + token_dispatcher.approve(dispatcher.contract_address, 10000); + stop_cheat_caller_address(erc20_address); + + // Set up test data + let content_id: felt252 = 'content1'; + let price: u256 = 1000_u256; + let transaction_hash: felt252 = 'tx1'; + + // Set up content with price + setup_content_with_price(dispatcher, admin_address, contract_address, content_id, price); + + // We set user as the caller for the purchase + cheat_caller_address(contract_address, user_address, CheatSpan::Indefinite); + + // Call the purchase function + let purchase_id = dispatcher.purchase_content(content_id, transaction_hash); + + // Verify the purchase details + let purchase = dispatcher.get_purchase_details(purchase_id); + assert(purchase.id == purchase_id, 'ID mismatch'); + assert(purchase.content_id == content_id, 'Content ID mismatch'); + assert(purchase.buyer == user_address, 'Buyer mismatch'); + assert(purchase.price == price, 'Price mismatch'); + assert(purchase.status == PurchaseStatus::Pending, 'Status mismatch'); + assert(purchase.transaction_hash == transaction_hash, 'Transaction hash mismatch'); +} + + +#[test] +#[should_panic(expected: 'Insufficient token allowance')] +fn test_purchase_content_should_fail_if_insufficient_allowance() { + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; + let user_address = contract_address_const::<'user'>(); + + let token_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // Transfer tokens from admin to user + start_cheat_caller_address(erc20_address, admin_address); + token_dispatcher.transfer(user_address, 10000); + stop_cheat_caller_address(erc20_address); + + // Set user as caller to approve the contract + start_cheat_caller_address(erc20_address, user_address); + token_dispatcher.approve(dispatcher.contract_address, 100); + stop_cheat_caller_address(erc20_address); + + // Set up test data + let content_id: felt252 = 'content1'; + let price: u256 = 1000_u256; + let transaction_hash: felt252 = 'tx1'; + + // Set up content with price + setup_content_with_price(dispatcher, admin_address, contract_address, content_id, price); + + // We set user as the caller for the purchase + cheat_caller_address(contract_address, user_address, CheatSpan::Indefinite); + + // Call the purchase function + let purchase_id = dispatcher.purchase_content(content_id, transaction_hash); + + // Verify the purchase details + let purchase = dispatcher.get_purchase_details(purchase_id); + assert(purchase.id == purchase_id, 'ID mismatch'); + assert(purchase.content_id == content_id, 'Content ID mismatch'); + assert(purchase.buyer == user_address, 'Buyer mismatch'); + assert(purchase.price == price, 'Price mismatch'); + assert(purchase.status == PurchaseStatus::Pending, 'Status mismatch'); + assert(purchase.transaction_hash == transaction_hash, 'Transaction hash mismatch'); +} + + #[test] #[should_panic(expected: "Content either doesn't exist")] fn test_purchase_nonexistent_content() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let user_address = contract_address_const::<'user'>(); @@ -575,7 +635,7 @@ fn test_purchase_nonexistent_content() { #[test] fn test_get_user_purchases() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let user_address = contract_address_const::<'user'>(); @@ -584,9 +644,10 @@ fn test_get_user_purchases() { let content_id_2: felt252 = 'content2'; let price: u256 = 1000_u256; + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); + // Set admin as caller to prepare the contract state cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); - // Set up content with price setup_content_with_price(dispatcher, admin_address, contract_address, content_id_1, price); setup_content_with_price(dispatcher, admin_address, contract_address, content_id_2, price * 2); @@ -618,10 +679,11 @@ fn test_get_user_purchases() { #[test] fn test_verify_purchase() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let user_address = contract_address_const::<'user'>(); + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); // Set up test data let content_id: felt252 = 'content1'; let price: u256 = 1000_u256; @@ -656,10 +718,12 @@ fn test_verify_purchase() { #[test] fn test_update_purchase_status() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let user_address = contract_address_const::<'user'>(); + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); + // Set up test data let content_id: felt252 = 'content1'; let price: u256 = 1000_u256; @@ -711,10 +775,11 @@ fn test_update_purchase_status() { #[test] #[should_panic(expected: 'Only admin can update status')] fn test_update_purchase_status_not_admin() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let user_address = contract_address_const::<'user'>(); + token_faucet_and_allowance(dispatcher, user_address, erc20_address, 100000); // Set up test data let content_id: felt252 = 'content1'; let price: u256 = 1000_u256; @@ -739,7 +804,7 @@ fn test_update_purchase_status_not_admin() { #[test] #[should_panic(expected: 'Purchase does not exist')] fn test_update_nonexistent_purchase() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Set admin as caller @@ -752,7 +817,7 @@ fn test_update_nonexistent_purchase() { #[test] fn test_cancel_subscription() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -783,7 +848,7 @@ fn test_cancel_subscription() { #[test] fn test_renew_subscription() { - let (contract_address, _) = setup(); + let (contract_address, _, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values diff --git a/tests/test_account_delegation.cairo b/tests/test_account_delegation.cairo index 54c23e8..30b17e3 100644 --- a/tests/test_account_delegation.cairo +++ b/tests/test_account_delegation.cairo @@ -26,23 +26,12 @@ use snforge_std::{ use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; - -fn setup() -> ContractAddress { - let contract_class = declare("ChainLib").unwrap().contract_class(); - - // Initial owner address - let owner: ContractAddress = contract_address_const::<'OWNER'>(); - - let calldata = array![owner.into()]; - - let (contract_address, _) = contract_class.deploy(@calldata).unwrap(); - contract_address -} +use crate::test_utils::{setup, setup_content_with_price, token_faucet_and_allowance}; #[test] fn test_create_delegation() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -88,7 +77,7 @@ fn test_create_delegation() { #[test] #[should_panic(expected: 'Invalid delegate address')] fn test_create_delegation_zero_address() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -112,7 +101,7 @@ fn test_create_delegation_zero_address() { #[test] fn test_revoke_delegation() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -157,7 +146,7 @@ fn test_revoke_delegation() { #[test] #[should_panic(expected: 'Delegate mismatch')] fn test_revoke_delegation_wrong_delegate() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -185,7 +174,7 @@ fn test_revoke_delegation_wrong_delegate() { #[test] fn test_use_delegation() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -254,7 +243,7 @@ fn test_use_delegation() { #[test] #[should_panic(expected: 'Permission denied')] fn test_use_delegation_exceed_max_actions() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -287,7 +276,7 @@ fn test_use_delegation_exceed_max_actions() { #[test] fn test_is_delegated_expired() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -324,7 +313,7 @@ fn test_is_delegated_expired() { #[test] fn test_is_delegated_multiple_permissions() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -364,7 +353,7 @@ fn test_is_delegated_multiple_permissions() { #[test] #[should_panic(expected: 'Permission denied')] fn test_use_delegation_wrong_delegate() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -396,7 +385,7 @@ fn test_use_delegation_wrong_delegate() { #[test] fn test_delegation_unlimited() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses @@ -439,7 +428,7 @@ fn test_delegation_unlimited() { #[test] fn test_get_delegation_info() { - let contract_address = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let contract_instance = IChainLibDispatcher { contract_address }; // Setup addresses diff --git a/tests/test_contentaccess.cairo b/tests/test_contentaccess.cairo index 89297f6..9a76038 100644 --- a/tests/test_contentaccess.cairo +++ b/tests/test_contentaccess.cairo @@ -17,26 +17,12 @@ mod permission_tests { use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::{ContractAddress, get_caller_address}; + use crate::test_utils::{setup, setup_content_with_price, token_faucet_and_allowance}; - fn setup() -> (ContractAddress, ContractAddress) { - let declare_result = declare("ChainLib"); - assert(declare_result.is_ok(), 'declare failed'); - let admin_address: ContractAddress = contract_address_const::<'admin'>(); - - let contract_class = declare_result.unwrap().contract_class(); - let mut calldata = array![admin_address.into()]; - - let deploy_result = contract_class.deploy(@calldata); - assert(deploy_result.is_ok(), 'deploy failed'); - - let (contract_address, _) = deploy_result.unwrap(); - - (contract_address, admin_address) - } #[test] fn test_content_access_rules_workflow() { - let (contract_address, _) = setup(); + let (contract_address, _, _) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let content_id = 123; let user = contract_address_const::<'user'>(); @@ -89,7 +75,7 @@ mod permission_tests { #[test] fn test_verification_workflow() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, _) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let content_id = 456; let user = contract_address_const::<0x03>(); @@ -156,7 +142,7 @@ mod permission_tests { #[test] fn test_edge_cases() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, _) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let content_id = 999; let user = contract_address_const::<0x08>(); @@ -196,7 +182,7 @@ mod permission_tests { #[test] #[should_panic] fn test_unauthorized_access() { - let (contract_address, _) = setup(); + let (contract_address, _, _) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let content_id = 111; let non_admin = contract_address_const::<0x0a>(); @@ -209,7 +195,7 @@ mod permission_tests { #[test] fn test_multiple_verification_types() { - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, _) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; let content_id = 222; let user = contract_address_const::<0x0b>(); diff --git a/tests/test_contentpost.cairo b/tests/test_contentpost.cairo index 4ef3e75..dcb555c 100644 --- a/tests/test_contentpost.cairo +++ b/tests/test_contentpost.cairo @@ -10,28 +10,13 @@ use starknet::ContractAddress; use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::testing::{set_caller_address, set_contract_address}; - - -fn setup() -> (ContractAddress, ContractAddress) { - let declare_result = declare("ChainLib"); - assert(declare_result.is_ok(), 'Contract declaration failed'); - let admin_address: ContractAddress = contract_address_const::<'admin'>(); - - let contract_class = declare_result.unwrap().contract_class(); - let mut calldata = array![admin_address.into()]; - - let deploy_result = contract_class.deploy(@calldata); - assert(deploy_result.is_ok(), 'Contract deployment failed'); - - let (contract_address, _) = deploy_result.unwrap(); - - (contract_address, admin_address) -} +use crate::test_utils::{setup, setup_content_with_price, token_faucet_and_allowance}; #[test] fn test_register_content() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; let mut spy = spy_events(); @@ -91,7 +76,8 @@ fn test_register_content() { #[test] fn test_register_content_with_different_types() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; let mut spy = spy_events(); @@ -159,7 +145,8 @@ fn test_register_content_with_different_types() { #[test] #[should_panic] fn test_register_content_not_writer() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; let title: felt252 = 'Unauthorized Content'; @@ -188,7 +175,8 @@ fn test_register_content_not_writer() { #[test] #[should_panic] fn test_register_content_empty_title() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; // Set up content with empty title @@ -218,7 +206,8 @@ fn test_register_content_empty_title() { #[test] #[should_panic] fn test_register_content_empty_description() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; // Set up content with empty description @@ -247,7 +236,8 @@ fn test_register_content_empty_description() { #[test] fn test_register_content_multiple_users() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; let mut spy = spy_events(); @@ -337,7 +327,8 @@ fn test_register_content_multiple_users() { #[test] fn test_content_metadata_retrieval() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; // Create a user with WRITER role @@ -418,7 +409,8 @@ fn test_content_metadata_retrieval() { #[test] fn test_register_multiple_content_items() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; let mut spy = spy_events(); @@ -526,7 +518,8 @@ fn test_register_multiple_content_items() { #[test] #[should_panic] fn test_get_non_existent_content() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + let dispatcher = IChainLibDispatcher { contract_address }; // Create a user with WRITER role diff --git a/tests/test_permissions.cairo b/tests/test_permissions.cairo index b431746..735faaf 100644 --- a/tests/test_permissions.cairo +++ b/tests/test_permissions.cairo @@ -11,26 +11,11 @@ mod permission_tests { use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::{ContractAddress, get_caller_address}; - - fn setup() -> (ContractAddress, ContractAddress) { - let declare_result = declare("ChainLib"); - assert(declare_result.is_ok(), 'declare failed'); - let admin_address: ContractAddress = contract_address_const::<'admin'>(); - - let contract_class = declare_result.unwrap().contract_class(); - let mut calldata = array![admin_address.into()]; - - let deploy_result = contract_class.deploy(@calldata); - assert(deploy_result.is_ok(), 'deploy failed'); - - let (contract_address, _) = deploy_result.unwrap(); - - (contract_address, admin_address) - } + use crate::test_utils::{setup, setup_content_with_price, token_faucet_and_allowance}; #[test] fn test_token_account_owner_permissions() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -50,7 +35,7 @@ mod permission_tests { #[test] fn test_set_and_get_operator_permissions() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Test input values @@ -106,7 +91,7 @@ mod permission_tests { #[test] fn test_manage_operators_permission() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Create a token account @@ -139,7 +124,7 @@ mod permission_tests { #[test] fn test_modify_account_permissions() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Create token account @@ -170,7 +155,7 @@ mod permission_tests { #[test] fn test_multiple_operators() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Create token account @@ -247,7 +232,7 @@ mod permission_tests { #[test] fn test_permission_combinations() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Create token account @@ -293,7 +278,7 @@ mod permission_tests { #[test] #[should_panic(expected: 'No permission')] fn test_unauthorized_set_operator() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Create token account @@ -318,7 +303,7 @@ mod permission_tests { #[test] #[should_panic(expected: 'No permission')] fn test_insufficient_permissions() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Create token account @@ -348,7 +333,7 @@ mod permission_tests { #[test] fn test_nonexistent_account() { - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); let dispatcher = IChainLibDispatcher { contract_address }; // Attempt to get permissions for a non-existent account diff --git a/tests/test_subscription.cairo b/tests/test_subscription.cairo index afc6325..26d9bd2 100644 --- a/tests/test_subscription.cairo +++ b/tests/test_subscription.cairo @@ -3,32 +3,17 @@ use chain_lib::chainlib::ChainLib::ChainLib::{ Event, PaymentProcessed, PaymentVerified, RecurringPaymentProcessed, RefundProcessed, }; use chain_lib::interfaces::IChainLib::{IChainLib, IChainLibDispatcher, IChainLibDispatcherTrait}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ CheatSpan, ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, - cheat_caller_address, declare, spy_events, + cheat_caller_address, declare, spy_events, start_cheat_caller_address, + stop_cheat_caller_address, }; use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::testing::{set_caller_address, set_contract_address}; use starknet::{ContractAddress, get_block_timestamp}; - - -fn setup() -> (ContractAddress, ContractAddress) { - let declare_result = declare("ChainLib"); - assert(declare_result.is_ok(), 'Contract declaration failed'); - let admin_address: ContractAddress = contract_address_const::<'admin'>(); - - let contract_class = declare_result.unwrap().contract_class(); - let mut calldata = array![admin_address.into()]; - - let deploy_result = contract_class.deploy(@calldata); - assert(deploy_result.is_ok(), 'Contract deployment failed'); - - let (contract_address, _) = deploy_result.unwrap(); - - (contract_address, admin_address) -} - +use crate::test_utils::{setup, setup_content_with_price, token_faucet_and_allowance}; // Helper function to create a token-bound account for testing fn create_test_account(dispatcher: IChainLibDispatcher) -> (u256, ContractAddress) { // Test input values for token-bound account @@ -50,7 +35,42 @@ fn create_test_account(dispatcher: IChainLibDispatcher) -> (u256, ContractAddres #[test] fn test_initial_payment() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); + + // Create dispatchers for both interfaces + let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; + let subscription_dispatcher = IChainLibDispatcher { contract_address }; + + // Create a specific subscriber address and use it consistently + let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); + + // Set the caller to the subscriber for the entire test + cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); + + // Create a token-bound account + let user_name: felt252 = 'Mark'; + let init_param1: felt252 = 'Mark@yahoo.com'; + let init_param2: felt252 = 'Mark is a boy'; + chain_lib_dispatcher.create_token_account(user_name, init_param1, init_param2); + + // Process an initial payment (caller is already set to subscriber) + let amount: u256 = 100000000000000000; // 0.1 STRK in wei + let result = subscription_dispatcher.process_initial_payment(amount, subscriber_address); + + // Verify the payment was processed successfully + assert(result == true, 'Initial payment failed'); +} + + +#[test] +#[should_panic(expected: 'Insufficient token allowance')] +fn test_initial_payment_should_panic_if_no_allowance() { + // Setup the contract + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -59,6 +79,12 @@ fn test_initial_payment() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + let token_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // Transfer tokens from admin to user + start_cheat_caller_address(erc20_address, admin_address); + token_dispatcher.transfer(subscriber_address, 100000000000000000); + stop_cheat_caller_address(erc20_address); + // Set the caller to the subscriber for the entire test cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -76,12 +102,55 @@ fn test_initial_payment() { assert(result == true, 'Initial payment failed'); } + +#[test] +#[should_panic(expected: 'Insufficient token balance')] +fn test_initial_payment_should_panic_if_insufficient_balance() { + // Setup the contract + let (contract_address, admin_address, erc20_address) = setup(); + + // Create dispatchers for both interfaces + let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; + let subscription_dispatcher = IChainLibDispatcher { contract_address }; + + // Create a specific subscriber address and use it consistently + let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + + let token_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // Transfer tokens from admin to user + start_cheat_caller_address(erc20_address, admin_address); + token_dispatcher.transfer(subscriber_address, 1000000000); + stop_cheat_caller_address(erc20_address); + + // Set user as caller to approve the contract + start_cheat_caller_address(erc20_address, subscriber_address); + token_dispatcher.approve(subscription_dispatcher.contract_address, 100000000000000000); + stop_cheat_caller_address(erc20_address); + + // Set the caller to the subscriber for the entire test + cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); + + // Create a token-bound account + let user_name: felt252 = 'Mark'; + let init_param1: felt252 = 'Mark@yahoo.com'; + let init_param2: felt252 = 'Mark is a boy'; + chain_lib_dispatcher.create_token_account(user_name, init_param1, init_param2); + + // Process an initial payment (caller is already set to subscriber) + let amount: u256 = 100000000000000000; // 0.1 STRK in wei + let result = subscription_dispatcher.process_initial_payment(amount, subscriber_address); + + // Verify the payment was processed successfully + assert(result == true, 'Initial payment failed'); +} + + // Test that the initial payment fails if the caller is not the subscriber #[test] #[should_panic(expected: 'Only subscriber can call')] fn test_initial_payment_invalid_subscriber() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create subscription dispatcher let subscription_dispatcher = IChainLibDispatcher { contract_address }; @@ -99,7 +168,7 @@ fn test_initial_payment_invalid_subscriber() { #[should_panic(expected: 'Only subscriber can call')] fn test_initial_payment_unauthorized() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -121,7 +190,7 @@ fn test_initial_payment_unauthorized() { #[test] fn test_token_bound_account_creation() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatcher let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -151,7 +220,7 @@ fn test_token_bound_account_creation() { #[test] fn test_initial_payment_event() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -161,7 +230,9 @@ fn test_initial_payment_event() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); - + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); // Set the caller to the subscriber for the entire test cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -203,7 +274,7 @@ fn test_initial_payment_event() { #[test] fn test_process_recurring_payment() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -212,6 +283,10 @@ fn test_process_recurring_payment() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); + // Set the caller to the subscriber for the entire test cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -245,13 +320,84 @@ fn test_process_recurring_payment() { assert(recurring_result == true, 'Recurring payment failed'); } +#[test] +#[should_panic(expected: 'Insufficient token allowance')] +fn test_process_recurring_payment_should_panic_if_insufficient_allowance() { + // Setup the contract + let (contract_address, admin_address, erc20_address) = setup(); + + // Create dispatchers for both interfaces + let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; + let subscription_dispatcher = IChainLibDispatcher { contract_address }; + + // Create a specific subscriber address and use it consistently + let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + + let token_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // Transfer tokens from admin to user + start_cheat_caller_address(erc20_address, admin_address); + token_dispatcher.transfer(subscriber_address, 1000000000); + stop_cheat_caller_address(erc20_address); + + // Set the caller to the subscriber for the entire test + cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); + + // Create a token-bound account + let user_name: felt252 = 'Mark'; + let init_param1: felt252 = 'Mark@yahoo.com'; + let init_param2: felt252 = 'Mark is a boy'; + chain_lib_dispatcher.create_token_account(user_name, init_param1, init_param2); + + // Process an initial payment (caller is already set to subscriber) + let amount: u256 = 100000000000000000; // 0.1 STRK in wei + let result = subscription_dispatcher.process_initial_payment(amount, subscriber_address); +} + + +#[test] +#[should_panic(expected: 'Insufficient token balance')] +fn test_process_recurring_payment_should_panic_if_insufficient_balance() { + // Setup the contract + let (contract_address, admin_address, erc20_address) = setup(); + + // Create dispatchers for both interfaces + let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; + let subscription_dispatcher = IChainLibDispatcher { contract_address }; + + // Create a specific subscriber address and use it consistently + let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + + let token_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // Transfer tokens from admin to user + start_cheat_caller_address(erc20_address, admin_address); + token_dispatcher.transfer(subscriber_address, 1000000000); + stop_cheat_caller_address(erc20_address); + + // Set user as caller to approve the contract + start_cheat_caller_address(erc20_address, subscriber_address); + token_dispatcher.approve(subscription_dispatcher.contract_address, 100000000000000000); + stop_cheat_caller_address(erc20_address); + + // Set the caller to the subscriber for the entire test + cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); + + // Create a token-bound account + let user_name: felt252 = 'Mark'; + let init_param1: felt252 = 'Mark@yahoo.com'; + let init_param2: felt252 = 'Mark is a boy'; + chain_lib_dispatcher.create_token_account(user_name, init_param1, init_param2); + + // Process an initial payment (caller is already set to subscriber) + let amount: u256 = 100000000000000000; // 0.1 STRK in wei + let result = subscription_dispatcher.process_initial_payment(amount, subscriber_address); +} // test should panic if payment not due yet #[test] #[should_panic(expected: 'Payment not due yet')] fn test_process_recurring_payment_not_due() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -260,6 +406,10 @@ fn test_process_recurring_payment_not_due() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); + // Set the caller to the subscriber for the entire test cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -289,7 +439,7 @@ fn test_process_recurring_payment_not_due() { #[should_panic(expected: 'Subscription not found')] fn test_process_recurring_payment_not_found() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let subscription_dispatcher = IChainLibDispatcher { contract_address }; @@ -310,7 +460,7 @@ fn test_process_recurring_payment_not_found() { #[test] fn test_process_recurring_payment_event() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -324,6 +474,10 @@ fn test_process_recurring_payment_event() { // Set the caller to the subscriber for the entire test cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); + // Create a token-bound account let user_name: felt252 = 'Mark'; let init_param1: felt252 = 'Mark@yahoo.com'; @@ -375,7 +529,7 @@ fn test_process_recurring_payment_event() { #[should_panic(expected: 'Only admin can verify payments')] fn test_verify_payment_admin_only() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -386,6 +540,9 @@ fn test_verify_payment_admin_only() { // Set the caller to the subscriber for creating a subscription cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); // Create a token-bound account let user_name: felt252 = 'Mark'; @@ -416,7 +573,7 @@ fn test_verify_payment_admin_only() { #[should_panic(expected: 'Payment not found')] fn test_verify_payment_not_found() { // Setup the contract - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let _chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -438,7 +595,7 @@ fn test_verify_payment_not_found() { #[should_panic(expected: 'Payment already verified')] fn test_verify_payment_success() { // Setup the contract - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -447,6 +604,9 @@ fn test_verify_payment_success() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); // Set the caller to the subscriber for creating a subscription cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -478,7 +638,7 @@ fn test_verify_payment_success() { #[test] fn test_verify_payment_event() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -490,6 +650,10 @@ fn test_verify_payment_event() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); + // Set the caller to the subscriber for creating a subscription cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -531,7 +695,7 @@ fn test_verify_payment_event() { #[should_panic(expected: 'Only admin can process refunds')] fn test_process_refund_admin_only() { // Setup the contract - let (contract_address, _) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -540,6 +704,10 @@ fn test_process_refund_admin_only() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); + // Set the caller to the subscriber for creating a subscription cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -572,7 +740,7 @@ fn test_process_refund_admin_only() { #[should_panic(expected: 'Subscription not found')] fn test_process_refund_subscription_not_found() { // Setup the contract - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let _chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -593,7 +761,7 @@ fn test_process_refund_subscription_not_found() { #[test] fn test_process_refund_success() { // Setup the contract - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -601,6 +769,9 @@ fn test_process_refund_success() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); // Set the caller to the subscriber for creating a subscription cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -636,7 +807,7 @@ fn test_process_refund_success() { #[should_panic(expected: 'Subscription not active')] fn test_process_refund_already_refunded() { // Setup the contract - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -644,7 +815,9 @@ fn test_process_refund_already_refunded() { // Create a specific subscriber address and use it consistently let subscriber_address: ContractAddress = contract_address_const::<'subscriber'>(); - + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); // Set the caller to the subscriber for creating a subscription cheat_caller_address(contract_address, subscriber_address, CheatSpan::Indefinite); @@ -685,7 +858,7 @@ fn test_process_refund_already_refunded() { #[test] fn test_process_refund_event() { // Setup the contract - let (contract_address, admin_address) = setup(); + let (contract_address, admin_address, erc20_address) = setup(); // Create dispatchers for both interfaces let chain_lib_dispatcher = IChainLibDispatcher { contract_address }; @@ -703,6 +876,10 @@ fn test_process_refund_event() { let init_param2: felt252 = 'Mark is a boy'; chain_lib_dispatcher.create_token_account(user_name, init_param1, init_param2); + token_faucet_and_allowance( + chain_lib_dispatcher, subscriber_address, erc20_address, 1000000000000000000, + ); + // Process an initial payment (caller is already set to subscriber) let amount: u256 = 100000000000000000; // 0.1 STRK in wei let result = subscription_dispatcher.process_initial_payment(amount, subscriber_address); diff --git a/tests/test_utils.cairo b/tests/test_utils.cairo new file mode 100644 index 0000000..e893ecf --- /dev/null +++ b/tests/test_utils.cairo @@ -0,0 +1,74 @@ +use chain_lib::interfaces::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, +}; +use starknet::{ContractAddress, contract_address_const}; + +pub fn setup() -> (ContractAddress, ContractAddress, ContractAddress) { + let admin_address: ContractAddress = contract_address_const::<'admin'>(); + + // Deploy mock ERC20 + let erc20_class = declare("mock_erc20").unwrap().contract_class(); + let mut calldata = array![admin_address.into(), admin_address.into(), 6]; + let (erc20_address, _) = erc20_class.deploy(@calldata).unwrap(); + + // Deploy the ChainLib contract + let declare_result = declare("ChainLib"); + assert(declare_result.is_ok(), 'Contract declaration failed'); + + let contract_class = declare_result.unwrap().contract_class(); + let mut calldata = array![admin_address.into(), erc20_address.into()]; + + let deploy_result = contract_class.deploy(@calldata); + assert(deploy_result.is_ok(), 'Contract deployment failed'); + + let (contract_address, _) = deploy_result.unwrap(); + + (contract_address, admin_address, erc20_address) +} + + +// Token faucet and allowance setup +pub fn token_faucet_and_allowance( + dispatcher: IChainLibDispatcher, + user_address: ContractAddress, + erc20_address: ContractAddress, + token_amount: u256, +) { + let admin_address = contract_address_const::<'admin'>(); + + let token_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + // Transfer tokens from admin to user + start_cheat_caller_address(erc20_address, admin_address); + token_dispatcher.transfer(user_address, token_amount); + stop_cheat_caller_address(erc20_address); + + let user_token_balance = token_dispatcher.balance_of(user_address); + assert(user_token_balance >= token_amount, 'User tokens not gotten'); + + // Set user as caller to approve the contract + start_cheat_caller_address(erc20_address, user_address); + token_dispatcher.approve(dispatcher.contract_address, token_amount); + stop_cheat_caller_address(erc20_address); + + let allowance = token_dispatcher.allowance(user_address, dispatcher.contract_address); + assert(allowance >= token_amount, 'Allowance not set correctly'); +} + +/// Helper function to create a content item with a price +/// We'll use the set_content_price function implemented in the contract +pub fn setup_content_with_price( + dispatcher: IChainLibDispatcher, + admin_address: ContractAddress, + contract_address: ContractAddress, + content_id: felt252, + price: u256, +) { + // Set admin as caller for setting content price + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + + // Use the new set_content_price function to set the price + dispatcher.set_content_price(content_id, price); +}