mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 11:25:49 +00:00
feat: unify real activity tracking, admin monitoring, and error UX
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import jwt
|
||||
|
||||
|
||||
def _decode_token_unverified(token: str) -> Dict[str, Any]:
|
||||
try:
|
||||
return jwt.decode(
|
||||
token,
|
||||
options={"verify_signature": False},
|
||||
algorithms=["HS256", "RS256"],
|
||||
)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def resolve_user_identity(request, db=None) -> Dict[str, Optional[str]]:
|
||||
"""Best-effort identity resolution from auth header, headers, payload, and optional DB lookup."""
|
||||
token = None
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header.split(" ", 1)[1]
|
||||
|
||||
payload = _decode_token_unverified(token) if token else {}
|
||||
request_json = request.get_json(silent=True) or {}
|
||||
|
||||
user_id = (
|
||||
payload.get("user_id")
|
||||
or payload.get("sub")
|
||||
or payload.get("uid")
|
||||
or request.headers.get("X-User-ID")
|
||||
or request.args.get("user_id")
|
||||
or request_json.get("user_id")
|
||||
)
|
||||
|
||||
wallet_address = (
|
||||
payload.get("wallet_address")
|
||||
or request.headers.get("X-Wallet-Address")
|
||||
or request.args.get("wallet_address")
|
||||
or request_json.get("wallet_address")
|
||||
)
|
||||
|
||||
email = (
|
||||
payload.get("email")
|
||||
or request.headers.get("X-User-Email")
|
||||
or request.args.get("email")
|
||||
or request_json.get("email")
|
||||
)
|
||||
|
||||
# Prefer wallet as canonical identity when available.
|
||||
if wallet_address:
|
||||
wallet_address = str(wallet_address).lower().strip()
|
||||
user_id = wallet_address
|
||||
|
||||
# If only email is known and DB exists, resolve to canonical user id.
|
||||
if not user_id and email and db is not None:
|
||||
user = db.users.find_one({"email": str(email).lower().strip()})
|
||||
if user:
|
||||
if user.get("wallet_address"):
|
||||
user_id = str(user.get("wallet_address")).lower().strip()
|
||||
wallet_address = user_id
|
||||
elif user.get("_id"):
|
||||
user_id = str(user.get("_id"))
|
||||
|
||||
if user_id:
|
||||
user_id = str(user_id).strip()
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"wallet_address": wallet_address,
|
||||
"email": str(email).lower().strip() if email else None,
|
||||
}
|
||||
|
||||
|
||||
def log_user_activity(
|
||||
db,
|
||||
user_id: Optional[str],
|
||||
activity_type: str,
|
||||
title: str,
|
||||
description: str,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
points_earned: int = 0,
|
||||
) -> bool:
|
||||
"""Write a user activity event. Returns True on success, False otherwise."""
|
||||
if not user_id:
|
||||
return False
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
doc = {
|
||||
"user_id": str(user_id),
|
||||
"type": activity_type,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"occurred_at": now_utc,
|
||||
"completed_at": now_utc,
|
||||
"timestamp_utc": now_utc.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
||||
"points_earned": int(points_earned or 0),
|
||||
"metadata": metadata or {},
|
||||
"source": "user_activity_events",
|
||||
}
|
||||
|
||||
try:
|
||||
db.user_activity_events.insert_one(doc)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"contract_address": "0xC2FE2F49B3a1384aEdFAae127F054FAf216eF684",
|
||||
"transaction_hash": "0xfe5a433dae316bd2d60b7190c21866a1fde30777f08d9d37e403ed642433fa28",
|
||||
"contract_address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
||||
"transaction_hash": "973fa79fea65613ef2ccbb35d72ee0cabee2cf3a5bc834a9dc439fef544ace7d",
|
||||
"deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||
"network": "local",
|
||||
"network": "anvil",
|
||||
"abi": [
|
||||
{
|
||||
"type": "constructor",
|
||||
@@ -684,7 +684,6 @@
|
||||
"anonymous": false
|
||||
}
|
||||
],
|
||||
"gas_used": 3387337,
|
||||
"block_number": 22994809,
|
||||
"status": 1
|
||||
"gas_used": 3391283,
|
||||
"block_number": 1
|
||||
}
|
||||
+299
-8
@@ -5,7 +5,7 @@ import uuid
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, jsonify, request
|
||||
from flask import Flask, jsonify, request, make_response, g
|
||||
from flask_cors import CORS
|
||||
from flask_jwt_extended import JWTManager, jwt_required, get_jwt_identity, create_access_token
|
||||
from dotenv import load_dotenv
|
||||
@@ -21,6 +21,14 @@ from Crypto.Cipher import AES
|
||||
from Crypto.Random import get_random_bytes
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
import secrets
|
||||
import re
|
||||
import json
|
||||
import jwt as pyjwt
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except Exception:
|
||||
psutil = None
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
@@ -211,29 +219,312 @@ app.config.update(
|
||||
# ✅ Initialize JWT with your configuration
|
||||
jwt = JWTManager(app)
|
||||
|
||||
# ✅ ENHANCED CORS configuration for professional dashboard
|
||||
# ✅ ENHANCED CORS configuration - Allow all localhost ports for development
|
||||
CORS(app, resources={r"/api/*": {
|
||||
"origins": [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost:3002",
|
||||
"http://localhost:3003",
|
||||
"http://localhost:3004",
|
||||
"http://localhost:3005",
|
||||
"http://localhost:3006",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:3001", # Development
|
||||
"https://openlearnx.vercel.app" # Production (if deployed)
|
||||
"http://127.0.0.1:3001",
|
||||
"http://127.0.0.1:3002",
|
||||
"http://127.0.0.1:3003",
|
||||
"http://127.0.0.1:3004",
|
||||
"http://127.0.0.1:3005",
|
||||
"http://127.0.0.1:3006",
|
||||
"https://openlearnx.vercel.app"
|
||||
],
|
||||
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
|
||||
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"],
|
||||
"allow_headers": [
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-Requested-With",
|
||||
"X-User-ID", # Custom header for user identification
|
||||
"X-User-ID",
|
||||
"X-Session-Token",
|
||||
"X-Firebase-Token" # Firebase authentication
|
||||
"X-Firebase-Token"
|
||||
],
|
||||
"expose_headers": ["Authorization", "X-Total-Count", "X-Rate-Limit", "Content-Type"],
|
||||
"supports_credentials": True,
|
||||
"expose_headers": ["Authorization", "X-Total-Count", "X-Rate-Limit"]
|
||||
"max_age": 3600
|
||||
}})
|
||||
|
||||
# ✅ Handle CORS preflight requests with explicit route
|
||||
@app.before_request
|
||||
def handle_preflight():
|
||||
if request.method == "OPTIONS":
|
||||
response = make_response()
|
||||
response.headers.add("Access-Control-Allow-Origin", request.headers.get("Origin", "*"))
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization,Accept,Origin,X-Requested-With")
|
||||
response.headers.add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD")
|
||||
response.headers.add("Access-Control-Max-Age", "3600")
|
||||
response.headers.add("Access-Control-Allow-Credentials", "true")
|
||||
return response, 200
|
||||
|
||||
|
||||
SUSPICIOUS_PAYLOAD_PATTERNS = [
|
||||
re.compile(r"(<script|javascript:|onerror=|onload=)", re.IGNORECASE),
|
||||
re.compile(r"(\$where|\$regex|\$ne|\$gt|\$lt|\$or)", re.IGNORECASE),
|
||||
re.compile(r"(union\s+select|drop\s+table|insert\s+into|delete\s+from)", re.IGNORECASE),
|
||||
re.compile(r"(\.\./|%2e%2e%2f|/etc/passwd)", re.IGNORECASE)
|
||||
]
|
||||
|
||||
|
||||
def _detect_suspicious_payload(payload_text):
|
||||
if not payload_text:
|
||||
return []
|
||||
matches = []
|
||||
for pattern in SUSPICIOUS_PAYLOAD_PATTERNS:
|
||||
if pattern.search(payload_text):
|
||||
matches.append(pattern.pattern)
|
||||
return matches
|
||||
|
||||
|
||||
def _infer_event_type(path, method, status_code, suspicious=False):
|
||||
if suspicious:
|
||||
return "suspicious_payload"
|
||||
if status_code == 403:
|
||||
return "forbidden_access"
|
||||
if "/api/auth/register" in path:
|
||||
return "signup"
|
||||
if "/api/auth/login" in path or "/api/auth/verify" in path or "/api/auth/wallet-login" in path:
|
||||
return "signin"
|
||||
if "/api/admin" in path:
|
||||
return "admin_panel"
|
||||
if "join" in path or "enroll" in path:
|
||||
return "course_join"
|
||||
if "attend" in path:
|
||||
return "attendance"
|
||||
if method == "GET":
|
||||
return "page_visit"
|
||||
return "api_activity"
|
||||
|
||||
|
||||
def _firewall_rule_matches(rule, ip, method, path):
|
||||
rule_ip = (rule.get("ip") or "").strip()
|
||||
rule_method = (rule.get("method") or "").strip().upper()
|
||||
path_pattern = (rule.get("path_pattern") or "").strip()
|
||||
|
||||
if rule_ip and rule_ip != ip:
|
||||
return False
|
||||
if rule_method and rule_method != method.upper():
|
||||
return False
|
||||
if path_pattern and path_pattern not in path:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@app.before_request
|
||||
def enforce_manual_firewall_rules():
|
||||
"""Apply admin-defined firewall rules only when manually configured."""
|
||||
if request.method == "OPTIONS":
|
||||
return None
|
||||
|
||||
# Keep firewall rule management reachable so admins can recover from bad rules.
|
||||
if request.path.startswith("/api/admin/firewall"):
|
||||
return None
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
if db is None:
|
||||
return None
|
||||
|
||||
ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() if request.headers.get("X-Forwarded-For") else (request.remote_addr or "unknown")
|
||||
method = request.method
|
||||
path = request.path
|
||||
|
||||
rules = list(db.firewall_rules.find({"enabled": True}).sort("created_at", 1))
|
||||
for rule in rules:
|
||||
if not _firewall_rule_matches(rule, ip, method, path):
|
||||
continue
|
||||
|
||||
action = (rule.get("action") or "block").strip().lower()
|
||||
if action == "allow":
|
||||
return None
|
||||
|
||||
# Manual block rule matched.
|
||||
db.security_logs.insert_one({
|
||||
"timestamp": datetime.utcnow(),
|
||||
"event_type": "firewall_block",
|
||||
"action": f"{method} {path}",
|
||||
"status_code": 403,
|
||||
"severity": "warning",
|
||||
"path": path,
|
||||
"method": method,
|
||||
"ip": ip,
|
||||
"user_agent": request.headers.get("User-Agent", ""),
|
||||
"metadata": {
|
||||
"rule_id": str(rule.get("_id")),
|
||||
"rule_name": rule.get("name", ""),
|
||||
"reason": "manual_firewall_rule"
|
||||
}
|
||||
})
|
||||
|
||||
return jsonify({"error": "Blocked by firewall rule"}), 403
|
||||
except Exception as e:
|
||||
logger.debug(f"Firewall check skipped: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@app.before_request
|
||||
def capture_request_start_and_payload():
|
||||
g.request_start_time = time.time()
|
||||
g.suspicious_matches = []
|
||||
if request.method == "OPTIONS":
|
||||
return None
|
||||
|
||||
try:
|
||||
raw_payload = request.get_data(cache=True, as_text=True)[:4000]
|
||||
except Exception:
|
||||
raw_payload = ""
|
||||
|
||||
request_json = None
|
||||
try:
|
||||
request_json = request.get_json(silent=True)
|
||||
except Exception:
|
||||
request_json = None
|
||||
|
||||
request_form = {}
|
||||
try:
|
||||
request_form = {k: request.form.get(k) for k in request.form.keys()}
|
||||
except Exception:
|
||||
request_form = {}
|
||||
|
||||
request_headers = {}
|
||||
try:
|
||||
for key, value in request.headers.items():
|
||||
lower = key.lower()
|
||||
if lower in {"authorization", "cookie", "set-cookie"}:
|
||||
request_headers[key] = "[redacted]"
|
||||
else:
|
||||
request_headers[key] = value
|
||||
except Exception:
|
||||
request_headers = {}
|
||||
|
||||
g.request_payload_preview = raw_payload
|
||||
g.request_json_payload = request_json
|
||||
g.request_form_payload = request_form
|
||||
g.request_headers_snapshot = request_headers
|
||||
|
||||
g.suspicious_matches = _detect_suspicious_payload(raw_payload)
|
||||
|
||||
|
||||
@app.after_request
|
||||
def write_request_audit_log(response):
|
||||
try:
|
||||
db = get_db()
|
||||
if db is None:
|
||||
return response
|
||||
|
||||
start = getattr(g, "request_start_time", None)
|
||||
duration_ms = int((time.time() - start) * 1000) if start else 0
|
||||
|
||||
ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() if request.headers.get("X-Forwarded-For") else (request.remote_addr or "unknown")
|
||||
suspicious_matches = getattr(g, "suspicious_matches", [])
|
||||
suspicious = len(suspicious_matches) > 0
|
||||
request_payload_preview = getattr(g, "request_payload_preview", "")
|
||||
request_json_payload = getattr(g, "request_json_payload", None)
|
||||
request_form_payload = getattr(g, "request_form_payload", {})
|
||||
request_headers_snapshot = getattr(g, "request_headers_snapshot", {})
|
||||
|
||||
auth_user_id = None
|
||||
auth_wallet_address = None
|
||||
auth_email = None
|
||||
try:
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header.split(" ", 1)[1]
|
||||
decoded = pyjwt.decode(
|
||||
token,
|
||||
options={"verify_signature": False},
|
||||
algorithms=["HS256", "RS256"],
|
||||
)
|
||||
auth_user_id = decoded.get("user_id") or decoded.get("sub") or decoded.get("uid")
|
||||
auth_wallet_address = decoded.get("wallet_address")
|
||||
auth_email = decoded.get("email")
|
||||
except Exception:
|
||||
auth_user_id = None
|
||||
|
||||
response_body_preview = ""
|
||||
try:
|
||||
response_body_preview = response.get_data(as_text=True)[:4000]
|
||||
except Exception:
|
||||
response_body_preview = ""
|
||||
|
||||
response_content_type = response.headers.get("Content-Type", "")
|
||||
parsed_response_json = None
|
||||
if response_body_preview and "json" in response_content_type.lower():
|
||||
try:
|
||||
parsed_response_json = json.loads(response_body_preview)
|
||||
except Exception:
|
||||
parsed_response_json = None
|
||||
|
||||
system_usage = {}
|
||||
if psutil is not None:
|
||||
try:
|
||||
vm = psutil.virtual_memory()
|
||||
system_usage = {
|
||||
"cpu_percent": psutil.cpu_percent(interval=None),
|
||||
"memory_percent": vm.percent,
|
||||
"memory_used_mb": round(vm.used / (1024 * 1024), 2),
|
||||
"memory_available_mb": round(vm.available / (1024 * 1024), 2),
|
||||
}
|
||||
except Exception:
|
||||
system_usage = {}
|
||||
|
||||
event_type = _infer_event_type(request.path, request.method, response.status_code, suspicious=suspicious)
|
||||
action = f"{request.method} {request.path}"
|
||||
|
||||
log_doc = {
|
||||
"timestamp": datetime.utcnow(),
|
||||
"event_type": event_type,
|
||||
"action": action,
|
||||
"status_code": int(response.status_code),
|
||||
"severity": "warning" if suspicious or response.status_code >= 400 else "info",
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"query": dict(request.args),
|
||||
"ip": ip,
|
||||
"user_agent": request.headers.get("User-Agent", ""),
|
||||
"origin": request.headers.get("Origin", ""),
|
||||
"duration_ms": duration_ms,
|
||||
"metadata": {
|
||||
"suspicious_matches": suspicious_matches,
|
||||
"content_type": request.headers.get("Content-Type", ""),
|
||||
"request_body": request_payload_preview,
|
||||
"response_body": response_body_preview,
|
||||
"request_details": {
|
||||
"query": dict(request.args),
|
||||
"json": request_json_payload,
|
||||
"form": request_form_payload,
|
||||
"headers": request_headers_snapshot,
|
||||
"content_length": request.content_length,
|
||||
},
|
||||
"response_details": {
|
||||
"content_type": response_content_type,
|
||||
"content_length": response.calculate_content_length(),
|
||||
"json": parsed_response_json,
|
||||
},
|
||||
"usage": system_usage,
|
||||
"duration_ms": duration_ms,
|
||||
"auth_user_id": auth_user_id,
|
||||
"auth_wallet_address": auth_wallet_address,
|
||||
"auth_email": auth_email,
|
||||
}
|
||||
}
|
||||
|
||||
db.security_logs.insert_one(log_doc)
|
||||
except Exception as e:
|
||||
logger.debug(f"Audit log write skipped: {e}")
|
||||
|
||||
return response
|
||||
|
||||
# Enhanced logging with your configuration
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
|
||||
@@ -1 +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}
|
||||
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220f45d6402490a29b9824fef150a4c5d3dced1f2cbcfad209feaeafb4ab56e45d664736f6c63430008210033","sourceMap":"194:9169:10:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220f45d6402490a29b9824fef150a4c5d3dced1f2cbcfad209feaeafb4ab56e45d664736f6c63430008210033","sourceMap":"194:9169:10:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","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":"prague","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/Address.sol":{"keccak256":"0x006dd67219697fe68d7fbfdea512e7c4cb64a43565ed86171d67e844982da6fa","urls":["bzz-raw://2455248c8ddd9cc6a7af76a13973cddf222072427e7b0e2a7d1aff345145e931","dweb:/ipfs/QmfYjnjRbWqYpuxurqveE6HtzsY1Xx323J428AKQgtBJZm"],"license":"MIT"}},"version":1},"id":10}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +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}
|
||||
{"abi":[],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","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":"prague","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/Context.sol":{"keccak256":"0xe2e337e6dde9ef6b680e07338c493ebea1b5fd09b43424112868e9cc1706bca7","urls":["bzz-raw://6df0ddf21ce9f58271bdfaa85cde98b200ef242a05a3f85c2bc10a8294800a92","dweb:/ipfs/QmRK2Y5Yc6BK7tGKkgsgn3aJEQGi5aakeSPZvS65PV8Xp3"],"license":"MIT"}},"version":1},"id":11}
|
||||
@@ -1 +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}
|
||||
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220478c3cfc4853642c2331cfe7c02aa6a34856bcaf4e69011b07e6f8fd812c59a064736f6c63430008210033","sourceMap":"424:971:12:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220478c3cfc4853642c2331cfe7c02aa6a34856bcaf4e69011b07e6f8fd812c59a064736f6c63430008210033","sourceMap":"424:971:12:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","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":"prague","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/Counters.sol":{"keccak256":"0xf0018c2440fbe238dd3a8732fa8e17a0f9dce84d31451dc8a32f6d62b349c9f1","urls":["bzz-raw://59e1c62884d55b70f3ae5432b44bb3166ad71ae3acd19c57ab6ddc3c87c325ee","dweb:/ipfs/QmezuXg5GK5oeA4F91EZhozBFekhq5TD966bHPH18cCqhu"],"license":"MIT"}},"version":1},"id":12}
|
||||
@@ -1 +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}
|
||||
{"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.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"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/","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":"prague","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol":{"keccak256":"0xd10975de010d89fd1c78dc5e8a9a7e7f496198085c151648f20cba166b32582b","urls":["bzz-raw://fb0048dee081f6fffa5f74afc3fb328483c2a30504e94a0ddd2a5114d731ec4d","dweb:/ipfs/QmZptt1nmYoA5SgjwnSgWqgUSDgm4q52Yos3xhnMv3MV43"],"license":"MIT"},"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol":{"keccak256":"0x447a5f3ddc18419d41ff92b3773fb86471b1db25773e07f877f548918a185bf1","urls":["bzz-raw://be161e54f24e5c6fae81a12db1a8ae87bc5ae1b0ddc805d82a1440a68455088f","dweb:/ipfs/QmP7C3CHdY9urF4dEMb9wmsp1wMxHF6nhA2yQE5SKiPAdy"],"license":"MIT"}},"version":1},"id":14}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +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}
|
||||
{"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.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"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/","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":"prague","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
@@ -1 +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}
|
||||
{"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.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"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/","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":"prague","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol":{"keccak256":"0xa82b58eca1ee256be466e536706850163d2ec7821945abd6b4778cfb3bee37da","urls":["bzz-raw://6e75cf83beb757b8855791088546b8337e9d4684e169400c20d44a515353b708","dweb:/ipfs/QmYvPafLfoquiDMEj7CKHtvbgHu7TJNPSVPSCjrtjV8HjV"],"license":"MIT"}},"version":1},"id":7}
|
||||
@@ -1 +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}
|
||||
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220213fb406c5c91a50fee019335c22a97ae8d977051df6576218009afaceea682e64736f6c63430008210033","sourceMap":"202:12582:16:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220213fb406c5c91a50fee019335c22a97ae8d977051df6576218009afaceea682e64736f6c63430008210033","sourceMap":"202:12582:16:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","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":"prague","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
@@ -1 +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}
|
||||
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220976bca310edbc44efb774b7d3260840e2af079bd8d6daf41434a8844ed9cce3a64736f6c63430008210033","sourceMap":"215:1047:17:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea2646970667358221220976bca310edbc44efb774b7d3260840e2af079bd8d6daf41434a8844ed9cce3a64736f6c63430008210033","sourceMap":"215:1047:17:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","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":"prague","libraries":{}},"sources":{"lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol":{"keccak256":"0xf92515413956f529d95977adc9b0567d583c6203fc31ab1c23824c35187e3ddc","urls":["bzz-raw://c50fcc459e49a9858b6d8ad5f911295cb7c9ab57567845a250bf0153f84a95c7","dweb:/ipfs/QmcEW85JRzvDkQggxiBBLVAasXWdkhEysqypj9EaB6H2g6"],"license":"MIT"}},"version":1},"id":17}
|
||||
@@ -1 +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}
|
||||
{"abi":[],"bytecode":{"object":"0x6055604b600b8282823980515f1a607314603f577f4e487b71000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b305f52607381538281f3fe730000000000000000000000000000000000000000301460806040525f5ffdfea26469706673582212202e07962ed64f0cd88180b20fd760b5a33982ce5cb580902c5da0e2d012315f9264736f6c63430008210033","sourceMap":"220:2559:13:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x730000000000000000000000000000000000000000301460806040525f5ffdfea26469706673582212202e07962ed64f0cd88180b20fd760b5a33982ce5cb580902c5da0e2d012315f9264736f6c63430008210033","sourceMap":"220:2559:13:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.33+commit.64118f21\"},\"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\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/openzeppelin-contracts/\",\":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.33+commit.64118f21"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/openzeppelin-contracts/","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":"prague","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}
|
||||
+1
-1
@@ -1 +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"}
|
||||
{"id":"b361ab5df899e494","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"}
|
||||
+941
-2
File diff suppressed because it is too large
Load Diff
+619
-1
@@ -7,6 +7,7 @@ import jwt
|
||||
import logging
|
||||
from eth_account.messages import encode_defunct
|
||||
from web3 import Web3
|
||||
from activity_logger import log_user_activity
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -131,6 +132,8 @@ def verify_signature():
|
||||
# Create new user
|
||||
user = {
|
||||
"wallet_address": wallet_address.lower(),
|
||||
"role": "student",
|
||||
"status": "active",
|
||||
"created_at": datetime.now(),
|
||||
"last_login": datetime.now(),
|
||||
"login_count": 1
|
||||
@@ -138,7 +141,31 @@ def verify_signature():
|
||||
result = db.users.insert_one(user)
|
||||
user["_id"] = str(result.inserted_id)
|
||||
logger.info(f"✅ Created new user: {wallet_address}")
|
||||
log_user_activity(
|
||||
db,
|
||||
wallet_address.lower(),
|
||||
"auth_register",
|
||||
"Account registered",
|
||||
"Created account via wallet authentication",
|
||||
{"auth_method": "wallet"},
|
||||
)
|
||||
else:
|
||||
account_status = str(user.get("status", "active")).lower().strip()
|
||||
if account_status == "banned":
|
||||
logger.warning(f"⛔ Banned wallet login blocked: {wallet_address}")
|
||||
log_user_activity(
|
||||
db,
|
||||
wallet_address.lower(),
|
||||
"account_status",
|
||||
"Login blocked",
|
||||
"Login blocked because account is banned",
|
||||
{"status": "banned"},
|
||||
)
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Your account is banned. Contact admin."
|
||||
}), 403
|
||||
|
||||
# Update existing user
|
||||
db.users.update_one(
|
||||
{"wallet_address": wallet_address.lower()},
|
||||
@@ -149,6 +176,15 @@ def verify_signature():
|
||||
)
|
||||
user["_id"] = str(user["_id"])
|
||||
logger.info(f"✅ Updated existing user: {wallet_address}")
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
wallet_address.lower(),
|
||||
"auth_login",
|
||||
"Login successful",
|
||||
"Wallet login completed successfully",
|
||||
{"auth_method": "wallet"},
|
||||
)
|
||||
|
||||
# Generate JWT token
|
||||
token_payload = {
|
||||
@@ -164,6 +200,12 @@ def verify_signature():
|
||||
user_response = {
|
||||
"id": user["wallet_address"],
|
||||
"wallet_address": user["wallet_address"],
|
||||
"email": user.get("email", ""),
|
||||
"name": user.get("name", ""),
|
||||
"bio": user.get("bio", ""),
|
||||
"avatar": user.get("avatar", ""),
|
||||
"role": user.get("role", "student"),
|
||||
"status": user.get("status", "active"),
|
||||
"created_at": user["created_at"].isoformat() if isinstance(user["created_at"], datetime) else str(user["created_at"]),
|
||||
"last_login": user["last_login"].isoformat() if isinstance(user["last_login"], datetime) else str(user["last_login"])
|
||||
}
|
||||
@@ -182,4 +224,580 @@ def verify_signature():
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
}), 500
|
||||
|
||||
@bp.route('/register', methods=['POST', 'OPTIONS'])
|
||||
def register():
|
||||
"""Register a new user with email and password"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
password = data.get('password', '')
|
||||
username = data.get('username', '').strip()
|
||||
|
||||
if not email or not password:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email and password are required"
|
||||
}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Password must be at least 6 characters"
|
||||
}), 400
|
||||
|
||||
# Check if user already exists
|
||||
existing_user = db.users.find_one({"email": email})
|
||||
if existing_user:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email already registered"
|
||||
}), 409
|
||||
|
||||
# Hash password using simple approach for development
|
||||
# TODO: Use werkzeug.security.generate_password_hash for production
|
||||
import hashlib
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
# Create new user
|
||||
user = {
|
||||
"email": email,
|
||||
"username": username or email.split("@")[0],
|
||||
"password_hash": password_hash,
|
||||
"name": "",
|
||||
"bio": "",
|
||||
"avatar": "",
|
||||
"role": "student",
|
||||
"status": "active",
|
||||
"created_at": datetime.now(),
|
||||
"last_login": datetime.now(),
|
||||
"login_count": 1,
|
||||
"auth_method": "email"
|
||||
}
|
||||
|
||||
result = db.users.insert_one(user)
|
||||
user["_id"] = str(result.inserted_id)
|
||||
|
||||
# Generate JWT token
|
||||
token_payload = {
|
||||
"user_id": str(result.inserted_id),
|
||||
"email": email,
|
||||
"iat": datetime.utcnow(),
|
||||
"exp": datetime.utcnow() + timedelta(days=7)
|
||||
}
|
||||
|
||||
token = jwt.encode(token_payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
user_response = {
|
||||
"id": str(result.inserted_id),
|
||||
"email": email,
|
||||
"username": username or email.split("@")[0],
|
||||
"name": "",
|
||||
"bio": "",
|
||||
"avatar": "",
|
||||
"role": "student",
|
||||
"status": "active",
|
||||
"created_at": user["created_at"].isoformat(),
|
||||
"last_login": user["last_login"].isoformat()
|
||||
}
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
str(result.inserted_id),
|
||||
"auth_register",
|
||||
"Account registered",
|
||||
"Created account with email and password",
|
||||
{"auth_method": "email"},
|
||||
)
|
||||
|
||||
logger.info(f"✅ New user registered: {email}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user": user_response,
|
||||
"message": "Registration successful"
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error during registration: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@bp.route('/login', methods=['POST', 'OPTIONS'])
|
||||
def login():
|
||||
"""Login with email and password"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not email or not password:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email and password are required"
|
||||
}), 400
|
||||
|
||||
# Find user by email
|
||||
user = db.users.find_one({"email": email})
|
||||
if not user:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid email or password"
|
||||
}), 401
|
||||
|
||||
account_status = str(user.get("status", "active")).lower().strip()
|
||||
if account_status == "banned":
|
||||
logger.warning(f"⛔ Banned email login blocked: {email}")
|
||||
log_user_activity(
|
||||
db,
|
||||
str(user.get("_id")),
|
||||
"account_status",
|
||||
"Login blocked",
|
||||
"Login blocked because account is banned",
|
||||
{"status": "banned", "email": email},
|
||||
)
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Your account is banned. Contact admin."
|
||||
}), 403
|
||||
|
||||
if account_status == "suspended":
|
||||
log_user_activity(
|
||||
db,
|
||||
str(user.get("_id")),
|
||||
"account_status",
|
||||
"Login attempted while suspended",
|
||||
"User logged in while account status is suspended",
|
||||
{"status": "suspended", "email": email},
|
||||
)
|
||||
|
||||
# Verify password
|
||||
import hashlib
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
if password_hash != user.get('password_hash'):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid email or password"
|
||||
}), 401
|
||||
|
||||
# Update last login
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]},
|
||||
{
|
||||
"$set": {"last_login": datetime.now()},
|
||||
"$inc": {"login_count": 1}
|
||||
}
|
||||
)
|
||||
|
||||
# Generate JWT token
|
||||
token_payload = {
|
||||
"user_id": str(user["_id"]),
|
||||
"email": email,
|
||||
"iat": datetime.utcnow(),
|
||||
"exp": datetime.utcnow() + timedelta(days=7)
|
||||
}
|
||||
|
||||
token = jwt.encode(token_payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
user_response = {
|
||||
"id": str(user["_id"]),
|
||||
"email": email,
|
||||
"username": user.get('username', ''),
|
||||
"name": user.get('name', ''),
|
||||
"bio": user.get('bio', ''),
|
||||
"avatar": user.get('avatar', ''),
|
||||
"role": user.get('role', 'student'),
|
||||
"status": user.get('status', 'active'),
|
||||
"created_at": user["created_at"].isoformat() if isinstance(user["created_at"], datetime) else str(user["created_at"]),
|
||||
"last_login": user["last_login"].isoformat() if isinstance(user["last_login"], datetime) else str(user["last_login"])
|
||||
}
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
str(user.get("_id")),
|
||||
"auth_login",
|
||||
"Login successful",
|
||||
"Email login completed successfully",
|
||||
{"auth_method": "email", "email": email},
|
||||
)
|
||||
|
||||
logger.info(f"✅ User logged in: {email}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user": user_response,
|
||||
"message": "Login successful"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error during login: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@bp.route('/profile/update', methods=['POST', 'OPTIONS'])
|
||||
def update_profile():
|
||||
"""Update user profile (name, bio, avatar)"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
# Get token from header
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Authorization header required"
|
||||
}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
|
||||
# Verify and decode token
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
user_id = payload.get('user_id')
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid token"
|
||||
}), 401
|
||||
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
bio = data.get('bio', '').strip()
|
||||
avatar = data.get('avatar', '').strip()
|
||||
|
||||
# Update user profile
|
||||
from bson.objectid import ObjectId
|
||||
result = db.users.update_one(
|
||||
{"_id": ObjectId(user_id)},
|
||||
{
|
||||
"$set": {
|
||||
"name": name,
|
||||
"bio": bio,
|
||||
"avatar": avatar,
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if result.matched_count == 0:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "User not found"
|
||||
}), 404
|
||||
|
||||
# Get updated user
|
||||
user = db.users.find_one({"_id": ObjectId(user_id)})
|
||||
|
||||
user_response = {
|
||||
"id": str(user["_id"]),
|
||||
"email": user.get('email', ''),
|
||||
"username": user.get('username', ''),
|
||||
"name": user.get('name', ''),
|
||||
"bio": user.get('bio', ''),
|
||||
"avatar": user.get('avatar', ''),
|
||||
"role": user.get('role', 'student'),
|
||||
"status": user.get('status', 'active'),
|
||||
"created_at": user["created_at"].isoformat() if isinstance(user["created_at"], datetime) else str(user["created_at"]),
|
||||
"last_login": user["last_login"].isoformat() if isinstance(user["last_login"], datetime) else str(user["last_login"])
|
||||
}
|
||||
|
||||
logger.info(f"✅ Profile updated for user: {user_id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"user": user_response,
|
||||
"message": "Profile updated successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error updating profile: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
@bp.route('/metamask/add-email', methods=['POST', 'OPTIONS'])
|
||||
def add_metamask_email():
|
||||
"""Store contact email for MetaMask wallet"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
# Get token from header
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Authorization header required"
|
||||
}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
|
||||
# Verify and decode token
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
wallet_address = payload.get('wallet_address')
|
||||
if not wallet_address:
|
||||
wallet_address = payload.get('user_id')
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid token"
|
||||
}), 401
|
||||
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
if not email:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email is required"
|
||||
}), 400
|
||||
|
||||
# Validate email format
|
||||
import re
|
||||
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid email format"
|
||||
}), 400
|
||||
|
||||
# Check if email already used by different wallet
|
||||
existing_user = db.users.find_one({"email": email, "wallet_address": {"$ne": wallet_address.lower()}})
|
||||
if existing_user:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Email already associated with another wallet"
|
||||
}), 409
|
||||
|
||||
# Update user with email and name
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
# Try updating by wallet address first (for new users)
|
||||
result = db.users.update_one(
|
||||
{"wallet_address": wallet_address.lower()},
|
||||
{
|
||||
"$set": {
|
||||
"email": email,
|
||||
"name": name or "",
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if result.matched_count == 0:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "User not found"
|
||||
}), 404
|
||||
|
||||
# Get updated user
|
||||
user = db.users.find_one({"wallet_address": wallet_address.lower()})
|
||||
|
||||
user_response = {
|
||||
"id": str(user.get("_id", wallet_address)),
|
||||
"wallet_address": user.get("wallet_address", wallet_address),
|
||||
"email": user.get("email", ""),
|
||||
"name": user.get("name", ""),
|
||||
"bio": user.get("bio", ""),
|
||||
"avatar": user.get("avatar", ""),
|
||||
"role": user.get("role", "student"),
|
||||
"status": user.get("status", "active"),
|
||||
"created_at": user.get("created_at", datetime.now()).isoformat() if isinstance(user.get("created_at"), datetime) else str(user.get("created_at", datetime.now())),
|
||||
"last_login": user.get("last_login", datetime.now()).isoformat() if isinstance(user.get("last_login"), datetime) else str(user.get("last_login", datetime.now()))
|
||||
}
|
||||
|
||||
logger.info(f"✅ Email added for MetaMask wallet: {wallet_address}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"user": user_response,
|
||||
"message": "Email saved successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error saving MetaMask email: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route('/verify-token', methods=['POST', 'OPTIONS'])
|
||||
def verify_token():
|
||||
"""Validate JWT token and return the latest user payload."""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({"valid": False, "error": "Authorization header required"}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({"valid": False, "error": "Invalid token"}), 401
|
||||
|
||||
user = None
|
||||
wallet_address = payload.get('wallet_address')
|
||||
email = payload.get('email')
|
||||
user_id = payload.get('user_id')
|
||||
|
||||
if wallet_address:
|
||||
user = db.users.find_one({"wallet_address": str(wallet_address).lower()})
|
||||
elif email:
|
||||
user = db.users.find_one({"email": str(email).lower()})
|
||||
elif user_id:
|
||||
try:
|
||||
from bson.objectid import ObjectId
|
||||
user = db.users.find_one({"_id": ObjectId(user_id)})
|
||||
except Exception:
|
||||
user = None
|
||||
|
||||
if not user:
|
||||
return jsonify({"valid": False, "error": "User not found"}), 404
|
||||
|
||||
status = str(user.get("status", "active")).lower().strip()
|
||||
if status == "banned":
|
||||
return jsonify({"valid": False, "error": "Account is banned"}), 403
|
||||
|
||||
user_response = {
|
||||
"id": str(user.get("_id", user.get("wallet_address", ""))),
|
||||
"wallet_address": user.get("wallet_address", ""),
|
||||
"email": user.get("email", ""),
|
||||
"username": user.get("username", ""),
|
||||
"name": user.get("name", ""),
|
||||
"bio": user.get("bio", ""),
|
||||
"avatar": user.get("avatar", ""),
|
||||
"role": user.get("role", "student"),
|
||||
"status": user.get("status", "active"),
|
||||
"created_at": user.get("created_at", datetime.now()).isoformat() if isinstance(user.get("created_at"), datetime) else str(user.get("created_at", datetime.now())),
|
||||
"last_login": user.get("last_login", datetime.now()).isoformat() if isinstance(user.get("last_login"), datetime) else str(user.get("last_login", datetime.now())),
|
||||
}
|
||||
|
||||
return jsonify({"valid": True, "user": user_response})
|
||||
except Exception as e:
|
||||
logger.error(f"❌ verify-token error: {str(e)}")
|
||||
return jsonify({"valid": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET', 'OPTIONS'])
|
||||
def get_me():
|
||||
"""Return authenticated user profile for current token."""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
verify_resp = verify_token()
|
||||
try:
|
||||
body, status = verify_resp
|
||||
if status != 200:
|
||||
return body, status
|
||||
data = body.get_json()
|
||||
return jsonify({"success": True, "user": data.get("user", {})})
|
||||
except Exception:
|
||||
return verify_resp
|
||||
|
||||
@bp.route('/upload-image', methods=['POST', 'OPTIONS'])
|
||||
def upload_image():
|
||||
"""Upload and convert image (PNG/JPG only) to base64"""
|
||||
if request.method == "OPTIONS":
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
try:
|
||||
# Get token from header
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Authorization header required"
|
||||
}), 401
|
||||
|
||||
token = auth_header.split('Bearer ')[1]
|
||||
|
||||
# Verify and decode token
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
user_id = payload.get('user_id')
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid token"
|
||||
}), 401
|
||||
|
||||
# Check if file is in request
|
||||
if 'file' not in request.files:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "No file provided"
|
||||
}), 400
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
if file.filename == '':
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "No file selected"
|
||||
}), 400
|
||||
|
||||
# Validate file type - only PNG and JPG
|
||||
allowed_extensions = {'png', 'jpg', 'jpeg'}
|
||||
file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
|
||||
|
||||
if file_ext not in allowed_extensions:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Only PNG and JPG formats are allowed"
|
||||
}), 400
|
||||
|
||||
# Validate file size (max 5MB)
|
||||
file.seek(0, 2) # Seek to end
|
||||
file_size = file.tell()
|
||||
file.seek(0) # Seek back to start
|
||||
|
||||
max_size = 5 * 1024 * 1024 # 5MB
|
||||
if file_size > max_size:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "File size must be less than 5MB"
|
||||
}), 400
|
||||
|
||||
# Read file and convert to base64
|
||||
import base64
|
||||
file_data = file.read()
|
||||
base64_image = base64.b64encode(file_data).decode('utf-8')
|
||||
|
||||
# Create data URL for the image
|
||||
mime_type = f"image/{file_ext if file_ext != 'jpg' else 'jpeg'}"
|
||||
data_url = f"data:{mime_type};base64,{base64_image}"
|
||||
|
||||
logger.info(f"✅ Image uploaded for user: {user_id}, size: {file_size} bytes")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"image": data_url,
|
||||
"size": file_size,
|
||||
"message": "Image uploaded successfully"
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error uploading image: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@@ -8,9 +8,16 @@ import uuid
|
||||
from datetime import datetime
|
||||
import docker
|
||||
import psutil
|
||||
from pymongo import MongoClient
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('coding', __name__)
|
||||
|
||||
# MongoDB connection
|
||||
mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
|
||||
client = MongoClient(mongo_uri)
|
||||
db = client.openlearnx
|
||||
|
||||
def secure_execution_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
@@ -34,6 +41,20 @@ def start_coding_session():
|
||||
session['start_time'] = datetime.now().isoformat()
|
||||
session['course_id'] = course_id
|
||||
session['lesson_id'] = lesson_id
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
log_user_activity(
|
||||
db,
|
||||
identity.get("user_id"),
|
||||
"coding",
|
||||
"Started coding session",
|
||||
"Entered secure coding session",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"course_id": course_id,
|
||||
"lesson_id": lesson_id,
|
||||
},
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
@@ -92,6 +113,36 @@ def submit_coding_test():
|
||||
code,
|
||||
test_result
|
||||
)
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id")
|
||||
if resolved_user_id:
|
||||
db.user_submissions.insert_one({
|
||||
"user_id": resolved_user_id,
|
||||
"session_id": session.get('coding_session_id'),
|
||||
"course_id": session.get('course_id'),
|
||||
"problem_id": problem_id,
|
||||
"score": test_result.get('score', 0),
|
||||
"points_earned": int(test_result.get('score', 0)),
|
||||
"submitted_at": datetime.now(),
|
||||
"status": "submitted",
|
||||
})
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"coding",
|
||||
"Submitted coding solution",
|
||||
f"Submitted coding test for problem '{problem_id}'",
|
||||
{
|
||||
"submission_id": submission_id,
|
||||
"problem_id": problem_id,
|
||||
"score": test_result.get('score', 0),
|
||||
"passed": test_result.get('passed', 0),
|
||||
"total": test_result.get('total', 0),
|
||||
},
|
||||
points_earned=int(test_result.get('score', 0)),
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
@@ -184,11 +235,6 @@ def get_run_command(language, filename):
|
||||
|
||||
def log_coding_attempt(session_id, code, language):
|
||||
"""Log all coding attempts for monitoring"""
|
||||
from pymongo import MongoClient
|
||||
|
||||
client = MongoClient(os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'))
|
||||
db = client.openlearnx
|
||||
|
||||
db.coding_logs.insert_one({
|
||||
"session_id": session_id,
|
||||
"code": code,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from flask import Blueprint, jsonify, current_app
|
||||
from flask import Blueprint, jsonify, current_app, request
|
||||
from pymongo import MongoClient
|
||||
import os
|
||||
from datetime import datetime
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('courses', __name__)
|
||||
|
||||
@@ -68,6 +70,38 @@ def get_lesson(course_id, lesson_id):
|
||||
def mark_lesson_complete(course_id, lesson_id):
|
||||
"""Mark a lesson as completed for the user"""
|
||||
try:
|
||||
identity = resolve_user_identity(request, db)
|
||||
user_id = identity.get("user_id")
|
||||
|
||||
if user_id:
|
||||
course = db.courses.find_one({"id": course_id}, {"title": 1}) or {}
|
||||
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"title": 1}) or {}
|
||||
|
||||
db.user_courses.update_one(
|
||||
{"user_id": user_id, "course_id": course_id},
|
||||
{
|
||||
"$set": {
|
||||
"user_id": user_id,
|
||||
"course_id": course_id,
|
||||
"last_activity_at": datetime.utcnow(),
|
||||
"completed_at": datetime.utcnow(),
|
||||
"completed": True,
|
||||
},
|
||||
"$addToSet": {"lessons_completed": lesson_id},
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
user_id,
|
||||
"course",
|
||||
"Lesson completed",
|
||||
f"Completed lesson '{lesson.get('title', lesson_id)}' in course '{course.get('title', course_id)}'",
|
||||
{"course_id": course_id, "lesson_id": lesson_id},
|
||||
points_earned=10,
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Lesson {lesson_id} marked as complete",
|
||||
@@ -76,6 +110,66 @@ def mark_lesson_complete(course_id, lesson_id):
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route("/<course_id>/activity", methods=["POST"])
|
||||
def log_course_activity(course_id):
|
||||
"""Log course interactions like view/start for real dashboard activity."""
|
||||
try:
|
||||
identity = resolve_user_identity(request, db)
|
||||
user_id = identity.get("user_id")
|
||||
if not user_id:
|
||||
return jsonify({"success": False, "error": "Authentication required"}), 401
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
action = str(data.get("action") or "view").strip().lower()
|
||||
lesson_id = str(data.get("lesson_id") or "").strip()
|
||||
|
||||
course = db.courses.find_one({"id": course_id}, {"title": 1}) or {}
|
||||
lesson_title = lesson_id
|
||||
if lesson_id:
|
||||
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"title": 1}) or {}
|
||||
lesson_title = lesson.get("title", lesson_id)
|
||||
|
||||
if action == "start":
|
||||
title = "Course started"
|
||||
description = f"Started course '{course.get('title', course_id)}'"
|
||||
elif action == "lesson_view":
|
||||
title = "Lesson viewed"
|
||||
description = f"Viewed lesson '{lesson_title}' in course '{course.get('title', course_id)}'"
|
||||
else:
|
||||
title = "Course viewed"
|
||||
description = f"Opened course '{course.get('title', course_id)}'"
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
user_id,
|
||||
"course",
|
||||
title,
|
||||
description,
|
||||
{"course_id": course_id, "lesson_id": lesson_id, "action": action},
|
||||
)
|
||||
|
||||
db.user_courses.update_one(
|
||||
{"user_id": user_id, "course_id": course_id},
|
||||
{
|
||||
"$set": {
|
||||
"user_id": user_id,
|
||||
"course_id": course_id,
|
||||
"last_activity_at": datetime.utcnow(),
|
||||
},
|
||||
"$setOnInsert": {
|
||||
"started_at": datetime.utcnow(),
|
||||
"completed": False,
|
||||
"lessons_completed": [],
|
||||
},
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
return jsonify({"success": True})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@bp.route("/<course_id>/progress", methods=["GET"])
|
||||
def get_course_progress(course_id):
|
||||
"""Get user's progress in a specific course"""
|
||||
|
||||
+160
-4
@@ -1,5 +1,5 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pymongo import MongoClient
|
||||
import os
|
||||
from bson import ObjectId
|
||||
@@ -188,8 +188,11 @@ def get_comprehensive_stats():
|
||||
"courses_completed": courses_completed,
|
||||
"coding_problems_solved": coding_problems_solved,
|
||||
"quiz_accuracy": quiz_accuracy,
|
||||
"coding_streak": coding_streak,
|
||||
"longest_streak": max(longest_streak, coding_streak),
|
||||
"streak_data": {
|
||||
"current_streak": coding_streak,
|
||||
"best_streak": max(longest_streak, coding_streak),
|
||||
"last_active_date": datetime.now().isoformat()
|
||||
},
|
||||
"total_courses": len(courses),
|
||||
"total_quizzes": len(quizzes),
|
||||
"global_rank": calculate_real_global_rank(user_stats, user_id) if user_stats else 0,
|
||||
@@ -270,9 +273,143 @@ def get_recent_activity():
|
||||
}), 401
|
||||
|
||||
logger.info(f"📋 Fetching REAL activity for wallet: {user_id}")
|
||||
|
||||
identity_candidates = {str(user_id)}
|
||||
if wallet_address:
|
||||
identity_candidates.add(str(wallet_address).lower())
|
||||
|
||||
# Resolve user identity aliases to avoid missing activity across auth methods.
|
||||
user_doc = None
|
||||
try:
|
||||
maybe_oid = ObjectId(str(user_id))
|
||||
user_doc = db.users.find_one({"_id": maybe_oid})
|
||||
except Exception:
|
||||
user_doc = db.users.find_one({"wallet_address": str(user_id).lower()}) or db.users.find_one({"email": str(user_id).lower()})
|
||||
|
||||
if user_doc:
|
||||
if user_doc.get("_id"):
|
||||
identity_candidates.add(str(user_doc.get("_id")))
|
||||
if user_doc.get("wallet_address"):
|
||||
identity_candidates.add(str(user_doc.get("wallet_address")).lower())
|
||||
if user_doc.get("email"):
|
||||
identity_candidates.add(str(user_doc.get("email")).lower())
|
||||
|
||||
logger.info(f"📋 Recent activity identity candidates: {sorted(identity_candidates)}")
|
||||
|
||||
activities = []
|
||||
|
||||
def parse_datetime(value):
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
candidate = value.replace("Z", "+00:00")
|
||||
try:
|
||||
return datetime.fromisoformat(candidate)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def to_utc_display(value):
|
||||
dt = parse_datetime(value) or datetime.now(timezone.utc)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
dt = dt.astimezone(timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Primary source: explicit user activity event log.
|
||||
event_docs = list(
|
||||
db.user_activity_events.find({"user_id": {"$in": list(identity_candidates)}}).sort("occurred_at", -1).limit(150)
|
||||
)
|
||||
for item in event_docs:
|
||||
occurred_at = item.get("occurred_at") or item.get("completed_at") or datetime.now(timezone.utc)
|
||||
activities.append({
|
||||
"id": str(item.get("_id", uuid.uuid4())),
|
||||
"type": item.get("type", "activity"),
|
||||
"title": item.get("title", "User Activity"),
|
||||
"description": item.get("description", "Activity recorded"),
|
||||
"completed_at": parse_datetime(occurred_at).isoformat() if parse_datetime(occurred_at) else datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp_utc": item.get("timestamp_utc") or to_utc_display(occurred_at),
|
||||
"points_earned": int(item.get("points_earned", 0) or 0),
|
||||
"success_rate": item.get("success_rate", 0),
|
||||
"difficulty": item.get("difficulty", ""),
|
||||
"blockchain_verified": item.get("blockchain_verified", False)
|
||||
})
|
||||
|
||||
# Include admin/account status events from security logs as real activity fallback.
|
||||
admin_status_logs = list(
|
||||
db.security_logs.find({
|
||||
"event_type": "admin_user_status",
|
||||
"metadata.user_id": {"$in": list(identity_candidates)}
|
||||
}).sort("timestamp", -1).limit(50)
|
||||
)
|
||||
for item in admin_status_logs:
|
||||
occurred_at = item.get("timestamp", datetime.now(timezone.utc))
|
||||
metadata = item.get("metadata") or {}
|
||||
new_status = metadata.get("status") or metadata.get("new_status") or "updated"
|
||||
activities.append({
|
||||
"id": str(item.get("_id", uuid.uuid4())),
|
||||
"type": "account_status",
|
||||
"title": f"Account status changed to {new_status}",
|
||||
"description": f"Admin changed your account status to {new_status}",
|
||||
"completed_at": parse_datetime(occurred_at).isoformat() if parse_datetime(occurred_at) else datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp_utc": to_utc_display(occurred_at),
|
||||
"points_earned": 0,
|
||||
"success_rate": 0,
|
||||
"difficulty": "",
|
||||
"blockchain_verified": False,
|
||||
})
|
||||
|
||||
# Fallback source: authenticated request audit logs for this user.
|
||||
audit_logs = list(
|
||||
db.security_logs.find({
|
||||
"$or": [
|
||||
{"metadata.auth_user_id": {"$in": list(identity_candidates)}},
|
||||
{"metadata.auth_wallet_address": {"$in": list(identity_candidates)}},
|
||||
{"metadata.auth_email": {"$in": list(identity_candidates)}},
|
||||
]
|
||||
}).sort("timestamp", -1).limit(150)
|
||||
)
|
||||
for item in audit_logs:
|
||||
path = str(item.get("path") or "")
|
||||
method = str(item.get("method") or "")
|
||||
ts = item.get("timestamp", datetime.now(timezone.utc))
|
||||
if not any(segment in path for segment in ["/api/quizzes", "/api/exam", "/api/coding", "/api/courses", "/api/auth"]):
|
||||
continue
|
||||
|
||||
log_type = "activity"
|
||||
title = f"{method} {path}"
|
||||
description = f"API activity on {path}"
|
||||
if "/api/quizzes" in path:
|
||||
log_type = "quiz"
|
||||
title = "Quiz activity"
|
||||
description = f"{method} {path}"
|
||||
elif "/api/exam" in path or "/api/coding" in path:
|
||||
log_type = "coding"
|
||||
title = "Coding activity"
|
||||
description = f"{method} {path}"
|
||||
elif "/api/courses" in path:
|
||||
log_type = "course"
|
||||
title = "Course activity"
|
||||
description = f"{method} {path}"
|
||||
elif "/api/auth" in path:
|
||||
log_type = "auth_login"
|
||||
title = "Authentication activity"
|
||||
description = f"{method} {path}"
|
||||
|
||||
activities.append({
|
||||
"id": str(item.get("_id", uuid.uuid4())),
|
||||
"type": log_type,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"completed_at": parse_datetime(ts).isoformat() if parse_datetime(ts) else datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp_utc": to_utc_display(ts),
|
||||
"points_earned": 0,
|
||||
"success_rate": 0,
|
||||
"difficulty": "",
|
||||
"blockchain_verified": False,
|
||||
})
|
||||
|
||||
# ✅ ONLY REAL ACTIVITY SOURCES
|
||||
activity_sources = [
|
||||
(db.user_courses, "course", "Course Activity", "completed_at"),
|
||||
@@ -285,7 +422,7 @@ def get_recent_activity():
|
||||
try:
|
||||
# Get ONLY real MongoDB data
|
||||
recent_items = list(collection.find(
|
||||
{"user_id": user_id}
|
||||
{"user_id": {"$in": list(identity_candidates)}}
|
||||
).sort(date_field, -1).limit(20))
|
||||
|
||||
for item in recent_items:
|
||||
@@ -305,6 +442,7 @@ def get_recent_activity():
|
||||
"title": item.get('title', item.get('name', default_title)),
|
||||
"description": format_real_activity_description(item, activity_type),
|
||||
"completed_at": completed_at.isoformat(),
|
||||
"timestamp_utc": to_utc_display(completed_at),
|
||||
"points_earned": item.get('points', item.get('points_earned', 0)),
|
||||
"success_rate": item.get('score', item.get('completion_percentage', 0)),
|
||||
"difficulty": item.get('difficulty', ''),
|
||||
@@ -314,8 +452,26 @@ def get_recent_activity():
|
||||
logger.warning(f"⚠️ Failed to fetch {activity_type} activities: {e}")
|
||||
continue
|
||||
|
||||
# Exclude known placeholder/demo activity content from older seeded data.
|
||||
fake_markers = {
|
||||
"completed react fundamentals",
|
||||
"scored 95% on javascript quiz",
|
||||
"7-day learning streak achieved",
|
||||
"moved up 5 positions in leaderboard",
|
||||
}
|
||||
|
||||
filtered_activities = []
|
||||
for entry in activities:
|
||||
entry_text = f"{entry.get('title', '')} {entry.get('description', '')}".strip().lower()
|
||||
if any(marker in entry_text for marker in fake_markers):
|
||||
continue
|
||||
filtered_activities.append(entry)
|
||||
|
||||
activities = filtered_activities
|
||||
|
||||
# Sort by completion date
|
||||
activities.sort(key=lambda x: x['completed_at'], reverse=True)
|
||||
activities = activities[:100]
|
||||
|
||||
logger.info(f"✅ Found {len(activities)} REAL activities for wallet {user_id}")
|
||||
return jsonify({
|
||||
|
||||
@@ -5,6 +5,7 @@ import string
|
||||
from datetime import datetime, timedelta
|
||||
from pymongo import MongoClient
|
||||
import os
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('exam', __name__)
|
||||
|
||||
@@ -255,6 +256,21 @@ def join_exam():
|
||||
session['session_id'] = participant['session_id']
|
||||
|
||||
print(f"✅ Participant {student_name} joined exam {exam_code}")
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
log_user_activity(
|
||||
db,
|
||||
identity.get("user_id"),
|
||||
"exam",
|
||||
"Joined coding exam",
|
||||
f"Joined exam '{exam.get('title', exam_code)}' as {student_name}",
|
||||
{
|
||||
"exam_code": exam_code,
|
||||
"exam_title": exam.get("title"),
|
||||
"student_name": student_name,
|
||||
"session_id": participant.get("session_id"),
|
||||
},
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
from activity_logger import log_user_activity, resolve_user_identity
|
||||
|
||||
bp = Blueprint('quizzes', __name__)
|
||||
|
||||
@@ -233,6 +234,24 @@ def join_room():
|
||||
|
||||
print(f"✅ User joined room: {username} -> {room_code}")
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id") or data.get("user_id") or data.get("wallet_address")
|
||||
if isinstance(resolved_user_id, str):
|
||||
resolved_user_id = resolved_user_id.strip().lower() if resolved_user_id.startswith("0x") else resolved_user_id.strip()
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"quiz",
|
||||
"Joined quiz room",
|
||||
f"Joined quiz room '{room.get('title', room_code)}' as {username}",
|
||||
{
|
||||
"room_code": room_code,
|
||||
"room_title": room.get("title"),
|
||||
"username": username,
|
||||
"session_id": participant_session.get("session_id"),
|
||||
},
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Successfully joined quiz room '{room.get('title')}'",
|
||||
@@ -423,6 +442,50 @@ def submit_answer(session_id):
|
||||
"updated_at": datetime.now()
|
||||
}}
|
||||
)
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id") or data.get("user_id") or data.get("wallet_address")
|
||||
if isinstance(resolved_user_id, str):
|
||||
resolved_user_id = resolved_user_id.strip().lower() if resolved_user_id.startswith("0x") else resolved_user_id.strip()
|
||||
if resolved_user_id:
|
||||
db.user_quizzes.update_one(
|
||||
{
|
||||
"user_id": resolved_user_id,
|
||||
"session_id": session_id,
|
||||
"question_id": question_data.get("question_id"),
|
||||
},
|
||||
{
|
||||
"$set": {
|
||||
"user_id": resolved_user_id,
|
||||
"session_id": session_id,
|
||||
"room_code": room.get("room_code"),
|
||||
"room_title": room.get("title"),
|
||||
"question_id": question_data.get("question_id"),
|
||||
"score": participant.get("score", 0),
|
||||
"completed_at": datetime.now(),
|
||||
"is_correct": is_correct,
|
||||
"difficulty": current_difficulty,
|
||||
"username": participant.get("username"),
|
||||
}
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"quiz",
|
||||
"Answered quiz question",
|
||||
f"Answered a {current_difficulty} question in '{room.get('title', 'Quiz Room')}'",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"room_code": room.get("room_code"),
|
||||
"room_title": room.get("title"),
|
||||
"is_correct": is_correct,
|
||||
"difficulty": current_difficulty,
|
||||
},
|
||||
points_earned=question_data.get('points', 10) if is_correct else 0,
|
||||
)
|
||||
|
||||
# Get AI prediction for comparison (if available)
|
||||
ai_feedback = None
|
||||
@@ -479,6 +542,68 @@ def submit_answer(session_id):
|
||||
# ✅ AI QUESTION GENERATION - IMPROVED VERSION
|
||||
# ===================================================================
|
||||
|
||||
@bp.route('/generate-ai', methods=['POST', 'OPTIONS'])
|
||||
def generate_ai_quiz():
|
||||
"""Generate a traditional quiz directly using AI"""
|
||||
if request.method == "OPTIONS":
|
||||
response = jsonify({'status': 'ok'})
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
||||
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
|
||||
return response
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
topic = data.get('topic', 'General')
|
||||
difficulty = data.get('difficulty', 'medium')
|
||||
num_questions = int(data.get('num_questions', 5))
|
||||
|
||||
print(f"🤖 AI Quiz Generation: topic={topic}, difficulty={difficulty}, questions={num_questions}")
|
||||
|
||||
ai_service = get_ai_service()
|
||||
if not ai_service:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "AI service not available"
|
||||
}), 503
|
||||
|
||||
# Generate questions using AI service
|
||||
generated_data = ai_service.generate_quiz(
|
||||
topic=topic,
|
||||
difficulty=difficulty,
|
||||
num_questions=num_questions
|
||||
)
|
||||
|
||||
# Save to database
|
||||
db = get_db()
|
||||
quiz_result = db.quizzes.insert_one({
|
||||
"id": str(uuid.uuid4()),
|
||||
"title": generated_data.get('title', f"AI Quiz - {topic}"),
|
||||
"description": generated_data.get('description', ''),
|
||||
"difficulty": difficulty,
|
||||
"questions": generated_data.get('questions', []),
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"total_points": len(generated_data.get('questions', [])) * 10,
|
||||
"generated_by": "AI",
|
||||
"topic": topic
|
||||
})
|
||||
|
||||
# Get the saved quiz
|
||||
saved_quiz = db.quizzes.find_one({"_id": quiz_result.inserted_id})
|
||||
saved_quiz['_id'] = str(saved_quiz['_id'])
|
||||
|
||||
print(f"✅ Quiz generated with {len(saved_quiz.get('questions', []))} questions")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"quiz": saved_quiz,
|
||||
"message": f"Generated {len(saved_quiz.get('questions', []))} AI questions on topic: {topic}"
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ AI generation error: {str(e)}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@bp.route('/room/<room_code>/generate-ai-questions', methods=['POST', 'OPTIONS'])
|
||||
def generate_ai_questions(room_code):
|
||||
"""Generate AI questions for the quiz room - IMPROVED VERSION"""
|
||||
@@ -1038,5 +1163,104 @@ def get_quiz_by_id(quiz_id):
|
||||
"quiz": quiz
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/<quiz_id>/submit', methods=['POST', 'OPTIONS'])
|
||||
def submit_traditional_quiz(quiz_id):
|
||||
"""Submit traditional quiz answers, store result, and log user activity."""
|
||||
if request.method == "OPTIONS":
|
||||
response = jsonify({'status': 'ok'})
|
||||
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
|
||||
response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS")
|
||||
return response
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
data = request.get_json() or {}
|
||||
answers = data.get('answers') or {}
|
||||
participant_name = (data.get('participant_name') or 'User').strip()
|
||||
|
||||
quiz = db.quizzes.find_one({"id": quiz_id})
|
||||
if not quiz:
|
||||
return jsonify({"success": False, "error": "Quiz not found"}), 404
|
||||
|
||||
questions = quiz.get('questions', [])
|
||||
total_questions = len(questions)
|
||||
correct_answers = 0
|
||||
total_points = 0
|
||||
ai_feedback = []
|
||||
|
||||
for idx, question in enumerate(questions):
|
||||
question_id = question.get('id') or question.get('question_id') or f"q_{idx}"
|
||||
expected = str(question.get('correct_answer', '')).strip().lower()
|
||||
user_answer = str(answers.get(question_id, '')).strip()
|
||||
is_correct = user_answer.lower() == expected if expected else False
|
||||
points = int(question.get('points', 10) or 10)
|
||||
|
||||
if is_correct:
|
||||
correct_answers += 1
|
||||
total_points += points
|
||||
|
||||
ai_feedback.append({
|
||||
"question": question.get('question_text', question.get('question', f"Question {idx + 1}")),
|
||||
"user_answer": user_answer,
|
||||
"is_correct": is_correct,
|
||||
"correct_answer": question.get('correct_answer', ''),
|
||||
"ai_feedback": {
|
||||
"feedback": "Correct" if is_correct else "Review this concept and try again"
|
||||
}
|
||||
})
|
||||
|
||||
score = round((correct_answers / total_questions) * 100, 1) if total_questions > 0 else 0
|
||||
|
||||
identity = resolve_user_identity(request, db)
|
||||
resolved_user_id = identity.get("user_id") or data.get("user_id") or data.get("wallet_address")
|
||||
if isinstance(resolved_user_id, str):
|
||||
resolved_user_id = resolved_user_id.strip().lower() if resolved_user_id.startswith("0x") else resolved_user_id.strip()
|
||||
|
||||
if resolved_user_id:
|
||||
db.user_quizzes.insert_one({
|
||||
"user_id": resolved_user_id,
|
||||
"quiz_id": quiz_id,
|
||||
"title": quiz.get('title', 'Quiz Submission'),
|
||||
"topic": quiz.get('topic', 'General'),
|
||||
"participant_name": participant_name,
|
||||
"score": score,
|
||||
"correct_answers": correct_answers,
|
||||
"total_questions": total_questions,
|
||||
"points": total_points,
|
||||
"completed_at": datetime.now(),
|
||||
"answers": answers,
|
||||
})
|
||||
|
||||
log_user_activity(
|
||||
db,
|
||||
resolved_user_id,
|
||||
"quiz",
|
||||
"Completed quiz",
|
||||
f"Completed quiz '{quiz.get('title', quiz_id)}' with score {score}%",
|
||||
{
|
||||
"quiz_id": quiz_id,
|
||||
"quiz_title": quiz.get('title', 'Quiz'),
|
||||
"score": score,
|
||||
"correct_answers": correct_answers,
|
||||
"total_questions": total_questions,
|
||||
},
|
||||
points_earned=total_points,
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"results": {
|
||||
"score": score,
|
||||
"correct_answers": correct_answers,
|
||||
"total_questions": total_questions,
|
||||
"total_points": total_points,
|
||||
"ai_feedback": ai_feedback,
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
@@ -61,7 +61,7 @@ def deploy_contract():
|
||||
|
||||
# Sign and send transaction
|
||||
signed_txn = w3.eth.account.sign_transaction(transaction, private_key)
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)
|
||||
|
||||
print(f"Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple deployment script for OpenLearnX smart contracts using Anvil
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from web3 import Web3
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(BASE_DIR / ".env")
|
||||
|
||||
def deploy_contract():
|
||||
provider_url = os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545')
|
||||
|
||||
# Connect to Web3
|
||||
w3 = Web3(Web3.HTTPProvider(provider_url))
|
||||
|
||||
if not w3.is_connected():
|
||||
raise Exception(f"Failed to connect to {provider_url}")
|
||||
|
||||
print(f"✓ Connected to {provider_url}")
|
||||
print(f"Chain ID: {w3.eth.chain_id}")
|
||||
|
||||
# Use Anvil's first account (well-known test account)
|
||||
# Get all accounts
|
||||
accounts = w3.eth.accounts
|
||||
if not accounts:
|
||||
raise Exception("No accounts available in Anvil")
|
||||
|
||||
deployer = accounts[0]
|
||||
balance = w3.eth.get_balance(deployer)
|
||||
print(f"✓ Deployer account: {deployer}")
|
||||
print(f"✓ Balance: {w3.from_wei(balance, 'ether')} ETH")
|
||||
|
||||
# Load contract
|
||||
contract_path = BASE_DIR / "out" / "CertificateNFT.sol" / "CertificateNFT.json"
|
||||
|
||||
if not contract_path.exists():
|
||||
print("❌ Contract JSON not found. Running forge build...")
|
||||
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)
|
||||
|
||||
print(f"✓ Loaded contract ABI and bytecode")
|
||||
|
||||
# Create contract
|
||||
contract = w3.eth.contract(
|
||||
abi=contract_data['abi'],
|
||||
bytecode=contract_data['bytecode']['object']
|
||||
)
|
||||
|
||||
# Deploy
|
||||
print("⏳ Deploying contract...")
|
||||
tx_hash = contract.constructor().transact({
|
||||
'from': deployer,
|
||||
'gas': 5000000,
|
||||
'gasPrice': w3.to_wei('1', 'gwei')
|
||||
})
|
||||
|
||||
print(f"✓ Transaction sent: {tx_hash.hex()}")
|
||||
print("⏳ Waiting for receipt...")
|
||||
|
||||
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
|
||||
|
||||
contract_address = receipt.contractAddress
|
||||
print(f"\n✅ Contract deployed successfully!")
|
||||
print(f"📍 Contract Address: {contract_address}")
|
||||
print(f"⛽ Gas Used: {receipt.gasUsed}")
|
||||
|
||||
# Save deployment info
|
||||
deployment_info = {
|
||||
'contract_address': contract_address,
|
||||
'transaction_hash': tx_hash.hex(),
|
||||
'deployer': deployer,
|
||||
'network': 'anvil',
|
||||
'abi': contract_data['abi'],
|
||||
'gas_used': receipt.gasUsed,
|
||||
'block_number': receipt.blockNumber
|
||||
}
|
||||
|
||||
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"\n📝 Update your .env file:")
|
||||
print(f"CONTRACT_ADDRESS={contract_address}")
|
||||
|
||||
return contract_address
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
address = deploy_contract()
|
||||
print(f"\n✨ Deployment complete!")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
exit(1)
|
||||
@@ -1,14 +1,22 @@
|
||||
import tensorflow as tf
|
||||
import pickle
|
||||
import json
|
||||
import numpy as np
|
||||
import random
|
||||
import os
|
||||
from tensorflow.keras.preprocessing.sequence import pad_sequences
|
||||
from datetime import datetime
|
||||
from bson import ObjectId
|
||||
import uuid
|
||||
|
||||
# Optional TensorFlow import with fallback
|
||||
try:
|
||||
import tensorflow as tf
|
||||
from tensorflow.keras.preprocessing.sequence import pad_sequences
|
||||
TENSORFLOW_AVAILABLE = True
|
||||
except ImportError:
|
||||
tf = None
|
||||
pad_sequences = None
|
||||
TENSORFLOW_AVAILABLE = False
|
||||
|
||||
class AdaptiveQuizMasterLLM:
|
||||
def __init__(self, models_path="./models/"):
|
||||
"""
|
||||
@@ -18,13 +26,13 @@ class AdaptiveQuizMasterLLM:
|
||||
self.model_available = False
|
||||
|
||||
try:
|
||||
# Try to load model files
|
||||
# Try to load model files only if TensorFlow is available
|
||||
model_file = f'{models_path}improved_cnn_model.h5'
|
||||
tokenizer_file = f'{models_path}tokenizer.pickle'
|
||||
label_encoder_file = f'{models_path}label_encoder.pickle'
|
||||
data_file = f'{models_path}processed_commonsenseqa_data.json'
|
||||
|
||||
if all(os.path.exists(f) for f in [model_file, tokenizer_file, label_encoder_file, data_file]):
|
||||
if TENSORFLOW_AVAILABLE and all(os.path.exists(f) for f in [model_file, tokenizer_file, label_encoder_file, data_file]):
|
||||
try:
|
||||
self.model = tf.keras.models.load_model(model_file)
|
||||
print("✅ CNN Model loaded successfully")
|
||||
|
||||
@@ -13,10 +13,11 @@ import signal
|
||||
|
||||
class RealCompilerService:
|
||||
def __init__(self):
|
||||
self.client = docker.from_env()
|
||||
self.client = None # Lazy initialization
|
||||
self.execution_queue = queue.Queue()
|
||||
self.active_executions = {}
|
||||
self.max_concurrent_executions = 5
|
||||
self.docker_available = False
|
||||
|
||||
# Enhanced language configurations with real execution
|
||||
self.language_configs = {
|
||||
@@ -97,6 +98,18 @@ class RealCompilerService:
|
||||
# Start execution worker
|
||||
self.start_execution_worker()
|
||||
|
||||
def _get_docker_client(self):
|
||||
"""Lazily initialize Docker client"""
|
||||
if self.client is None:
|
||||
try:
|
||||
self.client = docker.from_env()
|
||||
self.docker_available = True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Docker initialization failed: {e}")
|
||||
self.docker_available = False
|
||||
self.client = None
|
||||
return self.client
|
||||
|
||||
def start_execution_worker(self):
|
||||
"""Start background worker for code execution"""
|
||||
def worker():
|
||||
@@ -176,6 +189,17 @@ class RealCompilerService:
|
||||
input_data = context['input_data']
|
||||
config = context['config']
|
||||
|
||||
# Check Docker availability
|
||||
docker_client = self._get_docker_client()
|
||||
if docker_client is None or not self.docker_available:
|
||||
return {
|
||||
"output": "",
|
||||
"error": "Docker service is not available. Compiler service cannot execute code.",
|
||||
"exit_code": -1,
|
||||
"execution_time": 0,
|
||||
"memory_used": 0
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Prepare code file
|
||||
filename = f"code{config['file_ext']}" if language != 'java' else "Main.java"
|
||||
@@ -193,7 +217,7 @@ class RealCompilerService:
|
||||
start_time = time.time()
|
||||
|
||||
# Create and run container
|
||||
container = self.client.containers.run(
|
||||
container = docker_client.containers.run(
|
||||
config['image'],
|
||||
command=self._build_execution_command(config, filename),
|
||||
volumes={temp_dir: {'bind': '/app', 'mode': 'rw'}},
|
||||
@@ -302,4 +326,8 @@ class RealCompilerService:
|
||||
return False
|
||||
|
||||
# Create global instance
|
||||
real_compiler_service = RealCompilerService()
|
||||
try:
|
||||
real_compiler_service = RealCompilerService()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to initialize RealCompilerService: {e}")
|
||||
real_compiler_service = RealCompilerService() # Still create instance for graceful fallback
|
||||
|
||||
Reference in New Issue
Block a user