Compare commits
12
Commits
v1.2.0-rc1
...
v1.2.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff24847980 | ||
|
|
659ec31edd | ||
|
|
edb7d66efc | ||
|
|
efcc5e2148 | ||
|
|
55889abd88 | ||
|
|
063aa4fd06 | ||
|
|
7c0a955b30 | ||
|
|
b1a09af82c | ||
|
|
6d2f39d390 | ||
|
|
18496f0dd9 | ||
|
|
acf6c732ad | ||
|
|
b39bd257e0 |
+4
-2
@@ -101,8 +101,10 @@ ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
|
|||||||
|
|
||||||
# BoltzWallet
|
# BoltzWallet
|
||||||
BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
|
BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
|
||||||
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroon" # or HEXSTRING
|
# HEXSTRING instead of path also possible
|
||||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" # or HEXSTRING
|
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
||||||
|
# HEXSTRING instead of path also possible
|
||||||
|
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||||
BOLTZ_CLIENT_WALLET="lnbits"
|
BOLTZ_CLIENT_WALLET="lnbits"
|
||||||
|
|
||||||
# StrikeWallet
|
# StrikeWallet
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ inputs:
|
|||||||
description: "Python Version"
|
description: "Python Version"
|
||||||
required: true
|
required: true
|
||||||
default: "3.10"
|
default: "3.10"
|
||||||
poetry-version:
|
|
||||||
description: "Poetry Version"
|
|
||||||
default: "1.8.5"
|
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node Version"
|
description: "Node Version"
|
||||||
default: "20.x"
|
default: "20.x"
|
||||||
@@ -27,10 +24,8 @@ runs:
|
|||||||
# cache poetry install via pip
|
# cache poetry install via pip
|
||||||
cache: "pip"
|
cache: "pip"
|
||||||
|
|
||||||
- name: Set up Poetry ${{ inputs.poetry-version }}
|
- name: Set up Poetry
|
||||||
uses: abatilo/actions-poetry@v2
|
uses: abatilo/actions-poetry@v2
|
||||||
with:
|
|
||||||
poetry-version: ${{ inputs.poetry-version }}
|
|
||||||
|
|
||||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.10"]
|
||||||
poetry-version: ["1.5.1"]
|
|
||||||
uses: ./.github/workflows/jmeter.yml
|
uses: ./.github/workflows/jmeter.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
poetry-version: ${{ matrix.poetry-version }}
|
|
||||||
|
|||||||
@@ -51,3 +51,14 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|
||||||
|
- name: Build and push boltz
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.boltz
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
default: "3.10"
|
default: "3.10"
|
||||||
type: string
|
type: string
|
||||||
poetry-version:
|
|
||||||
description: "Poetry Version"
|
|
||||||
required: true
|
|
||||||
default: "1.5.1"
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
jmeter:
|
jmeter:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nix:
|
nix:
|
||||||
|
if: false # temporarly disable nix support until the `poetry2nix` issue is resolved
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ __pycache__
|
|||||||
.mypy_cache
|
.mypy_cache
|
||||||
.vscode
|
.vscode
|
||||||
*-lock.json
|
*-lock.json
|
||||||
|
.python-version
|
||||||
|
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
|||||||
+4
-3
@@ -4,13 +4,14 @@ RUN apt-get clean
|
|||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
|
||||||
|
|
||||||
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
ENV PATH="/root/.local/bin:$PATH"
|
ENV PATH="/root/.local/bin:$PATH"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Only copy the files required to install the dependencies
|
# Only copy the files required to install the dependencies
|
||||||
COPY pyproject.toml poetry.lock ./
|
COPY pyproject.toml poetry.lock ./
|
||||||
|
RUN touch README.md
|
||||||
|
|
||||||
RUN mkdir data
|
RUN mkdir data
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ ENV POETRY_NO_INTERACTION=1 \
|
|||||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||||
|
|
||||||
ARG POETRY_INSTALL_ARGS="--only main"
|
ARG POETRY_INSTALL_ARGS="--only main"
|
||||||
RUN poetry install ${POETRY_INSTALL_ARGS}
|
RUN poetry install --no-root ${POETRY_INSTALL_ARGS}
|
||||||
|
|
||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ RUN apt-get update && apt-get -y upgrade && \
|
|||||||
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
apt-get -y install postgresql-client-14 postgresql-client-common && \
|
||||||
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
apt-get clean all && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
ENV PATH="/root/.local/bin:$PATH"
|
ENV PATH="/root/.local/bin:$PATH"
|
||||||
|
|
||||||
ENV POETRY_NO_INTERACTION=1 \
|
ENV POETRY_NO_INTERACTION=1 \
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
FROM boltz/boltz-client:latest AS boltz
|
||||||
|
|
||||||
|
FROM lnbits/lnbits:latest
|
||||||
|
|
||||||
|
COPY --from=boltz /bin/boltzd /bin/boltzcli /usr/local/bin/
|
||||||
|
RUN ls -l /usr/local/bin/boltzd
|
||||||
|
RUN apt-get update && apt-get -y upgrade && \
|
||||||
|
apt-get install -y netcat-openbsd
|
||||||
|
|
||||||
|
# Reinstall dependencies just in case (needed for CMD usage)
|
||||||
|
ARG POETRY_INSTALL_ARGS="--only main"
|
||||||
|
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||||
|
|
||||||
|
# LNbits + boltzd configuration
|
||||||
|
ENV LNBITS_PORT="5000"
|
||||||
|
ENV LNBITS_HOST="0.0.0.0"
|
||||||
|
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
|
||||||
|
ENV FUNDING_SOURCE_MAX_RETRIES=10
|
||||||
|
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
|
||||||
|
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
|
||||||
|
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
|
||||||
|
ENV BOLTZ_CLIENT_WALLET="lnbits"
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Entrypoint to start boltzd and LNbits
|
||||||
|
COPY dockerboltz.sh /dockerboltz.sh
|
||||||
|
RUN chmod +x /dockerboltz.sh
|
||||||
|
|
||||||
|
CMD ["/dockerboltz.sh"]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
boltzd --standalone --referralId lnbits &
|
||||||
|
|
||||||
|
# Capture boltzd PID to monitor if needed
|
||||||
|
BOLTZ_PID=$!
|
||||||
|
|
||||||
|
# Wait for boltzd to start
|
||||||
|
for i in {1..10}; do
|
||||||
|
if nc -z localhost 9002; then
|
||||||
|
echo "boltzd is up!"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Waiting for boltzd to start..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Optional: check if still not up
|
||||||
|
if ! nc -z localhost 9002; then
|
||||||
|
echo "boltzd did not start successfully."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
|
||||||
|
exec poetry run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
|
||||||
@@ -10,11 +10,11 @@ if [ ! -d lnbits/data ]; then
|
|||||||
# Add the deadsnakes PPA repository non-interactively
|
# Add the deadsnakes PPA repository non-interactively
|
||||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||||
|
|
||||||
# Install Python 3.9 and distutils non-interactively
|
# Install Python 3.10 and distutils non-interactively
|
||||||
sudo apt install -y python3.9 python3.9-distutils
|
sudo apt install -y python3.10 python3.10-distutils
|
||||||
|
|
||||||
# Install Poetry
|
# Install Poetry
|
||||||
curl -sSL https://install.python-poetry.org | python3.9 -
|
curl -sSL https://install.python-poetry.org | python3.10 -
|
||||||
|
|
||||||
# Add Poetry to PATH for the current session
|
# Add Poetry to PATH for the current session
|
||||||
export PATH="/home/$USER/.local/bin:$PATH"
|
export PATH="/home/$USER/.local/bin:$PATH"
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
import bcrypt
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@@ -11,15 +9,6 @@ def _do_nothing(*_):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SolveBugBcryptWarning:
|
|
||||||
__version__: str = getattr(bcrypt, "__version__") # noqa
|
|
||||||
|
|
||||||
|
|
||||||
# fix annoying warning in the logs
|
|
||||||
setattr(bcrypt, "__about__", SolveBugBcryptWarning()) # noqa
|
|
||||||
|
|
||||||
|
|
||||||
class CoreAppExtra:
|
class CoreAppExtra:
|
||||||
register_new_ext_routes: Callable = _do_nothing
|
register_new_ext_routes: Callable = _do_nothing
|
||||||
register_new_ratelimiter: Callable
|
register_new_ratelimiter: Callable
|
||||||
|
|||||||
@@ -48,6 +48,17 @@
|
|||||||
class="cursor-pointer q-ml-sm"
|
class="cursor-pointer q-ml-sm"
|
||||||
@click="copyText(wallet.adminkey)"
|
@click="copyText(wallet.adminkey)"
|
||||||
></q-icon>
|
></q-icon>
|
||||||
|
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
|
||||||
|
<q-popup-proxy>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="wallet.adminkey"
|
||||||
|
:options="{ width: 250 }"
|
||||||
|
class="rounded-borders"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -70,6 +81,17 @@
|
|||||||
class="cursor-pointer q-ml-sm"
|
class="cursor-pointer q-ml-sm"
|
||||||
@click="copyText(wallet.inkey)"
|
@click="copyText(wallet.inkey)"
|
||||||
></q-icon>
|
></q-icon>
|
||||||
|
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
|
||||||
|
<q-popup-proxy>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="wallet.inkey"
|
||||||
|
:options="{ width: 250 }"
|
||||||
|
class="rounded-borders"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|||||||
@@ -548,6 +548,12 @@
|
|||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<div class="row q-mb-md q-mt-md">
|
||||||
|
<q-btn @click="updateAccount" unelevated color="primary">
|
||||||
|
<span v-text="$t('update_account')"></span>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="api_acls">
|
<q-tab-panel name="api_acls">
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
|
|||||||
@@ -232,15 +232,57 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-select
|
<q-btn
|
||||||
v-else-if="['status'].includes(col.name)"
|
v-else-if="['status'].includes(col.name)"
|
||||||
v-model="searchData[col.name]"
|
flat
|
||||||
:options="searchOptions[col.name]"
|
dense
|
||||||
@update:model-value="searchPaymentsBy()"
|
:label="$q.screen.gt.md ? 'Status' : null"
|
||||||
:label="col.label"
|
icon="filter_alt"
|
||||||
clearable
|
color="grey"
|
||||||
style="width: 100px"
|
class="text-capitalize"
|
||||||
></q-select>
|
>
|
||||||
|
<q-menu anchor="top right" self="top start">
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="statusFilters.success"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Success Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="statusFilters.pending"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Pending Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="statusFilters.failed"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Failed Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="statusFilters.incoming"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Incoming Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="statusFilters.outgoing"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Outgoing Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
</q-menu>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('filter_payments')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
<q-select
|
<q-select
|
||||||
v-else-if="['tag'].includes(col.name)"
|
v-else-if="['tag'].includes(col.name)"
|
||||||
v-model="searchData[col.name]"
|
v-model="searchData[col.name]"
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
:label="$t('set_password')"
|
:label="$t('set_password')"
|
||||||
v-model="activeUser.setPassword"
|
v-model="activeUser.setPassword"
|
||||||
>
|
>
|
||||||
<q-tooltip>Toggle Admin</q-tooltip>
|
<q-tooltip v-text="$t('set_password_tooltip')"></q-tooltip>
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
@@ -105,11 +105,12 @@
|
|||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="activeUser.data.pubkey"
|
v-model="activeUser.data.pubkey"
|
||||||
:label="$t('pubkey')"
|
:label="'Nostr '+ $t('pubkey')"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
|
<q-tooltip v-text="$t('nostr_pubkey_tooltip')"></q-tooltip>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="activeUser.data.email"
|
v-model="activeUser.data.email"
|
||||||
|
|||||||
@@ -130,14 +130,20 @@ async def api_lnurlscan(
|
|||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||||
check_callback_url(url)
|
check_callback_url(url)
|
||||||
r = await client.get(url, timeout=5)
|
try:
|
||||||
r.raise_for_status()
|
r = await client.get(url, timeout=5)
|
||||||
if r.is_error:
|
r.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
if exc.response.status_code == 404:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Not found") from exc
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail={"domain": domain, "message": "failed to get parameters"},
|
detail={
|
||||||
)
|
"domain": domain,
|
||||||
|
"message": "failed to get parameters",
|
||||||
|
},
|
||||||
|
) from exc
|
||||||
try:
|
try:
|
||||||
data = json.loads(r.text)
|
data = json.loads(r.text)
|
||||||
except json.decoder.JSONDecodeError as exc:
|
except json.decoder.JSONDecodeError as exc:
|
||||||
|
|||||||
@@ -553,7 +553,9 @@ class BoltzFundingSource(LNbitsSettings):
|
|||||||
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
||||||
boltz_client_macaroon: str | None = Field(default=None)
|
boltz_client_macaroon: str | None = Field(default=None)
|
||||||
boltz_client_wallet: str | None = Field(default="lnbits")
|
boltz_client_wallet: str | None = Field(default="lnbits")
|
||||||
|
boltz_client_password: str = Field(default="")
|
||||||
boltz_client_cert: str | None = Field(default=None)
|
boltz_client_cert: str | None = Field(default=None)
|
||||||
|
boltz_mnemonic: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class StrikeFundingSource(LNbitsSettings):
|
class StrikeFundingSource(LNbitsSettings):
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -328,7 +328,9 @@ window.localisation.en = {
|
|||||||
change_password: 'Change Password',
|
change_password: 'Change Password',
|
||||||
update_credentials: 'Update Credentials',
|
update_credentials: 'Update Credentials',
|
||||||
update_pubkey: 'Update Public Key',
|
update_pubkey: 'Update Public Key',
|
||||||
|
nostr_pubkey_tooltip: "Enter this user's Nostr public key (hex value)",
|
||||||
set_password: 'Set Password',
|
set_password: 'Set Password',
|
||||||
|
set_password_tooltip: 'Set a password for this user',
|
||||||
invalid_password: 'Password must have at least 8 characters',
|
invalid_password: 'Password must have at least 8 characters',
|
||||||
invalid_password_repeat: 'Passwords do not match',
|
invalid_password_repeat: 'Passwords do not match',
|
||||||
reset_key_generated: 'A reset key has been generated.',
|
reset_key_generated: 'A reset key has been generated.',
|
||||||
|
|||||||
@@ -517,7 +517,9 @@ window.windowMixin = {
|
|||||||
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
|
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
|
||||||
},
|
},
|
||||||
goToWallets() {
|
goToWallets() {
|
||||||
window.location = '/wallets'
|
this.$router.push({
|
||||||
|
path: '/wallets'
|
||||||
|
})
|
||||||
},
|
},
|
||||||
submitAddWallet() {
|
submitAddWallet() {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
fundingSource => fundingSource[0] === item
|
fundingSource => fundingSource[0] === item
|
||||||
)
|
)
|
||||||
return fundingSource ? fundingSource[1] : item
|
return fundingSource ? fundingSource[1] : item
|
||||||
|
},
|
||||||
|
showQRValue(value) {
|
||||||
|
this.qrValue = value
|
||||||
|
this.showQRDialog = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -17,7 +21,8 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
const tmpObj = {}
|
const tmpObj = {}
|
||||||
if (obj !== null) {
|
if (obj !== null) {
|
||||||
for (let [k, v] of Object.entries(obj)) {
|
for (let [k, v] of Object.entries(obj)) {
|
||||||
tmpObj[k] = {label: v, value: null}
|
tmpObj[k] =
|
||||||
|
typeof v === 'string' ? {label: v, value: null} : v || {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tmp.push([key, tmpObj])
|
tmp.push([key, tmpObj])
|
||||||
@@ -31,6 +36,8 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hideInput: true,
|
hideInput: true,
|
||||||
|
showQRDialog: false,
|
||||||
|
qrValue: '',
|
||||||
rawFundingSources: [
|
rawFundingSources: [
|
||||||
['VoidWallet', 'Void Wallet', null],
|
['VoidWallet', 'Void Wallet', null],
|
||||||
[
|
[
|
||||||
@@ -138,7 +145,14 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
boltz_client_endpoint: 'Endpoint',
|
boltz_client_endpoint: 'Endpoint',
|
||||||
boltz_client_macaroon: 'Admin Macaroon path or hex',
|
boltz_client_macaroon: 'Admin Macaroon path or hex',
|
||||||
boltz_client_cert: 'Certificate path or hex',
|
boltz_client_cert: 'Certificate path or hex',
|
||||||
boltz_client_wallet: 'Wallet Name'
|
boltz_client_wallet: 'Wallet Name',
|
||||||
|
boltz_client_password: 'Wallet Password (can be empty)',
|
||||||
|
boltz_mnemonic: {
|
||||||
|
label: 'Liquid mnemonic (copy into greenwallet)',
|
||||||
|
readonly: true,
|
||||||
|
copy: true,
|
||||||
|
qrcode: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ window.PaymentsPageLogic = {
|
|||||||
searchData: {
|
searchData: {
|
||||||
wallet_id: null,
|
wallet_id: null,
|
||||||
payment_hash: null,
|
payment_hash: null,
|
||||||
status: null,
|
|
||||||
memo: null
|
memo: null
|
||||||
},
|
},
|
||||||
|
statusFilters: {
|
||||||
|
success: true,
|
||||||
|
pending: true,
|
||||||
|
failed: true,
|
||||||
|
incoming: true,
|
||||||
|
outgoing: true
|
||||||
|
},
|
||||||
chartData: {
|
chartData: {
|
||||||
showPaymentStatus: true,
|
showPaymentStatus: true,
|
||||||
showPaymentTags: true,
|
showPaymentTags: true,
|
||||||
@@ -131,6 +137,7 @@ window.PaymentsPageLogic = {
|
|||||||
if (this.searchDate.to) {
|
if (this.searchDate.to) {
|
||||||
filter['time[le]'] = this.searchDate.to + 'T23:59:59'
|
filter['time[le]'] = this.searchDate.to + 'T23:59:59'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.paymentsTable.filter = filter
|
this.paymentsTable.filter = filter
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -198,7 +205,38 @@ window.PaymentsPageLogic = {
|
|||||||
|
|
||||||
this.fetchPayments()
|
this.fetchPayments()
|
||||||
},
|
},
|
||||||
|
handleFilterChanged() {
|
||||||
|
const {success, pending, failed, incoming, outgoing} = this.statusFilters
|
||||||
|
|
||||||
|
delete this.searchData['status[ne]']
|
||||||
|
delete this.searchData['status[eq]']
|
||||||
|
if (success && pending && failed) {
|
||||||
|
// do nothing, all statuses are selected
|
||||||
|
} else if (success && pending) {
|
||||||
|
this.searchData['status[ne]'] = 'failed'
|
||||||
|
} else if (success && failed) {
|
||||||
|
this.searchData['status[ne]'] = 'pending'
|
||||||
|
} else if (failed && pending) {
|
||||||
|
this.searchData['status[ne]'] = 'success'
|
||||||
|
} else if (success) {
|
||||||
|
this.searchData['status[eq]'] = 'success'
|
||||||
|
} else if (pending) {
|
||||||
|
this.searchData['status[eq]'] = 'pending'
|
||||||
|
} else if (failed) {
|
||||||
|
this.searchData['status[eq]'] = 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.searchData['amount[ge]']
|
||||||
|
delete this.searchData['amount[le]']
|
||||||
|
if (incoming && outgoing) {
|
||||||
|
// do nothing
|
||||||
|
} else if (incoming) {
|
||||||
|
this.searchData['amount[ge]'] = '0'
|
||||||
|
} else if (outgoing) {
|
||||||
|
this.searchData['amount[le]'] = '0'
|
||||||
|
}
|
||||||
|
this.fetchPayments()
|
||||||
|
},
|
||||||
showDetailsToggle(payment) {
|
showDetailsToggle(payment) {
|
||||||
this.paymentDetails = payment
|
this.paymentDetails = payment
|
||||||
return (this.showDetails = !this.showDetails)
|
return (this.showDetails = !this.showDetails)
|
||||||
|
|||||||
@@ -338,9 +338,6 @@ window.WalletPageLogic = {
|
|||||||
'/api/v1/lnurlscan/' + this.parse.data.request,
|
'/api/v1/lnurlscan/' + this.parse.data.request,
|
||||||
this.g.wallet.adminkey
|
this.g.wallet.adminkey
|
||||||
)
|
)
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
|
|
||||||
@@ -378,6 +375,9 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
decodeQR(res) {
|
decodeQR(res) {
|
||||||
this.parse.data.request = res[0].rawValue
|
this.parse.data.request = res[0].rawValue
|
||||||
|
|||||||
@@ -1141,12 +1141,41 @@
|
|||||||
:type="hideInput ? 'password' : 'text'"
|
:type="hideInput ? 'password' : 'text'"
|
||||||
:label="prop.label"
|
:label="prop.label"
|
||||||
:hint="prop.hint"
|
:hint="prop.hint"
|
||||||
|
:readonly="prop.readonly || false"
|
||||||
>
|
>
|
||||||
|
<q-btn
|
||||||
|
v-if="prop.copy"
|
||||||
|
@click="copyText(formData[key])"
|
||||||
|
icon="content_copy"
|
||||||
|
class="cursor-pointer"
|
||||||
|
color="grey"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="prop.qrcode"
|
||||||
|
@click="showQRValue(formData[key])"
|
||||||
|
icon="qr_code"
|
||||||
|
class="cursor-pointer"
|
||||||
|
color="grey"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
></q-btn>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
<q-dialog v-model="showQRDialog">
|
||||||
|
<q-card class="q-pa-md">
|
||||||
|
<q-card-section>
|
||||||
|
<lnbits-qrcode :value="qrValue" :size="200"></lnbits-qrcode>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Close" v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import statistics
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -187,6 +188,54 @@ def allowed_currencies() -> list[str]:
|
|||||||
return list(currencies.keys())
|
return list(currencies.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def apply_trimmed_mean_filter(
|
||||||
|
rates: list[tuple[str, float]], threshold_percentage: float = 0.01
|
||||||
|
) -> list[tuple[str, float]]:
|
||||||
|
"""
|
||||||
|
Apply trimmed mean filtering to remove outliers from exchange rates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rates: List of (provider_name, rate_value) tuples
|
||||||
|
threshold_percentage: Percentage threshold for outlier removal (default 1%)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of rates with outliers removed
|
||||||
|
"""
|
||||||
|
if len(rates) < 3:
|
||||||
|
# Need at least 3 rates to apply filtering
|
||||||
|
return rates
|
||||||
|
|
||||||
|
rates_values = [r[1] for r in rates]
|
||||||
|
median_value = statistics.median(rates_values)
|
||||||
|
|
||||||
|
# Filter out values that are more than threshold_percentage away from median
|
||||||
|
filtered_rates = []
|
||||||
|
for rate in rates:
|
||||||
|
provider_name, value = rate
|
||||||
|
deviation = abs(value - median_value) / median_value
|
||||||
|
if deviation <= threshold_percentage:
|
||||||
|
logger.debug(
|
||||||
|
f"Keeping {provider_name}: {value} (deviation: {deviation:.4f})"
|
||||||
|
)
|
||||||
|
filtered_rates.append(rate)
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"Removing outlier {provider_name}: {value} "
|
||||||
|
f"(deviation: {deviation:.4f})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we still have at least 2 rates after filtering, use them
|
||||||
|
if len(filtered_rates) >= 2:
|
||||||
|
logger.debug(f"Filtered rates: {filtered_rates}")
|
||||||
|
return filtered_rates
|
||||||
|
else:
|
||||||
|
# Fall back to median if filtering removed too many values
|
||||||
|
logger.debug("Filtering removed too many values, using median instead")
|
||||||
|
# Find the rate closest to median
|
||||||
|
closest_rate = min(rates, key=lambda x: abs(x[1] - median_value))
|
||||||
|
return [closest_rate]
|
||||||
|
|
||||||
|
|
||||||
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||||
if currency.upper() not in allowed_currencies():
|
if currency.upper() not in allowed_currencies():
|
||||||
raise ValueError(f"Currency '{currency}' not allowed.")
|
raise ValueError(f"Currency '{currency}' not allowed.")
|
||||||
@@ -236,7 +285,9 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
]
|
]
|
||||||
results = await asyncio.gather(*calls)
|
results = await asyncio.gather(*calls)
|
||||||
|
|
||||||
return [r for r in results if r is not None]
|
all_rates = [r for r in results if r is not None]
|
||||||
|
|
||||||
|
return apply_trimmed_mean_filter(all_rates)
|
||||||
|
|
||||||
|
|
||||||
async def btc_price(currency: str) -> float:
|
async def btc_price(currency: str) -> float:
|
||||||
|
|||||||
+50
-3
@@ -63,6 +63,27 @@ class BoltzWallet(Wallet):
|
|||||||
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
|
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
|
||||||
self.wallet_id: int = 0
|
self.wallet_id: int = 0
|
||||||
|
|
||||||
|
# Auto-create wallet if running in Docker mode
|
||||||
|
async def _init_boltz_wallet():
|
||||||
|
try:
|
||||||
|
wallet_name = settings.boltz_client_wallet or "lnbits"
|
||||||
|
mnemonic = await self._fetch_wallet(wallet_name)
|
||||||
|
if mnemonic:
|
||||||
|
logger.info(
|
||||||
|
"✅ Mnemonic found for Boltz wallet, saving to settings"
|
||||||
|
)
|
||||||
|
settings.boltz_mnemonic = mnemonic
|
||||||
|
|
||||||
|
from lnbits.core.crud.settings import set_settings_field
|
||||||
|
|
||||||
|
await set_settings_field("boltz_mnemonic", mnemonic)
|
||||||
|
else:
|
||||||
|
logger.warning("⚠️ No mnemonic returned from Boltz")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Failed to auto-create Boltz wallet: {e}")
|
||||||
|
|
||||||
|
self._init_wallet_task = asyncio.create_task(_init_boltz_wallet())
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
try:
|
try:
|
||||||
request = boltzrpc_pb2.GetWalletRequest(name=settings.boltz_client_wallet)
|
request = boltzrpc_pb2.GetWalletRequest(name=settings.boltz_client_wallet)
|
||||||
@@ -70,9 +91,10 @@ class BoltzWallet(Wallet):
|
|||||||
request, metadata=self.metadata
|
request, metadata=self.metadata
|
||||||
)
|
)
|
||||||
except AioRpcError as exc:
|
except AioRpcError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
return StatusResponse(
|
return StatusResponse(
|
||||||
exc.details()
|
"make sure you have macaroon and certificate configured,"
|
||||||
+ " make sure you have macaroon and certificate configured, unless your client runs without", # noqa: E501
|
"unless your client runs without",
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,7 +215,10 @@ class BoltzWallet(Wallet):
|
|||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
|
response: boltzrpc_pb2.GetSwapInfoResponse = await self.rpc.GetSwapInfo(
|
||||||
boltzrpc_pb2.GetSwapInfoRequest(id=checking_id), metadata=self.metadata
|
boltzrpc_pb2.GetSwapInfoRequest(
|
||||||
|
payment_hash=bytes.fromhex(checking_id)
|
||||||
|
),
|
||||||
|
metadata=self.metadata,
|
||||||
)
|
)
|
||||||
swap = response.swap
|
swap = response.swap
|
||||||
except AioRpcError:
|
except AioRpcError:
|
||||||
@@ -225,3 +250,25 @@ class BoltzWallet(Wallet):
|
|||||||
" 5 seconds"
|
" 5 seconds"
|
||||||
)
|
)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
async def _fetch_wallet(self, wallet_name: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
request = boltzrpc_pb2.GetWalletRequest(name=wallet_name)
|
||||||
|
response = await self.rpc.GetWallet(request, metadata=self.metadata)
|
||||||
|
logger.info(f"Wallet '{wallet_name}' already exists with ID {response.id}")
|
||||||
|
return settings.boltz_mnemonic
|
||||||
|
except AioRpcError as exc:
|
||||||
|
details = exc.details() or "unknown error"
|
||||||
|
if exc.code() != grpc.StatusCode.NOT_FOUND:
|
||||||
|
logger.error(f"Error checking wallet existence: {details}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info(f"Creating new wallet '{wallet_name}'")
|
||||||
|
params = boltzrpc_pb2.WalletParams(
|
||||||
|
name=wallet_name,
|
||||||
|
currency=boltzrpc_pb2.LBTC,
|
||||||
|
password=settings.boltz_client_password,
|
||||||
|
)
|
||||||
|
create_request = boltzrpc_pb2.CreateWalletRequest(params=params)
|
||||||
|
response = await self.rpc.CreateWallet(create_request, metadata=self.metadata)
|
||||||
|
return response.mnemonic
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
package boltzrpc;
|
package boltzrpc;
|
||||||
option go_package = "github.com/BoltzExchange/boltz-client/boltzrpc";
|
option go_package = "github.com/BoltzExchange/boltz-client/v2/pkg/boltzrpc";
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
service Boltz {
|
service Boltz {
|
||||||
@@ -122,6 +122,23 @@ service Boltz {
|
|||||||
*/
|
*/
|
||||||
rpc GetWallet (GetWalletRequest) returns (Wallet);
|
rpc GetWallet (GetWalletRequest) returns (Wallet);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calculates the fee for an equivalent `WalletSend` request.
|
||||||
|
If `address` is left empty, a dummy swap address will be used, allowing for a fee estimation of a swap lockup transaction.
|
||||||
|
*/
|
||||||
|
rpc GetWalletSendFee (WalletSendRequest) returns (WalletSendFee);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns recent transactions from a wallet.
|
||||||
|
*/
|
||||||
|
rpc ListWalletTransactions (ListWalletTransactionsRequest) returns (ListWalletTransactionsResponse);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Increase the fee of a transaction using RBF.
|
||||||
|
The transaction has to belong to one of the clients wallets.
|
||||||
|
*/
|
||||||
|
rpc BumpTransaction (BumpTransactionRequest) returns (BumpTransactionResponse);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Returns the credentials of a wallet. The password will be required if the wallet is encrypted.
|
Returns the credentials of a wallet. The password will be required if the wallet is encrypted.
|
||||||
*/
|
*/
|
||||||
@@ -132,6 +149,16 @@ service Boltz {
|
|||||||
*/
|
*/
|
||||||
rpc RemoveWallet (RemoveWalletRequest) returns (RemoveWalletResponse);
|
rpc RemoveWallet (RemoveWalletRequest) returns (RemoveWalletResponse);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Send coins from a wallet. Only the confirmed balance can be spent.
|
||||||
|
*/
|
||||||
|
rpc WalletSend (WalletSendRequest) returns (WalletSendResponse);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get a new address of the wallet.
|
||||||
|
*/
|
||||||
|
rpc WalletReceive (WalletReceiveRequest) returns (WalletReceiveResponse);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Gracefully stops the daemon.
|
Gracefully stops the daemon.
|
||||||
*/
|
*/
|
||||||
@@ -265,11 +292,12 @@ message SwapInfo {
|
|||||||
repeated ChannelId chan_ids = 14;
|
repeated ChannelId chan_ids = 14;
|
||||||
optional string blinding_key = 15;
|
optional string blinding_key = 15;
|
||||||
int64 created_at = 16;
|
int64 created_at = 16;
|
||||||
optional uint64 service_fee = 17;
|
optional int64 service_fee = 17;
|
||||||
optional uint64 onchain_fee = 18;
|
optional uint64 onchain_fee = 18;
|
||||||
// internal wallet which was used to pay the swap
|
// internal wallet which was used to pay the swap
|
||||||
optional uint64 wallet_id = 20;
|
optional uint64 wallet_id = 20;
|
||||||
uint64 tenant_id = 21;
|
uint64 tenant_id = 21;
|
||||||
|
bool is_auto = 23;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SwapType {
|
enum SwapType {
|
||||||
@@ -322,7 +350,8 @@ message ReverseSwapInfo {
|
|||||||
string redeem_script = 7;
|
string redeem_script = 7;
|
||||||
string invoice = 8;
|
string invoice = 8;
|
||||||
string claim_address = 9;
|
string claim_address = 9;
|
||||||
int64 onchain_amount = 10;
|
uint64 onchain_amount = 10;
|
||||||
|
uint64 invoice_amount = 25;
|
||||||
uint32 timeout_block_height = 11;
|
uint32 timeout_block_height = 11;
|
||||||
string lockup_transaction_id = 12;
|
string lockup_transaction_id = 12;
|
||||||
string claim_transaction_id = 13;
|
string claim_transaction_id = 13;
|
||||||
@@ -331,11 +360,12 @@ message ReverseSwapInfo {
|
|||||||
optional string blinding_key = 16;
|
optional string blinding_key = 16;
|
||||||
int64 created_at = 17;
|
int64 created_at = 17;
|
||||||
optional int64 paid_at = 23; // the time when the invoice was paid
|
optional int64 paid_at = 23; // the time when the invoice was paid
|
||||||
optional uint64 service_fee = 18;
|
optional int64 service_fee = 18;
|
||||||
optional uint64 onchain_fee = 19;
|
optional uint64 onchain_fee = 19;
|
||||||
optional uint64 routing_fee_msat = 20;
|
optional uint64 routing_fee_msat = 20;
|
||||||
bool external_pay = 21;
|
bool external_pay = 21;
|
||||||
uint64 tenant_id = 22;
|
uint64 tenant_id = 22;
|
||||||
|
bool is_auto = 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BlockHeights {
|
message BlockHeights {
|
||||||
@@ -406,17 +436,44 @@ enum IncludeSwaps {
|
|||||||
AUTO = 2;
|
AUTO = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AnySwapInfo {
|
||||||
|
string id = 1;
|
||||||
|
SwapType type = 2;
|
||||||
|
Pair pair = 3;
|
||||||
|
SwapState state = 4;
|
||||||
|
optional string error = 5;
|
||||||
|
string status = 6;
|
||||||
|
// The expected amount to be sent to the lockup address for submarine and chain swaps and
|
||||||
|
// the invoice amount for reverse swaps.
|
||||||
|
uint64 from_amount = 7;
|
||||||
|
// `from_amount` minus the service and network fee.
|
||||||
|
uint64 to_amount = 13;
|
||||||
|
int64 created_at = 8;
|
||||||
|
optional int64 service_fee = 9;
|
||||||
|
// inclues the routing fee for reverse swaps
|
||||||
|
optional uint64 onchain_fee = 10;
|
||||||
|
bool is_auto = 11;
|
||||||
|
uint64 tenant_id = 12;
|
||||||
|
}
|
||||||
|
|
||||||
message ListSwapsRequest {
|
message ListSwapsRequest {
|
||||||
optional Currency from = 1;
|
optional Currency from = 1;
|
||||||
optional Currency to = 2;
|
optional Currency to = 2;
|
||||||
optional SwapState state = 4;
|
optional SwapState state = 4;
|
||||||
IncludeSwaps include = 5;
|
IncludeSwaps include = 5;
|
||||||
|
optional uint64 limit = 6;
|
||||||
|
optional uint64 offset = 7;
|
||||||
|
// wether to return swaps in the shared `all_swaps` list or in the detailed lists.
|
||||||
|
// the `limit` and `offset` are only considered when `unify` is true.
|
||||||
|
optional bool unify = 8;
|
||||||
}
|
}
|
||||||
message ListSwapsResponse {
|
message ListSwapsResponse {
|
||||||
repeated SwapInfo swaps = 1;
|
repeated SwapInfo swaps = 1;
|
||||||
repeated CombinedChannelSwapInfo channel_creations = 2;
|
repeated CombinedChannelSwapInfo channel_creations = 2 [deprecated = true];
|
||||||
repeated ReverseSwapInfo reverse_swaps = 3;
|
repeated ReverseSwapInfo reverse_swaps = 3;
|
||||||
repeated ChainSwapInfo chain_swaps = 4;
|
repeated ChainSwapInfo chain_swaps = 4;
|
||||||
|
// populated when `unify` is set to true in the request
|
||||||
|
repeated AnySwapInfo all_swaps = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetStatsRequest {
|
message GetStatsRequest {
|
||||||
@@ -448,7 +505,12 @@ message ClaimSwapsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message GetSwapInfoRequest {
|
message GetSwapInfoRequest {
|
||||||
string id = 1;
|
string id = 1 [deprecated = true];
|
||||||
|
oneof identifier {
|
||||||
|
string swap_id = 2;
|
||||||
|
// Only implemented for submarine swaps
|
||||||
|
bytes payment_hash = 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
message GetSwapInfoResponse {
|
message GetSwapInfoResponse {
|
||||||
SwapInfo swap = 1;
|
SwapInfo swap = 1;
|
||||||
@@ -471,22 +533,29 @@ message DepositResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message CreateSwapRequest {
|
message CreateSwapRequest {
|
||||||
|
// amount of sats to be received on lightning.
|
||||||
|
// related: `invoice` field
|
||||||
uint64 amount = 1;
|
uint64 amount = 1;
|
||||||
Pair pair = 2;
|
Pair pair = 2;
|
||||||
// not yet supported
|
|
||||||
// repeated string chan_ids = 3;
|
|
||||||
|
|
||||||
// the daemon will pay the swap using the onchain wallet specified in the `wallet` field or any wallet otherwise.
|
// the daemon will pay the swap using the onchain wallet specified in the `wallet` field
|
||||||
|
// or the first internal wallet with the correct currency otherwise.
|
||||||
bool send_from_internal = 4;
|
bool send_from_internal = 4;
|
||||||
// address where the coins should go if the swap fails. Refunds will go to any of the daemons wallets otherwise.
|
// address where the coins should go if the swap fails. Refunds will go to any of the daemons wallets otherwise.
|
||||||
optional string refund_address = 5;
|
optional string refund_address = 5;
|
||||||
// wallet to pay swap from. only used if `send_from_internal` is set to true
|
// wallet to pay swap from. only used if `send_from_internal` is set to true
|
||||||
optional uint64 wallet_id = 6;
|
optional uint64 wallet_id = 6;
|
||||||
// invoice to use for the swap. if not set, the daemon will get a new invoice from the lightning node
|
// bolt11 invoice, lnurl, or lnaddress to use for the swap.
|
||||||
|
// required in standalone mode.
|
||||||
|
// when connected to a lightning node, a new invoice for `amount` sats will be fetched
|
||||||
|
// the `amount` field has to be populated in case of a lnurl and lnaddress
|
||||||
optional string invoice = 7;
|
optional string invoice = 7;
|
||||||
// Boltz does not accept 0-conf for Liquid transactions with a fee of 0.01 sat/vByte;
|
optional bool zero_conf = 8 [deprecated = true];
|
||||||
// when `zero_conf` is enabled, a fee of 0.1 sat/vByte will be used for Liquid lockup transactions
|
// Fee rate to use when sending from internal wallet
|
||||||
optional bool zero_conf = 8;
|
optional double sat_per_vbyte = 9;
|
||||||
|
// Rates to accept for the swap. Queries latest from boltz otherwise
|
||||||
|
// The recommended way to use this is to pass a user approved value from a previous `GetPairInfo` call
|
||||||
|
optional PairInfo accepted_pair = 10;
|
||||||
}
|
}
|
||||||
message CreateSwapResponse {
|
message CreateSwapResponse {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
@@ -530,6 +599,16 @@ message CreateReverseSwapRequest {
|
|||||||
optional bool external_pay = 8;
|
optional bool external_pay = 8;
|
||||||
// Description of the invoice which will be created for the swap
|
// Description of the invoice which will be created for the swap
|
||||||
optional string description = 9;
|
optional string description = 9;
|
||||||
|
// Description hash of the invoice which will be created for the swap. Takes precedence over `description`
|
||||||
|
optional bytes description_hash = 10;
|
||||||
|
// Expiry of the reverse swap invoice in seconds
|
||||||
|
optional uint64 invoice_expiry = 11;
|
||||||
|
// Rates to accept for the swap. Queries latest from boltz otherwise
|
||||||
|
// The recommended way to use this is to pass a user approved value from a previous `GetPairInfo` call
|
||||||
|
optional PairInfo accepted_pair = 12;
|
||||||
|
|
||||||
|
// The routing fee limit for paying the lightning invoice in ppm (parts per million)
|
||||||
|
optional uint64 routing_fee_limit_ppm = 13;
|
||||||
}
|
}
|
||||||
message CreateReverseSwapResponse {
|
message CreateReverseSwapResponse {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
@@ -545,7 +624,8 @@ message CreateReverseSwapResponse {
|
|||||||
|
|
||||||
message CreateChainSwapRequest {
|
message CreateChainSwapRequest {
|
||||||
// Amount of satoshis to swap. It is the amount expected to be sent to the lockup address.
|
// Amount of satoshis to swap. It is the amount expected to be sent to the lockup address.
|
||||||
uint64 amount = 1;
|
// If left empty, any amount within the limits will be accepted.
|
||||||
|
optional uint64 amount = 1;
|
||||||
Pair pair = 2;
|
Pair pair = 2;
|
||||||
// Address where funds will be swept to if the swap succeeds
|
// Address where funds will be swept to if the swap succeeds
|
||||||
optional string to_address = 3;
|
optional string to_address = 3;
|
||||||
@@ -561,9 +641,12 @@ message CreateChainSwapRequest {
|
|||||||
optional bool accept_zero_conf = 7;
|
optional bool accept_zero_conf = 7;
|
||||||
// If set, the daemon will not pay the swap from an internal wallet.
|
// If set, the daemon will not pay the swap from an internal wallet.
|
||||||
optional bool external_pay = 8;
|
optional bool external_pay = 8;
|
||||||
// Boltz does not accept 0-conf for Liquid transactions with a fee of 0.01 sat/vByte;
|
optional bool lockup_zero_conf = 9 [deprecated = true];
|
||||||
// when `lockup_zero_conf` is enabled, a fee of 0.1 sat/vByte will be used for Liquid lockup transactions
|
// Fee rate to use when sending from internal wallet
|
||||||
optional bool lockup_zero_conf = 9;
|
optional double sat_per_vbyte = 10;
|
||||||
|
// Rates to accept for the swap. Queries latest from boltz otherwise
|
||||||
|
// The recommended way to use this is to pass a user approved value from a previous `GetPairInfo` call
|
||||||
|
optional PairInfo accepted_pair = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChainSwapInfo {
|
message ChainSwapInfo {
|
||||||
@@ -574,7 +657,7 @@ message ChainSwapInfo {
|
|||||||
string status = 5;
|
string status = 5;
|
||||||
string preimage = 6;
|
string preimage = 6;
|
||||||
bool is_auto = 8;
|
bool is_auto = 8;
|
||||||
optional uint64 service_fee = 9;
|
optional int64 service_fee = 9;
|
||||||
double service_fee_percent = 10;
|
double service_fee_percent = 10;
|
||||||
optional uint64 onchain_fee = 11;
|
optional uint64 onchain_fee = 11;
|
||||||
int64 created_at = 12;
|
int64 created_at = 12;
|
||||||
@@ -614,9 +697,9 @@ message LightningChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message SwapStats {
|
message SwapStats {
|
||||||
uint64 total_fees = 1;
|
int64 total_fees = 1;
|
||||||
uint64 total_amount = 2;
|
uint64 total_amount = 2;
|
||||||
uint64 avg_fees = 3;
|
int64 avg_fees = 3;
|
||||||
uint64 avg_amount = 4;
|
uint64 avg_amount = 4;
|
||||||
uint64 count = 5;
|
uint64 count = 5;
|
||||||
uint64 success_count = 6;
|
uint64 success_count = 6;
|
||||||
@@ -687,6 +770,75 @@ message GetWalletRequest {
|
|||||||
optional uint64 id = 2;
|
optional uint64 id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message WalletSendFee {
|
||||||
|
// amount of sats which would be sent
|
||||||
|
uint64 amount = 1;
|
||||||
|
uint64 fee = 2;
|
||||||
|
// the fee rate used for the estimation in sat/vbyte
|
||||||
|
double fee_rate = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListWalletTransactionsRequest {
|
||||||
|
uint64 id = 1;
|
||||||
|
optional bool exclude_swap_related = 2;
|
||||||
|
optional uint64 limit = 3;
|
||||||
|
optional uint64 offset = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransactionType {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
LOCKUP = 1;
|
||||||
|
REFUND = 2;
|
||||||
|
CLAIM = 3;
|
||||||
|
CONSOLIDATION = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WalletTransaction {
|
||||||
|
string id = 1;
|
||||||
|
// balance change of the wallet in satoshis.
|
||||||
|
// its the sum of all output values minus the sum of all input values which are controlled by the wallet.
|
||||||
|
// positive values indicate incoming transactions, negative values outgoing transactions
|
||||||
|
int64 balance_change = 2;
|
||||||
|
int64 timestamp = 3;
|
||||||
|
repeated TransactionOutput outputs = 4;
|
||||||
|
uint32 block_height = 6;
|
||||||
|
// additional informations about the tx (type, related swaps etc.)
|
||||||
|
repeated TransactionInfo infos = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BumpTransactionRequest {
|
||||||
|
oneof previous {
|
||||||
|
// Id of the transaction to bump. The transaction has to belong to one of the clients wallets
|
||||||
|
string tx_id = 1;
|
||||||
|
// Depending on the state of the swap, the lockup, refund or claim transaction will be bumped
|
||||||
|
string swap_id = 2;
|
||||||
|
}
|
||||||
|
// Fee rate for the new transaction. if not specified, the daemon will query the fee rate from the configured provider
|
||||||
|
// and bump the fee by at least 1 sat/vbyte.
|
||||||
|
optional double sat_per_vbyte = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BumpTransactionResponse {
|
||||||
|
string tx_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransactionInfo {
|
||||||
|
// will be populated for LOCKUP, REFUND and CLAIM
|
||||||
|
optional string swap_id = 1;
|
||||||
|
TransactionType type = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransactionOutput {
|
||||||
|
string address = 1;
|
||||||
|
uint64 amount = 2;
|
||||||
|
// wether the address is controlled by the wallet
|
||||||
|
bool is_our_address = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListWalletTransactionsResponse {
|
||||||
|
repeated WalletTransaction transactions = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetWalletCredentialsRequest {
|
message GetWalletCredentialsRequest {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
optional string password = 2;
|
optional string password = 2;
|
||||||
@@ -696,6 +848,31 @@ message RemoveWalletRequest {
|
|||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message WalletSendRequest {
|
||||||
|
uint64 id = 1;
|
||||||
|
string address = 2;
|
||||||
|
// Amount of satoshis to be sent to 'address`
|
||||||
|
uint64 amount = 3;
|
||||||
|
// Fee rate to use for the transaction
|
||||||
|
optional double sat_per_vbyte = 4;
|
||||||
|
// Sends all available funds to the address. The `amount` field is ignored.
|
||||||
|
optional bool send_all = 5;
|
||||||
|
// whether `address` is the lockup of a swap.
|
||||||
|
optional bool is_swap_address = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WalletSendResponse {
|
||||||
|
string tx_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WalletReceiveRequest {
|
||||||
|
uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WalletReceiveResponse {
|
||||||
|
string address = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
message Wallet {
|
message Wallet {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
@@ -720,6 +897,7 @@ message Subaccount {
|
|||||||
Balance balance = 1;
|
Balance balance = 1;
|
||||||
uint64 pointer = 2;
|
uint64 pointer = 2;
|
||||||
string type = 3;
|
string type = 3;
|
||||||
|
repeated string descriptors = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveWalletResponse {}
|
message RemoveWalletResponse {}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
|||||||
|
|
||||||
wget https://raw.githubusercontent.com/BoltzExchange/boltz-client/master/boltzrpc/boltzrpc.proto -O lnbits/wallets/boltz_grpc_files/boltzrpc.proto
|
wget https://raw.githubusercontent.com/BoltzExchange/boltz-client/refs/heads/master/pkg/boltzrpc/boltzrpc.proto -O boltz_grpc_files/boltzrpc.proto
|
||||||
poetry run python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. --pyi_out=. lnbits/wallets/boltz_grpc_files/boltzrpc.proto
|
poetry run python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. --pyi_out=. boltz_grpc_files/boltzrpc.proto
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Any, AsyncGenerator, Optional
|
from collections.abc import AsyncGenerator
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -70,8 +72,8 @@ class EclairWallet(Wallet):
|
|||||||
|
|
||||||
if r.is_error or "total" not in data:
|
if r.is_error or "total" not in data:
|
||||||
return StatusResponse(f"Server error: '{r.text}'", 0)
|
return StatusResponse(f"Server error: '{r.text}'", 0)
|
||||||
|
total = round(Decimal(data.get("total")), 8) * 100_000_000_000
|
||||||
return StatusResponse(None, int(data.get("total") * 100_000_000_000))
|
return StatusResponse(balance_msat=int(total), error_message=None)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return StatusResponse("Server error: 'invalid json response'", 0)
|
return StatusResponse("Server error: 'invalid json response'", 0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
Generated
+1479
-1144
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.2.0-rc1"
|
version = "1.2.0-rc2"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -33,7 +33,7 @@ uvloop = "0.21.0"
|
|||||||
websockets = "11.0.3"
|
websockets = "11.0.3"
|
||||||
loguru = "0.7.2"
|
loguru = "0.7.2"
|
||||||
grpcio = "1.69.0"
|
grpcio = "1.69.0"
|
||||||
protobuf = "5.28.0"
|
protobuf = "5.29.1"
|
||||||
pyln-client = "24.11"
|
pyln-client = "24.11"
|
||||||
pywebpush = "1.14.1"
|
pywebpush = "1.14.1"
|
||||||
slowapi = "0.1.9"
|
slowapi = "0.1.9"
|
||||||
@@ -41,8 +41,6 @@ websocket-client = "1.8.0"
|
|||||||
pycryptodomex = "3.20.0"
|
pycryptodomex = "3.20.0"
|
||||||
packaging = "24.0"
|
packaging = "24.0"
|
||||||
bolt11 = "2.1.0"
|
bolt11 = "2.1.0"
|
||||||
# needed for new login methods: username-password, google-auth, github-auth
|
|
||||||
bcrypt = "4.2.0"
|
|
||||||
pyjwt = "2.9.0"
|
pyjwt = "2.9.0"
|
||||||
passlib = "1.7.4"
|
passlib = "1.7.4"
|
||||||
itsdangerous = "2.2.0"
|
itsdangerous = "2.2.0"
|
||||||
@@ -56,7 +54,7 @@ environs = "9.5.0"
|
|||||||
# needed for scheduler extension
|
# needed for scheduler extension
|
||||||
python-crontab = "3.2.0"
|
python-crontab = "3.2.0"
|
||||||
# needed for liquid support boltz
|
# needed for liquid support boltz
|
||||||
wallycore = {version = "1.3.0", optional = true}
|
wallycore = {version = "1.4.0", optional = true}
|
||||||
# needed for breez funding source
|
# needed for breez funding source
|
||||||
breez-sdk = {version = "0.6.6", optional = true}
|
breez-sdk = {version = "0.6.6", optional = true}
|
||||||
|
|
||||||
@@ -64,6 +62,7 @@ jsonpath-ng = "^1.7.0"
|
|||||||
pynostr = "^0.6.2"
|
pynostr = "^0.6.2"
|
||||||
python-multipart = "^0.0.20"
|
python-multipart = "^0.0.20"
|
||||||
filetype = "^1.2.0"
|
filetype = "^1.2.0"
|
||||||
|
nostr-sdk = "^0.42.1"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk"]
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ async def test_pay_real_invoice(
|
|||||||
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
balance = await get_node_balance_sats()
|
balance = await get_node_balance_sats()
|
||||||
# TODO: maybe take fee into consideration?
|
|
||||||
assert prev_balance - balance == 100
|
assert prev_balance - balance == 100
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
from lnbits.utils.exchange_rates import (
|
||||||
|
apply_trimmed_mean_filter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplyTrimmedMeanFilter:
|
||||||
|
"""Test the trimmed mean filtering function"""
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_with_outliers(self):
|
||||||
|
"""Test filtering removes outliers that deviate more than threshold"""
|
||||||
|
# Mock rates with one outlier (20% deviation)
|
||||||
|
rates = [
|
||||||
|
("Binance", 50000.0),
|
||||||
|
("Coinbase", 51000.0),
|
||||||
|
("Kraken", 52000.0),
|
||||||
|
("Outlier", 60000.0), # 20% higher than others
|
||||||
|
]
|
||||||
|
|
||||||
|
result = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
# Should remove the outliers (binance and outlier)
|
||||||
|
assert len(result) == 2
|
||||||
|
assert ("Outlier", 60000.0) not in result
|
||||||
|
assert ("Binance", 50000.0) not in result
|
||||||
|
assert ("Coinbase", 51000.0) in result
|
||||||
|
assert ("Kraken", 52000.0) in result
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_no_outliers(self):
|
||||||
|
"""Test filtering keeps all rates when none are outliers"""
|
||||||
|
rates = [
|
||||||
|
("Binance", 50000.0),
|
||||||
|
("Coinbase", 50100.0),
|
||||||
|
("Kraken", 50200.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
# Should keep all rates
|
||||||
|
assert len(result) == 3
|
||||||
|
assert result == rates
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_insufficient_data(self):
|
||||||
|
"""Test filtering returns original data when less than 3 rates"""
|
||||||
|
rates = [
|
||||||
|
("Binance", 50000.0),
|
||||||
|
("Coinbase", 51000.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
# Should return original rates unchanged
|
||||||
|
assert result == rates
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_single_rate(self):
|
||||||
|
"""Test filtering with single rate"""
|
||||||
|
rates = [("Binance", 50000.0)]
|
||||||
|
|
||||||
|
result = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
# Should return original rate unchanged
|
||||||
|
assert result == rates
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_empty_list(self):
|
||||||
|
"""Test filtering with empty list"""
|
||||||
|
rates = []
|
||||||
|
|
||||||
|
result = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
# Should return empty list
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_too_many_outliers(self):
|
||||||
|
"""Test fallback to median when filtering removes too many values"""
|
||||||
|
rates = [
|
||||||
|
("Provider1", 50000.0),
|
||||||
|
("Provider2", 60000.0), # 20% higher
|
||||||
|
("Provider3", 40000.0), # 20% lower
|
||||||
|
]
|
||||||
|
|
||||||
|
result = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
# Should fall back to rate closest to median (Provider1)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == ("Provider1", 50000.0)
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_different_thresholds(self):
|
||||||
|
"""Test filtering with different threshold percentages"""
|
||||||
|
rates = [
|
||||||
|
("Binance", 50000.0),
|
||||||
|
("Coinbase", 51000.0),
|
||||||
|
("Kraken", 53000.0),
|
||||||
|
("Outlier", 55000.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
# For the values, the average is 52250
|
||||||
|
# 1% either side of the average is 51727.50 and 52772.50
|
||||||
|
# This would result in three rates being removed (Binance, Kraken and Outlier)
|
||||||
|
result_1pct = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
assert len(result_1pct) == 1
|
||||||
|
assert ("Binance", 50000.0) not in result_1pct
|
||||||
|
assert ("Coinbase", 51000.0) in result_1pct
|
||||||
|
assert ("Kraken", 53000.0) not in result_1pct
|
||||||
|
assert ("Outlier", 55000.0) not in result_1pct
|
||||||
|
|
||||||
|
# With 5% threshold, should keep just three
|
||||||
|
result_5pct = apply_trimmed_mean_filter(rates, threshold_percentage=0.05)
|
||||||
|
assert len(result_5pct) == 3
|
||||||
|
assert ("Binance", 50000.0) in result_5pct
|
||||||
|
assert ("Coinbase", 51000.0) in result_5pct
|
||||||
|
assert ("Kraken", 53000.0) in result_5pct
|
||||||
|
assert ("Outlier", 55000.0) not in result_5pct
|
||||||
|
|
||||||
|
def test_trimmed_mean_filter_edge_case_exact_threshold(self):
|
||||||
|
"""Test filtering with rates exactly at the threshold"""
|
||||||
|
rates = [
|
||||||
|
("Binance", 50000.0),
|
||||||
|
("Coinbase", 50500.0), # Exactly 1% higher
|
||||||
|
]
|
||||||
|
|
||||||
|
result = apply_trimmed_mean_filter(rates, threshold_percentage=0.01)
|
||||||
|
|
||||||
|
# Should keep the rate at exactly 1% deviation
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result == rates
|
||||||
Reference in New Issue
Block a user