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 @@ |
|||||||
|
*.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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
# import celery stuff |
||||||
|
from .celery import app as celery_app |
||||||
|
|
||||||
|
__all__ = ['celery_app'] |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
""" |
||||||
|
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 @@ |
|||||||
|
"""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 @@ |
|||||||
|
""" |
||||||
|
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 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
|
||||||
|
# Register your models here. |
@ -0,0 +1,5 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
|
||||||
|
class IntegralAuthConfig(AppConfig): |
||||||
|
name = 'integral_auth' |
@ -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 @@ |
|||||||
|
from django.db import models |
||||||
|
|
||||||
|
# Create your models here. |
@ -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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
#!/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 @@ |
|||||||
|
celery |
||||||
|
Django==1.11 |
||||||
|
django-crispy-forms |
||||||
|
django-simple-captcha |
||||||
|
Markdown |
||||||
|
redis |
||||||
|
python-bitcoinrpc |
@ -0,0 +1,3 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
|
||||||
|
# Register your models here. |
@ -0,0 +1,5 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
|
||||||
|
class WalletConfig(AppConfig): |
||||||
|
name = 'wallet' |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
# -*- 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 @@ |
|||||||
|
# -*- 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 @@ |
|||||||
|
# -*- 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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
/*! 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 @@ |
|||||||
|
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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
{% 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 @@ |
|||||||
|
<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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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