diff --git a/ARCHITECTURE_DIAGRAM.md b/ARCHITECTURE_DIAGRAM.md index d35faad..59dade8 100644 --- a/ARCHITECTURE_DIAGRAM.md +++ b/ARCHITECTURE_DIAGRAM.md @@ -310,6 +310,7 @@ User Action Frontend Backend Database ``` This architecture ensures: + - โœ… Scalability - โœ… Security - โœ… Performance diff --git a/CONTRIBUTING_GIT.md b/CONTRIBUTING_GIT.md index e45a82e..530a979 100644 --- a/CONTRIBUTING_GIT.md +++ b/CONTRIBUTING_GIT.md @@ -1,6 +1,7 @@ # Git Commit Guidelines -To keep project history clear and professional, this repository enforces [Conventional Commits](https://www.conventionalcommits.org/). +To keep project history clear and professional, this repository enforces +[Conventional Commits](https://www.conventionalcommits.org/). ## Commit Format @@ -45,4 +46,5 @@ Invalid: ## Contributor Coordination Rule -When expressing interest in an issue, include an ETA of no more than 2 days so maintainers can coordinate work efficiently. +When expressing interest in an issue, include an ETA of no more than 2 days so maintainers can +coordinate work efficiently. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index cb6f7d6..863846c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,6 +1,7 @@ # ๐Ÿ† Contribution Hall of Fame -Welcome to the Web3 Student Lab! This project thrives on community effort. Below are the contributors who have helped build this hub. +Welcome to the Web3 Student Lab! This project thrives on community effort. Below are the +contributors who have helped build this hub. ## ๐Ÿ’ป Code & Technical diff --git a/FRONTEND_IMPLEMENTATION_SUMMARY.md b/FRONTEND_IMPLEMENTATION_SUMMARY.md index db8174a..4303aa6 100644 --- a/FRONTEND_IMPLEMENTATION_SUMMARY.md +++ b/FRONTEND_IMPLEMENTATION_SUMMARY.md @@ -2,17 +2,19 @@ ## Overview -A complete, production-ready frontend has been implemented for the Web3 Student Lab platform. The implementation includes full authentication, course management, dashboard functionality, and blockchain certificate verification integrated with Soroban smart contracts. +A complete, production-ready frontend has been implemented for the Web3 Student Lab platform. The +implementation includes full authentication, course management, dashboard functionality, and +blockchain certificate verification integrated with Soroban smart contracts. ## โœ… Completed Features ### 1. Authentication System + - **Login Page** (`/auth/login`) - Email/password authentication - JWT token management - Session persistence - Error handling and validation - - **Registration Page** (`/auth/register`) - New user registration - Password confirmation @@ -26,6 +28,7 @@ A complete, production-ready frontend has been implemented for the Web3 Student - Logout functionality ### 2. Dashboard & User Features + - **Student Dashboard** (`/dashboard`) - Statistics overview (courses, enrollments, certificates) - Recent courses display @@ -34,6 +37,7 @@ A complete, production-ready frontend has been implemented for the Web3 Student - Quick navigation ### 3. Course Management + - **Course Catalog** (`/courses`) - Browse all available courses - Search functionality @@ -48,6 +52,7 @@ A complete, production-ready frontend has been implemented for the Web3 Student - Course syllabus preview ### 4. Blockchain Integration + - **Certificate Verification** (`/verify`) - Public verification page - Soroban blockchain integration @@ -57,6 +62,7 @@ A complete, production-ready frontend has been implemented for the Web3 Student - Educational content about blockchain verification ### 5. Landing Page + - **Home Page** (`/`) - Hero section with CTA - Feature highlights @@ -66,6 +72,7 @@ A complete, production-ready frontend has been implemented for the Web3 Student - Footer with links ### 6. Infrastructure + - **API Client** (`/src/lib/api.ts`) - Typed API endpoints - Axios configuration @@ -122,16 +129,19 @@ frontend/src/ ## ๐ŸŽจ Design Features ### Responsive Design + - Mobile-first approach - Breakpoints: sm (640px), md (768px), lg (1024px) - Adaptive layouts for all screen sizes ### Dark Mode + - Automatic based on system preferences - Consistent dark theme across all pages - Proper color contrast ratios ### UI Components + - Gradient backgrounds - Card-based layouts - Hover effects and transitions @@ -140,6 +150,7 @@ frontend/src/ - Toast notifications (via browser alerts) ### Accessibility + - Semantic HTML - ARIA labels where needed - Keyboard navigation support @@ -149,6 +160,7 @@ frontend/src/ ## ๐Ÿ” Security Implementation ### Authentication + - JWT token storage in localStorage - Automatic token attachment to API requests - Token expiration handling @@ -156,6 +168,7 @@ frontend/src/ - Secure password requirements ### API Security + - CORS configuration - Request validation - Error message sanitization @@ -164,12 +177,14 @@ frontend/src/ ## ๐Ÿ“Š State Management ### React Context + - `AuthContext` for global authentication state - User data persistence - Login/logout state synchronization - Error state management ### Local Storage + - JWT tokens - User session data - Persistent login across refreshes @@ -186,12 +201,15 @@ frontend/src/ ## ๐Ÿงช Build & Testing ### Build Status + โœ… Production build successful + - No TypeScript errors - All pages generated - Static and dynamic routes configured ### Routes Generated + - `/` - Static - `/auth/login` - Static - `/auth/register` - Static @@ -209,6 +227,7 @@ frontend/src/ ## ๐Ÿ”„ Integration Points ### Backend Integration + All frontend API calls are configured to work with existing backend endpoints: - โœ… Authentication endpoints (`/api/auth/*`) @@ -219,6 +238,7 @@ All frontend API calls are configured to work with existing backend endpoints: - โœ… Dashboard (`/api/dashboard/*`) ### Blockchain Integration + Prepared for Soroban contract integration: - โš ๏ธ Certificate verification function (placeholder until contract deployed) @@ -228,13 +248,16 @@ Prepared for Soroban contract integration: ## ๐ŸŽฏ Next Steps for Full Integration ### Immediate (Required) + 1. **Start Backend Server** + ```bash cd backend npm run dev ``` 2. **Configure Environment** + ```bash cd frontend cp .env.local.example .env.local @@ -246,6 +269,7 @@ Prepared for Soroban contract integration: - Create test users ### Short Term + 4. **Deploy Soroban Contract** - Deploy certificate contract to Stellar testnet - Update `NEXT_PUBLIC_CERTIFICATE_CONTRACT_ID` @@ -255,6 +279,7 @@ Prepared for Soroban contract integration: - Issue Certificate โ†’ Verify on Blockchain ### Long Term + 6. **Production Deployment** - Set up hosting (Vercel/Netlify) - Configure production environment @@ -272,6 +297,7 @@ Prepared for Soroban contract integration: ## โœจ Highlights ### Code Quality + - โœ… Fully typed with TypeScript - โœ… Consistent code style - โœ… Component-based architecture @@ -279,6 +305,7 @@ Prepared for Soroban contract integration: - โœ… Clean separation of concerns ### User Experience + - โœ… Smooth animations and transitions - โœ… Loading states for all async operations - โœ… Error feedback to users @@ -286,6 +313,7 @@ Prepared for Soroban contract integration: - โœ… Professional design ### Developer Experience + - โœ… Hot reload enabled - โœ… TypeScript error checking - โœ… Clear file organization @@ -305,6 +333,7 @@ Prepared for Soroban contract integration: ## ๐ŸŽ“ Learning Resources Implemented The frontend includes several educational features: + - Interactive certificate verification demo - Course browsing with search - Progress tracking dashboard @@ -314,6 +343,7 @@ The frontend includes several educational features: ## ๐Ÿ”ฎ Future Enhancements Potential features to add: + - Wallet connection for Web3 authentication - NFT certificate minting - Gamification (badges, points) @@ -327,13 +357,15 @@ Potential features to add: ## Summary -The frontend is **fully implemented and production-ready** for all core features except blockchain contract deployment. It provides a modern, responsive, and secure user interface that seamlessly integrates with the backend API and is prepared for Soroban smart contract integration. +The frontend is **fully implemented and production-ready** for all core features except blockchain +contract deployment. It provides a modern, responsive, and secure user interface that seamlessly +integrates with the backend API and is prepared for Soroban smart contract integration. **Build Status**: โœ… Successful **Type Safety**: โœ… 100% TypeScript **Responsive**: โœ… Mobile to Desktop **Accessibility**: โœ… Basic compliance **Performance**: โœ… Optimized build -**Documentation**: โœ… Comprehensive +**Documentation**: โœ… Comprehensive Ready for testing and deployment! ๐Ÿš€ diff --git a/INTEGRATION_GUIDE.md b/INTEGRATION_GUIDE.md index db1a271..74a0d9f 100644 --- a/INTEGRATION_GUIDE.md +++ b/INTEGRATION_GUIDE.md @@ -1,6 +1,7 @@ # Full Stack Integration Guide -This guide explains how the frontend, backend, and smart contract work together in the Web3 Student Lab platform. +This guide explains how the frontend, backend, and smart contract work together in the Web3 Student +Lab platform. ## Architecture Overview @@ -31,9 +32,9 @@ All API communication happens through the typed client in `/frontend/src/lib/api ```typescript // Frontend: User login -const { user, token } = await authAPI.login({ - email: 'student@example.com', - password: 'password123' +const { user, token } = await authAPI.login({ + email: 'student@example.com', + password: 'password123', }); // Token is automatically stored in localStorage @@ -46,22 +47,23 @@ const courses = await coursesAPI.getAll(); #### Backend API Endpoints -| Endpoint | Method | Frontend Function | Description | -|----------|--------|-------------------|-------------| -| `/auth/register` | POST | `authAPI.register()` | Register new student | -| `/auth/login` | POST | `authAPI.login()` | Student login | -| `/auth/me` | GET | `authAPI.getCurrentUser()` | Get current user | -| `/courses` | GET | `coursesAPI.getAll()` | List all courses | -| `/courses/:id` | GET | `coursesAPI.getById()` | Get course details | -| `/enrollments` | POST | `enrollmentsAPI.enroll()` | Enroll in course | -| `/certificates/student/:id` | GET | `certificatesAPI.getByStudentId()` | Get student certificates | -| `/verify` | GET | `verifyCertificateOnChain()` | Verify on blockchain | +| Endpoint | Method | Frontend Function | Description | +| --------------------------- | ------ | ---------------------------------- | ------------------------ | +| `/auth/register` | POST | `authAPI.register()` | Register new student | +| `/auth/login` | POST | `authAPI.login()` | Student login | +| `/auth/me` | GET | `authAPI.getCurrentUser()` | Get current user | +| `/courses` | GET | `coursesAPI.getAll()` | List all courses | +| `/courses/:id` | GET | `coursesAPI.getById()` | Get course details | +| `/enrollments` | POST | `enrollmentsAPI.enroll()` | Enroll in course | +| `/certificates/student/:id` | GET | `certificatesAPI.getByStudentId()` | Get student certificates | +| `/verify` | GET | `verifyCertificateOnChain()` | Verify on blockchain | ### 2. Backend โ†” Database The backend uses Prisma ORM to interact with PostgreSQL. **Schema Models**: + - `Student` - User accounts - `Course` - Available courses - `Enrollment` - Course enrollments @@ -69,6 +71,7 @@ The backend uses Prisma ORM to interact with PostgreSQL. - `Feedback` - Course reviews **Database Connection**: + ```typescript // Backend: src/db/index.ts import { PrismaClient } from '@prisma/client'; @@ -81,10 +84,12 @@ export default prisma; The backend integrates with Soroban contracts for certificate issuance and verification. **Contract Functions**: + - `issue(symbol, student, course_name)` - Issue certificate on-chain - `get_certificate(symbol)` - Retrieve certificate from blockchain **Integration Point**: + ```typescript // Backend: When issuing a certificate await prisma.certificate.create({ @@ -92,7 +97,7 @@ await prisma.certificate.create({ studentId, courseId, status: 'pending', // Will be updated after on-chain issuance - } + }, }); // Then call Soroban contract @@ -116,24 +121,28 @@ const certData = await verifyCertificateOnChain('CERTIFICATE_SYMBOL'); ### Example 1: Student Enrollment Flow 1. **Frontend**: Student browses courses (`/courses`) + ```typescript const courses = await coursesAPI.getAll(); ``` 2. **Backend**: Fetches courses from database + ```typescript const courses = await prisma.course.findMany(); ``` 3. **Frontend**: Student clicks "Enroll" on course detail page + ```typescript await enrollmentsAPI.enroll(user.id, course.id); ``` 4. **Backend**: Creates enrollment record + ```typescript const enrollment = await prisma.enrollment.create({ - data: { studentId, courseId, status: 'active' } + data: { studentId, courseId, status: 'active' }, }); ``` @@ -142,33 +151,36 @@ const certData = await verifyCertificateOnChain('CERTIFICATE_SYMBOL'); ### Example 2: Certificate Issuance & Verification 1. **Backend**: Student completes course + ```typescript // Create certificate in database const cert = await prisma.certificate.create({ data: { studentId, courseId, - status: 'pending' - } + status: 'pending', + }, }); ``` 2. **Backend**: Issue on Soroban blockchain + ```typescript const symbol = `CERT-${course.code}-${student.id}`; await sorobanClient.issue(symbol, student.name, course.title); - + // Update certificate with on-chain hash await prisma.certificate.update({ where: { id: cert.id }, - data: { + data: { certificateHash: txHash, - status: 'issued' - } + status: 'issued', + }, }); ``` 3. **Frontend**: Display certificate to student + ```typescript const certificates = await certificatesAPI.getByStudentId(user.id); ``` @@ -295,6 +307,7 @@ curl -X POST http://localhost:8080/api/enrollments \ **Problem**: CORS errors or connection refused **Solutions**: + 1. Ensure backend is running: `curl http://localhost:8080/health` 2. Check `NEXT_PUBLIC_API_URL` in frontend `.env.local` 3. Verify backend CORS configuration allows `http://localhost:3000` @@ -304,6 +317,7 @@ curl -X POST http://localhost:8080/api/enrollments \ **Problem**: 401 Unauthorized errors **Solutions**: + 1. Clear browser localStorage 2. Re-login to get fresh token 3. Check JWT_SECRET matches in backend @@ -314,6 +328,7 @@ curl -X POST http://localhost:8080/api/enrollments \ **Problem**: Can't verify certificates on-chain **Solutions**: + 1. Check `NEXT_PUBLIC_CERTIFICATE_CONTRACT_ID` is set 2. Verify Soroban RPC endpoint is accessible 3. Ensure contract is deployed to the network @@ -402,4 +417,5 @@ app.use((req, res, next) => { --- -This integration ensures seamless communication between all three layers while maintaining security, performance, and scalability. +This integration ensures seamless communication between all three layers while maintaining security, +performance, and scalability. diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md index 1ebbd03..d3ec815 100644 --- a/PR_DESCRIPTION.md +++ b/PR_DESCRIPTION.md @@ -1,17 +1,21 @@ # [Contract] RS-Token Burn Functionality ## Summary -Implemented a burn functionality for RS-Tokens that allows students or the contract owner to destroy tokens, reducing the total supply or removing them from specific wallets. + +Implemented a burn functionality for RS-Tokens that allows students or the contract owner to destroy +tokens, reducing the total supply or removing them from specific wallets. ## Implementation Details ### ๐Ÿ”ฅ Core Functionality + - **`burn(env, caller, student, token_id, amount)`** - Burns specified amount of RS-Tokens - **Authorization**: Only contract owner or the student themselves can burn tokens - **Validation**: Proper amount validation and balance checking - **Storage Optimization**: Removes balance entry when balance reaches zero ### ๐Ÿ“ข Event Emission + - **Burned Event**: Emitted on successful burn operations with: - `burner`: Address that initiated the burn - `student`: Address whose tokens were burned @@ -19,12 +23,15 @@ Implemented a burn functionality for RS-Tokens that allows students or the contr - `amount`: Amount of tokens burned ### ๐Ÿ›ก๏ธ Error Handling + - `InsufficientBalance`: When trying to burn more tokens than available - `NotAuthorized`: When unauthorized users attempt to burn tokens - `InvalidAmount`: When burn amount is zero or negative ### ๐Ÿงช Test Coverage + Added comprehensive test suite covering: + - โœ… Student can burn own tokens - โœ… Owner can burn student tokens - โœ… Unauthorized users cannot burn tokens @@ -35,12 +42,15 @@ Added comprehensive test suite covering: ## Technical Changes ### New Data Key + - `Owner`: Stores the contract owner address for authorization ### New Error Type + - `InsufficientBalance = 5`: For insufficient balance scenarios ### Event Structure + ```rust env.events().publish( ("Burned", "burner", "student", "token_id", "amount"), @@ -51,29 +61,35 @@ env.events().publish( ## Usage Examples ### Student Burning Own Tokens + ```rust // Student burns 50 of their own tokens contract.burn(&student_address, &student_address, &1, &50); ``` ### Owner Burning Student Tokens + ```rust // Owner burns 30 tokens from student's balance contract.burn(&owner_address, &student_address, &1, &30); ``` ## Security Considerations + - Proper authorization checks prevent unauthorized token burning - Balance validation prevents underflow scenarios - Event emission provides transparency for all burn operations - Storage optimization removes zero-balance entries to save gas ## Testing + All tests pass successfully: + ```bash cargo test token # 12 tests passed, 0 failed ``` ## Level: Intermediate โœ… + This implementation provides a secure and efficient burn mechanism suitable for production use. diff --git a/PR_DESCRIPTION_METADATA.md b/PR_DESCRIPTION_METADATA.md index 68b4863..1a6ed2e 100644 --- a/PR_DESCRIPTION_METADATA.md +++ b/PR_DESCRIPTION_METADATA.md @@ -1,34 +1,41 @@ # [Contract] Token Metadata & URI Support ## Summary -Implemented comprehensive token metadata and URI support for RS-Tokens, following SEP-style patterns and providing standardized format for frontend display. + +Implemented comprehensive token metadata and URI support for RS-Tokens, following SEP-style patterns +and providing standardized format for frontend display. ## Implementation Details ### ๐Ÿ“Š Core Metadata Structure + - **`TokenMetadata` struct** with name, symbol, decimals, and URI fields - **SEP-compliant format** following Stellar ecosystem standards - **Frontend-ready** structure for easy integration ### ๐Ÿ” Metadata Retrieval + - **`get_metadata()`** - Returns complete token metadata in standardized format - **Default values** initialized during contract deployment - **Error handling** with `MetadataNotFound` for missing metadata ### โš™๏ธ Admin URI Management + - **`update_uri(caller, new_uri)`** - Admin-only URI updates - **Authorization** ensures only contract owner can modify metadata - **Event emission** tracks all URI changes for transparency ### ๐ŸŽฏ Default Token Information + - **Name**: "RS-Token" -- **Symbol**: "RST" +- **Symbol**: "RST" - **Decimals**: 0 (non-divisible tokens) - **URI**: "https://metadata.web3-student-lab.com/token/{id}" ## Technical Changes ### New Data Structures + ```rust #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -41,6 +48,7 @@ pub struct TokenMetadata { ``` ### Enhanced DataKey Enum + ```rust enum DataKey { // ... existing keys ... @@ -49,11 +57,13 @@ enum DataKey { ``` ### New Error Type + ```rust MetadataNotFound = 8, // When metadata is not initialized ``` ### Storage Integration + - **Metadata initialization** in `init()` function - **Persistent storage** using instance storage - **Efficient retrieval** with proper error handling @@ -61,6 +71,7 @@ MetadataNotFound = 8, // When metadata is not initialized ## Usage Examples ### Getting Token Metadata + ```rust // Retrieve complete token information let metadata = contract.get_metadata(); @@ -68,6 +79,7 @@ let metadata = contract.get_metadata(); ``` ### Updating Token URI (Admin Only) + ```rust // Only contract owner can update URI contract.update_uri(&owner_address, &"https://new-metadata.example.com/token/{id}"); @@ -76,6 +88,7 @@ contract.update_uri(&owner_address, &"https://new-metadata.example.com/token/{id ## Frontend Integration ### ๐ŸŒ **Standardized Output Format** + The `get_metadata()` function returns a structure perfect for frontend consumption: ```javascript @@ -87,6 +100,7 @@ console.log(`Metadata URI: ${metadata.uri}`); ``` ### ๐Ÿ“ฑ **Display Ready** + - **Token names** for UI display - **Symbols** for compact display (RST) - **Decimals** for proper formatting @@ -95,6 +109,7 @@ console.log(`Metadata URI: ${metadata.uri}`); ## Event System ### ๐Ÿ“ข **URI Update Events** + ```rust // Emitted when URI is updated env.events().publish( @@ -104,6 +119,7 @@ env.events().publish( ``` ### ๐Ÿ” **Event Tracking** + - **Transparency**: All URI changes are logged - **Audit trail**: Complete history of metadata updates - **Frontend integration**: Real-time metadata change notifications @@ -111,16 +127,19 @@ env.events().publish( ## Security Considerations ### ๐Ÿ”’ **Admin-Only Updates** + - **Owner verification** prevents unauthorized metadata changes - **Authorization checks** ensure only privileged addresses can modify URI - **Error handling** prevents unauthorized access ### ๐Ÿ›ก๏ธ **Data Validation** + - **Type safety** with Rust's type system - **Error boundaries** prevent metadata corruption - **Fallback values** ensure system stability ### ๐Ÿ“‹ **SEP Compliance** + - **Standard format** follows Stellar Enhancement Proposals - **Interoperability** with existing wallet and exchange systems - **Future-proof** design for ecosystem evolution @@ -128,6 +147,7 @@ env.events().publish( ## Testing Coverage ### โœ… **Comprehensive Test Suite** + - Default metadata initialization verification - Owner URI update functionality - Unauthorized update rejection @@ -135,12 +155,14 @@ env.events().publish( - Event emission confirmation ### ๐Ÿงช **Test Results** + ```bash cargo test token # 22 tests passed, 0 failed ``` ### ๐Ÿ“Š **Test Categories** + - **Initialization**: Metadata properly set during contract deployment - **Authorization**: Only owners can update metadata - **Functionality**: URI updates work correctly @@ -149,16 +171,19 @@ cargo test token ## Integration Benefits ### ๐ŸŽ“ **Educational Ecosystem** + - **Professional appearance** with proper token metadata - **Exchange readiness** with standard token information - **Student recognition** through branded tokens ### ๐Ÿ”„ **Backward Compatibility** + - **No breaking changes** to existing functionality - **Optional features** that enhance without disrupting - **Seamless upgrade** path for existing deployments ### ๐Ÿ“ˆ **Scalability** + - **Efficient storage** with minimal gas overhead - **Cachable metadata** for frontend performance - **Flexible URI templates** for various metadata providers @@ -166,13 +191,17 @@ cargo test token ## URI Template Support ### ๐Ÿ”— **Dynamic Token IDs** + The default URI template supports dynamic token ID insertion: + ``` https://metadata.web3-student-lab.com/token/{id} ``` ### ๐Ÿ“ **Custom Metadata** + Admins can update to any URI template: + ``` https://ipfs.io/ipfs/{hash} // IPFS-based metadata https://api.example.com/metadata/{id} // Custom API @@ -180,9 +209,13 @@ https://gateway.pinata.cloud/{cid} // Pinata gateway ``` ## Level: Intermediate โœ… -This implementation provides a complete, SEP-compliant metadata system that enhances the RS-Token contract with professional-grade token information management suitable for production educational platforms. + +This implementation provides a complete, SEP-compliant metadata system that enhances the RS-Token +contract with professional-grade token information management suitable for production educational +platforms. ## Next Steps + 1. **PR Review**: Visit https://github.com/success-OG/Web3-Student-Lab/pull/new/feat/meta-data 2. **Frontend Integration**: Update UI to display token metadata 3. **Metadata Hosting**: Set up off-chain JSON metadata server diff --git a/PR_DESCRIPTION_RESTRICTED_TRANSFER.md b/PR_DESCRIPTION_RESTRICTED_TRANSFER.md index 46b6066..a16fca5 100644 --- a/PR_DESCRIPTION_RESTRICTED_TRANSFER.md +++ b/PR_DESCRIPTION_RESTRICTED_TRANSFER.md @@ -1,30 +1,38 @@ # [Contract] Restricted Transfer Logic ## Summary -Implemented a "Whitelisted Transfer" system for RS-Tokens that allows transfers only between verified students with active student profiles/enrollments, ensuring tokens remain within the educational ecosystem. + +Implemented a "Whitelisted Transfer" system for RS-Tokens that allows transfers only between +verified students with active student profiles/enrollments, ensuring tokens remain within the +educational ecosystem. ## Implementation Details ### ๐Ÿ”„ Core Transfer Functionality + - **`transfer(from, to, token_id, amount)`** - Transfers RS-Tokens between verified students only -- **Student Verification**: Both sender and recipient must have `Role::Student` in the certificate contract +- **Student Verification**: Both sender and recipient must have `Role::Student` in the certificate + contract - **Authorization**: Only token owner can initiate transfers - **Balance Validation**: Proper balance checking and zero-balance cleanup ### ๐Ÿ“ข Event Emission + - **Transferred Event**: Emitted on successful transfers with: - `from`: Address of the sender - - `to`: Address of the recipient + - `to`: Address of the recipient - `token_id`: ID of the token type transferred - `amount`: Amount of tokens transferred ### ๐Ÿ›ก๏ธ Security & Validation + - `NotStudent`: When either sender or recipient is not a verified student - `InvalidAmount`: When transfer amount is zero or negative - `InsufficientBalance`: When sender lacks sufficient tokens - `TransferFailed`: General transfer failure protection ### ๐Ÿ”— Integration with Certificate Contract + - **Cross-Contract Calls**: Uses `CertificateContractClient` to verify student roles - **Role-Based Access**: Leverages existing RBAC system for student verification - **Seamless Integration**: Works with existing certificate and token minting system @@ -32,12 +40,14 @@ Implemented a "Whitelisted Transfer" system for RS-Tokens that allows transfers ## Technical Changes ### New Error Types + ```rust NotStudent = 6, // Either party not a verified student TransferFailed = 7, // General transfer failure ``` ### Student Verification Helper + ```rust fn require_both_students(env: &Env, from: &Address, to: &Address) { // Verifies both addresses have Role::Student in certificate contract @@ -46,6 +56,7 @@ fn require_both_students(env: &Env, from: &Address, to: &Address) { ``` ### Event Structure + ```rust env.events().publish( ("Transferred", "from", "to", "token_id", "amount"), @@ -56,12 +67,14 @@ env.events().publish( ## Usage Examples ### Successful Student-to-Student Transfer + ```rust // Both students must have Role::Student in certificate contract contract.transfer(&student_a, &student_b, &1, &50); ``` ### Failed Transfer - Non-Student Recipient + ```rust // This will fail with NotStudent error contract.transfer(&student_a, &non_student, &1, &50); @@ -70,16 +83,19 @@ contract.transfer(&student_a, &non_student, &1, &50); ## Security Considerations ### ๐Ÿ”’ **Whitelisted System** + - Only verified students can participate in transfers - Prevents tokens from leaving the educational ecosystem - Maintains token value within intended user base ### ๐Ÿ›ก๏ธ **Cross-Contract Security** + - Secure integration with certificate contract's role system - Proper error handling for cross-contract calls - No single point of failure in verification process ### ๐Ÿ’พ **Storage Optimization** + - Removes zero-balance entries to save gas - Efficient balance updates with minimal storage operations - Follows existing token contract patterns @@ -87,6 +103,7 @@ contract.transfer(&student_a, &non_student, &1, &50); ## Testing Coverage ### โœ… **Comprehensive Test Suite** + - Transfer between verified students succeeds - Insufficient balance transfers fail appropriately - Zero amount transfers are rejected @@ -94,6 +111,7 @@ contract.transfer(&student_a, &non_student, &1, &50); - Zero-balance entries are properly cleaned up ### ๐Ÿงช **Test Results** + ```bash cargo test token # 17 tests passed, 0 failed @@ -102,24 +120,31 @@ cargo test token ## Integration Benefits ### ๐ŸŽ“ **Educational Ecosystem** + - Ensures tokens remain within student community - Supports token-based educational incentives - Maintains ecosystem integrity ### ๐Ÿ”„ **Backward Compatibility** + - Fully compatible with existing minting and burning - No changes to current token structure - Seamless upgrade path ### ๐Ÿ“ˆ **Scalability** + - Efficient verification process - Minimal gas overhead for transfers - Scales with growing student base ## Level: Intermediate โœ… -This implementation provides a secure, efficient, and well-integrated whitelisted transfer system suitable for production educational token ecosystems. + +This implementation provides a secure, efficient, and well-integrated whitelisted transfer system +suitable for production educational token ecosystems. ## Next Steps -1. **PR Review**: Visit https://github.com/success-OG/Web3-Student-Lab/pull/new/feat/restricted-transfer + +1. **PR Review**: Visit + https://github.com/success-OG/Web3-Student-Lab/pull/new/feat/restricted-transfer 2. **Integration Testing**: Test with live certificate contract 3. **Documentation**: Update user guides for transfer functionality diff --git a/README_COMPLETE.md b/README_COMPLETE.md index 19f7d5a..4dcb736 100644 --- a/README_COMPLETE.md +++ b/README_COMPLETE.md @@ -1,10 +1,13 @@ # Web3 Student Lab - Complete Platform Documentation -A full-stack blockchain education platform with Soroban smart contracts, Stellar blockchain integration, and modern React frontend. +A full-stack blockchain education platform with Soroban smart contracts, Stellar blockchain +integration, and modern React frontend. ## ๐ŸŽฏ Project Overview -Web3 Student Lab is a comprehensive learning platform that teaches blockchain development through hands-on courses. Students can enroll in courses, complete lessons, and earn verifiable certificates stored on the Stellar blockchain using Soroban smart contracts. +Web3 Student Lab is a comprehensive learning platform that teaches blockchain development through +hands-on courses. Students can enroll in courses, complete lessons, and earn verifiable certificates +stored on the Stellar blockchain using Soroban smart contracts. ## ๐Ÿ—๏ธ Architecture @@ -22,6 +25,7 @@ web3-student-lab/ ### Technology Stack **Frontend**: + - Next.js 16 (App Router) - React 19 - TypeScript 5 @@ -30,6 +34,7 @@ web3-student-lab/ - @stellar/stellar-sdk **Backend**: + - Node.js + Express - TypeScript - Prisma ORM @@ -38,6 +43,7 @@ web3-student-lab/ - bcryptjs **Smart Contracts**: + - Rust - Soroban SDK - Stellar Blockchain @@ -45,6 +51,7 @@ web3-student-lab/ ## โœ… Implementation Status ### Frontend - COMPLETE โœ… + - [x] Authentication (Login/Register) - [x] Student Dashboard - [x] Course Catalog @@ -56,6 +63,7 @@ web3-student-lab/ - [x] Blockchain Integration (Soroban ready) ### Backend - COMPLETE โœ… + - [x] RESTful API - [x] JWT Authentication - [x] User Management @@ -67,6 +75,7 @@ web3-student-lab/ - [x] Error Handling ### Smart Contract - READY โš ๏ธ + - [x] Certificate Contract (Rust) - [x] Issue Function - [x] Verify Function @@ -90,6 +99,7 @@ web3-student-lab/ ``` This script will: + 1. Install all dependencies 2. Create environment files 3. Set up the database @@ -206,11 +216,13 @@ The platform uses JWT-based authentication: ## ๐Ÿ“ฆ API Endpoints ### Authentication + - `POST /api/auth/register` - Register new student - `POST /api/auth/login` - Student login - `GET /api/auth/me` - Get current user ### Courses + - `GET /api/courses` - List all courses - `GET /api/courses/:id` - Get course details - `POST /api/courses` - Create course (admin) @@ -218,6 +230,7 @@ The platform uses JWT-based authentication: - `DELETE /api/courses/:id` - Delete course ### Enrollments + - `GET /api/enrollments` - List enrollments - `GET /api/enrollments/student/:id` - Student's enrollments - `POST /api/enrollments` - Enroll in course @@ -225,6 +238,7 @@ The platform uses JWT-based authentication: - `DELETE /api/enrollments/:id` - Cancel enrollment ### Certificates + - `GET /api/certificates` - List all certificates - `GET /api/certificates/:id` - Get certificate - `GET /api/certificates/student/:id` - Student's certificates @@ -233,11 +247,13 @@ The platform uses JWT-based authentication: - `DELETE /api/certificates/:id` - Revoke certificate ### Feedback + - `POST /api/feedback` - Submit course feedback - `GET /api/feedback/course/:id` - Course feedback - `GET /api/feedback/course/:id/summary` - Feedback summary ### Dashboard + - `GET /api/dashboard/stats` - Platform statistics - `GET /api/dashboard/student/:id` - Student dashboard data @@ -253,7 +269,7 @@ impl CertificateContract { pub fn issue(env: Env, symbol: Symbol, student: String, course_name: String) -> Certificate { // Issues certificate to blockchain } - + pub fn get_certificate(env: Env, symbol: Symbol) -> Certificate { // Retrieves certificate from blockchain } @@ -313,7 +329,7 @@ model Student { lastName String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - + enrollments Enrollment[] certificates Certificate[] feedback Feedback[] @@ -327,7 +343,7 @@ model Course { credits Int @default(3) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - + enrollments Enrollment[] certificates Certificate[] feedback Feedback[] @@ -340,7 +356,7 @@ model Certificate { issuedAt DateTime @default(now()) certificateHash String? status String @default("pending") - + student Student @relation(fields: [studentId], references: [id]) course Course @relation(fields: [courseId], references: [id]) } @@ -351,10 +367,10 @@ model Enrollment { courseId String enrolledAt DateTime @default(now()) status String @default("active") - + student Student @relation(fields: [studentId], references: [id]) course Course @relation(fields: [courseId], references: [id]) - + @@unique([studentId, courseId]) } @@ -366,10 +382,10 @@ model Feedback { review String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - + student Student @relation(fields: [studentId], references: [id]) course Course @relation(fields: [courseId], references: [id]) - + @@unique([studentId, courseId]) } ``` @@ -392,14 +408,17 @@ model Feedback { ## ๐Ÿ”ง Development Tools ### Backend + - Prisma Studio: `npx prisma studio` - Database migrations: `npx prisma migrate dev` ### Frontend + - Build analyzer: `npm run build` - Linting: `npm run lint` ### Contracts + - Build: `cargo build --release` - Test: `cargo test` - Deploy: `soroban contract deploy` @@ -407,16 +426,19 @@ model Feedback { ## ๐Ÿ› Troubleshooting ### Backend won't start + - Check PostgreSQL is running - Verify DATABASE_URL in .env - Run `npx prisma generate` ### Frontend build errors + - Clear `.next` folder - Delete `node_modules` and reinstall - Check TypeScript version ### Database connection issues + - Ensure PostgreSQL is running - Check database credentials - Verify database exists diff --git a/backend/package.json b/backend/package.json index a37e541..b719731 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "build": "tsc", + "build": "prisma generate && tsc", "start": "node dist/index.js", "dev": "tsx watch src/index.ts", "test": "NODE_ENV=test jest", diff --git a/backend/src/blockchain/blockchain.service.ts b/backend/src/blockchain/blockchain.service.ts index 588da0d..68d6440 100644 --- a/backend/src/blockchain/blockchain.service.ts +++ b/backend/src/blockchain/blockchain.service.ts @@ -19,10 +19,12 @@ export const getStudentAchievements = async (studentId: string): Promise ({ - id: cert.id, - txHash: cert.certificateHash || `0x${Math.random().toString(16).substring(2, 40)}`, - timestamp: cert.issuedAt, - status: cert.certificateHash ? 'verified' : 'pending', - })); + return certificates.map( + (cert: (typeof certificates)[number]): BlockchainRecord => ({ + id: cert.id, + txHash: cert.certificateHash || `0x${Math.random().toString(16).substring(2, 40)}`, + timestamp: cert.issuedAt, + status: cert.certificateHash ? 'verified' : 'pending', + }) + ); }; diff --git a/backend/src/blockchain/stellarUtils.ts b/backend/src/blockchain/stellarUtils.ts index c4fcadb..1f6fa6b 100644 --- a/backend/src/blockchain/stellarUtils.ts +++ b/backend/src/blockchain/stellarUtils.ts @@ -64,7 +64,7 @@ export async function checkAccountExists(publicKey: string): Promise ({ - asset: b.asset_type === 'native' ? 'XLM' : ('asset_code' in b ? b.asset_code : b.asset_type), + asset: b.asset_type === 'native' ? 'XLM' : 'asset_code' in b ? b.asset_code : b.asset_type, balance: b.balance, })); diff --git a/backend/src/dashboard/dashboard.routes.ts b/backend/src/dashboard/dashboard.routes.ts index a4cf362..8ec1cd9 100644 --- a/backend/src/dashboard/dashboard.routes.ts +++ b/backend/src/dashboard/dashboard.routes.ts @@ -18,11 +18,11 @@ router.get('/stats', async (req: Request, res: Response) => { }); /** - * @route GET /api/dashboard/:studentId + * @route GET /api/dashboard/student/:studentId * @desc Get accurate student profile and achievements aggregated from all modules * @access Public (should apply auth globally later) */ -router.get('/:studentId', async (req: Request, res: Response) => { +router.get('/student/:studentId', async (req: Request, res: Response) => { try { const { studentId } = req.params; diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index b219f1e..4c6e308 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -10,7 +10,7 @@ const pool = new pg.Pool({ connectionString }); const adapter = new PrismaPg(pool); const globalForPrisma = globalThis as unknown as { - prisma: any | undefined; + prisma: PrismaClient | undefined; }; export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter }); diff --git a/backend/src/feedback/feedback.routes.ts b/backend/src/feedback/feedback.routes.ts index efb4530..143a4fb 100644 --- a/backend/src/feedback/feedback.routes.ts +++ b/backend/src/feedback/feedback.routes.ts @@ -1,12 +1,12 @@ -import { Router, Request, Response } from 'express'; +import { Request, Response, Router } from 'express'; import { authenticate } from '../auth/auth.middleware.js'; import { - createFeedback, - getFeedbackByCourse, - getFeedbackByStudentAndCourse, - updateFeedback, - deleteFeedback, - getCourseRatingSummary, + createFeedback, + deleteFeedback, + getCourseRatingSummary, + getFeedbackByCourse, + getFeedbackByStudentAndCourse, + updateFeedback, } from './feedback.service.js'; import { CreateFeedbackRequest, UpdateFeedbackRequest } from './types.js'; @@ -34,7 +34,7 @@ router.post('/', authenticate, async (req: Request, res: Response) => { review: review || undefined, }); res.status(201).json(feedback); - } catch (error: any) { + } catch (error: unknown) { if (error instanceof Error) { if (error.message === 'Course not found') { res.status(404).json({ error: error.message }); @@ -67,7 +67,7 @@ router.get('/course/:courseId', async (req: Request, res: Response) => { } const feedback = await getFeedbackByCourse(courseId); res.json(feedback); - } catch (error: any) { + } catch (error: unknown) { if (error instanceof Error && error.message === 'Course not found') { res.status(404).json({ error: error.message }); return; @@ -90,7 +90,7 @@ router.get('/course/:courseId/summary', async (req: Request, res: Response) => { } const summary = await getCourseRatingSummary(courseId); res.json(summary); - } catch (error: any) { + } catch (error: unknown) { if (error instanceof Error && error.message === 'Course not found') { res.status(404).json({ error: error.message }); return; @@ -120,7 +120,7 @@ router.get('/my-feedback/:courseId', authenticate, async (req: Request, res: Res } res.json(feedback); - } catch (error: any) { + } catch (_error: unknown) { res.status(500).json({ error: 'Failed to fetch feedback' }); } }); @@ -151,7 +151,7 @@ router.put('/:courseId', authenticate, async (req: Request, res: Response) => { review: review || undefined, }); res.json(feedback); - } catch (error: any) { + } catch (error: unknown) { if (error instanceof Error) { if (error.message === 'Feedback not found') { res.status(404).json({ error: error.message }); @@ -181,7 +181,7 @@ router.delete('/:courseId', authenticate, async (req: Request, res: Response) => } await deleteFeedback(studentId, courseId); res.status(204).send(); - } catch (error: any) { + } catch (error: unknown) { if (error instanceof Error && error.message === 'Feedback not found') { res.status(404).json({ error: error.message }); return; diff --git a/backend/src/routes/certificates.ts b/backend/src/routes/certificates.ts index f368e90..18eafff 100644 --- a/backend/src/routes/certificates.ts +++ b/backend/src/routes/certificates.ts @@ -1,4 +1,4 @@ -import { Router } from 'express'; +import { Request, Response, Router } from 'express'; const router = Router(); @@ -15,7 +15,7 @@ interface MockCertificate { let certificates: MockCertificate[] = []; // GET /api/certificates - Get all certificates -router.get('/', async (req, res) => { +router.get('/', async (req: Request, res: Response) => { try { res.json(certificates); } catch { @@ -24,7 +24,7 @@ router.get('/', async (req, res) => { }); // GET /api/certificates/:id - Get certificate by ID -router.get('/:id', async (req, res) => { +router.get('/:id', async (req: Request, res: Response) => { try { const { id } = req.params; const certificate = certificates.find((c) => c.id === id); @@ -40,7 +40,7 @@ router.get('/:id', async (req, res) => { }); // GET /api/certificates/student/:studentId - Get certificates by student -router.get('/student/:studentId', async (req, res) => { +router.get('/student/:studentId', async (req: Request, res: Response) => { try { const { studentId } = req.params; const studentCerts = certificates.filter((c) => c.studentId === studentId); @@ -51,7 +51,7 @@ router.get('/student/:studentId', async (req, res) => { }); // POST /api/certificates - Issue a new certificate -router.post('/', async (req, res) => { +router.post('/', async (req: Request, res: Response) => { try { const { studentId, courseId, certificateHash } = req.body; @@ -95,7 +95,7 @@ router.post('/', async (req, res) => { }); // PUT /api/certificates/:id - Update certificate status -router.put('/:id', async (req, res) => { +router.put('/:id', async (req: Request, res: Response) => { try { const { id } = req.params; const { status, certificateHash } = req.body; @@ -112,8 +112,28 @@ router.put('/:id', async (req, res) => { } }); +// GET /api/certificates/:id/verify - Verify a certificate on-chain +router.get('/:id/verify', async (req: Request, res: Response) => { + try { + const { id } = req.params; + const certificate = certificates.find((c) => c.id === id); + + if (!certificate) { + return res.status(404).json({ error: 'Certificate not found' }); + } + + // Mock on-chain verification + res.json({ + verified: !!certificate.certificateHash, + hash: certificate.certificateHash, + }); + } catch { + res.status(500).json({ error: 'Failed to verify certificate' }); + } +}); + // DELETE /api/certificates/:id - Revoke a certificate -router.delete('/:id', async (req, res) => { +router.delete('/:id', async (req: Request, res: Response) => { try { const { id } = req.params; certificates = certificates.filter((c) => c.id !== id); diff --git a/backend/src/routes/learning/learning.routes.ts b/backend/src/routes/learning/learning.routes.ts index 240048d..8012ce7 100644 --- a/backend/src/routes/learning/learning.routes.ts +++ b/backend/src/routes/learning/learning.routes.ts @@ -1,16 +1,16 @@ -import { Router, Request, Response } from 'express'; +import { Request, Response, Router } from 'express'; import { authenticate } from '../../auth/auth.middleware.js'; import { validateBody, validateParams, validateQuery } from '../../utils/validation.js'; import { - getCourseCurriculum, - getStudentProgress, - listCourses, - updateStudentProgress, + getCourseCurriculum, + getStudentProgress, + listCourses, + updateStudentProgress, } from './learning.service.js'; import { - courseParamsSchema, - coursesQuerySchema, - progressUpdateSchema, + courseParamsSchema, + coursesQuerySchema, + progressUpdateSchema, } from './validation.schemas.js'; const router = Router(); @@ -113,8 +113,8 @@ router.patch( const progress = await updateStudentProgress(req.user!.id, courseId, req.body); res.json({ progress }); - } catch (error: any) { - if (error && error.message === 'LESSON_NOT_FOUND') { + } catch (error: unknown) { + if (error instanceof Error && error.message === 'LESSON_NOT_FOUND') { res.status(404).json({ error: 'Lesson not found' }); return; } @@ -132,7 +132,7 @@ router.get('/modules', async (req: Request, res: Response) => { try { const modules = await getCourseCurriculum('course-1'); res.json({ modules: modules?.modules || [] }); - } catch (error) { + } catch (_error) { res.status(500).json({ error: 'Internal server error' }); } }); @@ -149,7 +149,7 @@ router.post('/progress/:userId/complete', async (req: Request, res: Response) => status: 'completed', }); res.json({ progress, message: 'Lesson marked as complete' }); - } catch (error) { + } catch (_error) { res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/backend/src/routes/learning/learning.service.ts b/backend/src/routes/learning/learning.service.ts index bbfc3e3..4819da2 100644 --- a/backend/src/routes/learning/learning.service.ts +++ b/backend/src/routes/learning/learning.service.ts @@ -1,17 +1,17 @@ import prisma from '../../db/index.js'; -import { COURSES, curriculumByCourseId, getCurriculumForCourse } from './curriculum.data.js'; +import { COURSES, getCurriculumForCourse } from './curriculum.data.js'; import { - CurriculumCourse, - Module, - Progress, - ProgressStatus, - ProgressUpdateInput, + CurriculumCourse, + Module, + Progress, + ProgressStatus, + ProgressUpdateInput, } from './types.js'; // In-memory mock store for demo resilience const mockProgressStore: Record = {}; -const toProgress = (progress: any): Progress => ({ +const toProgress = (progress: Progress): Progress => ({ id: progress.id, studentId: progress.studentId, courseId: progress.courseId, @@ -80,11 +80,11 @@ export const listCourses = async (difficulty?: string): Promise ({ + return courses.map((course: { id: string }) => ({ ...course, modules: filterModulesByDifficulty(getCurriculumForCourse(course.id), difficulty), })); - } catch (error) { + } catch (_error) { console.warn('Database error in listCourses, falling back to mock data'); const now = new Date(); return COURSES.map((course) => ({ @@ -134,7 +134,7 @@ export const getCourseCurriculum = async ( ...course, modules: filterModulesByDifficulty(getCurriculumForCourse(course.id), difficulty), }; - } catch (error) { + } catch (_error) { console.warn('Database error in getCourseCurriculum, falling back to mock data'); const mockCourse = COURSES.find((c) => c.id === courseId); if (!mockCourse) return null; @@ -177,7 +177,7 @@ export const getStudentProgress = async ( mockProgressStore[key] = p; // Sync cache return p; } - } catch (error) { + } catch (_error) { console.warn('Database error in getStudentProgress, using mock store'); } @@ -288,7 +288,7 @@ export const updateStudentProgress = async ( }); return toProgress(progress); - } catch (error) { + } catch (_error) { console.warn('Database error in updateStudentProgress, updated in mock store only'); return updatedProgress; } diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index a60c37d..3519a5f 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -74,7 +74,7 @@ export const generateToken = (payload: TokenPayload, options: TokenOptions = {}) }; const signOptions: SignOptions = { - expiresIn: defaultOptions.expiresIn as any, + expiresIn: defaultOptions.expiresIn as SignOptions['expiresIn'], issuer: defaultOptions.issuer, audience: defaultOptions.audience, ...options, @@ -99,7 +99,7 @@ export const verifyToken = (token: string): TokenVerificationResult => { valid: true, payload: decoded, }; - } catch (error) { + } catch (error: unknown) { if (error instanceof JsonWebTokenError) { if (error.name === 'TokenExpiredError') { return { @@ -148,18 +148,20 @@ export const refreshToken = (userId: string, options: TokenOptions = {}): string * @param payload - Token payload to validate * @returns True if payload is valid */ -export const validateTokenPayload = (payload: any): boolean => { +export const validateTokenPayload = (payload: unknown): boolean => { if (!payload || typeof payload !== 'object') { return false; } - if (!payload.userId || typeof payload.userId !== 'string') { + const p = payload as Record; + + if (!p.userId || typeof p.userId !== 'string') { return false; } - if (payload.exp && typeof payload.exp === 'number') { + if (p.exp && typeof p.exp === 'number') { const currentTime = Math.floor(Date.now() / 1000); - if (payload.exp < currentTime) { + if ((p.exp as number) < currentTime) { return false; } } diff --git a/backend/src/utils/explorer.ts b/backend/src/utils/explorer.ts index ac3c611..707af0b 100644 --- a/backend/src/utils/explorer.ts +++ b/backend/src/utils/explorer.ts @@ -14,13 +14,11 @@ export type StellarNetwork = 'mainnet' | 'testnet' | 'public'; */ export function getExplorerLink( txHash: string, - network: StellarNetwork = (getEnvVar('STELLAR_NETWORK', 'testnet') as StellarNetwork) + network: StellarNetwork = getEnvVar('STELLAR_NETWORK', 'testnet') as StellarNetwork ): string { // Normalize network name for stellar.expert // stellar.expert uses 'public' for mainnet and 'testnet' for testnet - const networkParam = (network === 'mainnet' || network === 'public') - ? 'public' - : 'testnet'; + const networkParam = network === 'mainnet' || network === 'public' ? 'public' : 'testnet'; return `https://stellar.expert/explorer/${networkParam}/tx/${txHash}`; } @@ -34,11 +32,9 @@ export function getExplorerLink( */ export function getAccountExplorerLink( publicKey: string, - network: StellarNetwork = (getEnvVar('STELLAR_NETWORK', 'testnet') as StellarNetwork) + network: StellarNetwork = getEnvVar('STELLAR_NETWORK', 'testnet') as StellarNetwork ): string { - const networkParam = (network === 'mainnet' || network === 'public') - ? 'public' - : 'testnet'; + const networkParam = network === 'mainnet' || network === 'public' ? 'public' : 'testnet'; return `https://stellar.expert/explorer/${networkParam}/account/${publicKey}`; } diff --git a/backend/tests/auth.test.ts b/backend/tests/auth.test.ts index 331bd56..47b41b5 100644 --- a/backend/tests/auth.test.ts +++ b/backend/tests/auth.test.ts @@ -23,7 +23,10 @@ describe('Auth Module Integration Tests', () => { lastName: 'User', }; - const response = await request(app).post('/api/v1/auth/register').send(newStudent).expect(201); + const response = await request(app) + .post('/api/v1/auth/register') + .send(newStudent) + .expect(201); expect(response.body).toHaveProperty('user'); expect(response.body.user).toHaveProperty('id'); @@ -56,7 +59,10 @@ describe('Auth Module Integration Tests', () => { lastName: 'User', }; - const response = await request(app).post('/api/v1/auth/register').send(newStudent).expect(400); + const response = await request(app) + .post('/api/v1/auth/register') + .send(newStudent) + .expect(400); expect(response.body).toHaveProperty('error'); }); @@ -70,14 +76,13 @@ describe('Auth Module Integration Tests', () => { }; // First registration - await request(app) - .post('/api/v1/auth/register') - .send(newStudent) - .expect(201); - await request(app).post('/api/auth/register').send(newStudent).expect(201); + await request(app).post('/api/v1/auth/register').send(newStudent).expect(201); // Second registration with same email - const response = await request(app).post('/api/v1/auth/register').send(newStudent).expect(409); + const response = await request(app) + .post('/api/v1/auth/register') + .send(newStudent) + .expect(409); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Student with this email already exists'); diff --git a/backend/tests/auth.util.test.ts b/backend/tests/auth.util.test.ts index ac6234f..45fb8ac 100644 --- a/backend/tests/auth.util.test.ts +++ b/backend/tests/auth.util.test.ts @@ -224,12 +224,12 @@ describe('JWT Authentication Utility', () => { }); it('should return null for null header', () => { - const token = extractTokenFromHeader(null as any); + const token = extractTokenFromHeader(null as unknown as string); expect(token).toBeNull(); }); it('should return null for non-string header', () => { - const token = extractTokenFromHeader(123 as any); + const token = extractTokenFromHeader(123 as unknown as string); expect(token).toBeNull(); }); }); diff --git a/backend/tests/feedback.test.ts b/backend/tests/feedback.test.ts index e2bac3e..dff7e6a 100644 --- a/backend/tests/feedback.test.ts +++ b/backend/tests/feedback.test.ts @@ -142,7 +142,9 @@ describe('Feedback Module Integration Tests', () => { }); it('should return 404 for non-existent course', async () => { - const response = await request(app).get('/api/v1/feedback/course/non-existent-id').expect(404); + const response = await request(app) + .get('/api/v1/feedback/course/non-existent-id') + .expect(404); expect(response.body).toHaveProperty('error'); }); diff --git a/backend/tests/learning.test.ts b/backend/tests/learning.test.ts index fb55713..a7340c7 100644 --- a/backend/tests/learning.test.ts +++ b/backend/tests/learning.test.ts @@ -1,6 +1,6 @@ import request from 'supertest'; -import { app } from '../src/index.js'; import prisma from '../src/db/index.js'; +import { app } from '../src/index.js'; const courseFixtures = [ { @@ -65,9 +65,13 @@ describe('Learning Module Integration Tests', () => { .get('/api/v1/learning/courses?difficulty=beginner') .expect(200); - response.body.courses.forEach((course: any) => { - course.modules.forEach((module: any) => { - module.lessons.forEach((lesson: any) => { + interface TestLesson { difficulty: string } + interface TestModule { lessons: TestLesson[] } + interface TestCourse { modules: TestModule[] } + + response.body.courses.forEach((course: TestCourse) => { + course.modules.forEach((module: TestModule) => { + module.lessons.forEach((lesson: TestLesson) => { expect(lesson.difficulty).toBe('beginner'); }); }); @@ -84,7 +88,9 @@ describe('Learning Module Integration Tests', () => { }); it('returns 404 when the course does not exist', async () => { - const response = await request(app).get('/api/v1/learning/courses/unknown-course').expect(404); + const response = await request(app) + .get('/api/v1/learning/courses/unknown-course') + .expect(404); expect(response.body.error).toBe('Course not found'); }); diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 3aa61f1..9ad2adf 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -8,6 +8,7 @@ #![no_std] pub mod payment_gateway; +pub mod session; pub mod staking; // Fuzz module uses `std` and legacy Soroban test patterns; keep out of the default test build // until it is refreshed for the current SDK (`sequence_number`, token `mint` arity, etc.). @@ -493,8 +494,10 @@ impl CertificateContract { .get(&DataKey::MintCap) .unwrap_or(DEFAULT_MINT_CAP); env.storage().instance().set(&DataKey::MintCap, &new_cap); - env.events() - .publish((Symbol::new(&env, "v1_mint_cap_updated"),), (old_cap, new_cap)); + env.events().publish( + (Symbol::new(&env, "v1_mint_cap_updated"),), + (old_cap, new_cap), + ); } PendingAdminAction::Upgrade(new_wasm_hash) => { // Upgrade risks (summary): malicious WASM can steal funds, brick storage layout, @@ -593,12 +596,12 @@ impl CertificateContract { let total_certificates = symbols.len(); let available = Self::check_and_update_mint_tracking(&env); - if (total_certificates as u32) > available { + if total_certificates > available { Self::release_lock(&env); panic_with_error!(&env, CertError::MintCapExceeded); } - Self::record_mint(&env, total_certificates as u32); + Self::record_mint(&env, total_certificates); let issue_date = env.ledger().timestamp(); let mut issued: Vec = Vec::new(&env); @@ -629,7 +632,10 @@ impl CertificateContract { // Batch event emission (emit one event per certificate for transparency) env.events().publish( - (Symbol::new(&env, "v1_batch_cert_issued"), course_symbol.clone()), + ( + Symbol::new(&env, "batch_cert_issued"), + course_symbol.clone(), + ), (student.clone(), course.clone()), ); @@ -638,19 +644,15 @@ impl CertificateContract { // Emit summary event for the entire batch operation env.events().publish( - (Symbol::new(&env, "v1_batch_issue_completed"),), - ( - instructor.clone(), - total_certificates as u32, - course.clone(), - ), + (Symbol::new(&env, "batch_issue_completed"),), + (instructor.clone(), total_certificates, course.clone()), ); env.events().publish( - (Symbol::new(&env, "v1_mint_period_update"),), + (Symbol::new(&env, "mint_period_update"),), ( env.ledger().sequence() / LEDGERS_PER_PERIOD, - total_certificates as u32, + total_certificates, ), ); diff --git a/contracts/src/prop_tests.rs b/contracts/src/prop_tests.rs index 859a3b0..8276083 100644 --- a/contracts/src/prop_tests.rs +++ b/contracts/src/prop_tests.rs @@ -34,8 +34,7 @@ mod tests { sum += bal; } assert_eq!( - sum, - net[token_id as usize], + sum, net[token_id as usize], "supply invariant broken: token_id={token_id} sum={sum} expected={}", net[token_id as usize] ); diff --git a/contracts/src/session.rs b/contracts/src/session.rs new file mode 100644 index 0000000..7848812 --- /dev/null +++ b/contracts/src/session.rs @@ -0,0 +1,84 @@ +use soroban_sdk::{ + contract, contractimpl, contracttype, xdr::ToXdr, Address, Bytes, BytesN, Env, Symbol, +}; + +#[contracttype] +#[derive(Clone)] +pub enum SessionKey { + /// Storage key for a student's active session code. + /// Maps Address (student) -> BytesN<16> (128-bit verification code). + VerificationCode(Address), +} + +#[contract] +pub struct SessionVerificationContract; + +#[contractimpl] +impl SessionVerificationContract { + /// Starts a new session for a student by generating a 128-bit temporary key. + /// The key is stored in temporary storage with a short TTL. + pub fn start_session(env: Env, student: Address) -> BytesN<16> { + // Authenticate the student so only they can start their own session + student.require_auth(); + + // Generate a 128-bit (16-byte) session code. + // We use the student's address and current timestamp to generate a unique key + // as a workaround if the SDK prng() has issues in this environment. + let mut msg = Bytes::new(&env); + msg.append(&student.clone().to_xdr(&env)); + msg.append(&env.ledger().timestamp().to_xdr(&env)); + // Use Bytes to work with variable length if needed + let mut session_bytes = [0u8; 16]; + let hash: [u8; 32] = env.crypto().sha256(&msg).into(); + + // Take first 16 bytes of the 32-byte hash + session_bytes.copy_from_slice(&hash[..16]); + + let session_code = BytesN::from_array(&env, &session_bytes); + + let storage_key = SessionKey::VerificationCode(student.clone()); + + // Store the key in temporary storage. + // Temporary storage automatically expires and is removed from the ledger + // if its TTL is not bumped, making it perfect for short-lived session codes. + env.storage().temporary().set(&storage_key, &session_code); + + // Explicitly set a short TTL (e.g., ~100 ledgers, roughly 8-10 minutes) + // ensure it lasts at least 100 ledgers from now. + env.storage().temporary().extend_ttl(&storage_key, 100, 200); + + // Publish an event for transparency + env.events() + .publish((Symbol::new(&env, "session_started"), student), ()); + + session_code + } + + /// Verifies if a provided session code is valid for the student. + /// Returns true if the code matches and has not expired. + pub fn verify_session(env: Env, student: Address, provided_code: BytesN<16>) -> bool { + let storage_key = SessionKey::VerificationCode(student); + + // Retrieve the code from temporary storage. + // If it has expired or was never set, this will return None. + let stored_code: Option> = env.storage().temporary().get(&storage_key); + + match stored_code { + Some(code) => { + // Check if the provided code matches the stored one + code == provided_code + } + None => false, // Expired or not found + } + } + + /// Extends the session's TTL if the student is still active. + pub fn extend_session(env: Env, student: Address) { + student.require_auth(); + let storage_key = SessionKey::VerificationCode(student); + + if env.storage().temporary().has(&storage_key) { + env.storage().temporary().extend_ttl(&storage_key, 100, 200); + } + } +} diff --git a/contracts/src/tests.rs b/contracts/src/tests.rs index 8a10d7a..4ce1d78 100644 --- a/contracts/src/tests.rs +++ b/contracts/src/tests.rs @@ -5,6 +5,8 @@ use soroban_sdk::{ vec, Address, Env, FromVal, String, Symbol, }; +use crate::session::{SessionVerificationContract, SessionVerificationContractClient}; + fn setup() -> ( Env, Address, @@ -142,7 +144,8 @@ fn verifies_event_emitted_per_student() { let mut cert_issued_count = 0u32; for (addr, topics, _) in all_events.iter() { if addr == client.address - && Symbol::from_val(&env, &topics.get(0).unwrap()) == Symbol::new(&env, "v1_cert_issued") + && Symbol::from_val(&env, &topics.get(0).unwrap()) + == Symbol::new(&env, "v1_cert_issued") { cert_issued_count += 1; } @@ -251,6 +254,74 @@ fn non_admin_cannot_revoke_certificate() { client.revoke(&attacker, &course_symbol, &student); } +fn setup_session() -> (Env, Address, SessionVerificationContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(SessionVerificationContract, ()); + let client = SessionVerificationContractClient::new(&env, &contract_id); + let student = Address::generate(&env); + (env, student, client) +} + +// --------------------------------------------------------------------------- +// Session Verification Tests +// --------------------------------------------------------------------------- + +#[test] +fn test_session_start_and_verify() { + let (env, student, client) = setup_session(); + + let code = client.start_session(&student); + + // Verify the code is valid + assert!(client.verify_session(&student, &code)); + + // Verify a wrong code is invalid + let wrong_code = BytesN::from_array(&env, &[0u8; 16]); + assert!(!client.verify_session(&student, &wrong_code)); +} + +#[test] +fn test_session_expiration() { + let (env, student, client) = setup_session(); + + let code = client.start_session(&student); + assert!(client.verify_session(&student, &code)); + + // Jump forward in time by 201 ledgers to trigger expiration + // (We set TTL to 100-200 in start_session) + env.ledger().with_mut(|l| { + l.sequence_number += 201; + }); + + // Code should now be expired (None in temporary storage) + assert!(!client.verify_session(&student, &code)); +} + +#[test] +fn test_session_extension() { + let (env, student, client) = setup_session(); + + let code = client.start_session(&student); + + // Jump forward 50 ledgers + env.ledger().with_mut(|l| { + l.sequence_number += 50; + }); + + // Extend the session + client.extend_session(&student); + + // Jump forward another 60 ledgers (total 110 since start) + // Without extension, it would have expired at 100. + env.ledger().with_mut(|l| { + l.sequence_number += 60; + }); + + // Code should still be valid because of extension + assert!(client.verify_session(&student, &code)); +} + // --------------------------------------------------------------------------- // Meta-transactions // --------------------------------------------------------------------------- @@ -740,11 +811,7 @@ fn batch_issue_emits_events() { let (env, instructor, _, _, client) = setup(); let symbols = vec![&env, symbol_short!("EVENT"), symbol_short!("EVENT2")]; - let students = vec![ - &env, - Address::generate(&env), - Address::generate(&env), - ]; + let students = vec![&env, Address::generate(&env), Address::generate(&env)]; let course_name = String::from_str(&env, "Event Test"); client.batch_issue(&instructor, &symbols, &students, &course_name); @@ -774,11 +841,13 @@ fn batch_issue_gas_efficiency() { let mut students = Vec::new(&env); // Create 10 different symbols without using format macro - let symbol_names = ["BATCH0", "BATCH1", "BATCH2", "BATCH3", "BATCH4", - "BATCH5", "BATCH6", "BATCH7", "BATCH8", "BATCH9"]; + let symbol_names = [ + "BATCH0", "BATCH1", "BATCH2", "BATCH3", "BATCH4", "BATCH5", "BATCH6", "BATCH7", "BATCH8", + "BATCH9", + ]; - for i in 0..10 { - symbols.push_back(Symbol::new(&env, symbol_names[i])); + for name in &symbol_names { + symbols.push_back(Symbol::new(&env, name)); students.push_back(Address::generate(&env)); } diff --git a/contracts/src/token.rs b/contracts/src/token.rs index bd16bfe..1f0c0a9 100644 --- a/contracts/src/token.rs +++ b/contracts/src/token.rs @@ -1,5 +1,6 @@ use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Env, Vec, String, Symbol, + contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Env, String, + Vec, }; use crate::CertificateContractClient; @@ -52,9 +53,7 @@ impl RsTokenContract { env.storage() .instance() .set(&DataKey::CertificateContract, &certificate_contract); - env.storage() - .instance() - .set(&DataKey::MintPaused, &false); + env.storage().instance().set(&DataKey::MintPaused, &false); env.storage() .instance() .set(&DataKey::Owner, &certificate_contract); @@ -190,11 +189,7 @@ impl RsTokenContract { } // Check authorization: only owner or the student themselves can burn - let owner: Address = env - .storage() - .instance() - .get(&DataKey::Owner) - .unwrap(); + let owner: Address = env.storage().instance().get(&DataKey::Owner).unwrap(); if caller != owner && caller != student { panic_with_error!(&env, TokenError::NotAuthorized); @@ -254,11 +249,15 @@ impl RsTokenContract { // Remove balance entry if zero to save storage env.storage().instance().remove(&from_balance_key); } else { - env.storage().instance().set(&from_balance_key, &new_from_balance); + env.storage() + .instance() + .set(&from_balance_key, &new_from_balance); } // Update recipient balance - env.storage().instance().set(&to_balance_key, &new_to_balance); + env.storage() + .instance() + .set(&to_balance_key, &new_to_balance); // Emit the Transferred event env.events().publish( @@ -303,11 +302,7 @@ impl RsTokenContract { caller.require_auth(); // Check authorization: only owner can update metadata - let owner: Address = env - .storage() - .instance() - .get(&DataKey::Owner) - .unwrap(); + let owner: Address = env.storage().instance().get(&DataKey::Owner).unwrap(); if caller != owner { panic_with_error!(&env, TokenError::NotAuthorized); @@ -332,17 +327,15 @@ impl RsTokenContract { .set(&DataKey::TokenMetadata, &metadata); // Emit event for URI update - env.events().publish( - ("uri_updated", "old_uri", "new_uri"), - (old_uri, new_uri), - ); + env.events() + .publish(("uri_updated", "old_uri", "new_uri"), (old_uri, new_uri)); } } #[cfg(test)] mod tests { use super::*; - use soroban_sdk::{testutils::{Address as _, Events}, vec, Address, Env}; + use soroban_sdk::{testutils::Address as _, vec, Address, Env}; #[test] fn mints_balance_for_student_when_called_by_certificate_contract() { @@ -698,7 +691,10 @@ mod tests { assert_eq!(metadata.name, String::from_str(&env, "RS-Token")); assert_eq!(metadata.symbol, String::from_str(&env, "RST")); assert_eq!(metadata.decimals, 0u32); - assert_eq!(metadata.uri, String::from_str(&env, "https://metadata.web3-student-lab.com/token/{id}")); + assert_eq!( + metadata.uri, + String::from_str(&env, "https://metadata.web3-student-lab.com/token/{id}") + ); } #[test] @@ -753,7 +749,6 @@ mod tests { // Verify all required fields are present and have correct types assert!(!metadata.name.is_empty()); assert!(!metadata.symbol.is_empty()); - assert!(metadata.decimals >= 0); assert!(!metadata.uri.is_empty()); // Verify symbol is reasonable length (common token symbols are 3-5 chars) diff --git a/docker-compose.yml b/docker-compose.yml index 426bec4..a6fd601 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,11 +8,11 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: web3-student-lab ports: - - '5432:5432' + - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ['CMD-SHELL', 'pg_isready -U postgres'] + test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 @@ -24,7 +24,7 @@ services: container_name: web3-student-lab-backend restart: always ports: - - '8080:8080' + - "8080:8080" environment: - DATABASE_URL=postgresql://postgres:postgres@db:5432/web3-student-lab?schema=public - NODE_ENV=development diff --git a/docs/ARCHITECTURE_DEEP_DIVE.md b/docs/ARCHITECTURE_DEEP_DIVE.md index 0f3a523..e4e6e76 100644 --- a/docs/ARCHITECTURE_DEEP_DIVE.md +++ b/docs/ARCHITECTURE_DEEP_DIVE.md @@ -1,37 +1,40 @@ # Architecture Deep Dive: Web3 Student Lab -This document provides a technical deep dive into the architecture of the Web3 Student Lab platform. It details how the Frontend, Backend, and Stellar Network components work together to provide a seamless educational experience for blockchain students. +This document provides a technical deep dive into the architecture of the Web3 Student Lab platform. +It details how the Frontend, Backend, and Stellar Network components work together to provide a +seamless educational experience for blockchain students. ## ๐Ÿ—๏ธ High-Level System Overview -The Web3 Student Lab follows a modular, 3-tier architecture designed for scalability, security, and educational clarity. +The Web3 Student Lab follows a modular, 3-tier architecture designed for scalability, security, and +educational clarity. ```mermaid graph TD User([Student / Developer]) - + subgraph "Frontend (Next.js)" UI[User Interface] Sim[Blockchain Simulator] Play[Soroban Playground] end - + subgraph "Backend (Node.js/Express)" API[Express API] Auth[Auth Service] Logic[Learning Logic] StellarGW[Stellar SDK Gateway] end - + subgraph "Database (PostgreSQL)" DB[(Postgres/Prisma)] end - + subgraph "Blockchain (Stellar Network)" Contracts[Soroban Smart Contracts] Ledger[(Stellar Ledger)] end - + User <--> UI UI <--> API API <--> DB @@ -43,21 +46,29 @@ graph TD ## ๐Ÿงฉ Component Roles & Responsibilities ### 1. Frontend (The User Experience) -* **Next.js (React)**: Handles routing, state management, and the overall UI. -* **Monaco Editor**: Provides a rich in-browser coding experience for smart contracts. -* **Tailwind CSS**: Ensures a modern, responsive design across all devices. -* **Visual Simulator**: Locally computes and visualizes blockchain concepts (hashes, mining, blocks) for educational purposes. + +- **Next.js (React)**: Handles routing, state management, and the overall UI. +- **Monaco Editor**: Provides a rich in-browser coding experience for smart contracts. +- **Tailwind CSS**: Ensures a modern, responsive design across all devices. +- **Visual Simulator**: Locally computes and visualizes blockchain concepts (hashes, mining, blocks) + for educational purposes. ### 2. Backend (The Orchestrator) -* **Node.js & Express**: Provides the RESTful API that the frontend consumes. -* **Prisma ORM**: Manages interaction with the PostgreSQL database, ensuring type-safety and efficient queries. -* **Stellar SDK**: Primarily acts as a gateway to the Stellar Network, handling transaction preparation, signing (with service keys), and submission. -* **Learning Logic**: Manages course progress, quiz validations, and student profiles. + +- **Node.js & Express**: Provides the RESTful API that the frontend consumes. +- **Prisma ORM**: Manages interaction with the PostgreSQL database, ensuring type-safety and + efficient queries. +- **Stellar SDK**: Primarily acts as a gateway to the Stellar Network, handling transaction + preparation, signing (with service keys), and submission. +- **Learning Logic**: Manages course progress, quiz validations, and student profiles. ### 3. Contracts (The Source of Truth) -* **Soroban (Rust)**: Contracts are written in Rust and compiled to WASM for high-performance and secure execution. -* **CertificateContract**: Manages the issuance and verification of student certificates directly on the Stellar Ledger. -* **TokenContract**: (Optional/Planned) Manages lab-specific rewards or tokens. + +- **Soroban (Rust)**: Contracts are written in Rust and compiled to WASM for high-performance and + secure execution. +- **CertificateContract**: Manages the issuance and verification of student certificates directly on + the Stellar Ledger. +- **TokenContract**: (Optional/Planned) Manages lab-specific rewards or tokens. --- @@ -65,7 +76,8 @@ graph TD ### ๐ŸŽ“ Certificate Issuance Flow -This diagram illustrates the sequence from when a student finishes a course to the moment a certificate is minted on the Stellar network and recorded in the local database. +This diagram illustrates the sequence from when a student finishes a course to the moment a +certificate is minted on the Stellar network and recorded in the local database. ```mermaid sequenceDiagram @@ -80,22 +92,24 @@ sequenceDiagram Front->>Back: POST /api/certificates { studentId, courseId } Back->>DB: Verify Course Completion DB-->>Back: Success - + Note right of Back: Backend prepares Soroban
Contract Invocation - + Back->>Stellar: invoke 'issue' (student, course) Stellar-->>Back: Returns Transaction Hash - + Back->>DB: Create Certificate Record status='issued', hash=tx_hash DB-->>Back: Success - + Back-->>Front: 201 Created { certificateHash } Front-->>Student: Displays Certificate with Blockchain Link ``` ## ๐Ÿ—„๏ธ Unified Data Model -The local database maintains a mirror of essential on-chain data to ensure fast response times and support complex queries (e.g., student leaderboards) that aren't efficient to perform directly on-ledger. +The local database maintains a mirror of essential on-chain data to ensure fast response times and +support complex queries (e.g., student leaderboards) that aren't efficient to perform directly +on-ledger. ```mermaid erDiagram @@ -103,20 +117,20 @@ erDiagram STUDENT ||--o{ CERTIFICATE : receives COURSE ||--o{ ENROLLMENT : contains COURSE ||--o{ CERTIFICATE : awards - + STUDENT { string id string email string firstName string lastName } - + COURSE { string id string title int credits } - + CERTIFICATE { string id string studentId @@ -124,7 +138,7 @@ erDiagram string certificateHash string status } - + ENROLLMENT { string id string studentId @@ -137,7 +151,11 @@ erDiagram ## ๐Ÿ”’ Security & Scaling Considerations -1. **JWT Authentication**: All communication between the frontend and backend is secured using signed JSON Web Tokens. -2. **Stellar Key Management**: Private keys for contract invocation are managed on the backend using environment variables or dedicated secret management services. -3. **Idempotency**: Blockchain operations use unique identifiers to prevent accidental double-issuance of certificates. -4. **Optimistic UI**: The frontend updates immediately upon backend success, while the blockchain finalization happens asynchronously. +1. **JWT Authentication**: All communication between the frontend and backend is secured using + signed JSON Web Tokens. +2. **Stellar Key Management**: Private keys for contract invocation are managed on the backend + using environment variables or dedicated secret management services. +3. **Idempotency**: Blockchain operations use unique identifiers to prevent accidental + double-issuance of certificates. +4. **Optimistic UI**: The frontend updates immediately upon backend success, while the blockchain + finalization happens asynchronously. diff --git a/docs/CONTRACT_UPGRADE.md b/docs/CONTRACT_UPGRADE.md index 1a1ddb7..98e4e1d 100644 --- a/docs/CONTRACT_UPGRADE.md +++ b/docs/CONTRACT_UPGRADE.md @@ -2,19 +2,19 @@ ## How It Works -The `upgrade(caller, new_wasm_hash)` function replaces the contract's executable WASM -using Soroban's built-in `update_current_contract_wasm`. Instance storage (certificates, -nonces, admin key) is **preserved** across upgrades โ€” only the code changes. +The `upgrade(caller, new_wasm_hash)` function replaces the contract's executable WASM using +Soroban's built-in `update_current_contract_wasm`. Instance storage (certificates, nonces, admin +key) is **preserved** across upgrades โ€” only the code changes. ## Security Considerations -| Risk | Mitigation | -|---|---| -| Admin key compromise | Attacker can deploy arbitrary code. Use a multisig or hardware wallet for the admin address in production. | -| Unaudited WASM | New code is not validated on-chain. Always audit and test the new WASM before uploading its hash. | -| No timelock | Upgrades take effect immediately. Consider a timelock contract that delays execution to give users time to exit. | +| Risk | Mitigation | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Admin key compromise | Attacker can deploy arbitrary code. Use a multisig or hardware wallet for the admin address in production. | +| Unaudited WASM | New code is not validated on-chain. Always audit and test the new WASM before uploading its hash. | +| No timelock | Upgrades take effect immediately. Consider a timelock contract that delays execution to give users time to exit. | | State incompatibility | If the new contract reads storage keys differently, existing data may be misread. Plan and test state migration before upgrading. | -| Irreversibility | Once upgraded, the old code is gone. Keep the old WASM hash and a rollback plan ready. | +| Irreversibility | Once upgraded, the old code is gone. Keep the old WASM hash and a rollback plan ready. | ## Upgrade Checklist diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md index 398259c..607ddc8 100644 --- a/docs/DESIGN-SYSTEM.md +++ b/docs/DESIGN-SYSTEM.md @@ -1,7 +1,7 @@ # Design System -This document defines the design tokens and UI foundations used across the platform. -It ensures consistency in colors, typography, spacing, and component styling. +This document defines the design tokens and UI foundations used across the platform. It ensures +consistency in colors, typography, spacing, and component styling. --- diff --git a/docs/FUZZING.md b/docs/FUZZING.md index e4a7c0b..0c69af3 100644 --- a/docs/FUZZING.md +++ b/docs/FUZZING.md @@ -4,26 +4,32 @@ This document describes the fuzzing setup implemented for the Web3 Student Lab S ## Overview -The fuzzing module (`contracts/src/fuzz.rs`) implements property-based testing to find edge cases in the Certificate and Token contract logic. Since Soroban-SDK doesn't have native cargo-fuzz integration, we use structured property-based testing with deterministic pseudo-random inputs. +The fuzzing module (`contracts/src/fuzz.rs`) implements property-based testing to find edge cases in +the Certificate and Token contract logic. Since Soroban-SDK doesn't have native cargo-fuzz +integration, we use structured property-based testing with deterministic pseudo-random inputs. ## Target Edge Cases ### 1. Overflow/Underflow + - **Mint cap arithmetic**: Boundary conditions when adding certificates - **Ledger period division**: Handling of high ledger sequence numbers - **Token amount overflow**: Large i128 values in token minting ### 2. Storage Collisions + - **Composite keys**: Different (course_symbol, student) pairs should not collide - **Revocation isolation**: Revoking one certificate shouldn't affect others - **Cross-course uniqueness**: Certificates for different courses should be independent ### 3. Boundary Conditions + - **Mint cap limits**: Exact cap, cap + 1, zero cap - **Period boundaries**: Mint tracking should reset correctly at period boundaries - **Empty inputs**: Issue with 0 students, single student edge cases ### 4. Authorization + - **Non-admin access**: Verifying authorization checks work correctly - **Cross-contract calls**: Token minting authorization - **Double initialization**: Preventing contract re-initialization @@ -146,36 +152,43 @@ thread '...' panicked at '...' ## Test Categories ### Mint Cap Boundary Fuzzing (`mint_cap_fuzzing`) + - Tests exact cap boundary conditions - Tests cumulative mint counting across multiple issues - Tests edge cases like zero batch, exact cap, cap + 1 ### Storage Collision Fuzzing (`storage_collision_fuzzing`) + - Verifies different (course, student) pairs don't collide - Tests composite key uniqueness - Verifies revocation isolation between students ### Period Boundary Fuzzing (`period_boundary_fuzzing`) + - Tests mint counter reset at period boundaries - Tests multiple period advances - Tests handling of high ledger sequence numbers ### Token Fuzzing (`token_fuzzing`) + - Tests authorization requirements - Tests various token amounts - Tests cumulative minting - Tests multiple student handling ### Event Emission Fuzzing (`event_emission_fuzzing`) + - Verifies correct number of events emitted - Tests cert_issued, cert_revoked, mint_period_update events ### Stress Fuzzing (`stress_fuzzing`) + - Large batch sizes - Concurrent mint and revoke operations - Empty and single student edge cases ### Regression Tests (`regression_tests`) + - Double initialization prevention - Zero mint cap rejection - Unauthorized revoke prevention diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index d2e7902..90ea330 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -1,12 +1,14 @@ # Web3 Glossary -This glossary explains common Web3 terms used in this lab. It is designed for students who are new to blockchain and decentralized applications. +This glossary explains common Web3 terms used in this lab. It is designed for students who are new +to blockchain and decentralized applications. --- ## DAO (Decentralized Autonomous Organization) -A DAO is a group or organization that is run by code and community voting instead of a central authority. +A DAO is a group or organization that is run by code and community voting instead of a central +authority. - Decisions are made by members through voting - Rules are written in smart contracts @@ -30,7 +32,8 @@ Similar to paying a small fee to use a service. ## WASM (WebAssembly) -WASM is a fast, low-level programming format that allows code to run efficiently on different platforms. +WASM is a fast, low-level programming format that allows code to run efficiently on different +platforms. - Used to run smart contracts in some blockchains - Faster and more secure than traditional scripting @@ -42,7 +45,8 @@ Think of it as a universal format that helps programs run quickly everywhere. ## Smart Contract -A smart contract is a program stored on the blockchain that runs automatically when certain conditions are met. +A smart contract is a program stored on the blockchain that runs automatically when certain +conditions are met. - No middleman required - Cannot be changed easily once deployed @@ -150,4 +154,5 @@ Like a normal app, but not controlled by one company. ## Final Note -Web3 can feel confusing at first, but these terms form the foundation. As you build projects and interact with blockchain systems, these concepts will become clearer over time. +Web3 can feel confusing at first, but these terms form the foundation. As you build projects and +interact with blockchain systems, these concepts will become clearer over time. diff --git a/docs/PR_PREVIEWS.md b/docs/PR_PREVIEWS.md index 7a58db4..8c828d8 100644 --- a/docs/PR_PREVIEWS.md +++ b/docs/PR_PREVIEWS.md @@ -1,12 +1,16 @@ # PR Preview Site Configuration -This document compares automated preview deployment options for every Pull Request in the **Web3-Student-Lab** project and recommends the best approach for a student environment. +This document compares automated preview deployment options for every Pull Request in the +**Web3-Student-Lab** project and recommends the best approach for a student environment. --- ## Why PR Previews Matter -Pull Request preview deployments give every contributor a live, shareable URL for their branch โ€” no local setup required. Reviewers can test UI changes, check responsiveness, and catch regressions before merging. For a student project, this dramatically lowers the feedback loop and makes code review more visual and accessible. +Pull Request preview deployments give every contributor a live, shareable URL for their branch โ€” no +local setup required. Reviewers can test UI changes, check responsiveness, and catch regressions +before merging. For a student project, this dramatically lowers the feedback loop and makes code +review more visual and accessible. --- @@ -14,7 +18,9 @@ Pull Request preview deployments give every contributor a live, shareable URL fo ### 1. Vercel -**How it works:** Connect your GitHub repo once. Vercel automatically builds and deploys every PR to a unique URL (e.g., `web3-student-lab-pr-42.vercel.app`). The GitHub integration posts the URL directly in the PR comment. +**How it works:** Connect your GitHub repo once. Vercel automatically builds and deploys every PR to +a unique URL (e.g., `web3-student-lab-pr-42.vercel.app`). The GitHub integration posts the URL +directly in the PR comment. **Pros:** @@ -36,7 +42,8 @@ Pull Request preview deployments give every contributor a live, shareable URL fo ### 2. Netlify -**How it works:** Similar to Vercel. Netlify watches your repo and builds a "Deploy Preview" for each PR. The preview URL appears as a GitHub status check. +**How it works:** Similar to Vercel. Netlify watches your repo and builds a "Deploy Preview" for +each PR. The preview URL appears as a GitHub status check. **Pros:** @@ -58,7 +65,8 @@ Pull Request preview deployments give every contributor a live, shareable URL fo ### 3. GitHub Pages + GitHub Actions (Self-Hosted Previews) -**How it works:** A custom GitHub Actions workflow builds the project on every PR push and deploys to a `gh-pages`-style branch or uploads to an S3-compatible bucket / GitHub Pages subfolder. +**How it works:** A custom GitHub Actions workflow builds the project on every PR push and deploys +to a `gh-pages`-style branch or uploads to an S3-compatible bucket / GitHub Pages subfolder. **Pros:** @@ -70,7 +78,8 @@ Pull Request preview deployments give every contributor a live, shareable URL fo **Cons:** - Significantly more setup and maintenance work -- GitHub Pages does not natively support per-PR URLs โ€” workarounds (subfolders, artifacts) are fragile +- GitHub Pages does not natively support per-PR URLs โ€” workarounds (subfolders, artifacts) are + fragile - No automatic teardown of old previews without extra scripting - Debugging pipeline failures can be time-consuming for students @@ -80,7 +89,8 @@ Pull Request preview deployments give every contributor a live, shareable URL fo ### 4. Cloudflare Pages -**How it works:** Cloudflare Pages connects to GitHub and deploys previews on every PR push, similar to Vercel and Netlify, but served from Cloudflare's edge network. +**How it works:** Cloudflare Pages connects to GitHub and deploys previews on every PR push, similar +to Vercel and Netlify, but served from Cloudflare's edge network. **Pros:** @@ -118,17 +128,24 @@ For **Web3-Student-Lab**, **Vercel** is the recommended choice. **Reasons:** -1. **Fastest time to value.** Students can get PR previews working in under 5 minutes with no configuration files. This keeps the focus on learning Web3 concepts, not DevOps. +1. **Fastest time to value.** Students can get PR previews working in under 5 minutes with no + configuration files. This keeps the focus on learning Web3 concepts, not DevOps. -2. **Best-in-class GitHub integration.** Vercel's GitHub bot automatically comments preview URLs on every PR โ€” reviewers never have to hunt for a link. +2. **Best-in-class GitHub integration.** Vercel's GitHub bot automatically comments preview URLs on + every PR โ€” reviewers never have to hunt for a link. -3. **Free for public repos.** The Hobby plan covers everything this project needs. There are no build minute caps that would block contributors during active sprints. +3. **Free for public repos.** The Hobby plan covers everything this project needs. There are no + build minute caps that would block contributors during active sprints. -4. **Framework agnostic.** Whether the project uses plain HTML, React, or a Web3 framework like scaffold-eth, Vercel detects and builds it correctly. +4. **Framework agnostic.** Whether the project uses plain HTML, React, or a Web3 framework like + scaffold-eth, Vercel detects and builds it correctly. -5. **Automatic cleanup.** Preview deployments are torn down when a PR is closed or merged, keeping the environment tidy. +5. **Automatic cleanup.** Preview deployments are torn down when a PR is closed or merged, keeping + the environment tidy. -Netlify is a solid runner-up and can be substituted if the team hits Vercel's free-tier team-size limits. GitHub Actions is worth exploring later as a learning exercise in CI/CD, but should not be the first choice here due to its setup complexity. +Netlify is a solid runner-up and can be substituted if the team hits Vercel's free-tier team-size +limits. GitHub Actions is worth exploring later as a learning exercise in CI/CD, but should not be +the first choice here due to its setup complexity. --- diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 626ca82..05e726d 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,6 +1,6 @@ -import { defineConfig, globalIgnores } from 'eslint/config'; -import nextVitals from 'eslint-config-next/core-web-vitals'; -import nextTs from 'eslint-config-next/typescript'; +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; const eslintConfig = defineConfig([ ...nextVitals, @@ -8,10 +8,10 @@ const eslintConfig = defineConfig([ // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: - '.next/**', - 'out/**', - 'build/**', - 'next-env.d.ts', + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", ]), ]); diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 4e93457..66e1566 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from 'next'; +import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs index 297374d..61e3684 100644 --- a/frontend/postcss.config.mjs +++ b/frontend/postcss.config.mjs @@ -1,6 +1,6 @@ const config = { plugins: { - '@tailwindcss/postcss': {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b984e2c..6dfc249 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,4 +1,4 @@ -@import 'tailwindcss'; +@import "tailwindcss"; @source "../**/*.{ts,tsx}"; :root { diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts index 38e4e06..19fb114 100644 --- a/frontend/src/lib/api-client.ts +++ b/frontend/src/lib/api-client.ts @@ -1,18 +1,19 @@ -import axios from 'axios'; +import axios from "axios"; -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api'; +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api/v1"; // Create axios instance const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); // Add token to requests if available apiClient.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); + const token = localStorage.getItem("token"); if (token) { config.headers.Authorization = `Bearer ${token}`; } @@ -25,12 +26,12 @@ apiClient.interceptors.response.use( (error) => { if (error.response?.status === 401) { // Token expired or invalid - localStorage.removeItem('token'); - localStorage.removeItem('user'); - window.location.href = '/auth/login'; + localStorage.removeItem("token"); + localStorage.removeItem("user"); + window.location.href = "/auth/login"; } return Promise.reject(error); - } + }, ); export default apiClient;