Initial commit

master
root 2022-11-18 08:03:05 +00:00
commit b3c3e2c82a
96 changed files with 30127 additions and 0 deletions

20
.env Executable file
View File

@ -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"

1
.gitignore vendored Executable file
View File

@ -0,0 +1 @@
**/.DS_Store

2
caddyproxy/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
/config/*
/data/*

13
caddyproxy/Caddyfile Executable file
View File

@ -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
}

85
docker-compose.yml Executable file
View File

@ -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

64
docker-compose_unproxy.yml Executable file
View File

@ -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

3
m0lecoin-backend/.dockerignore Executable file
View File

@ -0,0 +1,3 @@
pyenv
__pycache__
data

3
m0lecoin-backend/.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
/pyenv/
*pycache*
/data/

14
m0lecoin-backend/Dockerfile Executable file
View File

@ -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"]

1
m0lecoin-backend/abi/bank.json Executable file
View File

@ -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"}]

1
m0lecoin-backend/abi/shop.json Executable file
View File

@ -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"}]

View File

@ -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"}]

View File

@ -0,0 +1,4 @@
import json
token_interface = json.load(open('./abi/token.json', 'r'))
shop_interface = json.load(open('./abi/shop.json', 'r'))

294
m0lecoin-backend/api.py Executable file
View File

@ -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."
})

24
m0lecoin-backend/app.py Executable file
View File

@ -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')))

22
m0lecoin-backend/middleware.py Executable file
View File

@ -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

82
m0lecoin-backend/models.py Executable file
View File

@ -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

View File

@ -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

14
m0lecoin-backend/web3service.py Executable file
View File

@ -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)

1
m0lecoin-eth-contracts/.gitattributes vendored Executable file
View File

@ -0,0 +1 @@
*.sol linguist-language=Solidity

0
m0lecoin-eth-contracts/.gitignore vendored Executable file
View File

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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!");
}
}

View File

@ -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);
}

View File

@ -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];
}
}

View File

@ -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 { }
}

View File

@ -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) }
}
}
}

View File

@ -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 { }
}

View File

@ -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

View File

@ -0,0 +1,4 @@
node_modules
.git
.gitignore
dist

16
m0lecoin-frontend/.editorconfig Executable file
View File

@ -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

42
m0lecoin-frontend/.gitignore vendored Executable file
View File

@ -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

4
m0lecoin-frontend/.vscode/extensions.json vendored Executable file
View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
m0lecoin-frontend/.vscode/launch.json vendored Executable file
View File

@ -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"
}
]
}

42
m0lecoin-frontend/.vscode/tasks.json vendored Executable file
View File

@ -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"
}
}
}
}
]
}

28
m0lecoin-frontend/Dockerfile Executable file
View File

@ -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

27
m0lecoin-frontend/README.md Executable file
View File

@ -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.

133
m0lecoin-frontend/angular.json Executable file
View File

@ -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": []
}
}
}
}
}
}

44
m0lecoin-frontend/karma.conf.js Executable file
View File

@ -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
});
};

26655
m0lecoin-frontend/package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

50
m0lecoin-frontend/package.json Executable file
View File

@ -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"
}
}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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();
});
});

View File

@ -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}`]);
}
}

View File

@ -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 { }

View File

@ -0,0 +1,7 @@
<app-header></app-header>
<main>
<router-outlet></router-outlet>
</main>
<p-toast position="bottom-center"></p-toast>

View File

@ -0,0 +1,3 @@
main {
padding: 20px;
}

View File

@ -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!');
});
});

View File

@ -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';
}

View File

@ -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 { }

View File

@ -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>

View File

@ -0,0 +1,9 @@
#wrapper {
.content {
.summary, .operations {
margin: 30px 0;
}
}
}

View File

@ -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();
});
});

View File

@ -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());
}
}

View File

@ -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>

View File

@ -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;
}
}
}
}
}

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
export * from './request.models';
export * from './response.models';
export * from './window.extension';

View File

@ -0,0 +1,9 @@
export interface LoginRequest {
address: string,
password: string
}
export interface RegisterRequest extends LoginRequest {
otp: string,
otpSign: string
}

View File

@ -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
}

View File

@ -0,0 +1 @@
export type Web3EnabledWindow = Window & {ethereum?: any};

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
export * from './api.service';
export * from './auth.service';
export * from './auth-guard.service';
export * from './web3.service';

View File

@ -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();
}
}
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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();
});
});

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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%;
}
}
}
}
}

View File

@ -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();
});
});

View File

@ -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());
}
}

View File

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

View File

@ -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"}];

View File

@ -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"}];

View File

@ -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"}];

View File

@ -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.

View File

@ -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.

View File

@ -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.

BIN
m0lecoin-frontend/src/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@ -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>

12
m0lecoin-frontend/src/main.ts Executable file
View File

@ -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));

View File

@ -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
*/

View File

@ -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%;
}

26
m0lecoin-frontend/src/test.ts Executable file
View File

@ -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);

View File

@ -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"
]
}

44
m0lecoin-frontend/tsconfig.json Executable file
View File

@ -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
}
}

View File

@ -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"
]
}