Compare commits
No commits in common. "e90c89dcf12043d855655346d6598180922ac526" and "77c8b675404c0602800fc2509ccc194691d5d259" have entirely different histories.
e90c89dcf1
...
77c8b67540
|
@ -1,165 +1,11 @@
|
|||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
*.pyc
|
||||
*.swp
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.python_history
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
# lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
stack.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
env-glibc/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
.bash_history
|
||||
.vscode/
|
||||
core/static/admin
|
||||
core/static/debug_toolbar
|
||||
|
||||
venv/
|
||||
keys/
|
||||
handler/settings.ini
|
||||
handler/otp.key
|
||||
handler/certs/
|
||||
.vscode/
|
||||
|
|
28
Dockerfile
28
Dockerfile
|
@ -1,28 +0,0 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM python:3.10
|
||||
ARG OPERATION
|
||||
|
||||
RUN useradd -d /code xf
|
||||
RUN mkdir -p /code
|
||||
RUN chown -R xf:xf /code
|
||||
|
||||
RUN mkdir -p /conf/static
|
||||
RUN chown -R xf:xf /conf
|
||||
|
||||
RUN mkdir /venv
|
||||
RUN chown xf:xf /venv
|
||||
|
||||
USER xf
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
WORKDIR /code
|
||||
COPY requirements.txt /code/
|
||||
RUN python -m venv /venv
|
||||
RUN . /venv/bin/activate && pip install -r requirements.txt
|
||||
|
||||
# CMD . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini
|
||||
|
||||
CMD if [ "$OPERATION" = "uwsgi" ] ; then . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini ; else . /venv/bin/activate && exec python manage.py runserver 0.0.0.0:8000; fi
|
||||
|
||||
# CMD . /venv/bin/activate && uvicorn --reload --reload-include *.html --workers 2 --uds /var/run/socks/app.sock app.asgi:application
|
||||
# CMD . /venv/bin/activate && gunicorn -b 0.0.0.0:8000 --reload app.asgi:application -k uvicorn.workers.UvicornWorker
|
26
Makefile
26
Makefile
|
@ -1,26 +0,0 @@
|
|||
run:
|
||||
docker-compose --env-file=stack.env up -d
|
||||
|
||||
build:
|
||||
docker-compose --env-file=stack.env build
|
||||
|
||||
stop:
|
||||
docker-compose --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose --env-file=stack.env logs -f
|
||||
|
||||
test:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
|
||||
|
||||
migrate:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||
|
||||
makemigrations:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py makemigrations"
|
||||
|
||||
auth:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py createsuperuser"
|
||||
|
||||
token:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py addstatictoken m"
|
16
app/asgi.py
16
app/asgi.py
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
ASGI config for app project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||
|
||||
application = get_asgi_application()
|
|
@ -1,55 +0,0 @@
|
|||
from os import getenv
|
||||
|
||||
trues = ("t", "true", "yes", "y", "1")
|
||||
|
||||
# URLs
|
||||
DOMAIN = getenv("DOMAIN", "example.com")
|
||||
URL = getenv("URL", f"https://{DOMAIN}")
|
||||
|
||||
# Access control
|
||||
ALLOWED_HOSTS = getenv("ALLOWED_HOSTS", f"127.0.0.1,{DOMAIN}").split(",")
|
||||
|
||||
# CSRF
|
||||
CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",")
|
||||
|
||||
# Stripe
|
||||
BILLING_ENABLED = getenv("BILLING_ENABLED", "false").lower() in trues
|
||||
|
||||
STRIPE_TEST = getenv("STRIPE_TEST", "true").lower() in trues
|
||||
STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "")
|
||||
STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "")
|
||||
|
||||
STRIPE_API_KEY_PROD = getenv("STRIPE_API_KEY_PROD", "")
|
||||
STRIPE_PUBLIC_API_KEY_PROD = getenv("STRIPE_PUBLIC_API_KEY_PROD", "")
|
||||
|
||||
STRIPE_ENDPOINT_SECRET = getenv("STRIPE_ENDPOINT_SECRET", "")
|
||||
STATIC_ROOT = getenv("STATIC_ROOT", "")
|
||||
SECRET_KEY = getenv("SECRET_KEY", "")
|
||||
|
||||
STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "")
|
||||
|
||||
REGISTRATION_OPEN = getenv("REGISTRATION_OPEN", "false").lower() in trues
|
||||
|
||||
NOTIFY_TOPIC = getenv("NOTIFY_TOPIC", "great-pluto")
|
||||
|
||||
ELASTICSEARCH_USERNAME = getenv("ELASTICSEARCH_USERNAME", "elastic")
|
||||
ELASTICSEARCH_PASSWORD = getenv("ELASTICSEARCH_PASSWORD", "changeme")
|
||||
ELASTICSEARCH_HOST = getenv("ELASTICSEARCH_HOST", "localhost")
|
||||
ELASTICSEARCH_TLS = getenv("ELASTICSEARCH_TLS", "false") in trues
|
||||
ELASTICSEARCH_INDEX = getenv("ELASTICSEARCH_INDEX", "pluto")
|
||||
ELASTICSEARCH_INDEX_ADS = getenv("ELASTICSEARCH_INDEX_ADS", "ads")
|
||||
DEBUG = getenv("DEBUG", "false").lower() in trues
|
||||
PROFILER = getenv("PROFILER", "false").lower() in trues
|
||||
|
||||
DUMMY = getenv("DUMMY", "false").lower() in trues
|
||||
|
||||
if DEBUG:
|
||||
import socket # only if you haven't already imported this
|
||||
|
||||
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
|
||||
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [
|
||||
"127.0.0.1",
|
||||
"10.0.2.2",
|
||||
]
|
||||
|
||||
SETTINGS_EXPORT = ["BILLING_ENABLED", "URL"]
|
230
app/settings.py
230
app/settings.py
|
@ -1,230 +0,0 @@
|
|||
"""
|
||||
Django settings for app project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.0.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
# MOVED TO local_settings.py
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"core",
|
||||
"django.contrib.admin",
|
||||
# 'core.apps.LibraryAdminConfig', # our custom OTP'ed admin
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"debug_toolbar",
|
||||
"template_profiler_panel",
|
||||
"django_htmx",
|
||||
"crispy_forms",
|
||||
"crispy_bulma",
|
||||
# "django_tables2",
|
||||
# "django_tables2_bulma_template",
|
||||
"django_otp",
|
||||
"django_otp.plugins.otp_totp",
|
||||
# "django_otp.plugins.otp_email",
|
||||
# 'django_otp.plugins.otp_hotp',
|
||||
"django_otp.plugins.otp_static",
|
||||
"two_factor",
|
||||
"two_factor.plugins.phonenumber",
|
||||
# "two_factor.plugins.email",
|
||||
# "two_factor.plugins.yubikey",
|
||||
# "otp_yubikey",
|
||||
"mixins",
|
||||
"cachalot",
|
||||
]
|
||||
|
||||
|
||||
# Performance optimisations
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "unix:///var/run/socks/redis.sock",
|
||||
"OPTIONS": {
|
||||
"db": "10",
|
||||
# "parser_class": "django_redis.cache.RedisCache",
|
||||
"pool_class": "redis.BlockingConnectionPool",
|
||||
},
|
||||
}
|
||||
}
|
||||
# CACHE_MIDDLEWARE_ALIAS = 'default'
|
||||
# CACHE_MIDDLEWARE_SECONDS = '600'
|
||||
# CACHE_MIDDLEWARE_KEY_PREFIX = ''
|
||||
|
||||
CRISPY_TEMPLATE_PACK = "bulma"
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
||||
DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html"
|
||||
|
||||
MIDDLEWARE = [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
# 'django.middleware.cache.UpdateCacheMiddleware',
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
# 'django.middleware.cache.FetchFromCacheMiddleware',
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django_otp.middleware.OTPMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "app.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [os.path.join(BASE_DIR, "core/templates")],
|
||||
"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",
|
||||
"core.util.django_settings_export.settings_export",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "app.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": "/conf/db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": f"django.contrib.auth.password_validation.{name}"}
|
||||
for name in [
|
||||
"UserAttributeSimilarityValidator",
|
||||
"MinimumLengthValidator",
|
||||
"CommonPasswordValidator",
|
||||
"NumericPasswordValidator",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
AUTH_USER_MODEL = "core.User"
|
||||
|
||||
|
||||
LOGOUT_REDIRECT_URL = "home"
|
||||
|
||||
LOGIN_REDIRECT_URL = "home"
|
||||
# LOGIN_URL = "/accounts/login/"
|
||||
|
||||
# 2FA
|
||||
LOGIN_URL = "two_factor:login"
|
||||
# LOGIN_REDIRECT_URL = 'two_factor:profile'
|
||||
|
||||
# ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"]
|
||||
ALLOWED_PAYMENT_METHODS = ["card"]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_PARSER_CLASSES": [
|
||||
"rest_framework.parsers.JSONParser",
|
||||
]
|
||||
}
|
||||
|
||||
INTERNAL_IPS = [
|
||||
"127.0.0.1",
|
||||
"10.1.10.11",
|
||||
]
|
||||
|
||||
DEBUG_TOOLBAR_PANELS = [
|
||||
"template_profiler_panel.panels.template.TemplateProfilerPanel",
|
||||
"debug_toolbar.panels.history.HistoryPanel",
|
||||
"debug_toolbar.panels.versions.VersionsPanel",
|
||||
"debug_toolbar.panels.timer.TimerPanel",
|
||||
"debug_toolbar.panels.settings.SettingsPanel",
|
||||
"debug_toolbar.panels.headers.HeadersPanel",
|
||||
"debug_toolbar.panels.request.RequestPanel",
|
||||
"debug_toolbar.panels.sql.SQLPanel",
|
||||
"debug_toolbar.panels.staticfiles.StaticFilesPanel",
|
||||
"debug_toolbar.panels.templates.TemplatesPanel",
|
||||
"debug_toolbar.panels.cache.CachePanel",
|
||||
"debug_toolbar.panels.signals.SignalsPanel",
|
||||
"debug_toolbar.panels.logging.LoggingPanel",
|
||||
"debug_toolbar.panels.redirects.RedirectsPanel",
|
||||
"debug_toolbar.panels.profiling.ProfilingPanel",
|
||||
"cachalot.panels.CachalotPanel",
|
||||
]
|
||||
|
||||
from app.local_settings import * # noqa
|
||||
|
||||
if PROFILER: # noqa - trust me its there
|
||||
import pyroscope
|
||||
|
||||
pyroscope.configure(
|
||||
application_name="neptune",
|
||||
server_address="http://pyroscope:4040",
|
||||
auth_token=os.getenv("PYROSCOPE_AUTH_TOKEN", ""),
|
||||
# tags = {
|
||||
# "region": f'{os.getenv("REGION")}',
|
||||
# }
|
||||
)
|
||||
|
||||
|
||||
def show_toolbar(request):
|
||||
return DEBUG # noqa: from local imports
|
||||
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"SHOW_TOOLBAR_CALLBACK": show_toolbar,
|
||||
}
|
312
app/urls.py
312
app/urls.py
|
@ -1,312 +0,0 @@
|
|||
"""app URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.urls import include, path
|
||||
from two_factor.urls import urlpatterns as tf_urls
|
||||
|
||||
from core.views import (
|
||||
ads,
|
||||
aggregators,
|
||||
banks,
|
||||
base,
|
||||
linkgroups,
|
||||
notifications,
|
||||
payouts,
|
||||
platforms,
|
||||
profit,
|
||||
wallets,
|
||||
)
|
||||
|
||||
# from core.views.stripe_callbacks import Callback
|
||||
|
||||
urlpatterns = [
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
path("", base.Home.as_view(), name="home"),
|
||||
path("sapp/", admin.site.urls),
|
||||
# 2FA login urls
|
||||
path("", include(tf_urls)),
|
||||
path("accounts/signup/", base.Signup.as_view(), name="signup"),
|
||||
path("accounts/logout/", LogoutView.as_view(), name="logout"),
|
||||
# Notifications
|
||||
path(
|
||||
"notifications/<str:type>/update/",
|
||||
notifications.NotificationsUpdate.as_view(),
|
||||
name="notifications_update",
|
||||
),
|
||||
# Aggregators
|
||||
path(
|
||||
"aggs/<str:type>/",
|
||||
aggregators.AggregatorList.as_view(),
|
||||
name="aggregators",
|
||||
),
|
||||
path(
|
||||
"aggs/<str:type>/create/",
|
||||
aggregators.AggregatorCreate.as_view(),
|
||||
name="aggregator_create",
|
||||
),
|
||||
path(
|
||||
"aggs/<str:type>/update/<str:pk>/",
|
||||
aggregators.AggregatorUpdate.as_view(),
|
||||
name="aggregator_update",
|
||||
),
|
||||
path(
|
||||
"aggs/<str:type>/delete/<str:pk>/",
|
||||
aggregators.AggregatorDelete.as_view(),
|
||||
name="aggregator_delete",
|
||||
),
|
||||
# Aggregator Requisitions
|
||||
path(
|
||||
"aggs/<str:type>/info/<str:pk>/",
|
||||
aggregators.ReqsList.as_view(),
|
||||
name="reqs",
|
||||
),
|
||||
# Aggregator Account link flow
|
||||
path(
|
||||
"aggs/<str:type>/countries/<str:pk>/",
|
||||
aggregators.AggregatorCountriesList.as_view(),
|
||||
name="aggregator_countries",
|
||||
),
|
||||
path(
|
||||
"aggs/<str:type>/countries/<str:pk>/<str:country>/banks/",
|
||||
aggregators.AggregatorCountryBanksList.as_view(),
|
||||
name="aggregator_country_banks",
|
||||
),
|
||||
path(
|
||||
"aggs/<str:type>/link/<str:pk>/<str:bank>/",
|
||||
aggregators.AggregatorLinkBank.as_view(),
|
||||
name="aggregator_link",
|
||||
),
|
||||
# Delete requisition
|
||||
path(
|
||||
"aggs/<str:type>/delete/<str:pk>/<str:req_id>/",
|
||||
aggregators.ReqDelete.as_view(),
|
||||
name="req_delete",
|
||||
),
|
||||
# Requisition info
|
||||
path(
|
||||
"aggs/<str:type>/info/<str:pk>/<str:req_id>/",
|
||||
aggregators.ReqInfo.as_view(),
|
||||
name="req_info",
|
||||
),
|
||||
# Request bank fetch
|
||||
path(
|
||||
"ops/bank_fetch/<str:pk>/",
|
||||
aggregators.RequestBankFetch.as_view(),
|
||||
name="bank_fetch",
|
||||
),
|
||||
path(
|
||||
"ops/bank_fetch/",
|
||||
aggregators.RequestBankFetch.as_view(),
|
||||
name="bank_fetch",
|
||||
),
|
||||
# Bank details by currency
|
||||
path(
|
||||
"banks/<str:type>/details/",
|
||||
banks.BanksCurrencies.as_view(),
|
||||
name="currencies",
|
||||
),
|
||||
path(
|
||||
"banks/<str:type>/req/<str:aggregator_id>/<str:req_id>/",
|
||||
banks.BanksRequisitionUpdate.as_view(),
|
||||
name="requisition_update",
|
||||
),
|
||||
path(
|
||||
"banks/<str:type>/req_delete/<str:pk>/",
|
||||
banks.BanksRequisitionDelete.as_view(),
|
||||
name="requisition_delete",
|
||||
),
|
||||
# Bank balances
|
||||
path(
|
||||
"banks/<str:type>/balances/",
|
||||
banks.BanksBalances.as_view(),
|
||||
name="balances",
|
||||
),
|
||||
# Transactions
|
||||
path(
|
||||
"banks/<str:type>/transactions/<str:aggregator_id>/<str:account_id>/",
|
||||
banks.BanksTransactions.as_view(),
|
||||
name="transactions",
|
||||
),
|
||||
# Platforms
|
||||
path(
|
||||
"platforms/<str:type>/",
|
||||
platforms.PlatformList.as_view(),
|
||||
name="platforms",
|
||||
),
|
||||
path(
|
||||
"platforms/<str:type>/create/",
|
||||
platforms.PlatformCreate.as_view(),
|
||||
name="platform_create",
|
||||
),
|
||||
path(
|
||||
"platforms/<str:type>/update/<str:pk>/",
|
||||
platforms.PlatformUpdate.as_view(),
|
||||
name="platform_update",
|
||||
),
|
||||
path(
|
||||
"platforms/<str:type>/delete/<str:pk>/",
|
||||
platforms.PlatformDelete.as_view(),
|
||||
name="platform_delete",
|
||||
),
|
||||
# Trades
|
||||
path(
|
||||
"trades/<str:type>/",
|
||||
platforms.PlatformTrades.as_view(),
|
||||
name="trades",
|
||||
),
|
||||
# Ads
|
||||
path(
|
||||
"ads/<str:type>/",
|
||||
ads.AdList.as_view(),
|
||||
name="ads",
|
||||
),
|
||||
path(
|
||||
"ads/<str:type>/create/",
|
||||
ads.AdCreate.as_view(),
|
||||
name="ad_create",
|
||||
),
|
||||
path(
|
||||
"ads/<str:type>/update/<str:pk>/",
|
||||
ads.AdUpdate.as_view(),
|
||||
name="ad_update",
|
||||
),
|
||||
path(
|
||||
"ads/<str:type>/delete/<str:pk>/",
|
||||
ads.AdDelete.as_view(),
|
||||
name="ad_delete",
|
||||
),
|
||||
path(
|
||||
"ops/ads/dist/",
|
||||
ads.AdDist.as_view(),
|
||||
name="ad_dist",
|
||||
),
|
||||
path(
|
||||
"ops/ads/nuke/",
|
||||
ads.AdNuke.as_view(),
|
||||
name="ad_nuke",
|
||||
),
|
||||
path(
|
||||
"ops/ads/redist/",
|
||||
ads.AdRedist.as_view(),
|
||||
name="ad_redist",
|
||||
),
|
||||
path(
|
||||
"ops/ads/dedup/",
|
||||
ads.AdDedup.as_view(),
|
||||
name="ad_dedup",
|
||||
),
|
||||
path(
|
||||
"ops/ads/cheat/",
|
||||
ads.Cheat.as_view(),
|
||||
name="cheat",
|
||||
),
|
||||
path(
|
||||
"profit/<str:type>/",
|
||||
profit.Profit.as_view(),
|
||||
name="profit",
|
||||
),
|
||||
# Wallets
|
||||
path(
|
||||
"wallets/<str:type>/",
|
||||
wallets.WalletList.as_view(),
|
||||
name="wallets",
|
||||
),
|
||||
path(
|
||||
"wallets/<str:type>/create/",
|
||||
wallets.WalletCreate.as_view(),
|
||||
name="wallet_create",
|
||||
),
|
||||
path(
|
||||
"wallets/<str:type>/update/<str:pk>/",
|
||||
wallets.WalletUpdate.as_view(),
|
||||
name="wallet_update",
|
||||
),
|
||||
path(
|
||||
"operator_wallets/<str:type>/update/",
|
||||
wallets.OperatorWalletsUpdate.as_view(),
|
||||
name="operator_wallets_update",
|
||||
),
|
||||
path(
|
||||
"wallets/<str:type>/delete/<str:pk>/",
|
||||
wallets.WalletDelete.as_view(),
|
||||
name="wallet_delete",
|
||||
),
|
||||
# Payouts
|
||||
path(
|
||||
"payouts/<str:type>/",
|
||||
payouts.PayoutList.as_view(),
|
||||
name="payouts",
|
||||
),
|
||||
path(
|
||||
"payouts/<str:type>/create/",
|
||||
payouts.PayoutCreate.as_view(),
|
||||
name="payout_create",
|
||||
),
|
||||
path(
|
||||
"payouts/<str:type>/update/<str:pk>/",
|
||||
payouts.PayoutUpdate.as_view(),
|
||||
name="payout_update",
|
||||
),
|
||||
path(
|
||||
"payouts/<str:type>/delete/<str:pk>/",
|
||||
payouts.PayoutDelete.as_view(),
|
||||
name="payout_delete",
|
||||
),
|
||||
path(
|
||||
"payouts/action/delete_all/",
|
||||
payouts.PayoutDeleteAll.as_view(),
|
||||
name="payout_delete_all",
|
||||
),
|
||||
# Link groups
|
||||
path(
|
||||
"links/<str:type>/",
|
||||
linkgroups.LinkGroupList.as_view(),
|
||||
name="linkgroups",
|
||||
),
|
||||
path(
|
||||
"links/<str:type>/create/",
|
||||
linkgroups.LinkGroupCreate.as_view(),
|
||||
name="linkgroup_create",
|
||||
),
|
||||
path(
|
||||
"links/<str:type>/update/<str:pk>/",
|
||||
linkgroups.LinkGroupUpdate.as_view(),
|
||||
name="linkgroup_update",
|
||||
),
|
||||
path(
|
||||
"links/<str:type>/delete/<str:pk>/",
|
||||
linkgroups.LinkGroupDelete.as_view(),
|
||||
name="linkgroup_delete",
|
||||
),
|
||||
path(
|
||||
"links/<str:type>/info/<str:pk>/",
|
||||
linkgroups.LinkGroupInfo.as_view(),
|
||||
name="linkgroup_info",
|
||||
),
|
||||
path(
|
||||
"links/<str:type>/simulate/<str:pk>/",
|
||||
linkgroups.LinkGroupSimulation.as_view(),
|
||||
name="linkgroup_simulate",
|
||||
),
|
||||
path(
|
||||
"links/<str:type>/withdraw/<str:pk>/",
|
||||
linkgroups.LinkGroupWithdraw.as_view(),
|
||||
name="linkgroup_withdraw",
|
||||
),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
16
app/wsgi.py
16
app/wsgi.py
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
WSGI config for app 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/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||
|
||||
application = get_wsgi_application()
|
|
@ -1,11 +0,0 @@
|
|||
import os
|
||||
|
||||
# import stripe
|
||||
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||
|
||||
# r = StrictRedis(unix_socket_path="/var/run/redis/redis.sock", db=0)
|
||||
|
||||
# if settings.STRIPE_TEST:
|
||||
# stripe.api_key = settings.STRIPE_API_KEY_TEST
|
||||
# else:
|
||||
# stripe.api_key = settings.STRIPE_API_KEY_PROD
|
|
@ -1,33 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .forms import CustomUserCreationForm
|
||||
from .models import NotificationSettings, User # AssetRestriction,; Plan,; Session,
|
||||
|
||||
# admin.site.__class__ = OTPAdminSite
|
||||
|
||||
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
|
||||
# for model_cls, model_admin in admin.site._registry.items():
|
||||
# otp_admin_site.register(model_cls, model_admin.__class__)
|
||||
|
||||
|
||||
# Register your models here.
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
# list_filter = ["plans"]
|
||||
model = User
|
||||
add_form = CustomUserCreationForm
|
||||
fieldsets = (
|
||||
*UserAdmin.fieldsets,
|
||||
(
|
||||
"Billing information",
|
||||
{"fields": ("billing_provider_id", "payment_provider_id")},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class NotificationSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "ntfy_topic", "ntfy_url")
|
||||
|
||||
|
||||
admin.site.register(User, CustomUserAdmin)
|
||||
admin.site.register(NotificationSettings, NotificationSettingsAdmin)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "core"
|
|
@ -1,423 +0,0 @@
|
|||
from abc import ABC
|
||||
|
||||
import orjson
|
||||
|
||||
from core.clients.platforms.agora import AgoraClient
|
||||
from core.lib import notify
|
||||
from core.lib.money import money
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("aggregator")
|
||||
|
||||
|
||||
class AggregatorClient(ABC):
|
||||
def store_account_info(self, account_infos):
|
||||
# account_infos = {
|
||||
# bank: accounts
|
||||
# for bank, accounts in account_info.items()
|
||||
# for account in accounts
|
||||
# #if account["account_id"] in self.banks
|
||||
# }
|
||||
# For each bank
|
||||
for bank, accounts in account_infos.items():
|
||||
# Iterate the accounts
|
||||
for index, account in enumerate(list(accounts)):
|
||||
if account["ownerName"] is None:
|
||||
requisition = self.instance.get_requisition(
|
||||
account["requisition_id"]
|
||||
)
|
||||
if requisition is not None:
|
||||
account["ownerName"] = requisition.owner_name
|
||||
if "account_number" not in account:
|
||||
account_infos[bank][index]["account_number"] = {}
|
||||
fields = ["sort_code", "number", "iban"]
|
||||
for field in fields:
|
||||
if field in account:
|
||||
account_infos[bank][index]["account_number"][
|
||||
field
|
||||
] = account[field]
|
||||
del account_infos[bank][index][field]
|
||||
# if len(account["account_number"]) == 1:
|
||||
# account_infos[bank].remove(account)
|
||||
currencies = [
|
||||
account["currency"]
|
||||
for bank, accounts in account_infos.items()
|
||||
for account in accounts
|
||||
]
|
||||
for bank, accounts in account_infos.items():
|
||||
if not self.instance.account_info:
|
||||
self.instance.account_info = {}
|
||||
self.instance.account_info[bank] = []
|
||||
for account in accounts:
|
||||
self.instance.account_info[bank].append(account)
|
||||
# self.account_info = account_infos
|
||||
self.currencies = currencies
|
||||
|
||||
self.instance.currencies = currencies
|
||||
self.instance.save()
|
||||
|
||||
async def process_transactions(self, account_id, transactions, req):
|
||||
if not transactions:
|
||||
return False
|
||||
|
||||
if not req:
|
||||
return False
|
||||
|
||||
platforms = self.instance.platforms
|
||||
for transaction in transactions:
|
||||
transaction_id = transaction["transaction_id"]
|
||||
tx_obj = self.instance.get_transaction(
|
||||
account_id,
|
||||
transaction_id,
|
||||
)
|
||||
if tx_obj is None:
|
||||
tx_cast = {
|
||||
"transaction_id": transaction_id,
|
||||
"recipient": transaction["creditorName"],
|
||||
"sender": transaction["debtorName"],
|
||||
"amount": transaction["amount"],
|
||||
"currency": transaction["currency"],
|
||||
"note": transaction["reference"],
|
||||
}
|
||||
tx_obj = self.instance.add_transaction(
|
||||
req,
|
||||
account_id,
|
||||
tx_cast,
|
||||
)
|
||||
# New transaction
|
||||
await notify.sendmsg(
|
||||
self.instance.user,
|
||||
f"New transaction: {orjson.dumps(tx_cast)}",
|
||||
title="New transaction",
|
||||
)
|
||||
await self.transaction(platforms, tx_obj)
|
||||
else:
|
||||
# Transaction exists
|
||||
continue
|
||||
# transaction_ids = [x["transaction_id"] for x in transactions]
|
||||
# new_key_name = f"new.transactions.{self.instance.id}.{self.name}.{account_id}"
|
||||
# old_key_name = f"transactions.{self.instance.id}.{self.name}.{account_id}"
|
||||
# # for transaction_id in transaction_ids:
|
||||
# if not transaction_ids:
|
||||
# return
|
||||
# await db.r.sadd(new_key_name, *transaction_ids)
|
||||
|
||||
# difference = list(await db.r.sdiff(new_key_name, old_key_name))
|
||||
|
||||
# difference = db.convert(difference)
|
||||
|
||||
# new_transactions = [
|
||||
# x for x in transactions if x["transaction_id"] in difference
|
||||
# ]
|
||||
|
||||
# # Rename the new key to the old key so we can run the diff again
|
||||
# await db.r.rename(new_key_name, old_key_name)
|
||||
# for transaction in new_transactions:
|
||||
# transaction["subclass"] = self.name
|
||||
# # self.tx.transaction(transaction)
|
||||
|
||||
def valid_transaction(self, tx_obj):
|
||||
"""
|
||||
Determine if a given transaction object is valid.
|
||||
:param data: a transaction cast
|
||||
:type data: dict
|
||||
:return: whether the transaction is valid
|
||||
:rtype: bool
|
||||
"""
|
||||
txid = tx_obj.transaction_id
|
||||
if tx_obj.amount is None:
|
||||
return False
|
||||
if tx_obj.currency is None:
|
||||
return False
|
||||
amount = tx_obj.amount
|
||||
if amount <= 0:
|
||||
log.info(f"Ignoring transaction with negative/zero amount: {txid}")
|
||||
return False
|
||||
return True
|
||||
|
||||
# def extract_reference(self, data):
|
||||
# """
|
||||
# Extract a reference from the transaction cast.
|
||||
# :param data: a transaction cast
|
||||
# :type data: dict
|
||||
# :return: the extracted reference or not_set
|
||||
# :rtype: str
|
||||
# """
|
||||
# if "reference" in data:
|
||||
# return data["reference"]
|
||||
# elif "meta" in data:
|
||||
# if "provider_reference" in data["meta"]:
|
||||
# return data["meta"]["provider_reference"]
|
||||
# return "not_set"
|
||||
|
||||
# def extract_sender(self, data):
|
||||
# """
|
||||
# Extract a sender name from the transaction cast.
|
||||
# :param data: a transaction cast
|
||||
# :type data: dict
|
||||
# :return: the sender name or not_set
|
||||
# :rtype: str
|
||||
# """
|
||||
# if "debtorName" in data:
|
||||
# return data["debtorName"]
|
||||
# elif "meta" in data:
|
||||
# if "debtor_account_name" in data["meta"]:
|
||||
# return data["meta"]["debtor_account_name"]
|
||||
# elif " " in data["reference"]:
|
||||
# refsplit = data["reference"].split(" ")
|
||||
# if not len(refsplit) == 2:
|
||||
# log.error(f"Sender cannot be extracted: {data}")
|
||||
# return "not_set"
|
||||
# realname, part2 = data["reference"].split(" ")
|
||||
# return realname
|
||||
|
||||
# return "not_set"
|
||||
|
||||
async def reference_partial_check(
|
||||
self, platform, reference, txid, currency, amount
|
||||
):
|
||||
"""
|
||||
Perform a partial check by intersecting all parts of the split of the
|
||||
reference against the existing references, and returning a set of the matches.
|
||||
:param reference: the reference to check
|
||||
:type reference: str
|
||||
:return: matching trade ID string
|
||||
:rtype: str
|
||||
"""
|
||||
# Partial reference implementation
|
||||
# Account for silly people not removing the default string
|
||||
# Split the reference into parts
|
||||
ref_split = reference.split(" ")
|
||||
# Get all existing references
|
||||
existing_refs = platform.references
|
||||
# Get all parts of the given reference split that match the existing references
|
||||
# stored_trade_reference = set(existing_refs).intersection(set(ref_split))
|
||||
stored_trade_reference = [x for x in existing_refs if x in ref_split]
|
||||
if len(stored_trade_reference) > 1:
|
||||
message = (
|
||||
f"Multiple references valid for TXID {txid}: {reference}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
title = "Error: multiple references valid"
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
if len(stored_trade_reference) == 0:
|
||||
return None
|
||||
return stored_trade_reference.pop()
|
||||
|
||||
# TODO: pass platform here
|
||||
async def can_alt_lookup(self, platform, amount, currency, reference):
|
||||
amount_usd = await money.to_usd(amount, currency)
|
||||
# Amount is reliable here as it is checked by find_trade,
|
||||
# so no need for stored_trade["amount"]
|
||||
if amount_usd > platform.no_reference_amount_check_max_usd:
|
||||
message = (
|
||||
f"Amount exceeds max for {reference}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
title = "Amount exceeds max for {reference}"
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
return True
|
||||
|
||||
def find_trade(self, platform, txid, currency, amount):
|
||||
"""
|
||||
Get a trade reference that matches the given currency and amount.
|
||||
Only works if there is one result.
|
||||
:param txid: Sink transaction ID
|
||||
:param currency: currency
|
||||
:param amount: amount
|
||||
:type txid: string
|
||||
:type currency: string
|
||||
:type amount: int
|
||||
:return: matching trade object or False
|
||||
:rtype: dict or bool
|
||||
"""
|
||||
refs = platform.references
|
||||
matching_refs = []
|
||||
# TODO: use get_ref_map in this function instead of calling get_ref multiple
|
||||
# times
|
||||
for ref in refs:
|
||||
stored_trade = platform.get_trade_by_reference(ref)
|
||||
if stored_trade.currency == currency and stored_trade.amount_fiat == amount:
|
||||
matching_refs.append(stored_trade)
|
||||
if len(matching_refs) != 1:
|
||||
log.error(
|
||||
f"Find trade returned multiple results for TXID {txid}: {matching_refs}"
|
||||
)
|
||||
return False
|
||||
return matching_refs[0]
|
||||
|
||||
async def amount_currency_lookup(self, platform, amount, currency, txid, ref):
|
||||
title = f"Checking against amount and currency for TXID {txid}"
|
||||
message = (
|
||||
f"Checking against amount and currency for TXID {txid}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
|
||||
if not await self.can_alt_lookup(platform, amount, currency, ref):
|
||||
return False
|
||||
stored_trade = self.find_trade(platform, txid, currency, amount)
|
||||
if not stored_trade:
|
||||
title = f"Failed to get reference by amount and currency: {txid}"
|
||||
message = (
|
||||
f"Failed to get reference by amount and currency: {txid}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return None
|
||||
return stored_trade
|
||||
|
||||
async def normal_lookup(
|
||||
self, platform, stored_trade_reference, reference, currency, amount
|
||||
):
|
||||
stored_trade = platform.get_trade_by_reference(stored_trade_reference)
|
||||
if not stored_trade:
|
||||
title = f"No reference in DB for {reference}"
|
||||
message = (
|
||||
f"No reference in DB for {reference}"
|
||||
f"Currency: {currency} | Amount: {amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
# stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
||||
return stored_trade
|
||||
|
||||
async def currency_check(self, currency, stored_trade):
|
||||
if not stored_trade.currency == currency:
|
||||
title = "Currency mismatch"
|
||||
message = (
|
||||
f"Currency mismatch, Agora: {stored_trade.currency} "
|
||||
f"/ Sink: {currency}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def alt_amount_check(self, platform, amount, currency, stored_trade):
|
||||
# If the amount does not match exactly, get the min and max values for our
|
||||
# given acceptable margins for trades
|
||||
min_amount, max_amount = await money.get_acceptable_margins(
|
||||
platform, currency, stored_trade.amount_fiat
|
||||
)
|
||||
log.info(
|
||||
(
|
||||
f"Amount does not match exactly, trying with margins: min: {min_amount}"
|
||||
f" / max: {max_amount}"
|
||||
)
|
||||
)
|
||||
title = "Amount does not match exactly"
|
||||
message = (
|
||||
f"Amount does not match exactly, trying with margins: min: "
|
||||
f"{min_amount} / max: {max_amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
if not min_amount < amount < max_amount:
|
||||
title = "Amount mismatch - not in margins"
|
||||
message = (
|
||||
f"Amount mismatch - not in margins: {stored_trade.amount_fiat} "
|
||||
f"(min: {min_amount} / max: {max_amount}"
|
||||
)
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def transaction(self, platforms, tx_obj):
|
||||
"""
|
||||
Store details of transaction and post notifications to IRC.
|
||||
Matches it up with data stored in Redis to attempt to reconcile with an Agora
|
||||
trade.
|
||||
:param data: details of transaction
|
||||
:type data: dict
|
||||
"""
|
||||
valid = self.valid_transaction(tx_obj)
|
||||
if not valid:
|
||||
return False
|
||||
txid = tx_obj.transaction_id
|
||||
amount = tx_obj.amount
|
||||
currency = tx_obj.currency
|
||||
|
||||
reference = tx_obj.note
|
||||
|
||||
# reference = self.extract_reference(data)
|
||||
# sender = tx_obj.sender
|
||||
|
||||
log.info(f"Transaction processed: {tx_obj}")
|
||||
await notify.sendmsg(
|
||||
self.instance.user,
|
||||
(f"Transaction: {txid} {amount}{currency}: {reference}"),
|
||||
title="Incoming transaction",
|
||||
)
|
||||
for platform in platforms:
|
||||
stored_trade_reference = await self.reference_partial_check(
|
||||
platform, reference, txid, currency, amount
|
||||
)
|
||||
if stored_trade_reference is False: # can be None though
|
||||
continue
|
||||
|
||||
stored_trade = False
|
||||
looked_up_without_reference = False
|
||||
|
||||
# Normal implementation for when we have a reference
|
||||
if stored_trade_reference:
|
||||
stored_trade = await self.normal_lookup(
|
||||
platform, stored_trade_reference, reference, currency, amount
|
||||
)
|
||||
# if not stored_trade:
|
||||
# return
|
||||
|
||||
# Amount/currency lookup implementation for when we have no reference
|
||||
else:
|
||||
if not stored_trade: # check we don't overwrite the lookup above
|
||||
stored_trade = await self.amount_currency_lookup(
|
||||
platform, amount, currency, txid, reference
|
||||
)
|
||||
if stored_trade is False:
|
||||
continue
|
||||
if stored_trade:
|
||||
# Note that we have looked it up without reference so we don't
|
||||
# use +- below
|
||||
# This might be redundant given the checks in find_trade,
|
||||
# but better safe than sorry!
|
||||
looked_up_without_reference = True
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
# Stored trade reference is none, the checks below will do nothing
|
||||
continue
|
||||
|
||||
# Make sure it was sent in the expected currency
|
||||
if not await self.currency_check(currency, stored_trade):
|
||||
continue
|
||||
|
||||
# Make sure the expected amount was sent
|
||||
if not stored_trade.amount_fiat == amount:
|
||||
if looked_up_without_reference:
|
||||
continue
|
||||
if not await self.alt_amount_check(
|
||||
platform, amount, currency, stored_trade
|
||||
):
|
||||
continue
|
||||
# platform_buyer = stored_trade["buyer"]
|
||||
|
||||
# Check sender - we don't do anything with this yet
|
||||
# sender_valid = antifraud.check_valid_sender(
|
||||
# reference, platform, sender, platform_buyer
|
||||
# )
|
||||
# log.info(f"Trade {reference} buyer {platform_buyer}
|
||||
# valid: {sender_valid}")
|
||||
instance = await AgoraClient(platform)
|
||||
rtrn = await instance.release_map_trade(stored_trade, tx_obj)
|
||||
# if trade_released:
|
||||
# self.ux.notify.notify_complete_trade(amount, currency)
|
||||
# else:
|
||||
# log.error(f"Cannot release trade {reference}.")
|
||||
# return
|
||||
|
||||
# rtrn = await platform.release_funds(stored_trade["id"],
|
||||
# stored_trade["reference"])
|
||||
if rtrn:
|
||||
title = "Trade complete"
|
||||
message = f"Trade complete: {amount}{currency}"
|
||||
await notify.sendmsg(self.instance.user, message, title=title)
|
|
@ -1,336 +0,0 @@
|
|||
from datetime import timedelta
|
||||
from hashlib import sha256
|
||||
|
||||
import orjson
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from core.clients.aggregator import AggregatorClient
|
||||
from core.clients.base import BaseClient
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("nordigen")
|
||||
|
||||
|
||||
class NordigenClient(BaseClient, AggregatorClient):
|
||||
url = "https://ob.nordigen.com/api/v2"
|
||||
|
||||
async def connect(self):
|
||||
now = timezone.now()
|
||||
# Check if access token expires later than now
|
||||
if self.instance.access_token_expires is not None:
|
||||
if self.instance.access_token_expires > now:
|
||||
self.token = self.instance.access_token
|
||||
return
|
||||
await self.get_access_token()
|
||||
|
||||
def method_filter(self, method):
|
||||
new_method = method.replace("/", "_")
|
||||
return new_method
|
||||
|
||||
async def get_access_token(self):
|
||||
"""
|
||||
Get the access token for the Nordigen API.
|
||||
"""
|
||||
log.debug(f"Getting new access token for {self.instance}")
|
||||
data = {
|
||||
"secret_id": self.instance.secret_id,
|
||||
"secret_key": self.instance.secret_key,
|
||||
}
|
||||
|
||||
response = await self.call("token/new", http_method="post", data=data)
|
||||
access = response["access"]
|
||||
access_expires = response["access_expires"]
|
||||
now = timezone.now()
|
||||
# Offset now by access_expires seconds
|
||||
access_expires = now + timedelta(seconds=access_expires)
|
||||
self.instance.access_token = access
|
||||
self.instance.access_token_expires = access_expires
|
||||
self.instance.save()
|
||||
|
||||
self.token = access
|
||||
|
||||
async def get_requisitions(self):
|
||||
"""
|
||||
Get a list of active accounts.
|
||||
"""
|
||||
response = await self.call("requisitions")
|
||||
return response["results"]
|
||||
|
||||
async def get_countries(self):
|
||||
"""
|
||||
Get a list of countries.
|
||||
"""
|
||||
# This function is a stub.
|
||||
|
||||
return ["GB", "SE", "BG"]
|
||||
|
||||
async def get_banks(self, country):
|
||||
"""
|
||||
Get a list of supported banks for a country.
|
||||
:param country: country to query
|
||||
:return: list of institutions
|
||||
:rtype: list
|
||||
"""
|
||||
if not len(country) == 2:
|
||||
return False
|
||||
path = f"institutions/?country={country}"
|
||||
response = await self.call(path, schema="Institutions", append_slash=False)
|
||||
|
||||
return response
|
||||
|
||||
async def build_link(self, institution_id, redirect=None):
|
||||
"""Create a link to access an institution.
|
||||
:param institution_id: ID of the institution
|
||||
"""
|
||||
|
||||
data = {
|
||||
"institution_id": institution_id,
|
||||
"redirect": settings.URL,
|
||||
}
|
||||
if redirect:
|
||||
data["redirect"] = redirect
|
||||
response = await self.call(
|
||||
"requisitions", schema="RequisitionsPost", http_method="post", data=data
|
||||
)
|
||||
if "link" in response:
|
||||
return response["link"]
|
||||
return False
|
||||
|
||||
async def delete_requisition(self, requisition_id):
|
||||
"""
|
||||
Delete a requisision ID.
|
||||
"""
|
||||
path = f"requisitions/{requisition_id}"
|
||||
|
||||
response = await self.call(
|
||||
path, schema="RequisitionDelete", http_method="delete"
|
||||
)
|
||||
return response
|
||||
|
||||
async def get_requisition(self, requisition):
|
||||
"""
|
||||
Get a list of accounts for a requisition.
|
||||
:param requisition: requisition ID"""
|
||||
path = f"requisitions/{requisition}"
|
||||
response = await self.call(path, schema="Requisition")
|
||||
|
||||
return response
|
||||
|
||||
# def get_ownernames(self):
|
||||
# """
|
||||
# Get list of supplementary owner names.
|
||||
# """
|
||||
# ownernames = loads(settings.Nordigen.OwnerNames)
|
||||
# return ownernames
|
||||
|
||||
async def get_account(self, req_id, account_id):
|
||||
"""
|
||||
Get details of an account.
|
||||
:param account_id: account ID"""
|
||||
|
||||
path = f"accounts/{account_id}/details"
|
||||
response = await self.call(path, schema="AccountDetails")
|
||||
if "account" not in response:
|
||||
return False
|
||||
parsed = response["account"]
|
||||
if "iban" in parsed and parsed["currency"] == "GBP":
|
||||
if parsed["iban"]:
|
||||
sort_code = parsed["iban"][-14:-8]
|
||||
account_number = parsed["iban"][-8:]
|
||||
del parsed["iban"]
|
||||
# if "iban" in parsed:
|
||||
# del parsed["iban"]
|
||||
sort_code = "-".join(list(map("".join, zip(*[iter(sort_code)] * 2))))
|
||||
parsed["sort_code"] = sort_code
|
||||
parsed["number"] = account_number
|
||||
# Let's add the account ID so we can reference it later
|
||||
parsed["account_id"] = account_id
|
||||
parsed["aggregator_id"] = str(self.instance.id)
|
||||
parsed["requisition_id"] = str(req_id)
|
||||
return parsed
|
||||
|
||||
async def get_all_account_info(self, requisition=None, store=False):
|
||||
to_return = {}
|
||||
if not requisition:
|
||||
requisitions = await self.get_requisitions()
|
||||
else:
|
||||
requisitions = [await self.get_requisition(requisition)]
|
||||
|
||||
for req in requisitions:
|
||||
accounts = req["accounts"]
|
||||
for account_id in accounts:
|
||||
account_info = await self.get_account(req["id"], account_id)
|
||||
if not account_info:
|
||||
continue
|
||||
if req["institution_id"] in to_return:
|
||||
to_return[req["institution_id"]].append(account_info)
|
||||
else:
|
||||
to_return[req["institution_id"]] = [account_info]
|
||||
|
||||
if store:
|
||||
if requisition is not None:
|
||||
raise Exception("Cannot store partial data")
|
||||
self.store_account_info(to_return)
|
||||
return to_return
|
||||
|
||||
async def get_balance(self, account_id):
|
||||
"""
|
||||
Get the balance and currency of an account.
|
||||
:param account_id: the account ID
|
||||
:return: tuple of (currency, amount)
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
path = f"accounts/{account_id}/balances"
|
||||
response = await self.call(path, schema="AccountBalances")
|
||||
|
||||
total = 0
|
||||
currency = None
|
||||
if "balances" not in response:
|
||||
return (False, False)
|
||||
for entry in response["balances"]:
|
||||
if currency:
|
||||
if not currency == entry["balanceAmount"]["currency"]:
|
||||
return (False, False)
|
||||
if not entry["balanceType"] == "interimAvailable":
|
||||
continue
|
||||
total += float(entry["balanceAmount"]["amount"])
|
||||
currency = entry["balanceAmount"]["currency"]
|
||||
return (currency, total)
|
||||
|
||||
async def get_all_balances(self):
|
||||
"""
|
||||
Get all balances.
|
||||
Keyed by bank.
|
||||
"""
|
||||
if self.instance.account_info is None:
|
||||
await self.get_all_account_info(store=True)
|
||||
|
||||
account_balances = {}
|
||||
for bank, accounts in self.instance.account_info.items():
|
||||
if bank not in account_balances:
|
||||
account_balances[bank] = []
|
||||
for account in accounts:
|
||||
account_id = account["account_id"]
|
||||
currency, amount = await self.get_balance(account_id)
|
||||
account_balances[bank].append(
|
||||
{
|
||||
"currency": currency,
|
||||
"balance": amount,
|
||||
"account_id": account_id,
|
||||
}
|
||||
)
|
||||
return account_balances
|
||||
|
||||
async def get_total_map(self):
|
||||
"""
|
||||
Return a dictionary keyed by currencies with the amounts as values.
|
||||
:return: dict keyed by currency, values are amounts
|
||||
:rtype: dict
|
||||
"""
|
||||
if self.instance.account_info is None:
|
||||
await self.get_all_account_info(store=True)
|
||||
|
||||
totals = {}
|
||||
for bank, accounts in self.instance.account_info.items():
|
||||
for account in accounts:
|
||||
account_id = account["account_id"]
|
||||
currency, amount = await self.get_balance(account_id)
|
||||
if not amount:
|
||||
continue
|
||||
if not currency:
|
||||
continue
|
||||
if currency in totals:
|
||||
totals[currency] += amount
|
||||
else:
|
||||
totals[currency] = amount
|
||||
return totals
|
||||
|
||||
def normalise_transactions(self, transactions, state=None):
|
||||
for transaction in transactions:
|
||||
# Rename ID
|
||||
if transaction["transactionId"]:
|
||||
transaction["transaction_id"] = transaction["transactionId"]
|
||||
del transaction["transactionId"]
|
||||
elif transaction["internalTransactionId"]:
|
||||
transaction["transaction_id"] = transaction["internalTransactionId"]
|
||||
del transaction["internalTransactionId"]
|
||||
else:
|
||||
# No transaction ID. This is a problem for our implementation
|
||||
tx_hash = sha256(
|
||||
orjson.dumps(transaction, option=orjson.OPT_SORT_KEYS)
|
||||
).hexdigest()
|
||||
transaction["transaction_id"] = tx_hash
|
||||
|
||||
# Rename timestamp
|
||||
|
||||
if transaction["bookingDateTime"]:
|
||||
transaction["ts"] = transaction["bookingDateTime"]
|
||||
del transaction["bookingDateTime"]
|
||||
elif transaction["bookingDate"]:
|
||||
transaction["ts"] = transaction["bookingDate"]
|
||||
del transaction["bookingDate"]
|
||||
elif transaction["valueDate"]:
|
||||
transaction["ts"] = transaction["valueDate"]
|
||||
del transaction["valueDate"]
|
||||
|
||||
transaction["amount"] = float(transaction["transactionAmount"]["amount"])
|
||||
transaction["currency"] = transaction["transactionAmount"]["currency"]
|
||||
if state:
|
||||
transaction["state"] = state
|
||||
del transaction["transactionAmount"]
|
||||
|
||||
if transaction["remittanceInformationUnstructuredArray"]:
|
||||
ref_list = transaction["remittanceInformationUnstructuredArray"]
|
||||
reference = "|".join(ref_list)
|
||||
transaction["reference"] = reference
|
||||
del transaction["remittanceInformationUnstructuredArray"]
|
||||
elif transaction["remittanceInformationUnstructured"]:
|
||||
reference = transaction["remittanceInformationUnstructured"]
|
||||
transaction["reference"] = reference
|
||||
del transaction["remittanceInformationUnstructured"]
|
||||
else:
|
||||
raise Exception(f"No way to get reference: {transaction}")
|
||||
|
||||
async def get_transactions(
|
||||
self, account_id, req=None, process=False, pending=False
|
||||
):
|
||||
"""
|
||||
Get all transactions for an account.
|
||||
:param account_id: account to fetch transactions for
|
||||
:return: list of transactions
|
||||
:rtype: dict
|
||||
"""
|
||||
path = f"accounts/{account_id}/transactions"
|
||||
response = await self.call(path, schema="Transactions")
|
||||
|
||||
if response["status_code"] == 401:
|
||||
log.error(
|
||||
f"Error getting transactions for {account_id}: {response['summary']}"
|
||||
)
|
||||
return []
|
||||
|
||||
source = "booked"
|
||||
|
||||
# If requisition is specified, try to get the object
|
||||
# If present, take the transaction source from there,
|
||||
# pending or booked.
|
||||
if req:
|
||||
requisition = self.instance.get_requisition(req)
|
||||
if requisition:
|
||||
source = requisition.transaction_source
|
||||
|
||||
parsed = response["transactions"][source]
|
||||
self.normalise_transactions(parsed, state=source)
|
||||
|
||||
if process:
|
||||
await self.process_transactions(account_id, parsed, req=req)
|
||||
if pending:
|
||||
if process:
|
||||
raise Exception("Cannot process and get pending")
|
||||
parsed_pending = response["transactions"]["pending"]
|
||||
self.normalise_transactions(parsed_pending, state="pending")
|
||||
parsed_pending.extend(parsed)
|
||||
parsed = parsed_pending
|
||||
return parsed
|
|
@ -1,205 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
import aiohttp
|
||||
import orjson
|
||||
from glom import glom
|
||||
from pydantic.error_wrappers import ValidationError
|
||||
|
||||
from core.lib import schemas
|
||||
from core.util import logs
|
||||
|
||||
# Return error if the schema for the message type is not found
|
||||
STRICT_VALIDATION = False
|
||||
|
||||
# Raise exception if the conversion schema is not found
|
||||
STRICT_CONVERSION = False
|
||||
|
||||
# TODO: Set them to True when all message types are implemented
|
||||
|
||||
log = logs.get_logger("clients")
|
||||
|
||||
|
||||
class NoSchema(Exception):
|
||||
"""
|
||||
Raised when:
|
||||
- The schema for the message type is not found
|
||||
- The conversion schema is not found
|
||||
- There is no schema library for the client
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchMethod(Exception):
|
||||
"""
|
||||
Client library has no such method.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GenericAPIError(Exception):
|
||||
"""
|
||||
Generic API error.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def is_camel_case(s):
|
||||
return s != s.lower() and s != s.upper() and "_" not in s
|
||||
|
||||
|
||||
def snake_to_camel(word):
|
||||
if is_camel_case(word):
|
||||
return word
|
||||
return "".join(x.capitalize() or "_" for x in word.split("_"))
|
||||
|
||||
|
||||
DEFAULT_HEADERS = {
|
||||
"accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
class BaseClient(ABC):
|
||||
token = None
|
||||
|
||||
async def __new__(cls, *a, **kw):
|
||||
instance = super().__new__(cls)
|
||||
await instance.__init__(*a, **kw)
|
||||
return instance
|
||||
|
||||
async def __init__(self, instance):
|
||||
"""
|
||||
Initialise the client.
|
||||
:param instance: the database object, e.g. Aggregator
|
||||
"""
|
||||
name = self.__class__.__name__
|
||||
self.name = name.replace("Client", "").lower()
|
||||
self.instance = instance
|
||||
self.client = None
|
||||
|
||||
await self.connect()
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
"""
|
||||
Get the schema library for the client.
|
||||
"""
|
||||
# Does the schemas library have a library for this client name?
|
||||
if hasattr(schemas, f"{self.name}_s"):
|
||||
schema_instance = getattr(schemas, f"{self.name}_s")
|
||||
else:
|
||||
log.error(f"No schema library for {self.name}")
|
||||
raise Exception(f"No schema library for client {self.name}")
|
||||
|
||||
return schema_instance
|
||||
|
||||
def get_schema(self, method, convert=False):
|
||||
if isinstance(method, str):
|
||||
to_camel = snake_to_camel(method)
|
||||
else:
|
||||
to_camel = snake_to_camel(method.__class__.__name__)
|
||||
if convert:
|
||||
to_camel = f"{to_camel}Schema"
|
||||
|
||||
# if hasattr(self.schema, method):
|
||||
# schema = getattr(self.schema, method)
|
||||
if hasattr(self.schema, to_camel):
|
||||
schema = getattr(self.schema, to_camel)
|
||||
else:
|
||||
raise NoSchema(f"Could not get schema: {to_camel}")
|
||||
return schema
|
||||
|
||||
async def call_method(self, method, *args, **kwargs):
|
||||
"""
|
||||
Call a method with aiohttp.
|
||||
"""
|
||||
if kwargs.get("append_slash", True):
|
||||
path = f"{self.url}/{method}/"
|
||||
else:
|
||||
path = f"{self.url}/{method}"
|
||||
|
||||
http_method = kwargs.get("http_method", "get")
|
||||
|
||||
cast = {
|
||||
"headers": DEFAULT_HEADERS,
|
||||
}
|
||||
|
||||
# Use the token if it's set
|
||||
if self.token is not None:
|
||||
cast["headers"]["Authorization"] = f"Bearer {self.token}"
|
||||
|
||||
if "data" in kwargs:
|
||||
cast["data"] = orjson.dumps(kwargs["data"])
|
||||
|
||||
# Use the method to send a HTTP request
|
||||
async with aiohttp.ClientSession() as session:
|
||||
session_method = getattr(session, http_method)
|
||||
async with session_method(path, **cast) as response:
|
||||
response_json = await response.json()
|
||||
return response_json
|
||||
|
||||
def convert_spec(self, response, method):
|
||||
"""
|
||||
Convert an API response to the requested spec.
|
||||
:raises NoSchema: If the conversion schema is not found
|
||||
"""
|
||||
schema = self.get_schema(method, convert=True)
|
||||
|
||||
# Use glom to convert the response to the schema
|
||||
converted = glom(response, schema)
|
||||
return converted
|
||||
|
||||
def validate_response(self, response, method):
|
||||
schema = self.get_schema(method)
|
||||
# Return a dict of the validated response
|
||||
try:
|
||||
response_valid = schema(**response).dict()
|
||||
except ValidationError as e:
|
||||
log.error(f"Error validating {method} response: {response}")
|
||||
log.error(f"Errors: {e}")
|
||||
raise GenericAPIError("Error validating response")
|
||||
return response_valid
|
||||
|
||||
def method_filter(self, method):
|
||||
"""
|
||||
Return a new method.
|
||||
"""
|
||||
return method
|
||||
|
||||
async def call(self, method, *args, **kwargs):
|
||||
"""
|
||||
Call the exchange API and validate the response
|
||||
:raises NoSchema: If the method is not in the schema mapping
|
||||
:raises ValidationError: If the response cannot be validated
|
||||
"""
|
||||
# try:
|
||||
response = await self.call_method(method, *args, **kwargs)
|
||||
# except (APIError, V20Error) as e:
|
||||
# log.error(f"Error calling method {method}: {e}")
|
||||
# raise GenericAPIError(e)
|
||||
|
||||
if "schema" in kwargs:
|
||||
method = kwargs["schema"]
|
||||
else:
|
||||
method = self.method_filter(method)
|
||||
try:
|
||||
response_valid = self.validate_response(response, method)
|
||||
except NoSchema as e:
|
||||
log.error(f"{e} - {response}")
|
||||
response_valid = response
|
||||
# Convert the response to a format that we can use
|
||||
try:
|
||||
response_converted = self.convert_spec(response_valid, method)
|
||||
except NoSchema as e:
|
||||
log.error(f"{e} - {response}")
|
||||
response_converted = response_valid
|
||||
|
||||
# return (True, response_converted)
|
||||
return response_converted
|
File diff suppressed because it is too large
Load Diff
350
core/forms.py
350
core/forms.py
|
@ -1,350 +0,0 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.forms import ModelForm
|
||||
from mixins.restrictions import RestrictedFormMixin
|
||||
|
||||
from .models import (
|
||||
Ad,
|
||||
Aggregator,
|
||||
Asset,
|
||||
LinkGroup,
|
||||
NotificationSettings,
|
||||
OperatorWallets,
|
||||
Payout,
|
||||
Platform,
|
||||
Provider,
|
||||
Requisition,
|
||||
User,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
# flake8: noqa: E501
|
||||
|
||||
|
||||
class NewUserForm(UserCreationForm):
|
||||
email = forms.EmailField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
"username",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"password1",
|
||||
"password2",
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super(NewUserForm, self).save(commit=False)
|
||||
user.email = self.cleaned_data["email"]
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class CustomUserCreationForm(UserCreationForm):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class NotificationSettingsForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = NotificationSettings
|
||||
fields = (
|
||||
"ntfy_topic",
|
||||
"ntfy_url",
|
||||
)
|
||||
help_texts = {
|
||||
"ntfy_topic": "The topic to send notifications to.",
|
||||
"ntfy_url": "Custom NTFY server. Leave blank to use the default server.",
|
||||
}
|
||||
|
||||
|
||||
class AggregatorForm(RestrictedFormMixin, ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AggregatorForm, self).__init__(*args, **kwargs)
|
||||
self.fields["secret_id"].label = "Secret ID"
|
||||
|
||||
class Meta:
|
||||
model = Aggregator
|
||||
fields = (
|
||||
"name",
|
||||
"service",
|
||||
"secret_id",
|
||||
"secret_key",
|
||||
"poll_interval",
|
||||
"link_group",
|
||||
"enabled",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "The name of the aggregator connection.",
|
||||
"service": "The aggregator service to use.",
|
||||
"secret_id": "The secret ID for the aggregator service.",
|
||||
"secret_key": "The secret key for the aggregator service.",
|
||||
"poll_interval": "The interval in seconds to poll the aggregator service.",
|
||||
"link_group": "The link group to use for this aggregator connection.",
|
||||
"enabled": "Whether or not the aggregator connection is enabled.",
|
||||
}
|
||||
|
||||
|
||||
class PlatformForm(RestrictedFormMixin, ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PlatformForm, self).__init__(*args, **kwargs)
|
||||
upper = ["usd", "otp"]
|
||||
for field in self.fields:
|
||||
for up in upper:
|
||||
if self.fields[field].label:
|
||||
if up in self.fields[field].label:
|
||||
self.fields[field].label = self.fields[field].label.replace(
|
||||
up, up.upper()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = (
|
||||
"name",
|
||||
"service",
|
||||
"token",
|
||||
"password",
|
||||
"otp_token",
|
||||
"username",
|
||||
"send",
|
||||
"cheat",
|
||||
"dummy",
|
||||
"cheat_interval_seconds",
|
||||
"margin",
|
||||
"max_margin",
|
||||
"min_margin",
|
||||
"min_trade_size_usd",
|
||||
"max_trade_size_usd",
|
||||
"accept_within_usd",
|
||||
"no_reference_amount_check_max_usd",
|
||||
"base_usd",
|
||||
"withdrawal_trigger",
|
||||
"payees",
|
||||
"link_group",
|
||||
"enabled",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "The name of the platform connection.",
|
||||
"service": "The platform service to use.",
|
||||
"token": "The JWT auth token.",
|
||||
"password": "Account password",
|
||||
"otp_token": "The OTP secret key.",
|
||||
"username": "Account username",
|
||||
"send": "Whether or not to send messages on new trades.",
|
||||
"cheat": "Whether or not to run the Autoprice cheat.",
|
||||
"dummy": "When enabled, the trade escrow feature will be disabled.",
|
||||
"cheat_interval_seconds": "The interval in seconds to run the Autoprice cheat.",
|
||||
"margin": "The current margin. Only valid for initial ads post. Autoprice will override this.",
|
||||
"max_margin": "The maximum margin to use.",
|
||||
"min_margin": "The minimum margin to use.",
|
||||
"min_trade_size_usd": "The minimum trade size in USD.",
|
||||
"max_trade_size_usd": "The maximum trade size in USD.",
|
||||
"accept_within_usd": "When a trade is wrong by less than this amount, it will be accepted.",
|
||||
"no_reference_amount_check_max_usd": "When ticked, when no reference was found and a trade is higher than this amount, we will not accept payment even if it is the only one with this amount.",
|
||||
"base_usd": "The amount in USD to keep in the platform.",
|
||||
"withdrawal_trigger": "The amount above the base USD to trigger a withdrawal.",
|
||||
"payees": "The wallet addresses to send profit concerning this platform to.",
|
||||
"link_group": "The link group to use for this platform.",
|
||||
"enabled": "Whether or not the platform connection is enabled.",
|
||||
}
|
||||
|
||||
payees = forms.ModelMultipleChoiceField(
|
||||
queryset=Wallet.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["payees"],
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class AdForm(RestrictedFormMixin, ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdForm, self).__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = Ad
|
||||
fields = (
|
||||
"name",
|
||||
"text",
|
||||
"payment_details",
|
||||
"payment_details_real",
|
||||
"payment_method_details",
|
||||
"dist_list",
|
||||
"asset_list",
|
||||
"provider_list",
|
||||
# "platforms",
|
||||
# "aggregators",
|
||||
"require_feedback_score",
|
||||
"account_whitelist",
|
||||
"send_reference",
|
||||
"visible",
|
||||
"link_group",
|
||||
"enabled",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "The name of the ad.",
|
||||
"text": "The content of the ad.",
|
||||
"payment_details": "Shown before a user opens a trade.",
|
||||
"payment_details_real": "Shown after a user opens a trade.",
|
||||
"payment_method_details": "Shown in the list",
|
||||
"dist_list": "Currency and country, space separated, one pair per line.",
|
||||
"asset_list": "List of assets to distribute ads for.",
|
||||
"provider_list": "List of providers to distribute ads for.",
|
||||
# "platforms": "Enabled platforms for this ad",
|
||||
# "aggregators": "Enabled aggregators for this ad",
|
||||
"require_feedback_score": "Mminimum feedback score for users. Set to 0 to disable.",
|
||||
"account_whitelist": "List of account IDs to use, one per line.",
|
||||
"send_reference": "Whether or not to send the reference on new trades.",
|
||||
"visible": "Whether or not this ad is visible.",
|
||||
"link_group": "The link group to use for this ad.",
|
||||
"enabled": "Whether or not this ad is enabled.",
|
||||
}
|
||||
|
||||
asset_list = forms.ModelMultipleChoiceField(
|
||||
queryset=Asset.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["asset_list"],
|
||||
required=True,
|
||||
)
|
||||
provider_list = forms.ModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["provider_list"],
|
||||
required=True,
|
||||
)
|
||||
# platforms = forms.ModelMultipleChoiceField(
|
||||
# queryset=Platform.objects.all(),
|
||||
# widget=forms.CheckboxSelectMultiple,
|
||||
# help_text=Meta.help_texts["platforms"],
|
||||
# required=True,
|
||||
# )
|
||||
# aggregators = forms.ModelMultipleChoiceField(
|
||||
# queryset=Aggregator.objects.all(),
|
||||
# widget=forms.CheckboxSelectMultiple,
|
||||
# help_text=Meta.help_texts["aggregators"],
|
||||
# required=True,
|
||||
# )
|
||||
|
||||
|
||||
class RequisitionForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Requisition
|
||||
fields = (
|
||||
"payment_details",
|
||||
"owner_name",
|
||||
"transaction_source",
|
||||
"payees",
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
"payment_details": "Shown once a user opens a trade.",
|
||||
"owner_name": "Owner name to send with payment details if not provided by aggregator.",
|
||||
"transaction_source": "Whether to check pending or booked transactions.",
|
||||
"payees": "The wallet addresses to send profit concerning this requisition to.",
|
||||
}
|
||||
|
||||
payees = forms.ModelMultipleChoiceField(
|
||||
queryset=Wallet.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["payees"],
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class WalletForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Wallet
|
||||
fields = (
|
||||
"name",
|
||||
"address",
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
"name": "The name of the wallet.",
|
||||
"address": "The XMR address to send funds to.",
|
||||
}
|
||||
|
||||
|
||||
class LinkGroupForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = LinkGroup
|
||||
fields = (
|
||||
"name",
|
||||
"platform_owner_cut_percentage",
|
||||
"requisition_owner_cut_percentage",
|
||||
"operator_cut_percentage",
|
||||
"enabled",
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
"name": "The name of the link group.",
|
||||
"platform_owner_cut_percentage": "The percentage of the total profit of this group to give to the platform owners.",
|
||||
"requisition_owner_cut_percentage": "The percentage of the total profit of this group to give to the requisition owners.",
|
||||
"operator_cut_percentage": "The percentage of the total profit of this group to give to the operator.",
|
||||
"enabled": "Whether or not this link group is enabled.",
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(LinkGroupForm, self).clean()
|
||||
platform_owner_cut_percentage = cleaned_data.get(
|
||||
"platform_owner_cut_percentage"
|
||||
)
|
||||
requisition_owner_cut_percentage = cleaned_data.get(
|
||||
"requisition_owner_cut_percentage"
|
||||
)
|
||||
operator_cut_percentage = cleaned_data.get("operator_cut_percentage")
|
||||
total_sum = (
|
||||
platform_owner_cut_percentage
|
||||
+ requisition_owner_cut_percentage
|
||||
+ operator_cut_percentage
|
||||
)
|
||||
if total_sum != 100:
|
||||
self.add_error(
|
||||
"platform_owner_cut_percentage",
|
||||
f"The sum of the percentages must be 100, not {total_sum}.",
|
||||
)
|
||||
self.add_error(
|
||||
"requisition_owner_cut_percentage",
|
||||
f"The sum of the percentages must be 100, not {total_sum}.",
|
||||
)
|
||||
self.add_error(
|
||||
"operator_cut_percentage",
|
||||
f"The sum of the percentages must be 100, not {total_sum}.",
|
||||
)
|
||||
return
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class OperatorWalletsForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = OperatorWallets
|
||||
fields = ("payees",)
|
||||
help_texts = {
|
||||
"payees": "Wallets to designate as payees for this operator.",
|
||||
}
|
||||
|
||||
payees = forms.ModelMultipleChoiceField(
|
||||
queryset=Wallet.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["payees"],
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class PayoutForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Payout
|
||||
fields = (
|
||||
"wallet",
|
||||
"amount",
|
||||
"description",
|
||||
)
|
||||
help_texts = {
|
||||
"wallet": "The wallet the payment was sent to.",
|
||||
"amount": "The amount of the payment.",
|
||||
"description": "The description of the payment.",
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
# Project imports
|
||||
# from core.lib import db # , notify
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("antifraud")
|
||||
|
||||
|
||||
class AntiFraud(object):
|
||||
async def add_bank_sender(self, platform_buyer, bank_sender):
|
||||
"""
|
||||
Add the bank senders into Redis.
|
||||
:param platform: name of the platform - freeform
|
||||
:param platform_buyer: the username of the buyer on the platform
|
||||
:param bank_sender: the sender name from the bank
|
||||
"""
|
||||
# key = f"namemap.{platform}.{platform_buyer}"
|
||||
# await db.r.sadd(key, bank_sender)
|
||||
# TODO
|
||||
|
||||
async def get_previous_senders(self, platform, platform_buyer):
|
||||
"""
|
||||
Get all the previous bank sender names for the given buyer on the platform.
|
||||
:param platform: name of the platform - freeform
|
||||
:param platform_buyer: the username of the buyer on the platform
|
||||
:return: set of previous buyers
|
||||
:rtype: set
|
||||
"""
|
||||
# key = f"namemap.{platform}.{platform_buyer}"
|
||||
# senders = await db.r.smembers(key)
|
||||
# if not senders:
|
||||
# return None
|
||||
# senders = db.convert(senders)
|
||||
# return senders
|
||||
# TODO
|
||||
|
||||
async def check_valid_sender(
|
||||
self, reference, platform, bank_sender, platform_buyer
|
||||
):
|
||||
"""
|
||||
Check that either:
|
||||
* The platform buyer has never had a recognised transaction before
|
||||
* The bank sender name matches a previous transaction from the platform buyer
|
||||
:param reference: the trade reference
|
||||
:param platform: name of the platform - freeform
|
||||
:param bank_sender: the sender of the bank transaction
|
||||
:param platform_buyer: the username of the buyer on the platform
|
||||
:return: whether the sender is valid
|
||||
:rtype: bool
|
||||
"""
|
||||
# senders = await self.get_previous_senders(platform, platform_buyer)
|
||||
# if senders is None: # no senders yet, assume it's valid
|
||||
# return True
|
||||
# if platform_buyer in senders:
|
||||
# return True
|
||||
# self.ux.notify.notify_sender_name_mismatch(
|
||||
# reference, platform_buyer, bank_sender
|
||||
# )
|
||||
# # title = "Sender name mismatch"
|
||||
# # message = (
|
||||
# # f"Sender name mismatch for {reference}:\n"
|
||||
# # f"Platform buyer: {platform_buyer}"
|
||||
# # f"Bank sender: {bank_sender}"
|
||||
# # )
|
||||
# # await notify.sendmsg(self.instance.) # TODO
|
||||
# return False
|
||||
# TODO
|
||||
|
||||
async def check_tx_sender(self, tx, reference):
|
||||
"""
|
||||
Check whether the sender of a given transaction is authorised based on the
|
||||
previous transactions of the username that originated the trade reference.
|
||||
:param tx: the transaction ID
|
||||
:param reference: the trade reference
|
||||
"""
|
||||
# stored_trade = await db.get_ref(reference)
|
||||
# if not stored_trade:
|
||||
# return None
|
||||
# stored_tx = await db.get_tx(tx)
|
||||
# if not stored_tx:
|
||||
# return None
|
||||
# bank_sender = stored_tx["sender"]
|
||||
# platform_buyer = stored_trade["buyer"]
|
||||
# platform = stored_trade["subclass"]
|
||||
# is_allowed = await self.check_valid_sender(
|
||||
# reference, platform, bank_sender, platform_buyer
|
||||
# )
|
||||
# if is_allowed is True:
|
||||
# return True
|
||||
# return False
|
||||
# TODO
|
||||
|
||||
# def user_verification_successful(self, uid):
|
||||
# """
|
||||
# A user has successfully completed verification.
|
||||
# """
|
||||
# self.log.info(f"User has completed verification: {uid}")
|
||||
# trade_list = self.markets.find_trades_by_uid(uid)
|
||||
# for platform, trade_id, reference, currency in trade_list:
|
||||
# self.markets.send_bank_details(platform, currency, trade_id)
|
||||
# self.markets.send_reference(platform, trade_id, reference)
|
||||
|
||||
# def send_verification_url(self, platform, uid, trade_id):
|
||||
# send_setting, post_message = self.markets.get_send_settings(platform)
|
||||
# if send_setting == "1":
|
||||
# auth_url = self.ux.verify.create_applicant_and_get_link(uid)
|
||||
# if platform == "lbtc":
|
||||
# auth_url = auth_url.replace("https://", "") # hack
|
||||
# post_message(
|
||||
# trade_id,
|
||||
# f"Hi! To continue the trade, please complete the verification form:
|
||||
# {auth_url}",
|
||||
# )
|
||||
|
||||
|
||||
antifraud = AntiFraud()
|
|
@ -1,32 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
client = None
|
||||
|
||||
|
||||
def initialise_elasticsearch():
|
||||
"""
|
||||
Initialise the Elasticsearch client.
|
||||
"""
|
||||
auth = (settings.ELASTICSEARCH_USERNAME, settings.ELASTICSEARCH_PASSWORD)
|
||||
client = Elasticsearch(
|
||||
settings.ELASTICSEARCH_HOST, http_auth=auth, verify_certs=False
|
||||
)
|
||||
return client
|
||||
|
||||
|
||||
def store_msg(index, msg):
|
||||
global client
|
||||
if not client:
|
||||
client = initialise_elasticsearch()
|
||||
if "ts" not in msg:
|
||||
msg["ts"] = datetime.utcnow().isoformat()
|
||||
result = client.index(index=index, body=msg)
|
||||
if not result["result"] == "created":
|
||||
log.error(f"Indexing of '{msg}' failed: {result}")
|
|
@ -1,631 +0,0 @@
|
|||
# Twisted imports
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import urllib3
|
||||
from aiocoingecko import AsyncCoinGeckoAPISession
|
||||
from django.conf import settings
|
||||
from elasticsearch import AsyncElasticsearch
|
||||
from forex_python.converter import CurrencyRates
|
||||
|
||||
# Other library imports
|
||||
from core.models import Aggregator, OperatorWallets, Platform
|
||||
from core.util import logs
|
||||
|
||||
# TODO: secure ES traffic properly
|
||||
urllib3.disable_warnings()
|
||||
|
||||
tracer = logging.getLogger("opensearch")
|
||||
tracer.setLevel(logging.CRITICAL)
|
||||
tracer = logging.getLogger("elastic_transport.transport")
|
||||
tracer.setLevel(logging.CRITICAL)
|
||||
|
||||
log = logs.get_logger("money")
|
||||
|
||||
|
||||
class Money(object):
|
||||
"""
|
||||
Generic class for handling money-related matters that aren't Revolut or Agora.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialise the Money object.
|
||||
Set the logger.
|
||||
Initialise the CoinGecko API.
|
||||
"""
|
||||
self.cr = CurrencyRates()
|
||||
self.cg = AsyncCoinGeckoAPISession()
|
||||
auth = (settings.ELASTICSEARCH_USERNAME, settings.ELASTICSEARCH_PASSWORD)
|
||||
client = AsyncElasticsearch(
|
||||
settings.ELASTICSEARCH_HOST, http_auth=auth, verify_certs=False
|
||||
)
|
||||
self.es = client
|
||||
|
||||
async def check_all(self, user=None, link_group=None, nordigen=None, agora=None):
|
||||
"""
|
||||
Run all the balance checks that output into ES in another thread.
|
||||
"""
|
||||
if not all([nordigen, agora]):
|
||||
raise Exception
|
||||
if not any([user, link_group]):
|
||||
raise Exception
|
||||
|
||||
# I hate circular dependencies
|
||||
self.nordigen = nordigen
|
||||
self.agora = agora
|
||||
|
||||
cast = {}
|
||||
if user is not None:
|
||||
cast["user"] = user
|
||||
if link_group is not None:
|
||||
cast["link_group"] = link_group
|
||||
|
||||
aggregators = Aggregator.objects.filter(enabled=True, **cast)
|
||||
platforms = Platform.objects.filter(enabled=True, **cast)
|
||||
|
||||
total = await self.get_total(aggregators, platforms, trades=True)
|
||||
|
||||
return total
|
||||
|
||||
# def setup_loops(self):
|
||||
# """
|
||||
# Set up the LoopingCalls to get the balance so we have data in ES.
|
||||
# """
|
||||
# if settings.ES.Enabled == "1" or settings.Logstash.Enabled == "1":
|
||||
# self.lc_es_checks = LoopingCall(self.run_checks_in_thread)
|
||||
# delay = int(settings.ES.RefreshSec)
|
||||
# self.lc_es_checks.start(delay)
|
||||
# if settings.ES.Enabled == "1":
|
||||
# self.agora.es = self.es
|
||||
# self.lbtc.es = self.es
|
||||
|
||||
async def write_to_es(self, msgtype, cast):
|
||||
cast["type"] = "money"
|
||||
cast["ts"] = str(datetime.now().isoformat())
|
||||
cast["xtype"] = msgtype
|
||||
# cast["user_id"] = self.instance.user.id
|
||||
# cast["platform_id"] = self.instance.id
|
||||
try:
|
||||
await self.es.index(index=settings.ELASTICSEARCH_INDEX, body=cast)
|
||||
except RuntimeError:
|
||||
log.warning("Could not write to ES")
|
||||
|
||||
async def lookup_rates(self, platform, ads, rates=None):
|
||||
"""
|
||||
Lookup the rates for a list of public ads.
|
||||
"""
|
||||
if not rates:
|
||||
rates = await self.cg.get_price(
|
||||
ids=["monero", "bitcoin"],
|
||||
vs_currencies=self.markets.get_all_currencies(platform),
|
||||
)
|
||||
# Set the price based on the asset
|
||||
for ad in ads:
|
||||
if ad[4] == "XMR":
|
||||
coin = "monero"
|
||||
elif ad[4] == "BTC":
|
||||
coin = "bitcoin" # No s here
|
||||
currency = ad[5]
|
||||
base_currency_price = rates[coin][currency.lower()]
|
||||
price = float(ad[2])
|
||||
rate = round(price / base_currency_price, 2)
|
||||
ad.append(rate)
|
||||
# TODO: sort?
|
||||
return sorted(ads, key=lambda x: x[2])
|
||||
|
||||
async def get_rates_all(self):
|
||||
"""
|
||||
Get all rates that pair with USD.
|
||||
:return: dictionary of USD/XXX rates
|
||||
:rtype: dict
|
||||
"""
|
||||
rates = self.cr.get_rates("USD")
|
||||
return rates
|
||||
|
||||
# TODO: pass platform
|
||||
async def get_acceptable_margins(self, platform, currency, amount):
|
||||
"""
|
||||
Get the minimum and maximum amounts we would accept a trade for.
|
||||
:param currency: currency code
|
||||
:param amount: amount
|
||||
:return: (min, max)
|
||||
:rtype: tuple
|
||||
"""
|
||||
rates = await self.get_rates_all()
|
||||
if currency == "USD":
|
||||
min_amount = amount - platform.accept_within_usd
|
||||
max_amount = amount + platform.accept_within_usd
|
||||
return (min_amount, max_amount)
|
||||
amount_usd = amount / rates[currency]
|
||||
min_usd = amount_usd - platform.accept_within_usd
|
||||
max_usd = amount_usd + platform.accept_within_usd
|
||||
min_local = min_usd * rates[currency]
|
||||
max_local = max_usd * rates[currency]
|
||||
return (min_local, max_local)
|
||||
|
||||
async def get_minmax(self, min_usd, max_usd, asset, currency):
|
||||
rates = await self.get_rates_all()
|
||||
if currency not in rates and not currency == "USD":
|
||||
log.error(f"Can't create ad without rates: {currency}")
|
||||
return (None, None)
|
||||
if currency == "USD":
|
||||
min_amount = min_usd
|
||||
max_amount = max_usd
|
||||
else:
|
||||
min_amount = rates[currency] * min_usd
|
||||
max_amount = rates[currency] * max_usd
|
||||
|
||||
return (min_amount, max_amount)
|
||||
|
||||
async def to_usd(self, amount, currency):
|
||||
if currency == "USD":
|
||||
return float(amount)
|
||||
else:
|
||||
rates = await self.get_rates_all()
|
||||
return float(amount) / rates[currency]
|
||||
|
||||
async def multiple_to_usd(self, currency_map, rates=None):
|
||||
"""
|
||||
Convert multiple curencies to USD while saving API calls.
|
||||
"""
|
||||
if not rates:
|
||||
rates = await self.get_rates_all()
|
||||
cumul = 0
|
||||
for currency, amount in currency_map.items():
|
||||
if currency == "USD":
|
||||
cumul += float(amount)
|
||||
else:
|
||||
cumul += float(amount) / rates[currency]
|
||||
return cumul
|
||||
|
||||
async def get_total_usd(self):
|
||||
"""
|
||||
Get total USD in all our accounts, bank and trading.
|
||||
:return: value in USD
|
||||
:rtype float:
|
||||
"""
|
||||
total_sinks_usd = await self.sinks.get_total_usd()
|
||||
agora_wallet_xmr = await self.agora.api.wallet_balance_xmr()
|
||||
agora_wallet_btc = await self.agora.api.wallet_balance()
|
||||
# lbtc_wallet_btc = await self.lbtc.api.wallet_balance()
|
||||
if not agora_wallet_xmr["success"]:
|
||||
return False
|
||||
if not agora_wallet_btc["success"]:
|
||||
return False
|
||||
# if not lbtc_wallet_btc["success"]:
|
||||
# return False
|
||||
if not agora_wallet_xmr["response"]:
|
||||
return False
|
||||
if not agora_wallet_btc["response"]:
|
||||
return False
|
||||
# if not lbtc_wallet_btc["response"]:
|
||||
# return False
|
||||
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
|
||||
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
|
||||
# total_btc_lbtc = lbtc_wallet_btc["response"]["data"]["total"]["balance"]
|
||||
# Get the XMR -> USD exchange rate
|
||||
xmr_usd = await self.cg.get_price(ids="monero", vs_currencies=["USD"])
|
||||
|
||||
# Get the BTC -> USD exchange rate
|
||||
btc_usd = await self.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
|
||||
|
||||
# Convert the Agora BTC total to USD
|
||||
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
|
||||
|
||||
# Convert the LBTC BTC total to USD
|
||||
# total_usd_lbtc_btc = float(total_btc_lbtc) * btc_usd["bitcoin"]["usd"]
|
||||
|
||||
# Convert the Agora XMR total to USD
|
||||
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
|
||||
|
||||
# Add it all up
|
||||
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
|
||||
# total_usd_lbtc = total_usd_lbtc_btc
|
||||
total_usd = total_usd_agora + total_sinks_usd
|
||||
# total_usd_lbtc +
|
||||
cast_es = {
|
||||
"price_usd": total_usd,
|
||||
"total_usd_agora_xmr": total_usd_agora_xmr,
|
||||
"total_usd_agora_btc": total_usd_agora_btc,
|
||||
# "total_usd_lbtc_btc": total_usd_lbtc_btc,
|
||||
"total_xmr_agora": total_xmr_agora,
|
||||
"total_btc_agora": total_btc_agora,
|
||||
# "total_btc_lbtc": total_btc_lbtc,
|
||||
"xmr_usd": xmr_usd["monero"]["usd"],
|
||||
"btc_usd": btc_usd["bitcoin"]["usd"],
|
||||
"total_sinks_usd": total_sinks_usd,
|
||||
"total_usd_agora": total_usd_agora,
|
||||
}
|
||||
await self.write_to_es("get_total_usd", cast_es)
|
||||
return total_usd
|
||||
|
||||
async def gather_total_map(self, aggregators, rates):
|
||||
"""
|
||||
Gather the total USD of specified aggregators.
|
||||
"""
|
||||
total_run_tasks = [self.nordigen(x) for x in aggregators]
|
||||
total_run = await asyncio.gather(*total_run_tasks)
|
||||
|
||||
total_map_tasks = [x.get_total_map() for x in total_run]
|
||||
total_map = await asyncio.gather(*total_map_tasks)
|
||||
|
||||
to_usd_tasks = [self.multiple_to_usd(x, rates=rates) for x in total_map]
|
||||
total = await asyncio.gather(*to_usd_tasks)
|
||||
|
||||
total = sum(total)
|
||||
|
||||
return total
|
||||
|
||||
async def gather_wallet_balance_xmr(self, platforms):
|
||||
"""
|
||||
Gather the total XMR of the specified platforms.
|
||||
"""
|
||||
run_tasks = [self.agora(platform) for platform in platforms]
|
||||
run = await asyncio.gather(*run_tasks)
|
||||
|
||||
xmr_tasks = [x.api.wallet_balance_xmr() for x in run]
|
||||
xmr_pre = await asyncio.gather(*xmr_tasks)
|
||||
|
||||
xmr = [float(x["response"]["data"]["total"]["balance"]) for x in xmr_pre]
|
||||
xmr = sum(xmr)
|
||||
return xmr
|
||||
|
||||
async def gather_wallet_balance(self, platforms):
|
||||
"""
|
||||
Gather the total BTC of the specified platforms.
|
||||
"""
|
||||
run_tasks = [self.agora(platform) for platform in platforms]
|
||||
run = await asyncio.gather(*run_tasks)
|
||||
|
||||
btc_tasks = [x.api.wallet_balance() for x in run]
|
||||
btc_pre = await asyncio.gather(*btc_tasks)
|
||||
|
||||
btc = [float(x["response"]["data"]["total"]["balance"]) for x in btc_pre]
|
||||
btc = sum(btc)
|
||||
return btc
|
||||
|
||||
def gather_base_usd(self, platforms):
|
||||
total = 0
|
||||
for platform in platforms:
|
||||
total += platform.base_usd
|
||||
return total
|
||||
|
||||
def gather_withdrawal_limit(self, platforms):
|
||||
total = 0
|
||||
for platform in platforms:
|
||||
total += platform.withdrawal_trigger
|
||||
return total
|
||||
|
||||
# TODO: possibly refactor this into smaller functions which don't return as much
|
||||
# check if this is all really needed in the corresponding withdraw function
|
||||
async def get_total(self, aggregators, platforms, trades=False):
|
||||
"""
|
||||
Get all the values corresponding to the amount of money we hold.
|
||||
:return: ((total SEK, total USD, total GBP),
|
||||
(total XMR USD, total BTC USD),
|
||||
(total XMR, total BTC))
|
||||
:rtype: tuple(tuple(float, float, float),
|
||||
tuple(float, float),
|
||||
tuple(float, float))
|
||||
"""
|
||||
rates = await self.get_rates_all()
|
||||
total_sinks_usd = await self.gather_total_map(aggregators, rates=rates)
|
||||
agora_wallet_xmr = await self.gather_wallet_balance_xmr(platforms)
|
||||
agora_wallet_btc = await self.gather_wallet_balance(platforms)
|
||||
|
||||
total_xmr_agora = agora_wallet_xmr
|
||||
total_btc_agora = agora_wallet_btc
|
||||
# Get the XMR -> USD exchange rate
|
||||
async with AsyncCoinGeckoAPISession() as cg:
|
||||
xmr_usd = await cg.get_price(ids="monero", vs_currencies="USD")
|
||||
|
||||
# Get the BTC -> USD exchange rate
|
||||
btc_usd = await cg.get_price(ids="bitcoin", vs_currencies="USD")
|
||||
|
||||
# Convert the Agora XMR total to USD
|
||||
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
|
||||
|
||||
# Convert the Agora BTC total to USD
|
||||
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
|
||||
|
||||
# Add it all up
|
||||
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
|
||||
total_usd = total_usd_agora + total_sinks_usd
|
||||
|
||||
# Get aggregate totals and withdrawal limits
|
||||
total_base_usd = self.gather_base_usd(platforms)
|
||||
total_withdrawal_limit = self.gather_withdrawal_limit(platforms)
|
||||
|
||||
# Use those to calculate amount remaining
|
||||
withdraw_threshold = total_base_usd + total_withdrawal_limit
|
||||
remaining = withdraw_threshold - total_usd
|
||||
|
||||
profit = total_usd - total_base_usd
|
||||
profit_in_xmr = profit / xmr_usd["monero"]["usd"]
|
||||
|
||||
# Convert the total USD price to GBP and SEK
|
||||
price_sek = rates["SEK"] * total_usd
|
||||
price_usd = total_usd
|
||||
price_gbp = rates["GBP"] * total_usd
|
||||
|
||||
# Get open trades value
|
||||
if trades:
|
||||
dashboards = await self.gather_dashboards(platforms)
|
||||
cumul_trades = 0
|
||||
for dash in dashboards:
|
||||
cumul_add = await self.open_trades_usd_parse_dash(dash, rates)
|
||||
cumul_trades += cumul_add
|
||||
|
||||
total_with_trades = total_usd + cumul_trades
|
||||
total_remaining = withdraw_threshold - total_with_trades
|
||||
|
||||
total_profit = total_with_trades - total_base_usd
|
||||
total_profit_in_xmr = total_profit / xmr_usd["monero"]["usd"]
|
||||
|
||||
# cast = (
|
||||
# (
|
||||
# price_sek,
|
||||
# price_usd,
|
||||
# price_gbp,
|
||||
# ), # Total prices in our 3 favourite currencies
|
||||
# (
|
||||
# total_xmr_usd,
|
||||
# total_btc_usd,
|
||||
# ), # Total USD balance in only Agora
|
||||
# (total_xmr, total_btc),
|
||||
# ) # Total XMR and BTC balance in Agora
|
||||
|
||||
# TODO
|
||||
cast_es = {
|
||||
"price_sek": price_sek,
|
||||
"price_usd": price_usd,
|
||||
"price_gbp": price_gbp,
|
||||
"total_usd_agora_xmr": total_usd_agora_xmr,
|
||||
"total_usd_agora_btc": total_usd_agora_btc,
|
||||
# "total_usd_lbtc_btc": total_usd_lbtc_btc,
|
||||
"total_xmr_agora": total_xmr_agora,
|
||||
"total_btc_agora": total_btc_agora,
|
||||
# "total_btc_lbtc": total_btc_lbtc,
|
||||
"xmr_usd": xmr_usd["monero"]["usd"],
|
||||
"btc_usd": btc_usd["bitcoin"]["usd"],
|
||||
"total_sinks_usd": total_sinks_usd,
|
||||
"total_usd_agora": total_usd_agora,
|
||||
"total_usd": total_usd,
|
||||
"total_base_usd": total_base_usd,
|
||||
"total_withdrawal_limit": total_withdrawal_limit,
|
||||
"remaining": remaining,
|
||||
"profit": profit,
|
||||
"profit_in_xmr": profit_in_xmr,
|
||||
"withdraw_threshold": withdraw_threshold,
|
||||
}
|
||||
if trades:
|
||||
cast_es["open_trade_value"] = cumul_trades
|
||||
cast_es["total_with_trades"] = total_with_trades
|
||||
cast_es["total_remaining"] = total_remaining
|
||||
cast_es["total_profit"] = total_profit
|
||||
cast_es["total_profit_in_xmr"] = total_profit_in_xmr
|
||||
await self.write_to_es("get_total", cast_es)
|
||||
return cast_es
|
||||
|
||||
async def open_trades_usd_parse_dash(self, dash, rates):
|
||||
cumul_usd = 0
|
||||
cache = {}
|
||||
async with AsyncCoinGeckoAPISession() as cg:
|
||||
for _, contact in dash.items():
|
||||
# We need created at in order to look up the historical prices
|
||||
created_at = contact["data"]["created_at"]
|
||||
|
||||
# Reformat the date how CoinGecko likes
|
||||
# 2022-05-02T11:17:14+00:00
|
||||
if "+" in created_at:
|
||||
date_split = created_at.split("+")
|
||||
date_split[1].replace(".", "")
|
||||
date_split[1].replace(":", "")
|
||||
created_at = "+".join(date_split)
|
||||
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S%z")
|
||||
else:
|
||||
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
|
||||
date_formatted = date_parsed.strftime("%d-%m-%Y")
|
||||
|
||||
# Get the historical rates for the right asset, extract the price
|
||||
asset = contact["data"]["advertisement"]["asset"]
|
||||
|
||||
if asset == "XMR":
|
||||
amount_crypto = contact["data"]["amount_xmr"]
|
||||
if (asset, date_formatted) in cache:
|
||||
history = cache[(asset, date_formatted)]
|
||||
else:
|
||||
history = await cg.get_coin_history_by_id(
|
||||
coin_id="monero", date=date_formatted
|
||||
)
|
||||
if "market_data" not in history:
|
||||
return False
|
||||
crypto_usd = float(history["market_data"]["current_price"]["usd"])
|
||||
elif asset == "BTC":
|
||||
amount_crypto = contact["data"]["amount_btc"]
|
||||
if (asset, date_formatted) in cache:
|
||||
history = cache[(asset, date_formatted)]
|
||||
else:
|
||||
history = await cg.get_coin_history_by_id(
|
||||
coin_id="bitcoin", date=date_formatted
|
||||
)
|
||||
if "market_data" not in history:
|
||||
return False
|
||||
crypto_usd = float(history["market_data"]["current_price"]["usd"])
|
||||
if (asset, date_formatted) not in cache:
|
||||
cache[(asset, date_formatted)] = history
|
||||
# Convert crypto to USD
|
||||
amount = float(amount_crypto) * crypto_usd
|
||||
# currency = contact["data"]["currency"]
|
||||
if not contact["data"]["is_selling"]:
|
||||
continue
|
||||
cumul_usd += float(amount)
|
||||
# else:
|
||||
# rate = rates[currency]
|
||||
# print("RATE", rate)
|
||||
# print("AMOUNT", amount)
|
||||
# amount_usd = float(amount) / rate
|
||||
# print("AMOUJT USD", amount_usd)
|
||||
# cumul_usd += amount_usd
|
||||
return cumul_usd
|
||||
|
||||
async def gather_dashboards(self, platforms):
|
||||
dashboards = []
|
||||
for platform in platforms:
|
||||
run = await self.agora(platform)
|
||||
dash = await run.wrap_dashboard()
|
||||
dashboards.append(dash)
|
||||
return dashboards
|
||||
|
||||
# async def get_open_trades_usd(self, rates):
|
||||
# """
|
||||
# Get total value of open trades in USD.
|
||||
# :return: total trade value
|
||||
# :rtype: float
|
||||
# """
|
||||
# dash_agora = await self.agora.wrap_dashboard()
|
||||
# # dash_lbtc = self.lbtc.wrap_dashboard()
|
||||
# # dash_lbtc = yield dash_lbtc
|
||||
# if dash_agora is False:
|
||||
# return False
|
||||
# # if dash_lbtc is False:
|
||||
# # return False
|
||||
|
||||
# # rates = await self.get_rates_all()
|
||||
# cumul_usd_agora = await self.open_trades_usd_parse_dash(
|
||||
# "agora", dash_agora, rates
|
||||
# )
|
||||
# # cumul_usd_lbtc = await self.open_trades_usd_parse_dash("lbtc", dash_lbtc,
|
||||
# # rates)
|
||||
# cumul_usd = cumul_usd_agora # + cumul_usd_lbtc
|
||||
|
||||
# cast_es = {
|
||||
# "trades_usd": cumul_usd,
|
||||
# }
|
||||
# await self.write_to_es("get_open_trades_usd", cast_es)
|
||||
# return cumul_usd
|
||||
|
||||
async def get_total_remaining(self):
|
||||
"""
|
||||
Check how much profit we need to make in order to withdraw, taking into account
|
||||
open trade value.
|
||||
:return: profit remaining in USD
|
||||
:rtype: float
|
||||
"""
|
||||
total_usd = await self.get_total_usd()
|
||||
total_trades_usd = await self.get_open_trades_usd()
|
||||
if not total_usd:
|
||||
return False
|
||||
total_usd += total_trades_usd
|
||||
withdraw_threshold = float(settings.Money.BaseUSD) + float(
|
||||
settings.Money.WithdrawLimit
|
||||
)
|
||||
remaining = withdraw_threshold - total_usd
|
||||
|
||||
cast_es = {
|
||||
"total_remaining_usd": remaining,
|
||||
}
|
||||
await self.write_to_es("get_total_remaining", cast_es)
|
||||
return remaining
|
||||
|
||||
async def get_total_with_trades(self):
|
||||
total_usd = await self.get_total_usd()
|
||||
if not total_usd:
|
||||
return False
|
||||
total_trades_usd = await self.get_open_trades_usd()
|
||||
total_with_trades = total_usd + total_trades_usd
|
||||
cast_es = {
|
||||
"total_with_trades": total_with_trades,
|
||||
}
|
||||
await self.write_to_es("get_total_with_trades", cast_es)
|
||||
return total_with_trades
|
||||
|
||||
def get_pay_list(self, linkgroup, requisitions, platforms, user, profit):
|
||||
pay_list = {} # Wallet: [(amount, reason), (amount, reason), ...]
|
||||
|
||||
# Get the total amount of money we have
|
||||
total_throughput_platform = 0
|
||||
total_throughput_requisition = 0
|
||||
for requisition in requisitions:
|
||||
total_throughput_requisition += requisition.throughput
|
||||
for platform in platforms:
|
||||
total_throughput_platform += platform.throughput
|
||||
|
||||
cut_platform = profit * (linkgroup.platform_owner_cut_percentage / 100)
|
||||
cut_req = profit * (linkgroup.requisition_owner_cut_percentage / 100)
|
||||
cut_operator = profit * (linkgroup.operator_cut_percentage / 100)
|
||||
|
||||
# Add the operator payment
|
||||
operator_wallets = OperatorWallets.objects.filter(user=user).first()
|
||||
operator_length = len(operator_wallets.payees.all())
|
||||
payment_per_operator = cut_operator / operator_length
|
||||
for wallet in operator_wallets.payees.all():
|
||||
if wallet not in pay_list:
|
||||
pay_list[wallet] = []
|
||||
detail = (
|
||||
f"Operator cut for 1 of {operator_length} operators, total "
|
||||
f"{cut_operator}"
|
||||
)
|
||||
pay_list[wallet].append((payment_per_operator, detail))
|
||||
|
||||
# Add the platform payment
|
||||
for platform in platforms:
|
||||
# Get ratio of platform.throughput to the total platform throughput
|
||||
if total_throughput_platform == 0:
|
||||
ratio = 0
|
||||
else:
|
||||
ratio = platform.throughput / total_throughput_platform
|
||||
platform_payment = cut_platform * ratio
|
||||
payees_length = len(platform.payees.all())
|
||||
if payees_length == 0:
|
||||
payment_per_payee = 0
|
||||
else:
|
||||
payment_per_payee = platform_payment / payees_length
|
||||
for wallet in platform.payees.all():
|
||||
if wallet not in pay_list:
|
||||
pay_list[wallet] = []
|
||||
detail = (
|
||||
f"Platform {platform} cut for 1 of {payees_length} payees, "
|
||||
f"total {cut_platform}"
|
||||
)
|
||||
pay_list[wallet].append((payment_per_payee, detail))
|
||||
|
||||
# Add the requisition payment
|
||||
for requisition in requisitions:
|
||||
# Get ratio of requisition.throughput to the requisition cut
|
||||
if total_throughput_requisition == 0:
|
||||
ratio = 0
|
||||
else:
|
||||
ratio = requisition.throughput / total_throughput_requisition
|
||||
req_payment = cut_req * ratio
|
||||
payees_length = len(requisition.payees.all())
|
||||
if payees_length == 0:
|
||||
payment_per_payee = 0
|
||||
else:
|
||||
payment_per_payee = req_payment / payees_length
|
||||
for wallet in requisition.payees.all():
|
||||
if wallet not in pay_list:
|
||||
pay_list[wallet] = []
|
||||
detail = (
|
||||
f"Requisition {requisition} cut for 1 of {payees_length} payees, "
|
||||
f"total {cut_req}"
|
||||
)
|
||||
pay_list[wallet].append((payment_per_payee, detail))
|
||||
|
||||
return pay_list
|
||||
|
||||
def collapse_pay_list(self, pay_list):
|
||||
"""
|
||||
Collapse the pay list into a single dict of wallet: amount.
|
||||
"""
|
||||
collapsed = {}
|
||||
for wallet, payments in pay_list.items():
|
||||
collapsed[wallet] = sum([x[0] for x in payments])
|
||||
|
||||
return collapsed
|
||||
|
||||
|
||||
money = Money()
|
|
@ -1,39 +0,0 @@
|
|||
import aiohttp
|
||||
|
||||
from core.util import logs
|
||||
|
||||
NTFY_URL = "https://ntfy.sh"
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
# Actual function to send a message to a topic
|
||||
async def raw_sendmsg(msg, title=None, priority=None, tags=None, url=None, topic=None):
|
||||
if url is None:
|
||||
url = NTFY_URL
|
||||
headers = {"Title": "Pluto"}
|
||||
if title:
|
||||
headers["Title"] = title
|
||||
if priority:
|
||||
headers["Priority"] = priority
|
||||
if tags:
|
||||
headers["Tags"] = tags
|
||||
cast = {
|
||||
"headers": headers,
|
||||
"data": msg,
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await session.post(f"{url}/{topic}", **cast)
|
||||
|
||||
|
||||
# Sendmsg helper to send a message to a user's notification settings
|
||||
async def sendmsg(user, *args, **kwargs):
|
||||
notification_settings = user.get_notification_settings()
|
||||
|
||||
if notification_settings.ntfy_topic is None:
|
||||
# No topic set, so don't send
|
||||
return
|
||||
else:
|
||||
topic = notification_settings.ntfy_topic
|
||||
|
||||
await raw_sendmsg(*args, **kwargs, url=notification_settings.ntfy_url, topic=topic)
|
|
@ -1,2 +0,0 @@
|
|||
from core.lib.schemas import agora_s # noqa
|
||||
from core.lib.schemas import nordigen_s # noqa
|
|
@ -1,298 +0,0 @@
|
|||
from pydantic import BaseModel, Extra
|
||||
|
||||
|
||||
class MyModel(BaseModel):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
class ContactDataBuyerSeller(MyModel):
|
||||
username: str
|
||||
name: str
|
||||
feedback_score: int
|
||||
trade_count: str
|
||||
last_online: str
|
||||
|
||||
|
||||
class ContactDataAd(MyModel):
|
||||
payment_method: str
|
||||
trade_type: str
|
||||
advertiser: ContactDataBuyerSeller
|
||||
asset: str
|
||||
id: str | None
|
||||
contact_id: str | None
|
||||
|
||||
|
||||
class ContactData(MyModel):
|
||||
buyer: ContactDataBuyerSeller
|
||||
seller: ContactDataBuyerSeller
|
||||
amount: str
|
||||
amount_xmr: str | None
|
||||
fee_xmr: str | None
|
||||
amount_btc: str | None
|
||||
fee_btc: str | None
|
||||
advertisement: ContactDataAd
|
||||
contact_id: str
|
||||
currency: str
|
||||
country: str
|
||||
account_info: str
|
||||
price_equation: str
|
||||
is_buying: bool
|
||||
is_selling: bool
|
||||
created_at: str
|
||||
escrowed_at: str
|
||||
funded_at: str
|
||||
canceled_at: str | None
|
||||
closed_at: str | None
|
||||
msg: str
|
||||
released_at: str | None
|
||||
payment_completed_at: str | None
|
||||
disputed_at: str | None
|
||||
arbitrated: bool
|
||||
transfer_to_seller_non_custodial_wallet_transaction_confirmations: str | None
|
||||
transfer_to_buyer_settlement_wallet_transaction_id: str | None
|
||||
transfer_to_buyer_settlement_wallet_transaction_key: str | None
|
||||
buyer_settlement_address: str | None
|
||||
buyer_settlement_fee_level: str | None
|
||||
seller_non_custodial_wallet_mnemonic: str | None
|
||||
transfer_to_seller_non_custodial_wallet_transaction_id: str | None
|
||||
|
||||
|
||||
class ContactActions(MyModel):
|
||||
advertisement_public_view: str | None
|
||||
advertisement_url: str | None
|
||||
message_post_url: str | None
|
||||
messages_url: str | None
|
||||
release_url: str | None
|
||||
cancel_url: str | None
|
||||
dispute_url: str | None
|
||||
|
||||
|
||||
class Contact(MyModel):
|
||||
data: ContactData
|
||||
actions: ContactActions
|
||||
|
||||
|
||||
class DashboardResponseData(MyModel):
|
||||
contact_count: int
|
||||
contact_list: list[Contact]
|
||||
|
||||
|
||||
class DashboardResponse(MyModel):
|
||||
data: DashboardResponseData
|
||||
|
||||
|
||||
class Dashboard(MyModel):
|
||||
success: bool
|
||||
status: int | None
|
||||
message: str
|
||||
response: DashboardResponse
|
||||
|
||||
|
||||
DashboardSchema = {
|
||||
"success": "success",
|
||||
"message": "message",
|
||||
"contact_count": "response.data.contact_count",
|
||||
"contact_list": "response.data.contact_list",
|
||||
}
|
||||
|
||||
|
||||
class Pagination(MyModel):
|
||||
prev: str | None
|
||||
next: str | None
|
||||
total_elements: int
|
||||
total_pages: int
|
||||
current_page: int
|
||||
|
||||
|
||||
class Profile(MyModel):
|
||||
username: str
|
||||
name: str
|
||||
feedback_score: int
|
||||
trade_count: str
|
||||
last_online: str
|
||||
localbitcoins_trade_count: int | None
|
||||
paxful_trade_count: int | None
|
||||
|
||||
|
||||
class BuyBitcoinsOnlineAd(MyModel):
|
||||
ad_id: str
|
||||
countrycode: str
|
||||
created_at: str
|
||||
currency: str
|
||||
max_amount: float | None
|
||||
max_amount_available: float
|
||||
min_amount: float | None
|
||||
msg: str
|
||||
online_provider: str
|
||||
require_trusted_by_advertiser: bool
|
||||
verified_email_required: bool
|
||||
temp_price: float
|
||||
track_max_amount: bool
|
||||
trade_type: str
|
||||
trusted_required: bool
|
||||
visible: bool
|
||||
asset: str
|
||||
payment_method_detail: str | None
|
||||
profile: Profile
|
||||
require_feedback_score: int | None
|
||||
first_time_limit_btc: float | None
|
||||
limit_to_fiat_amounts: str | None
|
||||
|
||||
|
||||
class Actions(MyModel):
|
||||
public_view: str
|
||||
|
||||
|
||||
class BuyBitcoinsOnlineResponseDataAdList(MyModel):
|
||||
data: BuyBitcoinsOnlineAd
|
||||
actions: Actions
|
||||
|
||||
|
||||
class BuyBitcoinsOnlineResponseData(MyModel):
|
||||
ad_count: int
|
||||
ad_list: list[BuyBitcoinsOnlineResponseDataAdList]
|
||||
|
||||
|
||||
class BuyBitcoinsOnlineResponse(MyModel):
|
||||
data: BuyBitcoinsOnlineResponseData
|
||||
pagination: Pagination | None
|
||||
|
||||
|
||||
class BuyBitcoinsOnline(MyModel):
|
||||
success: bool
|
||||
message: str
|
||||
response: BuyBitcoinsOnlineResponse
|
||||
status: int | None
|
||||
|
||||
|
||||
class BuyMoneroOnlineAd(MyModel):
|
||||
ad_id: str
|
||||
countrycode: str
|
||||
created_at: str
|
||||
currency: str
|
||||
max_amount: float | None
|
||||
max_amount_available: float
|
||||
min_amount: float | None
|
||||
msg: str
|
||||
online_provider: str
|
||||
require_trusted_by_advertiser: bool
|
||||
verified_email_required: bool
|
||||
temp_price: float
|
||||
track_max_amount: bool
|
||||
trade_type: str
|
||||
trusted_required: bool
|
||||
visible: bool
|
||||
asset: str
|
||||
payment_method_detail: str | None
|
||||
profile: Profile
|
||||
require_feedback_score: int | None
|
||||
first_time_limit_xmr: float | None
|
||||
limit_to_fiat_amounts: str | None
|
||||
|
||||
|
||||
class BuyMoneroOnlineAdList(MyModel):
|
||||
data: BuyMoneroOnlineAd
|
||||
actions: Actions
|
||||
|
||||
|
||||
class BuyMoneroOnlineResponseData(MyModel):
|
||||
ad_count: int
|
||||
ad_list: list[BuyMoneroOnlineAdList]
|
||||
|
||||
|
||||
class BuyMoneroOnlineResponse(MyModel):
|
||||
data: BuyMoneroOnlineResponseData
|
||||
pagination: Pagination | None
|
||||
|
||||
|
||||
class BuyMoneroOnline(MyModel):
|
||||
success: bool
|
||||
message: str
|
||||
response: BuyMoneroOnlineResponse
|
||||
status: int | None
|
||||
|
||||
|
||||
BuyBitcoinsOnlineSchema = {
|
||||
"success": "success",
|
||||
"message": "message",
|
||||
"ad_count": "response.data.ad_count",
|
||||
"ad_list": "response.data.ad_list",
|
||||
"pagination": "response.pagination",
|
||||
}
|
||||
|
||||
BuyMoneroOnlineSchema = {
|
||||
"success": "success",
|
||||
"message": "message",
|
||||
"ad_count": "response.data.ad_count",
|
||||
"ad_list": "response.data.ad_list",
|
||||
"pagination": "response.pagination",
|
||||
}
|
||||
|
||||
|
||||
class AccountInfo(MyModel):
|
||||
...
|
||||
|
||||
|
||||
class AdsResponseDataAd(MyModel):
|
||||
ad_id: str
|
||||
countrycode: str
|
||||
created_at: str
|
||||
currency: str
|
||||
max_amount: float | None
|
||||
max_amount_available: float
|
||||
min_amount: float | None
|
||||
msg: str
|
||||
online_provider: str
|
||||
require_trusted_by_advertiser: bool
|
||||
verified_email_required: bool
|
||||
temp_price: float
|
||||
track_max_amount: bool
|
||||
trade_type: str
|
||||
trusted_required: bool
|
||||
visible: bool
|
||||
asset: str
|
||||
payment_method_detail: str | None
|
||||
require_feedback_score: int | None
|
||||
first_time_limit_xmr: float | None
|
||||
first_time_limit_btc: float | None
|
||||
limit_to_fiat_amounts: str | None
|
||||
account_info: str
|
||||
price_equation: str
|
||||
|
||||
|
||||
class AdsActions(MyModel):
|
||||
change_form: str | None
|
||||
html_form: str | None
|
||||
public_view: str | None
|
||||
|
||||
|
||||
class AdsResponseDataAdList(MyModel):
|
||||
data: AdsResponseDataAd
|
||||
actions: AdsActions
|
||||
|
||||
|
||||
class AdsResponseData(MyModel):
|
||||
ad_count: int
|
||||
ad_list: list[AdsResponseDataAdList]
|
||||
|
||||
|
||||
class AdsResponse(MyModel):
|
||||
data: AdsResponseData
|
||||
pagination: Pagination | None
|
||||
|
||||
|
||||
class Ads(MyModel):
|
||||
success: bool
|
||||
message: str
|
||||
response: AdsResponse
|
||||
status: int | None
|
||||
|
||||
|
||||
AdsSchema = {
|
||||
"success": "success",
|
||||
"message": "message",
|
||||
"ad_count": "response.data.ad_count",
|
||||
"ad_list": "response.data.ad_list",
|
||||
"pagination": "response.pagination",
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
from pydantic import BaseModel, Extra
|
||||
|
||||
|
||||
class MyModel(BaseModel):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
# TODO: inherit from MyModel
|
||||
|
||||
|
||||
class TokenNew(MyModel):
|
||||
access: str
|
||||
access_expires: int
|
||||
refresh: str
|
||||
refresh_expires: int
|
||||
|
||||
|
||||
TokenNewSchema = {
|
||||
"access": "access",
|
||||
"access_expires": "access_expires",
|
||||
"refresh": "refresh",
|
||||
"refresh_expires": "refresh_expires",
|
||||
}
|
||||
|
||||
|
||||
class RequisitionResult(MyModel):
|
||||
id: str
|
||||
created: str
|
||||
redirect: str
|
||||
status: str
|
||||
institution_id: str
|
||||
agreement: str
|
||||
reference: str
|
||||
accounts: list[str]
|
||||
link: str
|
||||
ssn: str | None
|
||||
account_selection: bool
|
||||
redirect_immediate: bool
|
||||
|
||||
|
||||
class Requisitions(MyModel):
|
||||
count: int
|
||||
next: str | None
|
||||
previous: str | None
|
||||
results: list[RequisitionResult]
|
||||
|
||||
|
||||
RequisitionsSchema = {
|
||||
"count": "count",
|
||||
"next": "next",
|
||||
"previous": "previous",
|
||||
"results": "results",
|
||||
}
|
||||
|
||||
|
||||
class RequisitionsPost(MyModel):
|
||||
id: str
|
||||
created: str
|
||||
redirect: str
|
||||
status: str
|
||||
institution_id: str
|
||||
agreement: str
|
||||
reference: str
|
||||
accounts: list[str]
|
||||
link: str
|
||||
ssn: str | None
|
||||
account_selection: bool
|
||||
redirect_immediate: bool
|
||||
|
||||
|
||||
RequisitionsPostSchema = {
|
||||
"id": "id",
|
||||
"created": "created",
|
||||
"redirect": "redirect",
|
||||
"status": "status",
|
||||
"institution_id": "institution_id",
|
||||
"agreement": "agreement",
|
||||
"reference": "reference",
|
||||
"accounts": "accounts",
|
||||
"link": "link",
|
||||
"ssn": "ssn",
|
||||
"account_selection": "account_selection",
|
||||
"redirect_immediate": "redirect_immediate",
|
||||
}
|
||||
|
||||
|
||||
class Requisition(MyModel):
|
||||
id: str
|
||||
created: str
|
||||
redirect: str
|
||||
status: str
|
||||
institution_id: str
|
||||
agreement: str
|
||||
reference: str
|
||||
accounts: list[str]
|
||||
link: str
|
||||
ssn: str | None
|
||||
account_selection: bool
|
||||
redirect_immediate: bool
|
||||
|
||||
|
||||
RequisitionSchema = {
|
||||
"id": "id",
|
||||
"created": "created",
|
||||
"redirect": "redirect",
|
||||
"status": "status",
|
||||
"institution_id": "institution_id",
|
||||
"agreement": "agreement",
|
||||
"reference": "reference",
|
||||
"accounts": "accounts",
|
||||
"link": "link",
|
||||
"ssn": "ssn",
|
||||
"account_selection": "account_selection",
|
||||
"redirect_immediate": "redirect_immediate",
|
||||
}
|
||||
|
||||
|
||||
class AccountDetailsNested(MyModel):
|
||||
resourceId: str
|
||||
currency: str
|
||||
ownerName: str | None
|
||||
cashAccountType: str | None
|
||||
status: str | None
|
||||
maskedPan: str | None
|
||||
details: str | None
|
||||
iban: str | None
|
||||
bban: str | None
|
||||
name: str | None
|
||||
product: str | None
|
||||
bic: str | None
|
||||
|
||||
|
||||
class AccountDetails(MyModel):
|
||||
account: AccountDetailsNested
|
||||
|
||||
|
||||
AccountDetailsSchema = {
|
||||
"account": "account",
|
||||
}
|
||||
|
||||
|
||||
class AccountBalance(MyModel):
|
||||
balanceAmount: dict[str, str]
|
||||
balanceType: str | None
|
||||
referenceDate: str | None
|
||||
|
||||
|
||||
class AccountBalances(MyModel):
|
||||
balances: list[AccountBalance]
|
||||
summary: str | None
|
||||
|
||||
|
||||
AccountBalancesSchema = {
|
||||
"balances": "balances",
|
||||
"summary": "summary",
|
||||
}
|
||||
|
||||
|
||||
class TXCurrencyAmount(MyModel):
|
||||
amount: str
|
||||
currency: str
|
||||
|
||||
|
||||
class TransactionsCurrencyExchange(MyModel):
|
||||
instructedAmount: TXCurrencyAmount
|
||||
sourceCurrency: str
|
||||
exchangeRate: str
|
||||
unitCurrency: str
|
||||
targetCurrency: str
|
||||
|
||||
|
||||
class TXAccount(MyModel):
|
||||
iban: str | None
|
||||
bban: str | None
|
||||
|
||||
|
||||
class TransactionsNested(MyModel):
|
||||
transactionId: str | None
|
||||
bookingDate: str | None
|
||||
valueDate: str | None
|
||||
bookingDateTime: str | None
|
||||
valueDateTime: str | None
|
||||
transactionAmount: TXCurrencyAmount
|
||||
creditorName: str | None
|
||||
creditorAccount: TXAccount | None
|
||||
debtorName: str | None
|
||||
debtorAccount: TXAccount | None
|
||||
remittanceInformationUnstructuredArray: list[str] | None
|
||||
remittanceInformationUnstructured: str | None
|
||||
proprietaryBankTransactionCode: str | None
|
||||
internalTransactionId: str | None
|
||||
currencyExchange: TransactionsCurrencyExchange | None
|
||||
merchantCategoryCode: str | None
|
||||
|
||||
|
||||
class TransactionsBookedPending(MyModel):
|
||||
booked: list[TransactionsNested] | None
|
||||
pending: list[TransactionsNested] | None
|
||||
|
||||
|
||||
class Transactions(MyModel):
|
||||
transactions: TransactionsBookedPending | None
|
||||
detail: str | None
|
||||
status_code: int | None
|
||||
summary: str | None
|
||||
|
||||
|
||||
TransactionsSchema = {
|
||||
"transactions": "transactions",
|
||||
"status_code": "status_code",
|
||||
"summary": "summary",
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import asyncio
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# from core.clients.aggregators.nordigen import NordigenClient
|
||||
from core.clients.platforms.agora import AgoraClient
|
||||
from core.models import Aggregator, Platform
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("polling")
|
||||
|
||||
INTERVAL = 5
|
||||
|
||||
|
||||
async def poll_aggregator(aggregator):
|
||||
pass
|
||||
|
||||
|
||||
async def poll_platform(platform):
|
||||
client = await AgoraClient(platform)
|
||||
await client.poll()
|
||||
|
||||
|
||||
async def job():
|
||||
platforms = Platform.objects.filter(enabled=True)
|
||||
aggregators = Aggregator.objects.filter(enabled=True)
|
||||
|
||||
tasks = []
|
||||
for platform in platforms:
|
||||
tasks.append(poll_platform(platform))
|
||||
|
||||
for aggregator in aggregators:
|
||||
tasks.append(poll_aggregator(aggregator))
|
||||
|
||||
# Run it all at once
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Start the polling process.
|
||||
"""
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
log.debug(f"Scheduling polling process job every {INTERVAL} seconds")
|
||||
scheduler.add_job(job, "interval", seconds=INTERVAL)
|
||||
scheduler.start()
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
log.info("Process terminating")
|
||||
finally:
|
||||
loop.close()
|
|
@ -1,228 +0,0 @@
|
|||
import asyncio
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from pyotp import TOTP
|
||||
|
||||
from core.clients.aggregators.nordigen import NordigenClient
|
||||
from core.clients.platforms.agora import AgoraClient
|
||||
from core.lib.money import Money
|
||||
from core.lib.notify import sendmsg
|
||||
from core.models import (
|
||||
INTERVAL_CHOICES,
|
||||
Aggregator,
|
||||
LinkGroup,
|
||||
Payout,
|
||||
Platform,
|
||||
Requisition,
|
||||
)
|
||||
from core.util import logs
|
||||
from core.util.validation import Validation
|
||||
|
||||
log = logs.get_logger("scheduling")
|
||||
|
||||
INTERVAL_AGGREGATOR = 10
|
||||
INTERVAL_WITHDRAWAL = 7200
|
||||
|
||||
INTERVALS_PLATFORM = [x[0] for x in INTERVAL_CHOICES]
|
||||
|
||||
|
||||
async def withdrawal_job(group=None):
|
||||
money = Money()
|
||||
if group is not None:
|
||||
groups = [group]
|
||||
else:
|
||||
groups = LinkGroup.objects.filter(enabled=True)
|
||||
for group in groups:
|
||||
checks = await money.check_all(
|
||||
link_group=group, nordigen=NordigenClient, agora=AgoraClient
|
||||
)
|
||||
|
||||
if checks["total_remaining"] > 0:
|
||||
# More than 0 remaining, so we can't withdraw
|
||||
await sendmsg(
|
||||
group.user,
|
||||
f"{checks['total_remaining']} left until you can withdraw.",
|
||||
title="Balance update",
|
||||
)
|
||||
continue
|
||||
|
||||
print("CHECKS", checks)
|
||||
aggregators = Aggregator.objects.filter(
|
||||
user=group.user,
|
||||
link_group=group,
|
||||
)
|
||||
platforms = Platform.objects.filter(
|
||||
user=group.user,
|
||||
link_group=group,
|
||||
)
|
||||
requisitions = Requisition.objects.filter(
|
||||
user=group.user,
|
||||
aggregator__in=aggregators,
|
||||
)
|
||||
pay_list = money.get_pay_list(
|
||||
group,
|
||||
requisitions,
|
||||
platforms,
|
||||
group.user,
|
||||
checks["total_profit_in_xmr"],
|
||||
)
|
||||
collapsed = money.collapse_pay_list(pay_list)
|
||||
if any(collapsed.values()):
|
||||
message = ""
|
||||
print("COLLAPSED", collapsed)
|
||||
for wallet, amount in collapsed.items():
|
||||
print("ITER", wallet, amount)
|
||||
message += f"{wallet}: {amount}\n"
|
||||
print("MESSAGE", message)
|
||||
await sendmsg(
|
||||
group.user,
|
||||
message,
|
||||
title="Your withdrawal is ready!",
|
||||
)
|
||||
|
||||
# TODO: UNCOMMENT
|
||||
# COMMENTED FOR TESTING
|
||||
if not checks["total_profit_in_xmr"] >= 0:
|
||||
return
|
||||
|
||||
total_withdrawal = sum(collapsed.values())
|
||||
if checks["total_xmr_agora"] < total_withdrawal:
|
||||
await sendmsg(
|
||||
group.user,
|
||||
(
|
||||
f"Attempting to withdraw {total_withdrawal}, but you only have"
|
||||
f" {checks['total_xmr_agora']} in your Agora wallet."
|
||||
),
|
||||
title="Withdrawal failed",
|
||||
)
|
||||
continue
|
||||
if group.platforms.count() != 1:
|
||||
raise Exception("You can only have one platform per group")
|
||||
|
||||
platform = group.platforms.first()
|
||||
run = await AgoraClient(platform)
|
||||
|
||||
for wallet, pay_list_iter in pay_list.items():
|
||||
print("WALLET ITER", wallet)
|
||||
if not Validation.is_address("xmr", wallet.address):
|
||||
print("NOT VALID", wallet.address)
|
||||
await sendmsg(
|
||||
group.user,
|
||||
f"Invalid XMR address: {wallet.address}, ignored",
|
||||
title="Invalid XMR address",
|
||||
)
|
||||
continue
|
||||
|
||||
for amount, reason in pay_list_iter:
|
||||
await asyncio.sleep(31)
|
||||
print("ITER", wallet, pay_list_iter)
|
||||
print("ITER SENT", wallet, amount, reason)
|
||||
|
||||
# for wallet, amount in collapsed.items():
|
||||
print("ITER SEND", wallet, amount)
|
||||
amount_rounded = round(amount, 8)
|
||||
cast = {
|
||||
"address": wallet.address,
|
||||
"amount": amount_rounded,
|
||||
"password": platform.password,
|
||||
"otp": TOTP(platform.otp_token).now(),
|
||||
}
|
||||
|
||||
print("CAST ADDRESS", cast["address"])
|
||||
print("CAST AMOUNT", cast["amount"])
|
||||
print("CAST OTP TRUNCATED BY 2", cast["otp"][-2])
|
||||
if not settings.DUMMY:
|
||||
sent = await run.call("wallet_send_xmr", **cast)
|
||||
print("SENT", sent)
|
||||
|
||||
payout = Payout.objects.create( # noqa
|
||||
user=group.user,
|
||||
wallet=wallet,
|
||||
amount=amount_rounded,
|
||||
description=reason,
|
||||
)
|
||||
|
||||
if not settings.DUMMY:
|
||||
payout.response = sent
|
||||
payout.save()
|
||||
|
||||
|
||||
async def aggregator_job():
|
||||
aggregators = Aggregator.objects.filter(enabled=True)
|
||||
for aggregator in aggregators:
|
||||
open_trade_currencies = aggregator.trades_currencies
|
||||
if aggregator.service == "nordigen":
|
||||
instance = None
|
||||
if aggregator.fetch_accounts is True:
|
||||
aggregator.account_info = {}
|
||||
aggregator.save()
|
||||
instance = await NordigenClient(aggregator)
|
||||
await instance.get_all_account_info(store=True)
|
||||
|
||||
# fetch_tasks = []
|
||||
for bank, accounts in aggregator.account_info.items():
|
||||
for account in accounts:
|
||||
account_id = account["account_id"]
|
||||
requisition_id = account["requisition_id"]
|
||||
if account["currency"] not in open_trade_currencies:
|
||||
continue # Next account
|
||||
# Avoid hammering the API with new access token requests
|
||||
if instance is None:
|
||||
instance = await NordigenClient(aggregator)
|
||||
# task = instance.get_transactions(
|
||||
# account_id, req=requisition_id, process=True
|
||||
# )
|
||||
await instance.get_transactions(
|
||||
account_id, req=requisition_id, process=True
|
||||
)
|
||||
# fetch_tasks.append(task)
|
||||
# await asyncio.gather(*fetch_tasks)
|
||||
else:
|
||||
raise NotImplementedError(f"No such client library: {aggregator.service}")
|
||||
aggregator.fetch_accounts = False
|
||||
aggregator.save()
|
||||
|
||||
|
||||
async def platform_job(interval):
|
||||
if interval == 0:
|
||||
return
|
||||
platforms = Platform.objects.filter(enabled=True, cheat_interval_seconds=interval)
|
||||
for platform in platforms:
|
||||
if platform.service == "agora":
|
||||
if platform.cheat is True:
|
||||
instance = await AgoraClient(platform)
|
||||
await instance.cheat()
|
||||
else:
|
||||
raise NotImplementedError(f"No such client library: {platform.service}")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Start the scheduling process.
|
||||
"""
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
log.debug(f"Scheduling {INTERVAL_AGGREGATOR} second aggregator job")
|
||||
scheduler.add_job(aggregator_job, "interval", seconds=INTERVAL_AGGREGATOR)
|
||||
|
||||
log.debug(f"Scheduling {INTERVAL_WITHDRAWAL} second withdrawal job")
|
||||
scheduler.add_job(withdrawal_job, "interval", seconds=INTERVAL_WITHDRAWAL)
|
||||
|
||||
for interval in INTERVALS_PLATFORM:
|
||||
if interval == 0:
|
||||
continue
|
||||
log.debug(f"Scheduling {interval} second platform job")
|
||||
scheduler.add_job(
|
||||
platform_job, "interval", seconds=interval, args=[interval]
|
||||
)
|
||||
scheduler.start()
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
log.info("Process terminating")
|
||||
finally:
|
||||
loop.close()
|
|
@ -1,59 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-04 14:11
|
||||
|
||||
import uuid
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('payment_provider_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('billing_provider_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,35 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-07 16:54
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notificationsettings',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Aggregator',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('service', models.CharField(choices=[('nordigen', 'Nordigen')], max_length=255)),
|
||||
('secret_id', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('secret_key', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('access_token', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('poll_interval', models.IntegerField(default=10)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-07 17:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_alter_notificationsettings_user_aggregator'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-08 10:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_aggregator_enabled'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='access_token_expires',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-09 11:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_aggregator_access_token_expires'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='account_info',
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='currencies',
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-09 11:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_aggregator_account_info_aggregator_currencies'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='fetch_accounts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-09 14:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_aggregator_fetch_accounts'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='aggregator',
|
||||
name='account_info',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aggregator',
|
||||
name='fetch_accounts',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -1,42 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-09 20:50
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_alter_aggregator_account_info_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Platform',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('service', models.CharField(choices=[('agora', 'Agora')], max_length=255)),
|
||||
('token', models.CharField(max_length=1024)),
|
||||
('password', models.CharField(max_length=1024)),
|
||||
('otp_token', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('username', models.CharField(max_length=255)),
|
||||
('send', models.BooleanField(default=True)),
|
||||
('cheat', models.BooleanField(default=False)),
|
||||
('dummy', models.BooleanField(default=False)),
|
||||
('cheat_interval_seconds', models.IntegerField(default=600)),
|
||||
('margin', models.FloatField(default=1.2)),
|
||||
('max_margin', models.FloatField(default=1.3)),
|
||||
('min_margin', models.FloatField(default=1.15)),
|
||||
('min_trade_size_usd', models.FloatField(default=10)),
|
||||
('max_trade_size_usd', models.FloatField(default=4000)),
|
||||
('accept_within_usd', models.FloatField(default=1)),
|
||||
('no_reference_amount_check_max_usd', models.FloatField(default=400)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,48 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-10 00:41
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_platform'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Asset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=64)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Provider',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=64)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ad',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('text', models.TextField()),
|
||||
('payment_details', models.TextField()),
|
||||
('payment_details_real', models.TextField()),
|
||||
('payment_method_details', models.CharField(max_length=255)),
|
||||
('dist_list', models.TextField()),
|
||||
('asset_list', models.ManyToManyField(to='core.asset')),
|
||||
('provider_list', models.ManyToManyField(to='core.provider')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-10 00:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_asset_provider_ad'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='account_map',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='aggregators',
|
||||
field=models.ManyToManyField(to='core.aggregator'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='platforms',
|
||||
field=models.ManyToManyField(to='core.platform'),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-10 02:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_ad_account_map_ad_aggregators_ad_enabled_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='visible',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-10 14:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0011_ad_visible'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='last_messages',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-10 14:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_platform_last_messages'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='platform_ad_ids',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-10 15:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_platform_platform_ad_ids'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='account_whitelist',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-10 15:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_ad_account_whitelist'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='base_usd',
|
||||
field=models.FloatField(default=2800),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='withdrawal_trigger',
|
||||
field=models.FloatField(default=200),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-11 17:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_platform_base_usd_platform_withdrawal_trigger'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='cheat_interval_seconds',
|
||||
field=models.IntegerField(choices=[(60, 'Every minute'), (300, 'Every 5 minutes'), (600, 'Every 10 minutes'), (3600, 'Every hour'), (14400, 'Every 4 hours'), (86400, 'Every day')], default=3600),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-11 17:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0016_alter_platform_cheat_interval_seconds'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='cheat_interval_seconds',
|
||||
field=models.IntegerField(choices=[(0, 'Never'), (5, 'Every 5 seconds'), (15, 'Every 15 seconds'), (30, 'Every 30 seconds'), (60, 'Every minute'), (300, 'Every 5 minutes'), (600, 'Every 10 minutes'), (3600, 'Every hour'), (14400, 'Every 4 hours'), (86400, 'Every day')], default=0),
|
||||
),
|
||||
]
|
|
@ -1,53 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-12 12:18
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_alter_platform_cheat_interval_seconds'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transaction',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('account_id', models.CharField(max_length=255)),
|
||||
('recipient', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('sender', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('amount', models.FloatField()),
|
||||
('currency', models.CharField(max_length=16)),
|
||||
('note', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('reconciled', models.BooleanField(default=False)),
|
||||
('aggregator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.aggregator')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Trade',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('contact_id', models.CharField(max_length=255)),
|
||||
('reference', models.CharField(max_length=255)),
|
||||
('buyer', models.CharField(max_length=255)),
|
||||
('seller', models.CharField(max_length=255)),
|
||||
('amount_fiat', models.FloatField()),
|
||||
('currency', models.CharField(max_length=16)),
|
||||
('amount_crypto', models.FloatField()),
|
||||
('coin', models.CharField(max_length=16)),
|
||||
('provider', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=255)),
|
||||
('ad_id', models.CharField(max_length=255)),
|
||||
('status', models.CharField(max_length=255)),
|
||||
('reconciled', models.BooleanField(default=False)),
|
||||
('released', models.BooleanField(default=False)),
|
||||
('release_response', models.JSONField(default=dict)),
|
||||
('linked', models.ManyToManyField(to='core.transaction')),
|
||||
('platform', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.platform')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-12 12:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_transaction_trade'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='trade',
|
||||
name='status',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='open',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-12 12:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_remove_trade_status_trade_open'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='trade',
|
||||
old_name='coin',
|
||||
new_name='asset',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='trade',
|
||||
name='seller',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='trade',
|
||||
name='type',
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-12 12:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_rename_coin_trade_asset_remove_trade_seller_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='ad_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-12 19:21
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_alter_trade_ad_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='transaction_id',
|
||||
field=models.CharField(default='NONE', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='ts_added',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-13 09:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_transaction_transaction_id_transaction_ts_added'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='linked',
|
||||
field=models.ManyToManyField(blank=True, to='core.transaction'),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-14 09:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0023_alter_trade_linked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='send_reference',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-15 10:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0024_ad_send_reference'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Requisition',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('requisition_id', models.CharField(max_length=255)),
|
||||
('payment_details', models.TextField()),
|
||||
('transaction_source', models.CharField(choices=[('booked', 'Booked'), ('pending', 'Pending')], max_length=255)),
|
||||
('aggregator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.aggregator')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-15 10:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0025_requisition'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='requisition',
|
||||
name='transaction_source',
|
||||
field=models.CharField(choices=[('booked', 'Booked'), ('pending', 'Pending')], default='booked', max_length=255),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-15 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0026_alter_requisition_transaction_source'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='requisition',
|
||||
name='payment_details',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,39 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-17 18:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0027_alter_requisition_payment_details'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='requisition',
|
||||
name='throughput',
|
||||
field=models.FloatField(default=0),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Wallet',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('address', models.CharField(max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='payees',
|
||||
field=models.ManyToManyField(blank=True, to='core.wallet'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='requisition',
|
||||
name='payees',
|
||||
field=models.ManyToManyField(blank=True, to='core.wallet'),
|
||||
),
|
||||
]
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-17 18:31
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0028_requisition_throughput_wallet_platform_payees_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='requisition',
|
||||
name='id',
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='wallet',
|
||||
name='id',
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
|
@ -1,31 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-18 10:12
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0029_alter_requisition_id_alter_wallet_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LinkGroup',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('platform_owner_cut_percentage', models.FloatField(default=0)),
|
||||
('requisition_owner_cut_percentage', models.FloatField(default=0)),
|
||||
('operator_cut_percentage', models.FloatField(default=0)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('aggregators', models.ManyToManyField(to='core.aggregator')),
|
||||
('platforms', models.ManyToManyField(to='core.platform')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,45 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-18 10:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0030_linkgroup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='ad',
|
||||
name='aggregators',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='ad',
|
||||
name='platforms',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='linkgroup',
|
||||
name='aggregators',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='linkgroup',
|
||||
name='platforms',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='link_group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.linkgroup'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='link_group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.linkgroup'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='link_group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.linkgroup'),
|
||||
),
|
||||
]
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-20 09:35
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0031_remove_ad_aggregators_remove_ad_platforms_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OperatorWallets',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('payees', models.ManyToManyField(blank=True, to='core.wallet')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-20 09:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0032_operatorwallets'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='throughput',
|
||||
field=models.FloatField(default=0),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-20 13:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0033_platform_throughput'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='requisition',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.requisition'),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-04-18 07:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0034_transaction_requisition'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ad',
|
||||
name='require_feedback_score',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-04-18 07:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0035_ad_require_feedback_score'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='requisition',
|
||||
name='owner_name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-05-06 10:09
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0036_requisition_owner_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Payout',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('amount', models.FloatField()),
|
||||
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('ts', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('wallet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.wallet')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-05-06 10:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0037_payout'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='payout',
|
||||
name='response',
|
||||
field=models.JSONField(blank=True, default=dict, null=True),
|
||||
),
|
||||
]
|
685
core/models.py
685
core/models.py
|
@ -1,685 +0,0 @@
|
|||
import uuid
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
# from core.lib.customers import get_or_create, update_customer_fields
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
SERVICE_CHOICES = (("nordigen", "Nordigen"),)
|
||||
|
||||
PLATFORM_SERVICE_CHOICES = (("agora", "Agora"),)
|
||||
|
||||
INTERVAL_CHOICES = (
|
||||
(0, "Never"),
|
||||
(5, "Every 5 seconds"),
|
||||
(15, "Every 15 seconds"),
|
||||
(30, "Every 30 seconds"),
|
||||
(60, "Every minute"),
|
||||
(60 * 5, "Every 5 minutes"),
|
||||
(60 * 10, "Every 10 minutes"),
|
||||
(60 * 60, "Every hour"),
|
||||
(60 * 60 * 4, "Every 4 hours"),
|
||||
(86400, "Every day"),
|
||||
)
|
||||
|
||||
TRANSACTION_SOURCE_CHOICES = (
|
||||
("booked", "Booked"),
|
||||
("pending", "Pending"),
|
||||
)
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
payment_provider_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
billing_provider_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
email = models.EmailField(unique=True)
|
||||
|
||||
def get_notification_settings(self):
|
||||
return NotificationSettings.objects.get_or_create(user=self)[0]
|
||||
|
||||
|
||||
class NotificationSettings(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
ntfy_topic = models.CharField(max_length=255, null=True, blank=True)
|
||||
ntfy_url = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification settings for {self.user}"
|
||||
|
||||
|
||||
class LinkGroup(models.Model):
|
||||
"""
|
||||
A group linking Aggregators, Platforms and defining a percentage split
|
||||
that the owners of each should receive.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
platform_owner_cut_percentage = models.FloatField(default=0)
|
||||
requisition_owner_cut_percentage = models.FloatField(default=0)
|
||||
operator_cut_percentage = models.FloatField(default=0)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def payees(self):
|
||||
payees = {}
|
||||
for platform in self.platform_set.all():
|
||||
for payee in platform.payees.all():
|
||||
if "platform" not in payees:
|
||||
payees["platform"] = []
|
||||
payees["platform"].append(payee)
|
||||
|
||||
for aggregator in self.aggregator_set.all():
|
||||
agg_reqs = aggregator.requisition_set.all()
|
||||
for req in agg_reqs:
|
||||
for payee in req.payees.all():
|
||||
if "requisition" not in payees:
|
||||
payees["requisition"] = []
|
||||
payees["requisition"].append(payee)
|
||||
|
||||
return payees
|
||||
|
||||
@property
|
||||
def platforms(self):
|
||||
return Platform.objects.filter(link_group=self)
|
||||
|
||||
@property
|
||||
def aggregators(self):
|
||||
return Aggregator.objects.filter(link_group=self)
|
||||
|
||||
|
||||
class Aggregator(models.Model):
|
||||
"""
|
||||
A connection to an API aggregator to pull transactions from bank accounts.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
service = models.CharField(max_length=255, choices=SERVICE_CHOICES)
|
||||
secret_id = models.CharField(max_length=1024, null=True, blank=True)
|
||||
secret_key = models.CharField(max_length=1024, null=True, blank=True)
|
||||
access_token = models.CharField(max_length=1024, null=True, blank=True)
|
||||
access_token_expires = models.DateTimeField(null=True, blank=True)
|
||||
poll_interval = models.IntegerField(default=10)
|
||||
|
||||
account_info = models.JSONField(default=dict)
|
||||
currencies = models.JSONField(default=list)
|
||||
|
||||
fetch_accounts = models.BooleanField(default=True)
|
||||
|
||||
link_group = models.ForeignKey(
|
||||
LinkGroup, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_service_display()})"
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, obj_id, user):
|
||||
return cls.objects.get(id=obj_id, user=user)
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_for_platform(cls, platform):
|
||||
# aggregators = []
|
||||
# linkgroups = LinkGroup.objects.filter(
|
||||
# platforms=platform,
|
||||
# enabled=True,
|
||||
# )
|
||||
# for link in linkgroups:
|
||||
# for aggregator in link.aggregators.all():
|
||||
# if aggregator not in aggregators:
|
||||
# aggregators.append(aggregator)
|
||||
platform_link = platform.link_group
|
||||
|
||||
# return aggregators
|
||||
return cls.objects.filter(
|
||||
link_group=platform_link,
|
||||
)
|
||||
|
||||
@property
|
||||
def platforms(self):
|
||||
"""
|
||||
Get platforms for this aggregator.
|
||||
Do this by looking up LinkGroups with the aggregator.
|
||||
Then, join them all together.
|
||||
"""
|
||||
return Platform.objects.filter(link_group=self.link_group)
|
||||
|
||||
@property
|
||||
def requisitions(self):
|
||||
"""
|
||||
Get requisitions for this aggregator.
|
||||
Do this by looking up LinkGroups with the aggregator.
|
||||
Then, join them all together.
|
||||
"""
|
||||
return Requisition.objects.filter(
|
||||
aggregator=self,
|
||||
)
|
||||
|
||||
def get_requisition(self, requisition_id):
|
||||
return Requisition.objects.filter(
|
||||
aggregator=self,
|
||||
requisition_id=requisition_id,
|
||||
).first()
|
||||
|
||||
@classmethod
|
||||
def get_currencies_for_platform(cls, platform):
|
||||
# aggregators = Aggregator.get_for_platform(platform)
|
||||
aggregators = platform.aggregators
|
||||
currencies = set()
|
||||
for aggregator in aggregators:
|
||||
for currency in aggregator.currencies:
|
||||
currencies.add(currency)
|
||||
|
||||
return list(currencies)
|
||||
|
||||
@classmethod
|
||||
def get_account_info_for_platform(cls, platform):
|
||||
# aggregators = Aggregator.get_for_platform(platform)
|
||||
aggregators = platform.aggregators
|
||||
account_info = {}
|
||||
for agg in aggregators:
|
||||
for bank, accounts in agg.account_info.items():
|
||||
if bank not in account_info:
|
||||
account_info[bank] = []
|
||||
for account in accounts:
|
||||
account_info[bank].append(account)
|
||||
return account_info
|
||||
|
||||
def add_transaction(self, requisition_id, account_id, tx_data):
|
||||
requisition = Requisition.objects.filter(
|
||||
aggregator=self, requisition_id=requisition_id
|
||||
).first()
|
||||
# if requisition:
|
||||
# tx_data["requisition"] = requisition
|
||||
return Transaction.objects.create(
|
||||
aggregator=self,
|
||||
account_id=account_id,
|
||||
reconciled=False,
|
||||
requisition=requisition,
|
||||
**tx_data,
|
||||
)
|
||||
|
||||
def get_transaction(self, account_id, tx_id):
|
||||
transaction = Transaction.objects.filter(
|
||||
account_id=account_id,
|
||||
transaction_id=tx_id,
|
||||
).first()
|
||||
if not transaction:
|
||||
return None
|
||||
return transaction
|
||||
|
||||
@property
|
||||
def trades(self):
|
||||
"""
|
||||
Get all trades for the platforms of this aggregator's link group.
|
||||
"""
|
||||
trades = []
|
||||
for platform in self.platforms:
|
||||
platform_trades = platform.trades
|
||||
for trade in platform_trades:
|
||||
trades.append(trade)
|
||||
|
||||
return trades
|
||||
|
||||
@property
|
||||
def trades_currencies(self):
|
||||
"""
|
||||
Get all the trade fiat currencies.
|
||||
"""
|
||||
currencies = []
|
||||
for trade in self.trades:
|
||||
if trade.currency not in currencies:
|
||||
currencies.append(trade.currency)
|
||||
|
||||
return currencies
|
||||
|
||||
|
||||
class Wallet(models.Model):
|
||||
"""
|
||||
A wallet for a user.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
address = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
"""
|
||||
A connection to an arbitrage platform like AgoraDesk.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
service = models.CharField(max_length=255, choices=PLATFORM_SERVICE_CHOICES)
|
||||
token = models.CharField(max_length=1024)
|
||||
password = models.CharField(max_length=1024)
|
||||
otp_token = models.CharField(max_length=1024, null=True, blank=True)
|
||||
username = models.CharField(max_length=255)
|
||||
|
||||
send = models.BooleanField(default=True)
|
||||
cheat = models.BooleanField(default=False)
|
||||
dummy = models.BooleanField(default=False)
|
||||
cheat_interval_seconds = models.IntegerField(default=0, choices=INTERVAL_CHOICES)
|
||||
|
||||
margin = models.FloatField(default=1.20)
|
||||
max_margin = models.FloatField(default=1.30)
|
||||
min_margin = models.FloatField(default=1.15)
|
||||
min_trade_size_usd = models.FloatField(default=10)
|
||||
max_trade_size_usd = models.FloatField(default=4000)
|
||||
|
||||
accept_within_usd = models.FloatField(default=1)
|
||||
no_reference_amount_check_max_usd = models.FloatField(default=400)
|
||||
|
||||
last_messages = models.JSONField(default=dict)
|
||||
platform_ad_ids = models.JSONField(default=dict)
|
||||
|
||||
base_usd = models.FloatField(default=2800)
|
||||
withdrawal_trigger = models.FloatField(default=200)
|
||||
|
||||
payees = models.ManyToManyField(Wallet, blank=True)
|
||||
|
||||
link_group = models.ForeignKey(
|
||||
LinkGroup, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
throughput = models.FloatField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_ad(self, platform_ad_id):
|
||||
ad_id = self.platform_ad_ids.get(platform_ad_id, None)
|
||||
if not ad_id:
|
||||
return None
|
||||
ad_object = Ad.objects.filter(
|
||||
id=ad_id, user=self.user, link_group=self.link_group, enabled=True
|
||||
).first()
|
||||
return ad_object
|
||||
|
||||
@classmethod
|
||||
def get_for_user(cls, user):
|
||||
return cls.objects.filter(user=user, enabled=True)
|
||||
|
||||
@property
|
||||
def currencies(self):
|
||||
return Aggregator.get_currencies_for_platform(self)
|
||||
|
||||
@property
|
||||
def account_info(self):
|
||||
return Aggregator.get_account_info_for_platform(self)
|
||||
|
||||
@property
|
||||
def ads(self):
|
||||
"""
|
||||
Get all ads linked to this platform.
|
||||
"""
|
||||
return Ad.objects.filter(
|
||||
user=self.user, enabled=True, link_group=self.link_group
|
||||
)
|
||||
|
||||
@property
|
||||
def ads_assets(self):
|
||||
"""
|
||||
Get all the assets of all the ads.
|
||||
"""
|
||||
assets = set()
|
||||
|
||||
for ad in self.ads:
|
||||
for asset in ad.asset_list.all():
|
||||
assets.add(asset.code)
|
||||
|
||||
return list(assets)
|
||||
|
||||
@property
|
||||
def ads_providers(self):
|
||||
"""
|
||||
Get all the providers of all the ads.
|
||||
"""
|
||||
providers = set()
|
||||
|
||||
for ad in self.ads:
|
||||
for provider in ad.provider_list.all():
|
||||
providers.add(provider.code)
|
||||
|
||||
return list(providers)
|
||||
|
||||
@property
|
||||
def references(self):
|
||||
"""
|
||||
Get references of all our trades that are open.
|
||||
"""
|
||||
references = []
|
||||
our_trades = Trade.objects.filter(platform=self, open=True)
|
||||
for trade in our_trades:
|
||||
references.append(trade.reference)
|
||||
|
||||
return references
|
||||
|
||||
@property
|
||||
def trade_ids(self):
|
||||
"""
|
||||
Get trade IDs of all our trades that are open.
|
||||
"""
|
||||
references = []
|
||||
our_trades = Trade.objects.filter(platform=self, open=True)
|
||||
for trade in our_trades:
|
||||
references.append(trade.contact_id)
|
||||
|
||||
return references
|
||||
|
||||
def get_trade_by_reference(self, reference):
|
||||
return Trade.objects.filter(
|
||||
platform=self,
|
||||
open=True,
|
||||
reference=reference,
|
||||
).first()
|
||||
|
||||
@property
|
||||
def trades(self):
|
||||
"""
|
||||
Get all our open trades.
|
||||
"""
|
||||
our_trades = Trade.objects.filter(platform=self, open=True)
|
||||
return our_trades
|
||||
|
||||
def contact_id_to_reference(self, contact_id):
|
||||
"""
|
||||
Get a reference from a contact_id.
|
||||
"""
|
||||
trade = Trade.objects.filter(
|
||||
platform=self, open=True, contact_id=contact_id
|
||||
).first()
|
||||
if not trade:
|
||||
return None
|
||||
return trade.reference
|
||||
|
||||
def get_trade_by_trade_id(self, trade_id):
|
||||
return Trade.objects.filter(
|
||||
platform=self,
|
||||
open=True,
|
||||
contact_id=trade_id,
|
||||
).first()
|
||||
|
||||
def new_trade(self, trade_cast):
|
||||
trade = Trade.objects.create(
|
||||
platform=self,
|
||||
**trade_cast,
|
||||
)
|
||||
return trade
|
||||
|
||||
def remove_trades_with_reference_not_in(self, reference_list):
|
||||
"""
|
||||
Set trades with reference not in list to open=False.
|
||||
"""
|
||||
trades = Trade.objects.filter(platform=self, open=True)
|
||||
messages = []
|
||||
for trade in trades:
|
||||
if trade.reference not in reference_list:
|
||||
trade.open = False
|
||||
trade.save()
|
||||
msg = f"[{trade.reference}]: Archiving ID: {trade.contact_id}"
|
||||
messages.append(msg)
|
||||
log.info(msg)
|
||||
return messages
|
||||
|
||||
@classmethod
|
||||
def get_for_aggregator(cls, aggregator):
|
||||
# platforms = []
|
||||
# linkgroups = LinkGroup.objects.filter(
|
||||
# aggregators=aggregator,
|
||||
# enabled=True,
|
||||
# )
|
||||
# for link in linkgroups:
|
||||
# for platform in link.platforms.all():
|
||||
# if platform not in platforms:
|
||||
# platforms.append(platform)
|
||||
|
||||
# return platforms
|
||||
|
||||
aggregator_link = aggregator.link_group
|
||||
return cls.objects.filter(
|
||||
link_group=aggregator_link,
|
||||
)
|
||||
|
||||
@property
|
||||
def aggregators(self):
|
||||
"""
|
||||
Get aggregators for this platform.
|
||||
Do this by looking up LinkGroups with the platform.
|
||||
Then, join them all together.
|
||||
"""
|
||||
|
||||
return Aggregator.objects.filter(
|
||||
link_group=self.link_group,
|
||||
)
|
||||
|
||||
@property
|
||||
def platforms(self):
|
||||
"""
|
||||
Get all platforms in this link group.
|
||||
Do this by looking up LinkGroups with the platform.
|
||||
Then, join them all together.
|
||||
"""
|
||||
|
||||
return Platform.objects.filter(
|
||||
link_group=self.link_group,
|
||||
)
|
||||
|
||||
def get_requisition(self, aggregator_id, requisition_id):
|
||||
"""
|
||||
Get a Requisition object with the provided values.
|
||||
"""
|
||||
requisition = Requisition.objects.filter(
|
||||
aggregator_id=aggregator_id,
|
||||
requisition_id=requisition_id,
|
||||
).first()
|
||||
return requisition
|
||||
|
||||
|
||||
class Asset(models.Model):
|
||||
code = models.CharField(max_length=64)
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
code = models.CharField(max_length=64)
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
|
||||
class Ad(models.Model):
|
||||
"""
|
||||
An advert definition
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
text = models.TextField()
|
||||
|
||||
# Shown when the user opens a trade
|
||||
payment_details = models.TextField()
|
||||
|
||||
# Shown after
|
||||
payment_details_real = models.TextField()
|
||||
|
||||
payment_method_details = models.CharField(max_length=255)
|
||||
|
||||
require_feedback_score = models.IntegerField(default=0)
|
||||
|
||||
dist_list = models.TextField()
|
||||
|
||||
asset_list = models.ManyToManyField(Asset)
|
||||
provider_list = models.ManyToManyField(Provider)
|
||||
|
||||
account_map = models.JSONField(default=dict)
|
||||
account_whitelist = models.TextField(null=True, blank=True)
|
||||
|
||||
send_reference = models.BooleanField(default=True)
|
||||
|
||||
visible = models.BooleanField(default=True)
|
||||
link_group = models.ForeignKey(
|
||||
LinkGroup, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return [x.code for x in self.provider_list.all()]
|
||||
|
||||
@property
|
||||
def assets(self):
|
||||
return [x.code for x in self.asset_list.all()]
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, ad_id, user):
|
||||
return cls.objects.filter(id=ad_id, user=user, enabled=True).first()
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
"""
|
||||
A transaction on an aggregator.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
|
||||
requisition = models.ForeignKey(
|
||||
"core.Requisition", null=True, on_delete=models.CASCADE
|
||||
)
|
||||
account_id = models.CharField(max_length=255)
|
||||
transaction_id = models.CharField(max_length=255)
|
||||
|
||||
ts_added = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
recipient = models.CharField(max_length=255, null=True, blank=True)
|
||||
sender = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
amount = models.FloatField()
|
||||
currency = models.CharField(max_length=16)
|
||||
|
||||
note = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
reconciled = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class Trade(models.Model):
|
||||
"""
|
||||
A trade on a Platform.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
platform = models.ForeignKey(Platform, on_delete=models.CASCADE)
|
||||
|
||||
contact_id = models.CharField(max_length=255)
|
||||
reference = models.CharField(max_length=255)
|
||||
buyer = models.CharField(max_length=255)
|
||||
amount_fiat = models.FloatField()
|
||||
currency = models.CharField(max_length=16)
|
||||
amount_crypto = models.FloatField()
|
||||
asset = models.CharField(max_length=16)
|
||||
provider = models.CharField(max_length=255)
|
||||
ad_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
open = models.BooleanField(default=True)
|
||||
|
||||
linked = models.ManyToManyField(Transaction, blank=True)
|
||||
reconciled = models.BooleanField(default=False)
|
||||
|
||||
released = models.BooleanField(default=False)
|
||||
release_response = models.JSONField(default=dict)
|
||||
|
||||
|
||||
class Requisition(models.Model):
|
||||
"""
|
||||
A requisition for an Aggregator
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
|
||||
requisition_id = models.CharField(max_length=255)
|
||||
payment_details = models.TextField(null=True, blank=True)
|
||||
|
||||
owner_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
transaction_source = models.CharField(
|
||||
max_length=255, choices=TRANSACTION_SOURCE_CHOICES, default="booked"
|
||||
)
|
||||
|
||||
throughput = models.FloatField(default=0)
|
||||
payees = models.ManyToManyField(Wallet, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Aggregator: {self.aggregator.name} ID: {self.requisition_id}"
|
||||
|
||||
|
||||
class OperatorWallets(models.Model):
|
||||
"""
|
||||
A list of wallets to designate as operator wallets for this user.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
payees = models.ManyToManyField(Wallet, blank=True)
|
||||
|
||||
|
||||
class Payout(models.Model):
|
||||
"""
|
||||
A profit payout.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
wallet = models.ForeignKey(Wallet, on_delete=models.CASCADE)
|
||||
amount = models.FloatField()
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
response = models.JSONField(default=dict, null=True, blank=True)
|
||||
|
||||
ts = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
assets = {
|
||||
"XMR": "Monero",
|
||||
"BTC": "Bitcoin",
|
||||
}
|
||||
providers = {
|
||||
"REVOLUT": "Revolut",
|
||||
"NATIONAL_BANK": "Bank transfer",
|
||||
"SWISH": "Swish",
|
||||
}
|
||||
for code, name in assets.items():
|
||||
if not Asset.objects.filter(code=code).exists():
|
||||
Asset.objects.create(code=code, name=name)
|
||||
|
||||
for code, name in providers.items():
|
||||
if not Provider.objects.filter(code=code).exists():
|
||||
Provider.objects.create(code=code, name=name)
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
const data = document.currentScript.dataset;
|
||||
const isDebug = data.debug === "True";
|
||||
|
||||
if (isDebug) {
|
||||
document.addEventListener("htmx:beforeOnLoad", function (event) {
|
||||
const xhr = event.detail.xhr;
|
||||
if (xhr.status == 500 || xhr.status == 404) {
|
||||
// Tell htmx to stop processing this response
|
||||
event.stopPropagation();
|
||||
|
||||
document.children[0].innerHTML = xhr.response;
|
||||
|
||||
// Run Django’s inline script
|
||||
// (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript
|
||||
(1, eval)(document.scripts[0].innerText);
|
||||
// Need to directly call Django’s onload function since browser won’t
|
||||
window.onload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,27 +0,0 @@
|
|||
(function(){
|
||||
function maybeRemoveMe(elt) {
|
||||
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
|
||||
if (timing) {
|
||||
setTimeout(function () {
|
||||
elt.parentElement.removeChild(elt);
|
||||
}, htmx.parseInterval(timing));
|
||||
}
|
||||
}
|
||||
|
||||
htmx.defineExtension('remove-me', {
|
||||
onEvent: function (name, evt) {
|
||||
if (name === "htmx:afterProcessNode") {
|
||||
var elt = evt.detail.elt;
|
||||
if (elt.getAttribute) {
|
||||
maybeRemoveMe(elt);
|
||||
if (elt.querySelectorAll) {
|
||||
var children = elt.querySelectorAll("[remove-me], [data-remove-me]");
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
maybeRemoveMe(children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"background_color": "white",
|
||||
"description": "Cryptocurrency arbitrage automation",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/logo.png",
|
||||
"sizes": "800x800",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "Pluto Arbitrage",
|
||||
"short_name": "Pluto",
|
||||
"start_url": "/"
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
// var modal = document.querySelector('.modal'); // assuming you have only 1
|
||||
var modal = document.getElementById("modal");
|
||||
var html = document.querySelector('html');
|
||||
|
||||
var disableModal = function() {
|
||||
modal.classList.remove('is-active');
|
||||
html.classList.remove('is-clipped');
|
||||
var modal_refresh = document.getElementsByClassName("modal-refresh");
|
||||
for(var i = 0; i < modal_refresh.length; i++) {
|
||||
modal_refresh[i].remove();
|
||||
}
|
||||
}
|
||||
|
||||
var elements = document.querySelectorAll('.modal-background');
|
||||
for(var i = 0; i < elements.length; i++) {
|
||||
elements[i].addEventListener('click', function(e) {
|
||||
// elements[i].preventDefault();
|
||||
disableModal();
|
||||
});
|
||||
}
|
||||
|
||||
var elements = document.querySelectorAll('.modal-close');
|
||||
for(var i = 0; i < elements.length; i++) {
|
||||
elements[i].addEventListener('click', function(e) {
|
||||
// elements[i].preventDefault();
|
||||
disableModal();
|
||||
});
|
||||
}
|
||||
|
||||
function activateButtons() {
|
||||
var elements = document.querySelectorAll('.modal-close-button');
|
||||
for(var i = 0; i < elements.length; i++) {
|
||||
elements[i].addEventListener('click', function(e) {
|
||||
// elements[i].preventDefault();
|
||||
disableModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
activateButtons();
|
||||
// modal.querySelector('.modal-close-button').addEventListener('click', function(e) {
|
||||
// e.preventDefault();
|
||||
// modal.classList.remove('is-active');
|
||||
// html.classList.remove('is-clipped');
|
||||
// });
|
|
@ -1,388 +0,0 @@
|
|||
{% load static %}
|
||||
{% load cache %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
{# cache 600 head request.path_info #}
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pluto - {{ request.path_info }}</title>
|
||||
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
|
||||
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
|
||||
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
|
||||
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/gridstack-all.js' %}"></script>
|
||||
<script defer src="{% static 'js/magnet.min.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener("restore-scroll", function(event) {
|
||||
var scrollpos = localStorage.getItem('scrollpos');
|
||||
if (scrollpos) {
|
||||
window.scrollTo(0, scrollpos)
|
||||
};
|
||||
});
|
||||
|
||||
document.addEventListener("htmx:beforeSwap", function(event) {
|
||||
localStorage.setItem('scrollpos', window.scrollY);
|
||||
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Get all "navbar-burger" elements
|
||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
|
||||
// Add a click event on each of them
|
||||
$navbarBurgers.forEach( el => {
|
||||
el.addEventListener('click', () => {
|
||||
|
||||
// Get the target from the "data-target" attribute
|
||||
const target = el.dataset.target;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||
el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.icon { border-bottom: 0px !important;}
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.nowrap-parent {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nowrap-child {
|
||||
display: inline-block;
|
||||
}
|
||||
.htmx-indicator{
|
||||
opacity:0;
|
||||
transition: opacity 500ms ease-in;
|
||||
}
|
||||
.htmx-request .htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
.htmx-request.htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
visibility: hidden;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rounded-tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.table {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
tr {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
a.panel-block {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
a.panel-block:hover {
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.panel, .box, .modal {
|
||||
background-color:rgba(250, 250, 250, 0.5) !important;
|
||||
}
|
||||
.modal, .modal.box{
|
||||
background-color:rgba(210, 210, 210, 0.9) !important;
|
||||
}
|
||||
.modal-background{
|
||||
background-color:rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.has-background-grey-lighter{
|
||||
background-color:rgba(219, 219, 219, 0.5) !important;
|
||||
}
|
||||
.navbar {
|
||||
background-color:rgba(0, 0, 0, 0.03) !important;
|
||||
}
|
||||
|
||||
.grid-stack-item-content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
overflow-y:auto;
|
||||
overflow-x:auto;
|
||||
min-height: 90%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.floating-window {
|
||||
/* background-color:rgba(210, 210, 210, 0.6) !important; */
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
max-height: 300px;
|
||||
z-index: 9000;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.floating-window .panel {
|
||||
background-color:rgba(250, 250, 250, 0.8) !important;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.grid-stack-item:hover .ui-resizable-handle {
|
||||
display: block !important;
|
||||
}
|
||||
.ui-resizable-handle {
|
||||
z-index: 39 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
<!-- Piwik --> {# Yes it's in the source, fight me #}
|
||||
<script type="text/javascript">
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
_paq.push(['setTrackerUrl', 'https://api-dd242151ac50129c3320f209578a406c.s.zm.is']);
|
||||
_paq.push(['setSiteId', 6]);
|
||||
_paq.push(['setApiToken', 'owVUM8fMHxHtyDoIFdyZxx1TWTNECV5ImmoKI1y5muc']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src='https://c87zpt9a74m181wto33r.s.zm.is/embed.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Piwik Code -->
|
||||
</head>
|
||||
{# endcache #}
|
||||
<body>
|
||||
|
||||
{# cache 600 nav request.user.id #}
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
<img src="{% static 'logo-128.png' %}" alt="logo">
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="bar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="bar" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
Home
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="{% url 'profit' type='page' %}">
|
||||
Profit
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'payouts' type='page' %}">
|
||||
Payouts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Banks
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'currencies' type='page' %}">
|
||||
Currencies
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'balances' type='page' %}">
|
||||
Balances
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Platforms
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'trades' type='page' %}">
|
||||
Trades
|
||||
</a>
|
||||
<a class="navbar-item" href="#">
|
||||
Wallets
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'ads' type='page' %}">
|
||||
Ads
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'ads' type='page' %}">
|
||||
Posted Ads
|
||||
</a>
|
||||
<a class="navbar-item" href="#">
|
||||
Market Research
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Setup
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'aggregators' type='page' %}">
|
||||
Bank Aggregators
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'platforms' type='page' %}">
|
||||
Platform Connections
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'wallets' type='page' %}">
|
||||
Profit Wallets
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'linkgroups' type='page' %}">
|
||||
Link Groups
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Account
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
||||
Security
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
||||
Notifications
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'operator_wallets_update' type='page' %}">
|
||||
Operator Wallets
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings.BILLING_ENABLED %}
|
||||
{% if user.is_authenticated %}
|
||||
<a class="navbar-item" href="{# url 'billing' #}">
|
||||
Billing
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a class="navbar-item add-button">
|
||||
Install
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
{% if not user.is_authenticated %}
|
||||
<a class="button" href="{% url 'signup' %}">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button" href="{% url 'two_factor:login' %}">
|
||||
Log in
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<a class="button" href="{% url 'logout' %}">Logout</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{# endcache #}
|
||||
<script>
|
||||
let deferredPrompt;
|
||||
const addBtn = document.querySelector('.add-button');
|
||||
addBtn.style.display = 'none';
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
// Prevent Chrome 67 and earlier from automatically showing the prompt
|
||||
e.preventDefault();
|
||||
// Stash the event so it can be triggered later.
|
||||
deferredPrompt = e;
|
||||
// Update UI to notify the user they can add to home screen
|
||||
addBtn.style.display = 'block';
|
||||
|
||||
addBtn.addEventListener('click', (e) => {
|
||||
// hide our user interface that shows our A2HS button
|
||||
addBtn.style.display = 'none';
|
||||
// Show the prompt
|
||||
deferredPrompt.prompt();
|
||||
// Wait for the user to respond to the prompt
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
console.log('User accepted the A2HS prompt');
|
||||
} else {
|
||||
console.log('User dismissed the A2HS prompt');
|
||||
}
|
||||
deferredPrompt = null;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block outer_content %}
|
||||
{% endblock %}
|
||||
<section class="section">
|
||||
<div class="container is-widescreen">
|
||||
{% block content_wrapper %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
<div id="modals-here">
|
||||
</div>
|
||||
<div id="windows-here">
|
||||
</div>
|
||||
<div id="widgets-here" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -1,103 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load joinsep %}
|
||||
{% block content %}
|
||||
<div class="grid-stack" id="grid-stack-main">
|
||||
<script>
|
||||
var grid = GridStack.init({
|
||||
cellHeight: 20,
|
||||
cellWidth: 50,
|
||||
cellHeightUnit: 'px',
|
||||
auto: true,
|
||||
float: true,
|
||||
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
|
||||
removable: false,
|
||||
animate: true,
|
||||
});
|
||||
// GridStack.init();
|
||||
|
||||
// a widget is ready to be loaded
|
||||
document.addEventListener('load-widget', function(event) {
|
||||
let containers = htmx.findAll('#widget');
|
||||
for (let x = 0, len = containers.length; x < len; x++) {
|
||||
container = containers[x];
|
||||
// get the scripts, they won't be run on the new element so we need to eval them
|
||||
let widgetelement = container.firstElementChild.cloneNode(true);
|
||||
console.log(widgetelement);
|
||||
var scripts = htmx.findAll(widgetelement, "script");
|
||||
var new_id = widgetelement.id;
|
||||
|
||||
// check if there's an existing element like the one we want to swap
|
||||
let grid_element = htmx.find('#grid-stack-main');
|
||||
let existing_widget = htmx.find(grid_element, "#"+new_id);
|
||||
|
||||
// get the size and position attributes
|
||||
if (existing_widget) {
|
||||
let attrs = existing_widget.getAttributeNames();
|
||||
for (let i = 0, len = attrs.length; i < len; i++) {
|
||||
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
|
||||
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
// clear the queue element
|
||||
container.outerHTML = "";
|
||||
// container.firstElementChild.outerHTML = "";
|
||||
grid.addWidget(widgetelement);
|
||||
|
||||
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
|
||||
htmx.process(widgetelement);
|
||||
|
||||
// update the size of the widget according to its content
|
||||
var added_widget = htmx.find(grid_element, "#"+new_id);
|
||||
var itemContent = htmx.find(added_widget, ".control");
|
||||
var scrollheight = itemContent.scrollHeight+80;
|
||||
var verticalmargin = 0;
|
||||
var cellheight = grid.opts.cellHeight;
|
||||
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
|
||||
var opts = {
|
||||
h: height,
|
||||
}
|
||||
grid.update(
|
||||
added_widget,
|
||||
opts
|
||||
);
|
||||
|
||||
// run the JS scripts inside the added element again
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
eval(scripts[i].innerHTML);
|
||||
}
|
||||
}
|
||||
// clear the containers we just added
|
||||
// for (let x = 0, len = containers.length; x < len; x++) {
|
||||
// container = containers[x];
|
||||
// container.inner = "";
|
||||
// }
|
||||
grid.compact();
|
||||
});
|
||||
</script>
|
||||
<!-- <div>
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{# url 'example' type='widget' #}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{# url 'example' type='widget' #}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{# url 'example' type='widget' #}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
</div> -->
|
||||
|
||||
{% endblock %}
|
|
@ -1,79 +0,0 @@
|
|||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Ad' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{# cache 600 objects_ads request.user.id object_list type last #}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>enabled</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>
|
||||
{% if item.enabled %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'ad_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'ad_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{# endcache #}
|
|
@ -1,36 +0,0 @@
|
|||
{% include 'mixins/partials/notify.html' %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>country</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td> {{ item }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'aggregator_country_banks' type=type pk=pk country=item %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
|
@ -1,38 +0,0 @@
|
|||
{% include 'mixins/partials/notify.html' %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>name</th>
|
||||
<th>logo</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td><img src="{{ item.logo }}" width="35" height="35"></td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'aggregator_link' type=type pk=pk bank=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
|
@ -1,94 +0,0 @@
|
|||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>created</th>
|
||||
<th>institution</th>
|
||||
<th>accounts</th>
|
||||
<th>payees</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.created }}</td>
|
||||
<td>{{ item.institution_id }}</td>
|
||||
<td>{{ item.accounts|length }}</td>
|
||||
<td>
|
||||
{% for payee in item.requisition.payees.all %}
|
||||
{{ payee.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'requisition_update' type=type aggregator_id=pk req_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon has-text-black" data-tooltip="Configure">
|
||||
<i class="fa-solid fa-wrench"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'req_delete' type=type pk=pk req_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.id }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'req_info' type=type pk=pk req_id=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'req_info' type=type pk=pk req_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
|
@ -1,83 +0,0 @@
|
|||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Aggregator' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{# cache 600 objects_aggregators request.user.id object_list type last #}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>service</th>
|
||||
<th>link group</th>
|
||||
<th>enabled</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td><a href="{% url 'reqs' type='page' pk=item.id %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.get_service_display }}</td>
|
||||
<td>{{ item.link_group|default_if_none:"—" }}</td>
|
||||
<td>
|
||||
{% if item.enabled %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'aggregator_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'aggregator_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{# endcache #}
|
|
@ -1,28 +0,0 @@
|
|||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<h1 class="title">{{ title_singular }} info</h1>
|
||||
{% for key, item in object.items %}
|
||||
<h1 class="title is-4">Bank: {{ key }}</h1>
|
||||
{% for account in item %}
|
||||
<table class="table is-fullwidth is-hoverable box">
|
||||
<thead>
|
||||
<th>attribute</th>
|
||||
<th>value</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key_i, item_i in account.items %}
|
||||
<tr>
|
||||
<th>{{ key_i }}</th>
|
||||
<td>
|
||||
{% if item_i is not None %}
|
||||
{{ item_i }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
<hr/>
|
||||
{% endfor %}
|
|
@ -1,42 +0,0 @@
|
|||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% load nsep %}
|
||||
|
||||
{% get_last_invalidation 'core.Aggregator' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{# cache 600 objects_banks_balances request.user.id object_list type last #}
|
||||
|
||||
{% for bank, accounts in object_list.items %}
|
||||
<h1 class="title is-4">{{ bank }}</h1>
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ bank }}-table"
|
||||
id="{{ bank }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>currency</th>
|
||||
<th>balance</th>
|
||||
<th>id</th>
|
||||
|
||||
</thead>
|
||||
{% for account in accounts %}
|
||||
<tr>
|
||||
<td>{{ account.currency }}</td>
|
||||
<td>{{ account.balance }}</td>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ account.account_id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endfor %}
|
||||
{# endcache #}
|
|
@ -1,73 +0,0 @@
|
|||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% load nsep %}
|
||||
|
||||
{% get_last_invalidation 'core.Aggregator' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{# cache 600 objects_banks_currencies request.user.id object_list type last #}
|
||||
|
||||
{% for bank, accounts in object_list.items %}
|
||||
<h1 class="title is-4">{{ bank.0 }} <code>{{ bank.1 }}</code>
|
||||
<a
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'requisition_update' type=type aggregator_id=bank.2 req_id=bank.1 %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML">
|
||||
<span class="icon has-text-black" data-tooltip="Configure">
|
||||
<i class="fa-solid fa-wrench"></i>
|
||||
</span>
|
||||
</a>
|
||||
</h1>
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ bank }}-table"
|
||||
id="{{ bank }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>currency</th>
|
||||
<th>owner</th>
|
||||
<th>details</th>
|
||||
<th>payment</th>
|
||||
<th>id</th>
|
||||
<th>actions</th>
|
||||
|
||||
</thead>
|
||||
{% for account in accounts %}
|
||||
<tr>
|
||||
<td>{{ account.currency }}</td>
|
||||
<td>{{ account.ownerName }}</td>
|
||||
<td>{{ account.details|default_if_none:"—" }}</td>
|
||||
<td>
|
||||
{% for item in account.account_number.values %}
|
||||
<code>{{ item|default_if_none:"—" }}</code>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ account.account_id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'transactions' type='page' account_id=account.account_id aggregator_id=account.aggregator_id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-table-list"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endfor %}
|
||||
{# endcache #}
|
|
@ -1,70 +0,0 @@
|
|||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Aggregator' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{# cache 600 objects_banks_transactions request.user.id object_list type last #}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>ts</th>
|
||||
<th>recipient</th>
|
||||
<th>sender</th>
|
||||
<th>amount</th>
|
||||
<th>currency</th>
|
||||
<th>reference</th>
|
||||
<th>state</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.proprietaryBankTransactionCode == 'EXCHANGE' %}has-background-grey-light
|
||||
{% elif item.amount < 0 %}has-background-danger-light
|
||||
{% elif item.amount > 0 %}has-background-success-light
|
||||
{% endif %}">
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.transaction_id }}');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.ts }}</td>
|
||||
<td>
|
||||
{{ item.creditorName }}
|
||||
{% for item in item.creditorAccount.values %}
|
||||
{{ item|default_if_none:"—" }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.debtorName }}
|
||||
{% for item in item.debtorAccount.values %}
|
||||
{{ item|default_if_none:"—" }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{{ item.amount }}</td>
|
||||
<td>{{ item.currency }}</td>
|
||||
<td>{{ item.reference }}</td>
|
||||
<td>
|
||||
{% if item.state == 'pending' %}
|
||||
<span class="icon has-text-warning" data-tooltip="Pending">
|
||||
<i class="fa-solid fa-hourglass" aria-hidden="true"></i>
|
||||
</span>
|
||||
{% elif item.state == 'booked' %}
|
||||
<span class="icon has-text-success" data-tooltip="Booked">
|
||||
<i class="fa-solid fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{# endcache #}
|
|
@ -1,26 +0,0 @@
|
|||
<h1 class="title">Simulation with live data</h1>
|
||||
<p>Total profit (XMR): {{ total_profit }}</p>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="content">
|
||||
<ul>
|
||||
{% for wallet, list in pay_list.items %}
|
||||
<li>{{ wallet.name }} to <code>{{ wallet.address }}</code>: </li>
|
||||
<ul>
|
||||
{% for amount, item in list %}
|
||||
<li>{{ amount }} for {{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="title is-4">Total for wallets (XMR)</h1>
|
||||
<div class="box">
|
||||
{% for wallet, amount in collapsed.items %}
|
||||
{{ wallet }}: {{ amount }}
|
||||
<progress class="progress" value="{{ amount }}" max="{{ total_profit }}"></progress>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,152 +0,0 @@
|
|||
<h1 class="title">Information for link group {{ linkgroup.name }}</h1>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title is-4">Platforms</h1>
|
||||
{% include 'partials/platform-list.html' with object_list=platforms type=type %}
|
||||
<h1 class="title is-4">Aggregators</h1>
|
||||
{% include 'partials/aggregator-list.html' with object_list=aggregators type=type %}
|
||||
|
||||
<h1 class="title is-4">Requisitions</h1>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>aggregator</th>
|
||||
<th>payees</th>
|
||||
<th>throughput</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in requisitions %}
|
||||
<tr>
|
||||
<td>{{ item.id|truncatechars:20 }}</td>
|
||||
<td>{{ item.aggregator.name }}</td>
|
||||
<td>
|
||||
{% for payee in item.payees.all %}
|
||||
{{ payee.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{{ item.throughput }}</td>
|
||||
<td>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'requisition_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete requisition {{ item.id }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h1 class="title is-4">Platform payees</h1>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<th>name</th>
|
||||
<th>address</th>
|
||||
</thead>
|
||||
{% for item in linkgroup.payees.platform %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.address }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<h1 class="title is-4">Requisition payees</h1>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<th>name</th>
|
||||
<th>address</th>
|
||||
</thead>
|
||||
{% for item in linkgroup.payees.requisition %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.address }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<h1 class="title is-4">Split</h1>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<th>attribute</th>
|
||||
<th>%</th>
|
||||
<th>graphic</th>
|
||||
|
||||
</thead>
|
||||
<tr>
|
||||
<td>platform</td>
|
||||
<td>{{ linkgroup.platform_owner_cut_percentage }}</td>
|
||||
<td>
|
||||
<progress class="progress" value="{{ linkgroup.platform_owner_cut_percentage }}" max="100"></progress>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>requisition</td>
|
||||
<td>{{ linkgroup.requisition_owner_cut_percentage }}</td>
|
||||
<td>
|
||||
<progress class="progress" value="{{ linkgroup.requisition_owner_cut_percentage }}" max="100"></progress>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>operator</td>
|
||||
<td>{{ linkgroup.operator_cut_percentage }}</td>
|
||||
<td>
|
||||
<progress class="progress" value="{{ linkgroup.operator_cut_percentage }}" max="100"></progress>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="title">Simulation for $1000</h1>
|
||||
<p>
|
||||
Assuming equal throughput for platforms and requisitions.
|
||||
<strong>Note that this is just a simulation, equal throughput is highly unlikely.</strong>
|
||||
</p>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="content">
|
||||
<ul>
|
||||
{% for key, list in simulation.items %}
|
||||
<li>
|
||||
{{ key.0 }}: ${{ key.1 }}
|
||||
<ul>
|
||||
{% for item in list %}
|
||||
<li>${{ item.amount }} to {{ item.name }} at <code>{{ item.address }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="title is-4">Total for wallets</h1>
|
||||
<div class="box">
|
||||
{% for wallet, pay_list in pay_list.items %}
|
||||
{{ wallet }}: ${{ pay_list.amount }}
|
||||
<progress class="progress" value="{{ pay_list.amount }}" max="1000"></progress>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="box">
|
||||
{% for key, list in simulation.items %}
|
||||
<strong>{{ key.0 }}: ${{ key.1 }}</strong>
|
||||
<progress class="progress" value="{{ key.1 }}" max="1000"></progress>
|
||||
{% for item in list %}
|
||||
<em>{{ item.name }}: ${{ item.amount }}</em><progress class="progress" value="{{ item.amount }}" max="{{ item.max }}"></progress>
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue