mirror of
https://github.com/GOSTSec/gostweb
synced 2025-03-11 21:01:17 +00:00
First public release
This commit is contained in:
commit
294982b746
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.db
|
||||||
|
*.pot
|
||||||
|
*.doctree
|
||||||
|
*.swp
|
||||||
|
.doctrees
|
||||||
|
.DS_Store
|
||||||
|
.coverage
|
||||||
|
.idea/
|
||||||
|
local_settings.py
|
||||||
|
data/db.sqlite3
|
||||||
|
data/*.log
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
static/*
|
20
LICENSE
Normal file
20
LICENSE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2017 Purple Tech
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
GOSTCoin Web Wallet
|
||||||
|
===================
|
||||||
|
|
||||||
|
Django-powered web wallet for [GOSTCoin](http://gostco.in)
|
||||||
|
|
||||||
|
|
||||||
|
# Celery development workers
|
||||||
|
|
||||||
|
celery -A gst_web_wallet.celery worker --loglevel=debug
|
||||||
|
|
||||||
|
celery -A gst_web_wallet.celery beat --loglevel=debug
|
0
data/.keep
Normal file
0
data/.keep
Normal file
4
gst_web_wallet/__init__.py
Normal file
4
gst_web_wallet/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# import celery stuff
|
||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ['celery_app']
|
21
gst_web_wallet/celery.py
Normal file
21
gst_web_wallet/celery.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gst_web_wallet.settings')
|
||||||
|
app = Celery('gst_web_wallet')
|
||||||
|
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||||
|
# Load task modules from all registered Django app configs.
|
||||||
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
|
app.conf.beat_schedule = {
|
||||||
|
'check-received-txs': {
|
||||||
|
'task': 'wallet.tasks.check_transactions_task',
|
||||||
|
'schedule': 120.0,
|
||||||
|
# 'args': (123,),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.task(bind=True)
|
||||||
|
def debug_task(self):
|
||||||
|
print('Request: {0!r}'.format(self.request))
|
58
gst_web_wallet/local_settings.template
Normal file
58
gst_web_wallet/local_settings.template
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from gst_web_wallet.settings import *
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'change this key'
|
||||||
|
DEBUG = True
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'data', 'db.sqlite3'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||||
|
|
||||||
|
# gostcoind settngs
|
||||||
|
COIN_USER = "gostcoinrpc"
|
||||||
|
COIN_PASSWORD = "change this key"
|
||||||
|
COIN_ADDRESS = ("127.0.0.1", 9386)
|
||||||
|
COIN_CONNECTION = "http://{}:{}@{}:{}".format(
|
||||||
|
COIN_USER, COIN_PASSWORD, COIN_ADDRESS[0], COIN_ADDRESS[1])
|
||||||
|
|
||||||
|
# webapp config
|
||||||
|
GST_NETWORK_FEE = Decimal("0.002")
|
||||||
|
SERVICE_FEE = Decimal("0.0")
|
||||||
|
GST_DUST = Decimal("0.0001")
|
||||||
|
# address to receive change
|
||||||
|
GST_CHANGE_ADDRESS = "change this to your GST address"
|
||||||
|
|
||||||
|
# LOGGING = {
|
||||||
|
# 'version': 1,
|
||||||
|
# 'disable_existing_loggers': True,
|
||||||
|
# 'handlers': {
|
||||||
|
# 'file': {
|
||||||
|
# 'level': 'INFO',
|
||||||
|
# 'class': 'logging.FileHandler',
|
||||||
|
# 'filename': os.path.join(BASE_DIR, 'data', 'app.log'),
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# 'loggers': {
|
||||||
|
# 'django': {
|
||||||
|
# 'handlers': ['file'],
|
||||||
|
# 'level': 'INFO',
|
||||||
|
# 'propagate': True,
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
# ALLOWED_HOSTS = ['127.0.0.1', 'localhost', 'gostwallet.i2p']
|
||||||
|
# DATABASES = {
|
||||||
|
# 'default': {
|
||||||
|
# 'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
# 'NAME': 'gstwallet',
|
||||||
|
# 'USER': 'gstwallet',
|
||||||
|
# 'PASSWORD': 'gstwallet',
|
||||||
|
# 'HOST': '127.0.0.1',
|
||||||
|
# 'PORT': '',
|
||||||
|
# }
|
||||||
|
# }
|
113
gst_web_wallet/settings.py
Normal file
113
gst_web_wallet/settings.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
Django settings for gst_web_wallet project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 1.11.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from django.contrib.messages import constants as message_constants
|
||||||
|
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
|
||||||
|
'captcha',
|
||||||
|
'crispy_forms', # reusable Bootstrap forms
|
||||||
|
|
||||||
|
'wallet.apps.WalletConfig',
|
||||||
|
|
||||||
|
'integral_auth.apps.IntegralAuthConfig',
|
||||||
|
|
||||||
|
# 'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'gst_web_wallet.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'gst_web_wallet.wsgi.application'
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
CRISPY_TEMPLATE_PACK = 'bootstrap3'
|
||||||
|
|
||||||
|
LOGIN_URL = "integral_auth:signin"
|
||||||
|
|
||||||
|
# Override message tag for compatibility with bootstrap3
|
||||||
|
MESSAGE_TAGS = {message_constants.ERROR: 'danger'}
|
||||||
|
|
27
gst_web_wallet/urls.py
Normal file
27
gst_web_wallet/urls.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""gst_web_wallet URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/1.11/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.conf.urls import url, include
|
||||||
|
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.conf.urls import url, include
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from wallet.views import index
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# url(r'^admin/', admin.site.urls),
|
||||||
|
url(r'^captcha/', include('captcha.urls')),
|
||||||
|
|
||||||
|
url(r'^$', index, name="site_index"),
|
||||||
|
url(r'^auth/', include('integral_auth.urls')),
|
||||||
|
]
|
16
gst_web_wallet/wsgi.py
Normal file
16
gst_web_wallet/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for gst_web_wallet project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gst_web_wallet.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
0
integral_auth/__init__.py
Normal file
0
integral_auth/__init__.py
Normal file
3
integral_auth/admin.py
Normal file
3
integral_auth/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
integral_auth/apps.py
Normal file
5
integral_auth/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IntegralAuthConfig(AppConfig):
|
||||||
|
name = 'integral_auth'
|
23
integral_auth/forms.py
Normal file
23
integral_auth/forms.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||||
|
from crispy_forms.helper import FormHelper
|
||||||
|
from crispy_forms.layout import Submit
|
||||||
|
from captcha.fields import CaptchaField
|
||||||
|
|
||||||
|
class PasswordSignUpForm(UserCreationForm):
|
||||||
|
password1 = None
|
||||||
|
password2 = None
|
||||||
|
captcha = CaptchaField()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(PasswordSignUpForm, self).__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_method = 'post'
|
||||||
|
self.helper.add_input(Submit('submit', 'Continue'))
|
||||||
|
|
||||||
|
class PasswordSignInForm(AuthenticationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(PasswordSignInForm, self).__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_method = 'post'
|
||||||
|
self.helper.add_input(Submit('submit', 'Sign in'))
|
||||||
|
|
0
integral_auth/migrations/__init__.py
Normal file
0
integral_auth/migrations/__init__.py
Normal file
3
integral_auth/models.py
Normal file
3
integral_auth/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
25
integral_auth/templates/integral_auth/base.html
Normal file
25
integral_auth/templates/integral_auth/base.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "wallet/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
{% block signup_form %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{% url 'integral_auth:signin' %}">
|
||||||
|
Sign in</a> •
|
||||||
|
<a href="{% url 'integral_auth:signup' %}">
|
||||||
|
Sign up</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
14
integral_auth/templates/integral_auth/signin.html
Normal file
14
integral_auth/templates/integral_auth/signin.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "integral_auth/base.html" %}
|
||||||
|
|
||||||
|
{% block signup_form %}
|
||||||
|
|
||||||
|
<h1 class="text-center">
|
||||||
|
Sign in
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% crispy form form.helper %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
14
integral_auth/templates/integral_auth/signup.html
Normal file
14
integral_auth/templates/integral_auth/signup.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "integral_auth/base.html" %}
|
||||||
|
|
||||||
|
{% block signup_form %}
|
||||||
|
|
||||||
|
<h1 class="text-center">
|
||||||
|
Sign up
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% crispy form form.helper %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
143
integral_auth/tests_func.py
Normal file
143
integral_auth/tests_func.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
from django.test import TestCase, RequestFactory, Client
|
||||||
|
from django.urls import reverse_lazy, reverse
|
||||||
|
from django.contrib.auth.models import User, AnonymousUser
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
|
from django.http import Http404
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import integral_auth.views as views
|
||||||
|
from integral_auth.utils import rand_string
|
||||||
|
|
||||||
|
import captcha
|
||||||
|
captcha.conf.settings.CAPTCHA_TEST_MODE = True
|
||||||
|
|
||||||
|
views.conn = MagicMock()
|
||||||
|
views.conn.getnewaddress = lambda: "G" + rand_string(33)
|
||||||
|
|
||||||
|
class SignupTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||||
|
self.page_url = reverse_lazy("integral_auth:signup")
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_page(self):
|
||||||
|
request = self.factory.get(self.page_url)
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
resp = views.PasswordSignUp.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get(self.page_url)
|
||||||
|
request.user = self.user
|
||||||
|
resp = views.PasswordSignUp.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_signup(self):
|
||||||
|
|
||||||
|
resp = Client().post(self.page_url, {})
|
||||||
|
self.assertIs(resp.status_code, 200)
|
||||||
|
self.assertFalse(resp.context["form"].is_valid())
|
||||||
|
|
||||||
|
resp = Client().post(self.page_url, {
|
||||||
|
"username": "sophia",
|
||||||
|
'captcha_0': 'abc', "captcha_1": "passed"})
|
||||||
|
self.assertFalse(resp.context["form"].is_valid())
|
||||||
|
|
||||||
|
resp = Client().post(self.page_url, {
|
||||||
|
"username": "test11",
|
||||||
|
'captcha_0': 'abc', "captcha_1": "wrong"})
|
||||||
|
self.assertFalse(resp.context["form"].is_valid())
|
||||||
|
|
||||||
|
|
||||||
|
def test_success_signup(self):
|
||||||
|
|
||||||
|
resp = Client().post(self.page_url, {
|
||||||
|
"username": "testuser",
|
||||||
|
'captcha_0': 'abc', "captcha_1": "passed"})
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
class SigninTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||||
|
self.page_url = reverse_lazy("integral_auth:signin")
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def post_request(self, data):
|
||||||
|
request = self.factory.post(self.page_url)
|
||||||
|
request.user, request.session = AnonymousUser(), SessionStore()
|
||||||
|
request.POST = data
|
||||||
|
return request
|
||||||
|
|
||||||
|
def test_page(self):
|
||||||
|
request = self.factory.get(self.page_url)
|
||||||
|
request.user, request.session = AnonymousUser(), SessionStore()
|
||||||
|
resp = views.PasswordSignIn.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
request = self.factory.get(self.page_url)
|
||||||
|
request.user = self.user
|
||||||
|
resp = views.PasswordSignIn.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
def test_signin_incorrect(self):
|
||||||
|
request = self.post_request({"username": "", "password": ""})
|
||||||
|
resp = views.PasswordSignIn.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue(request.user.is_anonymous)
|
||||||
|
|
||||||
|
request = self.post_request({"username": "hacker", "password": "qweqweqwe"})
|
||||||
|
resp = views.PasswordSignIn.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue(request.user.is_anonymous)
|
||||||
|
|
||||||
|
request = self.post_request({"username": "sophia", "password": ""})
|
||||||
|
resp = views.PasswordSignIn.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue(request.user.is_anonymous)
|
||||||
|
|
||||||
|
request = self.post_request({"username": "sophia", "password": "aiosoidsoaas"})
|
||||||
|
resp = views.PasswordSignIn.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue(request.user.is_anonymous)
|
||||||
|
|
||||||
|
def test_signin_success(self):
|
||||||
|
request = self.post_request({"username": "sophia", "password": "qweqweqwe"})
|
||||||
|
resp = views.PasswordSignIn.as_view()(request)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertEqual(request.user.username, "sophia")
|
||||||
|
|
||||||
|
class SigninUrlTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||||
|
self.user1 = User.objects.create_user("hacker", password="qweqweqwe")
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_signin_incorrect(self):
|
||||||
|
creds = {"username": "sophia", "password": "qweqweqw"}
|
||||||
|
page_url = reverse("integral_auth:signin_url", kwargs=creds)
|
||||||
|
request = self.factory.get(page_url)
|
||||||
|
request.session, request.user = SessionStore(), AnonymousUser()
|
||||||
|
with self.assertRaises(Http404):
|
||||||
|
views.signin_url(request, **creds)
|
||||||
|
self.assertTrue(request.user.is_anonymous)
|
||||||
|
# it redirects instead of 404 if user is logged in
|
||||||
|
c = Client()
|
||||||
|
c.login(username="hacker", password="qweqweqwe")
|
||||||
|
resp = c.get(page_url)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
def test_signin_success(self):
|
||||||
|
creds = {"username": "sophia", "password": "qweqweqwe"}
|
||||||
|
page_url = reverse("integral_auth:signin_url", kwargs=creds)
|
||||||
|
request = self.factory.get(page_url)
|
||||||
|
request.session, request.user = SessionStore(), AnonymousUser()
|
||||||
|
resp = views.signin_url(request, **creds)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertEqual(request.user.username, "sophia")
|
||||||
|
|
41
integral_auth/tests_unit.py
Normal file
41
integral_auth/tests_unit.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from integral_auth import forms
|
||||||
|
|
||||||
|
import captcha
|
||||||
|
captcha.conf.settings.CAPTCHA_TEST_MODE = True
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordSignUpFormTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.form = forms.PasswordSignUpForm
|
||||||
|
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||||
|
|
||||||
|
def test_signup_valid(self):
|
||||||
|
data = {'username': 'paul',
|
||||||
|
'captcha_0':'abc', 'captcha_1': 'passed'}
|
||||||
|
form = self.form(data)
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_signup_invalid_username(self):
|
||||||
|
data = {'username': 'sophia',
|
||||||
|
'captcha_0':'abc', 'captcha_1': 'passed'}
|
||||||
|
form = self.form(data)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
def test_signup_invalid_input(self):
|
||||||
|
data = {}
|
||||||
|
form = self.form(data)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
data = {'username': 'sophia3'}
|
||||||
|
form = self.form(data)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
data = {'username': 'sophia3',
|
||||||
|
'captcha_0':'abc', 'captcha_1': 'wtf'}
|
||||||
|
form = self.form(data)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
21
integral_auth/urls.py
Normal file
21
integral_auth/urls.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from django.conf.urls import url, include
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
app_name = "integral_auth"
|
||||||
|
|
||||||
|
anon_only = user_passes_test(
|
||||||
|
lambda u: u.is_anonymous(), reverse_lazy('site_index'))
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.PasswordSignUp.as_view(), name='signup'),
|
||||||
|
url(r'^signin$', views.PasswordSignIn.as_view(), name='signin'),
|
||||||
|
url(r'^url/(?P<username>\w{1,150})/(?P<password>\w+)$',
|
||||||
|
anon_only(views.signin_url), name='signin_url'),
|
||||||
|
|
||||||
|
url(r'^logout/$', auth_views.LogoutView.as_view(), name='logout'),
|
||||||
|
]
|
||||||
|
|
7
integral_auth/utils.py
Normal file
7
integral_auth/utils.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
def rand_string(length):
|
||||||
|
"""Generate lame random hexdigest string"""
|
||||||
|
return "".join([random.choice(string.hexdigits) for _ in range(length)])
|
||||||
|
|
77
integral_auth/views.py
Normal file
77
integral_auth/views.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.http import Http404
|
||||||
|
from django.urls import reverse_lazy, reverse
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth import authenticate, login
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
|
from integral_auth.utils import rand_string
|
||||||
|
|
||||||
|
from .forms import PasswordSignUpForm, PasswordSignInForm
|
||||||
|
|
||||||
|
from wallet.models import Account, Address
|
||||||
|
from wallet.gostcoin import GOSTCOIN_CONNECTION as conn
|
||||||
|
|
||||||
|
class PasswordAuth(UserPassesTestMixin, FormView):
|
||||||
|
login_url = reverse_lazy("site_index")
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user.is_anonymous()
|
||||||
|
|
||||||
|
class PasswordSignUp(PasswordAuth):
|
||||||
|
template_name = "integral_auth/signup.html"
|
||||||
|
form_class = PasswordSignUpForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
addr = conn.getnewaddress()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
username, password = rand_string(16), rand_string(32)
|
||||||
|
user = User.objects.create_user(username, password=password)
|
||||||
|
break
|
||||||
|
except IntegrityError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
name = rand_string(16)
|
||||||
|
account = Account.objects.create(user=user, name=name)
|
||||||
|
Address.objects.create(account=account, address=addr, used=False)
|
||||||
|
|
||||||
|
signin_url = self.request.build_absolute_uri(
|
||||||
|
reverse('integral_auth:signin_url', kwargs={
|
||||||
|
"username": username, "password": password}))
|
||||||
|
|
||||||
|
messages.success(self.request,
|
||||||
|
"Success! Username: {}, password: {}".format(
|
||||||
|
username, password))
|
||||||
|
messages.success(self.request,
|
||||||
|
"You can login by visiting this link: {}".format(signin_url))
|
||||||
|
messages.warning(self.request,
|
||||||
|
"""Save username/password RIGHT NOW in a secure place. There is
|
||||||
|
NO WAY to recover them!""")
|
||||||
|
|
||||||
|
login(self.request, user)
|
||||||
|
return redirect("site_index")
|
||||||
|
|
||||||
|
class PasswordSignIn(PasswordAuth):
|
||||||
|
template_name = "integral_auth/signin.html"
|
||||||
|
form_class = PasswordSignInForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
user = authenticate(username=form.cleaned_data["username"],
|
||||||
|
password=form.cleaned_data["password"])
|
||||||
|
if user is not None and user.is_active:
|
||||||
|
login(self.request, user)
|
||||||
|
return redirect("site_index")
|
||||||
|
|
||||||
|
def signin_url(request, username, password):
|
||||||
|
"""Handle sign in by url"""
|
||||||
|
user = authenticate(username=username, password=password)
|
||||||
|
if user is not None and user.is_active:
|
||||||
|
login(request, user)
|
||||||
|
else:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
return redirect("site_index")
|
22
manage.py
Executable file
22
manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gst_web_wallet.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError:
|
||||||
|
# The above import may fail for some other reason. Ensure that the
|
||||||
|
# issue is really that Django is missing to avoid masking other
|
||||||
|
# exceptions on Python 2.
|
||||||
|
try:
|
||||||
|
import django
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
execute_from_command_line(sys.argv)
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
celery
|
||||||
|
Django==1.11
|
||||||
|
django-crispy-forms
|
||||||
|
django-simple-captcha
|
||||||
|
Markdown
|
||||||
|
redis
|
||||||
|
python-bitcoinrpc
|
0
wallet/__init__.py
Normal file
0
wallet/__init__.py
Normal file
3
wallet/admin.py
Normal file
3
wallet/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
wallet/apps.py
Normal file
5
wallet/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WalletConfig(AppConfig):
|
||||||
|
name = 'wallet'
|
58
wallet/forms.py
Normal file
58
wallet/forms.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from crispy_forms.helper import FormHelper
|
||||||
|
from crispy_forms.layout import Submit, Layout
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from .models import Account
|
||||||
|
|
||||||
|
class SendCoins(forms.Form):
|
||||||
|
recipient = forms.RegexField(
|
||||||
|
regex="^[a-zA-Z0-9]{16,35}$",
|
||||||
|
label="Address or local account")
|
||||||
|
amount = forms.DecimalField(label="amount",
|
||||||
|
min_value=Decimal("0.01"),
|
||||||
|
max_value=Decimal("2000000"))
|
||||||
|
|
||||||
|
|
||||||
|
def clean_recipient(self):
|
||||||
|
"""Validate recipient"""
|
||||||
|
data = self.cleaned_data["recipient"]
|
||||||
|
|
||||||
|
if (len(data) == 34 or len(data) == 35) and data.startswith("G"):
|
||||||
|
"""Valid GST address"""
|
||||||
|
return data
|
||||||
|
elif len(data) == 16:
|
||||||
|
"""Account name"""
|
||||||
|
try:
|
||||||
|
a = Account.objects.get(name=data)
|
||||||
|
except Account.DoesNotExist:
|
||||||
|
raise ValidationError("Invalid account name", code="invalid")
|
||||||
|
else:
|
||||||
|
raise ValidationError("Incorrect recipient address/account name",
|
||||||
|
code="invalid")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Check balance"""
|
||||||
|
if self.user and self.is_valid():
|
||||||
|
if len(self.cleaned_data["recipient"]) == 16:
|
||||||
|
total_amount = self.cleaned_data["amount"]
|
||||||
|
else:
|
||||||
|
total_amount = self.cleaned_data["amount"] + \
|
||||||
|
settings.GST_NETWORK_FEE + settings.SERVICE_FEE
|
||||||
|
if self.user.account.balance < total_amount:
|
||||||
|
raise ValidationError("Not enough coins on balance")
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.user = kwargs.pop('user', None)
|
||||||
|
super(SendCoins, self).__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_method = 'post'
|
||||||
|
self.helper.add_input(Submit('submit', 'Send'))
|
42
wallet/gostcoin.py
Normal file
42
wallet/gostcoin.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from bitcoinrpc.authproxy import AuthServiceProxy
|
||||||
|
|
||||||
|
def select_inputs(conn, amount):
|
||||||
|
"""Select unspent inputs to craft tx"""
|
||||||
|
unspent_inputs = conn.listunspent(0)
|
||||||
|
unspent_inputs.sort(key=lambda u: u['amount'] * u['confirmations'],
|
||||||
|
reverse=True)
|
||||||
|
|
||||||
|
inputs, total = [], 0
|
||||||
|
for usin in unspent_inputs:
|
||||||
|
inputs.append(usin)
|
||||||
|
total += usin["amount"]
|
||||||
|
if total >= amount: break
|
||||||
|
|
||||||
|
if total < amount:
|
||||||
|
raise GostCoinException("Not enough coins on the server")
|
||||||
|
|
||||||
|
return inputs, total
|
||||||
|
|
||||||
|
def create_raw_tx(conn, address, amount):
|
||||||
|
"""Prepare raw transaction and return with output amount"""
|
||||||
|
# TODO calculate fee per kB
|
||||||
|
output_amount = amount + settings.GST_NETWORK_FEE
|
||||||
|
inputs, total = select_inputs(conn, output_amount)
|
||||||
|
|
||||||
|
change_amount = total - output_amount
|
||||||
|
outputs = {address: amount}
|
||||||
|
if change_amount > settings.GST_DUST:
|
||||||
|
outputs[settings.GST_CHANGE_ADDRESS] = change_amount
|
||||||
|
|
||||||
|
return conn.createrawtransaction(inputs, outputs)
|
||||||
|
|
||||||
|
|
||||||
|
class GostCoinException(Exception):
|
||||||
|
"""
|
||||||
|
Raised when something is wrong with account balance
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
GOSTCOIN_CONNECTION = AuthServiceProxy(settings.COIN_CONNECTION)
|
47
wallet/migrations/0001_initial.py
Normal file
47
wallet/migrations/0001_initial.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-04-27 09:17
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Account',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=16, unique=True)),
|
||||||
|
('balance', models.DecimalField(decimal_places=8, default=0, max_digits=15)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Address',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('address', models.CharField(max_length=35, unique=True)),
|
||||||
|
('used', models.BooleanField(default=False)),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Transaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('txid', models.CharField(max_length=64, unique=True)),
|
||||||
|
('confirmed', models.BooleanField(default=False)),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||||
|
('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Address')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
55
wallet/migrations/0002_auto_20171021_2029.py
Normal file
55
wallet/migrations/0002_auto_20171021_2029.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-10-21 20:29
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wallet', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DepositTransaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('txid', models.CharField(max_length=64, unique=True)),
|
||||||
|
('confirmed', models.BooleanField(default=False)),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||||
|
('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Address')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WithdrawalTransaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('txid', models.CharField(max_length=64, unique=True)),
|
||||||
|
('confirmed', models.BooleanField(default=False)),
|
||||||
|
('address', models.CharField(max_length=35)),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='account',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='address',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Transaction',
|
||||||
|
),
|
||||||
|
]
|
25
wallet/migrations/0003_auto_20171021_2051.py
Normal file
25
wallet/migrations/0003_auto_20171021_2051.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-10-21 20:51
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wallet', '0002_auto_20171021_2029'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='deposittransaction',
|
||||||
|
name='amount',
|
||||||
|
field=models.DecimalField(decimal_places=8, default=0, max_digits=15),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='withdrawaltransaction',
|
||||||
|
name='amount',
|
||||||
|
field=models.DecimalField(decimal_places=8, default=0, max_digits=15),
|
||||||
|
),
|
||||||
|
]
|
0
wallet/migrations/__init__.py
Normal file
0
wallet/migrations/__init__.py
Normal file
38
wallet/models.py
Normal file
38
wallet/models.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from django.db import models, IntegrityError
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
|
||||||
|
from integral_auth.utils import rand_string
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger("django")
|
||||||
|
|
||||||
|
class Account(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=16, unique=True)
|
||||||
|
balance = models.DecimalField(default=0, decimal_places=8, max_digits=15)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Address(models.Model):
|
||||||
|
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||||
|
address = models.CharField(max_length=35, unique=True)
|
||||||
|
used = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Transaction(models.Model):
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||||
|
txid = models.CharField(max_length=64, unique=True)
|
||||||
|
confirmed = models.BooleanField(default=False)
|
||||||
|
amount = models.DecimalField(default=0, decimal_places=8, max_digits=15)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class DepositTransaction(Transaction):
|
||||||
|
address = models.ForeignKey(Address, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class WithdrawalTransaction(Transaction):
|
||||||
|
address = models.CharField(max_length=35)
|
7
wallet/static/wallet/css/bootstrap.css
vendored
Normal file
7
wallet/static/wallet/css/bootstrap.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
wallet/static/wallet/css/bootstrap.css.bak
Normal file
11
wallet/static/wallet/css/bootstrap.css.bak
Normal file
File diff suppressed because one or more lines are too long
19
wallet/static/wallet/css/main.css
Normal file
19
wallet/static/wallet/css/main.css
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
p.flash-message {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-block {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-password-signup:checked + div {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-retrieve-options:checked + div {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
424
wallet/static/wallet/css/normalize.css
vendored
Normal file
424
wallet/static/wallet/css/normalize.css
vendored
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Set default font family to sans-serif.
|
||||||
|
* 2. Prevent iOS and IE text size adjust after device orientation change,
|
||||||
|
* without disabling user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default margin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display definitions
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||||
|
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||||
|
* and Firefox.
|
||||||
|
* Correct `block` display not defined for `main` in IE 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
main,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section,
|
||||||
|
summary {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||||
|
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
progress,
|
||||||
|
video {
|
||||||
|
display: inline-block; /* 1 */
|
||||||
|
vertical-align: baseline; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||||
|
* Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden],
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background color from active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability of focused elements when they are also in an
|
||||||
|
* active/hover state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: 1px dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in Safari and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dfn {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address variable `h1` font-size and margin within `section` and `article`
|
||||||
|
* contexts in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 8/9.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background: #ff0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent and variable font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove border when inside `a` element in IE 8/9/10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct overflow not hidden in IE 9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address margin not present in IE 8/9 and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 1em 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address differences between Firefox and other browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contain overflow in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address odd `em`-unit font size rendering in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
pre,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||||
|
* styling of `select`, unless a `border` property is set.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited.
|
||||||
|
* Known issue: affects color of disabled elements.
|
||||||
|
* 2. Correct font properties not being inherited.
|
||||||
|
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: inherit; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
margin: 0; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||||
|
* Correct `select` style inheritance in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 4+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It's recommended that you don't attempt to style these elements.
|
||||||
|
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||||
|
*
|
||||||
|
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||||
|
* 2. Remove excess padding in IE 8/9/10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="checkbox"],
|
||||||
|
input[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||||
|
* `font-size` values of the `input`, it causes the cursor style of the
|
||||||
|
* decrement button to change from `default` to `text`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
box-sizing: content-box; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||||
|
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||||
|
* padding (and `textfield` appearance).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define consistent border, margin, and padding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||||
|
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't inherit the `font-weight` (applied by a rule above).
|
||||||
|
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
optgroup {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 0;
|
||||||
|
}
|
BIN
wallet/static/wallet/favicon.ico
Normal file
BIN
wallet/static/wallet/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
66
wallet/tasks.py
Normal file
66
wallet/tasks.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
from django.db import transaction
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from .models import Account, Address, DepositTransaction, WithdrawalTransaction
|
||||||
|
|
||||||
|
from .gostcoin import GOSTCOIN_CONNECTION as conn
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def check_transactions_task():
|
||||||
|
check_received_transactions(conn)
|
||||||
|
check_confirmed_transactions(conn)
|
||||||
|
|
||||||
|
def check_received_transactions(conn):
|
||||||
|
for t in conn.listtransactions("", 100):
|
||||||
|
if "txid" not in t:
|
||||||
|
continue
|
||||||
|
elif t["category"] == "receive":
|
||||||
|
try:
|
||||||
|
tx = DepositTransaction.objects.get(txid=t["txid"])
|
||||||
|
except DepositTransaction.DoesNotExist:
|
||||||
|
try:
|
||||||
|
address = Address.objects.get(address=t["address"])
|
||||||
|
if not address.used:
|
||||||
|
address.used = True
|
||||||
|
address.save()
|
||||||
|
# create new address
|
||||||
|
Address.objects.create(account=address.account,
|
||||||
|
address=conn.getnewaddress(), used=False)
|
||||||
|
DepositTransaction.objects.create(account=address.account,
|
||||||
|
txid=t["txid"], address=address, confirmed=False)
|
||||||
|
except Address.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def check_confirmed_transactions(conn):
|
||||||
|
"""
|
||||||
|
Looks for unconfirmed transactions in the database
|
||||||
|
checks if they are confirmed in gostd
|
||||||
|
add received coins to balance
|
||||||
|
"""
|
||||||
|
unconfirmed_recv = DepositTransaction.objects.filter(confirmed=False)
|
||||||
|
for t in unconfirmed_recv:
|
||||||
|
result = conn.gettransaction(t.txid)
|
||||||
|
if result["confirmations"] >= 6:
|
||||||
|
amount = Decimal("0")
|
||||||
|
|
||||||
|
for d in result["details"]:
|
||||||
|
if d["category"] == "receive" and d["address"] == t.address.address:
|
||||||
|
amount += d["amount"]
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
t.amount = amount
|
||||||
|
t.confirmed = True
|
||||||
|
t.save()
|
||||||
|
t.account.balance += amount
|
||||||
|
t.account.save()
|
||||||
|
|
||||||
|
unconfirmed_send = WithdrawalTransaction.objects.filter(confirmed=False)
|
||||||
|
for t in unconfirmed_send:
|
||||||
|
result = conn.gettransaction(t.txid)
|
||||||
|
if result["confirmations"] >= 6:
|
||||||
|
t.confirmed = True
|
||||||
|
t.save()
|
||||||
|
|
40
wallet/templates/wallet/base.html
Normal file
40
wallet/templates/wallet/base.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>{% block title %}GOSTCoin Web Wallet{% endblock %}</title>
|
||||||
|
<meta name="description" content="{% block description %}{% endblock %}">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="{% static 'wallet/favicon.ico' %}" type="image/x-icon">
|
||||||
|
<!-- <link rel="stylesheet" href="{% static 'wallet/css/normalize.css' %}"> -->
|
||||||
|
<link rel="stylesheet" href="{% static 'wallet/css/bootstrap.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'wallet/css/main.css' %}">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include 'wallet/navigation.html' %}
|
||||||
|
|
||||||
|
<div class="container text-center">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<p{% if message.tags %} class="flash-message bg-{{ message.tags }} {{message.extra_tags}}"{% endif %}>{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" id="content">
|
||||||
|
{% block content %}
|
||||||
|
<p>Hello world! This is HTML5 Boilerplate.</p>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer small text-center">
|
||||||
|
<p>© Purple Tech, 2017.
|
||||||
|
GST donations: GM5cSsCW14eB822rEHqTGFpm2XKB9nYndd</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
85
wallet/templates/wallet/index_page.html
Normal file
85
wallet/templates/wallet/index_page.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
{% extends "wallet/base.html" %}
|
||||||
|
{% load wallet_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>Your address for receiving coins</h3>
|
||||||
|
<p class="lead"><span class="text-success">{{address}}</span></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Local account: <span class="text-warning">{{user.account.name}}</span>
|
||||||
|
</p>
|
||||||
|
<h3>Balance: {{balance|floatformat:4}} GST</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>Transfer coins</h3>
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% crispy form form.helper %}
|
||||||
|
<p class="small">
|
||||||
|
{% if fees.network %}{{ fees.network }} GST network fee{% endif %}
|
||||||
|
{% if fees.service %}, +{{ fees.service }} GST service fee{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if fees.network or fees.service %}
|
||||||
|
<p class="small">Local transfers don't require fees.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Deposit Transactions</h3>
|
||||||
|
<p>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Transaction id</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>status</th>
|
||||||
|
</tr>
|
||||||
|
{% for t in deposit_transactions reversed %}
|
||||||
|
<tr>
|
||||||
|
<td>{{t.txid }}</td>
|
||||||
|
<td>{% if t.amount %}+{{t.amount|floatformat:4 }} GST{% endif %}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% if t.confirmed %}
|
||||||
|
<span class="text-success">confirmed</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">unconfirmed</span>
|
||||||
|
{%endif%}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td>no transactions yet</td><td></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</p>
|
||||||
|
<h3>Withdrawal Transactions</h3>
|
||||||
|
<p>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Transaction id</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>status</th>
|
||||||
|
</tr>
|
||||||
|
{% for t in withdrawal_transactions reversed %}
|
||||||
|
<tr>
|
||||||
|
<td>{{t.txid }}</td>
|
||||||
|
<td>-{{t.amount|floatformat:4 }} GST</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% if t.confirmed %}
|
||||||
|
<span class="text-success">confirmed</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger">unconfirmed</span>
|
||||||
|
{%endif%}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td>no transactions yet</td><td></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
18
wallet/templates/wallet/navigation.html
Normal file
18
wallet/templates/wallet/navigation.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<nav class="navbar navbar-inverse">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<a class="navbar-brand" href="{% url 'site_index' %}">GOSTCoin Web Wallet</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
<!-- <li><a href="#" >About</a></li> -->
|
||||||
|
{% if not user.is_authenticated %}
|
||||||
|
<li class="active"><a href="{% url 'integral_auth:signin' %}">
|
||||||
|
Sign in</a></li>
|
||||||
|
{% else%}
|
||||||
|
<li><a href="{% url 'integral_auth:logout' %}?next={% url 'site_index' %}">
|
||||||
|
Logout</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
0
wallet/templatetags/__init__.py
Normal file
0
wallet/templatetags/__init__.py
Normal file
10
wallet/templatetags/wallet_tags.py
Normal file
10
wallet/templatetags/wallet_tags.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def timestamp_to_time(timestamp):
|
||||||
|
"""Converts a timestamp into datetime obj"""
|
||||||
|
import datetime
|
||||||
|
return datetime.datetime.fromtimestamp(timestamp)
|
||||||
|
|
93
wallet/tests/fake_data.py
Normal file
93
wallet/tests/fake_data.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
DEMO_TRANSACTIONS = [
|
||||||
|
{
|
||||||
|
'txid': '3c03439fab4f53c97185e66217fe5fe61cdc4e80b25dfe5a0d19bf0b8c77f994',
|
||||||
|
'blocktime': 1491951914,
|
||||||
|
'blockindex': 1,
|
||||||
|
'timereceived': 1491951811,
|
||||||
|
'category': 'receive',
|
||||||
|
'time': 1491951811,
|
||||||
|
'blockhash': '0000004d184d3df898180937bd22c2717543706443b33ec7e16e369fa9d8c334',
|
||||||
|
'amount': Decimal('616.00000000'),
|
||||||
|
'account': '',
|
||||||
|
'confirmations': 6238,
|
||||||
|
'address': 'GXaNvzURu4fRAkjSodybjcXnwwUPKxB6rQ'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'txid': '62abf6d27ea48937e2979948012b353bf0710c2df91b0d7c63c5cdeba034b835',
|
||||||
|
'time': 1492013400,
|
||||||
|
'timereceived': 1492013400,
|
||||||
|
'confirmations': 5453,
|
||||||
|
'account': '',
|
||||||
|
'fee': Decimal('0E-8'),
|
||||||
|
'blockindex': 1,
|
||||||
|
'category': 'send',
|
||||||
|
'blockhash': '00000018021fcb42490ab49053994ddb71f9316278625ec1d3d8119d98a18333',
|
||||||
|
'amount': Decimal('-616.00000000'),
|
||||||
|
'blocktime': 1492013470,
|
||||||
|
'address': 'GWAYdECe4rQfrjyynHHNx72FqGCoxSW939'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
'txid': 'a94b83eec4f267aa503cc9ec65a229350cae60799ff73c28283a7aa5dad15435',
|
||||||
|
'blocktime': 1492779489,
|
||||||
|
'blockindex': 1,
|
||||||
|
'timereceived': 1492779203,
|
||||||
|
'category': 'receive',
|
||||||
|
'time': 1492779203,
|
||||||
|
'blockhash': '000000060161934be4eeb7670c8ab004306e9bc5333fa0fb560d597a3220fc97',
|
||||||
|
'amount': Decimal('1.00000000'),
|
||||||
|
'account': '',
|
||||||
|
'confirmations': 1134,
|
||||||
|
'address': 'GHwVSdza9QB5zPa9kZWjFdByynTAtDb6M9'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'txid': 'a94b83eec4f267aa503cc9ec65a229350cae60799ff73c28283a7aa5dad15435',
|
||||||
|
'blocktime': 1492779489,
|
||||||
|
'blockindex': 1,
|
||||||
|
'timereceived': 1492779203,
|
||||||
|
'category': 'receive',
|
||||||
|
'time': 1492779203,
|
||||||
|
'blockhash': '000000060161934be4eeb7670c8ab004306e9bc5333fa0fb560d597a3220fc97',
|
||||||
|
'amount': Decimal('1.00000000'),
|
||||||
|
'account': '',
|
||||||
|
'confirmations': 1134,
|
||||||
|
'address': 'GHwVSdza9QB5zPa9kZWjFdByynTAtD0000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'txid': 'a94b83eec4f267aa503cc9ec65a229350cae60799ff73c28283a7aa5dad15435',
|
||||||
|
'time': 1492779203,
|
||||||
|
'timereceived': 1492779203,
|
||||||
|
'confirmations': 1134,
|
||||||
|
'account': '',
|
||||||
|
'fee': Decimal('0E-8'),
|
||||||
|
'blockindex': 1,
|
||||||
|
'category': 'send',
|
||||||
|
'blockhash': '000000060161934be4eeb7670c8ab004306e9bc5333fa0fb560d597a3220fc97',
|
||||||
|
'amount': Decimal('-1.00000000'),
|
||||||
|
'blocktime': 1492779489,
|
||||||
|
'address': 'GHwVSdza9QB5zPa9kZWjFdByynTAtDb6M9'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
DEMO_UNSPENT = [
|
||||||
|
{'address': 'GdNgGq9S78N1nr5HMyKPmHoFTnPqt5PXAU', 'vout': 1, 'txid': '16c0d08bf53e9c132a8522cca2544d53fe4389167329731f7f9645963f92d595', 'scriptPubKey': '76a914d63b7a8ba4f33cbd2f723c6ba68449166630472188ac', 'amount': Decimal('4.00000000'), 'confirmations': 904},
|
||||||
|
{'address': 'GQtF5PvK4WRdAjMKjXCSmrqDbUEXz6W7z5', 'vout': 1, 'txid': '192b1cabf1b199909da44678daf0e4fac3559cd8e0c222c24ec86a9609decf84', 'scriptPubKey': '76a9144d3949f0f4207438acbb1c71177b0bb42bda614088ac', 'amount': Decimal('0.99800000'), 'confirmations': 889},
|
||||||
|
{'address': 'GQ5V5DKrAo1QWoEpPLWfbwrbY55jqGFsCN', 'vout': 0, 'txid': '1e2f148fd3655fae34459791d7127afca31ac87d16af737eb9410f656ce56588', 'scriptPubKey': '76a91444616c30428e6c1581d899fc276e5205e61381c788ac', 'amount': Decimal('0.07000000'), 'confirmations': 803},
|
||||||
|
{'address': 'GcKiTtJ1BL5ddZJZEqL6Q3Ax52k5MYhebm', 'account': '', 'vout': 1, 'txid': '1e2f148fd3655fae34459791d7127afca31ac87d16af737eb9410f656ce56588', 'scriptPubKey': '76a914cab3ef02db6ebf0d17f29e6ebfa43e94da58c4db88ac', 'amount': Decimal('2.00000000'), 'confirmations': 803},
|
||||||
|
{'address': 'GepEjzLppwxsft6KSrT8poBRKhkeaT8pvw', 'vout': 1, 'txid': '3190ed2a9be245fc80505a0f46054009821af64865edd85bfe52bd9e9dc85e97', 'scriptPubKey': '76a914e6091b4d99fb48e261e1c965025d15200b55b0f788ac', 'amount': Decimal('200.90000000'), 'confirmations': 1297},
|
||||||
|
{'address': 'GRKtS8H84k6Ta9rH1GbxtJkCgh3unSsWyY', 'vout': 1, 'txid': '476aca8593d4a292a1d08a9872b8736c54a1867a3b4394c841aa56752f6b9edd', 'scriptPubKey': '76a9145212df4dcd2e9cf805fc7c8db1cced27f06fc8cd88ac', 'amount': Decimal('4.00000000'), 'confirmations': 904},
|
||||||
|
{'address': 'GZr7csPKkJhCs7Ybk6V4RjQnGDiwe7SjNG', 'account': '', 'vout': 0, 'txid': '529c78221fd9f8f88eb8e57c71afb8c5ae80255fa943c2aea24d43669f8d51c4', 'scriptPubKey': '76a914af8b5d2729e6736ed876571af8c06cc3c9c52ee888ac', 'amount': Decimal('10.00000000'), 'confirmations': 1294},
|
||||||
|
{'address': 'GcKiTtJ1BL5ddZJZEqL6Q3Ax52k5MYhebm', 'account': '', 'vout': 0, 'txid': '586bd540f917f5472feee45d709554183395dd350b6ce76e4654534a4be28cf1', 'scriptPubKey': '76a914cab3ef02db6ebf0d17f29e6ebfa43e94da58c4db88ac', 'amount': Decimal('2.00000000'), 'confirmations': 803},
|
||||||
|
{'address': 'GXa6TrdAHMMnKZY8UC1ACXq41LqLomsnrX', 'vout': 1, 'txid': '586bd540f917f5472feee45d709554183395dd350b6ce76e4654534a4be28cf1', 'scriptPubKey': '76a91496937e10d514abce8df94a8c024491f8a25e8ea388ac', 'amount': Decimal('0.01200000'), 'confirmations': 803},
|
||||||
|
{'address': 'GU9HLpYFErjnVGsgnpwUiCsakk8uApLSFk', 'account': '', 'vout': 0, 'txid': 'c76350792f44c4d8a0fbc499ed7090ed67cbd2ecb490faffde1e1c0c1f62838d', 'scriptPubKey': '76a91470f9cc102e2807ad439531abe07601b0e97ec82188ac', 'amount': Decimal('22.16000000'), 'confirmations': 397},
|
||||||
|
{'address': 'GPFEKc6dsUbTJBWGCDmtwdB1DDhhdBAsue', 'account': '', 'vout': 0, 'txid': 'd3c7dd35d371312443327fc66982092f96ac612272aa4d66fdf01f653e4e85e1', 'scriptPubKey': '76a9143b4124688a92cd772e787a305e7282409ca6cf2388ac', 'amount': Decimal('2.00000000'), 'confirmations': 887}
|
||||||
|
]
|
||||||
|
|
||||||
|
DEMO_DATA_1 = {
|
||||||
|
"listunspent": [{'address': 'GJS2ua47eTe19eBtCdjKuYdMUSJ6uZP8wL', 'txid': '71ea479aca93b1fb6f44b2081c711b54b1261711462ab6b7121b39763b19f328', 'account': '', 'amount': Decimal('30.00000000'), 'scriptPubKey': '76a91406738f84bdd0b95764154de9ed66976f3a76bb1288ac', 'vout': 1, 'confirmations': 2492}, {'address': 'GXpJEiv2koV4RxbpgHWtAHrPXKABPVtQ7x', 'txid': '286d116d918345a1a737f991f686a8a869126fefdd5db74de94ffdc6e68cd198', 'account': '', 'amount': Decimal('19.16000000'), 'scriptPubKey': '76a914994324dc98a6561b9dc8889c9dce6bf2cde8e85288ac', 'vout': 0, 'confirmations': 2267}, {'address': 'GZagQvQJcKLhEp5rkuXrEXMLGp1KLUjUQs', 'txid': '71ea479aca93b1fb6f44b2081c711b54b1261711462ab6b7121b39763b19f328', 'account': '', 'amount': Decimal('7.99800000'), 'scriptPubKey': '76a914aca0156313983bbc9a9f80fdc7435f483dedbb4588ac', 'vout': 0, 'confirmations': 2492}, {'address': 'GZagQvQJcKLhEp5rkuXrEXMLGp1KLUjUQs', 'txid': 'a8cf18264fce1adceb2e85b183373054d193659d9b5c8ae78b0930c5f41676c4', 'account': '', 'amount': Decimal('1859.99800000'), 'scriptPubKey': '76a914aca0156313983bbc9a9f80fdc7435f483dedbb4588ac', 'vout': 0, 'confirmations': 8}, {'address': 'GPFEKc6dsUbTJBWGCDmtwdB1DDhhdBAsue', 'txid': 'd3c7dd35d371312443327fc66982092f96ac612272aa4d66fdf01f653e4e85e1', 'account': '', 'amount': Decimal('2.00000000'), 'scriptPubKey': '76a9143b4124688a92cd772e787a305e7282409ca6cf2388ac', 'vout': 0, 'confirmations': 5356}, {'address': 'GcKiTtJ1BL5ddZJZEqL6Q3Ax52k5MYhebm', 'txid': '1e2f148fd3655fae34459791d7127afca31ac87d16af737eb9410f656ce56588', 'account': '', 'amount': Decimal('2.00000000'), 'scriptPubKey': '76a914cab3ef02db6ebf0d17f29e6ebfa43e94da58c4db88ac', 'vout': 1, 'confirmations': 5272}, {'address': 'GcKiTtJ1BL5ddZJZEqL6Q3Ax52k5MYhebm', 'txid': '586bd540f917f5472feee45d709554183395dd350b6ce76e4654534a4be28cf1', 'account': '', 'amount': Decimal('2.00000000'), 'scriptPubKey': '76a914cab3ef02db6ebf0d17f29e6ebfa43e94da58c4db88ac', 'vout': 0, 'confirmations': 5272}, {'address': 'GQtF5PvK4WRdAjMKjXCSmrqDbUEXz6W7z5', 'txid': '192b1cabf1b199909da44678daf0e4fac3559cd8e0c222c24ec86a9609decf84', 'confirmations': 5358, 'amount': Decimal('0.99800000'), 'scriptPubKey': '76a9144d3949f0f4207438acbb1c71177b0bb42bda614088ac', 'vout': 1}, {'address': 'GQ5V5DKrAo1QWoEpPLWfbwrbY55jqGFsCN', 'txid': '1e2f148fd3655fae34459791d7127afca31ac87d16af737eb9410f656ce56588', 'confirmations': 5272, 'amount': Decimal('0.07000000'), 'scriptPubKey': '76a91444616c30428e6c1581d899fc276e5205e61381c788ac', 'vout': 0}, {'address': 'GJS2ua47eTe19eBtCdjKuYdMUSJ6uZP8wL', 'txid': 'a8cf18264fce1adceb2e85b183373054d193659d9b5c8ae78b0930c5f41676c4', 'account': '', 'amount': Decimal('30.00000000'), 'scriptPubKey': '76a91406738f84bdd0b95764154de9ed66976f3a76bb1288ac', 'vout': 1, 'confirmations': 8}, {'address': 'GXa6TrdAHMMnKZY8UC1ACXq41LqLomsnrX', 'txid': '586bd540f917f5472feee45d709554183395dd350b6ce76e4654534a4be28cf1', 'confirmations': 5272, 'amount': Decimal('0.01200000'), 'scriptPubKey': '76a91496937e10d514abce8df94a8c024491f8a25e8ea388ac', 'vout': 1}],
|
||||||
|
"createrawtransaction": "010000000228f3193b76391b12b7b62a46111726b1541b711c08b2446ffbb193ca9a47ea710100000000ffffffff98d18ce6c6fd4fe94db75dddef6f1269a8a886f691f937a7a14583916d116d280000000000ffffffff02c0c93072000000001976a914aca0156313983bbc9a9f80fdc7435f483dedbb4588ac005ed0b2000000001976a91406738f84bdd0b95764154de9ed66976f3a76bb1288ac00000000",
|
||||||
|
"signrawtransaction": {'complete': True, 'hex': '010000000228f3193b76391b12b7b62a46111726b1541b711c08b2446ffbb193ca9a47ea71010000006b483045022100ee57ecd2d136ac6bba120db2c4ded470d66e7b1f1f550186a795a847d5c499a20220188847e8a1df6d68aa202203a126ddac4d6b6f0d20afef7ea37fc3af29b6c879012103ce84ad18c5997fc1ffd7ef124bd39f7c7fa201ab42fc86daafa43067491a0d6bffffffff98d18ce6c6fd4fe94db75dddef6f1269a8a886f691f937a7a14583916d116d28000000006b483045022038d44f3f3b299ed7838d3d53dc597b9e96fe47e6b691d1d61805be226ddc2ee0022100d5ea3855d88cd3cc762251956f2cfee7e8f1def8df3f4feb88fd17a8e7696de5012103821021c870844de0661765569edfbf617fc5122217c86e0e98fac8326f00dbfcffffffff02c0c93072000000001976a914aca0156313983bbc9a9f80fdc7435f483dedbb4588ac005ed0b2000000001976a91406738f84bdd0b95764154de9ed66976f3a76bb1288ac00000000'},
|
||||||
|
"sendrawtransaction": "71ea479aca93b1fb6f44b2081c711b54b1261711462ab6b7121b39763b19f328"
|
||||||
|
}
|
191
wallet/tests/tests.py
Normal file
191
wallet/tests/tests.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
from django.test import TestCase, SimpleTestCase
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.conf import settings
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from wallet.models import Account, Address, DepositTransaction
|
||||||
|
from wallet.forms import SendCoins
|
||||||
|
from wallet.tasks import check_received_transactions, check_confirmed_transactions
|
||||||
|
|
||||||
|
from wallet.tests.fake_data import DEMO_TRANSACTIONS, DEMO_UNSPENT, DEMO_DATA_1
|
||||||
|
from wallet import gostcoin
|
||||||
|
|
||||||
|
from integral_auth.utils import rand_string
|
||||||
|
|
||||||
|
class SendCoinsFormTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("tester", password="asdasdasd")
|
||||||
|
Account.objects.create(user=self.user, name="A2aac03D5F5Adae0")
|
||||||
|
|
||||||
|
def test_valid_data(self):
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "0.1"})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
form = SendCoins({"recipient": self.user.account.name, "amount": "1"})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_invalid_data(self):
|
||||||
|
form = SendCoins({})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": self.user.account.name})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": self.user.account.name, "amount": ""})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"amount": "1"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "aaaa"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "0.000000001"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "0.0099"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "-0.1"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx00",
|
||||||
|
"amount": "0.1"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "aaa",
|
||||||
|
"amount": "0.1"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": ")%$#",
|
||||||
|
"amount": "0.1"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "abcdEFGH123456780", "amount": "1"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = SendCoins({"recipient": "APem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "0.1"})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
def test_valid_cleaned_data(self):
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "0.1"})
|
||||||
|
form.is_valid()
|
||||||
|
|
||||||
|
self.assertEqual(form.cleaned_data["amount"], Decimal("0.1"))
|
||||||
|
|
||||||
|
def test_with_balance(self):
|
||||||
|
self.user.account.balance = 100
|
||||||
|
self.user.account.save()
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "1"}, user=self.user)
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_without_balance(self):
|
||||||
|
self.user.account.balance = 1
|
||||||
|
self.user.account.save()
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "100"}, user=self.user)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
def test_without_fee_balance(self):
|
||||||
|
with self.settings(GST_NETWORK_FEE=Decimal('0.02'),
|
||||||
|
SERVICE_FEE=Decimal('0.0')):
|
||||||
|
self.user.account.balance = 100
|
||||||
|
self.user.account.save()
|
||||||
|
form = SendCoins({"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "100"}, user=self.user)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
class AccountModelTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("tester", password="asdasdasd")
|
||||||
|
Account.objects.create(user=self.user, name="A2aac03D5F5Adae0")
|
||||||
|
self.user2 = User.objects.create_user("tester2", password="asdasdasd")
|
||||||
|
Account.objects.create(user=self.user2, name="B6cac03D5F5Adae0")
|
||||||
|
|
||||||
|
|
||||||
|
class GostcoinTests(TestCase):
|
||||||
|
|
||||||
|
def test_select_inputs(self):
|
||||||
|
conn = MagicMock()
|
||||||
|
conn.listunspent.return_value = DEMO_UNSPENT
|
||||||
|
|
||||||
|
inputs, total = gostcoin.select_inputs(conn, Decimal("212"))
|
||||||
|
self.assertEquals(total, Decimal("233.06"))
|
||||||
|
self.assertEquals(len(inputs), 3)
|
||||||
|
|
||||||
|
def test_create_raw_tx(self):
|
||||||
|
conn = MagicMock()
|
||||||
|
conn.listunspent.return_value = DEMO_DATA_1["listunspent"]
|
||||||
|
conn.createrawtransaction.return_value = DEMO_DATA_1["createrawtransaction"]
|
||||||
|
rawtx = gostcoin.create_raw_tx(conn,
|
||||||
|
"GXaNvzURu4fRAkjSodybjcXnwwUPKxB6rQ", Decimal("212"))
|
||||||
|
self.assertEquals(rawtx, DEMO_DATA_1["createrawtransaction"])
|
||||||
|
|
||||||
|
|
||||||
|
class CeleryTaskTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("tester", password="asdasdasd")
|
||||||
|
Account.objects.create(user=self.user, name="A2aac03D5F5Adae0")
|
||||||
|
self.user2 = User.objects.create_user("tester2", password="asdasdasd")
|
||||||
|
Account.objects.create(user=self.user2, name="B6cac03D5F5Adae0")
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_received_transactions(self):
|
||||||
|
Address.objects.create(account=self.user.account, used=False,
|
||||||
|
address="GXaNvzURu4fRAkjSodybjcXnwwUPKxB6rQ")
|
||||||
|
Address.objects.create(account=self.user2.account, used=False,
|
||||||
|
address="GHwVSdza9QB5zPa9kZWjFdByynTAtDb6M9")
|
||||||
|
|
||||||
|
conn = MagicMock()
|
||||||
|
conn.listtransactions.return_value = DEMO_TRANSACTIONS
|
||||||
|
conn.getnewaddress = lambda: "G" + rand_string(33)
|
||||||
|
|
||||||
|
result = check_received_transactions(conn)
|
||||||
|
|
||||||
|
tx = DepositTransaction.objects.get(
|
||||||
|
txid='3c03439fab4f53c97185e66217fe5fe61cdc4e80b25dfe5a0d19bf0b8c77f994')
|
||||||
|
self.assertEqual(tx.account, self.user.account)
|
||||||
|
self.assertEqual(tx.address.used, True)
|
||||||
|
|
||||||
|
self.assertEqual(self.user.account.address_set.filter(used=True).first().used, True)
|
||||||
|
|
||||||
|
tx = DepositTransaction.objects.get(
|
||||||
|
txid='a94b83eec4f267aa503cc9ec65a229350cae60799ff73c28283a7aa5dad15435')
|
||||||
|
self.assertEqual(tx.account, self.user2.account)
|
||||||
|
self.assertEqual(tx.address.used, True)
|
||||||
|
|
||||||
|
def test_check_confirmed_transactions(self):
|
||||||
|
Address.objects.create(account=self.user.account, used=False,
|
||||||
|
address="GXaNvzURu4fRAkjSodybjcXnwwUPKxB6rQ")
|
||||||
|
|
||||||
|
conn = MagicMock()
|
||||||
|
conn.listtransactions.return_value = DEMO_TRANSACTIONS
|
||||||
|
conn.gettransaction.return_value = {'time': 1491951811, 'blocktime': 1491951914, 'txid': '3c03439fab4f53c97185e66217fe5fe61cdc4e80b25dfe5a0d19bf0b8c77f994', 'amount': Decimal('616.00000000'), 'confirmations': 7430, 'blockindex': 1, 'blockhash': '0000004d184d3df898180937bd22c2717543706443b33ec7e16e369fa9d8c334', 'timereceived': 1491951811, 'details': [{'account': '', 'amount': Decimal('616.00000000'), 'address': 'GXaNvzURu4fRAkjSodybjcXnwwUPKxB6rQ', 'category': 'receive'}]}
|
||||||
|
conn.getreceivedbyaddress.return_value = Decimal('616.00000000')
|
||||||
|
conn.getnewaddress = lambda: "G" + rand_string(33)
|
||||||
|
|
||||||
|
check_received_transactions(conn)
|
||||||
|
check_confirmed_transactions(conn)
|
||||||
|
|
||||||
|
tx = DepositTransaction.objects.get(
|
||||||
|
txid='3c03439fab4f53c97185e66217fe5fe61cdc4e80b25dfe5a0d19bf0b8c77f994')
|
||||||
|
self.assertEqual(tx.confirmed, True)
|
||||||
|
self.assertEqual(tx.account.balance, 616)
|
||||||
|
|
||||||
|
check_confirmed_transactions(conn)
|
||||||
|
|
||||||
|
tx = DepositTransaction.objects.get(
|
||||||
|
txid='3c03439fab4f53c97185e66217fe5fe61cdc4e80b25dfe5a0d19bf0b8c77f994')
|
||||||
|
self.assertEqual(tx.account.balance, 616)
|
110
wallet/tests/tests_func.py
Normal file
110
wallet/tests/tests_func.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
from django.test import TestCase, RequestFactory, Client
|
||||||
|
from django.urls import reverse_lazy, reverse
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from decimal import Decimal
|
||||||
|
from wallet.models import Account, Address
|
||||||
|
|
||||||
|
import wallet.views as wv
|
||||||
|
|
||||||
|
from wallet.tests.fake_data import DEMO_TRANSACTIONS, DEMO_UNSPENT, DEMO_DATA_1
|
||||||
|
|
||||||
|
class SendCoinsFormTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("tester", password="asdasdasd")
|
||||||
|
account = Account.objects.create(user=self.user,
|
||||||
|
name="A2aac03D5F5Adae0")
|
||||||
|
Address.objects.create(account=account,
|
||||||
|
address="GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx", used=False)
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_view_wallet(self):
|
||||||
|
wv.conn = MagicMock()
|
||||||
|
wv.conn.getnewaddress.return_value = "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx"
|
||||||
|
self.user.account.balance = Decimal("0.998")
|
||||||
|
self.user.account.save()
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user, request.session = self.user, SessionStore()
|
||||||
|
resp = wv.index(request)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
def test_send_coins_not_enough_on_balance(self):
|
||||||
|
wv.conn = MagicMock()
|
||||||
|
wv.conn.getnewaddress.return_value = "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx"
|
||||||
|
self.user.account.balance = Decimal("1")
|
||||||
|
self.user.account.save()
|
||||||
|
request = self.factory.post("/")
|
||||||
|
request.user, request.session = self.user, SessionStore()
|
||||||
|
request.POST = {"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "999"}
|
||||||
|
|
||||||
|
resp = wv.index(request)
|
||||||
|
self.assertIn("Not enough", str(resp.content))
|
||||||
|
|
||||||
|
def test_send_coins_locally(self):
|
||||||
|
wv.conn = MagicMock()
|
||||||
|
wv.conn.getnewaddress.return_value = "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx"
|
||||||
|
wv.messages = MagicMock()
|
||||||
|
|
||||||
|
user2 = User.objects.create_user("second", password="asdasdasd")
|
||||||
|
Account.objects.create(user=user2, name="B1bbd03D5F5Adae0")
|
||||||
|
|
||||||
|
self.user.account.balance = Decimal("10")
|
||||||
|
self.user.account.save()
|
||||||
|
|
||||||
|
request = self.factory.post("/")
|
||||||
|
request.user, request.session = self.user, SessionStore()
|
||||||
|
request.POST = {"recipient": user2.account.name,
|
||||||
|
"amount": "1"}
|
||||||
|
|
||||||
|
resp = wv.index(request)
|
||||||
|
|
||||||
|
user2.account.refresh_from_db()
|
||||||
|
self.user.account.refresh_from_db()
|
||||||
|
self.assertEqual(user2.account.balance, 1)
|
||||||
|
self.assertEqual(self.user.account.balance, 9)
|
||||||
|
|
||||||
|
def test_send_coins_locally_not_enough(self):
|
||||||
|
wv.conn = MagicMock()
|
||||||
|
wv.conn.getnewaddress.return_value = "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx"
|
||||||
|
wv.messages = MagicMock()
|
||||||
|
|
||||||
|
user2 = User.objects.create_user("second", password="asdasdasd")
|
||||||
|
Account.objects.create(user=user2, name="B1bbd03D5F5Adae0")
|
||||||
|
|
||||||
|
self.user.account.balance = Decimal("10")
|
||||||
|
self.user.account.save()
|
||||||
|
|
||||||
|
request = self.factory.post("/")
|
||||||
|
request.user, request.session = self.user, SessionStore()
|
||||||
|
request.POST = {"recipient": user2.account.name,
|
||||||
|
"amount": "100"}
|
||||||
|
|
||||||
|
resp = wv.index(request)
|
||||||
|
self.assertIn("Not enough", str(resp.content))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_coins_to_address(self):
|
||||||
|
wv.conn = MagicMock()
|
||||||
|
wv.conn.getnewaddress.return_value = "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx"
|
||||||
|
wv.conn.listunspent.return_value = DEMO_UNSPENT
|
||||||
|
wv.conn.listunspent.return_value = DEMO_DATA_1["listunspent"]
|
||||||
|
wv.conn.sendrawtransaction.return_value = "71ea479aca93b1fb6f44b2081c711b54b1261711462ab6b7121b39763b19f328"
|
||||||
|
wv.messages = MagicMock()
|
||||||
|
|
||||||
|
self.user.account.balance = Decimal("10")
|
||||||
|
self.user.account.save()
|
||||||
|
request = self.factory.post("/")
|
||||||
|
request.user, request.session = self.user, SessionStore()
|
||||||
|
request.POST = {"recipient": "GPem1MVc49r7dxsUDL4sKFvq79rCtqs5Sx",
|
||||||
|
"amount": "1"}
|
||||||
|
|
||||||
|
resp = wv.index(request)
|
||||||
|
self.assertIs(resp.status_code, 200)
|
||||||
|
self.assertTrue(wv.messages.success.called)
|
||||||
|
|
||||||
|
|
75
wallet/views.py
Normal file
75
wallet/views.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from .forms import SendCoins
|
||||||
|
from .models import Account, WithdrawalTransaction
|
||||||
|
from .gostcoin import GostCoinException, create_raw_tx
|
||||||
|
from .gostcoin import GOSTCOIN_CONNECTION as conn
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger("django")
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def index(request):
|
||||||
|
form = SendCoins(request.POST or None, user=request.user)
|
||||||
|
|
||||||
|
if request.method == "POST" and form.is_valid():
|
||||||
|
recipient, amount = form.cleaned_data["recipient"], \
|
||||||
|
form.cleaned_data["amount"]
|
||||||
|
local_transfer = len(recipient) == 16
|
||||||
|
if local_transfer:
|
||||||
|
total_amount = amount
|
||||||
|
else:
|
||||||
|
if not conn.validateaddress(recipient)["isvalid"]:
|
||||||
|
raise GostCoinException("Invalid address")
|
||||||
|
|
||||||
|
total_amount = amount + settings.GST_NETWORK_FEE + \
|
||||||
|
settings.SERVICE_FEE
|
||||||
|
|
||||||
|
# TODO: check if address exists, then transfer locally
|
||||||
|
with transaction.atomic():
|
||||||
|
a = Account.objects.select_for_update().get(user=request.user)
|
||||||
|
if recipient == a.name:
|
||||||
|
raise GostCoinException("Can't transfer to yourself")
|
||||||
|
if total_amount > a.balance:
|
||||||
|
raise GostCoinException("Not enough coins on balance")
|
||||||
|
|
||||||
|
if local_transfer:
|
||||||
|
n = Account.objects.select_for_update().get(name=recipient)
|
||||||
|
a.balance -= total_amount
|
||||||
|
a.save()
|
||||||
|
n.balance += total_amount
|
||||||
|
n.save()
|
||||||
|
messages.success(request, "Transfer succeeded")
|
||||||
|
form = SendCoins()
|
||||||
|
else:
|
||||||
|
a.balance -= total_amount
|
||||||
|
a.save()
|
||||||
|
form = SendCoins()
|
||||||
|
|
||||||
|
if not local_transfer:
|
||||||
|
rawtx = create_raw_tx(conn, recipient, amount)
|
||||||
|
txid = conn.sendrawtransaction(
|
||||||
|
conn.signrawtransaction(rawtx)["hex"])
|
||||||
|
messages.success(request,
|
||||||
|
"Transfer succeeded. Transaction id: {}".format(txid))
|
||||||
|
WithdrawalTransaction.objects.create(account=request.user.account,
|
||||||
|
txid=txid, address=recipient, amount=amount, confirmed=False)
|
||||||
|
|
||||||
|
request.user.account.refresh_from_db()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"fees": {"network": settings.GST_NETWORK_FEE,
|
||||||
|
"service": settings.SERVICE_FEE},
|
||||||
|
"address": request.user.account.address_set.filter(used=False).first().address,
|
||||||
|
"balance": request.user.account.balance,
|
||||||
|
"deposit_transactions": request.user.account.deposittransaction_set.all(),
|
||||||
|
"withdrawal_transactions": request.user.account.withdrawaltransaction_set.all(),
|
||||||
|
"form": form
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "wallet/index_page.html", context)
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user