diff --git a/contracts/src/activity_log.rs b/contracts/src/activity_log.rs index 5307c93b..40ebdf56 100644 --- a/contracts/src/activity_log.rs +++ b/contracts/src/activity_log.rs @@ -5,7 +5,7 @@ //! Activity logs are append-only and cannot be modified once stored. use soroban_sdk::{ - Address, BytesN, Env, Vec, + contracttype, Address, BytesN, Env, Vec, }; // Activity log entry stored on-chain. diff --git a/contracts/src/blogging_platform.rs b/contracts/src/blogging_platform.rs index 04ffb7f0..0f4f8288 100644 --- a/contracts/src/blogging_platform.rs +++ b/contracts/src/blogging_platform.rs @@ -1,9 +1,17 @@ use soroban_sdk::{ + contract, contractimpl, contracttype, Address, BytesN, Env, String, Vec, Symbol, contracttype, Address, Env, String, Symbol, Vec, BytesN, }; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] +pub struct Post { + pub id: u64, + pub author: Address, + pub content_hash: BytesN<32>, + pub timestamp: u64, + pub price: i128, + pub is_paid: bool, pub struct BlogPost { pub id: u64, pub author: Address, @@ -17,11 +25,91 @@ pub struct BlogPost { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Comment { pub author: Address, + pub text_hash: BytesN<32>, pub content: String, pub timestamp: u64, } #[contracttype] +enum DataKey { + PostCount, + Posts(u64), + Comments(u64), + Reactions(u64, Symbol), // (post_id, reaction_type) -> count + HasAccess(Address, u64), // (user, post_id) -> bool +} + +#[contract] +pub struct BloggingPlatform; + +#[contractimpl] +impl BloggingPlatform { + pub fn create_post(env: Env, author: Address, content_hash: BytesN<32>, price: i128) -> u64 { + author.require_auth(); + + let mut count: u64 = env.storage().instance().get(&DataKey::PostCount).unwrap_or(0); + count += 1; + + let post = Post { + id: count, + author: author.clone(), + content_hash, + timestamp: env.ledger().timestamp(), + price, + is_paid: price > 0, + }; + + env.storage().persistent().set(&DataKey::Posts(count), &post); + env.storage().instance().set(&DataKey::PostCount, &count); + + env.events().publish( + (Symbol::new(&env, "post_created"), author), + (count, content_hash), + ); + + count + } + + pub fn get_post(env: Env, post_id: u64) -> Option { + env.storage().persistent().get(&DataKey::Posts(post_id)) + } + + pub fn add_comment(env: Env, author: Address, post_id: u64, text_hash: BytesN<32>) { + author.require_auth(); + + let mut comments: Vec = env + .storage() + .persistent() + .get(&DataKey::Comments(post_id)) + .unwrap_or(Vec::new(&env)); + + comments.push_back(Comment { + author: author.clone(), + text_hash, + timestamp: env.ledger().timestamp(), + }); + + env.storage().persistent().set(&DataKey::Comments(post_id), &comments); + + env.events().publish( + (Symbol::new(&env, "comment_added"), post_id), + (author, text_hash), + ); + } + + pub fn react(env: Env, user: Address, post_id: u64, reaction: Symbol) { + user.require_auth(); + + let key = DataKey::Reactions(post_id, reaction.clone()); + let mut count: u32 = env.storage().persistent().get(&key).unwrap_or(0); + count += 1; + + env.storage().persistent().set(&key, &count); + + env.events().publish( + (Symbol::new(&env, "post_reaction"), post_id), + (user, reaction, count), + ); #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ReactionType { Like, diff --git a/contracts/src/content_monetization.rs b/contracts/src/content_monetization.rs index 93199be0..9256dfb3 100644 --- a/contracts/src/content_monetization.rs +++ b/contracts/src/content_monetization.rs @@ -1,4 +1,78 @@ use soroban_sdk::{ + contract, contractimpl, contracttype, Address, Env, Symbol, +}; +use crate::blogging_platform::{BloggingPlatformClient, Post}; + +#[contracttype] +enum DataKey { + PlatformAdmin, + Earnings(Address), + Subscriptions(Address, Address), // (subscriber, creator) -> expiry +} + +#[contract] +pub struct ContentMonetization; + +#[contractimpl] +impl ContentMonetization { + pub fn init(env: Env, admin: Address) { + env.storage().instance().set(&DataKey::PlatformAdmin, &admin); + } + + pub fn tip_creator(env: Env, tipper: Address, creator: Address, token: Address, amount: i128) { + tipper.require_auth(); + + let token_client = crate::token::RsTokenContractClient::new(&env, &token); + // Using token_id 0 as default for TIPS + token_client.transfer(&tipper, &creator, &0, &amount); + + // Track earnings + let mut earnings: i128 = env.storage().persistent().get(&DataKey::Earnings(creator.clone())).unwrap_or(0); + earnings += amount; + env.storage().persistent().set(&DataKey::Earnings(creator.clone()), &earnings); + + env.events().publish( + (Symbol::new(&env, "tip_sent"), tipper), + (creator, amount), + ); + } + + pub fn purchase_access(env: Env, user: Address, post_id: u64, blog_contract: Address, token: Address) { + user.require_auth(); + + let blog_client = BloggingPlatformClient::new(&env, &blog_contract); + let post = blog_client.get_post(&post_id).unwrap(); + + if post.is_paid { + let token_client = crate::token::RsTokenContractClient::new(&env, &token); + token_client.transfer(&user, &post.author, &0, &post.price); + + // Note: In a real app, we would store access in a separate contract or this one + // For now, we emit an event that the frontend can use + env.events().publish( + (Symbol::new(&env, "access_purchased"), user), + (post_id, post.author), + ); + } + } + + pub fn subscribe(env: Env, subscriber: Address, creator: Address, token: Address, amount: i128, duration: u64) { + subscriber.require_auth(); + + let token_client = crate::token::RsTokenContractClient::new(&env, &token); + token_client.transfer(&subscriber, &creator, &0, &amount); + + let expiry = env.ledger().timestamp() + duration; + env.storage().persistent().set(&DataKey::Subscriptions(subscriber.clone(), creator.clone()), &expiry); + + env.events().publish( + (Symbol::new(&env, "subscription_created"), subscriber), + (creator, expiry), + ); + } + + pub fn get_earnings(env: Env, creator: Address) -> i128 { + env.storage().persistent().get(&DataKey::Earnings(creator)).unwrap_or(0) contracttype, Address, Env, Symbol, token, }; diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index a0cc8827..e3f39df6 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -43,6 +43,11 @@ pub mod quadratic_voting; // #[cfg(test)] // pub mod fuzz; pub mod token; +pub mod upgrade; +pub mod airdrop_manager; +pub mod merkle_distributor; +pub mod blogging_platform; +pub mod content_monetization; pub mod airdrop_manager; pub mod merkle_distributor; pub mod crowdfunding; diff --git a/contracts/src/statistics.rs b/contracts/src/statistics.rs index 425b4b90..9afb5a37 100644 --- a/contracts/src/statistics.rs +++ b/contracts/src/statistics.rs @@ -4,7 +4,7 @@ //! All statistics are tracked on-chain and can be queried for analytics dashboards. use soroban_sdk::{ - Address, BytesN, Env, + contracttype, Address, BytesN, Env, }; use crate::activity_log::ActivityLogManager; diff --git a/frontend/src/app/blog/page.tsx b/frontend/src/app/blog/page.tsx new file mode 100644 index 00000000..2487e891 --- /dev/null +++ b/frontend/src/app/blog/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { BlogDashboard } from '@/components/blogging/BlogDashboard'; + +export default function BlogPage() { + return ; +} diff --git a/frontend/src/components/blogging/BlogDashboard.tsx b/frontend/src/components/blogging/BlogDashboard.tsx index 028fae3e..9d233a23 100644 --- a/frontend/src/components/blogging/BlogDashboard.tsx +++ b/frontend/src/components/blogging/BlogDashboard.tsx @@ -1,3 +1,273 @@ +'use client'; + +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + PenTool, + BookOpen, + DollarSign, + MessageSquare, + Heart, + Share2, + TrendingUp, + Shield, + Lock, + ChevronRight, + Plus +} from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; + +interface BlogPost { + id: number; + title: string; + excerpt: string; + author: string; + date: string; + readTime: string; + isPaid: boolean; + price: string; + likes: number; + comments: number; + tags: string[]; +} + +const mockPosts: BlogPost[] = [ + { + id: 1, + title: "The Future of Soroban Smart Contracts", + excerpt: "Exploring the next generation of WASM-based smart contracts on the Stellar network...", + author: "StellarDev", + date: "Oct 24, 2026", + readTime: "8 min read", + isPaid: false, + price: "0", + likes: 124, + comments: 18, + tags: ["WASM", "Stellar", "Rust"] + }, + { + id: 2, + title: "Monetizing Open Source via On-Chain Tips", + excerpt: "How direct tipping mechanisms are revolutionizing the way developers fund their work...", + author: "OpenSourceGal", + date: "Oct 22, 2026", + readTime: "12 min read", + isPaid: true, + price: "50 RST", + likes: 89, + comments: 24, + tags: ["Economics", "Blogging", "Web3"] + }, + { + id: 3, + title: "Advanced Merkle Tree Optimizations", + excerpt: "Deep dive into gas-efficient merkle proof verification techniques for large-scale distributions...", + author: "CryptoWizard", + date: "Oct 20, 2026", + readTime: "15 min read", + isPaid: true, + price: "100 RST", + likes: 245, + comments: 42, + tags: ["Cryptography", "Performance"] + } +]; + +export const BlogDashboard: React.FC = () => { + const { user } = useAuth(); + const [view, setView] = useState<'feed' | 'editor' | 'analytics'>('feed'); + const [selectedPost, setSelectedPost] = useState(null); + + return ( +
+ {/* Navigation Sidebar */} + + + {/* Main Content Area */} +
+
+

+ Protocol Journal +

+
+
+ {[1, 2, 3].map(i => ( +
+ ))} +
+ +42 +
+
+ +
+
+ +
+ + {view === 'feed' && ( + +
+
+

Intelligence Feed

+

Encrypted decentralized content streams

+
+
+ {["Trending", "Latest", "Curated"].map(f => ( + + ))} +
+
+ +
+ {mockPosts.map((post) => ( + setSelectedPost(post)} /> + ))} +
+
+ )} + + {view === 'editor' && ( + +
+

New Transmission

+

Mint your thoughts to the immutable ledger

+
+ +
+ +
+ + +
+