Merge branch 'main' into fix/mypy

This commit is contained in:
dni
2022-07-25 13:46:22 +02:00
21 changed files with 338 additions and 81 deletions

View File

@@ -38,12 +38,14 @@ jobs:
./venv/bin/python -m pip install --upgrade pip ./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
sudo apt install unzip
- name: Run migrations - name: Run migrations
run: | run: |
rm -rf ./data rm -rf ./data
mkdir -p ./data mkdir -p ./data
export LNBITS_DATA_FOLDER="./data" export LNBITS_DATA_FOLDER="./data"
unzip tests/data/mock_data.zip -d ./data
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres" export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
./venv/bin/python tools/conv.py --dont-ignore-missing ./venv/bin/python tools/conv.py

View File

@@ -19,17 +19,11 @@ jobs:
docker build -t lnbits-legend . docker build -t lnbits-legend .
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
source docker-scripts.sh chmod +x ./tests
lnbits-regtest-start ./tests
echo "sleeping 60 seconds"
sleep 60
echo "continue"
lnbits-regtest-init
bitcoin-cli-sim -generate 1
lncli-sim 1 listpeers
sudo chmod -R a+rwx . sudo chmod -R a+rwx .
- name: Install dependencies - name: Install dependencies
env: env:
VIRTUAL_ENV: ./venv VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }} PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: | run: |
@@ -37,7 +31,7 @@ jobs:
./venv/bin/python -m pip install --upgrade pip ./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pylightning ./venv/bin/pip install pylightning
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests - name: Run tests
env: env:
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
@@ -66,17 +60,11 @@ jobs:
docker build -t lnbits-legend . docker build -t lnbits-legend .
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
source docker-scripts.sh chmod +x ./tests
lnbits-regtest-start ./tests
echo "sleeping 60 seconds"
sleep 60
echo "continue"
lnbits-regtest-init
bitcoin-cli-sim -generate 1
lncli-sim 1 listpeers
sudo chmod -R a+rwx . sudo chmod -R a+rwx .
- name: Install dependencies - name: Install dependencies
env: env:
VIRTUAL_ENV: ./venv VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }} PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: | run: |
@@ -84,7 +72,7 @@ jobs:
./venv/bin/python -m pip install --upgrade pip ./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pylightning ./venv/bin/pip install pylightning
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests - name: Run tests
env: env:
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
@@ -94,4 +82,4 @@ jobs:
CLIGHTNING_RPC: docker/data/clightning-1/regtest/lightning-rpc CLIGHTNING_RPC: docker/data/clightning-1/regtest/lightning-rpc
run: | run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet make test-real-wallet

2
.gitignore vendored
View File

