mirror of
https://github.com/lnbits/lnbits.git
synced 2025-03-28 18:52:00 +01:00
Merge branch 'dev' into backgroundimage
This commit is contained in:
commit
f7ed194e02
1
.github/workflows/regtest.yml
vendored
1
.github/workflows/regtest.yml
vendored
@ -36,7 +36,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
||||||
cd docker
|
cd docker
|
||||||
git checkout cln-24-11
|
|
||||||
chmod +x ./tests
|
chmod +x ./tests
|
||||||
./tests
|
./tests
|
||||||
sudo chmod -R a+rwx .
|
sudo chmod -R a+rwx .
|
||||||
|
@ -12,11 +12,15 @@ Note that by default LNbits uses SQLite as its database, which is simple and eff
|
|||||||
|
|
||||||
## Option 1 (recommended): Poetry
|
## Option 1 (recommended): Poetry
|
||||||
|
|
||||||
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed.
|
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed.
|
||||||
|
|
||||||
### Verify Python version
|
### Install Python 3.12
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3.12
|
||||||
|
sudo apt-get install python3.12-dev # ensure correct headers needed for secp256k1
|
||||||
python3 --version
|
python3 --version
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -28,16 +32,16 @@ curl -sSL https://install.python-poetry.org | python3 -
|
|||||||
export PATH="/home/user/.local/bin:$PATH"
|
export PATH="/home/user/.local/bin:$PATH"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### install LNbits
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits.git
|
git clone https://github.com/lnbits/lnbits.git
|
||||||
cd lnbits
|
cd lnbits
|
||||||
|
poetry env use 3.12
|
||||||
git checkout main
|
git checkout main
|
||||||
|
|
||||||
poetry install --only main
|
poetry install --only main
|
||||||
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# set funding source amongst other options
|
# Optional: to set funding source amongst other options via the env `nano .env`
|
||||||
nano .env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Running the server
|
#### Running the server
|
||||||
@ -49,9 +53,16 @@ poetry run lnbits
|
|||||||
# Note that you have to add the line DEBUG=true in your .env file, too.
|
# Note that you have to add the line DEBUG=true in your .env file, too.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### LNbits-cli
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
|
||||||
|
poetry run lnbits-cli --help
|
||||||
|
```
|
||||||
|
|
||||||
#### Updating the server
|
#### Updating the server
|
||||||
|
|
||||||
```
|
```sh
|
||||||
cd lnbits
|
cd lnbits
|
||||||
# Stop LNbits with `ctrl + x`
|
# Stop LNbits with `ctrl + x`
|
||||||
git pull
|
git pull
|
||||||
|
@ -123,15 +123,20 @@ async def get_payments_paginated(
|
|||||||
clause.append("wallet_id = :wallet_id")
|
clause.append("wallet_id = :wallet_id")
|
||||||
|
|
||||||
if complete and pending:
|
if complete and pending:
|
||||||
pass
|
clause.append(
|
||||||
|
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
||||||
|
)
|
||||||
elif complete:
|
elif complete:
|
||||||
clause.append(
|
clause.append(
|
||||||
f"((amount > 0 AND status = '{PaymentState.SUCCESS}') OR amount < 0)"
|
f"""
|
||||||
|
(
|
||||||
|
status = '{PaymentState.SUCCESS}'
|
||||||
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
|
)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
elif pending:
|
elif pending:
|
||||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if outgoing and incoming:
|
if outgoing and incoming:
|
||||||
pass
|
pass
|
||||||
@ -289,8 +294,14 @@ async def get_payments_history(
|
|||||||
values = {
|
values = {
|
||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
}
|
}
|
||||||
|
# count outgoing payments if they are still pending
|
||||||
where = [
|
where = [
|
||||||
f"wallet_id = :wallet_id AND (status = '{PaymentState.SUCCESS}' OR amount < 0)"
|
f"""
|
||||||
|
wallet_id = :wallet_id AND (
|
||||||
|
status = '{PaymentState.SUCCESS}'
|
||||||
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
|
)
|
||||||
|
"""
|
||||||
]
|
]
|
||||||
transactions: list[dict] = await db.fetchall(
|
transactions: list[dict] = await db.fetchall(
|
||||||
f"""
|
f"""
|
||||||
|
@ -51,8 +51,8 @@ class AuditFilters(FilterModel):
|
|||||||
|
|
||||||
|
|
||||||
class AuditCountStat(BaseModel):
|
class AuditCountStat(BaseModel):
|
||||||
field: str
|
field: str = ""
|
||||||
total: float
|
total: float = 0
|
||||||
|
|
||||||
|
|
||||||
class AuditStats(BaseModel):
|
class AuditStats(BaseModel):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}
|
%} {% block page %}
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md justify-center">
|
||||||
<div class="col q-my-md">
|
<div class="col q-mb-md">
|
||||||
<q-btn
|
<q-btn
|
||||||
:label="$t('save')"
|
:label="$t('save')"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block page %}
|
%} {% block page %}
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center q-mb-xl">
|
<div class="row q-col-gutter-md justify-center q-mb-lg">
|
||||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||||
<q-card class="q-pt-sm">
|
<q-card class="q-pt-sm">
|
||||||
<strong>Components</strong>
|
<strong>Components</strong>
|
||||||
|
@ -1,21 +1,5 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {{ window_vars(user, extensions) }}{% block page %}
|
%} {{ window_vars(user, extensions) }}{% block page %}
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col-sm-9 col-xs-12">
|
|
||||||
<p class="text-h4 gt-sm" v-text="$t('extensions')"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-3 col-xs-12 q-ml-auto">
|
|
||||||
<q-input v-model="searchTerm" :label="$t('search_extensions')">
|
|
||||||
<q-icon
|
|
||||||
v-if="searchTerm !== ''"
|
|
||||||
name="close"
|
|
||||||
@click="searchTerm = ''"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -36,6 +20,25 @@
|
|||||||
v-text="$t('only_admins_can_install')"
|
v-text="$t('only_admins_can_install')"
|
||||||
></i>
|
></i>
|
||||||
<q-space></q-space>
|
<q-space></q-space>
|
||||||
|
<q-input
|
||||||
|
:label="$t('search_extensions')"
|
||||||
|
:dense="dense"
|
||||||
|
class="float-right q-pr-xl"
|
||||||
|
v-model="searchTerm"
|
||||||
|
>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search"> </q-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
v-if="searchTerm !== ''"
|
||||||
|
name="close"
|
||||||
|
@click="searchTerm = ''"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="g.user.admin && updatableExtensions?.length"
|
v-if="g.user.admin && updatableExtensions?.length"
|
||||||
@click="showUpdateAllDialog = true"
|
@click="showUpdateAllDialog = true"
|
||||||
|
@ -25,6 +25,68 @@
|
|||||||
} : ''"
|
} : ''"
|
||||||
>
|
>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<q-scroll-area
|
||||||
|
v-if="!mobileSimple"
|
||||||
|
style="
|
||||||
|
height: 115px;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="row no-wrap q-gutter-md q-pr-md">
|
||||||
|
<q-card
|
||||||
|
v-for="wallet in g.user.wallets"
|
||||||
|
:key="wallet.id"
|
||||||
|
class="wallet-list-card"
|
||||||
|
bordered
|
||||||
|
tag="a"
|
||||||
|
:href="wallet.url"
|
||||||
|
:style="
|
||||||
|
g.wallet && g.wallet.id === wallet.id
|
||||||
|
? `border: 1px solid ${primaryColor}; width: 250px; text-decoration: none;`
|
||||||
|
: 'width: 250px; text-decoration: none;'
|
||||||
|
"
|
||||||
|
:class="{
|
||||||
|
'active-wallet-card': g.wallet && g.wallet.id === wallet.id
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-avatar
|
||||||
|
size="lg"
|
||||||
|
:color="
|
||||||
|
g.wallet && g.wallet.id === wallet.id
|
||||||
|
? $q.dark.isActive
|
||||||
|
? 'primary'
|
||||||
|
: 'primary'
|
||||||
|
: 'grey-5'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="flash_on"
|
||||||
|
:size="$q.dark.isActive ? '21px' : '20px'"
|
||||||
|
:color="$q.dark.isActive ? 'black' : 'grey-3'"
|
||||||
|
></q-icon>
|
||||||
|
</q-avatar>
|
||||||
|
<div
|
||||||
|
class="text-h6 q-pl-md"
|
||||||
|
:class="{
|
||||||
|
'text-bold': g.wallet && g.wallet.id === wallet.id
|
||||||
|
}"
|
||||||
|
v-text="wallet.name"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-pt-sm">
|
||||||
|
<h6 class="q-my-none text-no-wrap">
|
||||||
|
<strong v-text="wallet.fsat"></strong>
|
||||||
|
<small> {{LNBITS_DENOMINATION}}</small>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</q-scroll-area>
|
||||||
<q-card
|
<q-card
|
||||||
:style="$q.screen.lt.md ? {
|
:style="$q.screen.lt.md ? {
|
||||||
background: $q.screen.lt.md ? 'none !important': ''
|
background: $q.screen.lt.md ? 'none !important': ''
|
||||||
|
@ -35,7 +35,7 @@ from lnbits.settings import settings
|
|||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
allowed_currencies,
|
allowed_currencies,
|
||||||
fiat_amount_as_satoshis,
|
fiat_amount_as_satoshis,
|
||||||
get_fiat_rate_satoshis,
|
get_fiat_rate_and_price_satoshis,
|
||||||
satoshis_amount_as_fiat,
|
satoshis_amount_as_fiat,
|
||||||
)
|
)
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
@ -235,7 +235,7 @@ async def api_exchange_rate_history() -> list[dict]:
|
|||||||
|
|
||||||
@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, price = await get_fiat_rate_satoshis(currency)
|
rate, price = await get_fiat_rate_and_price_satoshis(currency)
|
||||||
return {"rate": rate, "price": price}
|
return {"rate": rate, "price": price}
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,8 +108,6 @@ async def api_payments_paginated(
|
|||||||
await update_pending_payments(key_info.wallet.id)
|
await update_pending_payments(key_info.wallet.id)
|
||||||
page = await get_payments_paginated(
|
page = await get_payments_paginated(
|
||||||
wallet_id=key_info.wallet.id,
|
wallet_id=key_info.wallet.id,
|
||||||
pending=True,
|
|
||||||
complete=True,
|
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
)
|
||||||
return page
|
return page
|
||||||
|
@ -58,7 +58,8 @@ window.app = Vue.createApp({
|
|||||||
inkeyHidden: true,
|
inkeyHidden: true,
|
||||||
adminkeyHidden: true,
|
adminkeyHidden: true,
|
||||||
hasNfc: false,
|
hasNfc: false,
|
||||||
nfcReaderAbortController: null
|
nfcReaderAbortController: null,
|
||||||
|
primaryColor: this.$q.localStorage.getItem('lnbits.primaryColor')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -672,6 +673,11 @@ window.app = Vue.createApp({
|
|||||||
watch: {
|
watch: {
|
||||||
updatePayments() {
|
updatePayments() {
|
||||||
this.updateFiatBalance()
|
this.updateFiatBalance()
|
||||||
|
},
|
||||||
|
'$q.screen.gt.sm'(value) {
|
||||||
|
if (value == true) {
|
||||||
|
this.mobileSimple = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -247,7 +247,7 @@ async def btc_price(currency: str) -> float:
|
|||||||
return sum(rates_values) / len(rates_values)
|
return sum(rates_values) / len(rates_values)
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> tuple[float, float]:
|
async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]:
|
||||||
price = await cache.save_result(
|
price = await cache.save_result(
|
||||||
lambda: btc_price(currency),
|
lambda: btc_price(currency),
|
||||||
f"btc-price-{currency}",
|
f"btc-price-{currency}",
|
||||||
@ -256,11 +256,16 @@ async def get_fiat_rate_satoshis(currency: str) -> tuple[float, float]:
|
|||||||
return float(100_000_000 / price), price
|
return float(100_000_000 / price), price
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
|
rate, _ = await get_fiat_rate_and_price_satoshis(currency)
|
||||||
|
return rate
|
||||||
|
|
||||||
|
|
||||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
rate, _ = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
return int(amount * (rate))
|
return int(amount * (rate))
|
||||||
|
|
||||||
|
|
||||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||||
rate, _ = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
return float(amount / rate)
|
return float(amount / rate)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.0.0-rc6"
|
version = "1.0.0-rc7"
|
||||||
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"
|
||||||
|
66
tests/unit/test_crud_payments.py
Normal file
66
tests/unit/test_crud_payments.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from lnbits.core.crud import create_wallet, get_payments, update_payment
|
||||||
|
from lnbits.core.models import PaymentState
|
||||||
|
from lnbits.core.services import create_user_account, update_wallet_balance
|
||||||
|
|
||||||
|
|
||||||
|
async def update_payments(payments):
|
||||||
|
payments[0].status = PaymentState.FAILED
|
||||||
|
await update_payment(payments[0])
|
||||||
|
payments[1].status = PaymentState.PENDING
|
||||||
|
await update_payment(payments[1])
|
||||||
|
payments[2].status = PaymentState.PENDING
|
||||||
|
await update_payment(payments[2])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_crud_get_payments(app):
|
||||||
|
|
||||||
|
user = await create_user_account()
|
||||||
|
wallet = await create_wallet(user_id=user.id)
|
||||||
|
|
||||||
|
for _ in range(11):
|
||||||
|
await update_wallet_balance(wallet, 10)
|
||||||
|
wallet.balance_msat += 10 * 1000
|
||||||
|
await update_wallet_balance(wallet, -10)
|
||||||
|
wallet.balance_msat += -10 * 1000
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id)
|
||||||
|
assert len(payments) == 22, "should return 22 successful payments"
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id, incoming=True)
|
||||||
|
assert len(payments) == 11, "should return 11 successful incoming payments"
|
||||||
|
await update_payments(payments)
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id, outgoing=True)
|
||||||
|
assert len(payments) == 11, "should return 11 successful outgoing payments"
|
||||||
|
await update_payments(payments)
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id, pending=True)
|
||||||
|
assert len(payments) == 4, "should return 4 pending payments"
|
||||||
|
|
||||||
|
# function signature should have Optional[bool] for complete and pending to make
|
||||||
|
# this distinction possible
|
||||||
|
payments = await get_payments(wallet_id=wallet.id, pending=False)
|
||||||
|
assert len(payments) == 22, "should return all payments"
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id, complete=True, pending=True)
|
||||||
|
assert len(payments) == 20, "should return 4 pending and 16 complete payments"
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id, complete=True, outgoing=True)
|
||||||
|
assert (
|
||||||
|
len(payments) == 10
|
||||||
|
), "should return 8 complete outgoing payments and 2 pending outgoing payments"
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id)
|
||||||
|
assert len(payments) == 22, "should return all payments"
|
||||||
|
|
||||||
|
payments = await get_payments(wallet_id=wallet.id, complete=True)
|
||||||
|
assert (
|
||||||
|
len(payments) == 18
|
||||||
|
), "should return 14 successful payment and 4 pending payments"
|
||||||
|
|
||||||
|
# both false should return failed payments
|
||||||
|
# payments = await get_payments(wallet_id=wallet.id, complete=False, pending=False)
|
||||||
|
# assert len(payments) == 2, "should return 2 failed payment"
|
Loading…
x
Reference in New Issue
Block a user