diff --git a/Server/.flaskenv b/Server/.flaskenv new file mode 100644 index 00000000..c80b622a --- /dev/null +++ b/Server/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=app.py +FLASK_DEBUG=1 \ No newline at end of file diff --git a/Server/Pipfile b/Server/Pipfile index 2f75bdb1..7b6815f8 100644 --- a/Server/Pipfile +++ b/Server/Pipfile @@ -9,8 +9,6 @@ flask = "*" flask_cors = "*" thirdweb-sdk = "*" python-dotenv = "*" +psycopg2 = "*" [dev-packages] - -[requires] -python_version = "3.8.9" \ No newline at end of file diff --git a/Server/Pipfile.lock b/Server/Pipfile.lock index d2d2fbac..b4267f5c 100644 --- a/Server/Pipfile.lock +++ b/Server/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "de84932730ab160c6a016c357d0c3a330c7e195c043e947474ae5d05ebe79688" + "sha256": "a63f487dff2f58c06a49601445fc0d7d98abe93421506ec625627bd23a5760a7" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.8.9" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -651,14 +649,6 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, - "importlib-metadata": { - "hashes": [ - "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", - "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" - ], - "markers": "python_version < '3.10'", - "version": "==6.0.0" - }, "ipfshttpclient": { "hashes": [ "sha256:0d80e95ee60b02c7d414e79bf81a36fc3c8fbab74265475c52f70b2620812135", @@ -943,6 +933,25 @@ "markers": "python_version >= '3.7'", "version": "==3.20.3" }, + "psycopg2": { + "hashes": [ + "sha256:093e3894d2d3c592ab0945d9eba9d139c139664dcf83a1c440b8a7aa9bb21955", + "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa", + "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e", + "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a", + "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5", + "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee", + "sha256:920bf418000dd17669d2904472efeab2b20546efd0548139618f8fa305d1d7ad", + "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0", + "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a", + "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d", + "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f", + "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2", + "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5" + ], + "index": "pypi", + "version": "==2.9.5" + }, "pycryptodome": { "hashes": [ "sha256:0198fe96c22f7bc31e7a7c27a26b2cec5af3cf6075d577295f4850856c77af32", @@ -1348,14 +1357,6 @@ ], "markers": "python_version >= '3.7'", "version": "==1.8.2" - }, - "zipp": { - "hashes": [ - "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa", - "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766" - ], - "markers": "python_version >= '3.7'", - "version": "==3.11.0" } }, "develop": {} diff --git a/Server/README.md b/Server/README.md deleted file mode 100644 index 02a1afed..00000000 --- a/Server/README.md +++ /dev/null @@ -1 +0,0 @@ -Make sure to initialise the Flask app with `pipenv` and run the command `export FLASK_APP=app.py` \ No newline at end of file diff --git a/Server/app.py b/Server/app.py index 5c623578..fcf2c099 100644 --- a/Server/app.py +++ b/Server/app.py @@ -1,99 +1,24 @@ -from flask import Flask, request, make_response, jsonify +from flask import Flask, request, make_response, jsonify, Blueprint from thirdweb.types import LoginPayload from thirdweb import ThirdwebSDK from datetime import datetime, timedelta import os -import moralisHandler +from auth.moralisHandler import moralis_handler +from auth.thirdwebHandler import thirdweb_handler +from contracts.planetDrop import planet_drop +from database.connection import database_connection app = Flask(__name__) - -# Getting proposals (move to separate file) -web3Sdk = ThirdwebSDK("goerli") -contract = web3Sdk.get_contract("0xCcaA1ABA77Bae6296D386C2F130c46FEc3E5A004") -proposals = contract.call("getProposals") - -# Minting candidate nfts -nftSdk = ThirdwebSDK('mumbai') -nftContract = nftSdk.get_contract("0xed6e837Fda815FBf78E8E7266482c5Be80bC4bF9") +app.register_blueprint(moralis_handler, url_prefix='/moralis-auth') +app.register_blueprint(thirdweb_handler, url_prefix='/auth') +app.register_blueprint(planet_drop, url_prefix='/planets') +app.register_blueprint(database_connection, url_prefix='/database') @app.route('/') def index(): return "Hello World" -@app.route('/proposals', methods=["GET"]) -def getProposals(): - # Mint nft based on proposal id - proposalCandidate = nftContract.call("lazyMint", _amount, _baseURIForTokens, _data) # Get this from Jupyter notebook -> https://thirdweb.com/mumbai/0xed6e837Fda815FBf78E8E7266482c5Be80bC4bF9/nfts token id 0 (e.g.) - createProposal = contract.call("createProposal", _owner, _title, _description, _target, _deadline, _image) # Get this from PUSH req contents - - return proposals - -@app.route('/login', methods=['POST']) -def login(): - private_key = os.environ.get("PRIVATE_KEY") - - if not private_key: - print("Missing PRIVATE_KEY environment variable") - return "Wallet private key not set", 400 - - sdk = ThirdwebSDK.from_private_key(private_key, 'mumbai') # Initialise the sdk using the wallet and on mumbai testnet chain - payload = LoginPayload.from_json(request.json['payload']) - - # Generate access token using signed payload - domain = 'sailors.skinetics.tech' - token = sdk.auth.generate_auth_token(domain, payload) - - res = make_response() - res.set_cookie( - 'access_token', - token, - path='/', - httponly=True, - secure=True, - samesite='strict', - ) - return res, 200 - -@app.route('/authenticate', methods=['POST']) -def authenticate(): - private_key = os.environ.get("PRIVATE_KEY") - - if not private_key: - print("Missing PRIVATE_KEY environment variable") - return "Wallet private key not set", 400 - - sdk = ThirdwebSDK.from_private_key(private_key, 'mumbai') - - # Get access token from cookies - token = request.cookies.get('access_token') - if not token: - return 'Unauthorised', 401 - - domain = 'sailors.skinetics.tech' - - try: - address = sdk.auth.authenticate(domain, token) - except: - return "Unauthorized", 401 - - print(jsonify(address)) - return jsonify(address), 200 - -@app.route('/logout', methods=['POST']) -def logout(): - res = make_response() - res.set_cookie( - 'access_token', - 'none', - expires=datetime.utcnow() + timedelta(second = 5) - ) - return res, 200 - -@app.route('/helloworld') -def helloworld(): - return "address" #address - # Getting proposals route #@app.route('/proposals') #def getProposals(): diff --git a/Server/moralisHandler.py b/Server/auth/moralisHandler.py similarity index 86% rename from Server/moralisHandler.py rename to Server/auth/moralisHandler.py index f0a67e28..a5e4e728 100644 --- a/Server/moralisHandler.py +++ b/Server/auth/moralisHandler.py @@ -1,15 +1,15 @@ -from flask import Flask -from flask import request +from flask import Blueprint, request from moralis import auth from flask_cors import CORS -from app import app + +moralis_handler = Blueprint('moralis_handler', __name__) # Moralis setup apiKey = "kJfYYpmMmfKhvaWMdD3f3xMMb24B4MHBDDVrfjslkKgTilvMgdwr1bwKUr8vWdHH" # Move to env # Authentication routes -> move to auth.py later # Request a challenge when a user attempts to connect their wallet -@app.route('/requestChallenge', methods=['GET']) +@moralis_handler.route('/requestChallenge', methods=['GET']) def reqChallenge(): args = request.args # Fetch the arguments from the request @@ -35,7 +35,7 @@ def reqChallenge(): return result # Verify signature from user -@app.route('/verifyChallenge', methods=['GET']) +@moralis_handler.route('/verifyChallenge', methods=['GET']) def verifyChallenge(): args = request.args diff --git a/Server/auth/thirdwebHandler.py b/Server/auth/thirdwebHandler.py new file mode 100644 index 00000000..64e0948b --- /dev/null +++ b/Server/auth/thirdwebHandler.py @@ -0,0 +1,85 @@ +from flask import Blueprint, request +from thirdweb.types import LoginPayload +from thirdweb import ThirdwebSDK +from datetime import datetime, timedelta +import os + +thirdweb_handler = Blueprint('thirdweb_handler', __name__) + +# Getting proposals +web3Sdk = ThirdwebSDK("goerli") +contract = web3Sdk.get_contract("0xCcaA1ABA77Bae6296D386C2F130c46FEc3E5A004") +proposals = contract.call("getProposals") + +# Minting candidate nfts +nftSdk = ThirdwebSDK('mumbai') +nftContract = nftSdk.get_contract("0xed6e837Fda815FBf78E8E7266482c5Be80bC4bF9") + +@thirdweb_handler.route('/login', methods=['POST']) +def login(): + private_key = os.environ.get("PRIVATE_KEY") + + if not private_key: + print("Missing PRIVATE_KEY environment variable") + return "Wallet private key not set", 400 + + sdk = ThirdwebSDK.from_private_key(private_key, 'mumbai') # Initialise the sdk using the wallet and on mumbai testnet chain + payload = LoginPayload.from_json(request.json['payload']) + + # Generate access token using signed payload + domain = 'sailors.skinetics.tech' + token = sdk.auth.generate_auth_token(domain, payload) + + res = make_response() + res.set_cookie( + 'access_token', + token, + path='/', + httponly=True, + secure=True, + samesite='strict', + ) + return res, 200 + +@thirdweb_handler.route('/authenticate', methods=['POST']) +def authenticate(): + private_key = os.environ.get("PRIVATE_KEY") + + if not private_key: + print("Missing PRIVATE_KEY environment variable") + return "Wallet private key not set", 400 + + sdk = ThirdwebSDK.from_private_key(private_key, 'mumbai') + + # Get access token from cookies + token = request.cookies.get('access_token') + if not token: + return 'Unauthorised', 401 + + domain = 'sailors.skinetics.tech' + + try: + address = sdk.auth.authenticate(domain, token) + except: + return "Unauthorized", 401 + + print(jsonify(address)) + return jsonify(address), 200 + +@thirdweb_handler.route('/logout', methods=['POST']) +def logout(): + res = make_response() + res.set_cookie( + 'access_token', + 'none', + expires=datetime.utcnow() + timedelta(second = 5) + ) + return res, 200 + +@thirdweb_handler.route('/proposals', methods=["GET", "POST"]) +def getProposals(): + # Mint nft based on proposal id + proposalCandidate = nftContract.call("lazyMint", _amount, _baseURIForTokens, _data) # Get this from Jupyter notebook -> https://thirdweb.com/mumbai/0xed6e837Fda815FBf78E8E7266482c5Be80bC4bF9/nfts token id 0 (e.g.) + createProposal = contract.call("createProposal", _owner, _title, _description, _target, _deadline, _image) # Get this from PUSH req contents + + return proposals \ No newline at end of file diff --git a/Server/contracts/planetDrop.py b/Server/contracts/planetDrop.py new file mode 100644 index 00000000..61bab07a --- /dev/null +++ b/Server/contracts/planetDrop.py @@ -0,0 +1,39 @@ +from flask import Blueprint, request +from thirdweb import ThirdwebSDK + +planet_drop = Blueprint('planet_drop', __name__) + +# Get NFT balance for Planet Edition Drop (https://thirdweb.com/goerli/0xdf35Bb26d9AAD05EeC5183c6288f13c0136A7b43/code) +@planet_drop.route('/balance') +def get_balance(): + # Planet Edition Drop Contract + network = "goerli" + sdk = ThirdwebSDK(network) + contract = sdk.get_edition_drop("0xdf35Bb26d9AAD05EeC5183c6288f13c0136A7b43") + + address = "0xCdc5929e1158F7f0B320e3B942528E6998D8b25c" + token_id = 0 + balance = contract.balance_of(address, token_id) + + return str(balance) + +@planet_drop.route('/get_planet') +def get_planet(): + network = 'goerli' + sdk = ThirdwebSDK(network) + + # Getting Planet (candidate nfts) + contract = sdk.get_contract("0x766215a318E2AD1EbdC4D92cF2A3b70CBedeac31") + tic55525572 = contract.call("uri", 0) # For token id 0, tic id 55525572 + return str(tic55525572) + +@planet_drop.route('/mint_planet', methods=["GET", "POST"]) +def create_planet(): + #Output from IPFS gateway: #{"name":"TIC 55525572","description":"Exoplanet candidate discovered by TIC ID. \n\nReferences: https://exoplanets.nasa.gov/exoplanet-catalog/7557/toi-813-b/\nhttps://exofop.ipac.caltech.edu/tess/target.php?id=55525572\n\nDeepnote Analysis: https://deepnote.com/workspace/star-sailors-49d2efda-376f-4329-9618-7f871ba16007/project/Star-Sailors-Light-Curve-Plot-b4c251b4-c11a-481e-8206-c29934eb75da/%2FMultisector%20Analysis.ipynb","image":"ipfs://Qma2q8RgX1X2ZVcfnJ7b9RJeKHzoTXahs2ezzqQP4f5yvT/0.png","external_url":"","background_color":"","attributes":[{"trait_type":"tic","value":"55525572"},{"trait_type":"mass_earth","value":"36.4"},{"trait_type":"type","value":"neptune-like"},{"trait_type":"orbital_period","value":"83.9"},{"trait_type":"eccentricity","value":"0.0"},{"trait_type":"detection_method","value":"transit"},{"trait_type":"orbital_radius","value":"0.423"},{"trait_type":"radius_jupiter","value":"0.599"},{"trait_type":"distance_earth","value":"858"}]} + # Multiple instances for the same ID will be created (as long as the traits are the same), one for each person, as each planet instance appeared differently and will be manipulated differently by users + # Creating a planet nft based on discovery + network = 'goerli' + sdk = ThirdwebSDK(network) + contract = sdk.get_contract("0x766215a318E2AD1EbdC4D92cF2A3b70CBedeac31") + #data = contract.call("lazyMint", _amount, _baseURIForTokens, _data) (POST data) + # Interaction flow -> https://www.notion.so/skinetics/Sample-Planets-Contract-4c3bdcbca4b9450382f9cc4e72e081f7 \ No newline at end of file diff --git a/Server/database/connection.py b/Server/database/connection.py new file mode 100644 index 00000000..e78f0450 --- /dev/null +++ b/Server/database/connection.py @@ -0,0 +1,36 @@ +from flask import Blueprint +from dotenv import load_dotenv +import psycopg2 + +database_connection = Blueprint('database_connection', __name__) + +load_dotenv() +url = os.getenv("DATABASE_URL") +connection = psycopg2.connect(url) + +# PostgreSQL queries +CREATE_USERS_TABLE = ( + 'CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, address TEXT);' +) +CREATE_PLANETS_TABLE = ( + """CREATE TABLE IF NOT EXISTS planets (user_id INTEGER, temperature REAL, date TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE);""" +) +INSERT_USER_RETURN_ID = 'INSERT INTO users (address) VALUES (%s) RETURNING id;' +INSERT_PLANET = ( + 'INSERT INTO planets (user_id, temperature, date) VALUES (%s, %s, %s);' +) + +# User Management +@database_connection.post('/api/user') +def addUser(): + data = request.get_json() + address = data['address'] + + # Connect to the database + with connection: + with connection.cursor() as cursor: + cursor.execute(CREATE_USERS_TABLE) + cursor.execute(INSERT_USER_RETURN_ID, (address,)) + user_id = cursor.fetchone()[0] + + return {'id': user_id, 'message': f"User {address} created"}, 201 \ No newline at end of file diff --git a/Server/docs/README.md b/Server/docs/README.md new file mode 100644 index 00000000..7b96260b --- /dev/null +++ b/Server/docs/README.md @@ -0,0 +1,3 @@ +Make sure to initialise the Flask app with `pipenv` and run the command `export FLASK_APP=app.py` + +We'll have a simple wrapper on Deepnote that will communicate with the multi-layered (aka file) Flask app here, until Deepnote supports multiple files in a Flask container \ No newline at end of file