From 1f8dc0a17beb68690d7c27103b38a056a67b01c2 Mon Sep 17 00:00:00 2001 From: Ajidokwu Sabo Date: Sat, 28 Jun 2025 16:41:41 +0100 Subject: [PATCH 1/2] feat: Implemented Digital Purchase Receipt, Reporting and Analytics System --- .tool-versions | 2 +- src/base/types.cairo | 21 ++++++++ src/chainlib/ChainLib.cairo | 89 ++++++++++++++++++++++++++++++++-- src/interfaces/IChainLib.cairo | 18 ++++++- tests/test_ChainLib.cairo | 4 ++ 5 files changed, 129 insertions(+), 5 deletions(-) diff --git a/.tool-versions b/.tool-versions index 28c03e7..37dc27f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ scarb 2.11.4 -starknet-foundry 0.40.0 \ No newline at end of file +starknet-foundry 0.43.1 diff --git a/src/base/types.cairo b/src/base/types.cairo index 9abd5ca..aac0e03 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -147,3 +147,24 @@ pub struct Purchase { pub transaction_hash: felt252, pub timeout_expiry: u64, } + + +#[derive(Drop, Serde, starknet::Store, Debug)] +pub enum ReceiptStatus { + #[default] + Invalid, + Valid, +} + +#[derive(Drop, Serde, starknet::Store, Debug)] +pub struct Receipt { + pub id: u256, + pub purchase_id: u256, + pub content_id: felt252, + pub buyer: ContractAddress, + pub creator: ContractAddress, + pub price: u256, + pub status: ReceiptStatus, + pub issued_at: u64, + pub transaction_hash: felt252, +} diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 9bc077d..9de0309 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -11,8 +11,9 @@ pub mod ChainLib { ContractAddress, contract_address_const, get_block_timestamp, get_caller_address, }; use crate::base::types::{ - AccessRule, AccessType, Permissions, Purchase, PurchaseStatus, Rank, Role, Status, - TokenBoundAccount, User, VerificationRequirement, VerificationType, permission_flags, + AccessRule, AccessType, Permissions, Purchase, PurchaseStatus, Rank, Receipt, ReceiptStatus, + Role, Status, TokenBoundAccount, User, VerificationRequirement, VerificationType, + permission_flags, }; use crate::interfaces::IChainLib::IChainLib; @@ -211,7 +212,12 @@ 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 + // RECEIPTS + receipt_counter: u256, + receipt: Map, + creator_sales: Map, + total_sales_for_content: Map, } @@ -252,6 +258,7 @@ pub mod ChainLib { PurchaseStatusUpdated: PurchaseStatusUpdated, SubscriptionCancelled: SubscriptionCancelled, SubscriptionRenewed: SubscriptionRenewed, + ReceiptGenerated: ReceiptGenerated, } #[derive(Drop, starknet::Event)] @@ -278,6 +285,11 @@ pub mod ChainLib { new_end_time: u64, } + #[derive(Drop, starknet::Event)] + struct ReceiptGenerated { + receipt_id: u256, + } + #[derive(Drop, starknet::Event)] struct SubscriptionCancelled { user: ContractAddress, @@ -1835,7 +1847,23 @@ pub mod ChainLib { fn verify_purchase(ref self: ContractState, purchase_id: u256) -> bool { // Get the purchase details let purchase = self.purchases.read(purchase_id); + let content = self.content.read(purchase.content_id); + 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; + self.creator_sales.write(content.creator, total_creator_sales); + + self.total_sales_for_content.write(purchase.content_id, total_content_sales); + self + .issue_receipt( + purchase_id, + purchase.content_id, + purchase.buyer, + content.creator, + purchase.price, + purchase.transaction_hash, + ); // A purchase is valid if its status is Completed if purchase.status == PurchaseStatus::Completed { return true; @@ -1966,5 +1994,60 @@ pub mod ChainLib { true } + + fn issue_receipt( + ref self: ContractState, + purchase_id: u256, + content_id: felt252, + buyer: ContractAddress, + creator: ContractAddress, + price: u256, + transaction_hash: felt252, + ) -> u256 { + let receipt_id = self.receipt_counter.read() + 1; + let issued_at = starknet::get_block_timestamp(); + + let receipt = Receipt { + id: receipt_id, + purchase_id, + content_id, + buyer, + creator, + price, + status: ReceiptStatus::Valid, + issued_at, + transaction_hash, + }; + self.receipt_counter.write(receipt_id); + self.receipt.write(receipt_id, receipt); + + self.emit(ReceiptGenerated { receipt_id }); + + receipt_id + } + + fn get_receipt(self: @ContractState, receipt_id: u256) -> Receipt { + let receipt = self.receipt.read(receipt_id); + receipt + } + + fn is_receipt_valid(self: @ContractState, receipt_id: u256) -> bool { + let receipt = self.receipt.read(receipt_id); + + match receipt.status { + ReceiptStatus::Valid => true, + ReceiptStatus::Invalid => false, + } + } + + fn get_total_sales_by_creator(self: @ContractState, creator: ContractAddress) -> u256 { + let total_sales = self.creator_sales.read(creator); + total_sales + } + + fn get_total_sales_for_content(self: @ContractState, content_id: felt252) -> u256 { + let total_content_sales = self.total_sales_for_content.read(content_id); + total_content_sales + } } } diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index c0a487b..95d9c05 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -1,7 +1,7 @@ use core::array::Array; use starknet::ContractAddress; use crate::base::types::{ - AccessRule, Permissions, Purchase, PurchaseStatus, Rank, Role, TokenBoundAccount, User, + AccessRule, Permissions, Purchase, PurchaseStatus, Rank, Receipt, Role, TokenBoundAccount, User, VerificationRequirement, VerificationType, }; use crate::chainlib::ChainLib::ChainLib::{ @@ -213,4 +213,20 @@ pub trait IChainLib { fn get_user_subscription_record(ref self: TContractState, user_id: u256) -> Array; fn cancel_subscription(ref self: TContractState, user_id: u256) -> bool; fn renew_subscription(ref self: TContractState, user_id: u256) -> bool; + fn issue_receipt( + ref self: TContractState, + purchase_id: u256, + content_id: felt252, + buyer: ContractAddress, + creator: ContractAddress, + price: u256, + transaction_hash: felt252, + ) -> u256; + + fn get_receipt(self: @TContractState, receipt_id: u256) -> Receipt; + fn is_receipt_valid(self: @TContractState, receipt_id: u256) -> bool; + 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; } diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index e368833..24ab9d7 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -652,6 +652,10 @@ fn test_verify_purchase() { // Now the purchase should be verified let is_now_verified = dispatcher.verify_purchase(purchase_id); assert(is_now_verified, 'Purchase should be verified'); + + let receipt = dispatcher.get_receipt(1); + + assert(receipt.purchase_id == purchase_id, 'receipt error'); } #[test] From 51712bacd92ed94614f5f36fab6a917087105865 Mon Sep 17 00:00:00 2001 From: Ajidokwu Sabo Date: Mon, 30 Jun 2025 17:01:29 +0100 Subject: [PATCH 2/2] chore: fixed conflicts --- src/chainlib/ChainLib.cairo | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index 4ebc512..7fb6247 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -216,13 +216,12 @@ pub mod ChainLib { subscription_count: Map< u256, u256, >, // subscriber count to number of times the subscription record has been updated - // RECEIPTS receipt_counter: u256, receipt: Map, creator_sales: Map, - total_sales_for_content: Map, + token_address: ContractAddress, }