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