mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 11:25:49 +00:00
backend
This commit is contained in:
+1
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}"
|
||||
Submodule
+1
Submodule backend/lib/openzeppelin-contracts added at 54b3f14346
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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})
|
||||
@@ -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
|
||||
@@ -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})
|
||||
@@ -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}")
|
||||
@@ -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 {}
|
||||
Reference in New Issue
Block a user