This commit is contained in:
5t4l1n
2025-07-25 11:10:44 +05:30
parent 4455b39267
commit 7e6f0d0b1e
32 changed files with 2093 additions and 15 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "backend/lib/openzeppelin-contracts"]
path = backend/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
File diff suppressed because one or more lines are too long
+142
View File
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract CertificateNFT is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
struct Certificate {
string subject;
string studentName;
uint256 score;
uint256 timestamp;
bool verified;
}
mapping(uint256 => Certificate) public certificates;
mapping(address => uint256[]) public userCertificates;
event CertificateMinted(
uint256 indexed tokenId,
address indexed student,
string subject,
uint256 score,
string tokenURI
);
constructor() ERC721("OpenLearnX Certificate", "OLXC") {}
function mintCertificate(
address to,
string memory _tokenURI
) public onlyOwner returns (uint256) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_mint(to, newTokenId);
_setTokenURI(newTokenId, _tokenURI);
certificates[newTokenId] = Certificate({
subject: "General",
studentName: "",
score: 100,
timestamp: block.timestamp,
verified: true
});
userCertificates[to].push(newTokenId);
emit CertificateMinted(newTokenId, to, "General", 100, _tokenURI);
return newTokenId;
}
function mintCertificateWithDetails(
address to,
string memory _tokenURI,
string memory subject,
string memory studentName,
uint256 score
) public onlyOwner returns (uint256) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_mint(to, newTokenId);
_setTokenURI(newTokenId, _tokenURI);
certificates[newTokenId] = Certificate({
subject: subject,
studentName: studentName,
score: score,
timestamp: block.timestamp,
verified: true
});
userCertificates[to].push(newTokenId);
emit CertificateMinted(newTokenId, to, subject, score, _tokenURI);
return newTokenId;
}
function getCertificate(uint256 tokenId)
public
view
returns (Certificate memory)
{
require(_exists(tokenId), "Certificate does not exist");
return certificates[tokenId];
}
function getUserCertificates(address user)
public
view
returns (uint256[] memory)
{
return userCertificates[user];
}
function verifyCertificate(uint256 tokenId)
public
view
returns (bool)
{
require(_exists(tokenId), "Certificate does not exist");
return certificates[tokenId].verified;
}
function totalSupply() public view returns (uint256) {
return _tokenIds.current();
}
// Override required functions
function _burn(uint256 tokenId)
internal
override(ERC721, ERC721URIStorage)
{
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
+690
View File
@@ -0,0 +1,690 @@
{
"contract_address": "0x68b6014a12702891757fE994d70dE411FF74B94e",
"transaction_hash": "0x26fd0a4dda1212096c20a842d8aed7f5ee2c22258cc606ae97dcdbd55e01a675",
"deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"network": "local",
"abi": [
{
"type": "constructor",
"inputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "approve",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "balanceOf",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "certificates",
"inputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "subject",
"type": "string",
"internalType": "string"
},
{
"name": "studentName",
"type": "string",
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "timestamp",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "verified",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getApproved",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getCertificate",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "tuple",
"internalType": "struct CertificateNFT.Certificate",
"components": [
{
"name": "subject",
"type": "string",
"internalType": "string"
},
{
"name": "studentName",
"type": "string",
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "timestamp",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "verified",
"type": "bool",
"internalType": "bool"
}
]
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getUserCertificates",
"inputs": [
{
"name": "user",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "isApprovedForAll",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "mintCertificate",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "_tokenURI",
"type": "string",
"internalType": "string"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "mintCertificateWithDetails",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "_tokenURI",
"type": "string",
"internalType": "string"
},
{
"name": "subject",
"type": "string",
"internalType": "string"
},
{
"name": "studentName",
"type": "string",
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "name",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "owner",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "ownerOf",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "renounceOwnership",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "safeTransferFrom",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "safeTransferFrom",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "data",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "setApprovalForAll",
"inputs": [
{
"name": "operator",
"type": "address",
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "supportsInterface",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "symbol",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "tokenURI",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "totalSupply",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "transferFrom",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "transferOwnership",
"inputs": [
{
"name": "newOwner",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "userCertificates",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
},
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "verifyCertificate",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "event",
"name": "Approval",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "ApprovalForAll",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"type": "event",
"name": "BatchMetadataUpdate",
"inputs": [
{
"name": "_fromTokenId",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "_toTokenId",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "CertificateMinted",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "student",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "subject",
"type": "string",
"indexed": false,
"internalType": "string"
},
{
"name": "score",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "tokenURI",
"type": "string",
"indexed": false,
"internalType": "string"
}
],
"anonymous": false
},
{
"type": "event",
"name": "MetadataUpdate",
"inputs": [
{
"name": "_tokenId",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "OwnershipTransferred",
"inputs": [
{
"name": "previousOwner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "newOwner",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"type": "event",
"name": "Transfer",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "to",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
}
],
"gas_used": 3387337,
"block_number": 22993928,
"status": 1
}
+11
View File
@@ -0,0 +1,11 @@
[profile.default]
src = "contracts"
out = "out"
libs = ["lib"]
remappings = [
"@openzeppelin/=lib/openzeppelin-contracts/"
]
[rpc_endpoints]
local = "http://127.0.0.1:8545"
sepolia = "https://sepolia.infura.io/v3/${INFURA_API_KEY}"
+50
View File
@@ -0,0 +1,50 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
from dotenv import load_dotenv
import os
import asyncio
from mongo_service import MongoService
from web3_service import Web3Service
from routes import auth, test_flow, certificate, dashboard
load_dotenv()
app = Flask(__name__)
CORS(app)
# Configuration
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key')
app.config['MONGODB_URI'] = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/openlearnx')
app.config['WEB3_PROVIDER_URL'] = os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545')
app.config['CONTRACT_ADDRESS'] = os.getenv('CONTRACT_ADDRESS')
app.config['IPFS_GATEWAY'] = os.getenv('IPFS_GATEWAY', 'https://ipfs.infura.io:5001')
# Initialize services
mongo_service = MongoService(app.config['MONGODB_URI'])
web3_service = Web3Service(app.config['WEB3_PROVIDER_URL'], app.config['CONTRACT_ADDRESS'])
# Make services available to routes
app.config['MONGO_SERVICE'] = mongo_service
app.config['WEB3_SERVICE'] = web3_service
# Register blueprints
app.register_blueprint(auth.bp, url_prefix='/api/auth')
app.register_blueprint(test_flow.bp, url_prefix='/api/test')
app.register_blueprint(certificate.bp, url_prefix='/api/certificate')
app.register_blueprint(dashboard.bp, url_prefix='/api/dashboard')
@app.route('/')
def health_check():
return jsonify({"status": "OpenLearnX API is running", "version": "1.0.0"})
@app.errorhandler(Exception)
def handle_error(error):
app.logger.error(f"Error: {str(error)}")
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
# Initialize database
loop = asyncio.get_event_loop()
loop.run_until_complete(mongo_service.init_db())
app.run(debug=True, host='0.0.0.0', port=5000)
+63
View File
@@ -0,0 +1,63 @@
from motor.motor_asyncio import AsyncIOMotorClient
from pymongo.errors import ServerSelectionTimeoutError
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
class MongoService:
def __init__(self, uri: str):
try:
# Simple connection without custom SSL context
self.client = AsyncIOMotorClient(
uri,
serverSelectionTimeoutMS=30000,
connectTimeoutMS=30000,
socketTimeoutMS=30000
)
print("MongoDB client initialized successfully")
except Exception as e:
print(f"MongoDB connection failed: {e}")
# Fallback to basic connection
self.client = AsyncIOMotorClient(uri)
self.db = self.client.openlearnx
# Collections
self.users = self.db.users
self.questions = self.db.questions
self.test_sessions = self.db.test_sessions
self.certificates = self.db.certificates
self.peer_reviews = self.db.peer_reviews
async def init_db(self):
"""Initialize database with indexes and sample data"""
try:
# Test connection first
await self.client.admin.command('ping')
print("MongoDB connection successful!")
# Create indexes
await self.users.create_index("wallet_address", unique=True)
await self.users.create_index("email", unique=True, sparse=True)
await self.questions.create_index("subject")
await self.questions.create_index("difficulty")
await self.test_sessions.create_index("user_id")
await self.test_sessions.create_index("created_at")
await self.certificates.create_index("user_id")
await self.certificates.create_index("token_id", unique=True)
# Insert sample questions if none exist
if await self.questions.count_documents({}) == 0:
await self.insert_sample_questions()
print("Sample questions inserted successfully")
except ServerSelectionTimeoutError as e:
print(f"Failed to connect to MongoDB: {e}")
print("Continuing without database initialization...")
except Exception as e:
print(f"Database initialization error: {e}")
print("Continuing without database initialization...")
# ... rest of your existing methods remain the same
+1
View File
@@ -0,0 +1 @@
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220086786c2e82d490ba62f8e12df9157f5736c053e7cdd84543b9d7051b13134e364736f6c634300081e0033","sourceMap":"194:9169:10:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220086786c2e82d490ba62f8e12df9157f5736c053e7cdd84543b9d7051b13134e364736f6c634300081e0033","sourceMap":"194:9169:10:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"details\":\"Collection of functions related to the address type\",\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/Address.sol\":\"Address\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/Address.sol\":{\"keccak256\":\"0x006dd67219697fe68d7fbfdea512e7c4cb64a43565ed86171d67e844982da6fa\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://2455248c8ddd9cc6a7af76a13973cddf222072427e7b0e2a7d1aff345145e931\",\"dweb:/ipfs/QmfYjnjRbWqYpuxurqveE6HtzsY1Xx323J428AKQgtBJZm\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/Address.sol":"Address"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/Address.sol":{"keccak256":"0x006dd67219697fe68d7fbfdea512e7c4cb64a43565ed86171d67e844982da6fa","urls":["bzz-raw://2455248c8ddd9cc6a7af76a13973cddf222072427e7b0e2a7d1aff345145e931","dweb:/ipfs/QmfYjnjRbWqYpuxurqveE6HtzsY1Xx323J428AKQgtBJZm"],"license":"MIT"}},"version":1},"id":10}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"abi":[],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"details\":\"Provides information about the current execution context, including the sender of the transaction and its data. While these are generally available via msg.sender and msg.data, they should not be accessed in such a direct manner, since when dealing with meta-transactions the account sending and paying for execution may not be the actual sender (as far as an application is concerned). This contract is only required for intermediate, library-like contracts.\",\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/Context.sol\":\"Context\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/Context.sol\":{\"keccak256\":\"0xe2e337e6dde9ef6b680e07338c493ebea1b5fd09b43424112868e9cc1706bca7\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://6df0ddf21ce9f58271bdfaa85cde98b200ef242a05a3f85c2bc10a8294800a92\",\"dweb:/ipfs/QmRK2Y5Yc6BK7tGKkgsgn3aJEQGi5aakeSPZvS65PV8Xp3\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/Context.sol":"Context"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/Context.sol":{"keccak256":"0xe2e337e6dde9ef6b680e07338c493ebea1b5fd09b43424112868e9cc1706bca7","urls":["bzz-raw://6df0ddf21ce9f58271bdfaa85cde98b200ef242a05a3f85c2bc10a8294800a92","dweb:/ipfs/QmRK2Y5Yc6BK7tGKkgsgn3aJEQGi5aakeSPZvS65PV8Xp3"],"license":"MIT"}},"version":1},"id":11}
+1
View File
@@ -0,0 +1 @@
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220a3a42adcb4b001c32aa78ed40d4670ef16c1fc225305d63a33f8b9b9fd68df6d64736f6c634300081e0033","sourceMap":"424:971:12:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220a3a42adcb4b001c32aa78ed40d4670ef16c1fc225305d63a33f8b9b9fd68df6d64736f6c634300081e0033","sourceMap":"424:971:12:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"author\":\"Matt Condon (@shrugs)\",\"details\":\"Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number of elements in a mapping, issuing ERC721 ids, or counting request ids. Include with `using Counters for Counters.Counter;`\",\"kind\":\"dev\",\"methods\":{},\"title\":\"Counters\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/Counters.sol\":\"Counters\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/Counters.sol\":{\"keccak256\":\"0xf0018c2440fbe238dd3a8732fa8e17a0f9dce84d31451dc8a32f6d62b349c9f1\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://59e1c62884d55b70f3ae5432b44bb3166ad71ae3acd19c57ab6ddc3c87c325ee\",\"dweb:/ipfs/QmezuXg5GK5oeA4F91EZhozBFekhq5TD966bHPH18cCqhu\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/Counters.sol":"Counters"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/Counters.sol":{"keccak256":"0xf0018c2440fbe238dd3a8732fa8e17a0f9dce84d31451dc8a32f6d62b349c9f1","urls":["bzz-raw://59e1c62884d55b70f3ae5432b44bb3166ad71ae3acd19c57ab6ddc3c87c325ee","dweb:/ipfs/QmezuXg5GK5oeA4F91EZhozBFekhq5TD966bHPH18cCqhu"],"license":"MIT"}},"version":1},"id":12}
+1
View File
@@ -0,0 +1 @@
{"abi":[{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"supportsInterface(bytes4)":"01ffc9a7"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Implementation of the {IERC165} interface. Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check for the additional interface id that will be supported. For example: ```solidity function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); } ``` Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.\",\"kind\":\"dev\",\"methods\":{\"supportsInterface(bytes4)\":{\"details\":\"See {IERC165-supportsInterface}.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol\":\"ERC165\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol\":{\"keccak256\":\"0xd10975de010d89fd1c78dc5e8a9a7e7f496198085c151648f20cba166b32582b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://fb0048dee081f6fffa5f74afc3fb328483c2a30504e94a0ddd2a5114d731ec4d\",\"dweb:/ipfs/QmZptt1nmYoA5SgjwnSgWqgUSDgm4q52Yos3xhnMv3MV43\"]},\"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol\":{\"keccak256\":\"0x447a5f3ddc18419d41ff92b3773fb86471b1db25773e07f877f548918a185bf1\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://be161e54f24e5c6fae81a12db1a8ae87bc5ae1b0ddc805d82a1440a68455088f\",\"dweb:/ipfs/QmP7C3CHdY9urF4dEMb9wmsp1wMxHF6nhA2yQE5SKiPAdy\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"stateMutability":"view","type":"function","name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}]}],"devdoc":{"kind":"dev","methods":{"supportsInterface(bytes4)":{"details":"See {IERC165-supportsInterface}."}},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol":"ERC165"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol":{"keccak256":"0xd10975de010d89fd1c78dc5e8a9a7e7f496198085c151648f20cba166b32582b","urls":["bzz-raw://fb0048dee081f6fffa5f74afc3fb328483c2a30504e94a0ddd2a5114d731ec4d","dweb:/ipfs/QmZptt1nmYoA5SgjwnSgWqgUSDgm4q52Yos3xhnMv3MV43"],"license":"MIT"},"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol":{"keccak256":"0x447a5f3ddc18419d41ff92b3773fb86471b1db25773e07f877f548918a185bf1","urls":["bzz-raw://be161e54f24e5c6fae81a12db1a8ae87bc5ae1b0ddc805d82a1440a68455088f","dweb:/ipfs/QmP7C3CHdY9urF4dEMb9wmsp1wMxHF6nhA2yQE5SKiPAdy"],"license":"MIT"}},"version":1},"id":14}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"abi":[{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"supportsInterface(bytes4)":"01ffc9a7"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Interface of the ERC165 standard, as defined in the https://eips.ethereum.org/EIPS/eip-165[EIP]. Implementers can declare support of contract interfaces, which can then be queried by others ({ERC165Checker}). For an implementation, see {ERC165}.\",\"kind\":\"dev\",\"methods\":{\"supportsInterface(bytes4)\":{\"details\":\"Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol\":\"IERC165\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol\":{\"keccak256\":\"0x447a5f3ddc18419d41ff92b3773fb86471b1db25773e07f877f548918a185bf1\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://be161e54f24e5c6fae81a12db1a8ae87bc5ae1b0ddc805d82a1440a68455088f\",\"dweb:/ipfs/QmP7C3CHdY9urF4dEMb9wmsp1wMxHF6nhA2yQE5SKiPAdy\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"stateMutability":"view","type":"function","name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}]}],"devdoc":{"kind":"dev","methods":{"supportsInterface(bytes4)":{"details":"Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas."}},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol":"IERC165"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol":{"keccak256":"0x447a5f3ddc18419d41ff92b3773fb86471b1db25773e07f877f548918a185bf1","urls":["bzz-raw://be161e54f24e5c6fae81a12db1a8ae87bc5ae1b0ddc805d82a1440a68455088f","dweb:/ipfs/QmP7C3CHdY9urF4dEMb9wmsp1wMxHF6nhA2yQE5SKiPAdy"],"license":"MIT"}},"version":1},"id":15}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"abi":[{"type":"function","name":"onERC721Received","inputs":[{"name":"operator","type":"address","internalType":"address"},{"name":"from","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes4","internalType":"bytes4"}],"stateMutability":"nonpayable"}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"onERC721Received(address,address,uint256,bytes)":"150b7a02"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"onERC721Received\",\"outputs\":[{\"internalType\":\"bytes4\",\"name\":\"\",\"type\":\"bytes4\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Interface for any contract that wants to support safeTransfers from ERC721 asset contracts.\",\"kind\":\"dev\",\"methods\":{\"onERC721Received(address,address,uint256,bytes)\":{\"details\":\"Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} by `operator` from `from`, this function is called. It must return its Solidity selector to confirm the token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.\"}},\"title\":\"ERC721 token receiver interface\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol\":\"IERC721Receiver\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol\":{\"keccak256\":\"0xa82b58eca1ee256be466e536706850163d2ec7821945abd6b4778cfb3bee37da\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://6e75cf83beb757b8855791088546b8337e9d4684e169400c20d44a515353b708\",\"dweb:/ipfs/QmYvPafLfoquiDMEj7CKHtvbgHu7TJNPSVPSCjrtjV8HjV\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}]}],"devdoc":{"kind":"dev","methods":{"onERC721Received(address,address,uint256,bytes)":{"details":"Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} by `operator` from `from`, this function is called. It must return its Solidity selector to confirm the token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`."}},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol":"IERC721Receiver"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol":{"keccak256":"0xa82b58eca1ee256be466e536706850163d2ec7821945abd6b4778cfb3bee37da","urls":["bzz-raw://6e75cf83beb757b8855791088546b8337e9d4684e169400c20d44a515353b708","dweb:/ipfs/QmYvPafLfoquiDMEj7CKHtvbgHu7TJNPSVPSCjrtjV8HjV"],"license":"MIT"}},"version":1},"id":7}
+1
View File
@@ -0,0 +1 @@
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea26469706673582212207452f0ae8e85d061bad63a9a107ad312b49978d0830aa86d3756c17f6dbcc29c64736f6c634300081e0033","sourceMap":"202:12582:16:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea26469706673582212207452f0ae8e85d061bad63a9a107ad312b49978d0830aa86d3756c17f6dbcc29c64736f6c634300081e0033","sourceMap":"202:12582:16:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"details\":\"Standard math utilities missing in the Solidity language.\",\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/math/Math.sol\":\"Math\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/math/Math.sol\":{\"keccak256\":\"0xe4455ac1eb7fc497bb7402579e7b4d64d928b846fce7d2b6fde06d366f21c2b3\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://cc8841b3cd48ad125e2f46323c8bad3aa0e88e399ec62acb9e57efa7e7c8058c\",\"dweb:/ipfs/QmSqE4mXHA2BXW58deDbXE8MTcsL5JSKNDbm23sVQxRLPS\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/math/Math.sol":"Math"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/math/Math.sol":{"keccak256":"0xe4455ac1eb7fc497bb7402579e7b4d64d928b846fce7d2b6fde06d366f21c2b3","urls":["bzz-raw://cc8841b3cd48ad125e2f46323c8bad3aa0e88e399ec62acb9e57efa7e7c8058c","dweb:/ipfs/QmSqE4mXHA2BXW58deDbXE8MTcsL5JSKNDbm23sVQxRLPS"],"license":"MIT"}},"version":1},"id":16}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220fc3cbe91605fb574b20643e793dece5fabf482e1dfa74fe76bb18c24f02a675e64736f6c634300081e0033","sourceMap":"215:1047:17:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220fc3cbe91605fb574b20643e793dece5fabf482e1dfa74fe76bb18c24f02a675e64736f6c634300081e0033","sourceMap":"215:1047:17:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"details\":\"Standard signed math utilities missing in the Solidity language.\",\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol\":\"SignedMath\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol\":{\"keccak256\":\"0xf92515413956f529d95977adc9b0567d583c6203fc31ab1c23824c35187e3ddc\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c50fcc459e49a9858b6d8ad5f911295cb7c9ab57567845a250bf0153f84a95c7\",\"dweb:/ipfs/QmcEW85JRzvDkQggxiBBLVAasXWdkhEysqypj9EaB6H2g6\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol":"SignedMath"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol":{"keccak256":"0xf92515413956f529d95977adc9b0567d583c6203fc31ab1c23824c35187e3ddc","urls":["bzz-raw://c50fcc459e49a9858b6d8ad5f911295cb7c9ab57567845a250bf0153f84a95c7","dweb:/ipfs/QmcEW85JRzvDkQggxiBBLVAasXWdkhEysqypj9EaB6H2g6"],"license":"MIT"}},"version":1},"id":17}
+1
View File
@@ -0,0 +1 @@
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea26469706673582212201f54765c08e65c3ecf184681909ecdb2961e29431cd7450e467f2e6f1d0606d564736f6c634300081e0033","sourceMap":"220:2559:13:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea26469706673582212201f54765c08e65c3ecf184681909ecdb2961e29431cd7450e467f2e6f1d0606d564736f6c634300081e0033","sourceMap":"220:2559:13:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"details\":\"String operations.\",\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts/contracts/utils/Strings.sol\":\"Strings\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":openzeppelin/=lib/openzeppelin-contracts/contracts/\"]},\"sources\":{\"lib/openzeppelin-contracts/contracts/utils/Strings.sol\":{\"keccak256\":\"0x3088eb2868e8d13d89d16670b5f8612c4ab9ff8956272837d8e90106c59c14a0\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b81d9ff6559ea5c47fc573e17ece6d9ba5d6839e213e6ebc3b4c5c8fe4199d7f\",\"dweb:/ipfs/QmPCW1bFisUzJkyjroY3yipwfism9RRCigCcK1hbXtVM8n\"]},\"lib/openzeppelin-contracts/contracts/utils/math/Math.sol\":{\"keccak256\":\"0xe4455ac1eb7fc497bb7402579e7b4d64d928b846fce7d2b6fde06d366f21c2b3\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://cc8841b3cd48ad125e2f46323c8bad3aa0e88e399ec62acb9e57efa7e7c8058c\",\"dweb:/ipfs/QmSqE4mXHA2BXW58deDbXE8MTcsL5JSKNDbm23sVQxRLPS\"]},\"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol\":{\"keccak256\":\"0xf92515413956f529d95977adc9b0567d583c6203fc31ab1c23824c35187e3ddc\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c50fcc459e49a9858b6d8ad5f911295cb7c9ab57567845a250bf0153f84a95c7\",\"dweb:/ipfs/QmcEW85JRzvDkQggxiBBLVAasXWdkhEysqypj9EaB6H2g6\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/openzeppelin-contracts/contracts/utils/Strings.sol":"Strings"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/Strings.sol":{"keccak256":"0x3088eb2868e8d13d89d16670b5f8612c4ab9ff8956272837d8e90106c59c14a0","urls":["bzz-raw://b81d9ff6559ea5c47fc573e17ece6d9ba5d6839e213e6ebc3b4c5c8fe4199d7f","dweb:/ipfs/QmPCW1bFisUzJkyjroY3yipwfism9RRCigCcK1hbXtVM8n"],"license":"MIT"},"lib/openzeppelin-contracts/contracts/utils/math/Math.sol":{"keccak256":"0xe4455ac1eb7fc497bb7402579e7b4d64d928b846fce7d2b6fde06d366f21c2b3","urls":["bzz-raw://cc8841b3cd48ad125e2f46323c8bad3aa0e88e399ec62acb9e57efa7e7c8058c","dweb:/ipfs/QmSqE4mXHA2BXW58deDbXE8MTcsL5JSKNDbm23sVQxRLPS"],"license":"MIT"},"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol":{"keccak256":"0xf92515413956f529d95977adc9b0567d583c6203fc31ab1c23824c35187e3ddc","urls":["bzz-raw://c50fcc459e49a9858b6d8ad5f911295cb7c9ab57567845a250bf0153f84a95c7","dweb:/ipfs/QmcEW85JRzvDkQggxiBBLVAasXWdkhEysqypj9EaB6H2g6"],"license":"MIT"}},"version":1},"id":13}
@@ -0,0 +1 @@
{"id":"1d1b0b35c357d500","source_id_to_path":{"0":"contracts/CertificateNFT.sol","1":"lib/openzeppelin-contracts/contracts/access/Ownable.sol","2":"lib/openzeppelin-contracts/contracts/interfaces/IERC165.sol","3":"lib/openzeppelin-contracts/contracts/interfaces/IERC4906.sol","4":"lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol","5":"lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol","6":"lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol","7":"lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol","8":"lib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol","9":"lib/openzeppelin-contracts/contracts/token/ERC721/extensions/IERC721Metadata.sol","10":"lib/openzeppelin-contracts/contracts/utils/Address.sol","11":"lib/openzeppelin-contracts/contracts/utils/Context.sol","12":"lib/openzeppelin-contracts/contracts/utils/Counters.sol","13":"lib/openzeppelin-contracts/contracts/utils/Strings.sol","14":"lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol","15":"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol","16":"lib/openzeppelin-contracts/contracts/utils/math/Math.sol","17":"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol"},"language":"Solidity"}
+99
View File
@@ -0,0 +1,99 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime, timedelta
import uuid
bp = Blueprint('auth', __name__)
@bp.route('/nonce', methods=['POST'])
async def get_nonce():
"""Generate nonce for wallet signature"""
data = request.get_json()
wallet_address = data.get('wallet_address')
if not wallet_address:
return jsonify({"error": "Wallet address required"}), 400
web3_service = current_app.config['WEB3_SERVICE']
nonce = web3_service.generate_nonce()
# Store nonce temporarily (in production, use Redis)
message = f"Sign this message to authenticate with OpenLearnX: {nonce}"
return jsonify({
"nonce": nonce,
"message": message
})
@bp.route('/verify', methods=['POST'])
async def verify_signature():
"""Verify MetaMask signature and create session"""
data = request.get_json()
wallet_address = data.get('wallet_address')
signature = data.get('signature')
message = data.get('message')
if not all([wallet_address, signature, message]):
return jsonify({"error": "Missing required fields"}), 400
web3_service = current_app.config['WEB3_SERVICE']
mongo_service = current_app.config['MONGO_SERVICE']
# Verify signature
if not web3_service.verify_signature(wallet_address, message, signature):
return jsonify({"error": "Invalid signature"}), 401
# Create or get user
user = await mongo_service.create_user(wallet_address)
await mongo_service.update_user_login(wallet_address)
# Create JWT token
token_payload = {
'user_id': str(user['_id']),
'wallet_address': wallet_address,
'exp': datetime.utcnow() + timedelta(days=7)
}
token = jwt.encode(
token_payload,
current_app.config['SECRET_KEY'],
algorithm='HS256'
)
return jsonify({
"success": True,
"token": token,
"user": {
"id": str(user['_id']),
"wallet_address": user['wallet_address'],
"created_at": user['created_at'].isoformat(),
"total_tests": user.get('total_tests', 0),
"certificates": len(user.get('certificates', []))
}
})
@bp.route('/profile', methods=['GET'])
async def get_profile():
"""Get user profile"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({"error": "Token required"}), 401
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
user_id = payload['user_id']
mongo_service = current_app.config['MONGO_SERVICE']
analytics = await mongo_service.get_user_analytics(user_id)
return jsonify(analytics)
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
+181
View File
@@ -0,0 +1,181 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
import json
import uuid
from datetime import datetime
bp = Blueprint('certificate', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id'], payload['wallet_address']
except:
return None, None
@bp.route('/mint', methods=['POST'])
async def mint_certificate():
"""Mint NFT certificate for completed test"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id, wallet_address = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
session_id = data.get('session_id')
mongo_service = current_app.config['MONGO_SERVICE']
web3_service = current_app.config['WEB3_SERVICE']
# Get completed session
session = await mongo_service.get_test_session(session_id)
if not session or not session.get('completed'):
return jsonify({"error": "Test session not completed"}), 400
if session['user_id'] != user_id:
return jsonify({"error": "Unauthorized"}), 403
# Check if certificate already minted for this session
existing_cert = await mongo_service.certificates.find_one({"session_id": session_id})
if existing_cert:
return jsonify({"error": "Certificate already minted"}), 400
# Create certificate metadata
certificate_metadata = {
"name": f"OpenLearnX Certificate - {session['subject']}",
"description": f"Certificate of completion for {session['subject']} assessment",
"image": f"https://certificates.openlearnx.com/{session_id}.png",
"attributes": [
{"trait_type": "Subject", "value": session['subject']},
{"trait_type": "Score", "value": f"{session['score']:.1%}"},
{"trait_type": "Date", "value": session['created_at'].strftime("%Y-%m-%d")},
{"trait_type": "Questions", "value": len(session.get('answers', []))},
{"trait_type": "Difficulty", "value": session.get('current_difficulty', 2)}
],
"certificate_data": {
"student_wallet": wallet_address,
"subject": session['subject'],
"score": session['score'],
"completion_date": session.get('completed_at', datetime.utcnow()).isoformat(),
"questions_answered": len(session.get('answers', [])),
"session_id": session_id
}
}
# Upload to IPFS (simplified - in production use proper IPFS service)
ipfs_hash = f"Qm{uuid.uuid4().hex[:40]}" # Mock IPFS hash
token_uri = f"https://ipfs.io/ipfs/{ipfs_hash}"
try:
# Mint NFT (requires private key for the minting account)
private_key = current_app.config.get('MINTER_PRIVATE_KEY')
if not private_key:
return jsonify({"error": "Minting not configured"}), 500
tx_hash = web3_service.mint_certificate(
wallet_address,
token_uri,
private_key
)
if not tx_hash:
return jsonify({"error": "Minting failed"}), 500
# Get token ID from transaction (simplified)
token_id = await mongo_service.certificates.count_documents({}) + 1
# Save certificate record
cert_record = await mongo_service.create_certificate_record(
user_id=user_id,
token_id=token_id,
tx_hash=tx_hash,
ipfs_hash=ipfs_hash,
subject=session['subject'],
score=session['score']
)
# Update session with certificate info
await mongo_service.update_test_session(session_id, {
'certificate_minted': True,
'certificate_token_id': token_id,
'certificate_tx_hash': tx_hash
})
return jsonify({
"success": True,
"certificate": {
"token_id": token_id,
"transaction_hash": tx_hash,
"ipfs_hash": ipfs_hash,
"token_uri": token_uri,
"metadata": certificate_metadata
}
})
except Exception as e:
return jsonify({"error": f"Minting failed: {str(e)}"}), 500
@bp.route('/verify/<int:token_id>', methods=['GET'])
async def verify_certificate(token_id):
"""Verify certificate by token ID"""
web3_service = current_app.config['WEB3_SERVICE']
mongo_service = current_app.config['MONGO_SERVICE']
# Get certificate from blockchain
cert_details = web3_service.get_certificate_details(token_id)
if not cert_details:
return jsonify({"error": "Certificate not found"}), 404
# Get additional details from database
db_cert = await mongo_service.certificates.find_one({"token_id": token_id})
response = {
"valid": True,
"token_id": token_id,
"owner": cert_details['owner'],
"token_uri": cert_details['token_uri']
}
if db_cert:
response.update({
"subject": db_cert['subject'],
"score": db_cert['score'],
"issue_date": db_cert['created_at'].isoformat(),
"transaction_hash": db_cert['transaction_hash']
})
return jsonify(response)
@bp.route('/user/<user_id>', methods=['GET'])
async def get_user_certificates(user_id):
"""Get all certificates for a user"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id, _ = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
certificates = await mongo_service.get_user_certificates(user_id)
formatted_certs = []
for cert in certificates:
formatted_certs.append({
"id": str(cert['_id']),
"token_id": cert['token_id'],
"subject": cert['subject'],
"score": cert['score'],
"created_at": cert['created_at'].isoformat(),
"transaction_hash": cert['transaction_hash'],
"verified": cert.get('verified', True)
})
return jsonify({"certificates": formatted_certs})
+211
View File
@@ -0,0 +1,211 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime, timedelta
bp = Blueprint('dashboard', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id']
except:
return None
@bp.route('/student/<user_id>', methods=['GET'])
async def get_student_dashboard(user_id):
"""Get comprehensive student dashboard"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
analytics = await mongo_service.get_user_analytics(user_id)
if not analytics:
return jsonify({"error": "User not found"}), 404
# Get recent activity
recent_sessions = await mongo_service.test_sessions.find({
"user_id": user_id
}).sort("created_at", -1).limit(5).to_list(length=5)
# Get certificates
certificates = await mongo_service.get_user_certificates(user_id)
# Calculate streaks and progress
today = datetime.utcnow().date()
week_ago = today - timedelta(days=7)
month_ago = today - timedelta(days=30)
week_sessions = [s for s in recent_sessions
if s['created_at'].date() >= week_ago]
month_sessions = [s for s in recent_sessions
if s['created_at'].date() >= month_ago]
dashboard_data = {
"user_info": {
"id": str(analytics['user']['_id']),
"wallet_address": analytics['user']['wallet_address'],
"member_since": analytics['user']['created_at'].isoformat(),
"last_login": analytics['user']['last_login'].isoformat()
},
"overview": {
"total_tests": analytics['total_tests'],
"completed_tests": analytics['completed_tests'],
"average_score": round(analytics['average_score'] * 100, 1),
"certificates_earned": analytics['certificates_earned'],
"this_week_tests": len(week_sessions),
"this_month_tests": len(month_sessions)
},
"subject_breakdown": {
subject: {
"tests_taken": data['tests'],
"average_score": round(data['avg_score'] * 100, 1),
"mastery_level": get_mastery_level(data['avg_score'])
}
for subject, data in analytics['subject_breakdown'].items()
},
"recent_activity": [
{
"id": str(session['_id']),
"subject": session['subject'],
"score": round(session.get('score', 0) * 100, 1),
"completed": session.get('completed', False),
"date": session['created_at'].isoformat(),
"questions_answered": len(session.get('answers', []))
}
for session in recent_sessions
],
"certificates": [
{
"id": str(cert['_id']),
"token_id": cert['token_id'],
"subject": cert['subject'],
"score": round(cert['score'] * 100, 1),
"earned_date": cert['created_at'].isoformat(),
"blockchain_verified": cert.get('verified', True)
}
for cert in certificates
],
"progress_chart": await get_progress_chart_data(mongo_service, user_id),
"competency_radar": get_competency_radar_data(analytics['subject_breakdown'])
}
return jsonify(dashboard_data)
@bp.route('/instructor/overview', methods=['GET'])
async def get_instructor_dashboard():
"""Get instructor dashboard with class overview"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
# Get overall platform statistics
total_users = await mongo_service.users.count_documents({})
total_tests = await mongo_service.test_sessions.count_documents({})
total_certificates = await mongo_service.certificates.count_documents({})
# Get recent activity across all users
recent_sessions = await mongo_service.test_sessions.find({}).sort(
"created_at", -1
).limit(20).to_list(length=20)
# Calculate subject popularity
subject_stats = {}
for session in recent_sessions:
subject = session.get('subject', 'Unknown')
if subject not in subject_stats:
subject_stats[subject] = {'count': 0, 'total_score': 0}
subject_stats[subject]['count'] += 1
subject_stats[subject]['total_score'] += session.get('score', 0)
for subject in subject_stats:
subject_stats[subject]['avg_score'] = (
subject_stats[subject]['total_score'] / subject_stats[subject]['count']
)
dashboard_data = {
"platform_overview": {
"total_users": total_users,
"total_tests": total_tests,
"total_certificates": total_certificates,
"active_users_today": len([s for s in recent_sessions
if s['created_at'].date() == datetime.utcnow().date()])
},
"subject_performance": {
subject: {
"total_attempts": data['count'],
"average_score": round(data['avg_score'] * 100, 1),
"difficulty_trend": "increasing" if data['avg_score'] > 0.7 else "stable"
}
for subject, data in subject_stats.items()
},
"recent_activity": [
{
"user_id": session['user_id'],
"subject": session['subject'],
"score": round(session.get('score', 0) * 100, 1),
"completed": session.get('completed', False),
"timestamp": session['created_at'].isoformat()
}
for session in recent_sessions[:10]
]
}
return jsonify(dashboard_data)
def get_mastery_level(score):
"""Determine mastery level based on score"""
if score >= 0.9:
return "Expert"
elif score >= 0.8:
return "Advanced"
elif score >= 0.7:
return "Proficient"
elif score >= 0.6:
return "Developing"
else:
return "Beginner"
async def get_progress_chart_data(mongo_service, user_id):
"""Get progress chart data for the last 30 days"""
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
sessions = await mongo_service.test_sessions.find({
"user_id": user_id,
"created_at": {"$gte": thirty_days_ago},
"completed": True
}).sort("created_at", 1).to_list(length=None)
progress_data = []
for session in sessions:
progress_data.append({
"date": session['created_at'].strftime("%Y-%m-%d"),
"score": round(session.get('score', 0) * 100, 1),
"subject": session['subject']
})
return progress_data
def get_competency_radar_data(subject_breakdown):
"""Generate radar chart data for competencies"""
radar_data = []
for subject, data in subject_breakdown.items():
radar_data.append({
"subject": subject,
"score": round(data['avg_score'] * 100, 1),
"tests": data['tests']
})
return radar_data
+201
View File
@@ -0,0 +1,201 @@
from flask import Blueprint, request, jsonify, current_app
import jwt
from datetime import datetime
import random
bp = Blueprint('test', __name__)
def get_user_from_token(token):
"""Extract user from JWT token"""
try:
payload = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
return payload['user_id']
except:
return None
@bp.route('/start', methods=['POST'])
async def start_test():
"""Start a new test session"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
subject = data.get('subject', 'General')
mongo_service = current_app.config['MONGO_SERVICE']
# Create test session
session = await mongo_service.create_test_session(user_id, subject)
# Get first question
questions = await mongo_service.get_questions_by_difficulty(2, 1) # Start with medium
if not questions:
return jsonify({"error": "No questions available"}), 404
question = questions[0]
session['questions'].append(str(question['_id']))
await mongo_service.update_test_session(str(session['_id']), {
'questions': session['questions'],
'current_question': 0
})
return jsonify({
"session_id": str(session['_id']),
"question": {
"id": str(question['_id']),
"question": question['question'],
"options": question['options'],
"subject": question['subject'],
"difficulty": question['difficulty']
},
"question_number": 1,
"total_questions": 10
})
@bp.route('/answer', methods=['POST'])
async def submit_answer():
"""Submit answer and get feedback"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
user_id = get_user_from_token(token)
if not user_id:
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
session_id = data.get('session_id')
question_id = data.get('question_id')
answer = data.get('answer')
mongo_service = current_app.config['MONGO_SERVICE']
# Get session and question
session = await mongo_service.get_test_session(session_id)
question = await mongo_service.questions.find_one({"_id": question_id})
if not session or not question:
return jsonify({"error": "Invalid session or question"}), 404
# Check answer
is_correct = answer == question['correct_answer']
confidence_score = random.uniform(0.7, 0.95) if is_correct else random.uniform(0.1, 0.4)
# Update session
if 'answers' not in session:
session['answers'] = []
answer_record = {
'question_id': question_id,
'answer': answer,
'correct': is_correct,
'timestamp': datetime.utcnow()
}
session['answers'].append(answer_record)
# Calculate current score
correct_answers = sum(1 for a in session['answers'] if a['correct'])
current_score = correct_answers / len(session['answers'])
# Update difficulty for next question
current_difficulty = session.get('current_difficulty', 2)
if is_correct and confidence_score > 0.8:
current_difficulty = min(5, current_difficulty + 1)
elif not is_correct and confidence_score < 0.3:
current_difficulty = max(1, current_difficulty - 1)
await mongo_service.update_test_session(session_id, {
'answers': session['answers'],
'score': current_score,
'current_difficulty': current_difficulty
})
# Prepare response
feedback = {
"correct": is_correct,
"confidence_score": round(confidence_score, 2),
"explanation": question['explanation'],
"correct_answer": question['options'][question['correct_answer']],
"current_score": round(current_score * 100, 1),
"total_answered": len(session['answers'])
}
# Get next question if test not complete
next_question = None
if len(session['answers']) < 10: # 10 questions per test
questions = await mongo_service.get_questions_by_difficulty(current_difficulty, 1)
if questions:
next_q = questions[0]
session['questions'].append(str(next_q['_id']))
await mongo_service.update_test_session(session_id, {
'questions': session['questions']
})
next_question = {
"id": str(next_q['_id']),
"question": next_q['question'],
"options": next_q['options'],
"subject": next_q['subject'],
"difficulty": next_q['difficulty']
}
else:
# Test completed
await mongo_service.update_test_session(session_id, {
'completed': True,
'completed_at': datetime.utcnow()
})
# Update user stats
await mongo_service.users.update_one(
{"_id": user_id},
{
"$inc": {"total_tests": 1, "total_score": current_score},
"$set": {f"competency_scores.{session['subject']}": current_score}
}
)
response = {
"feedback": feedback,
"test_completed": len(session['answers']) >= 10
}
if next_question:
response['next_question'] = next_question
response['question_number'] = len(session['answers']) + 1
return jsonify(response)
@bp.route('/sessions/<user_id>', methods=['GET'])
async def get_user_sessions(user_id):
"""Get user's test sessions"""
token = request.headers.get('Authorization', '').replace('Bearer ', '')
token_user_id = get_user_from_token(token)
if not token_user_id or token_user_id != user_id:
return jsonify({"error": "Unauthorized"}), 403
mongo_service = current_app.config['MONGO_SERVICE']
sessions = await mongo_service.test_sessions.find(
{"user_id": user_id}
).sort("created_at", -1).limit(20).to_list(length=20)
# Format sessions for response
formatted_sessions = []
for session in sessions:
formatted_sessions.append({
"id": str(session['_id']),
"subject": session['subject'],
"score": session.get('score', 0),
"completed": session.get('completed', False),
"questions_answered": len(session.get('answers', [])),
"created_at": session['created_at'].isoformat()
})
return jsonify({"sessions": formatted_sessions})
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Deployment script for OpenLearnX smart contracts
"""
import os
import json
from pathlib import Path
from web3 import Web3
from eth_account import Account
from dotenv import load_dotenv
# Load environment variables from backend/.env
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env")
def deploy_contract():
# Load environment variables
provider_url = os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545')
private_key = os.getenv('DEPLOYER_PRIVATE_KEY')
if not private_key:
raise ValueError("DEPLOYER_PRIVATE_KEY environment variable required")
# Connect to Web3
w3 = Web3(Web3.HTTPProvider(provider_url))
if not w3.is_connected():
raise Exception(f"Failed to connect to {provider_url}")
account = Account.from_key(private_key)
print(f"Deploying from account: {account.address}")
print(f"Balance: {w3.eth.get_balance(account.address) / 10**18} ETH")
# Load contract bytecode and ABI
contract_path = BASE_DIR / "out" / "CertificateNFT.sol" / "CertificateNFT.json"
if not contract_path.exists():
print("Contract not compiled. Running forge build...")
import subprocess
result = subprocess.run(["forge", "build"], cwd=BASE_DIR, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"forge build failed: {result.stderr}")
with open(contract_path, 'r') as f:
contract_data = json.load(f)
# Deploy contract
contract = w3.eth.contract(
abi=contract_data['abi'],
bytecode=contract_data['bytecode']['object']
)
# Build transaction with higher gas limit
transaction = contract.constructor().build_transaction({
'from': account.address,
'nonce': w3.eth.get_transaction_count(account.address),
'gas': 5000000, # Increased gas limit
'gasPrice': w3.to_wei('20', 'gwei')
})
# Sign and send transaction
signed_txn = w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
print(f"Transaction hash: {tx_hash.hex()}")
# Wait for receipt with timeout
try:
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
# Check if transaction was successful
if receipt.status == 0:
raise Exception("Transaction failed - check gas limit and contract code")
contract_address = receipt.contractAddress
if not contract_address:
raise Exception("Contract address is None - deployment failed")
print(f"Contract deployed at: {contract_address}")
print(f"Gas used: {receipt.gasUsed}")
print(f"Transaction status: {'Success' if receipt.status == 1 else 'Failed'}")
except Exception as e:
print(f"Error waiting for transaction receipt: {e}")
return None
# Save deployment info
deployment_info = {
'contract_address': contract_address,
'transaction_hash': tx_hash.hex(),
'deployer': account.address,
'network': 'local' if 'localhost' in provider_url or '127.0.0.1' in provider_url else 'unknown',
'abi': contract_data['abi'],
'gas_used': receipt.gasUsed,
'block_number': receipt.blockNumber,
'status': receipt.status
}
deployment_file = BASE_DIR / "deployment.json"
with open(deployment_file, 'w') as f:
json.dump(deployment_info, f, indent=2)
print(f"Deployment info saved to: {deployment_file}")
print(f"\nAdd this to your .env file:")
print(f"CONTRACT_ADDRESS={contract_address}")
return contract_address
if __name__ == '__main__':
try:
contract_address = deploy_contract()
if contract_address:
print(f"\n✅ Deployment successful!")
print(f"Contract Address: {contract_address}")
else:
print(f"\n❌ Deployment failed!")
except Exception as e:
print(f"❌ Deployment failed: {e}")
+283
View File
@@ -0,0 +1,283 @@
from web3 import Web3
from eth_account.messages import encode_defunct
import json
import secrets
import time
from typing import Optional, Dict, Any
from pathlib import Path
class Web3Service:
def __init__(self, provider_url: str, contract_address: Optional[str] = None):
self.w3 = Web3(Web3.HTTPProvider(provider_url))
self.contract_address = contract_address
self.contract = None
if contract_address:
self.load_contract()
def load_contract(self):
"""Load the smart contract ABI and create contract instance"""
try:
# Updated path to match Foundry's output structure
contract_path = Path('out/CertificateNFT.sol/CertificateNFT.json')
if not contract_path.exists():
print(f"Contract JSON not found at {contract_path}")
return
with open(contract_path, 'r') as f:
contract_data = json.load(f)
abi = contract_data['abi']
self.contract = self.w3.eth.contract(
address=self.contract_address,
abi=abi
)
print(f"Contract loaded successfully at {self.contract_address}")
except Exception as e:
print(f"Failed to load contract: {e}")
def generate_nonce(self) -> str:
"""Generate a random nonce for signature verification"""
return secrets.token_hex(16)
def verify_signature(self, address: str, message: str, signature: str) -> bool:
"""Verify MetaMask signature"""
try:
# Create the message that was signed
message_hash = encode_defunct(text=message)
# Recover the address from signature
recovered_address = self.w3.eth.account.recover_message(
message_hash,
signature=signature
)
# Compare addresses (case insensitive)
return recovered_address.lower() == address.lower()
except Exception as e:
print(f"Signature verification failed: {e}")
return False
def mint_certificate(self, to_address: str, token_uri: str, private_key: str) -> Optional[str]:
"""Mint an NFT certificate using the simple mintCertificate function"""
if not self.contract:
raise Exception("Contract not loaded")
try:
# Get account from private key
account = self.w3.eth.account.from_key(private_key)
# Build transaction
transaction = self.contract.functions.mintCertificate(
to_address,
token_uri
).build_transaction({
'from': account.address,
'nonce': self.w3.eth.get_transaction_count(account.address),
'gas': 500000, # Increased gas limit
'gasPrice': self.w3.to_wei('20', 'gwei')
})
# Sign and send transaction
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
# Wait for transaction receipt
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
if receipt.status == 1:
print(f"Certificate minted successfully. TX: {receipt.transactionHash.hex()}")
return receipt.transactionHash.hex()
else:
print(f"Transaction failed. Status: {receipt.status}")
return None
except Exception as e:
print(f"Minting failed: {e}")
return None
def mint_certificate_with_details(self, to_address: str, token_uri: str,
subject: str, student_name: str, score: int,
private_key: str) -> Optional[str]:
"""Mint an NFT certificate with detailed information"""
if not self.contract:
raise Exception("Contract not loaded")
try:
# Get account from private key
account = self.w3.eth.account.from_key(private_key)
# Build transaction with detailed function
transaction = self.contract.functions.mintCertificateWithDetails(
to_address,
token_uri,
subject,
student_name,
score
).build_transaction({
'from': account.address,
'nonce': self.w3.eth.get_transaction_count(account.address),
'gas': 600000, # Higher gas for detailed function
'gasPrice': self.w3.to_wei('20', 'gwei')
})
# Sign and send transaction
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
# Wait for transaction receipt
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
if receipt.status == 1:
print(f"Detailed certificate minted successfully. TX: {receipt.transactionHash.hex()}")
return receipt.transactionHash.hex()
else:
print(f"Transaction failed. Status: {receipt.status}")
return None
except Exception as e:
print(f"Detailed minting failed: {e}")
return None
def get_certificate_details(self, token_id: int) -> Optional[Dict]:
"""Get certificate details by token ID"""
if not self.contract:
return None
try:
# Get certificate struct data
certificate = self.contract.functions.getCertificate(token_id).call()
# Get owner and token URI
owner = self.contract.functions.ownerOf(token_id).call()
token_uri = self.contract.functions.tokenURI(token_id).call()
return {
'token_id': token_id,
'owner': owner,
'token_uri': token_uri,
'subject': certificate[0],
'student_name': certificate[1],
'score': certificate[2],
'timestamp': certificate[3],
'verified': certificate[4]
}
except Exception as e:
print(f"Failed to get certificate details: {e}")
return None
def get_user_certificates(self, user_address: str) -> Optional[list]:
"""Get all certificate token IDs for a user"""
if not self.contract:
return None
try:
token_ids = self.contract.functions.getUserCertificates(user_address).call()
return token_ids
except Exception as e:
print(f"Failed to get user certificates: {e}")
return None
def verify_certificate(self, token_id: int) -> bool:
"""Verify if a certificate is valid"""
if not self.contract:
return False
try:
is_verified = self.contract.functions.verifyCertificate(token_id).call()
return is_verified
except Exception as e:
print(f"Failed to verify certificate: {e}")
return False
def get_total_supply(self) -> int:
"""Get total number of certificates minted"""
if not self.contract:
return 0
try:
total = self.contract.functions.totalSupply().call()
return total
except Exception as e:
print(f"Failed to get total supply: {e}")
return 0
def get_latest_token_id(self) -> int:
"""Get the latest token ID (useful for getting newly minted certificate)"""
return self.get_total_supply()
def get_transaction_receipt(self, tx_hash: str) -> Optional[Dict]:
"""Get transaction receipt for a given hash"""
try:
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
return {
'transaction_hash': receipt.transactionHash.hex(),
'block_number': receipt.blockNumber,
'gas_used': receipt.gasUsed,
'status': receipt.status,
'contract_address': receipt.contractAddress
}
except Exception as e:
print(f"Failed to get transaction receipt: {e}")
return None
def is_connected(self) -> bool:
"""Check if connected to blockchain"""
try:
return self.w3.is_connected()
except:
return False
def get_balance(self, address: str) -> float:
"""Get ETH balance for an address"""
try:
balance_wei = self.w3.eth.get_balance(address)
return self.w3.from_wei(balance_wei, 'ether')
except Exception as e:
print(f"Failed to get balance: {e}")
return 0.0
def get_gas_price(self) -> int:
"""Get current gas price"""
try:
return self.w3.eth.gas_price
except Exception as e:
print(f"Failed to get gas price: {e}")
return self.w3.to_wei('20', 'gwei') # Default fallback
def estimate_gas(self, to_address: str, token_uri: str, account_address: str) -> int:
"""Estimate gas for certificate minting"""
if not self.contract:
return 500000 # Default estimate
try:
gas_estimate = self.contract.functions.mintCertificate(
to_address,
token_uri
).estimate_gas({'from': account_address})
# Add 20% buffer
return int(gas_estimate * 1.2)
except Exception as e:
print(f"Failed to estimate gas: {e}")
return 500000 # Default fallback
def get_contract_info(self) -> Dict:
"""Get basic contract information"""
if not self.contract:
return {}
try:
return {
'address': self.contract_address,
'total_certificates': self.get_total_supply(),
'is_connected': self.is_connected(),
'network_id': self.w3.eth.chain_id,
'latest_block': self.w3.eth.block_number
}
except Exception as e:
print(f"Failed to get contract info: {e}")
return {}
+20 -15
View File
@@ -1,28 +1,33 @@
# FastAPI stack (optional — keep if you're using it too) # Python 3.10 compatible versions
fastapi==0.111.0
uvicorn[standard]==0.30.1
# Flask (add-on or alternative) # Web frameworks
Flask==3.0.3 fastapi==0.104.1
uvicorn[standard]==0.24.0
Flask==2.3.3
Flask-CORS==4.0.0
# MongoDB async driver # Database drivers
motor==3.4.0 motor==3.3.2
pymongo==4.6.0
# Web3 interaction # Blockchain & Web3
web3==6.18.0 web3==6.15.1
eth-account==0.10.0
# IPFS client (optional) # IPFS client (optional)
ipfshttpclient==0.8.0a2 ipfshttpclient==0.8.0a2
# Env var support # Environment & configuration
python-dotenv==1.0.1 python-dotenv==1.0.0
# Pydantic for validation # Data validation
pydantic==2.7.1 pydantic==2.5.3
# Auth & password hashing # Authentication & security
python-jose==3.3.0 python-jose==3.3.0
PyJWT==2.8.0
passlib[bcrypt]==1.7.4 passlib[bcrypt]==1.7.4
cryptography==41.0.7
# HTTP requests # HTTP requests
requests==2.32.3 requests==2.31.0