mirror of https://github.com/GOSTSec/gostweb
l-n-s
7 years ago
commit
294982b746
51 changed files with 2113 additions and 0 deletions
@ -0,0 +1,16 @@
@@ -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/* |
@ -0,0 +1,20 @@
@@ -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. |
@ -0,0 +1,11 @@
@@ -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,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
# import celery stuff |
||||
from .celery import app as celery_app |
||||
|
||||
__all__ = ['celery_app'] |
@ -0,0 +1,21 @@
@@ -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)) |
@ -0,0 +1,58 @@
@@ -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': '', |
||||
# } |
||||
# } |
@ -0,0 +1,113 @@
@@ -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'} |
||||
|
@ -0,0 +1,27 @@
@@ -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')), |
||||
] |
@ -0,0 +1,16 @@
@@ -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,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin |
||||
|
||||
# Register your models here. |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class IntegralAuthConfig(AppConfig): |
||||
name = 'integral_auth' |
@ -0,0 +1,23 @@
@@ -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,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
from django.db import models |
||||
|
||||
# Create your models here. |
@ -0,0 +1,25 @@
@@ -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 %} |
@ -0,0 +1,14 @@
@@ -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 %} |
@ -0,0 +1,14 @@
@@ -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 %} |
@ -0,0 +1,143 @@
@@ -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") |
||||
|
@ -0,0 +1,41 @@
@@ -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()) |
||||
|
@ -0,0 +1,21 @@
@@ -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'), |
||||
] |
||||
|
@ -0,0 +1,7 @@
@@ -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)]) |
||||
|
@ -0,0 +1,77 @@
@@ -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") |
@ -0,0 +1,22 @@
@@ -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) |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
celery |
||||
Django==1.11 |
||||
django-crispy-forms |
||||
django-simple-captcha |
||||
Markdown |
||||
redis |
||||
python-bitcoinrpc |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin |
||||
|
||||
# Register your models here. |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class WalletConfig(AppConfig): |
||||
name = 'wallet' |
@ -0,0 +1,58 @@
@@ -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')) |
@ -0,0 +1,42 @@
@@ -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) |
@ -0,0 +1,47 @@
@@ -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')), |
||||
], |
||||
), |
||||
] |
@ -0,0 +1,55 @@
@@ -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', |
||||
), |
||||
] |
@ -0,0 +1,25 @@
@@ -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,0 +1,38 @@
@@ -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) |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,19 @@
@@ -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; |
||||
} |
@ -0,0 +1,424 @@
@@ -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; |
||||
} |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,66 @@
@@ -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() |
||||
|
@ -0,0 +1,40 @@
@@ -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> |
@ -0,0 +1,85 @@
@@ -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 %} |
@ -0,0 +1,18 @@
@@ -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,0 +1,10 @@
@@ -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) |
||||
|
@ -0,0 +1,93 @@
@@ -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" |
||||
} |
@ -0,0 +1,191 @@
@@ -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) |
@ -0,0 +1,110 @@
@@ -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) |
||||
|
||||
|
@ -0,0 +1,75 @@
@@ -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…
Reference in new issue