Initial commit
commit
b3c3e2c82a
|
@ -0,0 +1,20 @@
|
|||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
API_SECRET_KEY=iF7vVfzZewPcFKEgKUOvt8Ga
|
||||
API_DATABASE_URL="mysql://m0leuser:6ScahhrBk821RIRSps0BpWLQ@m0lecoin_db/m0lecoindb"
|
||||
API_WEB3_PROVIDER="http://10.10.0.4:8552"
|
||||
MAILBOX_URL="http://10.10.0.5:10000/mailbox"
|
||||
|
||||
TOKEN_CONTRACT="0x3CD2285C82e814aE99FD2b74d5F050b6daA1eF86"
|
||||
SHOP_CONTRACT="0xaD4Bbb32252B768bBf8cD75a6503a2cCC0a15f28"
|
||||
BANK_CONTRACT="0x2A51d126F603a911c45fb0B7798de0A356154b13"
|
||||
|
||||
MYSQL_DATABASE="m0lecoindb"
|
||||
MYSQL_USER="m0leuser"
|
||||
MYSQL_PASSWORD="6ScahhrBk821RIRSps0BpWLQ"
|
||||
MYSQL_ROOT_PASSWORD="IXDhcjyOLeBIMQD8mGXnK9Qs"
|
||||
|
||||
FRONTEND_API_PORT=8000
|
||||
FRONTEND_PORT=4200
|
||||
|
||||
TEAM_NUMBER="7"
|
|
@ -0,0 +1 @@
|
|||
**/.DS_Store
|
|
@ -0,0 +1,2 @@
|
|||
/config/*
|
||||
/data/*
|
|
@ -0,0 +1,13 @@
|
|||
https://m0lecoin.team{$TEAM_NUMBER}.m0lecon.fans:{$FRONTEND_API_PORT} {
|
||||
|
||||
tls /certificates/fullchain.pem /certificates/privkey.pem
|
||||
|
||||
reverse_proxy http://m0lecoin_backend:{$API_PORT}
|
||||
}
|
||||
|
||||
https://m0lecoin.team{$TEAM_NUMBER}.m0lecon.fans:{$FRONTEND_PORT} {
|
||||
|
||||
tls /certificates/fullchain.pem /certificates/privkey.pem
|
||||
|
||||
reverse_proxy http://m0lecoin_frontend:80
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
m0lecoin_db:
|
||||
image: mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
MYSQL_USER: ${MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
# expose:
|
||||
# - 3306
|
||||
volumes:
|
||||
- "./m0lecoin-backend/data:/var/lib/mysql"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- m0lecoin_backend_net
|
||||
|
||||
m0lecoin_backend:
|
||||
build:
|
||||
context: "./m0lecoin-backend"
|
||||
args:
|
||||
API_PORT: ${API_PORT}
|
||||
API_HOST: ${API_HOST}
|
||||
# expose:
|
||||
# - ${API_PORT}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
API_HOST: ${API_HOST}
|
||||
API_PORT: ${API_PORT}
|
||||
API_SECRET_KEY: ${API_SECRET_KEY}
|
||||
API_DATABASE_URL: ${API_DATABASE_URL}
|
||||
API_WEB3_PROVIDER: ${API_WEB3_PROVIDER}
|
||||
TOKEN_CONTRACT: ${TOKEN_CONTRACT}
|
||||
SHOP_CONTRACT: ${SHOP_CONTRACT}
|
||||
BANK_CONTRACT: ${BANK_CONTRACT}
|
||||
MAILBOX_URL: ${MAILBOX_URL}
|
||||
depends_on:
|
||||
- m0lecoin_db
|
||||
networks:
|
||||
- m0lecoin_backend_net
|
||||
- m0lecoin_proxy_net
|
||||
|
||||
m0lecoin_frontend:
|
||||
build:
|
||||
context: "./m0lecoin-frontend"
|
||||
args:
|
||||
token_contract: ${TOKEN_CONTRACT}
|
||||
bank_contract: ${BANK_CONTRACT}
|
||||
shop_contract: ${SHOP_CONTRACT}
|
||||
frontend_api_port: ${FRONTEND_API_PORT}
|
||||
# expose:
|
||||
# - 80
|
||||
depends_on:
|
||||
- m0lecoin_backend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- m0lecoin_proxy_net
|
||||
|
||||
m0lecoin_proxy:
|
||||
image: caddy:2
|
||||
container_name: m0lecoin_caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:${FRONTEND_PORT}"
|
||||
- "${FRONTEND_API_PORT}:${FRONTEND_API_PORT}"
|
||||
volumes:
|
||||
- ./caddyproxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./caddyproxy/data:/data
|
||||
- ./caddyproxy/config:/config
|
||||
- /secrets/m0lecoin:/certificates
|
||||
environment:
|
||||
LOG_FILE: "/data/access.log"
|
||||
TEAM_NUMBER: ${TEAM_NUMBER}
|
||||
API_PORT: ${API_PORT}
|
||||
FRONTEND_API_PORT: ${FRONTEND_API_PORT}
|
||||
FRONTEND_PORT: ${FRONTEND_PORT}
|
||||
networks:
|
||||
- m0lecoin_proxy_net
|
||||
|
||||
networks:
|
||||
m0lecoin_backend_net:
|
||||
driver: bridge
|
||||
m0lecoin_proxy_net:
|
||||
driver: bridge
|
|
@ -0,0 +1,64 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
m0lecoin_db:
|
||||
image: mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
MYSQL_USER: ${MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- "./m0lecoin-backend/data:/var/lib/mysql"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- m0lecoin_backend_net
|
||||
|
||||
m0lecoin_backend:
|
||||
build:
|
||||
context: "./m0lecoin-backend"
|
||||
args:
|
||||
API_PORT: ${API_PORT}
|
||||
API_HOST: ${API_HOST}
|
||||
ports:
|
||||
- "${FRONTEND_API_PORT}:${API_PORT}"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
API_HOST: ${API_HOST}
|
||||
API_PORT: ${API_PORT}
|
||||
API_SECRET_KEY: ${API_SECRET_KEY}
|
||||
API_DATABASE_URL: ${API_DATABASE_URL}
|
||||
API_WEB3_PROVIDER: ${API_WEB3_PROVIDER}
|
||||
TOKEN_CONTRACT: ${TOKEN_CONTRACT}
|
||||
SHOP_CONTRACT: ${SHOP_CONTRACT}
|
||||
BANK_CONTRACT: ${BANK_CONTRACT}
|
||||
MAILBOX_URL: ${MAILBOX_URL}
|
||||
depends_on:
|
||||
- m0lecoin_db
|
||||
networks:
|
||||
- m0lecoin_backend_net
|
||||
- m0lecoin_proxy_net
|
||||
|
||||
m0lecoin_frontend:
|
||||
build:
|
||||
context: "./m0lecoin-frontend"
|
||||
args:
|
||||
token_contract: ${TOKEN_CONTRACT}
|
||||
bank_contract: ${BANK_CONTRACT}
|
||||
shop_contract: ${SHOP_CONTRACT}
|
||||
frontend_api_port: ${FRONTEND_API_PORT}
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
depends_on:
|
||||
- m0lecoin_backend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- m0lecoin_proxy_net
|
||||
|
||||
networks:
|
||||
m0lecoin_backend_net:
|
||||
driver: bridge
|
||||
m0lecoin_proxy_net:
|
||||
driver: bridge
|
|
@ -0,0 +1,3 @@
|
|||
pyenv
|
||||
__pycache__
|
||||
data
|
|
@ -0,0 +1,3 @@
|
|||
/pyenv/
|
||||
*pycache*
|
||||
/data/
|
|
@ -0,0 +1,14 @@
|
|||
FROM python:3.10
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install -U pip && pip install -r requirements.txt
|
||||
RUN pip install gunicorn
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "/bin/sh", "-c", "python3 app.py migrate && gunicorn -w 4 -b $API_HOST:$API_PORT app:app"]
|
|
@ -0,0 +1 @@
|
|||
[{"inputs": [{"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "deposit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "getBalance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "isRegistered", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint8", "name": "v", "type": "uint8"}, {"internalType": "bytes32", "name": "r", "type": "bytes32"}, {"internalType": "bytes32", "name": "s", "type": "bytes32"}, {"internalType": "bytes32", "name": "hash", "type": "bytes32"}], "name": "openAccount", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
@ -0,0 +1 @@
|
|||
[{"anonymous": false, "inputs": [{"indexed": true, "internalType": "int256", "name": "_id", "type": "int256"}, {"indexed": true, "internalType": "address", "name": "_to", "type": "address"}], "name": "ProductSale", "type": "event"}, {"inputs": [{"internalType": "int256", "name": "productId", "type": "int256"}], "name": "buy", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "int256", "name": "productId", "type": "int256"}], "name": "getPriceById", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "int256", "name": "productId", "type": "int256"}, {"internalType": "uint256", "name": "price", "type": "uint256"}], "name": "putOnSale", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
@ -0,0 +1 @@
|
|||
[{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "_to", "type": "address"}, {"indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256"}], "name": "Minted", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "_from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "_to", "type": "address"}, {"indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256"}], "name": "Sent", "type": "event"}, {"inputs": [{"internalType": "address", "name": "addr", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "getBalance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "granularity", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "pure", "type": "function"}, {"inputs": [], "name": "name", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "pure", "type": "function"}, {"inputs": [{"internalType": "address", "name": "_from", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "requestTransfer", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "pure", "type": "function"}, {"inputs": [{"internalType": "address", "name": "_to", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "transfer", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
@ -0,0 +1,4 @@
|
|||
import json
|
||||
|
||||
token_interface = json.load(open('./abi/token.json', 'r'))
|
||||
shop_interface = json.load(open('./abi/shop.json', 'r'))
|
|
@ -0,0 +1,294 @@
|
|||
import os
|
||||
from flask import Blueprint, request, jsonify, current_app as app
|
||||
from werkzeug.security import generate_password_hash,check_password_hash
|
||||
from middleware import token_required
|
||||
import jwt, requests, hashlib, hmac
|
||||
from models import db, Users, DigitalProducts, MaterialProducts, Otps
|
||||
from web3 import Web3
|
||||
from eth_account.messages import encode_defunct
|
||||
from web3service import shopContract, web3
|
||||
|
||||
api = Blueprint('api', import_name=__name__)
|
||||
|
||||
|
||||
@api.route('/otp', methods=['GET'])
|
||||
def get_otp():
|
||||
try:
|
||||
args = request.args
|
||||
if not 'address' in args.keys() or args['address'] == '':
|
||||
return jsonify({"otp": ""}), 500
|
||||
|
||||
address = args['address']
|
||||
otp = os.urandom(10).hex()
|
||||
|
||||
otp_db:Otps = Otps.query.filter_by(address=address).first()
|
||||
if otp_db is None:
|
||||
otp_db = Otps(address,otp)
|
||||
db.session.add(otp_db)
|
||||
else:
|
||||
otp_db.otp = otp
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"otp": otp}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"otp": str(e)}), 500
|
||||
|
||||
|
||||
@api.route('/login', methods=['POST'])
|
||||
def login():
|
||||
try:
|
||||
auth = request.json
|
||||
if not auth or not auth.get('address') or not auth.get('password'):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Couldn't authorize"
|
||||
})
|
||||
# User password verification
|
||||
user = Users.query.filter_by(address=auth['address']).first()
|
||||
if check_password_hash(user.password, auth['password']):
|
||||
token = jwt.encode({'address' : user.address}, app.config['SECRET_KEY'], "HS256")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": 'Login successful',
|
||||
"token": token
|
||||
})
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": 'Login failed, bad credentials'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": 'Login failed'
|
||||
})
|
||||
|
||||
|
||||
@api.route('/register', methods=['POST'])
|
||||
def register():
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# OTP sign verification for address property
|
||||
address = data['address']
|
||||
otp_db:Otps = Otps.query.filter_by(address=address).first()
|
||||
if otp_db is None or otp_db.otp is None:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Ask for otp message to sign before"
|
||||
})
|
||||
if not Web3.isChecksumAddress(address):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Address given is not valid or checksummed"
|
||||
})
|
||||
sign = data['otpSign']
|
||||
if address != web3.eth.account.recover_message(signable_message=encode_defunct(text=otp_db.otp), signature=sign):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Sign not matching with the given address"
|
||||
})
|
||||
otp_db.otp = None
|
||||
|
||||
hashed_password = generate_password_hash(data['password'], method='sha256')
|
||||
new_user = Users(address=data['address'], password=hashed_password, gadget_privatekey=None)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
token = jwt.encode({'address' : new_user.address}, app.config['SECRET_KEY'], "HS256")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": 'Register successful',
|
||||
"token": token
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": 'Register failed: ' + str(e)
|
||||
})
|
||||
|
||||
|
||||
@api.route('/product-sellers', methods=['GET'])
|
||||
def getProductSellers():
|
||||
try:
|
||||
products = DigitalProducts.query.all()
|
||||
gadgets = MaterialProducts.query.all()
|
||||
|
||||
sellers_ids = set()
|
||||
sellers_list = []
|
||||
for p in products:
|
||||
sellers_ids.add(p.seller_address)
|
||||
for g in gadgets:
|
||||
sellers_ids.add(g.seller_address)
|
||||
|
||||
for seller_id in sellers_ids:
|
||||
u = Users.query.get(seller_id)
|
||||
sellers_list.append(u.serialize)
|
||||
|
||||
return jsonify(sellers_list)
|
||||
except Exception as e:
|
||||
return jsonify([])
|
||||
|
||||
|
||||
@api.route('/set-gadget-key', methods=['POST'])
|
||||
@token_required
|
||||
def setGadgetPrivateKey(user: Users):
|
||||
payload = request.json
|
||||
|
||||
try:
|
||||
# Check if the key is a valid web3 private key
|
||||
if not web3.eth.account.from_key(payload['key']).address:
|
||||
raise Exception("Key provided is not a valid web3 private key")
|
||||
|
||||
user.gadget_privatekey = payload["key"].encode().hex()
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": 'Key updated'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": 'Key update failed: ' + str(e)
|
||||
})
|
||||
|
||||
## Digital products
|
||||
@api.route('/digitalproducts', methods=['GET'])
|
||||
@token_required
|
||||
def getDigitalProducts(user: Users):
|
||||
products = DigitalProducts.query.order_by(DigitalProducts.id.desc()).limit(30).all()
|
||||
return jsonify([p.serializeNoContent for p in products])
|
||||
|
||||
|
||||
@api.route('/digitalproducts/<id>', methods=['GET'])
|
||||
@token_required
|
||||
def getDigitalProductByIdWithContent(user: Users, id):
|
||||
product = DigitalProducts.query.get(id)
|
||||
|
||||
purchase_logs = shopContract.events.ProductSale.createFilter(fromBlock=0, toBlock='latest', argument_filters={'_id':int(id), '_to':user.address}).get_all_entries()
|
||||
|
||||
if product.seller_address == user.address or len(purchase_logs) > 0:
|
||||
return jsonify(product.serialize)
|
||||
else:
|
||||
return jsonify(DigitalProducts('', '', f'ERROR: Product not bought by {user.address} or not requested by the seller.').serialize)
|
||||
|
||||
|
||||
@api.route('/digitalproducts', methods=['POST'])
|
||||
@token_required
|
||||
def sellDigitalProduct(user: Users):
|
||||
payload = request.json
|
||||
try:
|
||||
product = DigitalProducts(
|
||||
seller=user.address,
|
||||
title=payload['title'],
|
||||
content=payload['content']
|
||||
)
|
||||
db.session.add(product)
|
||||
db.session.commit()
|
||||
return jsonify(product.serializeNoContent)
|
||||
except Exception as e:
|
||||
return jsonify(DigitalProducts('', '', f'ERROR: Product insert failed.').serialize)
|
||||
|
||||
|
||||
@api.route('/digitalproducts/<id>', methods=['DELETE'])
|
||||
@token_required
|
||||
def deleteDigitalProduct(user: Users, id):
|
||||
try:
|
||||
product = DigitalProducts.query.get(id)
|
||||
|
||||
if product.seller_address == user.address:
|
||||
db.session.delete(product)
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Product deleted."
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "You are not the owner, delete failed."
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "There was an error."
|
||||
})
|
||||
|
||||
|
||||
## Gadgets
|
||||
@api.route('/materialproducts', methods=['GET'])
|
||||
@token_required
|
||||
def getMaterialProducts(user: Users):
|
||||
products = MaterialProducts.query.order_by(MaterialProducts.id.desc()).limit(50).all()
|
||||
return jsonify([p.serializeNoContent for p in products])
|
||||
|
||||
|
||||
@api.route('/materialproducts', methods=['POST'])
|
||||
@token_required
|
||||
def publishMaterialProduct(user: Users):
|
||||
payload = request.json
|
||||
try:
|
||||
product = MaterialProducts(
|
||||
seller=user.address,
|
||||
content=payload['content'],
|
||||
seller_key=user.gadget_privatekey
|
||||
)
|
||||
db.session.add(product)
|
||||
db.session.commit()
|
||||
return jsonify(product.serializeNoContent)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify(MaterialProducts('', f'ERROR: Gadget insert failed.').serialize)
|
||||
|
||||
|
||||
@api.route('/materialproducts/<id>', methods=['POST'])
|
||||
@token_required
|
||||
def sendMaterialProductByIdWithContent(user: Users, id):
|
||||
product: MaterialProducts = MaterialProducts.query.get(id)
|
||||
payload = request.json
|
||||
|
||||
try:
|
||||
hmac_sign = payload['hmac']
|
||||
key = bytes.fromhex(product.seller_key)
|
||||
|
||||
if hmac.compare_digest(hmac_sign, hmac.digest(key, payload['destination'].encode(), 'sha256').hex()):
|
||||
res = requests.post(os.environ.get('MAILBOX_URL')+ f"/{payload['destination']}", data=product.content)
|
||||
assert res.status_code == 200
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Gadget correctly sent."
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Gadget not sent. HMAC not valid."
|
||||
})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "There was an error sending the gadget. Error: " +str(e)
|
||||
})
|
||||
|
||||
|
||||
@api.route('/materialproducts/<id>', methods=['DELETE'])
|
||||
@token_required
|
||||
def deleteMaterialProduct(user: Users, id):
|
||||
try:
|
||||
product = MaterialProducts.query.get(id)
|
||||
|
||||
if product.seller_address == user.address:
|
||||
db.session.delete(product)
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Gadget deleted."
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "You are not the owner, delete failed."
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "There was an error."
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
from flask import Flask, request, jsonify, Blueprint
|
||||
from flask_cors import CORS
|
||||
import os, sys
|
||||
from models import db
|
||||
from web3service import web3
|
||||
from api import api
|
||||
|
||||
app = Flask(__name__)
|
||||
cors = CORS(app)
|
||||
|
||||
app.config['SECRET_KEY'] = os.environ.get('API_SECRET_KEY')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('API_DATABASE_URL')
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
app.register_blueprint(api, url_prefix='/api')
|
||||
|
||||
if (__name__ == '__main__'):
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'migrate':
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
sys.exit(0)
|
||||
app.run(host=os.environ.get('API_HOST'), port=int(os.environ.get('API_PORT')))
|
|
@ -0,0 +1,22 @@
|
|||
from flask import request, jsonify, current_app as app
|
||||
from functools import wraps
|
||||
import jwt
|
||||
from models import Users
|
||||
|
||||
def token_required(f):
|
||||
@wraps(f)
|
||||
def decorator(*args, **kwargs):
|
||||
token = None
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers['Authorization'].split()[1]
|
||||
|
||||
if not token:
|
||||
return jsonify({'success': False, 'message': 'a valid token is missing'})
|
||||
try:
|
||||
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
|
||||
current_user = Users.query.get(data['address'])
|
||||
except:
|
||||
return jsonify({'success': False, 'message': 'token is invalid'})
|
||||
|
||||
return f(current_user, *args, **kwargs)
|
||||
return decorator
|
|
@ -0,0 +1,82 @@
|
|||
from flask_sqlalchemy import SQLAlchemy
|
||||
import hashlib
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
class Users(db.Model):
|
||||
address = db.Column(db.String(50), primary_key=True)
|
||||
password = db.Column(db.String(500), nullable=False)
|
||||
gadget_privatekey = db.Column(db.String(256), nullable=True)
|
||||
digital_products = db.relationship('DigitalProducts', lazy=True)
|
||||
material_products = db.relationship('MaterialProducts', lazy=True)
|
||||
|
||||
@property
|
||||
def serialize(self):
|
||||
return {
|
||||
'address': self.address,
|
||||
'gadget_key_checksum': hashlib.sha256(bytes.fromhex(self.gadget_privatekey)).hexdigest() if self.gadget_privatekey else ''
|
||||
}
|
||||
|
||||
class DigitalProducts(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
seller_address = db.Column(db.String(50), db.ForeignKey('users.address'), nullable=False)
|
||||
title = db.Column(db.String(50), nullable=False)
|
||||
content = db.Column(db.String(200), nullable=False)
|
||||
|
||||
@property
|
||||
def serialize(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'title': self.title,
|
||||
'content': self.content,
|
||||
'seller': self.seller_address
|
||||
}
|
||||
|
||||
@property
|
||||
def serializeNoContent(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'title': self.title,
|
||||
'seller': self.seller_address
|
||||
}
|
||||
|
||||
def __init__(self, seller: str, title: str, content: str):
|
||||
self.seller_address = seller
|
||||
self.title = title
|
||||
self.content = content
|
||||
|
||||
|
||||
class MaterialProducts(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
seller_address = db.Column(db.String(50), db.ForeignKey('users.address'), nullable=False)
|
||||
seller_key = db.Column(db.String(256), nullable=False)
|
||||
content = db.Column(db.String(200), nullable=False)
|
||||
|
||||
@property
|
||||
def serialize(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'content': self.content,
|
||||
'owner': self.seller_address
|
||||
}
|
||||
|
||||
@property
|
||||
def serializeNoContent(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'seller': self.seller_address
|
||||
}
|
||||
|
||||
def __init__(self, seller: str, content: str, seller_key: str):
|
||||
self.seller_address = seller
|
||||
self.content = content
|
||||
self.seller_key = seller_key
|
||||
|
||||
|
||||
class Otps(db.Model):
|
||||
address = db.Column(db.String(100), primary_key=True)
|
||||
otp = db.Column(db.String(25), nullable=True)
|
||||
|
||||
def __init__(self, address: str, otp: str):
|
||||
self.address = address
|
||||
self.otp = otp
|
|
@ -0,0 +1,52 @@
|
|||
aiohttp==3.8.3
|
||||
aiosignal==1.2.0
|
||||
async-timeout==4.0.2
|
||||
attrs==22.1.0
|
||||
base58==2.1.1
|
||||
bitarray==2.6.0
|
||||
certifi==2022.9.24
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
cytoolz==0.12.0
|
||||
eth-abi==2.2.0
|
||||
eth-account==0.5.9
|
||||
eth-hash==0.5.0
|
||||
eth-keyfile==0.5.1
|
||||
eth-keys==0.3.4
|
||||
eth-rlp==0.2.1
|
||||
eth-typing==2.3.0
|
||||
eth-utils==1.9.5
|
||||
Flask==2.2.2
|
||||
Flask-Cors==3.0.10
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
frozenlist==1.3.1
|
||||
greenlet==1.1.3
|
||||
hexbytes==0.3.0
|
||||
idna==3.4
|
||||
ipfshttpclient==0.8.0a2
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
jsonschema==4.16.0
|
||||
lru-dict==1.1.8
|
||||
MarkupSafe==2.1.1
|
||||
multiaddr==0.0.9
|
||||
multidict==6.0.2
|
||||
mysqlclient==2.1.1
|
||||
netaddr==0.8.0
|
||||
parsimonious==0.8.1
|
||||
protobuf==3.20.2
|
||||
pycryptodome==3.15.0
|
||||
PyJWT==2.5.0
|
||||
pyrsistent==0.18.1
|
||||
python-dotenv==0.21.0
|
||||
requests==2.28.1
|
||||
rlp==2.0.1
|
||||
six==1.16.0
|
||||
SQLAlchemy==1.4.41
|
||||
toolz==0.12.0
|
||||
urllib3==1.26.12
|
||||
varint==1.0.2
|
||||
web3==5.31.0
|
||||
websockets==9.1
|
||||
Werkzeug==2.2.2
|
||||
yarl==1.8.1
|
|
@ -0,0 +1,14 @@
|
|||
from web3 import Web3
|
||||
from abi_interfaces import token_interface, shop_interface
|
||||
import os
|
||||
|
||||
token_address = os.environ.get('TOKEN_CONTRACT')
|
||||
shop_address = os.environ.get('SHOP_CONTRACT')
|
||||
|
||||
API_WEB3_PROVIDER = os.environ.get('API_WEB3_PROVIDER')
|
||||
web3 = Web3(Web3.HTTPProvider(API_WEB3_PROVIDER))
|
||||
|
||||
#account = web3.eth.account.from_key(os.environ.get('TEAM_PRIVATE_KEY'))
|
||||
|
||||
tokenContract = web3.eth.contract(address=token_address, abi=token_interface)
|
||||
shopContract = web3.eth.contract(address=shop_address, abi=shop_interface)
|
|
@ -0,0 +1 @@
|
|||
*.sol linguist-language=Solidity
|
|
@ -0,0 +1,12 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
interface IM0leBank {
|
||||
|
||||
function openAccount(uint8 v, bytes32 r, bytes32 s, bytes32 hash) external;
|
||||
function isRegistered() external view returns(bool);
|
||||
function deposit(uint256 amount) external;
|
||||
function withdraw() external;
|
||||
function getBalance() external view returns (uint256);
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
interface IM0leShop {
|
||||
|
||||
event ProductSale(
|
||||
int256 indexed _id,
|
||||
address indexed _to
|
||||
);
|
||||
|
||||
function putOnSale(int256 productId, uint256 price) external;
|
||||
function buy(int256 productId) external;
|
||||
function getPriceById(int256 productId) external view returns(uint256);
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract ITokenReceiver {
|
||||
|
||||
event TokensReceived(string message);
|
||||
|
||||
function tokensReceived() virtual external {
|
||||
emit TokensReceived("Tokens received!");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
interface Im0leCoin {
|
||||
|
||||
event Sent(
|
||||
address indexed _from,
|
||||
address indexed _to,
|
||||
uint256 _value
|
||||
);
|
||||
|
||||
event Minted(
|
||||
address indexed _to,
|
||||
uint256 _value
|
||||
);
|
||||
|
||||
function name() external pure returns (string memory);
|
||||
function symbol() external pure returns (string memory);
|
||||
function granularity() external pure returns (uint256);
|
||||
function transfer(address _to, uint256 amount) external;
|
||||
function requestTransfer(address _from, uint256 amount) external;
|
||||
function balanceOf(address addr) external view returns (uint256);
|
||||
function getBalance() external view returns (uint256);
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./ITokenReceiver.sol";
|
||||
import "./IM0leBank.sol";
|
||||
|
||||
contract M0leBank is ITokenReceiver, IM0leBank {
|
||||
|
||||
address payable tokenContract;
|
||||
uint256[10] __gap; // Proxy variables reserved space
|
||||
mapping (address => uint256) _accounts;
|
||||
mapping (address => bool) _registrations;
|
||||
|
||||
// Move to the proxy
|
||||
constructor(address payable molecoin) {
|
||||
tokenContract = molecoin;
|
||||
}
|
||||
|
||||
function openAccount(uint8 v, bytes32 r, bytes32 s, bytes32 hash) external override {
|
||||
require(!_registrations[msg.sender], "user already registered");
|
||||
tokenContract.call{gas: 100000}(abi.encodeWithSignature("mintCoins(address,uint256,uint8,bytes32,bytes32,bytes32)", msg.sender, 10, v, r, s, hash));
|
||||
_registrations[msg.sender] = true;
|
||||
}
|
||||
|
||||
function isRegistered() external view override returns(bool) {
|
||||
return _registrations[msg.sender];
|
||||
}
|
||||
|
||||
function deposit(uint256 amount) external override {
|
||||
require(_registrations[msg.sender], "user not registered");
|
||||
tokenContract.call{gas: 50000}(abi.encodeWithSignature("requestTransfer(address,uint256)", msg.sender, amount));
|
||||
_accounts[msg.sender] += amount;
|
||||
}
|
||||
|
||||
function withdraw() external override {
|
||||
require(_registrations[msg.sender], "user not registered");
|
||||
require(_accounts[msg.sender] > 0, "bank account is empty");
|
||||
tokenContract.call{gas: 50000}(abi.encodeWithSignature("transfer(address,uint256)", msg.sender, _accounts[msg.sender]));
|
||||
_accounts[msg.sender] = 0;
|
||||
}
|
||||
|
||||
function getBalance() external view override returns (uint256) {
|
||||
return _accounts[msg.sender];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./ITokenReceiver.sol";
|
||||
import "./Im0leCoin.sol";
|
||||
import "./IM0leShop.sol";
|
||||
|
||||
contract M0leShop is ITokenReceiver, IM0leShop {
|
||||
|
||||
address payable tokenContract;
|
||||
uint256[10] __gap; // Proxy variables reserved space
|
||||
mapping (int256 => uint256) productPrices;
|
||||
mapping (int256 => address) productOwners;
|
||||
mapping (int256 => bool) productRegistered;
|
||||
|
||||
// move to proxy
|
||||
constructor(address payable token) {
|
||||
tokenContract = token;
|
||||
}
|
||||
|
||||
function putOnSale(int256 productId, uint256 price) external override {
|
||||
require(!productRegistered[productId], "product already registered");
|
||||
productRegistered[productId] = true;
|
||||
productOwners[productId] = msg.sender;
|
||||
productPrices[productId] = price;
|
||||
}
|
||||
|
||||
function buy(int256 productId) external override {
|
||||
require(productRegistered[productId]);
|
||||
require(productOwners[productId] != msg.sender, "you can't buy your own products");
|
||||
Im0leCoin token = Im0leCoin(tokenContract);
|
||||
require(token.balanceOf(msg.sender) >= productPrices[productId]);
|
||||
token.requestTransfer(msg.sender, productPrices[productId]);
|
||||
token.transfer(productOwners[productId], productPrices[productId]);
|
||||
emit ProductSale(productId, msg.sender);
|
||||
}
|
||||
|
||||
function getPriceById(int256 productId) external view override returns(uint256) {
|
||||
require(productRegistered[productId]);
|
||||
return productPrices[productId];
|
||||
}
|
||||
|
||||
fallback () external { }
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract Proxy {
|
||||
address payable tokenContract;
|
||||
address logic;
|
||||
address owner;
|
||||
|
||||
constructor(address payable _tokenAddr) {
|
||||
owner = tx.origin;
|
||||
tokenContract = _tokenAddr;
|
||||
}
|
||||
|
||||
function upgradeLogic(address _newLogic) public {
|
||||
require(msg.sender == owner);
|
||||
logic = _newLogic;
|
||||
}
|
||||
|
||||
function changeOwner(address _owner) public {
|
||||
require(msg.sender == owner);
|
||||
owner = _owner;
|
||||
}
|
||||
|
||||
fallback() external payable {
|
||||
address _impl = logic;
|
||||
assembly {
|
||||
let ptr := mload(0x40)
|
||||
|
||||
// (1) copy incoming call data
|
||||
calldatacopy(ptr, 0, calldatasize())
|
||||
|
||||
// (2) forward call to logic contract
|
||||
let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
|
||||
let size := returndatasize()
|
||||
|
||||
// (3) retrieve return data
|
||||
returndatacopy(ptr, 0, size)
|
||||
|
||||
// (4) forward return data back to caller
|
||||
switch result
|
||||
case 0 { revert(ptr, size) }
|
||||
default { return(ptr, size) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./ITokenReceiver.sol";
|
||||
import "./Im0leCoin.sol";
|
||||
|
||||
contract m0leCoin is Im0leCoin {
|
||||
mapping (address => uint256) private _balances;
|
||||
mapping (address => bool) private _banks;
|
||||
mapping (address => bool) private _shops;
|
||||
mapping (bytes32 => bool) private _usedOtps;
|
||||
address private owner;
|
||||
|
||||
constructor() {
|
||||
owner = tx.origin;
|
||||
_balances[tx.origin] = 10000;
|
||||
}
|
||||
|
||||
function isContract(address _addr) private view returns (bool) {
|
||||
uint32 size;
|
||||
assembly {
|
||||
size := extcodesize(_addr)
|
||||
}
|
||||
return (size > 0);
|
||||
}
|
||||
|
||||
function name() external pure override returns (string memory) {
|
||||
return "moleC0in";
|
||||
}
|
||||
|
||||
function symbol() external pure override returns (string memory) {
|
||||
return "M0L";
|
||||
}
|
||||
|
||||
function granularity() external pure override returns (uint256) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function mintCoins(address _to, uint256 amount, uint8 v, bytes32 r, bytes32 s, bytes32 hash) public {
|
||||
if (msg.sender == owner || _banks[msg.sender]) {
|
||||
if (!_usedOtps[hash] && owner == ecrecover(hash, v, r, s)) {
|
||||
_usedOtps[hash] = true;
|
||||
_balances[_to] += amount;
|
||||
emit Minted(_to, amount);
|
||||
if (isContract(_to)) {
|
||||
ITokenReceiver recvObj = ITokenReceiver(_to);
|
||||
recvObj.tokensReceived();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerBank(address _bank) public {
|
||||
require(msg.sender == owner);
|
||||
_banks[_bank] = true;
|
||||
_balances[_bank] = 1000000;
|
||||
}
|
||||
|
||||
function registerShop(address _shop) public {
|
||||
require(msg.sender == owner);
|
||||
_shops[_shop] = true;
|
||||
_balances[_shop] = 1000000;
|
||||
}
|
||||
|
||||
function transfer(address _to, uint256 amount) external override {
|
||||
if (!_banks[msg.sender] && !_shops[msg.sender]) {
|
||||
require(_balances[msg.sender] >= amount, "insufficient balance");
|
||||
_balances[msg.sender] -= amount;
|
||||
}
|
||||
if (!_banks[_to] && !_shops[_to]) {
|
||||
_balances[_to] += amount;
|
||||
}
|
||||
emit Sent(msg.sender, _to, amount);
|
||||
if (isContract(_to)) {
|
||||
ITokenReceiver recvObj = ITokenReceiver(_to);
|
||||
recvObj.tokensReceived();
|
||||
}
|
||||
}
|
||||
|
||||
function requestTransfer(address _from, uint256 amount) external override {
|
||||
require(_banks[msg.sender] || _shops[msg.sender] || msg.sender == owner);
|
||||
if (!_banks[_from] && !_shops[_from]) {
|
||||
require(_balances[_from] >= amount, "insufficient balance");
|
||||
_balances[_from] -= amount;
|
||||
}
|
||||
if (!_banks[msg.sender] && !_shops[msg.sender]) {
|
||||
_balances[msg.sender] += amount;
|
||||
}
|
||||
emit Sent(_from, msg.sender, amount);
|
||||
if (isContract(msg.sender)) {
|
||||
ITokenReceiver recvObj = ITokenReceiver(msg.sender);
|
||||
recvObj.tokensReceived();
|
||||
}
|
||||
}
|
||||
|
||||
function balanceOf(address addr) external view override returns (uint256) {
|
||||
return _balances[addr];
|
||||
}
|
||||
|
||||
function getBalance() external view override returns (uint256) {
|
||||
return _balances[msg.sender];
|
||||
}
|
||||
|
||||
fallback () external { }
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
dist
|
|
@ -0,0 +1,16 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,42 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# Stage 1, build angular
|
||||
FROM node:16 as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
# Update ETH contract address
|
||||
ARG token_contract
|
||||
ARG shop_contract
|
||||
ARG bank_contract
|
||||
ARG frontend_api_port
|
||||
RUN sed -i "s/{token_contract}/${token_contract}/g" ./src/environments/environment.prod.ts
|
||||
RUN sed -i "s/{bank_contract}/${bank_contract}/g" ./src/environments/environment.prod.ts
|
||||
RUN sed -i "s/{shop_contract}/${shop_contract}/g" ./src/environments/environment.prod.ts
|
||||
RUN sed -i "s/{frontend_api_port}/${frontend_api_port}/g" ./src/environments/environment.prod.ts
|
||||
|
||||
RUN npm run buildprod
|
||||
|
||||
# Stage 2, nginx serve
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=build /usr/src/app/dist/m0lecoin-frontend /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
|
@ -0,0 +1,27 @@
|
|||
# M0lecoinFrontend
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.3.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"m0lecoin-frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/m0lecoin-frontend",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"node_modules/primeicons/primeicons.css",
|
||||
"node_modules/primeng/resources/themes/lara-dark-purple/theme.css",
|
||||
"node_modules/primeng/resources/primeng.min.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"web3"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1.5mb",
|
||||
"maximumError": "4mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"localdev": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.dev.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "m0lecoin-frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "m0lecoin-frontend:build:development"
|
||||
},
|
||||
"localdev": {
|
||||
"browserTarget": "m0lecoin-frontend:build:localdev"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "m0lecoin-frontend:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"node_modules/primeicons/primeicons.css",
|
||||
"node_modules/primeng/resources/themes/lara-dark-purple/theme.css",
|
||||
"node_modules/primeng/resources/primeng.min.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/m0lecoin-frontend'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "m0lecoin-frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "npx ng serve --host 0.0.0.0",
|
||||
"startdev": "npx ng serve --configuration localdev --host 0.0.0.0",
|
||||
"build": "npx ng build",
|
||||
"buildprod": "npx ng build --configuration production",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^14.2.3",
|
||||
"@angular/common": "^14.2.0",
|
||||
"@angular/compiler": "^14.2.0",
|
||||
"@angular/core": "^14.2.0",
|
||||
"@angular/forms": "^14.2.0",
|
||||
"@angular/platform-browser": "^14.2.0",
|
||||
"@angular/platform-browser-dynamic": "^14.2.0",
|
||||
"@angular/router": "^14.2.0",
|
||||
"assert": "^2.0.0",
|
||||
"browser": "^0.2.6",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"primeicons": "^5.0.0",
|
||||
"primeng": "^14.1.1",
|
||||
"rxjs": "~7.5.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"tslib": "^2.3.0",
|
||||
"web3": "^1.8.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^14.2.3",
|
||||
"@angular/cli": "~14.2.3",
|
||||
"@angular/compiler-cli": "^14.2.0",
|
||||
"@types/jasmine": "~4.0.0",
|
||||
"jasmine-core": "~4.3.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"typescript": "~4.7.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<nav>
|
||||
<div class="logo" (click)="navigateTo('')">
|
||||
m0leCoin
|
||||
</div>
|
||||
|
||||
<div class="menu">
|
||||
<div class="menu-item" (click)="navigateTo('wallet')"><p>m0leWallet</p></div>
|
||||
<div class="menu-item" (click)="navigateTo('bank')"><p>m0leBank</p></div>
|
||||
<div class="menu-item" (click)="navigateTo('shop')"><p>m0leShop</p></div>
|
||||
<div class="menu-item">
|
||||
<button pButton
|
||||
[label]="(logged()) ? 'l0gout' : 'l0gin'"
|
||||
(click)="handleLogButton()"
|
||||
[disabled]="isLoginPage()"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -0,0 +1,52 @@
|
|||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background-color: var(--surface-800);
|
||||
color: var(--primary-color-text);
|
||||
user-select: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
.logo {
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
font-size: 28pt;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #C4B5FD;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: fit-content;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
&:last-child:hover {
|
||||
background: none;
|
||||
transform: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppHeaderComponent } from './app-header.component';
|
||||
|
||||
describe('AppHeaderComponent', () => {
|
||||
let component: AppHeaderComponent;
|
||||
let fixture: ComponentFixture<AppHeaderComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AppHeaderComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AppHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: './app-header.component.html',
|
||||
styleUrls: ['./app-header.component.scss']
|
||||
})
|
||||
export class AppHeaderComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private auth: AuthService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
logged() {
|
||||
return this.auth.isLogged();
|
||||
}
|
||||
|
||||
handleLogButton() {
|
||||
if (this.logged()) {
|
||||
this.auth.logout();
|
||||
this.navigateTo('');
|
||||
} else {
|
||||
this.router.navigate(['login'], { queryParams: { returnUrl: this.router.url }});
|
||||
}
|
||||
}
|
||||
|
||||
isLoginPage() {
|
||||
return this.router.url.startsWith('/login');
|
||||
}
|
||||
|
||||
navigateTo(dest: string) {
|
||||
this.router.navigate([`/${dest}`]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AuthGuardService } from './services';
|
||||
|
||||
import { BankComponent } from './bank/bank.component';
|
||||
import { ShopComponent } from './shop/shop.component';
|
||||
import { WalletComponent } from './wallet/wallet.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'wallet',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
redirectTo: 'wallet',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginComponent
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
component: WalletComponent
|
||||
},
|
||||
{
|
||||
path: 'bank',
|
||||
component: BankComponent,
|
||||
canActivate:[AuthGuardService]
|
||||
},
|
||||
{
|
||||
path: 'shop',
|
||||
component: ShopComponent,
|
||||
canActivate:[AuthGuardService]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
|
@ -0,0 +1,7 @@
|
|||
<app-header></app-header>
|
||||
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<p-toast position="bottom-center"></p-toast>
|
|
@ -0,0 +1,3 @@
|
|||
main {
|
||||
padding: 20px;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'm0lecoin-frontend'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('m0lecoin-frontend');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('.content span')?.textContent).toContain('m0lecoin-frontend app is running!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { PrimeNGConfig } from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private primengConfig: PrimeNGConfig
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.primengConfig.ripple = true;
|
||||
}
|
||||
|
||||
title = 'm0lecoin-frontend';
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { MessageService } from 'primeng/api';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppHeaderComponent } from './app-header/app-header.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { WalletComponent } from './wallet/wallet.component';
|
||||
import { BankComponent } from './bank/bank.component';
|
||||
import { ShopComponent } from './shop/shop.component';
|
||||
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { InputNumberModule } from 'primeng/inputnumber';
|
||||
import { PasswordModule } from 'primeng/password';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
AppHeaderComponent,
|
||||
WalletComponent,
|
||||
BankComponent,
|
||||
ShopComponent,
|
||||
LoginComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ButtonModule,
|
||||
ToastModule,
|
||||
InputTextModule,
|
||||
InputNumberModule,
|
||||
PasswordModule,
|
||||
DropdownModule,
|
||||
DialogModule
|
||||
],
|
||||
providers: [
|
||||
MessageService
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
|
@ -0,0 +1,31 @@
|
|||
<div id="wrapper">
|
||||
<h1>m0leBank</h1>
|
||||
<h3>A safe place where to put your savings! <span *ngIf="!registered">Register to create an account and get 10 M0L for free!</span></h3>
|
||||
|
||||
<div class="registration-button" *ngIf="!registered">
|
||||
<div><input style="width:100%" type="text" pInputText [(ngModel)]="bankOtpHash" placeholder="Team OTP Hash"/></div>
|
||||
<div style="display: flex; gap:10px; margin:10px 0">
|
||||
<input style="width:100%" type="text" pInputText [(ngModel)]="bankOtpV" placeholder="signature V"/>
|
||||
<input style="width:100%" type="text" pInputText [(ngModel)]="bankOtpR" placeholder="signature R"/>
|
||||
<input style="width:100%" type="text" pInputText [(ngModel)]="bankOtpS" placeholder="signature S"/>
|
||||
</div>
|
||||
<button pButton (click)="openBankAccount()" label="Open Account"></button>
|
||||
</div>
|
||||
|
||||
<div class="content" *ngIf="registered">
|
||||
<div class="summary">
|
||||
<div>
|
||||
<h3>Bank account balance: {{balance}} M0L</h3>
|
||||
<button pButton icon="pi pi-refresh" (click)="checkBankBalance()" label="Refresh balance"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operations">
|
||||
<h2>Bank operations</h2>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<p-inputNumber [(ngModel)]="depositValue" suffix=" M0L" label="Deposit quantity"></p-inputNumber>
|
||||
<button pButton icon="pi pi-money-bill" (click)="deposit()" label="Deposit"></button>
|
||||
<button pButton icon="pi pi-sign-out" (click)="withdraw()" label="Withdraw all"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
#wrapper {
|
||||
|
||||
.content {
|
||||
|
||||
.summary, .operations {
|
||||
margin: 30px 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BankComponent } from './bank.component';
|
||||
|
||||
describe('BankComponent', () => {
|
||||
let component: BankComponent;
|
||||
let fixture: ComponentFixture<BankComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ BankComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BankComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Web3Service } from '../services';
|
||||
|
||||
import { MessageService } from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bank',
|
||||
templateUrl: './bank.component.html',
|
||||
styleUrls: ['./bank.component.scss']
|
||||
})
|
||||
export class BankComponent implements OnInit {
|
||||
|
||||
registered = false;
|
||||
|
||||
balance = 0;
|
||||
depositValue = 0;
|
||||
|
||||
bankOtpHash = '';
|
||||
bankOtpV = '';
|
||||
bankOtpR = '';
|
||||
bankOtpS = '';
|
||||
|
||||
constructor(
|
||||
private web3Service: Web3Service,
|
||||
private messageService: MessageService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.checkBankRegistration();
|
||||
}
|
||||
|
||||
checkBankRegistration() {
|
||||
this.web3Service.checkBankRegistration()
|
||||
.then(res => {
|
||||
this.registered = res;
|
||||
if (res) {
|
||||
this.checkBankBalance();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openBankAccount() {
|
||||
if (this.bankOtpHash === '' || this.bankOtpV === '' || this.bankOtpR === '' || this.bankOtpS === '') {
|
||||
this.messageService.add({severity: 'error', summary: 'Please fill in the otp signature data!'});
|
||||
return;
|
||||
}
|
||||
|
||||
this.web3Service.openBankAccount(this.bankOtpHash, parseInt(this.bankOtpV), this.bankOtpR, this.bankOtpS)
|
||||
.then(() => this.checkBankRegistration());
|
||||
}
|
||||
|
||||
checkBankBalance() {
|
||||
if (this.registered) {
|
||||
this.web3Service.getUserBankBalance()
|
||||
.then(b => this.balance = b);
|
||||
}
|
||||
}
|
||||
|
||||
async deposit() {
|
||||
const realBalance = await this.web3Service.getUserBalance();
|
||||
if (realBalance < this.depositValue) {
|
||||
this.messageService.add({severity: 'error', summary: 'Insufficient balance!'});
|
||||
return;
|
||||
}
|
||||
this.web3Service.bankDeposit(this.depositValue)
|
||||
.then(() => this.checkBankBalance());
|
||||
}
|
||||
|
||||
withdraw() {
|
||||
this.web3Service.bankWithdraw()
|
||||
.then(() => this.checkBankBalance());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<div id="wrapper">
|
||||
<div class="form-box">
|
||||
<h1>Register / Login</h1>
|
||||
<div class="form-field">
|
||||
<p-dropdown [options]="accounts" [(ngModel)]="selectedAccount" placeholder="Address" emptyFilterMessage="Provider not linked or no accounts" emptyMessage="Provider not linked or no accounts"></p-dropdown>
|
||||
</div>
|
||||
<div class="p-inputgroup form-field">
|
||||
<span class="p-inputgroup-addon"><i class="pi pi-key"></i></span>
|
||||
<input type="password" pPassword placeholder="Password" [(ngModel)]="password" [feedback]="false">
|
||||
</div>
|
||||
<div class="form-buttons">
|
||||
<button pButton icon="pi pi-user-plus" label="Register" (click)="register()"></button>
|
||||
<div class="metamask-button" (click)="onWalletConnect()">
|
||||
<img src="assets/images/metamask_icon.webp" alt="MetaMask connect">
|
||||
</div>
|
||||
<button pButton icon="pi pi-arrow-circle-right" label="Login" (click)="login()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,41 @@
|
|||
#wrapper {
|
||||
|
||||
.form-box {
|
||||
background-color: var(--surface-800);
|
||||
padding: 25px;
|
||||
border-radius: 10pt;
|
||||
width: 600px;
|
||||
margin: 50px auto;
|
||||
|
||||
h1 {
|
||||
color: var(--primary-color-text);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin: 20px 0;
|
||||
|
||||
button, div {
|
||||
&:hover {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.metamask-button {
|
||||
padding: 2px 4px;
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 45px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { AuthService, Web3Service } from '../services';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
|
||||
accounts: any[] = [];
|
||||
selectedAccount: any;
|
||||
password = '';
|
||||
|
||||
returnUrl: string;
|
||||
|
||||
loginEventSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private auth: AuthService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private web3Service: Web3Service
|
||||
) {
|
||||
this.returnUrl = this.route.snapshot.queryParams['returnUrl'];
|
||||
this.loginEventSubscription = this.auth.loginEvent.subscribe(logged => {
|
||||
if (logged) {
|
||||
this.router.navigateByUrl(this.returnUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
|
||||
register() {
|
||||
this.auth.register(this.selectedAccount, this.password);
|
||||
}
|
||||
|
||||
login() {
|
||||
this.auth.login(this.selectedAccount, this.password);
|
||||
}
|
||||
|
||||
async onWalletConnect() {
|
||||
this.accounts = await this.web3Service.connectAccount();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './request.models';
|
||||
export * from './response.models';
|
||||
export * from './window.extension';
|
|
@ -0,0 +1,9 @@
|
|||
export interface LoginRequest {
|
||||
address: string,
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest extends LoginRequest {
|
||||
otp: string,
|
||||
otpSign: string
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* API RESPONSE MODEL OBJECTS
|
||||
*
|
||||
*/
|
||||
|
||||
export interface GenericResponse {
|
||||
success: boolean,
|
||||
message: string
|
||||
}
|
||||
export interface LoginResponse extends GenericResponse {
|
||||
token: string
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: number,
|
||||
title: string,
|
||||
content: string,
|
||||
seller: string,
|
||||
price: number
|
||||
}
|
||||
|
||||
export interface Gadget {
|
||||
id: number,
|
||||
content: string,
|
||||
seller: string
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export type Web3EnabledWindow = Window & {ethereum?: any};
|
|
@ -0,0 +1,139 @@
|
|||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { lastValueFrom, timeout } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Gadget, GenericResponse, Product } from '../models';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private auth: AuthService
|
||||
) { }
|
||||
|
||||
async getShopProducts() {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.get<Product[]>(`${environment.baseUrl}/digitalproducts`, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async getShopProductContent(productId: number) {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.get<Product>(`${environment.baseUrl}/digitalproducts/${productId}`, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async sellShopProduct(product: Product) {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.post<Product>(`${environment.baseUrl}/digitalproducts`, product, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async revertSellShopProduct(productId: number) {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.delete<GenericResponse>(`${environment.baseUrl}/digitalproducts/${productId}`, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async getGadgets() {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.get<Gadget[]>(`${environment.baseUrl}/materialproducts`, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async publishGadget(gadget: Gadget) {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.post<Gadget>(`${environment.baseUrl}/materialproducts`, gadget, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async sendGadgetToMailbox(mailbox_dest: string, hmac: string, gadgetId: number) {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.post<GenericResponse>(`${environment.baseUrl}/materialproducts/${gadgetId}`, {
|
||||
destination: mailbox_dest,
|
||||
hmac: hmac
|
||||
}, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateGagdetKey(key: string) {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.post<GenericResponse>(`${environment.baseUrl}/set-gadget-key`, {
|
||||
key: key
|
||||
}, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
async deleteGadget(gadgetId: number) {
|
||||
const headers = new HttpHeaders({
|
||||
'Authorization': `Bearer ${this.auth.getToken()}`
|
||||
})
|
||||
const res = await lastValueFrom(
|
||||
this.http.delete<GenericResponse>(`${environment.baseUrl}/materialproducts/${gadgetId}`, {
|
||||
headers: headers
|
||||
}
|
||||
).pipe(timeout(5000))
|
||||
);
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '.';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuardService implements CanActivate {
|
||||
|
||||
constructor(
|
||||
private auth: AuthService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
||||
if (!this.auth.isLogged()) {
|
||||
this.router.navigate(['login'], { queryParams: { returnUrl: state.url }});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, throwError, Subject, Subscription, lastValueFrom } from 'rxjs';
|
||||
import { catchError, retry } from 'rxjs/operators';
|
||||
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
import {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest
|
||||
} from 'src/app/models';
|
||||
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { Web3Service } from './web3.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private userAddress: string | null;
|
||||
private logged = false;
|
||||
private web3Linked = false
|
||||
|
||||
private loginEventSource = new Subject<boolean>();
|
||||
loginEvent = this.loginEventSource.asObservable();
|
||||
|
||||
web3subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private web3Service: Web3Service,
|
||||
private messageService: MessageService
|
||||
) {
|
||||
this.web3subscription = this.web3Service.linkStatus.subscribe(
|
||||
(linked) => {
|
||||
console.log(linked);
|
||||
this.web3Linked = linked;
|
||||
if (!linked) {
|
||||
this.logged = false;
|
||||
}
|
||||
}
|
||||
)
|
||||
this.userAddress = '';
|
||||
if (this.web3Linked && localStorage.getItem('jwt_token') && localStorage.getItem('user_address')) {
|
||||
this.logged = true;
|
||||
this.loginEventSource.next(true);
|
||||
this.userAddress = localStorage.getItem('user_address');
|
||||
}
|
||||
}
|
||||
|
||||
async register(address: string, password: string) {
|
||||
if (!address || !password) {
|
||||
return;
|
||||
}
|
||||
const otpRequest = this.http.get<{otp: string}>(`${environment.baseUrl}/otp`, {
|
||||
params: {
|
||||
"address": address
|
||||
}
|
||||
});
|
||||
const otp = (await lastValueFrom(otpRequest)).otp;
|
||||
const otpSign = await this.web3Service.sign(otp, address);
|
||||
if (otpSign !== '') {
|
||||
const body: RegisterRequest = {
|
||||
address, password, otp, otpSign
|
||||
};
|
||||
this.http.post<LoginResponse>(`${environment.baseUrl}/register`, body).subscribe(
|
||||
(data) => {
|
||||
if (data.success) {
|
||||
this.logged = true;
|
||||
this.userAddress = address;
|
||||
localStorage.setItem('jwt_token', data.token);
|
||||
localStorage.setItem('user_address', address);
|
||||
this.messageService.add({severity: 'success', summary: 'Registration successful.'});
|
||||
this.loginEventSource.next(true);
|
||||
if (this.web3Service.web3) {
|
||||
this.web3Service.web3.eth.defaultAccount = this.userAddress;
|
||||
}
|
||||
} else {
|
||||
this.messageService.add({severity: 'error', summary: 'Registration failed.', detail: data.message});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.messageService.add({severity: 'error', summary: 'Registration failed.', detail: err});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
login(address: string, password: string) {
|
||||
const body: LoginRequest = {
|
||||
address, password
|
||||
};
|
||||
this.http.post<LoginResponse>(`${environment.baseUrl}/login`, body).subscribe(
|
||||
(data) => {
|
||||
if (data.success) {
|
||||
this.logged = true;
|
||||
this.userAddress = address;
|
||||
localStorage.setItem('jwt_token', data.token);
|
||||
localStorage.setItem('user_address', address);
|
||||
this.messageService.add({severity: 'success', summary: 'Login successful.'});
|
||||
this.loginEventSource.next(true);
|
||||
if (this.web3Service.web3) {
|
||||
this.web3Service.web3.eth.defaultAccount = this.userAddress;
|
||||
}
|
||||
} else {
|
||||
this.messageService.add({severity: 'error', summary: 'Login failed.', detail: data.message});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.messageService.add({severity: 'error', summary: 'Login failed.', detail: err});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isLogged(): boolean {
|
||||
return this.logged;
|
||||
}
|
||||
|
||||
isweb3Linked(): boolean{
|
||||
return this.web3Linked;
|
||||
}
|
||||
|
||||
getUserAddress(): string {
|
||||
if (this.userAddress) {
|
||||
return this.userAddress;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return localStorage.getItem('jwt_token');
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.logged = false;
|
||||
this.userAddress = '';
|
||||
localStorage.removeItem('jwt_token');
|
||||
localStorage.removeItem('user_address');
|
||||
this.loginEventSource.next(false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from './api.service';
|
||||
export * from './auth.service';
|
||||
export * from './auth-guard.service';
|
||||
export * from './web3.service';
|
|
@ -0,0 +1,184 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Subject } from 'rxjs';
|
||||
import Web3 from 'web3';
|
||||
import { Product, Web3EnabledWindow } from '../models';
|
||||
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { tokenInterface } from 'src/assets/json-interfaces/m0leCoin';
|
||||
import { bankInterface } from 'src/assets/json-interfaces/m0leBank';
|
||||
import { shopInterface } from 'src/assets/json-interfaces/m0leShop';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class Web3Service {
|
||||
private window: Web3EnabledWindow;
|
||||
private accounts: any[] | undefined;
|
||||
web3: Web3 | undefined;
|
||||
private tokenContract: any;
|
||||
private bankContract: any;
|
||||
private shopContract: any;
|
||||
|
||||
private linkStatusSource = new Subject<boolean>();
|
||||
linkStatus = this.linkStatusSource.asObservable();
|
||||
|
||||
constructor(
|
||||
private messageService: MessageService
|
||||
) {
|
||||
this.window = window;
|
||||
if (!this.window.ethereum) {
|
||||
this.messageService.add({severity: 'error', summary: 'MetaMask not installed.'});
|
||||
return;
|
||||
} else {
|
||||
this.window.ethereum.on('disconnect', () => {
|
||||
this.linkStatusSource.next(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async connectAccount() {
|
||||
try {
|
||||
// Unlock MetaMask and connect user accounts
|
||||
await this.window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
} catch (err) {
|
||||
// something on error
|
||||
this.messageService.add({severity: 'error', summary: 'Provider linking error.'});
|
||||
return [];
|
||||
}
|
||||
this.messageService.add({severity: 'success', summary: 'Provider linked.'});
|
||||
// Wrap provider with web3 convenience library
|
||||
this.web3 = new Web3(this.window.ethereum);
|
||||
this.tokenContract = new this.web3.eth.Contract(tokenInterface, environment.tokenContractAddress);
|
||||
this.bankContract = new this.web3.eth.Contract(bankInterface, environment.bankContractAddress);
|
||||
this.shopContract = new this.web3.eth.Contract(shopInterface, environment.shopContractAddress)
|
||||
this.linkStatusSource.next(true);
|
||||
this.accounts = await this.web3.eth.getAccounts();
|
||||
return this.accounts;
|
||||
}
|
||||
|
||||
async sign(data: string, address: string) {
|
||||
try {
|
||||
if (this.web3) {
|
||||
return await this.web3.eth.personal.sign(data, address, '');
|
||||
}
|
||||
this.messageService.add({severity: 'error', summary: 'Sign failed. Web3 not initialized'});
|
||||
return '';
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Sign failed.'});
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getUserBalance() {
|
||||
if (this.tokenContract) {
|
||||
try {
|
||||
const balance = await this.tokenContract.methods.getBalance().call({from: this.web3?.eth.defaultAccount});
|
||||
return balance;
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Check balance failed.'});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async transferTokens(dest: string, amount: number) {
|
||||
if (this.tokenContract) {
|
||||
try {
|
||||
await this.tokenContract.methods.transfer(dest, amount).send({from: this.web3?.eth.defaultAccount});
|
||||
this.messageService.add({severity: 'success', summary: 'Transfer complete.'});
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Transfer failed.'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkBankRegistration() {
|
||||
if (this.bankContract) {
|
||||
try {
|
||||
const res = await this.bankContract.methods.isRegistered().call({from: this.web3?.eth.defaultAccount});
|
||||
return res;
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Bank account check failed.'});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async openBankAccount(mhash: string, v: number, r: string, s: string) {
|
||||
if (this.bankContract) {
|
||||
try {
|
||||
await this.bankContract.methods.openAccount(v,r,s,mhash).send({from: this.web3?.eth.defaultAccount});
|
||||
this.messageService.add({severity: 'success', summary: 'Account opened.'});
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Account creation failed.'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getUserBankBalance() {
|
||||
if (this.bankContract) {
|
||||
try {
|
||||
const bal = await this.bankContract.methods.getBalance().call({from: this.web3?.eth.defaultAccount});
|
||||
return bal;
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Check bank balance failed.'});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async bankDeposit(amount: number) {
|
||||
if (this.bankContract) {
|
||||
try {
|
||||
await this.bankContract.methods.deposit(amount).send({from: this.web3?.eth.defaultAccount});
|
||||
this.messageService.add({severity: 'success', summary: 'Deposited.'});
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Deposit failed.'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async bankWithdraw() {
|
||||
if (this.bankContract) {
|
||||
try {
|
||||
await this.bankContract.methods.withdraw().send({from: this.web3?.eth.defaultAccount});
|
||||
this.messageService.add({severity: 'success', summary: 'Withdrawn.'});
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Withdrawal failed.'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getProductPrice(productId: number) {
|
||||
if (this.shopContract) {
|
||||
try {
|
||||
const price = await this.shopContract.methods.getPriceById(productId).call({from: this.web3?.eth.defaultAccount});
|
||||
return price;
|
||||
} catch (err) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async shopBuyProduct(productId: number) {
|
||||
if (this.shopContract) {
|
||||
try {
|
||||
await this.shopContract.methods.buy(productId).send({from: this.web3?.eth.defaultAccount});
|
||||
} catch (err){
|
||||
this.messageService.add({severity: 'error', summary: 'Buy of product id-' + productId.toString() + ' failed.'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async shopPutOnSale(product: Product) {
|
||||
if (this.shopContract) {
|
||||
try {
|
||||
await this.shopContract.methods.putOnSale(product.id, product.price).send({from: this.web3?.eth.defaultAccount});
|
||||
} catch (err) {
|
||||
this.messageService.add({severity: 'error', summary: 'Put on sale of product -' + product.id.toString() + ' failed.'});
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<div id="wrapper">
|
||||
<h1>m0leShop</h1>
|
||||
<p>
|
||||
Our personal semi-decentralized e-commerce! You can sell and buy digital products or share your personal gadgets with the world.
|
||||
<br>
|
||||
All digital products can be bought using M0L tokens, of course you must have a sufficient balance on your wallet. Consider
|
||||
also using our bank to get 10 M0L for free!
|
||||
<br>
|
||||
If you want to send a gadget to someone just add the email of the receiver in the dedicated section, click 'Send' and we will cover all
|
||||
for you without any other overheads.
|
||||
</p>
|
||||
|
||||
<div class="tab-selector">
|
||||
<div [class]="(mode === 'buy') ? 'selected-tab' : ''" (click)="changeMode('buy')">
|
||||
<p>Buy</p>
|
||||
</div>
|
||||
<div [class]="(mode === 'sell') ? 'selected-tab' : ''" (click)="changeMode('sell')">
|
||||
<p>Sell</p>
|
||||
</div>
|
||||
<div [class]="(mode === 'gadget') ? 'selected-tab' : ''" (click)="changeMode('gadget')">
|
||||
<p>Gadgets</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="mode !== 'gadget'" class="user-balance-indicator">
|
||||
Your balance: {{userBalance}} M0L
|
||||
</div>
|
||||
|
||||
<div class="sell-box" *ngIf="mode === 'sell'">
|
||||
<h2>Sell product</h2>
|
||||
<div class="form">
|
||||
<div><input type="text" pInputText [(ngModel)]="productTitle" placeholder="Title"/></div>
|
||||
<div><input type="text" pInputText [(ngModel)]="productContent" placeholder="Content"/></div>
|
||||
<div style="display: flex; gap: 5px">
|
||||
<p-inputNumber [(ngModel)]="productPrice" suffix=" M0L" label="Price"></p-inputNumber>
|
||||
<button pButton icon="pi pi-send" (click)="sellProduct()" label="Sell"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buy-grid" *ngIf="mode === 'buy'">
|
||||
<div class="empty" *ngIf="products.length === 0">Sorry, no products available at the moment...</div>
|
||||
<div class="product-list" *ngIf="products.length > 0">
|
||||
<table>
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>title</th>
|
||||
<th>seller</th>
|
||||
<th>price</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="product-row" *ngFor="let p of products">
|
||||
<td>{{p.id}}</td>
|
||||
<td>{{p.title}}</td>
|
||||
<td>{{p.seller}}</td>
|
||||
<td>{{p.price !== -1 ? p.price : '--'}} M0L</td>
|
||||
<td>
|
||||
<button pButton (click)="clickProduct(p.id, p.seller)" [label]="(p.seller === userAddress) ? 'Delete' : 'Buy'" [disabled]="(p.seller === userAddress || p.price === -1) ? false : (userBalance < p.price)"></button>
|
||||
<button pButton (click)="seeProduct(p.id)" label="Open"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p-dialog [title]="'Product: ' + selectedProductTitle" [(visible)]="showSelectedProduct" [style]="{width: '30vw'}" [modal]="true" [draggable]="false" [resizable]="false">
|
||||
{{selectedProductContent}}
|
||||
</p-dialog>
|
||||
</div>
|
||||
|
||||
<div class="gadget-manager" *ngIf="mode === 'gadget'">
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||||
<div class="gadget-manager-box">
|
||||
<h2>Gadget manager options</h2>
|
||||
<div style="display: flex; gap: 5px">
|
||||
<input style="width:100%" type="text" pInputText [(ngModel)]="gadgetKey" placeholder="Gadget private key"/>
|
||||
<button pButton (click)="updateGadgetKey()" label="Update key" [disabled]="gadgetKey === ''"></button>
|
||||
</div>
|
||||
<div>
|
||||
<input style="width:100%" type="text" pInputText [(ngModel)]="receiverMailbox" placeholder="Mailbox destination"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gadget-manager-box">
|
||||
<h2>New gadget</h2>
|
||||
<div style="display: flex; gap: 5px">
|
||||
<input style="width:100%" type="text" pInputText [(ngModel)]="gadgetContent" placeholder="New gadget content"/>
|
||||
<button pButton (click)="publishGadget()" label="Create gadget" [disabled]="gadgetContent === ''"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty" *ngIf="gadgets.length === 0">You are not giving away any gadget, create one!</div>
|
||||
<div class="gadgets-list" *ngIf="gadgets.length > 0">
|
||||
<table>
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>seller</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="gadget-row" *ngFor="let g of gadgets">
|
||||
<td>{{g.id}}</td>
|
||||
<td>{{g.seller}}</td>
|
||||
<td>
|
||||
<button pButton (click)="sendGadget(g.id)" [disabled]="gadgetKey === '' || receiverMailbox === ''" label="Send"></button>
|
||||
<button pButton (click)="deleteGadget(g.id)" *ngIf="g.seller === userAddress" label="Delete"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p-dialog [title]="'Product: ' + selectedProductTitle" [(visible)]="showSelectedProduct" [style]="{width: '30vw'}" [modal]="true" [draggable]="false" [resizable]="false">
|
||||
{{selectedProductContent}}
|
||||
</p-dialog>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,139 @@
|
|||
#wrapper {
|
||||
|
||||
.user-balance-indicator {
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.tab-selector {
|
||||
width: 350px;
|
||||
height: 50px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px solid var(--primary-color);
|
||||
display: flex;
|
||||
margin: 30px auto;
|
||||
|
||||
div {
|
||||
border-right: 2px solid var(--primary-color);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover p {
|
||||
transform: scale(0.80);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.selected-tab {
|
||||
background: var(--primary-color);
|
||||
color: var(--primary-color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.buy-grid {
|
||||
|
||||
.product-list {
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
border-bottom: 2px solid var(--surface-800);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 0;
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid var(--surface-500);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gadget-manager {
|
||||
|
||||
.gadget-manager-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gadgets-list {
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
border-bottom: 2px solid var(--surface-800);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 0;
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid var(--surface-500);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sell-box {
|
||||
|
||||
.form {
|
||||
background: var(--surface-800);
|
||||
border-radius: var(--border-radius);
|
||||
width: fit-content;
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
div input {
|
||||
width: 450px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ShopComponent } from './shop.component';
|
||||
|
||||
describe('ShopComponent', () => {
|
||||
let component: ShopComponent;
|
||||
let fixture: ComponentFixture<ShopComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ShopComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShopComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,238 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ApiService, AuthService, Web3Service } from '../services';
|
||||
|
||||
import { MessageService } from 'primeng/api';
|
||||
|
||||
import { Product, Gadget, GenericResponse } from '../models';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shop',
|
||||
templateUrl: './shop.component.html',
|
||||
styleUrls: ['./shop.component.scss']
|
||||
})
|
||||
export class ShopComponent implements OnInit {
|
||||
|
||||
mode: 'buy' | 'sell' | 'gadget' = 'buy';
|
||||
|
||||
userBalance = 0;
|
||||
userAddress = '';
|
||||
|
||||
// Sell mode state variables
|
||||
productTitle = '';
|
||||
productContent = '';
|
||||
productPrice = 0;
|
||||
price = 0;
|
||||
|
||||
// Buy mode state variables
|
||||
products: Product[] = [];
|
||||
selectedProductId = 0;
|
||||
selectedProductTitle = '';
|
||||
selectedProductContent = '';
|
||||
selectedProductSeller = '';
|
||||
showSelectedProduct = false;
|
||||
|
||||
// Gadget mode state variables
|
||||
gadgets: Gadget[] = [];
|
||||
gadgetContent = '';
|
||||
receiverMailbox = '';
|
||||
gadgetKey = '';
|
||||
|
||||
constructor(
|
||||
private web3Service: Web3Service,
|
||||
private auth: AuthService,
|
||||
private api: ApiService,
|
||||
private messageService: MessageService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getUserBalance();
|
||||
this.userAddress = this.auth.getUserAddress();
|
||||
if (this.mode === 'buy') {
|
||||
this.fetchProducts();
|
||||
} else if (this.mode === 'gadget') {
|
||||
this.fetchGadgets();
|
||||
}
|
||||
}
|
||||
|
||||
getUserBalance() {
|
||||
this.web3Service.getUserBalance()
|
||||
.then(b => this.userBalance = b);
|
||||
}
|
||||
|
||||
fetchProducts() {
|
||||
this.api.getShopProducts()
|
||||
.then(
|
||||
async (res: Product[]) => {
|
||||
this.products = res;
|
||||
for (let i = 0; i < this.products.length; i++) {
|
||||
this.products[i].price = parseInt(await this.web3Service.getProductPrice(this.products[i].id));
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
this.messageService.add({severity: 'error', summary: 'Product fetch failed.', detail: err});
|
||||
});
|
||||
}
|
||||
|
||||
clickProduct(productId: number, seller: string) {
|
||||
if (seller === this.userAddress) {
|
||||
this.deleteProduct(productId);
|
||||
} else {
|
||||
this.buyProduct(productId);
|
||||
}
|
||||
}
|
||||
|
||||
buyProduct(productId: number) {
|
||||
this.web3Service.shopBuyProduct(productId)
|
||||
.then(() => this.getUserBalance());
|
||||
}
|
||||
|
||||
deleteProduct(productId: number) {
|
||||
this.api.revertSellShopProduct(productId)
|
||||
.then((res: GenericResponse) => {
|
||||
if (!res.success) {
|
||||
this.messageService.add({severity: 'error', summary: 'Failed reverting id-' + productId.toString() +'.'});
|
||||
} else {
|
||||
this.fetchProducts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
seeProduct(productId: number) {
|
||||
this.api.getShopProductContent(productId)
|
||||
.then((p: Product) => {
|
||||
this.selectedProductId = p.id;
|
||||
this.selectedProductTitle = p.title;
|
||||
this.selectedProductContent = p.content;
|
||||
this.selectedProductSeller = p.seller;
|
||||
this.showSelectedProduct = true;
|
||||
})
|
||||
.catch(err => this.messageService.add({severity: 'error', summary: 'Product open failed.', detail: err}))
|
||||
}
|
||||
|
||||
sellProduct() {
|
||||
const product: Product = {
|
||||
id: 0,
|
||||
title: this.productTitle,
|
||||
content: this.productContent,
|
||||
seller: '',
|
||||
price: this.productPrice
|
||||
}
|
||||
this.api.sellShopProduct(product)
|
||||
.then((p: Product) => {
|
||||
if (p.id == null) {
|
||||
this.messageService.add({severity: 'error', summary: 'Product sell failed.', detail: product.content});
|
||||
return;
|
||||
}
|
||||
p.price = product.price;
|
||||
this.web3Service.shopPutOnSale(p)
|
||||
.then(() => {
|
||||
this.messageService.add({severity: 'success', summary: 'Product put on sale with id ' + p.id.toString() +'.'});
|
||||
this.clearNewProduct();
|
||||
})
|
||||
.catch(() => {
|
||||
this.messageService.add({severity: 'error', summary: 'Put on sale error id-' + p.id.toString() +'.'});
|
||||
this.deleteProduct(p.id);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
clearNewProduct() {
|
||||
this.productTitle = '';
|
||||
this.productContent = '';
|
||||
this.productPrice = 0;
|
||||
this.gadgetContent = '';
|
||||
this.receiverMailbox = '';
|
||||
}
|
||||
|
||||
fetchGadgets() {
|
||||
this.api.getGadgets()
|
||||
.then(
|
||||
async (res: Gadget[]) => {
|
||||
this.gadgets = res;
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
this.messageService.add({severity: 'error', summary: 'Gadget fetch failed.', detail: err});
|
||||
});
|
||||
}
|
||||
|
||||
publishGadget() {
|
||||
const gadget: Gadget = {
|
||||
id: 0,
|
||||
content: this.gadgetContent,
|
||||
seller: ''
|
||||
}
|
||||
this.api.publishGadget(gadget)
|
||||
.then((g: Gadget) => {
|
||||
if (g.id == null) {
|
||||
this.messageService.add({severity: 'error', summary: 'Gadget publish failed.', detail: g.content});
|
||||
return;
|
||||
}
|
||||
this.messageService.add({severity: 'success', summary: 'Gadget published with id ' + g.id.toString() +'.'});
|
||||
this.clearNewProduct();
|
||||
this.fetchGadgets();
|
||||
})
|
||||
}
|
||||
|
||||
sendGadget(gadgetId: number) {
|
||||
let hmac = crypto
|
||||
.createHmac('sha256', this.gadgetKey)
|
||||
.update(this.receiverMailbox)
|
||||
.digest('hex')
|
||||
this.api.sendGadgetToMailbox(this.receiverMailbox, hmac, gadgetId)
|
||||
.then((res: GenericResponse) => {
|
||||
if (!res.success) {
|
||||
this.messageService.add({severity: 'error', summary: 'Failed sending gadget id-' + gadgetId.toString() +'.'});
|
||||
} else {
|
||||
this.messageService.add({severity: 'success', summary: 'Gadget sent to specified mailbox.'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateGadgetKey() {
|
||||
this.api.updateGagdetKey(this.gadgetKey)
|
||||
.then((res: GenericResponse) => {
|
||||
if (!res.success) {
|
||||
this.messageService.add({severity: 'error', summary: 'Failed updating gadget management key .'});
|
||||
} else {
|
||||
this.messageService.add({severity: 'success', summary: 'Profile gadget key updated.'});
|
||||
this.fetchGadgets();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteGadget(gadgetId: number) {
|
||||
this.api.deleteGadget(gadgetId)
|
||||
.then((res: GenericResponse) => {
|
||||
if (!res.success) {
|
||||
this.messageService.add({severity: 'error', summary: 'Failed deleted gadget id-' + gadgetId.toString() +'.'});
|
||||
} else {
|
||||
this.messageService.add({severity: 'success', summary: 'Gadget deleted.'});
|
||||
this.fetchGadgets();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changeMode(_mode: 'buy' | 'sell' | 'gadget') {
|
||||
this.mode = _mode;
|
||||
|
||||
if (_mode === 'buy') {
|
||||
this.fetchProducts();
|
||||
this.getUserBalance();
|
||||
}
|
||||
|
||||
if (_mode === 'gadget') {
|
||||
this.clearNewProduct();
|
||||
this.fetchGadgets();
|
||||
}
|
||||
|
||||
if (_mode ==='sell') {
|
||||
this.clearNewProduct();
|
||||
this.getUserBalance();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<div id="wrapper">
|
||||
<div class="banner-section">
|
||||
<img src="assets/images/m0leCoin_logo_white.png" alt="m0leWallet logo">
|
||||
<div class="description">
|
||||
A wallet interface for our fresh new ERC20-alike token.<br>Now in Typescript and with OTP signing for added safety!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="content">
|
||||
<h1>Wallet</h1>
|
||||
<div class="not-logged" *ngIf="!logged">
|
||||
Please connect MetaMask and login to use your wallet.
|
||||
</div>
|
||||
|
||||
<div class="logged" *ngIf="logged">
|
||||
<div class="info-section">
|
||||
<p>Address: {{userAddress}}</p>
|
||||
<div>
|
||||
<p>Token balance: {{tokenBalance}} M0L</p>
|
||||
<button pButton icon="pi pi-refresh" (click)="checkBalance()" label="Refresh balance"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="transfer-section">
|
||||
<h2>Transfer tokens</h2>
|
||||
<div><input type="text" pInputText [(ngModel)]="transferDestinationAddress" placeholder="Destination address"/></div>
|
||||
<div style="display: flex; gap: 5px">
|
||||
<p-inputNumber [(ngModel)]="transferQuantity" suffix=" M0L" label="Quantity"></p-inputNumber>
|
||||
<button pButton icon="pi pi-send" (click)="transfer()" label="Send"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,50 @@
|
|||
#wrapper {
|
||||
|
||||
.banner-section {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
margin: 50px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 25px 2%;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logged {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.transfer-section {
|
||||
width: 50%;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WalletComponent } from './wallet.component';
|
||||
|
||||
describe('WalletComponent', () => {
|
||||
let component: WalletComponent;
|
||||
let fixture: ComponentFixture<WalletComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ WalletComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WalletComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AuthService, Web3Service } from '../services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-wallet',
|
||||
templateUrl: './wallet.component.html',
|
||||
styleUrls: ['./wallet.component.scss']
|
||||
})
|
||||
export class WalletComponent implements OnInit {
|
||||
|
||||
loginEventSubscription: Subscription;
|
||||
logged = false;
|
||||
|
||||
userAddress = '';
|
||||
tokenBalance = 0;
|
||||
transferDestinationAddress = '';
|
||||
transferQuantity = 0;
|
||||
|
||||
constructor(
|
||||
private auth: AuthService,
|
||||
private web3Service: Web3Service
|
||||
) {
|
||||
this.userAddress = this.auth.getUserAddress();
|
||||
this.loginEventSubscription = this.auth.loginEvent.subscribe(logged => {
|
||||
this.logged = logged;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.logged = this.auth.isLogged();
|
||||
if (this.logged) {
|
||||
this.checkBalance();
|
||||
}
|
||||
}
|
||||
|
||||
checkBalance() {
|
||||
this.web3Service.getUserBalance()
|
||||
.then(b => this.tokenBalance = b);
|
||||
}
|
||||
|
||||
transfer() {
|
||||
this.web3Service.transferTokens(this.transferDestinationAddress, this.transferQuantity)
|
||||
.then(() => this.checkBalance());
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,4 @@
|
|||
import { AbiItem } from 'web3-utils';
|
||||
|
||||
// Abi Interface m0leCoin contract
|
||||
export const bankInterface: AbiItem[] = [{"inputs": [{"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "deposit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "getBalance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "isRegistered", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint8", "name": "v", "type": "uint8"}, {"internalType": "bytes32", "name": "r", "type": "bytes32"}, {"internalType": "bytes32", "name": "s", "type": "bytes32"}, {"internalType": "bytes32", "name": "hash", "type": "bytes32"}], "name": "openAccount", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function"}];
|
|
@ -0,0 +1,4 @@
|
|||
import { AbiItem } from 'web3-utils';
|
||||
|
||||
// Abi Interface m0leCoin contract
|
||||
export const tokenInterface: AbiItem[] = [{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "_to", "type": "address"}, {"indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256"}], "name": "Minted", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "_from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "_to", "type": "address"}, {"indexed": false, "internalType": "uint256", "name": "_value", "type": "uint256"}], "name": "Sent", "type": "event"}, {"inputs": [{"internalType": "address", "name": "addr", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "getBalance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "granularity", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "pure", "type": "function"}, {"inputs": [], "name": "name", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "pure", "type": "function"}, {"inputs": [{"internalType": "address", "name": "_from", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "requestTransfer", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "pure", "type": "function"}, {"inputs": [{"internalType": "address", "name": "_to", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "transfer", "outputs": [], "stateMutability": "nonpayable", "type": "function"}];
|
|
@ -0,0 +1,4 @@
|
|||
import { AbiItem } from 'web3-utils';
|
||||
|
||||
// Abi Interface m0leCoin contract
|
||||
export const shopInterface: AbiItem[] = [{"anonymous": false, "inputs": [{"indexed": true, "internalType": "int256", "name": "_id", "type": "int256"}, {"indexed": true, "internalType": "address", "name": "_to", "type": "address"}], "name": "ProductSale", "type": "event"}, {"inputs": [{"internalType": "int256", "name": "productId", "type": "int256"}], "name": "buy", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "int256", "name": "productId", "type": "int256"}], "name": "getPriceById", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "int256", "name": "productId", "type": "int256"}, {"internalType": "uint256", "name": "price", "type": "uint256"}], "name": "putOnSale", "outputs": [], "stateMutability": "nonpayable", "type": "function"}];
|
|
@ -0,0 +1,21 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
baseUrl: `${location.protocol}//${location.hostname}:8000/api`,
|
||||
tokenContractAddress: '0x3d554d562e4D4a7E7E88674FE64846200737ed21',
|
||||
bankContractAddress: '0xCC715Bb9EC9fa3F35B40528089B980C7c4c8BDDf',
|
||||
shopContractAddress: '0xE3Ed7978A2EFfD0A932cE0599eAd89fBECa86fA7'
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: true,
|
||||
baseUrl: `${location.protocol}//${location.hostname}:{frontend_api_port}/api`,
|
||||
tokenContractAddress: '{token_contract}',
|
||||
bankContractAddress: '{bank_contract}',
|
||||
shopContractAddress: '{shop_contract}'
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
|
@ -0,0 +1,20 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
baseUrl: `${location.protocol}//${location.hostname}:{frontend_api_port}/api`,
|
||||
tokenContractAddress: '{token_contract}',
|
||||
bankContractAddress: '{bank_contract}',
|
||||
shopContractAddress: '{shop_contract}'
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
Binary file not shown.
After Width: | Height: | Size: 948 B |
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>m0leCoin</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,12 @@
|
|||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
import * as process from 'process';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
window.process = process;
|
||||
(window as any).global = window;
|
||||
global.Buffer = global.Buffer || Buffer;
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
|
@ -0,0 +1,9 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap');
|
||||
|
||||
html, body, h1, h2, h3, h4, h5, h6 , p, span, div {
|
||||
font-family: Noto Sans Mono, Roboto Mono, 'Segoe UI';
|
||||
}
|
||||
|
||||
.p-dropdown {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
<T>(id: string): T;
|
||||
keys(): string[];
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting(),
|
||||
);
|
||||
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().forEach(context);
|
|
@ -0,0 +1,15 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"paths": {
|
||||
"crypto": ["./node_modules/crypto-browserify"],
|
||||
"stream": ["./node_modules/stream-browserify"],
|
||||
"assert": ["./node_modules/assert"],
|
||||
"http": ["./node_modules/stream-http"],
|
||||
"https": ["./node_modules/https-browserify"],
|
||||
"os": ["./node_modules/os-browserify"],
|
||||
"process": ["./node_modules/process/browser"]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue