Wallets: add custom invoice expiry (#1396)
* expiry for fakewallet * expiry for lnd * lnbits backend * fix: eclair descriptionHash fixed and expiry added * cln and sparko * test expiry * Eclair from AdminUI and bugfix for nonexistent payments * add to settings and .env and remove lntxbot * remove duplicate and format * add invoice expiry * add min max and step * UI works now * test should fail, sanity check, will revert * revert, ready for merge Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
This commit is contained in:
parent
5a0b217d63
commit
f0d58a8365
|
@ -63,6 +63,9 @@ LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||||
# just so you can see the UI before dealing with this file.
|
# just so you can see the UI before dealing with this file.
|
||||||
|
|
||||||
|
# Invoice expiry for LND, CLN, Eclair, LNbits funding sources
|
||||||
|
LIGHTNING_INVOICE_EXPIRY=600
|
||||||
|
|
||||||
# Set one of these blocks depending on the wallet kind you chose above:
|
# Set one of these blocks depending on the wallet kind you chose above:
|
||||||
|
|
||||||
# ClicheWallet
|
# ClicheWallet
|
||||||
|
|
|
@ -64,6 +64,7 @@ async def create_invoice(
|
||||||
memo: str,
|
memo: str,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
expiry: Optional[int] = None,
|
||||||
extra: Optional[Dict] = None,
|
extra: Optional[Dict] = None,
|
||||||
webhook: Optional[str] = None,
|
webhook: Optional[str] = None,
|
||||||
internal: Optional[bool] = False,
|
internal: Optional[bool] = False,
|
||||||
|
@ -79,6 +80,7 @@ async def create_invoice(
|
||||||
memo=invoice_memo,
|
memo=invoice_memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
unhashed_description=unhashed_description,
|
unhashed_description=unhashed_description,
|
||||||
|
expiry=expiry or settings.lightning_invoice_expiry,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
raise InvoiceFailure(error_message or "unexpected backend error.")
|
raise InvoiceFailure(error_message or "unexpected backend error.")
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p>Active Funding<small> (Requires server restart)</small></p>
|
<p>Active Funding<small> (Requires server restart)</small></p>
|
||||||
|
@ -30,28 +30,40 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-8">
|
||||||
<div class="col-12">
|
<div class="row q-col-gutter-md">
|
||||||
<p>Fee reserve</p>
|
<div class="col-12 col-md-4">
|
||||||
<div class="row q-col-gutter-md">
|
<p>Invoice Expiry</p>
|
||||||
<div class="col-6">
|
<q-input
|
||||||
<q-input
|
filled
|
||||||
type="number"
|
v-model="formData.lightning_invoice_expiry"
|
||||||
filled
|
label="Invoice expiry (seconds)"
|
||||||
v-model="formData.lnbits_reserve_fee_min"
|
mask="#######"
|
||||||
label="Reserve fee in msats"
|
>
|
||||||
>
|
</q-input>
|
||||||
</q-input>
|
</div>
|
||||||
</div>
|
<div class="col-12 col-md-8">
|
||||||
<div class="col-6">
|
<p>Fee reserve</p>
|
||||||
<q-input
|
<div class="row q-col-gutter-md">
|
||||||
type="number"
|
<div class="col-6">
|
||||||
filled
|
<q-input
|
||||||
name="lnbits_reserve_fee_percent"
|
type="number"
|
||||||
v-model="formData.lnbits_reserve_fee_percent"
|
filled
|
||||||
label="Reserve fee in percent"
|
v-model="formData.lnbits_reserve_fee_min"
|
||||||
step="0.1"
|
label="Reserve fee in msats"
|
||||||
></q-input>
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-input
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
name="lnbits_reserve_fee_percent"
|
||||||
|
v-model="formData.lnbits_reserve_fee_percent"
|
||||||
|
label="Reserve fee in percent"
|
||||||
|
step="0.1"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
:disabled="!checkChanges"
|
:disabled="!checkChanges"
|
||||||
>
|
>
|
||||||
<q-tooltip v-if="checkChanges"> Save your changes </q-tooltip>
|
<q-tooltip v-if="checkChanges"> Save your changes </q-tooltip>
|
||||||
|
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="checkChanges"
|
v-if="checkChanges"
|
||||||
color="red"
|
color="red"
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
style="padding: 6px; border-radius: 6px"
|
style="padding: 6px; border-radius: 6px"
|
||||||
/>
|
/>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="isSuperUser"
|
v-if="isSuperUser"
|
||||||
label="Restart server"
|
label="Restart server"
|
||||||
|
@ -26,6 +28,7 @@
|
||||||
<q-tooltip v-if="needsRestart">
|
<q-tooltip v-if="needsRestart">
|
||||||
Restart the server for changes to take effect
|
Restart the server for changes to take effect
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="needsRestart"
|
v-if="needsRestart"
|
||||||
color="red"
|
color="red"
|
||||||
|
@ -34,6 +37,7 @@
|
||||||
style="padding: 6px; border-radius: 6px"
|
style="padding: 6px; border-radius: 6px"
|
||||||
/>
|
/>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="isSuperUser"
|
v-if="isSuperUser"
|
||||||
label="Topup"
|
label="Topup"
|
||||||
|
@ -42,11 +46,13 @@
|
||||||
>
|
>
|
||||||
<q-tooltip> Add funds to a wallet. </q-tooltip>
|
<q-tooltip> Add funds to a wallet. </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<!-- <q-btn
|
<!-- <q-btn
|
||||||
label="Download Database Backup"
|
label="Download Database Backup"
|
||||||
flat
|
flat
|
||||||
@click="downloadBackup"
|
@click="downloadBackup"
|
||||||
></q-btn> -->
|
></q-btn> -->
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
v-if="isSuperUser"
|
v-if="isSuperUser"
|
||||||
|
@ -59,6 +65,7 @@
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md justify-center">
|
||||||
<div class="col q-gutter-y-md">
|
<div class="col q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
|
@ -70,16 +77,19 @@
|
||||||
label="Funding"
|
label="Funding"
|
||||||
@update="val => tab = val.name"
|
@update="val => tab = val.name"
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
|
||||||
<q-tab
|
<q-tab
|
||||||
name="users"
|
name="users"
|
||||||
label="Users"
|
label="Users"
|
||||||
@update="val => tab = val.name"
|
@update="val => tab = val.name"
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
|
||||||
<q-tab
|
<q-tab
|
||||||
name="server"
|
name="server"
|
||||||
label="Server"
|
label="Server"
|
||||||
@update="val => tab = val.name"
|
@update="val => tab = val.name"
|
||||||
></q-tab>
|
></q-tab>
|
||||||
|
|
||||||
<q-tab
|
<q-tab
|
||||||
name="theme"
|
name="theme"
|
||||||
label="Theme"
|
label="Theme"
|
||||||
|
@ -88,6 +98,7 @@
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-form name="settings_form" id="settings_form">
|
<q-form name="settings_form" id="settings_form">
|
||||||
<q-tab-panels v-model="tab" animated>
|
<q-tab-panels v-model="tab" animated>
|
||||||
{% include "admin/_tab_funding.html" %} {% include
|
{% include "admin/_tab_funding.html" %} {% include
|
||||||
|
@ -98,10 +109,12 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog v-if="isSuperUser" v-model="topUpDialog.show" position="top">
|
<q-dialog v-if="isSuperUser" v-model="topUpDialog.show" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<q-form class="q-gutter-md">
|
<q-form class="q-gutter-md">
|
||||||
<p>TopUp a wallet</p>
|
<p>TopUp a wallet</p>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-input
|
<q-input
|
||||||
|
@ -112,8 +125,10 @@
|
||||||
label="Wallet ID"
|
label="Wallet ID"
|
||||||
hint="Use the wallet ID to topup any wallet"
|
hint="Use the wallet ID to topup any wallet"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-input
|
<q-input
|
||||||
dense
|
dense
|
||||||
|
@ -124,14 +139,15 @@
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn label="Topup" color="primary" @click="topupWallet"></q-btn>
|
<q-btn label="Topup" color="primary" @click="topupWallet"></q-btn>
|
||||||
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script>
|
||||||
new Vue({
|
new Vue({
|
||||||
|
@ -173,7 +189,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'CLightningWallet',
|
'CoreLightningWallet',
|
||||||
{
|
{
|
||||||
corelightning_rpc: {
|
corelightning_rpc: {
|
||||||
value: null,
|
value: null,
|
||||||
|
@ -244,15 +260,15 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'LntxbotWallet',
|
'LnTipsWallet',
|
||||||
{
|
{
|
||||||
lntxbot_api_endpoint: {
|
lntips_api_endpoint: {
|
||||||
value: null,
|
value: null,
|
||||||
label: 'Endpoint'
|
label: 'Endpoint'
|
||||||
},
|
},
|
||||||
lntxbot_key: {
|
lntips_api_key: {
|
||||||
value: null,
|
value: null,
|
||||||
label: 'Key'
|
label: 'API Key'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -278,7 +294,7 @@
|
||||||
{
|
{
|
||||||
eclair_url: {
|
eclair_url: {
|
||||||
value: null,
|
value: null,
|
||||||
label: 'Endpoint'
|
label: 'URL'
|
||||||
},
|
},
|
||||||
eclair_pass: {
|
eclair_pass: {
|
||||||
value: null,
|
value: null,
|
||||||
|
@ -333,19 +349,6 @@
|
||||||
label: 'Token'
|
label: 'Token'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
[
|
|
||||||
'LnTipsWallet',
|
|
||||||
{
|
|
||||||
lntips_api_endpoint: {
|
|
||||||
value: null,
|
|
||||||
label: 'Endpoint'
|
|
||||||
},
|
|
||||||
lntips_api_key: {
|
|
||||||
value: null,
|
|
||||||
label: 'API Key'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,9 @@
|
||||||
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
|
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
<code
|
<code
|
||||||
>{"out": false, "amount": <int>, "memo": <string>, "unit":
|
>{"out": false, "amount": <int>, "memo": <string>,
|
||||||
<string>, "webhook": <url:string>, "internal":
|
"expiry": <int>, "unit": <string>, "webhook":
|
||||||
<bool>}</code
|
<url:string>, "internal": <bool>}</code
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 201 CREATED (application/json)
|
Returns 201 CREATED (application/json)
|
||||||
|
@ -62,8 +62,7 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
|
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
|
||||||
"amount": <int>, "memo": <string>, "webhook":
|
"amount": <int>, "memo": <string>}' -H "X-Api-Key:
|
||||||
<url:string>, "unit": <string>}' -H "X-Api-Key:
|
|
||||||
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
|
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
@ -133,6 +133,7 @@ class CreateInvoiceData(BaseModel):
|
||||||
unit: Optional[str] = "sat"
|
unit: Optional[str] = "sat"
|
||||||
description_hash: Optional[str] = None
|
description_hash: Optional[str] = None
|
||||||
unhashed_description: Optional[str] = None
|
unhashed_description: Optional[str] = None
|
||||||
|
expiry: Optional[int] = None
|
||||||
lnurl_callback: Optional[str] = None
|
lnurl_callback: Optional[str] = None
|
||||||
lnurl_balance_check: Optional[str] = None
|
lnurl_balance_check: Optional[str] = None
|
||||||
extra: Optional[dict] = None
|
extra: Optional[dict] = None
|
||||||
|
@ -178,6 +179,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
memo=memo,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
unhashed_description=unhashed_description,
|
unhashed_description=unhashed_description,
|
||||||
|
expiry=data.expiry,
|
||||||
extra=data.extra,
|
extra=data.extra,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
internal=data.internal,
|
internal=data.internal,
|
||||||
|
|
|
@ -149,6 +149,10 @@ class BoltzExtensionSettings(LNbitsSettings):
|
||||||
boltz_mempool_space_url_ws: str = Field(default="wss://mempool.space")
|
boltz_mempool_space_url_ws: str = Field(default="wss://mempool.space")
|
||||||
|
|
||||||
|
|
||||||
|
class LightningSettings(LNbitsSettings):
|
||||||
|
lightning_invoice_expiry: int = Field(default=600)
|
||||||
|
|
||||||
|
|
||||||
class FundingSourcesSettings(
|
class FundingSourcesSettings(
|
||||||
FakeWalletFundingSource,
|
FakeWalletFundingSource,
|
||||||
LNbitsFundingSource,
|
LNbitsFundingSource,
|
||||||
|
@ -172,6 +176,7 @@ class EditableSettings(
|
||||||
OpsSettings,
|
OpsSettings,
|
||||||
FundingSourcesSettings,
|
FundingSourcesSettings,
|
||||||
BoltzExtensionSettings,
|
BoltzExtensionSettings,
|
||||||
|
LightningSettings,
|
||||||
):
|
):
|
||||||
@validator(
|
@validator(
|
||||||
"lnbits_admin_users",
|
"lnbits_admin_users",
|
||||||
|
@ -217,20 +222,23 @@ class SuperUserSettings(LNbitsSettings):
|
||||||
default=[
|
default=[
|
||||||
"VoidWallet",
|
"VoidWallet",
|
||||||
"FakeWallet",
|
"FakeWallet",
|
||||||
"CLightningWallet",
|
"CoreLightningWallet",
|
||||||
"LndRestWallet",
|
"LndRestWallet",
|
||||||
|
"EclairWallet",
|
||||||
"LndWallet",
|
"LndWallet",
|
||||||
"LntxbotWallet",
|
"LnTipsWallet",
|
||||||
"LNPayWallet",
|
"LNPayWallet",
|
||||||
"LNbitsWallet",
|
"LNbitsWallet",
|
||||||
"OpenNodeWallet",
|
"OpenNodeWallet",
|
||||||
"LnTipsWallet",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlySettings(
|
class ReadOnlySettings(
|
||||||
EnvSettings, SaaSSettings, PersistenceSettings, SuperUserSettings
|
EnvSettings,
|
||||||
|
SaaSSettings,
|
||||||
|
PersistenceSettings,
|
||||||
|
SuperUserSettings,
|
||||||
):
|
):
|
||||||
lnbits_admin_ui: bool = Field(default=False)
|
lnbits_admin_ui: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ class ClicheWallet(Wallet):
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
if unhashed_description or description_hash:
|
if unhashed_description or description_hash:
|
||||||
description_hash_str = (
|
description_hash_str = (
|
||||||
|
|
|
@ -83,6 +83,7 @@ class CoreLightningWallet(Wallet):
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = "lbl{}".format(random.random())
|
label = "lbl{}".format(random.random())
|
||||||
msat: int = int(amount * 1000)
|
msat: int = int(amount * 1000)
|
||||||
|
@ -103,6 +104,7 @@ class CoreLightningWallet(Wallet):
|
||||||
deschashonly=True
|
deschashonly=True
|
||||||
if unhashed_description
|
if unhashed_description
|
||||||
else False, # we can't pass None here
|
else False, # we can't pass None here
|
||||||
|
expiry=kwargs.get("expiry"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.get("code") and r.get("code") < 0:
|
if r.get("code") and r.get("code") < 0:
|
||||||
|
|
|
@ -73,13 +73,17 @@ class EclairWallet(Wallet):
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
|
|
||||||
data: Dict = {"amountMsat": amount * 1000}
|
data: Dict = {"amountMsat": amount * 1000}
|
||||||
|
if kwargs.get("expiry"):
|
||||||
|
data["expireIn"] = kwargs["expiry"]
|
||||||
|
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["descriptionHash"] = description_hash.hex()
|
||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
data["descriptionHash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["description"] = memo or ""
|
data["description"] = memo or ""
|
||||||
|
|
||||||
|
@ -162,53 +166,62 @@ class EclairWallet(Wallet):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
async with httpx.AsyncClient() as client:
|
try:
|
||||||
r = await client.post(
|
async with httpx.AsyncClient() as client:
|
||||||
f"{self.url}/getreceivedinfo",
|
r = await client.post(
|
||||||
headers=self.auth,
|
f"{self.url}/getreceivedinfo",
|
||||||
data={"paymentHash": checking_id},
|
headers=self.auth,
|
||||||
)
|
data={"paymentHash": checking_id},
|
||||||
data = r.json()
|
)
|
||||||
|
|
||||||
if r.is_error or "error" in data or data.get("status") is None:
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if r.is_error or "error" in data or data.get("status") is None:
|
||||||
|
raise Exception("error in eclair response")
|
||||||
|
|
||||||
|
statuses = {
|
||||||
|
"received": True,
|
||||||
|
"expired": False,
|
||||||
|
"pending": None,
|
||||||
|
}
|
||||||
|
return PaymentStatus(statuses.get(data["status"]["type"]))
|
||||||
|
except:
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
statuses = {
|
|
||||||
"received": True,
|
|
||||||
"expired": False,
|
|
||||||
"pending": None,
|
|
||||||
}
|
|
||||||
return PaymentStatus(statuses.get(data["status"]["type"]))
|
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
async with httpx.AsyncClient() as client:
|
try:
|
||||||
r = await client.post(
|
async with httpx.AsyncClient() as client:
|
||||||
f"{self.url}/getsentinfo",
|
r = await client.post(
|
||||||
headers=self.auth,
|
f"{self.url}/getsentinfo",
|
||||||
data={"paymentHash": checking_id},
|
headers=self.auth,
|
||||||
timeout=40,
|
data={"paymentHash": checking_id},
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
data = r.json()[-1]
|
||||||
|
|
||||||
|
if r.is_error or "error" in data or data.get("status") is None:
|
||||||
|
raise Exception("error in eclair response")
|
||||||
|
|
||||||
|
fee_msat, preimage = None, None
|
||||||
|
if data["status"]["type"] == "sent":
|
||||||
|
fee_msat = -data["status"]["feesPaid"]
|
||||||
|
preimage = data["status"]["paymentPreimage"]
|
||||||
|
|
||||||
|
statuses = {
|
||||||
|
"sent": True,
|
||||||
|
"failed": False,
|
||||||
|
"pending": None,
|
||||||
|
}
|
||||||
|
return PaymentStatus(
|
||||||
|
statuses.get(data["status"]["type"]), fee_msat, preimage
|
||||||
)
|
)
|
||||||
|
except:
|
||||||
if r.is_error:
|
|
||||||
return PaymentStatus(None)
|
return PaymentStatus(None)
|
||||||
|
|
||||||
data = r.json()[-1]
|
|
||||||
|
|
||||||
if r.is_error or "error" in data or data.get("status") is None:
|
|
||||||
return PaymentStatus(None)
|
|
||||||
|
|
||||||
fee_msat, preimage = None, None
|
|
||||||
if data["status"]["type"] == "sent":
|
|
||||||
fee_msat = -data["status"]["feesPaid"]
|
|
||||||
preimage = data["status"]["paymentPreimage"]
|
|
||||||
|
|
||||||
statuses = {
|
|
||||||
"sent": True,
|
|
||||||
"failed": False,
|
|
||||||
"pending": None,
|
|
||||||
}
|
|
||||||
return PaymentStatus(statuses.get(data["status"]["type"]), fee_msat, preimage)
|
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -41,6 +41,7 @@ class FakeWallet(Wallet):
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {
|
data: Dict = {
|
||||||
"out": False,
|
"out": False,
|
||||||
|
@ -54,6 +55,7 @@ class FakeWallet(Wallet):
|
||||||
"expires": None,
|
"expires": None,
|
||||||
"route": None,
|
"route": None,
|
||||||
}
|
}
|
||||||
|
data["expires"] = kwargs.get("expiry")
|
||||||
data["amount"] = amount * 1000
|
data["amount"] = amount * 1000
|
||||||
data["timestamp"] = datetime.now().timestamp()
|
data["timestamp"] = datetime.now().timestamp()
|
||||||
if description_hash:
|
if description_hash:
|
||||||
|
|
|
@ -59,8 +59,11 @@ class LNbitsWallet(Wallet):
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"out": False, "amount": amount}
|
data: Dict = {"out": False, "amount": amount}
|
||||||
|
if kwargs.get("expiry"):
|
||||||
|
data["expiry"] = kwargs["expiry"]
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = description_hash.hex()
|
data["description_hash"] = description_hash.hex()
|
||||||
if unhashed_description:
|
if unhashed_description:
|
||||||
|
|
|
@ -154,8 +154,11 @@ class LndWallet(Wallet):
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
params: Dict = {"value": amount, "expiry": 600, "private": True}
|
params: Dict = {"value": amount, "private": True}
|
||||||
|
if kwargs.get("expiry"):
|
||||||
|
params["expiry"] = kwargs["expiry"]
|
||||||
if description_hash:
|
if description_hash:
|
||||||
params["description_hash"] = description_hash
|
params["description_hash"] = description_hash
|
||||||
elif unhashed_description:
|
elif unhashed_description:
|
||||||
|
|
|
@ -77,6 +77,8 @@ class LndRestWallet(Wallet):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"value": amount, "private": True}
|
data: Dict = {"value": amount, "private": True}
|
||||||
|
if kwargs.get("expiry"):
|
||||||
|
data["expiry"] = kwargs["expiry"]
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = base64.b64encode(description_hash).decode(
|
data["description_hash"] = base64.b64encode(description_hash).decode(
|
||||||
"ascii"
|
"ascii"
|
||||||
|
|
|
@ -119,6 +119,7 @@ class SparkWallet(Wallet):
|
||||||
label=label,
|
label=label,
|
||||||
description=memo or "",
|
description=memo or "",
|
||||||
exposeprivatechannels=True,
|
exposeprivatechannels=True,
|
||||||
|
expiry=kwargs.get("expiry"),
|
||||||
)
|
)
|
||||||
ok, payment_request, error_message = True, r["bolt11"], ""
|
ok, payment_request, error_message = True, r["bolt11"], ""
|
||||||
except (SparkError, UnknownError) as e:
|
except (SparkError, UnknownError) as e:
|
||||||
|
|
|
@ -94,6 +94,21 @@ async def test_create_internal_invoice(client, inkey_headers_to):
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
|
# check POST /api/v1/payments: invoice with custom expiry
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_invoice_custom_expiry(client, inkey_headers_to):
|
||||||
|
data = await get_random_invoice_data()
|
||||||
|
expiry_seconds = 600 * 6 * 24 * 31 # 31 days in the future
|
||||||
|
data["expiry"] = expiry_seconds
|
||||||
|
response = await client.post(
|
||||||
|
"/api/v1/payments", json=data, headers=inkey_headers_to
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
invoice = response.json()
|
||||||
|
bolt11_invoice = bolt11.decode(invoice["payment_request"])
|
||||||
|
assert bolt11_invoice.expiry == expiry_seconds
|
||||||
|
|
||||||
|
|
||||||
# check POST /api/v1/payments: make payment
|
# check POST /api/v1/payments: make payment
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pay_invoice(client, invoice, adminkey_headers_from):
|
async def test_pay_invoice(client, invoice, adminkey_headers_from):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user