From ff96ec98936a18d2ea2550cea5b004671a41e9be Mon Sep 17 00:00:00 2001 From: Dmytro Samoylenko Date: Tue, 6 Aug 2024 08:04:56 +0000 Subject: [PATCH] Implement fixed fee --- .../versions/cd6076e578ca_add_fee_policies.py | 34 +++ shkeeper/__init__.py | 20 +- shkeeper/models.py | 32 ++- shkeeper/modules/classes/tron_token.py | 4 +- shkeeper/static/js/custom-rates.js | 262 +++++++----------- shkeeper/templates/wallet/page.j2 | 2 +- shkeeper/templates/wallet/rates.j2 | 189 ++++++++----- shkeeper/utils.py | 3 +- shkeeper/wallet.py | 38 ++- 9 files changed, 330 insertions(+), 254 deletions(-) create mode 100644 migrations/versions/cd6076e578ca_add_fee_policies.py diff --git a/migrations/versions/cd6076e578ca_add_fee_policies.py b/migrations/versions/cd6076e578ca_add_fee_policies.py new file mode 100644 index 0000000..9b8466c --- /dev/null +++ b/migrations/versions/cd6076e578ca_add_fee_policies.py @@ -0,0 +1,34 @@ +"""Add fee policies + +Revision ID: cd6076e578ca +Revises: 0319bf7b9426 +Create Date: 2024-07-31 10:39:30.170808 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cd6076e578ca' +down_revision = '0319bf7b9426' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('exchange_rate', schema=None) as batch_op: + batch_op.add_column(sa.Column('fixed_fee', sa.Numeric(), nullable=True)) + batch_op.add_column(sa.Column('fee_policy', sa.Enum('NO_FEE', 'PERCENT_FEE', 'FIXED_FEE', 'PERCENT_OR_MINIMAL_FIXED_FEE', name='feecalculationpolicy'), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('exchange_rate', schema=None) as batch_op: + batch_op.drop_column('fee_policy') + batch_op.drop_column('fixed_fee') + + # ### end Alembic commands ### diff --git a/shkeeper/__init__.py b/shkeeper/__init__.py index 914b914..67e982b 100644 --- a/shkeeper/__init__.py +++ b/shkeeper/__init__.py @@ -8,6 +8,8 @@ from flask import Flask import requests +from shkeeper.wallet_encryption import WalletEncryptionRuntimeStatus + from .utils import format_decimal from flask_apscheduler import APScheduler @@ -45,6 +47,8 @@ def create_app(test_config=None): UNCONFIRMED_TX_NOTIFICATION=bool(os.environ.get('UNCONFIRMED_TX_NOTIFICATION')), REQUESTS_TIMEOUT=int(os.environ.get('REQUESTS_TIMEOUT', 10)), REQUESTS_NOTIFICATION_TIMEOUT=int(os.environ.get('REQUESTS_NOTIFICATION_TIMEOUT', 30)), + DEV_MODE=bool(os.environ.get('DEV_MODE', False)), + DEV_MODE_ENC_PW=os.environ.get('DEV_MODE_ENC_PW'), ) if test_config is None: @@ -62,7 +66,10 @@ def create_app(test_config=None): # clear all session on app restart if sess_dir := app.config.get('SESSION_FILE_DIR'): - shutil.rmtree(sess_dir, ignore_errors=True) + if app.config.get('DEV_MODE'): + pass + else: + shutil.rmtree(sess_dir, ignore_errors=True) from flask_session import Session Session(app) @@ -100,7 +107,6 @@ def default(self, obj): from .models import Wallet, User, PayoutDestination, Invoice, ExchangeRate, Setting db.create_all() - flask_migrate.upgrade() # Create default user default_user = 'admin' @@ -109,6 +115,10 @@ def default(self, obj): db.session.add(admin) db.session.commit() + flask_migrate.stamp(revision='head') + else: + flask_migrate.upgrade() + # Register rate sources import shkeeper.modules.rates @@ -136,6 +146,12 @@ def default(self, obj): db.session.commit() app.logger.info(f'WalletEncryptionPersistentStatus is set to {WalletEncryptionPersistentStatus(int(setting.value))}') + if app.config.get('DEV_MODE'): + if wallet_encryption.wallet_encryption.persistent_status() is WalletEncryptionPersistentStatus.enabled: + if key := app.config.get('DEV_MODE_ENC_PW'): + wallet_encryption.wallet_encryption.set_key(key) + wallet_encryption.wallet_encryption.set_runtime_status(WalletEncryptionRuntimeStatus.success) + from . import tasks scheduler.start() diff --git a/shkeeper/models.py b/shkeeper/models.py index 2545d29..6475ad7 100644 --- a/shkeeper/models.py +++ b/shkeeper/models.py @@ -1,3 +1,4 @@ +from collections import namedtuple import enum from datetime import datetime, timedelta from decimal import Decimal @@ -102,13 +103,26 @@ def do_payout(self): return res +class FeeCalculationPolicy(namedtuple('FeeCalculationPolicy', 'name desc'), enum.Enum): + NO_FEE = 'NO_FEE', 'No fee' + PERCENT_FEE = 'PERCENT_FEE', 'Percent' + FIXED_FEE = 'FIXED_FEE', 'Fixed fee' + PERCENT_OR_MINIMAL_FIXED_FEE = 'PERCENT_OR_MINIMAL_FIXED_FEE', 'Percent but not less than a minimal fixed fee' + + def __str__(self) -> str: + return self.name + + class ExchangeRate(db.Model): id = db.Column(db.Integer, primary_key=True) source = db.Column(db.String, default='dynamic') # manual or dynamic (binance, etc) crypto = db.Column(db.String) fiat = db.Column(db.String) rate = db.Column(db.Numeric, default=0) # crypto / fiat, only used is source is manual - fee = db.Column(db.Numeric, default=2) + fee = db.Column(db.Numeric, default=2) # percent + fixed_fee = db.Column(db.Numeric, default=0) + fee_policy = db.Column(db.Enum(FeeCalculationPolicy), default=FeeCalculationPolicy.PERCENT_FEE) + __table_args__ = (db.UniqueConstraint('crypto', 'fiat'), ) def get_rate(self): @@ -118,9 +132,23 @@ def get_rate(self): rs = RateSource.instances.get(self.source, RateSource.instances.get('binance')) return rs.get_rate(self.fiat, self.crypto) + def get_fee(self, amount: Decimal) -> Decimal: + fcp = FeeCalculationPolicy + if self.fee_policy == fcp.NO_FEE: + return Decimal(0) + percent_fee = Decimal(amount * (self.fee / 100)) + if self.fee_policy is None or self.fee_policy == fcp.PERCENT_FEE: + return percent_fee + if self.fee_policy == fcp.FIXED_FEE: + return self.fixed_fee + if self.fee_policy == fcp.PERCENT_OR_MINIMAL_FIXED_FEE: + return Decimal(max(percent_fee, self.fixed_fee)) + raise Exception(f'Unexpected fee policy: {self.fee_policy}') + def convert(self, amount): rate = self.get_rate() - converted = (amount / rate) * (1 + (self.fee / 100)) + converted = amount / rate + converted += self.get_fee(converted) # add fee crypto = Crypto.instances[self.crypto] converted = round(converted, crypto.precision) return (converted, rate) diff --git a/shkeeper/modules/classes/tron_token.py b/shkeeper/modules/classes/tron_token.py index 05e2418..ceac9b0 100644 --- a/shkeeper/modules/classes/tron_token.py +++ b/shkeeper/modules/classes/tron_token.py @@ -35,7 +35,7 @@ def balance(self): ).json(parse_float=Decimal) balance = response['balance'] except Exception as e: - app.logger.warning(f"Response: {response} Error: {e}") + app.logger.exception('balance error') balance = False return Decimal(balance) @@ -74,7 +74,7 @@ def getaddrbytx(self, txid): auth=self.get_auth_creds(), ).json(parse_float=Decimal) return [[response['address'], Decimal(response['amount']), response['confirmations'], response['category']]] - + def get_confirmations_by_txid(self, txid): transactions = self.getaddrbytx(txid) _, _, confirmations, _ = transactions[0] diff --git a/shkeeper/static/js/custom-rates.js b/shkeeper/static/js/custom-rates.js index d30107a..4cbb9db 100644 --- a/shkeeper/static/js/custom-rates.js +++ b/shkeeper/static/js/custom-rates.js @@ -1,134 +1,46 @@ -function changeSource(ind) -{ - let coinCostArray = document.getElementsByClassName("coin-cost"); - let ratesCurArray = document.getElementsByClassName("rates-current"); +function changeSource(ind) { + let rate_inputs = document.getElementsByClassName("rates-cost-value"); let sourceType = selectArray[ind].value; - if(sourceType == "manual") - { - coinCostArray[ind].style.display = "flex"; - ratesCurArray[ind].style.display = "none"; + if (sourceType == "manual") { + rate_inputs[ind].readOnly = false + rate_inputs[ind].dataset.manual = "on" + rate_inputs[ind].value = rate_inputs[ind].dataset.manual_rate; + } else { + delete rate_inputs[ind].dataset.manual + rate_inputs[ind].readOnly = true } - else if(sourceType == "binance"){ - coinCostArray[ind].style.display = "none"; - ratesCurArray[ind].style.display = "flex"; - } -} - -function saveRates() -{ - let cryptos = document.querySelectorAll('.rates-name'); - cryptos.forEach( item => { - let parent = item.parentNode; - - let crypto = parent.querySelector(".rates-name").getAttribute('crypto'); - let data = composeData(parent); - - sendSource(crypto, data); - }); - - function composeData(parentNode) - { - let fiat = parentNode.querySelector(".rates-name").getAttribute('fiat'); - let source = parentNode.querySelector(".rates-source-value").value; - let rate = validateFloatValue(parentNode.querySelector(".rates-cost-value")); - let fee = validateFloatValue(parentNode.querySelector(".rates-fee-value")); - - if(source == "manual") - { - return JSON.stringify({ - fiat: fiat, - source: source, - rate: rate, - fee: fee - }); - } - else{ - return JSON.stringify({ - fiat: fiat, - source: source, - fee: fee - }); - } - - - function validateFloatValue(element) - { - element.value = parseFloat(element.value); - if(element.value.match(/^\d+\.*\d*$/)) - { - element.classList.remove("red-highlight"); - } - else{ - element.classList.add("red-highlight"); - check = false; - } - return element.value; - } - } - - function sendSource(crypto,data) - { - const http = new XMLHttpRequest(); - http.open("POST"," /api/v1/" + crypto + "/exchange-rate",true); - http.send(data); - http.onload = function(){ - let data = checkAnswer(this); - if(data!=false) - { - alert("Rates for " + crypto + " saved"); - } - else{ - alert("Please input valid value for " + crypto); - } - } - - - function checkAnswer(response) - { - if(response.status == 200) - { - let data = JSON.parse(response.responseText); - if(data['status'] != "success") - { - alert(data['message']); - return false; - } - else - { - return data; - } - } - alert("Response stauts: " + response.status); - return false; - } - } - - } -function refreshRates() -{ - let currentRates = document.getElementsByClassName("rates-current-value"); - for(let i = 0; i < currentRates.length; i++) - { - currentRates[i].id = currentRates[i].id.toLowerCase(); +function change_fee_policy(ind) { + let percentArray = document.getElementsByClassName("percent-fee"); + let fixedArray = document.getElementsByClassName("fixed-fee"); + let policy = selectArray2[ind].value; + switch (policy) { + case "NO_FEE": + percentArray[ind].style.display = "none"; + fixedArray[ind].style.display = "none"; + break; + case "PERCENT_FEE": + percentArray[ind].style.display = "block"; + fixedArray[ind].style.display = "none"; + break; + case "FIXED_FEE": + percentArray[ind].style.display = "none"; + fixedArray[ind].style.display = "block"; + break; + case "PERCENT_OR_MINIMAL_FIXED_FEE": + percentArray[ind].style.display = "block"; + fixedArray[ind].style.display = "block"; + break; } - helperCycleBinanceRates(currentRates); } -function helperCycleBinanceRates(currentRates) -{ - for(let i = 0; i < currentRates.length; i++) - { - getBinanceRealTRates(currentRates[i].id, currentRates[i]); - } -} - -function getBinanceRealTRates(pairName, currentRate) -{ +function getBinanceRealTRates(pairName, currentRate) { if (['usdtusdt', 'eth-usdtusdt', 'bnb-usdtusdt', 'polygon-usdtusdt', 'avalanche-usdtusdt'].includes(pairName)) { - currentRate.innerHTML = "1"; + if (!currentRate.dataset.manual) { + currentRate.value = "1"; + } return; } @@ -138,64 +50,82 @@ function getBinanceRealTRates(pairName, currentRate) let url = 'wss://stream.binance.com:9443/ws/' + pairName + '@miniTicker'; let bSocket = new WebSocket(url); - bSocket.onmessage = function(data) { + bSocket.onmessage = function (data) { let quotation = JSON.parse(data.data); let price = parseFloat(quotation['c']); - currentRate.innerHTML = price; - } -} - -function setAll() -{ - let addedFee = document.getElementById("select-fee"); - let selectSource = document.getElementById("select-all"); - - let selectArray = document.getElementsByClassName("rates-source-value"); - let addFeeArray = document.getElementsByClassName("rates-fee-value"); - for(let i = 0; i < selectArray.length; i++) - { - selectArray[i].value=selectSource.value; - addFeeArray[i].value=addedFee.value; - changeSource(i) + if (!currentRate.dataset.manual) { + currentRate.value = price; + } } - } let selectArray = document.getElementsByClassName("select-rate"); -for(let i = 0; i < selectArray.length; i++) -{ - selectArray[i].addEventListener("change",function(){changeSource(i);}); +for (let i = 0; i < selectArray.length; i++) { + selectArray[i].addEventListener("change", function () { changeSource(i); }); changeSource(i); } -function activeRateTab() -{ - document.getElementById("wallet-link").classList.remove("nav-link--active"); - document.getElementById("rate-link").classList.add("nav-link--active"); +let selectArray2 = document.getElementsByClassName("fee_policy_select"); +for (let i = 0; i < selectArray2.length; i++) { + selectArray2[i].addEventListener("change", function () { change_fee_policy(i); }); } -function formatInitValue() -{ - function representVal(element) - { - element.value = parseFloat(element.value); - return element.value; +document.getElementById("all_fee_policy").addEventListener("change", function () { + let per_fee = document.getElementById("percent-fee-for-all-container"); + let fix_fee = document.getElementById("fixed-fee-for-all-container"); + let all_pol_sel = document.getElementById('all_fee_policy') + console.log(all_pol_sel.value); + switch (all_pol_sel.value) { + case "NO_FEE": + per_fee.style.display = "none"; + fix_fee.style.display = "none"; + break; + case "PERCENT_FEE": + per_fee.style.display = "block"; + fix_fee.style.display = "none"; + break; + case "FIXED_FEE": + per_fee.style.display = "none"; + fix_fee.style.display = "block"; + break; + case "PERCENT_OR_MINIMAL_FIXED_FEE": + per_fee.style.display = "block"; + fix_fee.style.display = "block"; + break; } +}); - let rates = document.querySelectorAll('.rates-cost-value'); - rates.forEach(item => { - representVal(item); - }); - let fees = document.querySelectorAll('.rates-fee-value '); - fees.forEach(item => { - representVal(item); - }); -} -window.addEventListener('DOMContentLoaded',formatInitValue); +document.getElementById("set-all").addEventListener("click", function (e) { + e.preventDefault() + let percent_fee_input_array = document.getElementsByClassName("percent-fee-value"); + let fixed_fee_input_array = document.getElementsByClassName("fixed-fee-value"); + let source_select_array = document.getElementsByClassName("rates-source-value"); + let policy_select_array = document.getElementsByClassName("fee_policy_select"); -document.getElementById("save-rates").addEventListener("click",function(){saveRates()}); + console.log(policy_select_array) -document.getElementById("set-all").addEventListener("click",function(){setAll()}); + let percent_fee_input = document.getElementById("percent-fee-value-for-all"); + let fixed_fee_input = document.getElementById("fixed-fee-value-for-all"); + let source_select = document.getElementById("select-all"); + let policy_select = document.getElementById("all_fee_policy"); -window.addEventListener('DOMContentLoaded',function(){refreshRates()}); + for (let i = 0; i < source_select_array.length; i++) { + console.log(i) + percent_fee_input_array[i].value = percent_fee_input.value; + fixed_fee_input_array[i].value = fixed_fee_input.value; + source_select_array[i].value = source_select.value; + policy_select_array[i].value = policy_select.value; + + changeSource(i) + change_fee_policy(i) + } +}); + +window.addEventListener('DOMContentLoaded', function () { + let currentRates = document.getElementsByClassName("rates-cost-value"); + for (let i = 0; i < currentRates.length; i++) { + let pairName = currentRates[i].dataset.pairname.toLowerCase(); + getBinanceRealTRates(pairName, currentRates[i]); + } +}); diff --git a/shkeeper/templates/wallet/page.j2 b/shkeeper/templates/wallet/page.j2 index 4eca18b..10fb827 100644 --- a/shkeeper/templates/wallet/page.j2 +++ b/shkeeper/templates/wallet/page.j2 @@ -35,7 +35,7 @@