mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-22 22:25:47 +01:00
[feat] custom exchange providers (#2797)
This commit is contained in:
parent
200b9b127c
commit
524a4c9213
16 changed files with 665 additions and 130 deletions
|
@ -26,6 +26,7 @@ from lnbits.core.helpers import migrate_extension_database
|
||||||
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
||||||
from lnbits.core.tasks import ( # watchdog_task
|
from lnbits.core.tasks import ( # watchdog_task
|
||||||
audit_queue,
|
audit_queue,
|
||||||
|
collect_exchange_rates_data,
|
||||||
killswitch_task,
|
killswitch_task,
|
||||||
purge_audit_data,
|
purge_audit_data,
|
||||||
wait_for_audit_data,
|
wait_for_audit_data,
|
||||||
|
@ -455,6 +456,7 @@ def register_async_tasks(app: FastAPI):
|
||||||
# create_permanent_task(watchdog_task)
|
# create_permanent_task(watchdog_task)
|
||||||
create_permanent_task(killswitch_task)
|
create_permanent_task(killswitch_task)
|
||||||
create_permanent_task(purge_audit_data)
|
create_permanent_task(purge_audit_data)
|
||||||
|
create_permanent_task(collect_exchange_rates_data)
|
||||||
|
|
||||||
# server logs for websocket
|
# server logs for websocket
|
||||||
if settings.lnbits_admin_ui:
|
if settings.lnbits_admin_ui:
|
||||||
|
|
|
@ -4,6 +4,7 @@ from typing import Any, Optional
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
|
from lnbits.db import dict_to_model
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
AdminSettings,
|
AdminSettings,
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
|
@ -18,7 +19,8 @@ async def get_super_settings() -> Optional[SuperSettings]:
|
||||||
if data:
|
if data:
|
||||||
super_user = await get_settings_field("super_user")
|
super_user = await get_settings_field("super_user")
|
||||||
super_user_id = super_user.value if super_user else None
|
super_user_id = super_user.value if super_user else None
|
||||||
return SuperSettings(**{"super_user": super_user_id, **data})
|
settings_dict = {"super_user": super_user_id, **data}
|
||||||
|
return dict_to_model(settings_dict, SuperSettings)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from loguru import logger
|
||||||
from py_vapid import Vapid
|
from py_vapid import Vapid
|
||||||
from py_vapid.utils import b64urlencode
|
from py_vapid.utils import b64urlencode
|
||||||
|
|
||||||
|
from lnbits.db import dict_to_model
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
readonly_variables,
|
readonly_variables,
|
||||||
|
@ -37,14 +38,16 @@ async def check_webpush_settings():
|
||||||
|
|
||||||
|
|
||||||
def update_cached_settings(sets_dict: dict):
|
def update_cached_settings(sets_dict: dict):
|
||||||
for key, value in sets_dict.items():
|
editable_settings = dict_to_model(sets_dict, EditableSettings)
|
||||||
|
for key in sets_dict.keys():
|
||||||
if key in readonly_variables:
|
if key in readonly_variables:
|
||||||
continue
|
continue
|
||||||
if key not in settings.dict().keys():
|
if key not in settings.dict().keys():
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
value = getattr(editable_settings, key)
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
logger.warning(f"Failed overriding setting: {key}.")
|
||||||
if "super_user" in sets_dict:
|
if "super_user" in sets_dict:
|
||||||
settings.super_user = sets_dict["super_user"]
|
settings.super_user = sets_dict["super_user"]
|
||||||
|
|
|
@ -19,6 +19,7 @@ from lnbits.core.services import (
|
||||||
)
|
)
|
||||||
from lnbits.settings import get_funding_source, settings
|
from lnbits.settings import get_funding_source, settings
|
||||||
from lnbits.tasks import send_push_notification
|
from lnbits.tasks import send_push_notification
|
||||||
|
from lnbits.utils.exchange_rates import btc_rates
|
||||||
|
|
||||||
api_invoice_listeners: Dict[str, asyncio.Queue] = {}
|
api_invoice_listeners: Dict[str, asyncio.Queue] = {}
|
||||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||||
|
@ -188,3 +189,27 @@ async def purge_audit_data():
|
||||||
|
|
||||||
# clean every hour
|
# clean every hour
|
||||||
await asyncio.sleep(60 * 60)
|
await asyncio.sleep(60 * 60)
|
||||||
|
|
||||||
|
|
||||||
|
async def collect_exchange_rates_data():
|
||||||
|
"""
|
||||||
|
Collect exchange rates data. Used for monitoring only.
|
||||||
|
"""
|
||||||
|
while settings.lnbits_running:
|
||||||
|
currency = settings.lnbits_default_accounting_currency or "USD"
|
||||||
|
max_history_size = settings.lnbits_exchange_history_size
|
||||||
|
sleep_time = settings.lnbits_exchange_history_refresh_interval_seconds
|
||||||
|
|
||||||
|
if sleep_time > 0:
|
||||||
|
try:
|
||||||
|
rates = await btc_rates(currency)
|
||||||
|
if rates:
|
||||||
|
rates_values = [r[1] for r in rates]
|
||||||
|
lnbits_rate = sum(rates_values) / len(rates_values)
|
||||||
|
rates.append(("LNbits", lnbits_rate))
|
||||||
|
settings.append_exchange_rate_datapoint(dict(rates), max_history_size)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
else:
|
||||||
|
sleep_time = 60
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
195
lnbits/core/templates/admin/_tab_exchange_providers.html
Normal file
195
lnbits/core/templates/admin/_tab_exchange_providers.html
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
<q-tab-panel name="exchange_providers">
|
||||||
|
<h6 class="q-my-none q-mb-sm">Exchange Providers</h6>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-sm-12">
|
||||||
|
<div class="q-pa-sm">
|
||||||
|
<canvas
|
||||||
|
style="
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 350px;
|
||||||
|
max-height: 50vh;
|
||||||
|
"
|
||||||
|
ref="exchangeRatesChart"
|
||||||
|
></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
|
||||||
|
type="number"
|
||||||
|
label="Refresh Interval (seconds)"
|
||||||
|
hint="How often should the exchange rates be fetched. Set to zero to disable."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_exchange_history_size"
|
||||||
|
type="number"
|
||||||
|
label="History Size"
|
||||||
|
hint="How many data points should be kept in memory."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>Refresh Interval</code> and <code> History Size </code>are for
|
||||||
|
historical purposes only.
|
||||||
|
</li>
|
||||||
|
<li>These two settings do not affect the live price computation.</li>
|
||||||
|
<li>
|
||||||
|
Chart currency:
|
||||||
|
<strong
|
||||||
|
><span
|
||||||
|
v-text="formData.lnbits_default_accounting_currency || 'USD'"
|
||||||
|
></span
|
||||||
|
></strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
@click="addExchangeProvider()"
|
||||||
|
label="Add Exchange Provider"
|
||||||
|
color="primary"
|
||||||
|
class="q-mb-md"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
@click="getDefaultSetting('lnbits_exchange_rate_providers')"
|
||||||
|
flat
|
||||||
|
:label="$t('reset_defaults')"
|
||||||
|
color="primary"
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
row-key="name"
|
||||||
|
:rows="formData.lnbits_exchange_rate_providers"
|
||||||
|
:columns="exchangesTable.columns"
|
||||||
|
v-model:pagination="exchangesTable.pagination"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width></q-th>
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
<span v-text="col.label"></span>
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
@click="removeExchangeProvider(props.row)"
|
||||||
|
round
|
||||||
|
icon="delete"
|
||||||
|
size="sm"
|
||||||
|
color="negative"
|
||||||
|
class="q-ml-xs"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="props.row.name"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</q-td>
|
||||||
|
<q-td full-width>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="props.row.api_url"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</q-input
|
||||||
|
></q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
v-model="props.row.path"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="props.row.exclude_to"
|
||||||
|
@update:model-value="touchSettings()"
|
||||||
|
multiple
|
||||||
|
:options="{{ currencies | safe }}"
|
||||||
|
></q-select>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
@click="showTickerConversionDialog(props.row)"
|
||||||
|
round
|
||||||
|
icon="add"
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
class="q-ml-xs"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
<q-chip
|
||||||
|
v-for="ticker, index in props.row.ticker_conversion"
|
||||||
|
:key="ticker"
|
||||||
|
removable
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
@remove="removeExchangeTickerConversion(props.row, ticker)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
:label="ticker"
|
||||||
|
class="ellipsis"
|
||||||
|
>
|
||||||
|
</q-chip>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>API URL</code> and <code>JSON Path</code> fields can use the
|
||||||
|
<code>{to}</code> and <code>{TO}</code> placeholders for the code of the
|
||||||
|
currency
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{TO}</code> is the uppercase code and <code>{to}</code> is the
|
||||||
|
lowercase code
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<q-separator class="q-ma-md"></q-separator>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-sm-12">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_exchange_rate_cache_seconds"
|
||||||
|
type="number"
|
||||||
|
label="Exchange rate cache (seconds)"
|
||||||
|
hint="For how many seconds should the exchange rate be cached."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8 col-sm-12"></div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
|
@ -63,7 +63,12 @@
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-splitter>
|
<q-splitter>
|
||||||
<template v-slot:before>
|
<template v-slot:before>
|
||||||
<q-tabs v-model="tab" vertical active-color="primary">
|
<q-tabs
|
||||||
|
@update:model-value="showExchangeProvidersTab"
|
||||||
|
v-model="tab"
|
||||||
|
vertical
|
||||||
|
active-color="primary"
|
||||||
|
>
|
||||||
<q-tab
|
<q-tab
|
||||||
name="funding"
|
name="funding"
|
||||||
icon="account_balance_wallet"
|
icon="account_balance_wallet"
|
||||||
|
@ -80,23 +85,6 @@
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('security')"></span></q-tooltip
|
><span v-text="$t('security')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
|
||||||
<q-tab
|
|
||||||
name="users"
|
|
||||||
icon="group"
|
|
||||||
:label="$q.screen.gt.sm ? $t('users') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('users')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="extensions"
|
|
||||||
icon="extension"
|
|
||||||
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
|
||||||
><span v-text="$t('extensions')"></span></q-tooltip
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
<q-tab
|
||||||
name="server"
|
name="server"
|
||||||
icon="price_change"
|
icon="price_change"
|
||||||
|
@ -105,6 +93,31 @@
|
||||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
><span v-text="$t('payments')"></span></q-tooltip
|
><span v-text="$t('payments')"></span></q-tooltip
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="exchange_providers"
|
||||||
|
icon="show_chart"
|
||||||
|
:label="$q.screen.gt.sm ? $t('exchanges') : null"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('exchanges')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="users"
|
||||||
|
icon="group"
|
||||||
|
:label="$q.screen.gt.sm ? $t('users') : null"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('users')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
|
|
||||||
|
<q-tab
|
||||||
|
name="extensions"
|
||||||
|
icon="extension"
|
||||||
|
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('extensions')"></span></q-tooltip
|
||||||
|
></q-tab>
|
||||||
|
|
||||||
<q-tab
|
<q-tab
|
||||||
name="notifications"
|
name="notifications"
|
||||||
icon="notifications"
|
icon="notifications"
|
||||||
|
@ -145,7 +158,8 @@
|
||||||
>
|
>
|
||||||
{% include "admin/_tab_funding.html" %} {% include
|
{% include "admin/_tab_funding.html" %} {% include
|
||||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %}
|
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %}
|
||||||
{% include "admin/_tab_extensions.html" %} {% include
|
{% include "admin/_tab_exchange_providers.html" %} {% include
|
||||||
|
"admin/_tab_extensions.html" %} {% include
|
||||||
"admin/_tab_notifications.html" %} {% include
|
"admin/_tab_notifications.html" %} {% include
|
||||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||||
%}{% include "admin/_tab_audit.html"%}
|
%}{% include "admin/_tab_audit.html"%}
|
||||||
|
@ -157,6 +171,47 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
||||||
|
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||||
|
<strong>Create Currecny Ticker Converter</strong>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 q-mb-md">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="exchangeData.convertFromTicker"
|
||||||
|
label="From Currency"
|
||||||
|
:options="{{ currencies | safe }}"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<q-input
|
||||||
|
v-model="exchangeData.convertToTicker"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label="New Ticker"
|
||||||
|
hint="This ticker will be used for the exchange API calls."
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
@click="addExchangeTickerConversion()"
|
||||||
|
label="Add Ticker Conversion"
|
||||||
|
color="primary"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-auto"
|
||||||
|
v-text="$t('close')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -17,7 +17,7 @@ from lnbits.core.services import (
|
||||||
from lnbits.core.tasks import api_invoice_listeners
|
from lnbits.core.tasks import api_invoice_listeners
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
|
|
||||||
from .. import core_app_extra
|
from .. import core_app_extra
|
||||||
|
@ -70,6 +70,16 @@ async def api_update_settings(data: UpdateSettings, user: User = Depends(check_a
|
||||||
return {"status": "Success"}
|
return {"status": "Success"}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
"/api/v1/settings/default",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def api_reset_settings(field_name: str):
|
||||||
|
default_settings = Settings()
|
||||||
|
return {"default_value": getattr(default_settings, field_name)}
|
||||||
|
|
||||||
|
|
||||||
@admin_router.delete(
|
@admin_router.delete(
|
||||||
"/api/v1/settings",
|
"/api/v1/settings",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
|
|
|
@ -225,6 +225,14 @@ async def api_perform_lnurlauth(
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get(
|
||||||
|
"/api/v1/rate/history",
|
||||||
|
dependencies=[Depends(require_invoice_key)],
|
||||||
|
)
|
||||||
|
async def api_exchange_rate_history() -> list[dict]:
|
||||||
|
return settings.lnbits_exchange_rate_history
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/api/v1/rate/{currency}")
|
@api_router.get("/api/v1/rate/{currency}")
|
||||||
async def api_check_fiat_rate(currency: str) -> dict[str, float]:
|
async def api_check_fiat_rate(currency: str) -> dict[str, float]:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import importlib.metadata
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from os import path
|
from os import path
|
||||||
|
@ -118,6 +119,26 @@ class RedirectPath(BaseModel):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeRateProvider(BaseModel):
|
||||||
|
name: str
|
||||||
|
api_url: str
|
||||||
|
path: str
|
||||||
|
exclude_to: list[str] = []
|
||||||
|
ticker_conversion: list[str] = []
|
||||||
|
|
||||||
|
def convert_ticker(self, currency: str) -> str:
|
||||||
|
if not self.ticker_conversion:
|
||||||
|
return currency
|
||||||
|
try:
|
||||||
|
for t in self.ticker_conversion:
|
||||||
|
_from, _to = t.split(":")
|
||||||
|
if _from == currency:
|
||||||
|
return _to
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(err)
|
||||||
|
return currency
|
||||||
|
|
||||||
|
|
||||||
class InstalledExtensionsSettings(LNbitsSettings):
|
class InstalledExtensionsSettings(LNbitsSettings):
|
||||||
# installed extensions that have been deactivated
|
# installed extensions that have been deactivated
|
||||||
lnbits_deactivated_extensions: set[str] = Field(default=[])
|
lnbits_deactivated_extensions: set[str] = Field(default=[])
|
||||||
|
@ -193,6 +214,20 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeHistorySettings(LNbitsSettings):
|
||||||
|
|
||||||
|
lnbits_exchange_rate_history: list[dict] = Field(default=[])
|
||||||
|
|
||||||
|
def append_exchange_rate_datapoint(self, rates: dict, max_size: int):
|
||||||
|
data = {
|
||||||
|
"timestamp": int(datetime.now(timezone.utc).timestamp()),
|
||||||
|
"rates": rates,
|
||||||
|
}
|
||||||
|
self.lnbits_exchange_rate_history.append(data)
|
||||||
|
if len(self.lnbits_exchange_rate_history) > max_size:
|
||||||
|
self.lnbits_exchange_rate_history.pop(0)
|
||||||
|
|
||||||
|
|
||||||
class ThemesSettings(LNbitsSettings):
|
class ThemesSettings(LNbitsSettings):
|
||||||
lnbits_site_title: str = Field(default="LNbits")
|
lnbits_site_title: str = Field(default="LNbits")
|
||||||
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
||||||
|
@ -250,6 +285,80 @@ class FeeSettings(LNbitsSettings):
|
||||||
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeProvidersSettings(LNbitsSettings):
|
||||||
|
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
|
||||||
|
lnbits_exchange_history_size: int = Field(default=60)
|
||||||
|
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
|
||||||
|
|
||||||
|
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
||||||
|
default=[
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Binance",
|
||||||
|
api_url="https://api.binance.com/api/v3/ticker/price?symbol=BTC{TO}",
|
||||||
|
path="$.price",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=["USD:USDT"],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Blockchain",
|
||||||
|
api_url="https://blockchain.info/frombtc?currency={TO}&value=100000000",
|
||||||
|
path="",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Exir",
|
||||||
|
api_url="https://api.exir.io/v1/ticker?symbol=btc-{to}",
|
||||||
|
path="$.last",
|
||||||
|
exclude_to=["czk", "eur"],
|
||||||
|
ticker_conversion=["USD:USDT"],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Bitfinex",
|
||||||
|
api_url="https://api.bitfinex.com/v1/pubticker/btc{to}",
|
||||||
|
path="$.last_price",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Bitstamp",
|
||||||
|
api_url="https://www.bitstamp.net/api/v2/ticker/btc{to}/",
|
||||||
|
path="$.last",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Coinbase",
|
||||||
|
api_url="https://api.coinbase.com/v2/exchange-rates?currency=BTC",
|
||||||
|
path="$.data.rates.{TO}",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="CoinMate",
|
||||||
|
api_url="https://coinmate.io/api/ticker?currencyPair=BTC_{TO}",
|
||||||
|
path="$.data.last",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=["USD:USDT"],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="Kraken",
|
||||||
|
api_url="https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
|
||||||
|
path="$.result.XXBTZ{TO}.c[0]",
|
||||||
|
exclude_to=["czk"],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
ExchangeRateProvider(
|
||||||
|
name="yadio",
|
||||||
|
api_url="https://api.yadio.io/exrates/BTC",
|
||||||
|
path="$.BTC.{TO}",
|
||||||
|
exclude_to=[],
|
||||||
|
ticker_conversion=[],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SecuritySettings(LNbitsSettings):
|
class SecuritySettings(LNbitsSettings):
|
||||||
lnbits_rate_limit_no: str = Field(default="200")
|
lnbits_rate_limit_no: str = Field(default="200")
|
||||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||||
|
@ -594,6 +703,7 @@ class EditableSettings(
|
||||||
ThemesSettings,
|
ThemesSettings,
|
||||||
OpsSettings,
|
OpsSettings,
|
||||||
FeeSettings,
|
FeeSettings,
|
||||||
|
ExchangeProvidersSettings,
|
||||||
SecuritySettings,
|
SecuritySettings,
|
||||||
FundingSourcesSettings,
|
FundingSourcesSettings,
|
||||||
LightningSettings,
|
LightningSettings,
|
||||||
|
@ -698,7 +808,7 @@ class SuperUserSettings(LNbitsSettings):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TransientSettings(InstalledExtensionsSettings):
|
class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
||||||
# Transient Settings:
|
# Transient Settings:
|
||||||
# - are initialized, updated and used at runtime
|
# - are initialized, updated and used at runtime
|
||||||
# - are not read from a file or from the `settings` table
|
# - are not read from a file or from the `settings` table
|
||||||
|
|
File diff suppressed because one or more lines are too long
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -108,6 +108,7 @@ window.localisation.en = {
|
||||||
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nThis service is in BETA. LNbits holds no responsibility for loss of access to funds.',
|
'You *must* save your login credentials to be able to access your wallet again. If you lose them, you will lose access to your wallet and funds.\n\nFind your login credentials on your account settings page.\n\nThis service is in BETA. LNbits holds no responsibility for loss of access to funds.',
|
||||||
no_transactions: 'No transactions made yet',
|
no_transactions: 'No transactions made yet',
|
||||||
manage: 'Manage',
|
manage: 'Manage',
|
||||||
|
exchanges: 'Exchanges',
|
||||||
extensions: 'Extensions',
|
extensions: 'Extensions',
|
||||||
no_extensions: "You don't have any extensions installed :(",
|
no_extensions: "You don't have any extensions installed :(",
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
|
|
|
@ -61,7 +61,61 @@ window.app = Vue.createApp({
|
||||||
'orange'
|
'orange'
|
||||||
],
|
],
|
||||||
tab: 'funding',
|
tab: 'funding',
|
||||||
needsRestart: false
|
needsRestart: false,
|
||||||
|
exchangesTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Exchange Name',
|
||||||
|
field: 'name',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'api_url',
|
||||||
|
align: 'left',
|
||||||
|
label: 'URL',
|
||||||
|
field: 'api_url',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'path',
|
||||||
|
align: 'left',
|
||||||
|
label: 'JSON Path',
|
||||||
|
field: 'path',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'exclude_to',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Exclude Currencies',
|
||||||
|
field: 'exclude_to',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ticker_conversion',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Ticker Conversion',
|
||||||
|
field: 'ticker_conversion',
|
||||||
|
sortable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'name',
|
||||||
|
rowsPerPage: 100,
|
||||||
|
page: 1,
|
||||||
|
rowsNumber: 100
|
||||||
|
},
|
||||||
|
search: null,
|
||||||
|
hideEmpty: true
|
||||||
|
},
|
||||||
|
exchangeData: {
|
||||||
|
selectedProvider: null,
|
||||||
|
showTickerConversion: false,
|
||||||
|
convertFromTicker: null,
|
||||||
|
convertToTicker: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -247,6 +301,57 @@ window.app = Vue.createApp({
|
||||||
this.formData.nostr_absolute_request_urls =
|
this.formData.nostr_absolute_request_urls =
|
||||||
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
||||||
},
|
},
|
||||||
|
addExchangeProvider() {
|
||||||
|
this.formData.lnbits_exchange_rate_providers = [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
api_url: '',
|
||||||
|
path: '',
|
||||||
|
exclude_to: []
|
||||||
|
},
|
||||||
|
...this.formData.lnbits_exchange_rate_providers
|
||||||
|
]
|
||||||
|
},
|
||||||
|
removeExchangeProvider(provider) {
|
||||||
|
this.formData.lnbits_exchange_rate_providers =
|
||||||
|
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
|
||||||
|
},
|
||||||
|
removeExchangeTickerConversion(provider, ticker) {
|
||||||
|
provider.ticker_conversion = provider.ticker_conversion.filter(
|
||||||
|
t => t !== ticker
|
||||||
|
)
|
||||||
|
this.touchSettings()
|
||||||
|
},
|
||||||
|
addExchangeTickerConversion() {
|
||||||
|
if (!this.exchangeData.selectedProvider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.exchangeData.selectedProvider.ticker_conversion.push(
|
||||||
|
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
|
||||||
|
)
|
||||||
|
this.touchSettings()
|
||||||
|
this.exchangeData.showTickerConversion = false
|
||||||
|
},
|
||||||
|
showTickerConversionDialog(provider) {
|
||||||
|
this.exchangeData.convertFromTicker = null
|
||||||
|
this.exchangeData.convertToTicker = null
|
||||||
|
this.exchangeData.selectedProvider = provider
|
||||||
|
this.exchangeData.showTickerConversion = true
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultSetting(fieldName) {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
`/admin/api/v1/settings/default?field_name=${fieldName}`
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
this.formData[fieldName] = response.data.default_value
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
restartServer() {
|
restartServer() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/restart/')
|
.request('GET', '/admin/api/v1/restart/')
|
||||||
|
@ -286,6 +391,16 @@ window.app = Vue.createApp({
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
|
getExchangeRateHistory() {
|
||||||
|
LNbits.api
|
||||||
|
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
||||||
|
.then(response => {
|
||||||
|
this.initExchangeChart(response.data)
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
getSettings() {
|
getSettings() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
@ -304,7 +419,8 @@ window.app = Vue.createApp({
|
||||||
updateSettings() {
|
updateSettings() {
|
||||||
const data = _.omit(this.formData, [
|
const data = _.omit(this.formData, [
|
||||||
'is_super_user',
|
'is_super_user',
|
||||||
'lnbits_allowed_funding_sources'
|
'lnbits_allowed_funding_sources',
|
||||||
|
'touch'
|
||||||
])
|
])
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
@ -350,6 +466,47 @@ window.app = Vue.createApp({
|
||||||
},
|
},
|
||||||
downloadBackup() {
|
downloadBackup() {
|
||||||
window.open('/admin/api/v1/backup', '_blank')
|
window.open('/admin/api/v1/backup', '_blank')
|
||||||
|
},
|
||||||
|
showExchangeProvidersTab(tabName) {
|
||||||
|
if (tabName === 'exchange_providers') {
|
||||||
|
this.getExchangeRateHistory()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
touchSettings() {
|
||||||
|
this.formData.touch = null
|
||||||
|
},
|
||||||
|
initExchangeChart(data) {
|
||||||
|
const xValues = data.map(d =>
|
||||||
|
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
||||||
|
)
|
||||||
|
const exchanges = [
|
||||||
|
...this.formData.lnbits_exchange_rate_providers,
|
||||||
|
{name: 'LNbits'}
|
||||||
|
]
|
||||||
|
const datasets = exchanges.map(exchange => ({
|
||||||
|
label: exchange.name,
|
||||||
|
data: data.map(d => d.rates[exchange.name]),
|
||||||
|
pointStyle: true,
|
||||||
|
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
|
||||||
|
tension: 0.4
|
||||||
|
}))
|
||||||
|
this.exchangeRatesChart = new Chart(
|
||||||
|
this.$refs.exchangeRatesChart.getContext('2d'),
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
labels: xValues,
|
||||||
|
datasets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import asyncio
|
from typing import Optional
|
||||||
from typing import Callable, NamedTuple
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import jsonpath_ng.ext as jpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import ExchangeRateProvider, settings
|
||||||
from lnbits.utils.cache import cache
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
currencies = {
|
currencies = {
|
||||||
|
@ -186,130 +186,70 @@ def allowed_currencies():
|
||||||
return list(currencies.keys())
|
return list(currencies.keys())
|
||||||
|
|
||||||
|
|
||||||
class Provider(NamedTuple):
|
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||||
name: str
|
def replacements(ticker: str):
|
||||||
domain: str
|
return {
|
||||||
api_url: str
|
"FROM": "BTC",
|
||||||
getter: Callable
|
"from": "btc",
|
||||||
exclude_to: list = []
|
"TO": ticker.upper(),
|
||||||
|
"to": ticker.lower(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fetch_price(
|
||||||
exchange_rate_providers = {
|
provider: ExchangeRateProvider,
|
||||||
# https://binance-docs.github.io/apidocs/spot/en/#symbol-price-ticker
|
) -> Optional[tuple[str, float]]:
|
||||||
"binance": Provider(
|
|
||||||
"Binance",
|
|
||||||
"binance.com",
|
|
||||||
"https://api.binance.com/api/v3/ticker/price?symbol={FROM}{TO}",
|
|
||||||
lambda data, replacements: data["price"],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"blockchain": Provider(
|
|
||||||
"Blockchain",
|
|
||||||
"blockchain.com",
|
|
||||||
"https://blockchain.info/tobtc?currency={TO}&value=1000000",
|
|
||||||
lambda data, replacements: 1000000 / data,
|
|
||||||
),
|
|
||||||
"exir": Provider(
|
|
||||||
"Exir",
|
|
||||||
"exir.io",
|
|
||||||
"https://api.exir.io/v1/ticker?symbol={from}-{to}",
|
|
||||||
lambda data, replacements: data["last"],
|
|
||||||
["czk", "eur"],
|
|
||||||
),
|
|
||||||
"bitfinex": Provider(
|
|
||||||
"Bitfinex",
|
|
||||||
"bitfinex.com",
|
|
||||||
"https://api.bitfinex.com/v1/pubticker/{from}{to}",
|
|
||||||
lambda data, replacements: data["last_price"],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"bitstamp": Provider(
|
|
||||||
"Bitstamp",
|
|
||||||
"bitstamp.net",
|
|
||||||
"https://www.bitstamp.net/api/v2/ticker/{from}{to}/",
|
|
||||||
lambda data, replacements: data["last"],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"coinbase": Provider(
|
|
||||||
"Coinbase",
|
|
||||||
"coinbase.com",
|
|
||||||
"https://api.coinbase.com/v2/exchange-rates?currency={FROM}",
|
|
||||||
lambda data, replacements: data["data"]["rates"][replacements["TO"]],
|
|
||||||
),
|
|
||||||
"coinmate": Provider(
|
|
||||||
"CoinMate",
|
|
||||||
"coinmate.io",
|
|
||||||
"https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}",
|
|
||||||
lambda data, replacements: data["data"]["last"],
|
|
||||||
),
|
|
||||||
"kraken": Provider(
|
|
||||||
"Kraken",
|
|
||||||
"kraken.com",
|
|
||||||
"https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
|
|
||||||
lambda data, replacements: data["result"]["XXBTZ" + replacements["TO"]]["c"][0],
|
|
||||||
["czk"],
|
|
||||||
),
|
|
||||||
"bitpay": Provider(
|
|
||||||
"BitPay",
|
|
||||||
"bitpay.com",
|
|
||||||
"https://bitpay.com/rates",
|
|
||||||
lambda data, replacements: next(
|
|
||||||
i["rate"] for i in data["data"] if i["code"] == replacements["TO"]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"yadio": Provider(
|
|
||||||
"yadio",
|
|
||||||
"yadio.io",
|
|
||||||
"https://api.yadio.io/exrates/{FROM}",
|
|
||||||
lambda data, replacements: data[replacements["FROM"]][replacements["TO"]],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def btc_price(currency: str) -> float:
|
|
||||||
replacements = {
|
|
||||||
"FROM": "BTC",
|
|
||||||
"from": "btc",
|
|
||||||
"TO": currency.upper(),
|
|
||||||
"to": currency.lower(),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def fetch_price(provider: Provider):
|
|
||||||
if currency.lower() in provider.exclude_to:
|
if currency.lower() in provider.exclude_to:
|
||||||
raise Exception(f"Provider {provider.name} does not support {currency}.")
|
raise Exception(f"Provider {provider.name} does not support {currency}.")
|
||||||
|
|
||||||
url = provider.api_url.format(**replacements)
|
ticker = provider.convert_ticker(currency)
|
||||||
|
url = provider.api_url.format(**replacements(ticker))
|
||||||
|
json_path = provider.path.format(**replacements(ticker))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
r = await client.get(url, timeout=0.5)
|
r = await client.get(url, timeout=0.5)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
if not provider.path:
|
||||||
|
return provider.name, float(r.text.replace(",", ""))
|
||||||
data = r.json()
|
data = r.json()
|
||||||
return float(provider.getter(data, replacements))
|
price_query = jpx.parse(json_path)
|
||||||
|
result = price_query.find(data)
|
||||||
|
return provider.name, float(result[0].value)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to fetch Bitcoin price "
|
f"Failed to fetch Bitcoin price "
|
||||||
f"for {currency} from {provider.name}: {e}"
|
f"for {currency} from {provider.name}: {e}"
|
||||||
)
|
)
|
||||||
raise
|
|
||||||
|
|
||||||
results = await asyncio.gather(
|
return None
|
||||||
*[fetch_price(provider) for provider in exchange_rate_providers.values()],
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
rates = [r for r in results if not isinstance(r, BaseException)]
|
|
||||||
|
|
||||||
|
# OK to be in squence: fetch_price times out after 0.5 seconds
|
||||||
|
results = [
|
||||||
|
await fetch_price(provider)
|
||||||
|
for provider in settings.lnbits_exchange_rate_providers
|
||||||
|
]
|
||||||
|
return [r for r in results if r is not None]
|
||||||
|
|
||||||
|
|
||||||
|
async def btc_price(currency: str) -> float:
|
||||||
|
rates = await btc_rates(currency)
|
||||||
if not rates:
|
if not rates:
|
||||||
return 9999999999
|
return 9999999999
|
||||||
elif len(rates) == 1:
|
elif len(rates) == 1:
|
||||||
logger.warning("Could only fetch one Bitcoin price.")
|
logger.warning("Could only fetch one Bitcoin price.")
|
||||||
|
|
||||||
return sum(rates) / len(rates)
|
rates_values = [r[1] for r in rates]
|
||||||
|
return sum(rates_values) / len(rates_values)
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
price = await cache.save_result(
|
price = await cache.save_result(
|
||||||
lambda: btc_price(currency), f"btc-price-{currency}"
|
lambda: btc_price(currency),
|
||||||
|
f"btc-price-{currency}",
|
||||||
|
settings.lnbits_exchange_rate_cache_seconds,
|
||||||
)
|
)
|
||||||
return float(100_000_000 / price)
|
return float(100_000_000 / price)
|
||||||
|
|
||||||
|
|
28
poetry.lock
generated
28
poetry.lock
generated
|
@ -1410,6 +1410,21 @@ files = [
|
||||||
{file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"},
|
{file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpath-ng"
|
||||||
|
version = "1.7.0"
|
||||||
|
description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
|
||||||
|
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
|
||||||
|
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
ply = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonschema"
|
name = "jsonschema"
|
||||||
version = "4.20.0"
|
version = "4.20.0"
|
||||||
|
@ -1891,6 +1906,17 @@ files = [
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ply"
|
||||||
|
version = "3.11"
|
||||||
|
description = "Python Lex & Yacc"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
|
||||||
|
{file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pre-commit"
|
name = "pre-commit"
|
||||||
version = "3.8.0"
|
version = "3.8.0"
|
||||||
|
@ -3187,4 +3213,4 @@ liquid = ["wallycore"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12 | ^3.11 | ^3.10 | ^3.9"
|
python-versions = "^3.12 | ^3.11 | ^3.10 | ^3.9"
|
||||||
content-hash = "7cba29a0ac6386ae77c238cb8fccb2c1801cb7d6a3c6a079fdb9625a953145d4"
|
content-hash = "e8077cd36e9647b0715d8aceeda4505eef0fe3d8eb0eb599025ea1584855c134"
|
||||||
|
|
|
@ -60,6 +60,7 @@ wallycore = {version = "1.3.0", optional = true}
|
||||||
# needed for breez funding source
|
# needed for breez funding source
|
||||||
breez-sdk = {version = "0.5.2", optional = true}
|
breez-sdk = {version = "0.5.2", optional = true}
|
||||||
|
|
||||||
|
jsonpath-ng = "^1.7.0"
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk"]
|
||||||
liquid = ["wallycore"]
|
liquid = ["wallycore"]
|
||||||
|
@ -140,6 +141,7 @@ module = [
|
||||||
"pywebpush.*",
|
"pywebpush.*",
|
||||||
"fastapi_sso.sso.*",
|
"fastapi_sso.sso.*",
|
||||||
"json5.*",
|
"json5.*",
|
||||||
|
"jsonpath_ng.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue