feat: lud11 disposable and storeable payRequests. (#3317)
This commit is contained in:
parent
1488f580ff
commit
ae6cf47244
12 changed files with 289 additions and 46 deletions
|
|
@ -33,7 +33,7 @@ async def create_wallet(
|
||||||
async def update_wallet(
|
async def update_wallet(
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Optional[Wallet]:
|
) -> Wallet:
|
||||||
wallet.updated_at = datetime.now(timezone.utc)
|
wallet.updated_at = datetime.now(timezone.utc)
|
||||||
await (conn or db).update("wallets", wallet)
|
await (conn or db).update("wallets", wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
|
||||||
|
|
@ -735,3 +735,11 @@ async def m032_add_external_id_to_accounts(db: Connection):
|
||||||
|
|
||||||
async def m033_update_payment_table(db: Connection):
|
async def m033_update_payment_table(db: Connection):
|
||||||
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
|
from time import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from lnurl import LnAddress, Lnurl, LnurlPayResponse
|
from lnurl import LnAddress, Lnurl, LnurlPayResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurlPayment(BaseModel):
|
class CreateLnurlPayment(BaseModel):
|
||||||
res: LnurlPayResponse
|
res: LnurlPayResponse | None = None
|
||||||
|
lnurl: Lnurl | LnAddress | None = None
|
||||||
amount: int
|
amount: int
|
||||||
comment: Optional[str] = None
|
comment: Optional[str] = None
|
||||||
unit: Optional[str] = None
|
unit: Optional[str] = None
|
||||||
|
|
@ -18,3 +20,13 @@ class CreateLnurlWithdraw(BaseModel):
|
||||||
|
|
||||||
class LnurlScan(BaseModel):
|
class LnurlScan(BaseModel):
|
||||||
lnurl: Lnurl | LnAddress
|
lnurl: Lnurl | LnAddress
|
||||||
|
|
||||||
|
|
||||||
|
class StoredPayLink(BaseModel):
|
||||||
|
lnurl: str
|
||||||
|
label: str
|
||||||
|
last_used: int = Field(default_factory=lambda: int(time()))
|
||||||
|
|
||||||
|
|
||||||
|
class StoredPayLinks(BaseModel):
|
||||||
|
links: list[StoredPayLink] = []
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from ecdsa import SECP256k1, SigningKey
|
|
||||||
from lnurl import encode as lnurl_encode
|
from lnurl import encode as lnurl_encode
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from lnbits.core.models.lnurl import StoredPayLinks
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
@ -41,6 +39,7 @@ class Wallet(BaseModel):
|
||||||
currency: str | None = None
|
currency: str | None = None
|
||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
extra: WalletExtra = WalletExtra()
|
||||||
|
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def balance(self) -> int:
|
def balance(self) -> int:
|
||||||
|
|
@ -58,14 +57,6 @@ class Wallet(BaseModel):
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def lnurlauth_key(self, domain: str) -> SigningKey:
|
|
||||||
hashing_key = hashlib.sha256(self.id.encode()).digest()
|
|
||||||
linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
|
|
||||||
|
|
||||||
return SigningKey.from_string(
|
|
||||||
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
|
from time import time
|
||||||
|
|
||||||
from lnurl import (
|
from lnurl import (
|
||||||
|
LnAddress,
|
||||||
|
Lnurl,
|
||||||
LnurlErrorResponse,
|
LnurlErrorResponse,
|
||||||
LnurlPayActionResponse,
|
LnurlPayActionResponse,
|
||||||
LnurlPayResponse,
|
LnurlPayResponse,
|
||||||
|
|
@ -6,8 +10,11 @@ from lnurl import (
|
||||||
execute_pay_request,
|
execute_pay_request,
|
||||||
handle,
|
handle,
|
||||||
)
|
)
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.models import CreateLnurlPayment
|
from lnbits.core.crud import update_wallet
|
||||||
|
from lnbits.core.models import CreateLnurlPayment, Wallet
|
||||||
|
from lnbits.core.models.lnurl import StoredPayLink
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||||
|
|
||||||
|
|
@ -34,12 +41,26 @@ async def get_pr_from_lnurl(
|
||||||
return res2.pr
|
return res2.pr
|
||||||
|
|
||||||
|
|
||||||
async def fetch_lnurl_pay_request(data: CreateLnurlPayment) -> LnurlPayActionResponse:
|
async def fetch_lnurl_pay_request(
|
||||||
|
data: CreateLnurlPayment, wallet: Wallet | None = None
|
||||||
|
) -> tuple[LnurlPayResponse, LnurlPayActionResponse]:
|
||||||
"""
|
"""
|
||||||
Pay an LNURL payment request.
|
Pay an LNURL payment request.
|
||||||
|
optional `wallet` is used to store the pay link in the wallet's stored links.
|
||||||
|
|
||||||
raises `LnurlResponseException` if pay request fails
|
raises `LnurlResponseException` if pay request fails
|
||||||
"""
|
"""
|
||||||
|
if not data.res and data.lnurl:
|
||||||
|
res = await handle(data.lnurl, user_agent=settings.user_agent, timeout=5)
|
||||||
|
if isinstance(res, LnurlErrorResponse):
|
||||||
|
raise LnurlResponseException(res.reason)
|
||||||
|
if not isinstance(res, LnurlPayResponse):
|
||||||
|
raise LnurlResponseException(
|
||||||
|
"Invalid LNURL response. Expected LnurlPayResponse."
|
||||||
|
)
|
||||||
|
data.res = res
|
||||||
|
if not data.res:
|
||||||
|
raise LnurlResponseException("No LNURL pay request provided.")
|
||||||
|
|
||||||
if data.unit and data.unit != "sat":
|
if data.unit and data.unit != "sat":
|
||||||
# shift to float with 2 decimal places
|
# shift to float with 2 decimal places
|
||||||
|
|
@ -49,10 +70,69 @@ async def fetch_lnurl_pay_request(data: CreateLnurlPayment) -> LnurlPayActionRes
|
||||||
else:
|
else:
|
||||||
amount_msat = data.amount
|
amount_msat = data.amount
|
||||||
|
|
||||||
return await execute_pay_request(
|
res2 = await execute_pay_request(
|
||||||
data.res,
|
data.res,
|
||||||
msat=amount_msat,
|
msat=amount_msat,
|
||||||
comment=data.comment,
|
comment=data.comment,
|
||||||
user_agent=settings.user_agent,
|
user_agent=settings.user_agent,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if wallet:
|
||||||
|
await store_paylink(data.res, res2, wallet, data.lnurl)
|
||||||
|
|
||||||
|
return data.res, res2
|
||||||
|
|
||||||
|
|
||||||
|
async def store_paylink(
|
||||||
|
res: LnurlPayResponse,
|
||||||
|
res2: LnurlPayActionResponse,
|
||||||
|
wallet: Wallet,
|
||||||
|
lnurl: LnAddress | Lnurl | None = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
if res2.disposable is not False:
|
||||||
|
return # do not store disposable LNURL pay links
|
||||||
|
|
||||||
|
logger.debug(f"storing lnurl pay link for wallet {wallet.id}. ")
|
||||||
|
|
||||||
|
stored_paylink = None
|
||||||
|
# If we have only a LnurlPayResponse, we can use its lnaddress
|
||||||
|
# because the lnurl is not available.
|
||||||
|
if not lnurl:
|
||||||
|
for _data in res.metadata.list():
|
||||||
|
if _data[0] == "text/identifier":
|
||||||
|
stored_paylink = StoredPayLink(
|
||||||
|
lnurl=LnAddress(_data[1]), label=res.metadata.text
|
||||||
|
)
|
||||||
|
if not stored_paylink:
|
||||||
|
logger.warning(
|
||||||
|
"No lnaddress found in metadata for LNURL pay link. "
|
||||||
|
"Skipping storage."
|
||||||
|
)
|
||||||
|
return # skip if lnaddress not found in metadata
|
||||||
|
else:
|
||||||
|
if isinstance(lnurl, Lnurl):
|
||||||
|
_lnurl = str(lnurl.lud17 or lnurl.bech32)
|
||||||
|
else:
|
||||||
|
_lnurl = str(lnurl)
|
||||||
|
stored_paylink = StoredPayLink(lnurl=_lnurl, label=res.metadata.text)
|
||||||
|
|
||||||
|
# update last_used if its already stored
|
||||||
|
for pl in wallet.stored_paylinks.links:
|
||||||
|
if pl.lnurl == stored_paylink.lnurl:
|
||||||
|
pl.last_used = int(time())
|
||||||
|
await update_wallet(wallet)
|
||||||
|
logger.debug(
|
||||||
|
"Updated last used time for LNURL "
|
||||||
|
f"pay link {stored_paylink.lnurl} in wallet {wallet.id}."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# if not already stored, append it
|
||||||
|
if not any(stored_paylink.lnurl == pl.lnurl for pl in wallet.stored_paylinks.links):
|
||||||
|
wallet.stored_paylinks.links.append(stored_paylink)
|
||||||
|
await update_wallet(wallet)
|
||||||
|
logger.debug(
|
||||||
|
f"Stored LNURL pay link {stored_paylink.lnurl} for wallet {wallet.id}."
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -289,9 +289,87 @@
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
group="extras"
|
||||||
|
icon="qr_code"
|
||||||
|
v-if="stored_paylinks.length > 0"
|
||||||
|
:label="$t('stored_paylinks')"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row flex" v-for="paylink in stored_paylinks">
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
icon="send"
|
||||||
|
size="xs"
|
||||||
|
@click="sendToPaylink(paylink.lnurl)"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="`send to: ${paylink.lnurl}`"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
color="secondary"
|
||||||
|
icon="content_copy"
|
||||||
|
size="xs"
|
||||||
|
@click="copyText(paylink.lnurl)"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="`copy: ${paylink.lnurl}`"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<span
|
||||||
|
v-text="paylink.label"
|
||||||
|
class="q-mr-xs q-ml-xs"
|
||||||
|
></span>
|
||||||
|
<q-btn dense flat color="primary" icon="edit" size="xs">
|
||||||
|
<q-popup-edit
|
||||||
|
@update:model-value="editPaylink()"
|
||||||
|
v-model="paylink.label"
|
||||||
|
v-slot="scope"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
dark
|
||||||
|
color="white"
|
||||||
|
v-model="scope.value"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
counter
|
||||||
|
@keyup.enter="scope.set"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="edit" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</q-popup-edit>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('edit')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<span style="flex-grow: 1"></span>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
color="red"
|
||||||
|
icon="delete"
|
||||||
|
size="xs"
|
||||||
|
@click="deletePaylink(paylink.lnurl)"
|
||||||
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('delete')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<span v-text="dateFromNow(paylink.last_used)"></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-expansion-item
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="phone_android"
|
icon="phone_android"
|
||||||
:label="$t('access_wallet_on_mobile')"
|
:label="$t('access_wallet_on_mobile')"
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,18 @@ from ..services import fetch_lnurl_pay_request, pay_invoice
|
||||||
lnurl_router = APIRouter(tags=["LNURL"])
|
lnurl_router = APIRouter(tags=["LNURL"])
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle(lnurl: str) -> LnurlResponseModel:
|
||||||
|
try:
|
||||||
|
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
||||||
|
if isinstance(res, LnurlErrorResponse):
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
@lnurl_router.get(
|
@lnurl_router.get(
|
||||||
"/api/v1/lnurlscan/{code}",
|
"/api/v1/lnurlscan/{code}",
|
||||||
dependencies=[Depends(require_invoice_key)],
|
dependencies=[Depends(require_invoice_key)],
|
||||||
|
|
@ -47,13 +59,7 @@ lnurl_router = APIRouter(tags=["LNURL"])
|
||||||
| LnurlErrorResponse,
|
| LnurlErrorResponse,
|
||||||
)
|
)
|
||||||
async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
||||||
try:
|
res = await _handle(code)
|
||||||
res = await lnurl_handle(code, user_agent=settings.user_agent, timeout=5)
|
|
||||||
except LnurlResponseException as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)):
|
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)):
|
||||||
check_callback_url(res.callback)
|
check_callback_url(res.callback)
|
||||||
return res
|
return res
|
||||||
|
|
@ -68,13 +74,7 @@ async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
||||||
| LnurlErrorResponse,
|
| LnurlErrorResponse,
|
||||||
)
|
)
|
||||||
async def api_lnurlscan_post(scan: LnurlScan) -> LnurlResponseModel:
|
async def api_lnurlscan_post(scan: LnurlScan) -> LnurlResponseModel:
|
||||||
try:
|
return await _handle(scan.lnurl)
|
||||||
res = await lnurl_handle(scan.lnurl, user_agent=settings.user_agent, timeout=5)
|
|
||||||
except LnurlResponseException as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
|
||||||
) from exc
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@lnurl_router.post("/api/v1/lnurlauth")
|
@lnurl_router.post("/api/v1/lnurlauth")
|
||||||
|
|
@ -101,16 +101,29 @@ async def api_perform_lnurlauth(
|
||||||
async def api_payments_pay_lnurl(
|
async def api_payments_pay_lnurl(
|
||||||
data: CreateLnurlPayment, wallet: WalletTypeInfo = Depends(require_admin_key)
|
data: CreateLnurlPayment, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
|
"""
|
||||||
|
Pay an LNURL payment request.
|
||||||
|
Either provice `res` (LnurlPayResponse) or `lnurl` (str) in the `data` object.
|
||||||
|
"""
|
||||||
|
if not data.res and not data.lnurl:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Missing LNURL or LnurlPayResponse data.",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = await fetch_lnurl_pay_request(data=data)
|
res, res2 = await fetch_lnurl_pay_request(data=data, wallet=wallet.wallet)
|
||||||
except LnurlResponseException as exc:
|
except LnurlResponseException as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
msg = f"Failed to connect to {data.res.callback}."
|
raise HTTPException(
|
||||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) from exc
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
extra: dict[str, Any] = {}
|
extra: dict[str, Any] = {}
|
||||||
if res.success_action:
|
if res2.disposable is False:
|
||||||
extra["success_action"] = res.success_action.json()
|
extra["stored"] = True
|
||||||
|
if res2.success_action:
|
||||||
|
extra["success_action"] = res2.success_action.json()
|
||||||
if data.comment:
|
if data.comment:
|
||||||
extra["comment"] = data.comment
|
extra["comment"] = data.comment
|
||||||
if data.unit and data.unit != "sat":
|
if data.unit and data.unit != "sat":
|
||||||
|
|
@ -119,8 +132,8 @@ async def api_payments_pay_lnurl(
|
||||||
|
|
||||||
payment = await pay_invoice(
|
payment = await pay_invoice(
|
||||||
wallet_id=wallet.wallet.id,
|
wallet_id=wallet.wallet.id,
|
||||||
payment_request=str(res.pr),
|
payment_request=str(res2.pr),
|
||||||
description=data.res.metadata.text,
|
description=res.metadata.text,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ from fastapi import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets_paginated
|
from lnbits.core.crud.wallets import get_wallets_paginated
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
|
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
||||||
|
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
from lnbits.core.models.wallets import WalletsFilters
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
parse_filters,
|
parse_filters,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
|
|
@ -93,6 +93,21 @@ async def api_reset_wallet_keys(
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.put("/stored_paylinks/{wallet_id}")
|
||||||
|
async def api_put_stored_paylinks(
|
||||||
|
wallet_id: str,
|
||||||
|
data: StoredPayLinks,
|
||||||
|
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> list[StoredPayLink]:
|
||||||
|
if key_info.wallet.id != wallet_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN, detail="You cannot modify this wallet"
|
||||||
|
)
|
||||||
|
key_info.wallet.stored_paylinks.links = data.links
|
||||||
|
wallet = await update_wallet(key_info.wallet)
|
||||||
|
return wallet.stored_paylinks.links
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.patch("")
|
@wallet_router.patch("")
|
||||||
async def api_update_wallet(
|
async def api_update_wallet(
|
||||||
name: Optional[str] = Body(None),
|
name: Optional[str] = Body(None),
|
||||||
|
|
|
||||||
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -49,6 +49,7 @@ window.localisation.en = {
|
||||||
export_to_phone_desc:
|
export_to_phone_desc:
|
||||||
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
||||||
access_wallet_on_mobile: 'Mobile Access',
|
access_wallet_on_mobile: 'Mobile Access',
|
||||||
|
stored_paylinks: 'Stored LNURL pay links',
|
||||||
wallet: 'Wallet: ',
|
wallet: 'Wallet: ',
|
||||||
wallet_name: 'Wallet name',
|
wallet_name: 'Wallet name',
|
||||||
wallets: 'Wallets',
|
wallets: 'Wallets',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ window.WalletPageLogic = {
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
baseUrl: `${window.location.protocol}//${window.location.host}/`,
|
||||||
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
websocketUrl: `${'http:' ? 'ws://' : 'wss://'}${window.location.host}/api/v1/ws`,
|
||||||
|
stored_paylinks: [],
|
||||||
parse: {
|
parse: {
|
||||||
show: false,
|
show: false,
|
||||||
invoice: null,
|
invoice: null,
|
||||||
|
|
@ -177,6 +178,10 @@ window.WalletPageLogic = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
dateFromNow(unix) {
|
||||||
|
const date = new Date(unix * 1000)
|
||||||
|
return moment.utc(date).fromNow()
|
||||||
|
},
|
||||||
formatFiatAmount(amount, currency) {
|
formatFiatAmount(amount, currency) {
|
||||||
this.update.currency = currency
|
this.update.currency = currency
|
||||||
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
this.formattedFiatAmount = LNbits.utils.formatCurrency(
|
||||||
|
|
@ -536,6 +541,7 @@ window.WalletPageLogic = {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
|
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
|
||||||
res: this.parse.lnurlpay,
|
res: this.parse.lnurlpay,
|
||||||
|
lnurl: this.parse.data.request,
|
||||||
unit: this.parse.data.unit,
|
unit: this.parse.data.unit,
|
||||||
amount: this.parse.data.amount * 1000,
|
amount: this.parse.data.amount * 1000,
|
||||||
comment: this.parse.data.comment,
|
comment: this.parse.data.comment,
|
||||||
|
|
@ -1098,9 +1104,51 @@ window.WalletPageLogic = {
|
||||||
saveChartsPreferences() {
|
saveChartsPreferences() {
|
||||||
this.$q.localStorage.set('lnbits.wallets.chartConfig', this.chartConfig)
|
this.$q.localStorage.set('lnbits.wallets.chartConfig', this.chartConfig)
|
||||||
this.refreshCharts()
|
this.refreshCharts()
|
||||||
|
},
|
||||||
|
updatePaylinks() {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/wallet/stored_paylinks/${this.g.wallet.id}`,
|
||||||
|
this.g.wallet.adminkey,
|
||||||
|
{
|
||||||
|
links: this.stored_paylinks
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Paylinks updated.',
|
||||||
|
type: 'positive',
|
||||||
|
timeout: 3500
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendToPaylink(lnurl) {
|
||||||
|
this.parse.data.request = lnurl
|
||||||
|
this.parse.show = true
|
||||||
|
this.lnurlScan()
|
||||||
|
},
|
||||||
|
editPaylink() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updatePaylinks()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deletePaylink(lnurl) {
|
||||||
|
const links = []
|
||||||
|
this.stored_paylinks.forEach(link => {
|
||||||
|
if (link.lnurl !== lnurl) {
|
||||||
|
links.push(link)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.stored_paylinks = links
|
||||||
|
this.updatePaylinks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.stored_paylinks = wallet.stored_paylinks.links
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
if (urlParams.has('lightning') || urlParams.has('lnurl')) {
|
if (urlParams.has('lightning') || urlParams.has('lnurl')) {
|
||||||
this.parse.data.request =
|
this.parse.data.request =
|
||||||
|
|
|
||||||
|
|
@ -792,9 +792,6 @@ async def test_api_payments_pay_lnurl(client, adminkey_headers_from):
|
||||||
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
|
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert (
|
|
||||||
response.json()["detail"] == "Failed to connect to https://xxxxxxx.lnbits.com."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test with invalid callback URL
|
# Test with invalid callback URL
|
||||||
lnurl_data["res"]["callback"] = "invalid-url.lnbits.com"
|
lnurl_data["res"]["callback"] = "invalid-url.lnbits.com"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue