Browse Source

First public release

master
l-n-s 7 years ago
commit
294982b746
  1. 16
      .gitignore
  2. 20
      LICENSE
  3. 11
      README.md
  4. 0
      data/.keep
  5. 4
      gst_web_wallet/__init__.py
  6. 21
      gst_web_wallet/celery.py
  7. 58
      gst_web_wallet/local_settings.template
  8. 113
      gst_web_wallet/settings.py
  9. 27
      gst_web_wallet/urls.py
  10. 16
      gst_web_wallet/wsgi.py
  11. 0
      integral_auth/__init__.py
  12. 3
      integral_auth/admin.py
  13. 5
      integral_auth/apps.py
  14. 23
      integral_auth/forms.py
  15. 0
      integral_auth/migrations/__init__.py
  16. 3
      integral_auth/models.py
  17. 25
      integral_auth/templates/integral_auth/base.html
  18. 14
      integral_auth/templates/integral_auth/signin.html
  19. 14
      integral_auth/templates/integral_auth/signup.html
  20. 143
      integral_auth/tests_func.py
  21. 41
      integral_auth/tests_unit.py
  22. 21
      integral_auth/urls.py
  23. 7
      integral_auth/utils.py
  24. 77
      integral_auth/views.py
  25. 22
      manage.py
  26. 7
      requirements.txt
  27. 0
      wallet/__init__.py
  28. 3
      wallet/admin.py
  29. 5
      wallet/apps.py
  30. 58
      wallet/forms.py
  31. 42
      wallet/gostcoin.py
  32. 47
      wallet/migrations/0001_initial.py
  33. 55
      wallet/migrations/0002_auto_20171021_2029.py
  34. 25
      wallet/migrations/0003_auto_20171021_2051.py
  35. 0
      wallet/migrations/__init__.py
  36. 38
      wallet/models.py
  37. 7
      wallet/static/wallet/css/bootstrap.css
  38. 11
      wallet/static/wallet/css/bootstrap.css.bak
  39. 19
      wallet/static/wallet/css/main.css
  40. 424
      wallet/static/wallet/css/normalize.css
  41. BIN
      wallet/static/wallet/favicon.ico
  42. 66
      wallet/tasks.py
  43. 40
      wallet/templates/wallet/base.html
  44. 85
      wallet/templates/wallet/index_page.html
  45. 18
      wallet/templates/wallet/navigation.html
  46. 0
      wallet/templatetags/__init__.py
  47. 10
      wallet/templatetags/wallet_tags.py
  48. 93
      wallet/tests/fake_data.py
  49. 191
      wallet/tests/tests.py
  50. 110
      wallet/tests/tests_func.py
  51. 75
      wallet/views.py

16
.gitignore vendored

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

20
LICENSE

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

11
README.md

@ -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
data/.keep

4
gst_web_wallet/__init__.py

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
# import celery stuff
from .celery import app as celery_app
__all__ = ['celery_app']

21
gst_web_wallet/celery.py

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

58
gst_web_wallet/local_settings.template

@ -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': '',
# }
# }

113
gst_web_wallet/settings.py

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

27
gst_web_wallet/urls.py

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

16
gst_web_wallet/wsgi.py

@ -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
integral_auth/__init__.py

3
integral_auth/admin.py

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
integral_auth/apps.py

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
from django.apps import AppConfig
class IntegralAuthConfig(AppConfig):
name = 'integral_auth'

23
integral_auth/forms.py

@ -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
integral_auth/migrations/__init__.py

3
integral_auth/models.py

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

25
integral_auth/templates/integral_auth/base.html

@ -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> &#8226;
<a href="{% url 'integral_auth:signup' %}">
Sign up</a>
</div>
</div>
</div>
{% endblock %}

14
integral_auth/templates/integral_auth/signin.html

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

14
integral_auth/templates/integral_auth/signup.html

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

143
integral_auth/tests_func.py

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

41
integral_auth/tests_unit.py

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

21
integral_auth/urls.py

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

7
integral_auth/utils.py

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

77
integral_auth/views.py

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

22
manage.py

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

7
requirements.txt

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
celery
Django==1.11
django-crispy-forms
django-simple-captcha
Markdown
redis
python-bitcoinrpc

0
wallet/__init__.py

3
wallet/admin.py

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
wallet/apps.py

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
from django.apps import AppConfig
class WalletConfig(AppConfig):
name = 'wallet'

58
wallet/forms.py

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

42
wallet/gostcoin.py

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

47
wallet/migrations/0001_initial.py

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

55
wallet/migrations/0002_auto_20171021_2029.py

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

25
wallet/migrations/0003_auto_20171021_2051.py

@ -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
wallet/migrations/__init__.py

38
wallet/models.py

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

7
wallet/static/wallet/css/bootstrap.css vendored

File diff suppressed because one or more lines are too long

11
wallet/static/wallet/css/bootstrap.css.bak

File diff suppressed because one or more lines are too long

19
wallet/static/wallet/css/main.css

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

424
wallet/static/wallet/css/normalize.css vendored

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

BIN
wallet/static/wallet/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

66
wallet/tasks.py

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

40
wallet/templates/wallet/base.html

@ -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>&copy; Purple Tech, 2017. &nbsp;&nbsp;
GST donations: GM5cSsCW14eB822rEHqTGFpm2XKB9nYndd</p>
</div>
</body>
</html>

85
wallet/templates/wallet/index_page.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 %}

18
wallet/templates/wallet/navigation.html

@ -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
wallet/templatetags/__init__.py

10
wallet/templatetags/wallet_tags.py

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

93
wallet/tests/fake_data.py

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

191
wallet/tests/tests.py

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

110
wallet/tests/tests_func.py

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

75
wallet/views.py

@ -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…
Cancel
Save