diff --git a/starklings-backend/README.md b/starklings-backend/README.md index bbb577f1..c5c81523 100644 --- a/starklings-backend/README.md +++ b/starklings-backend/README.md @@ -29,6 +29,9 @@ pip install -r requirements.txt ## Launch API +### Add ENV variables +create .env file in the same directory as this with the fields defined in .env.DEFAULT + - Development ``` APP_SETTINGS=config.DevConfig python app.py diff --git a/starklings-backend/abi/account.json b/starklings-backend/abi/account.json new file mode 100644 index 00000000..f47bc467 --- /dev/null +++ b/starklings-backend/abi/account.json @@ -0,0 +1,414 @@ +[ + { + "members": [ + { + "name": "to", + "offset": 0, + "type": "felt" + }, + { + "name": "selector", + "offset": 1, + "type": "felt" + }, + { + "name": "data_offset", + "offset": 2, + "type": "felt" + }, + { + "name": "data_len", + "offset": 3, + "type": "felt" + } + ], + "name": "CallArray", + "size": 4, + "type": "struct" + }, + { + "data": [ + { + "name": "new_signer", + "type": "felt" + } + ], + "keys": [], + "name": "signer_changed", + "type": "event" + }, + { + "data": [ + { + "name": "new_guardian", + "type": "felt" + } + ], + "keys": [], + "name": "guardian_changed", + "type": "event" + }, + { + "data": [ + { + "name": "new_guardian", + "type": "felt" + } + ], + "keys": [], + "name": "guardian_backup_changed", + "type": "event" + }, + { + "data": [ + { + "name": "active_at", + "type": "felt" + } + ], + "keys": [], + "name": "escape_guardian_triggered", + "type": "event" + }, + { + "data": [ + { + "name": "active_at", + "type": "felt" + } + ], + "keys": [], + "name": "escape_signer_triggered", + "type": "event" + }, + { + "data": [], + "keys": [], + "name": "escape_canceled", + "type": "event" + }, + { + "data": [ + { + "name": "new_guardian", + "type": "felt" + } + ], + "keys": [], + "name": "guardian_escaped", + "type": "event" + }, + { + "data": [ + { + "name": "new_signer", + "type": "felt" + } + ], + "keys": [], + "name": "signer_escaped", + "type": "event" + }, + { + "data": [ + { + "name": "account", + "type": "felt" + }, + { + "name": "key", + "type": "felt" + }, + { + "name": "guardian", + "type": "felt" + } + ], + "keys": [], + "name": "account_created", + "type": "event" + }, + { + "data": [ + { + "name": "new_implementation", + "type": "felt" + } + ], + "keys": [], + "name": "account_upgraded", + "type": "event" + }, + { + "data": [ + { + "name": "hash", + "type": "felt" + }, + { + "name": "response_len", + "type": "felt" + }, + { + "name": "response", + "type": "felt*" + } + ], + "keys": [], + "name": "transaction_executed", + "type": "event" + }, + { + "inputs": [ + { + "name": "signer", + "type": "felt" + }, + { + "name": "guardian", + "type": "felt" + } + ], + "name": "initialize", + "outputs": [], + "type": "function" + }, + { + "inputs": [ + { + "name": "call_array_len", + "type": "felt" + }, + { + "name": "call_array", + "type": "CallArray*" + }, + { + "name": "calldata_len", + "type": "felt" + }, + { + "name": "calldata", + "type": "felt*" + }, + { + "name": "nonce", + "type": "felt" + } + ], + "name": "__execute__", + "outputs": [ + { + "name": "retdata_size", + "type": "felt" + }, + { + "name": "retdata", + "type": "felt*" + } + ], + "type": "function" + }, + { + "inputs": [ + { + "name": "implementation", + "type": "felt" + } + ], + "name": "upgrade", + "outputs": [], + "type": "function" + }, + { + "inputs": [ + { + "name": "new_signer", + "type": "felt" + } + ], + "name": "change_signer", + "outputs": [], + "type": "function" + }, + { + "inputs": [ + { + "name": "new_guardian", + "type": "felt" + } + ], + "name": "change_guardian", + "outputs": [], + "type": "function" + }, + { + "inputs": [ + { + "name": "new_guardian", + "type": "felt" + } + ], + "name": "change_guardian_backup", + "outputs": [], + "type": "function" + }, + { + "inputs": [], + "name": "trigger_escape_guardian", + "outputs": [], + "type": "function" + }, + { + "inputs": [], + "name": "trigger_escape_signer", + "outputs": [], + "type": "function" + }, + { + "inputs": [], + "name": "cancel_escape", + "outputs": [], + "type": "function" + }, + { + "inputs": [ + { + "name": "new_guardian", + "type": "felt" + } + ], + "name": "escape_guardian", + "outputs": [], + "type": "function" + }, + { + "inputs": [ + { + "name": "new_signer", + "type": "felt" + } + ], + "name": "escape_signer", + "outputs": [], + "type": "function" + }, + { + "inputs": [ + { + "name": "hash", + "type": "felt" + }, + { + "name": "sig_len", + "type": "felt" + }, + { + "name": "sig", + "type": "felt*" + } + ], + "name": "is_valid_signature", + "outputs": [ + { + "name": "is_valid", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "interfaceId", + "type": "felt" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "name": "success", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "get_nonce", + "outputs": [ + { + "name": "nonce", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "get_signer", + "outputs": [ + { + "name": "signer", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "get_guardian", + "outputs": [ + { + "name": "guardian", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "get_guardian_backup", + "outputs": [ + { + "name": "guardian_backup", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "get_escape", + "outputs": [ + { + "name": "active_at", + "type": "felt" + }, + { + "name": "type", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "get_version", + "outputs": [ + { + "name": "version", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + } + ] \ No newline at end of file diff --git a/starklings-backend/app.py b/starklings-backend/app.py index b628258f..0b0f37fe 100644 --- a/starklings-backend/app.py +++ b/starklings-backend/app.py @@ -4,9 +4,9 @@ import sys sys.path.append('../src/exercises') from starklings_backend.routes import app_routes -from starklings_backend.models.shared import db - +from flask_sqlalchemy import SQLAlchemy +db = SQLAlchemy() app = Flask(__name__) CORS(app) env_config = os.getenv("APP_SETTINGS", "config.DevConfig") @@ -16,6 +16,5 @@ app.register_blueprint(app_routes) - if __name__ == '__main__': app.run(host="0.0.0.0", port=8080) \ No newline at end of file diff --git a/starklings-backend/db.py b/starklings-backend/db.py index 2c6cf658..7bb59086 100644 --- a/starklings-backend/db.py +++ b/starklings-backend/db.py @@ -1,36 +1,17 @@ import os import pymysql +from sqlalchemy.orm import declarative_base, sessionmaker +from starklings_backend.models import StarklingsUser,Path,Exercise,ValidatedExercise, Base +from sqlalchemy import create_engine from dotenv import load_dotenv load_dotenv() -connection = pymysql.connect( - host=os.environ.get('DATABASE_HOST', ''), - database=os.environ.get('DATABASE_NAME', ''), - user=os.environ.get('DATABASE_USER', ''), - password=os.environ.get('DATABASE_PWD', ''), - charset="utf8mb4", - cursorclass=pymysql.cursors.DictCursor -) +host=os.environ.get('DATABASE_HOST', '') +database=os.environ.get('DATABASE_NAME', '') +user=os.environ.get('DATABASE_USER', '') +password=os.environ.get('DATABASE_PWD', '') -cursor = connection.cursor() +engine = create_engine(f'mysql+pymysql://{user}:{password}@{host}/{database}', echo=True) -validated_sql_query = """CREATE TABLE validated_exercise ( - exercise_name varchar(255) NOT NULL, - user_id int NOT NULL FOREIGN KEY - ... -) -""" - - -user_sql_query = """CREATE TABLE starklings_user ( - user_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, - wallet_address varchar(255) NOT NULL UNIQUE, - score int NOT NULL DEFAULT 0, - username varchar(255) NOT NULL UNIQUE, - signature varchar(255) NOT NULL -) -""" - -#cursor.execute(validated_sql_query) -cursor.execute(user_sql_query) -connection.close() +Session = sessionmaker(bind=engine) +Base.metadata.create_all(engine) \ No newline at end of file diff --git a/starklings-backend/requirements.txt b/starklings-backend/requirements.txt index d421434f..fd4ccd42 100644 --- a/starklings-backend/requirements.txt +++ b/starklings-backend/requirements.txt @@ -3,7 +3,6 @@ Flask==2.1.3 flask-cors==3.0.10 aioflask==0.4.0 Flask-SQLAlchemy==2.5.1 -mysqlclient==2.1.1 pathspec==0.9.0 PyJWT==2.4.0 PyMySQL==1.0.2 @@ -14,3 +13,4 @@ SQLAlchemy==1.4.39 typing_extensions==4.3.0 urllib3==1.26.11 pytest==7.1.2 +starknet-py==0.5.2a0 \ No newline at end of file diff --git a/starklings-backend/starklings_backend/models.py b/starklings-backend/starklings_backend/models.py new file mode 100644 index 00000000..77187ca0 --- /dev/null +++ b/starklings-backend/starklings_backend/models.py @@ -0,0 +1,42 @@ +from sqlalchemy import create_engine, Column, Integer, String, ForeignKey +from sqlalchemy.orm import sessionmaker, relationship, declarative_base + +Base = declarative_base() + +class StarklingsUser(Base): + __tablename__ = "starklings_user" + wallet_address = Column(String(69), primary_key=True) + signature = Column(String(255), nullable=False) + github = Column(String(255), nullable=True) + username = Column(String(255), nullable=False) + score = Column(Integer, nullable=False, default=0) + starklings_user = relationship("ValidatedExercise") + + +class Path(Base): + __tablename__ = "path" + path_name = Column(String(255), primary_key=True) + num_exercises = Column(Integer, nullable=False) + path = relationship("Exercise") + + +class Exercise(Base): + __tablename__ = "exercise" + exercise_name = Column(String(255), primary_key=True) + score = Column(Integer, nullable=False, default=0) + path_name = Column(String(255), ForeignKey("path.path_name"), nullable=False) + exercise = relationship("ValidatedExercise") + + +class ValidatedExercise(Base): + __tablename__ = "validated_exercise" + validated_exercise_id = Column(String(64), primary_key=True) + exercise_name = Column( + String(255), + ForeignKey("exercise.exercise_name"), + nullable=False, + ) + wallet_address = Column( + String(69), ForeignKey("starklings_user.wallet_address"), nullable=False + ) + diff --git a/starklings-backend/starklings_backend/models/shared.py b/starklings-backend/starklings_backend/models/shared.py deleted file mode 100644 index 1770275b..00000000 --- a/starklings-backend/starklings_backend/models/shared.py +++ /dev/null @@ -1,4 +0,0 @@ -# apps.shared.models -from flask_sqlalchemy import SQLAlchemy - -db = SQLAlchemy() \ No newline at end of file diff --git a/starklings-backend/starklings_backend/models/user.py b/starklings-backend/starklings_backend/models/user.py deleted file mode 100644 index db5b1e9c..00000000 --- a/starklings-backend/starklings_backend/models/user.py +++ /dev/null @@ -1,8 +0,0 @@ -from starklings_backend.models.shared import db - -class Starklingsuser(db.Model): - user_id = db.Column(db.Integer, primary_key=True) - score = db.Column(db.Integer, nullable=False, default=0) - signature = db.Column(db.String(255), nullable=False) - username = db.Column(db.String(255), unique=True, nullable=False) - wallet_address = db.Column(db.String(255), unique=True, nullable=False) diff --git a/starklings-backend/starklings_backend/routes.py b/starklings-backend/starklings_backend/routes.py index 768916ec..1b09fc84 100644 --- a/starklings-backend/starklings_backend/routes.py +++ b/starklings-backend/starklings_backend/routes.py @@ -1,95 +1,150 @@ +# import pdb; pdb.set_trace() from flask import request, Blueprint import asyncio import bcrypt from sqlalchemy.exc import IntegrityError from flask_sqlalchemy import SQLAlchemy -from starklings_backend.utils import verify_email -from starklings_backend.models.shared import db -from starklings_backend.models.user import Starklingsuser -from starklings_backend.exercise import verify_exercise -from checker import ExerciceFailed +from starklings_backend.utils import verify_email, VerifySignature +from starklings_backend.models import ( + StarklingsUser, + Path, + Exercise, + ValidatedExercise, + Base, +) +# from starklings_backend.exercise import verify_exercise +# from checker import ExerciceFailed import tempfile +from db import Session +from pathlib import Path +import json -app_routes = Blueprint('app_routes', __name__) -@app_routes.route('/', methods=['GET']) +app_routes = Blueprint("app_routes", __name__) + +db = Session() + + +@app_routes.route("/", methods=["GET"]) def landing(): - return 'Starklings API' + return "Starklings API" + ####################### # Users Routes # ####################### -@app_routes.route('/registerUser', methods=['POST']) +@app_routes.route("/registerUser", methods=["POST"]) def register_user(): """ Inserts a new user in the Database - @TODO: Starknet ID / Signature and implements model """ try: - signature = request.json.get('signature', None) - wallet_address = request.json.get('wallet_address', None) - username = request.json.get('username', wallet_address) + signature = request.json.get("signature", None) + wallet_address = request.json.get("wallet_address", None) + username = request.json.get("username", wallet_address) + github = request.json.get("github", None) + message_hash = request.json.get("message_hash", "") + network = request.json.get("network", "testnet") if None in [wallet_address, signature]: - return "Wrong form", 400 - #@TODO: Check Signature validity - - user = Starklingsuser(wallet_address=wallet_address, signature=signature, username=username) - db.session.commit() - return f'Welcome! {username}', 200 + return "Wrong form", 400 + # verify signature + abi = Path.cwd() / "abi" / "account.json" + verify_signature = VerifySignature(abi, network, wallet_address) + is_valid, error = verify_signature(message_hash, signature) + # import pdb; pdb.set_trace() + if error is None: + user = StarklingsUser( + wallet_address=wallet_address, + signature=json.dumps(signature), + github=github, + username=username, + ) + db.add(user) + db.commit() + return f"Welcome! {username}", 200 + return "Signature invalid", 400 except IntegrityError as e: - db.session.rollback() - return 'User Already Exists', 400 + db.rollback() + return "User Already Exists", 400 except AttributeError: - return 'Provide an Email and Password in JSON format in the request body', 400 + return "Provide an Email and Password in JSON format in the request body", 400 +@app_routes.route("/updateUser", methods=["POST"]) +def update_user(): + try: + update_parameter = request.json.get("update_parameter", None) + if update_parameter is None: + return "Missing update parameter", 400 + for k, v in update_parameter.items(): + if k not in ['username', 'github']: + return f"Incorrect payload {k}", 400 -@app_routes.route('/fetchUserInfo', methods=['POST']) + wallet_address = request.json.get("wallet_address", None) + signature = request.json.get("signature", None) + if not wallet_address: + return "Missing address", 400 + if not signature: + return "Missing signature", 400 + # verify signature + abi = Path.cwd() / "abi" / "account.json" + verify_signature = VerifySignature(abi, network, wallet_address) + is_valid, error = verify_signature(message_hash, signature) + if error is None: + user = db.query(StarklingsUser).filter_by(wallet_address=wallet_address, signature=str(signature)) + if user is None: + return "Invalid wallet and signature pair", 400 + user.update(update_parameter, synchronize_session="fetch") + db.commit() + return f"Update successful", 200 + return "Signature invalid", 400 + except AttributeError as e: + print(e) + return "Provide the wallet address in JSON format in the request body", 400 + + + +@app_routes.route("/fetchUserInfo", methods=["POST"]) def fetch_user_info(): """ Authenticate a user @TODO Implements Fetch User Information """ try: - wallet_address = request.json.get('wallet_address', None) + wallet_address = request.json.get("wallet_address", None) if not wallet_address: - return 'Missing address', 400 - user = Starklingsuser.query.filter_by(wallet_address=wallet_address).first() + return "Missing address", 400 + user = db.query(StarklingsUser).filter_by(wallet_address=wallet_address).first() if not user: - return 'User Not Found!', 404 - - return f'Logged in, Welcome {user.username}!', 200 + return "User Not Found!", 404 + + return f"Logged in, Welcome {user.username}!", 200 except AttributeError as e: print(e) - return 'Provide the wallet address in JSON format in the request body', 400 + return "Provide the wallet address in JSON format in the request body", 400 ####################### # Exercises Routes # ####################### -@app_routes.route('/exercise/check', methods=['POST']) +@app_routes.route("/exercise/check", methods=["POST"]) async def starklings_exercise_checker(): """ Check exercise given a body and a user @TODO: Implement User DB for storing results """ try: - address = request.json.get('wallet_address', None) - exercise = request.json.get('exercise', 'storage/storage01') - exercise_data = request.json.get('exercise_data', None) + address = request.json.get("wallet_address", None) + exercise = request.json.get("exercise", "storage/storage01") + exercise_data = request.json.get("exercise_data", None) if not address: - return 'Missing Address', 400 + return "Missing Address", 400 tmp = tempfile.NamedTemporaryFile() - with open(tmp.name, 'w') as temp_exercise: + with open(tmp.name, "w") as temp_exercise: temp_exercise.write(exercise_data) res = await verify_exercise(tmp.name) tmp.close() - return { - "result": "success" - } + return {"result": "success"} except ExerciceFailed as error: print(error) - return { - "result": "failure", - "error": error.message - }, 400 + return {"result": "failure", "error": error.message}, 400 diff --git a/starklings-backend/starklings_backend/utils.py b/starklings-backend/starklings_backend/utils.py index 0ffa0266..8e7d05de 100644 --- a/starklings-backend/starklings_backend/utils.py +++ b/starklings-backend/starklings_backend/utils.py @@ -2,15 +2,21 @@ import json import os import re +from starknet_py.net.gateway_client import GatewayClient +from starknet_py.net.networks import TESTNET, MAINNET +from starknet_py.contract import Contract +import asyncio -regex = re.compile(r'([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+') + +regex = re.compile(r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+") def verify_email(email): if re.fullmatch(regex, email): - return True + return True else: - return False + return False + class Requester: def __init__(self, base_url, **kwargs): @@ -22,25 +28,25 @@ def __init__(self, base_url, **kwargs): setattr(self.session, arg, kwargs[arg]) def request(self, method, url, **kwargs): - return self.session.request(method, self.base_url+url, **kwargs) + return self.session.request(method, self.base_url + url, **kwargs) def head(self, url, **kwargs): - return self.session.head(self.base_url+url, **kwargs) + return self.session.head(self.base_url + url, **kwargs) def get(self, url, **kwargs): - return self.session.get(self.base_url+url, **kwargs) + return self.session.get(self.base_url + url, **kwargs) def post(self, url, data, **kwargs): - return self.session.post(self.base_url+url, data=data, **kwargs) + return self.session.post(self.base_url + url, data=data, **kwargs) def put(self, url, **kwargs): - return self.session.put(self.base_url+url, **kwargs) + return self.session.put(self.base_url + url, **kwargs) def patch(self, url, **kwargs): - return self.session.patch(self.base_url+url, **kwargs) + return self.session.patch(self.base_url + url, **kwargs) def delete(self, url, **kwargs): - return self.session.delete(self.base_url+url, **kwargs) + return self.session.delete(self.base_url + url, **kwargs) @staticmethod def __deep_merge(source, destination): @@ -51,3 +57,36 @@ def __deep_merge(source, destination): else: destination[key] = value return destination + + +class VerifySignature: + SUPPORTED_NETWORKS = ["mainnet", "testnet"] + + def __init__(self, abi, network, contract_address): + with open(abi, "r") as reader: + abi = json.load(reader) + assert network in self.SUPPORTED_NETWORKS + network = self.set_network(network) + self.contract = Contract( + contract_address, + abi, + network, + ) + + def set_network(self, network): + if network == "testnet": + return GatewayClient(TESTNET) + elif network == "mainnet": + return GatewayClient(MAINNET) + + def __call__(self, message_hash, signature): + try: + + asyncio.run( + self.contract.functions["is_valid_signature"].call( + message_hash, (signature[0], signature[1]) + ) + ) + return "Valid Signature", None + except Exception as e: + return "Invalid Signature", 400 \ No newline at end of file