@@ -15,7 +15,7 @@ __pycache__
.webassets-cache .webassets-cache
htmlcov htmlcov
test-reports test-reports
tests/data tests/data/*.sqlite3
*.swo *.swo
*.swp *.swp

View File

@@ -30,7 +30,7 @@ async def create_tpos(wallet_id: str, data: CreateTposData) -> TPoS:
async def get_tpos(tpos_id: str) -> Optional[TPoS]: async def get_tpos(tpos_id: str) -> Optional[TPoS]:
row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,)) row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,))
return TPoS.from_row(row) if row else None return TPoS(**row) if row else None
async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]: async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
@@ -42,7 +42,7 @@ async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,) f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,)
) )
return [TPoS.from_row(row) for row in rows] return [TPoS(**row) for row in rows]
async def delete_tpos(tpos_id: str) -> None: async def delete_tpos(tpos_id: str) -> None:

View File

@@ -1,13 +1,15 @@
from sqlite3 import Row from sqlite3 import Row
from typing import Optional
from fastapi import Query
from pydantic import BaseModel from pydantic import BaseModel
class CreateTposData(BaseModel): class CreateTposData(BaseModel):
name: str name: str
currency: str currency: str
tip_options: str tip_options: str = Query(None)
tip_wallet: str tip_wallet: str = Query(None)
class TPoS(BaseModel): class TPoS(BaseModel):
@@ -15,8 +17,8 @@ class TPoS(BaseModel):
wallet: str wallet: str
name: str name: str
currency: str currency: str
tip_options: str tip_options: Optional[str]
tip_wallet: str tip_wallet: Optional[str]
@classmethod @classmethod
def from_row(cls, row: Row) -> "TPoS": def from_row(cls, row: Row) -> "TPoS":

View File

@@ -26,7 +26,6 @@ async def on_invoice_paid(payment: Payment) -> None:
# now we make some special internal transfers (from no one to the receiver) # now we make some special internal transfers (from no one to the receiver)
tpos = await get_tpos(payment.extra.get("tposId")) tpos = await get_tpos(payment.extra.get("tposId"))
tipAmount = payment.extra.get("tipAmount") tipAmount = payment.extra.get("tipAmount")
if tipAmount is None: if tipAmount is None:
@@ -34,6 +33,7 @@ async def on_invoice_paid(payment: Payment) -> None:
return return
tipAmount = tipAmount * 1000 tipAmount = tipAmount * 1000
amount = payment.amount - tipAmount
# mark the original payment with one extra key, "splitted" # mark the original payment with one extra key, "splitted"
# (this prevents us from doing this process again and it's informative) # (this prevents us from doing this process again and it's informative)
@@ -41,13 +41,13 @@ async def on_invoice_paid(payment: Payment) -> None:
await core_db.execute( await core_db.execute(
""" """
UPDATE apipayments UPDATE apipayments
SET extra = ?, amount = amount - ? SET extra = ?, amount = ?
WHERE hash = ? WHERE hash = ?
AND checking_id NOT LIKE 'internal_%' AND checking_id NOT LIKE 'internal_%'
""", """,
( (
json.dumps(dict(**payment.extra, tipSplitted=True)), json.dumps(dict(**payment.extra, tipSplitted=True)),
tipAmount, amount,
payment.payment_hash, payment.payment_hash,
), ),
) )
@@ -60,7 +60,7 @@ async def on_invoice_paid(payment: Payment) -> None:
payment_request="", payment_request="",
payment_hash=payment.payment_hash, payment_hash=payment.payment_hash,
amount=tipAmount, amount=tipAmount,
memo=payment.memo, memo=f"Tip for {payment.memo}",
pending=False, pending=False,
extra={"tipSplitted": True}, extra={"tipSplitted": True},
) )

View File

@@ -54,8 +54,8 @@
></q-btn> ></q-btn>
</q-td> </q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ (col.name == 'tip_options' ? JSON.parse(col.value).join(", ") {{ (col.name == 'tip_options' && col.value ?
: col.value) }} JSON.parse(col.value).join(", ") : col.value) }}
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
<q-btn <q-btn

View File

@@ -167,7 +167,12 @@
<div class="text-center"> <div class="text-center">
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3> <h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none"> <h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small> {% raw %}{{ fsat }}
<small>sat</small>
<span v-show="tip_options" style="font-size: 0.75rem"
>( + {{ tipAmountSat }} tip)</span
>
{% endraw %}
</h5> </h5>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
@@ -272,7 +277,7 @@
return { return {
tposId: '{{ tpos.id }}', tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}', currency: '{{ tpos.currency }}',
tip_options: JSON.parse('{{ tpos.tip_options }}'), tip_options: null,
exchangeRate: null, exchangeRate: null,
stack: [], stack: [],
tipAmount: 0.0, tipAmount: 0.0,
@@ -310,7 +315,6 @@
return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000) return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000)
}, },
fsat: function () { fsat: function () {
console.log('sat', this.sat, LNbits.utils.formatSat(this.sat))
return LNbits.utils.formatSat(this.sat) return LNbits.utils.formatSat(this.sat)
} }
}, },
@@ -350,7 +354,7 @@
this.showInvoice() this.showInvoice()
}, },
submitForm: function () { submitForm: function () {
if (this.tip_options.length) { if (this.tip_options) {
this.showTipModal() this.showTipModal()
} else { } else {
this.showInvoice() this.showInvoice()
@@ -362,7 +366,6 @@
showInvoice: function () { showInvoice: function () {
var self = this var self = this
var dialog = this.invoiceDialog var dialog = this.invoiceDialog
console.log(this.sat, this.tposId)
axios axios
.post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, { .post('/tpos/api/v1/tposs/' + this.tposId + '/invoices', null, {
params: { params: {
@@ -416,6 +419,11 @@
created: function () { created: function () {
var getRates = this.getRates var getRates = this.getRates
getRates() getRates()
this.tip_options =
'{{ tpos.tip_options | tojson }}' == 'null'
? null
: JSON.parse('{{ tpos.tip_options }}')
console.log(typeof this.tip_options, this.tip_options)
setInterval(function () { setInterval(function () {
getRates() getRates()
}, 20000) }, 20000)

View File

@@ -17,7 +17,7 @@ from .models import CreateTposData
@tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK) @tpos_ext.get("/api/v1/tposs", status_code=HTTPStatus.OK)
async def api_tposs( async def api_tposs(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
): ):
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
if all_wallets: if all_wallets:
@@ -63,6 +63,9 @@ async def api_tpos_create_invoice(
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
) )
if tipAmount:
amount += tipAmount
try: try:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=tpos.wallet, wallet_id=tpos.wallet,

View File

@@ -26,6 +26,8 @@ LNBits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes t
- on details you can print the vouchers\ - on details you can print the vouchers\
![printable vouchers](https://i.imgur.com/2xLHbob.jpg) ![printable vouchers](https://i.imgur.com/2xLHbob.jpg)
- every printed LNURLw QR code is unique, it can only be used once - every printed LNURLw QR code is unique, it can only be used once
3. Bonus: you can use an LNbits themed voucher, or use a custom one. There's a _template.svg_ file in `static/images` folder if you want to create your own.\
![voucher](https://i.imgur.com/qyQoHi3.jpg)
#### Advanced #### Advanced

View File

@@ -26,9 +26,10 @@ async def create_withdraw_link(
k1, k1,
open_time, open_time,
usescsv, usescsv,
webhook_url webhook_url,
custom_url
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
link_id, link_id,
@@ -44,6 +45,7 @@ async def create_withdraw_link(
int(datetime.now().timestamp()) + data.wait_time, int(datetime.now().timestamp()) + data.wait_time,
usescsv, usescsv,
data.webhook_url, data.webhook_url,
data.custom_url,
), ),
) )
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)

View File

@@ -115,3 +115,10 @@ async def m004_webhook_url(db):
Adds webhook_url Adds webhook_url
""" """
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_url TEXT;") await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_url TEXT;")
async def m005_add_custom_print_design(db):
"""
Adds custom print design
"""
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;")

View File

@@ -16,6 +16,7 @@ class CreateWithdrawData(BaseModel):
wait_time: int = Query(..., ge=1) wait_time: int = Query(..., ge=1)
is_unique: bool is_unique: bool
webhook_url: str = Query(None) webhook_url: str = Query(None)
custom_url: str = Query(None)
class WithdrawLink(BaseModel): class WithdrawLink(BaseModel):
@@ -34,6 +35,7 @@ class WithdrawLink(BaseModel):
usescsv: str = Query(None) usescsv: str = Query(None)
number: int = Query(0) number: int = Query(0)
webhook_url: str = Query(None) webhook_url: str = Query(None)
custom_url: str = Query(None)
@property @property
def is_spent(self) -> bool: def is_spent(self) -> bool:

View File

@@ -20,9 +20,12 @@ var mapWithdrawLink = function (obj) {
obj.uses_left = obj.uses - obj.used obj.uses_left = obj.uses - obj.used
obj.print_url = [locationPath, 'print/', obj.id].join('') obj.print_url = [locationPath, 'print/', obj.id].join('')
obj.withdraw_url = [locationPath, obj.id].join('') obj.withdraw_url = [locationPath, obj.id].join('')
obj._data.use_custom = Boolean(obj.custom_url)
return obj return obj
} }
const CUSTOM_URL = '/static/images/default_voucher.png'
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
@@ -59,13 +62,15 @@ new Vue({
secondMultiplier: 'seconds', secondMultiplier: 'seconds',
secondMultiplierOptions: ['seconds', 'minutes', 'hours'], secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
data: { data: {
is_unique: false is_unique: false,
use_custom: false
} }
}, },
simpleformDialog: { simpleformDialog: {
show: false, show: false,
data: { data: {
is_unique: true, is_unique: true,
use_custom: true,
title: 'Vouchers', title: 'Vouchers',
min_withdrawable: 0, min_withdrawable: 0,
wait_time: 1 wait_time: 1
@@ -106,12 +111,14 @@ new Vue({
}, },
closeFormDialog: function () { closeFormDialog: function () {
this.formDialog.data = { this.formDialog.data = {
is_unique: false is_unique: false,
use_custom: false
} }
}, },
simplecloseFormDialog: function () { simplecloseFormDialog: function () {
this.simpleformDialog.data = { this.simpleformDialog.data = {
is_unique: false is_unique: false,
use_custom: false
} }
}, },
openQrCodeDialog: function (linkId) { openQrCodeDialog: function (linkId) {
@@ -133,6 +140,9 @@ new Vue({
id: this.formDialog.data.wallet id: this.formDialog.data.wallet
}) })
var data = _.omit(this.formDialog.data, 'wallet') var data = _.omit(this.formDialog.data, 'wallet')
if (data.use_custom && !data?.custom_url) {
data.custom_url = CUSTOM_URL
}
data.wait_time = data.wait_time =
data.wait_time * data.wait_time *
@@ -141,7 +151,6 @@ new Vue({
minutes: 60, minutes: 60,
hours: 3600 hours: 3600
}[this.formDialog.secondMultiplier] }[this.formDialog.secondMultiplier]
if (data.id) { if (data.id) {
this.updateWithdrawLink(wallet, data) this.updateWithdrawLink(wallet, data)
} else { } else {
@@ -159,6 +168,10 @@ new Vue({
data.title = 'vouchers' data.title = 'vouchers'
data.is_unique = true data.is_unique = true
if (data.use_custom && !data?.custom_url) {
data.custom_url = '/static/images/default_voucher.png'
}
if (data.id) { if (data.id) {
this.updateWithdrawLink(wallet, data) this.updateWithdrawLink(wallet, data)
} else { } else {
@@ -181,7 +194,8 @@ new Vue({
'uses', 'uses',
'wait_time', 'wait_time',
'is_unique', 'is_unique',
'webhook_url' 'webhook_url',
'custom_url'
) )
) )
.then(function (response) { .then(function (response) {

View File

@@ -217,6 +217,32 @@
label="Webhook URL (optional)" label="Webhook URL (optional)"
hint="A URL to be called whenever this link gets used." hint="A URL to be called whenever this link gets used."
></q-input> ></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.use_custom"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Use a custom voucher design </q-item-label>
<q-item-label caption
>You can use an LNbits voucher design or a custom
one</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<q-input
v-if="formDialog.data.use_custom"
filled
dense
v-model="formDialog.data.custom_url"
type="text"
label="Custom design .png (optional)"
hint="Enter a URL if you want to use a custom design or leave blank for showing only the QR"
></q-input>
<q-list> <q-list>
<q-item tag="label" class="rounded-borders"> <q-item tag="label" class="rounded-borders">
<q-item-section avatar> <q-item-section avatar>
@@ -303,6 +329,32 @@
:default="1" :default="1"
label="Number of vouchers" label="Number of vouchers"
></q-input> ></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="simpleformDialog.data.use_custom"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Use a custom voucher design </q-item-label>
<q-item-label caption
>You can use an LNbits voucher design or a custom
one</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<q-input
v-if="simpleformDialog.data.use_custom"
filled
dense
v-model="simpleformDialog.data.custom_url"
type="text"
label="Custom design .png (optional)"
hint="Enter a URL if you want to use a custom design or leave blank for showing only the QR"
></q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn

View File

@@ -0,0 +1,110 @@
{% extends "print.html" %} {% block page %}
<div class="row">
<div class="" id="vue">
{% for page in link %}
<page size="A4" id="pdfprint">
{% for one in page %}
<div class="wrapper">
<img src="{{custom_url}}" alt="..." />
<span>{{ amt }} sats</span>
<div class="lnurlw">
<qrcode :value="'{{one}}'" :options="{width: 95, margin: 1}"></qrcode>
</div>
</div>
{% endfor %}
</page>
{% endfor %}
</div>
</div>
{% endblock %} {% block styles %}
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400');
body {
background: rgb(204, 204, 204);
}
page {
background: white;
display: block;
margin: 0 auto;
margin-bottom: 0.5cm;
box-shadow: 0 0 0.5cm rgba(0, 0, 0, 0.5);
}
page[size='A4'] {
width: 21cm;
height: 29.7cm;
}
.wrapper {
position: relative;
margin-bottom: 1rem;
padding: 1rem;
width: fit-content;
}
.wrapper span {
display: block;
position: absolute;
font-family: 'Inter';
font-size: 0.75rem;
color: #fff;
top: calc(3.2mm + 1rem);
right: calc(4mm + 1rem);
}
.wrapper img {
display: block;
width: 187mm;
height: auto;
}
.wrapper .lnurlw {
display: block;
position: absolute;
top: calc(7.3mm + 1rem);
left: calc(7.5mm + 1rem);
transform: rotate(45deg);
}
@media print {
body,
page {
margin: 0px !important;
box-shadow: none !important;
}
.q-page,
.wrapper {
padding: 0px !important;
}
.wrapper span {
top: 3mm;
right: 4mm;
}
.wrapper .lnurlw {
display: block;
position: absolute;
top: 7.3mm;
left: 7.5mm;
transform: rotate(45deg);
}
}
</style>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
data: function () {
return {
theurl: location.protocol + '//' + location.host,
printDialog: {
show: true,
data: null
},
links: []
}
},
created() {
this.links = '{{ link | tojson }}'
}
})
</script>
{% endblock %}

View File

@@ -99,6 +99,18 @@ async def print_qr(request: Request, link_id):
page_link = list(chunks(links, 2)) page_link = list(chunks(links, 2))
linked = list(chunks(page_link, 5)) linked = list(chunks(page_link, 5))
if link.custom_url:
return withdraw_renderer().TemplateResponse(
"withdraw/print_qr_custom.html",
{
"request": request,
"link": page_link,
"unique": True,
"custom_url": link.custom_url,
"amt": link.max_withdrawable,
},
)
return withdraw_renderer().TemplateResponse( return withdraw_renderer().TemplateResponse(
"withdraw/print_qr.html", {"request": request, "link": linked, "unique": True} "withdraw/print_qr.html", {"request": request, "link": linked, "unique": True}
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -0,0 +1,16 @@
<svg width="2000" height="1422" viewBox="0 0 2000 1422" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2_2)">
<rect width="2000" height="1422" fill="#F0F0F0"/>
<line x1="-0.707107" y1="710.293" x2="710.293" y2="-0.707106" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line x1="0.707107" y1="710.293" x2="711.707" y2="1421.29" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line x1="710" y1="-0.00140647" x2="712" y2="1422" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line y1="710" x2="2000" y2="710" stroke="#696969" stroke-width="2" stroke-dasharray="21 21"/>
<line x1="709.707" y1="-0.707107" x2="1420.71" y2="710.293" stroke="#696969" stroke-opacity="0.5" stroke-width="2"/>
<rect x="26" y="216.454" width="275" height="275" transform="rotate(-45 26 216.454)" fill="white" fill-opacity="0.5"/>
</g>
<defs>
<clipPath id="clip0_2_2">
<rect width="2000" height="1422" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 985 B

BIN
tests/data/mock_data.zip Normal file

Binary file not shown.

View File

@@ -38,8 +38,6 @@ else:
pgport = LNBITS_DATABASE_URL.split("@")[1].split(":")[1].split("/")[0] pgport = LNBITS_DATABASE_URL.split("@")[1].split(":")[1].split("/")[0]
pgschema = "" pgschema = ""
print(pgdb, pguser, pgpswd, pghost, pgport, pgschema)
def get_sqlite_cursor(sqdb) -> sqlite3: def get_sqlite_cursor(sqdb) -> sqlite3:
consq = sqlite3.connect(sqdb) consq = sqlite3.connect(sqdb)
@@ -99,8 +97,12 @@ def insert_to_pg(query, data):
for d in data: for d in data:
try: try:
cursor.execute(query, d) cursor.execute(query, d)
except: except Exception as e:
raise ValueError(f"Failed to insert {d}") if args.ignore_errors:
print(e)
print(f"Failed to insert {d}")
else:
raise ValueError(f"Failed to insert {d}")
connection.commit() connection.commit()
cursor.close() cursor.close()
@@ -256,9 +258,10 @@ def migrate_ext(sqlite_db_file, schema, ignore_missing=True):
k1, k1,
open_time, open_time,
used, used,
usescsv usescsv,
webhook_url
) )
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
""" """
insert_to_pg(q, res.fetchall()) insert_to_pg(q, res.fetchall())
# WITHDRAW HASH CHECK # WITHDRAW HASH CHECK
@@ -316,8 +319,8 @@ def migrate_ext(sqlite_db_file, schema, ignore_missing=True):
# TPOSS # TPOSS
res = sq.execute("SELECT * FROM tposs;") res = sq.execute("SELECT * FROM tposs;")
q = f""" q = f"""
INSERT INTO tpos.tposs (id, wallet, name, currency) INSERT INTO tpos.tposs (id, wallet, name, currency, tip_wallet, tip_options)
VALUES (%s, %s, %s, %s); VALUES (%s, %s, %s, %s, %s, %s);
""" """
insert_to_pg(q, res.fetchall()) insert_to_pg(q, res.fetchall())
elif schema == "tipjar": elif schema == "tipjar":
@@ -512,12 +515,13 @@ def migrate_ext(sqlite_db_file, schema, ignore_missing=True):
wallet, wallet,
url, url,
memo, memo,
description,
amount, amount,
time, time,
remembers, remembers,
extra extras
) )
VALUES (%s, %s, %s, %s, %s, to_timestamp(%s), %s, %s); VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s), %s, %s);
""" """
insert_to_pg(q, res.fetchall()) insert_to_pg(q, res.fetchall())
elif schema == "offlineshop": elif schema == "offlineshop":
@@ -543,15 +547,15 @@ def migrate_ext(sqlite_db_file, schema, ignore_missing=True):
# lnurldevice # lnurldevice
res = sq.execute("SELECT * FROM lnurldevices;") res = sq.execute("SELECT * FROM lnurldevices;")
q = f""" q = f"""
INSERT INTO lnurldevice.lnurldevices (id, key, title, wallet, currency, device, profit) INSERT INTO lnurldevice.lnurldevices (id, key, title, wallet, currency, device, profit, timestamp)
VALUES (%s, %s, %s, %s, %s, %s, %s); VALUES (%s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
""" """
insert_to_pg(q, res.fetchall()) insert_to_pg(q, res.fetchall())
# lnurldevice PAYMENT # lnurldevice PAYMENT
res = sq.execute("SELECT * FROM lnurldevicepayment;") res = sq.execute("SELECT * FROM lnurldevicepayment;")
q = f""" q = f"""
INSERT INTO lnurldevice.lnurldevicepayment (id, deviceid, payhash, payload, pin, sats) INSERT INTO lnurldevice.lnurldevicepayment (id, deviceid, payhash, payload, pin, sats, timestamp)
VALUES (%s, %s, %s, %s, %s, %s); VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s));
""" """
insert_to_pg(q, res.fetchall()) insert_to_pg(q, res.fetchall())
elif schema == "lnurlp": elif schema == "lnurlp":
@@ -710,36 +714,69 @@ def migrate_ext(sqlite_db_file, schema, ignore_missing=True):
sq.close() sq.close()
parser = argparse.ArgumentParser(description="Migrate data from SQLite to PostgreSQL") parser = argparse.ArgumentParser(
description="LNbits migration tool for migrating data from SQLite to PostgreSQL"
)
parser.add_argument( parser.add_argument(
dest="sqlite_file", dest="sqlite_path",
const=True, const=True,
nargs="?", nargs="?",
help="SQLite DB to migrate from", help=f"SQLite DB folder *or* single extension db file to migrate. Default: {sqfolder}",
default="data/database.sqlite3", default=sqfolder,
type=str, type=str,
) )
parser.add_argument( parser.add_argument(
"-i", "-e",
"--dont-ignore-missing", "--extensions-only",
help="Error if migration is missing for an extension.", help="Migrate only extensions",
required=False, required=False,
default=False, default=False,
const=True, action="store_true",
nargs="?",
type=bool,
) )
parser.add_argument(
"-s",
"--skip-missing",
help="Error if migration is missing for an extension",
required=False,
default=False,
action="store_true",
)
parser.add_argument(
"-i",
"--ignore-errors",
help="Don't error if migration fails",
required=False,
default=False,
action="store_true",
)
args = parser.parse_args() args = parser.parse_args()
print(args) print("Selected path: ", args.sqlite_path)
check_db_versions(args.sqlite_file) if os.path.isdir(args.sqlite_path):
migrate_core(args.sqlite_file) file = os.path.join(args.sqlite_path, "database.sqlite3")
check_db_versions(file)
if not args.extensions_only:
print(f"Migrating: {file}")
migrate_core(file)
if os.path.isdir(args.sqlite_path):
files = [
os.path.join(args.sqlite_path, file) for file in os.listdir(args.sqlite_path)
]
else:
files = [args.sqlite_path]
files = os.listdir(sqfolder)
for file in files: for file in files:
path = f"data/{file}" filename = os.path.basename(file)
if file.startswith("ext_"): if filename.startswith("ext_"):
schema = file.replace("ext_", "").split(".")[0] schema = filename.replace("ext_", "").split(".")[0]
print(f"Migrating: {schema}") print(f"Migrating: {file}")
migrate_ext(path, schema, ignore_missing=not args.dont_ignore_missing) migrate_ext(
file,
schema,
ignore_missing=args.skip_missing,
)