diff --git a/.env.example b/.env.example
index a81f74e6..c6557bde 100644
--- a/.env.example
+++ b/.env.example
@@ -153,6 +153,12 @@ BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRIN
BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
# BREEZ_USE_TRAMPOLINE=true
+# BreezLiquidSdkWallet
+# get your own api key here https://breez.technology/request-api-key/#contact-us-form-sdk
+# or keep the api key empty to use the LNbits key for referrals (API key is not a secret)
+# BREEZ_LIQUID_API_KEY=""
+BREEZ_LIQUID_SEED="MNEMONIC SEED PHRASE"
+# BREEZ_LIQUID_FEE_OFFSET_SAT=50
# Google OAuth Config
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md
index cd943305..1c84585b 100644
--- a/docs/guide/wallets.md
+++ b/docs/guide/wallets.md
@@ -135,6 +135,18 @@ A Greenlight invite code or Greenlight partner certificate/key can be used to re
- `BREEZ_GREENLIGHT_DEVICE_KEY`: /path/to/breezsdk/device.pem or Base64/Hex
- `BREEZ_GREENLIGHT_DEVICE_CERT`: /path/to/breezsdk/device.crt or Base64/Hex
+### Breez Liquid SDK
+
+This funding source leverages the [Breez SDK - Liquid](https://sdk-doc-liquid.breez.technology/) to manage all Lightning payments via submarine swaps on the Liquid network. To get started, simply provide a mnemonic seed phrase. The easiest way to generate one is by using a liquid wallet, such as [Blockstream Green](https://blockstream.com/green/). Once generated, you can copy the seed to your environment variable or enter it in the admin UI.
+
+- `LNBITS_BACKEND_WALLET_CLASS`: **BreezLiquidSdkWallet**
+- `BREEZ_LIQUID_SEED`: ...
+
+Each submarine swap incurs service and on-chain fees. To account for these, you may need to increase the reserve fee in the admin UI by navigating to **Settings -> Funding**, or by setting the following environment variables:
+
+- `LNBITS_RESERVE_FEE_MIN`: ...
+- `LNBITS_RESERVE_FEE_PERCENT`: ...
+
### Cliche Wallet
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
diff --git a/lnbits/core/crud/payments.py b/lnbits/core/crud/payments.py
index c63b10cf..6f1b3cf6 100644
--- a/lnbits/core/crud/payments.py
+++ b/lnbits/core/crud/payments.py
@@ -268,7 +268,7 @@ async def create_payment(
preimage=data.preimage,
expiry=data.expiry,
webhook=data.webhook,
- fee=data.fee,
+ fee=-abs(data.fee),
tag=extra.get("tag", None),
extra=extra,
)
diff --git a/lnbits/core/services/payments.py b/lnbits/core/services/payments.py
index 5687c0ad..46859471 100644
--- a/lnbits/core/services/payments.py
+++ b/lnbits/core/services/payments.py
@@ -300,6 +300,7 @@ async def create_invoice(
memo=memo,
extra=extra,
webhook=webhook,
+ fee=payment_response.fee_msat or 0,
)
payment = await create_payment(
diff --git a/lnbits/settings.py b/lnbits/settings.py
index 7294555f..188f4801 100644
--- a/lnbits/settings.py
+++ b/lnbits/settings.py
@@ -305,7 +305,7 @@ class FeeSettings(LNbitsSettings):
return 0
reserve_min = self.lnbits_reserve_fee_min
reserve_percent = self.lnbits_reserve_fee_percent
- return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
+ return max(int(reserve_min), int(abs(amount_msat) * reserve_percent / 100.0))
class ExchangeProvidersSettings(LNbitsSettings):
@@ -549,6 +549,12 @@ class BreezSdkFundingSource(LNbitsSettings):
breez_use_trampoline: bool = Field(default=True)
+class BreezLiquidSdkFundingSource(LNbitsSettings):
+ breez_liquid_api_key: str | None = Field(default=None)
+ breez_liquid_seed: str | None = Field(default=None)
+ breez_liquid_fee_offset_sat: int = Field(default=50)
+
+
class BoltzFundingSource(LNbitsSettings):
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
boltz_client_macaroon: str | None = Field(default=None)
@@ -618,6 +624,7 @@ class FundingSourcesSettings(
NWCFundingSource,
BreezSdkFundingSource,
StrikeFundingSource,
+ BreezLiquidSdkFundingSource,
):
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
# How long to wait for the payment to be confirmed before returning a pending status
@@ -924,6 +931,7 @@ class SuperUserSettings(LNbitsSettings):
"BoltzWallet",
"BlinkWallet",
"BreezSdkWallet",
+ "BreezLiquidSdkWallet",
"CoreLightningRestWallet",
"CoreLightningWallet",
"EclairWallet",
diff --git a/lnbits/static/bundle-components.min.js b/lnbits/static/bundle-components.min.js
index f126b954..f1a49c3c 100644
--- a/lnbits/static/bundle-components.min.js
+++ b/lnbits/static/bundle-components.min.js
@@ -1 +1 @@
-window.app.component("lnbits-funding-sources",{template:"#lnbits-funding-sources",mixins:[window.windowMixin],props:["form-data","allowed-funding-sources"],methods:{getFundingSourceLabel(t){const e=this.rawFundingSources.find((e=>e[0]===t));return e?e[1]:t},showQRValue(t){this.qrValue=t,this.showQRDialog=!0}},computed:{fundingSources(){let t=[];for(const[e,i,s]of this.rawFundingSources){const i={};if(null!==s)for(let[t,e]of Object.entries(s))i[t]="string"==typeof e?{label:e,value:null}:e||{};t.push([e,i])}return new Map(t)},sortedAllowedFundingSources(){return this.allowedFundingSources.sort()}},data:()=>({hideInput:!0,showQRDialog:!1,qrValue:"",rawFundingSources:[["VoidWallet","Void Wallet",null],["FakeWallet","Fake Wallet",{fake_wallet_secret:"Secret",lnbits_denomination:'"sats" or 3 Letter Custom Denomination'}],["CoreLightningWallet","Core Lightning",{corelightning_rpc:"Endpoint",corelightning_pay_command:"Custom Pay Command"}],["CoreLightningRestWallet","Core Lightning Rest",{corelightning_rest_url:"Endpoint",corelightning_rest_cert:"Certificate",corelightning_rest_macaroon:"Macaroon"}],["LndRestWallet","Lightning Network Daemon (LND Rest)",{lnd_rest_endpoint:"Endpoint",lnd_rest_cert:"Certificate",lnd_rest_macaroon:"Macaroon",lnd_rest_macaroon_encrypted:"Encrypted Macaroon",lnd_rest_route_hints:"Enable Route Hints",lnd_rest_allow_self_payment:"Allow Self Payment"}],["LndWallet","Lightning Network Daemon (LND)",{lnd_grpc_endpoint:"Endpoint",lnd_grpc_cert:"Certificate",lnd_grpc_port:"Port",lnd_grpc_admin_macaroon:"Admin Macaroon",lnd_grpc_macaroon_encrypted:"Encrypted Macaroon"}],["LnTipsWallet","LN.Tips",{lntips_api_endpoint:"Endpoint",lntips_api_key:"API Key"}],["LNPayWallet","LN Pay",{lnpay_api_endpoint:"Endpoint",lnpay_api_key:"API Key",lnpay_wallet_key:"Wallet Key"}],["EclairWallet","Eclair (ACINQ)",{eclair_url:"URL",eclair_pass:"Password"}],["LNbitsWallet","LNbits",{lnbits_endpoint:"Endpoint",lnbits_key:"Admin Key"}],["BlinkWallet","Blink",{blink_api_endpoint:"Endpoint",blink_ws_endpoint:"WebSocket",blink_token:"Key"}],["AlbyWallet","Alby",{alby_api_endpoint:"Endpoint",alby_access_token:"Key"}],["BoltzWallet","Boltz",{boltz_client_endpoint:"Endpoint",boltz_client_macaroon:"Admin Macaroon path or hex",boltz_client_cert:"Certificate path or hex",boltz_client_wallet:"Wallet Name",boltz_client_password:"Wallet Password (can be empty)",boltz_mnemonic:{label:"Liquid mnemonic (copy into greenwallet)",readonly:!0,copy:!0,qrcode:!0}}],["ZBDWallet","ZBD",{zbd_api_endpoint:"Endpoint",zbd_api_key:"Key"}],["PhoenixdWallet","Phoenixd",{phoenixd_api_endpoint:"Endpoint",phoenixd_api_password:"Key"}],["OpenNodeWallet","OpenNode",{opennode_api_endpoint:"Endpoint",opennode_key:"Key"}],["ClicheWallet","Cliche (NBD)",{cliche_endpoint:"Endpoint"}],["SparkWallet","Spark",{spark_url:"Endpoint",spark_token:"Token"}],["NWCWallet","Nostr Wallet Connect",{nwc_pairing_url:"Pairing URL"}],["BreezSdkWallet","Breez SDK",{breez_api_key:"Breez API Key",breez_greenlight_seed:"Greenlight Seed",breez_greenlight_device_key:"Greenlight Device Key",breez_greenlight_device_cert:"Greenlight Device Cert",breez_greenlight_invite_code:"Greenlight Invite Code"}],["StrikeWallet","Strike (alpha)",{strike_api_endpoint:"API Endpoint",strike_api_key:"API Key"}]]})}),window.app.component("lnbits-extension-settings-form",{name:"lnbits-extension-settings-form",template:"#lnbits-extension-settings-form",props:["options","adminkey","endpoint"],methods:{async updateSettings(){if(!this.settings)return Quasar.Notify.create({message:"No settings to update",type:"negative"});try{const{data:t}=await LNbits.api.request("PUT",this.endpoint,this.adminkey,this.settings);this.settings=t}catch(t){LNbits.utils.notifyApiError(t)}},async getSettings(){try{const{data:t}=await LNbits.api.request("GET",this.endpoint,this.adminkey);this.settings=t}catch(t){LNbits.utils.notifyApiError(t)}},async resetSettings(){LNbits.utils.confirmDialog("Are you sure you want to reset the settings?").onOk((async()=>{try{await LNbits.api.request("DELETE",this.endpoint,this.adminkey),await this.getSettings()}catch(t){LNbits.utils.notifyApiError(t)}}))}},async created(){await this.getSettings()},data:()=>({settings:void 0})}),window.app.component("lnbits-extension-settings-btn-dialog",{template:"#lnbits-extension-settings-btn-dialog",name:"lnbits-extension-settings-btn-dialog",props:["options","adminkey","endpoint"],data:()=>({show:!1})}),window.app.component("payment-list",{name:"payment-list",template:"#payment-list",props:["update","lazy","wallet"],mixins:[window.windowMixin],data(){return{denomination:LNBITS_DENOMINATION,payments:[],paymentsTable:{columns:[{name:"time",align:"left",label:this.$t("memo")+"/"+this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount")+" ("+LNBITS_DENOMINATION+")",field:"sat",sortable:!0}],pagination:{rowsPerPage:10,page:1,sortBy:"time",descending:!0,rowsNumber:10},search:"",filter:{"status[ne]":"failed"},loading:!1},searchDate:{from:null,to:null},searchStatus:{success:!0,pending:!0,failed:!1,incoming:!0,outgoing:!0},exportTagName:"",exportPaymentTagList:[],paymentsCSV:{columns:[{name:"pending",align:"left",label:"Pending",field:"pending"},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"},{name:"time",align:"left",label:this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount")+" ("+LNBITS_DENOMINATION+")",field:"sat",sortable:!0},{name:"fee",align:"right",label:this.$t("fee")+" (m"+LNBITS_DENOMINATION+")",field:"fee"},{name:"tag",align:"right",label:this.$t("tag"),field:"tag"},{name:"payment_hash",align:"right",label:this.$t("payment_hash"),field:"payment_hash"},{name:"payment_proof",align:"right",label:this.$t("payment_proof"),field:"payment_proof"},{name:"webhook",align:"right",label:this.$t("webhook"),field:"webhook"},{name:"fiat_currency",align:"right",label:"Fiat Currency",field:t=>t.extra.wallet_fiat_currency},{name:"fiat_amount",align:"right",label:"Fiat Amount",field:t=>t.extra.wallet_fiat_amount}],loading:!1}}},computed:{currentWallet(){return this.wallet||this.g.wallet},filteredPayments(){const t=this.paymentsTable.search;return t&&""!==t?LNbits.utils.search(this.payments,t):this.payments},paymentsOmitter(){return this.$q.screen.lt.md&&this.mobileSimple?this.payments.length>0?[this.payments[0]]:[]:this.payments},pendingPaymentsExist(){return-1!==this.payments.findIndex((t=>t.pending))}},methods:{searchByDate(){"string"==typeof this.searchDate&&(this.searchDate={from:this.searchDate,to:this.searchDate}),this.searchDate.from&&(this.paymentsTable.filter["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(this.paymentsTable.filter["time[le]"]=this.searchDate.to+"T23:59:59"),this.fetchPayments()},clearDateSeach(){this.searchDate={from:null,to:null},delete this.paymentsTable.filter["time[ge]"],delete this.paymentsTable.filter["time[le]"],this.fetchPayments()},fetchPayments(t){this.$emit("filter-changed",{...this.paymentsTable.filter});const e=LNbits.utils.prepareFilterQuery(this.paymentsTable,t);return LNbits.api.getPayments(this.currentWallet,e).then((t=>{this.paymentsTable.loading=!1,this.paymentsTable.pagination.rowsNumber=t.data.total,this.payments=t.data.data.map((t=>LNbits.map.payment(t)))})).catch((t=>{this.paymentsTable.loading=!1,g.user.admin?this.fetchPaymentsAsAdmin(this.currentWallet.id,e):LNbits.utils.notifyApiError(t)}))},fetchPaymentsAsAdmin(t,e){return e=(e||"")+"&wallet_id="+t,LNbits.api.request("GET","/api/v1/payments/all/paginated?"+e).then((t=>{this.paymentsTable.loading=!1,this.paymentsTable.pagination.rowsNumber=t.data.total,this.payments=t.data.data.map((t=>LNbits.map.payment(t)))})).catch((t=>{this.paymentsTable.loading=!1,LNbits.utils.notifyApiError(t)}))},checkPayment(t){LNbits.api.getPayment(this.g.wallet,t).then((t=>{this.update=!this.update,"success"==t.data.status&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")}),"pending"==t.data.status&&Quasar.Notify.create({type:"info",message:this.$t("payment_pending")})})).catch(LNbits.utils.notifyApiError)},paymentTableRowKey:t=>t.payment_hash+t.amount,exportCSV(t=!1){const e=this.paymentsTable.pagination,i={sortby:e.sortBy??"time",direction:e.descending?"desc":"asc"},s=new URLSearchParams(i);LNbits.api.getPayments(this.g.wallet,s).then((e=>{let i=e.data.data.map(LNbits.map.payment),s=this.paymentsCSV.columns;if(t){this.exportPaymentTagList.length&&(i=i.filter((t=>this.exportPaymentTagList.includes(t.tag))));const t=Object.keys(i.reduce(((t,e)=>({...t,...e.details})),{})).map((t=>({name:t,align:"right",label:t.charAt(0).toUpperCase()+t.slice(1).replace(/([A-Z])/g," $1"),field:e=>e.details[t],format:t=>"object"==typeof t?JSON.stringify(t):t})));s=this.paymentsCSV.columns.concat(t)}LNbits.utils.exportCSV(s,i,this.g.wallet.name+"-payments")}))},addFilterTag(){if(!this.exportTagName)return;const t=this.exportTagName.trim();this.exportPaymentTagList=this.exportPaymentTagList.filter((e=>e!==t)),this.exportPaymentTagList.push(t),this.exportTagName=""},removeExportTag(t){this.exportPaymentTagList=this.exportPaymentTagList.filter((e=>e!==t))},formatCurrency(t,e){try{return LNbits.utils.formatCurrency(t,e)}catch(e){return console.error(e),`${t} ???`}},handleFilterChanged(){const{success:t,pending:e,failed:i,incoming:s,outgoing:n}=this.searchStatus;delete this.paymentsTable.filter["status[ne]"],delete this.paymentsTable.filter["status[eq]"],t&&e&&i||(t&&e?this.paymentsTable.filter["status[ne]"]="failed":t&&i?this.paymentsTable.filter["status[ne]"]="pending":i&&e?this.paymentsTable.filter["status[ne]"]="success":t?this.paymentsTable.filter["status[eq]"]="success":e?this.paymentsTable.filter["status[eq]"]="pending":i&&(this.paymentsTable.filter["status[eq]"]="failed")),delete this.paymentsTable.filter["amount[ge]"],delete this.paymentsTable.filter["amount[le]"],s&&n||(s?this.paymentsTable.filter["amount[ge]"]=0:n&&(this.paymentsTable.filter["amount[le]"]=0))}},watch:{"paymentsTable.search":{handler(){const t={};this.paymentsTable.search&&(t.search=this.paymentsTable.search),this.fetchPayments()}},lazy(t){!0===t&&this.fetchPayments()},update(){this.fetchPayments()},"g.updatePayments"(){this.fetchPayments()},"g.wallet":{handler(t){this.fetchPayments()},deep:!0}},created(){void 0===this.lazy&&this.fetchPayments()}}),window.app.component(QrcodeVue),window.app.component("lnbits-extension-rating",{template:"#lnbits-extension-rating",name:"lnbits-extension-rating",props:["rating"]}),window.app.component("lnbits-fsat",{template:"{{ fsat }}",props:{amount:{type:Number,default:0}},computed:{fsat(){return LNbits.utils.formatSat(this.amount)}}}),window.app.component("lnbits-wallet-list",{mixins:[window.windowMixin],template:"#lnbits-wallet-list",props:["balance"],data:()=>({activeWallet:null,balance:0,showForm:!1,walletName:"",LNBITS_DENOMINATION:LNBITS_DENOMINATION}),methods:{createWallet(){LNbits.api.createWallet(this.g.user.wallets[0],this.walletName)}},created(){document.addEventListener("updateWalletBalance",this.updateWalletBalance)}}),window.app.component("lnbits-extension-list",{mixins:[window.windowMixin],template:"#lnbits-extension-list",data:()=>({extensions:[],searchTerm:""}),watch:{"g.user.extensions":{handler(t){this.loadExtensions()},deep:!0}},computed:{userExtensions(){return this.updateUserExtensions(this.searchTerm)}},methods:{async loadExtensions(){try{const{data:t}=await LNbits.api.request("GET","/api/v1/extension");this.extensions=t.map((t=>LNbits.map.extension(t))).sort(((t,e)=>t.name.localeCompare(e.name)))}catch(t){LNbits.utils.notifyApiError(t)}},updateUserExtensions(t){const e=window.location.pathname,i=this.g.user.extensions;return this.extensions.filter((t=>i.includes(t.code))).filter((e=>!t||`${e.code} ${e.name} ${e.short_description} ${e.url}`.toLocaleLowerCase().includes(t.toLocaleLowerCase()))).map((t=>(t.isActive=e.startsWith(t.url),t)))}},async created(){await this.loadExtensions()}}),window.app.component("lnbits-manage",{mixins:[window.windowMixin],template:"#lnbits-manage",props:["showAdmin","showNode","showExtensions","showUsers","showAudit"],methods:{isActive:t=>window.location.pathname===t},data:()=>({extensions:[]})}),window.app.component("lnbits-payment-details",{mixins:[window.windowMixin],template:"#lnbits-payment-details",props:["payment"],mixins:[window.windowMixin],data:()=>({LNBITS_DENOMINATION:LNBITS_DENOMINATION}),computed:{hasPreimage(){return this.payment.preimage&&"0000000000000000000000000000000000000000000000000000000000000000"!==this.payment.preimage},hasExpiry(){return!!this.payment.expiry},hasSuccessAction(){return this.hasPreimage&&this.payment.extra&&this.payment.extra.success_action},webhookStatusColor(){return this.payment.webhook_status>=300||this.payment.webhook_status<0?"red-10":this.payment.webhook_status?"green-10":"cyan-7"},webhookStatusText(){return this.payment.webhook_status?this.payment.webhook_status:"not sent yet"},hasTag(){return this.payment.extra&&!!this.payment.extra.tag},extras(){if(!this.payment.extra)return[];let t=_.omit(this.payment.extra,["tag","success_action"]);return Object.keys(t).map((e=>({key:e,value:t[e]})))}}}),window.app.component("lnbits-lnurlpay-success-action",{mixins:[window.windowMixin],template:"#lnbits-lnurlpay-success-action",props:["payment","success_action"],data(){return{decryptedValue:this.success_action.ciphertext}},mounted(){if("aes"!==this.success_action.tag)return null;decryptLnurlPayAES(this.success_action,this.payment.preimage).then((t=>{this.decryptedValue=t}))}}),window.app.component("lnbits-qrcode",{mixins:[window.windowMixin],template:"#lnbits-qrcode",components:{QrcodeVue:QrcodeVue},props:{value:{type:String,required:!0},options:Object},data:()=>({custom:{margin:3,width:350,size:350,logo:LNBITS_QR_LOGO}}),created(){this.custom={...this.custom,...this.options}}}),window.app.component("lnbits-notifications-btn",{template:"#lnbits-notifications-btn",mixins:[window.windowMixin],props:["pubkey"],data:()=>({isSupported:!1,isSubscribed:!1,isPermissionGranted:!1,isPermissionDenied:!1}),methods:{urlB64ToUint8Array(t){const e=(t+"=".repeat((4-t.length%4)%4)).replace(/\-/g,"+").replace(/_/g,"/"),i=atob(e),s=new Uint8Array(i.length);for(let t=0;te!==t)),this.$q.localStorage.set("lnbits.webpush.subscribedUsers",JSON.stringify(e))},isUserSubscribed(t){return(JSON.parse(this.$q.localStorage.getItem("lnbits.webpush.subscribedUsers"))||[]).includes(t)},subscribe(){this.isSupported&&!this.isPermissionDenied&&(Notification.requestPermission().then((t=>{this.isPermissionGranted="granted"===t,this.isPermissionDenied="denied"===t})).catch(console.log),navigator.serviceWorker.ready.then((t=>{navigator.serviceWorker.getRegistration().then((t=>{t.pushManager.getSubscription().then((e=>{if(null===e||!this.isUserSubscribed(this.g.user.id)){const e={applicationServerKey:this.urlB64ToUint8Array(this.pubkey),userVisibleOnly:!0};t.pushManager.subscribe(e).then((t=>{LNbits.api.request("POST","/api/v1/webpush",null,{subscription:JSON.stringify(t)}).then((t=>{this.saveUserSubscribed(t.data.user),this.isSubscribed=!0})).catch(LNbits.utils.notifyApiError)}))}})).catch(console.log)}))})))},unsubscribe(){navigator.serviceWorker.ready.then((t=>{t.pushManager.getSubscription().then((t=>{t&&LNbits.api.request("DELETE","/api/v1/webpush?endpoint="+btoa(t.endpoint),null).then((()=>{this.removeUserSubscribed(this.g.user.id),this.isSubscribed=!1})).catch(LNbits.utils.notifyApiError)}))})).catch(console.log)},checkSupported(){let t="https:"===window.location.protocol,e="serviceWorker"in navigator,i="Notification"in window,s="PushManager"in window;return this.isSupported=t&&e&&i&&s,this.isSupported||console.log("Notifications disabled because requirements are not met:",{HTTPS:t,"Service Worker API":e,"Notification API":i,"Push API":s}),this.isSupported},async updateSubscriptionStatus(){await navigator.serviceWorker.ready.then((t=>{t.pushManager.getSubscription().then((t=>{this.isSubscribed=!!t&&this.isUserSubscribed(this.g.user.id)}))})).catch(console.log)}},created(){this.isPermissionDenied="denied"===Notification.permission,this.checkSupported()&&this.updateSubscriptionStatus()}}),window.app.component("lnbits-dynamic-fields",{template:"#lnbits-dynamic-fields",mixins:[window.windowMixin],props:["options","modelValue"],data:()=>({formData:null,rules:[t=>!!t||"Field is required"]}),methods:{applyRules(t){return t?this.rules:[]},buildData(t,e={}){return t.reduce(((t,i)=>(i.options?.length?t[i.name]=this.buildData(i.options,e[i.name]):t[i.name]=e[i.name]??i.default,t)),{})},handleValueChanged(){this.$emit("update:model-value",this.formData)}},created(){this.formData=this.buildData(this.options,this.modelValue)}}),window.app.component("lnbits-dynamic-chips",{template:"#lnbits-dynamic-chips",mixins:[window.windowMixin],props:["modelValue"],data:()=>({chip:"",chips:[]}),methods:{addChip(){this.chip&&(this.chips.push(this.chip),this.chip="",this.$emit("update:model-value",this.chips.join(",")))},removeChip(t){this.chips.splice(t,1),this.$emit("update:model-value",this.chips.join(","))}},created(){"string"==typeof this.modelValue?this.chips=this.modelValue.split(","):this.chips=[...this.modelValue]}}),window.app.component("lnbits-update-balance",{template:"#lnbits-update-balance",mixins:[window.windowMixin],props:["wallet_id","small_btn"],computed:{denomination:()=>LNBITS_DENOMINATION,admin:()=>user.super_user},data:()=>({credit:0}),methods:{updateBalance(t){LNbits.api.updateBalance(t.value,this.wallet_id).then((e=>{if(!0!==e.data.success)throw new Error(e.data);credit=parseInt(t.value),Quasar.Notify.create({type:"positive",message:this.$t("credit_ok",{amount:credit}),icon:null}),this.credit=0,t.value=0,t.set()})).catch(LNbits.utils.notifyApiError)}}}),window.app.component("user-id-only",{template:"#user-id-only",mixins:[window.windowMixin],props:{allowed_new_users:Boolean,authAction:String,authMethod:String,usr:String,wallet:String},data(){return{user:this.usr,walletName:this.wallet}},methods:{showLogin(t){this.$emit("show-login",t)},showRegister(t){this.$emit("show-register",t)},loginUsr(){this.$emit("update:usr",this.user),this.$emit("login-usr")},createWallet(){this.$emit("update:wallet",this.walletName),this.$emit("create-wallet")}},computed:{showInstantLogin(){return"username-password"!==this.authMethod||"register"!==this.authAction}},created(){}}),window.app.component("username-password",{template:"#username-password",mixins:[window.windowMixin],props:{allowed_new_users:Boolean,authMethods:Array,authAction:String,username:String,password_1:String,password_2:String,resetKey:String},data(){return{oauth:["nostr-auth-nip98","google-auth","github-auth","keycloak-auth"],username:this.userName,password:this.password_1,passwordRepeat:this.password_2,reset_key:this.resetKey,keycloakOrg:LNBITS_AUTH_KEYCLOAK_ORG||"Keycloak",keycloakIcon:LNBITS_AUTH_KEYCLOAK_ICON}},methods:{login(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("login")},register(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("register")},reset(){this.$emit("update:resetKey",this.reset_key),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("reset")},validateUsername:t=>new RegExp("^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$").test(t),async signInWithNostr(){try{const t=await this.createNostrToken();if(!t)return;resp=await LNbits.api.loginByProvider("nostr",{Authorization:t},{}),window.location.href="/wallet"}catch(t){console.warn(t);const e=t?.response?.data?.detail||`${t}`;Quasar.Notify.create({type:"negative",message:"Failed to sign in with Nostr.",caption:e})}},async createNostrToken(){try{if(!window.nostr?.signEvent)return void Quasar.Notify.create({type:"negative",message:"No Nostr signing app detected.",caption:'Is "window.nostr" present?'});const t=`${window.location}nostr`,e="POST",i=await NostrTools.nip98.getToken(t,e,(t=>async function(t){try{const{data:e}=await LNbits.api.getServerHealth();return t.created_at=e.server_time,await window.nostr.signEvent(t)}catch(t){console.error(t),Quasar.Notify.create({type:"negative",message:"Failed to sign nostr event.",caption:`${t}`})}}(t)),!0);if(!await NostrTools.nip98.validateToken(i,t,e))throw new Error("Invalid signed token!");return i}catch(t){console.warn(t),Quasar.Notify.create({type:"negative",message:"Failed create Nostr event.",caption:`${t}`})}}},computed:{showOauth(){return this.oauth.some((t=>this.authMethods.includes(t)))}},created(){}}),window.app.component("separator-text",{template:"#separator-text",props:{text:String,uppercase:{type:Boolean,default:!1},color:{type:String,default:"grey"}}});const DynamicComponent={props:{fetchUrl:{type:String,required:!0},scripts:{type:Array,default:()=>[]}},data:()=>({keys:[]}),async mounted(){await this.loadDynamicContent()},methods:{loadScript:async t=>new Promise(((e,i)=>{const s=document.querySelector(`script[src="${t}"]`);s&&s.remove();const n=document.createElement("script");n.src=t,n.async=!0,n.onload=e,n.onerror=()=>i(new Error(`Failed to load script: ${t}`)),document.head.appendChild(n)})),async loadDynamicContent(){this.$q.loading.show();try{const t=this.fetchUrl.split("#")[0],e=await fetch(t,{credentials:"include",headers:{Accept:"text/html","X-Requested-With":"XMLHttpRequest"}}),i=await e.text(),s=new DOMParser,n=s.parseFromString(i,"text/html").querySelector("#window-vars-script");n&&new Function(n.innerHTML)(),await this.loadScript("/static/js/base.js");for(const t of this.scripts)await this.loadScript(t);const a=this.$router.currentRoute.value.meta.previousRouteName;a&&window.app._context.components[a]&&delete window.app._context.components[a];const o=`${this.$route.name}PageLogic`,r=window[o];if(!r)throw new Error(`Component logic '${o}' not found. Ensure it is defined in the script.`);r.mixins=r.mixins||[],window.windowMixin&&r.mixins.push(window.windowMixin),window.app.component(this.$route.name,{...r,template:i}),delete window[o],this.$forceUpdate()}catch(t){console.error("Error loading dynamic content:",t)}finally{this.$q.loading.hide()}}},watch:{$route(t,e){routes.map((t=>t.name)).includes(t.name)?(this.$router.currentRoute.value.meta.previousRouteName=e.name,this.loadDynamicContent()):console.log(`Route '${t.name}' is not valid. Leave this one to Fastapi.`)}},template:'\n \n '},routes=[{path:"/wallet",name:"Wallet",component:DynamicComponent,props:t=>{let e="/wallet";if(Object.keys(t.query).length>0){e+="?";for(const[i,s]of Object.entries(t.query))e+=`${i}=${s}&`;e=e.slice(0,-1)}return{fetchUrl:e,scripts:["/static/js/wallet.js"]}}},{path:"/admin",name:"Admin",component:DynamicComponent,props:{fetchUrl:"/admin",scripts:["/static/js/admin.js"]}},{path:"/users",name:"Users",component:DynamicComponent,props:{fetchUrl:"/users",scripts:["/static/js/users.js"]}},{path:"/audit",name:"Audit",component:DynamicComponent,props:{fetchUrl:"/audit",scripts:["/static/js/audit.js"]}},{path:"/payments",name:"Payments",component:DynamicComponent,props:{fetchUrl:"/payments",scripts:["/static/js/payments.js"]}},{path:"/extensions",name:"Extensions",component:DynamicComponent,props:{fetchUrl:"/extensions",scripts:["/static/js/extensions.js"]}},{path:"/account",name:"Account",component:DynamicComponent,props:{fetchUrl:"/account",scripts:["/static/js/account.js"]}},{path:"/wallets",name:"Wallets",component:DynamicComponent,props:{fetchUrl:"/wallets",scripts:["/static/js/wallets.js"]}},{path:"/node",name:"Node",component:DynamicComponent,props:{fetchUrl:"/node",scripts:["/static/js/node.js"]}}];window.router=VueRouter.createRouter({history:VueRouter.createWebHistory(),routes:routes}),window.app.mixin({computed:{isVueRoute(){const t=window.location.pathname,e=window.router.resolve(t);return e?.matched?.length>0}}}),window.app.use(VueQrcodeReader),window.app.use(Quasar,{config:{loading:{spinner:Quasar.QSpinnerBars}}}),window.app.use(window.i18n),window.app.provide("g",g),window.app.use(window.router),window.app.component("DynamicComponent",DynamicComponent),window.app.mount("#vue");
+window.app.component("lnbits-funding-sources",{template:"#lnbits-funding-sources",mixins:[window.windowMixin],props:["form-data","allowed-funding-sources"],methods:{getFundingSourceLabel(t){const e=this.rawFundingSources.find((e=>e[0]===t));return e?e[1]:t},showQRValue(t){this.qrValue=t,this.showQRDialog=!0}},computed:{fundingSources(){let t=[];for(const[e,i,s]of this.rawFundingSources){const i={};if(null!==s)for(let[t,e]of Object.entries(s))i[t]="string"==typeof e?{label:e,value:null}:e||{};t.push([e,i])}return new Map(t)},sortedAllowedFundingSources(){return this.allowedFundingSources.sort()}},data:()=>({hideInput:!0,showQRDialog:!1,qrValue:"",rawFundingSources:[["VoidWallet","Void Wallet",null],["FakeWallet","Fake Wallet",{fake_wallet_secret:"Secret",lnbits_denomination:'"sats" or 3 Letter Custom Denomination'}],["CoreLightningWallet","Core Lightning",{corelightning_rpc:"Endpoint",corelightning_pay_command:"Custom Pay Command"}],["CoreLightningRestWallet","Core Lightning Rest",{corelightning_rest_url:"Endpoint",corelightning_rest_cert:"Certificate",corelightning_rest_macaroon:"Macaroon"}],["LndRestWallet","Lightning Network Daemon (LND Rest)",{lnd_rest_endpoint:"Endpoint",lnd_rest_cert:"Certificate",lnd_rest_macaroon:"Macaroon",lnd_rest_macaroon_encrypted:"Encrypted Macaroon",lnd_rest_route_hints:"Enable Route Hints",lnd_rest_allow_self_payment:"Allow Self Payment"}],["LndWallet","Lightning Network Daemon (LND)",{lnd_grpc_endpoint:"Endpoint",lnd_grpc_cert:"Certificate",lnd_grpc_port:"Port",lnd_grpc_admin_macaroon:"Admin Macaroon",lnd_grpc_macaroon_encrypted:"Encrypted Macaroon"}],["LnTipsWallet","LN.Tips",{lntips_api_endpoint:"Endpoint",lntips_api_key:"API Key"}],["LNPayWallet","LN Pay",{lnpay_api_endpoint:"Endpoint",lnpay_api_key:"API Key",lnpay_wallet_key:"Wallet Key"}],["EclairWallet","Eclair (ACINQ)",{eclair_url:"URL",eclair_pass:"Password"}],["LNbitsWallet","LNbits",{lnbits_endpoint:"Endpoint",lnbits_key:"Admin Key"}],["BlinkWallet","Blink",{blink_api_endpoint:"Endpoint",blink_ws_endpoint:"WebSocket",blink_token:"Key"}],["AlbyWallet","Alby",{alby_api_endpoint:"Endpoint",alby_access_token:"Key"}],["BoltzWallet","Boltz",{boltz_client_endpoint:"Endpoint",boltz_client_macaroon:"Admin Macaroon path or hex",boltz_client_cert:"Certificate path or hex",boltz_client_wallet:"Wallet Name",boltz_client_password:"Wallet Password (can be empty)",boltz_mnemonic:{label:"Liquid mnemonic (copy into greenwallet)",readonly:!0,copy:!0,qrcode:!0}}],["ZBDWallet","ZBD",{zbd_api_endpoint:"Endpoint",zbd_api_key:"Key"}],["PhoenixdWallet","Phoenixd",{phoenixd_api_endpoint:"Endpoint",phoenixd_api_password:"Key"}],["OpenNodeWallet","OpenNode",{opennode_api_endpoint:"Endpoint",opennode_key:"Key"}],["ClicheWallet","Cliche (NBD)",{cliche_endpoint:"Endpoint"}],["SparkWallet","Spark",{spark_url:"Endpoint",spark_token:"Token"}],["NWCWallet","Nostr Wallet Connect",{nwc_pairing_url:"Pairing URL"}],["BreezSdkWallet","Breez SDK",{breez_api_key:"Breez API Key",breez_greenlight_seed:"Greenlight Seed",breez_greenlight_device_key:"Greenlight Device Key",breez_greenlight_device_cert:"Greenlight Device Cert",breez_greenlight_invite_code:"Greenlight Invite Code"}],["StrikeWallet","Strike (alpha)",{strike_api_endpoint:"API Endpoint",strike_api_key:"API Key"}],["BreezLiquidSdkWallet","Breez Liquid SDK",{breez_liquid_api_key:"Breez API Key (can be empty)",breez_liquid_seed:"Liquid seed phrase",breez_liquid_fee_offset_sat:"Offset amount in sats to increase fee limit"}]]})}),window.app.component("lnbits-extension-settings-form",{name:"lnbits-extension-settings-form",template:"#lnbits-extension-settings-form",props:["options","adminkey","endpoint"],methods:{async updateSettings(){if(!this.settings)return Quasar.Notify.create({message:"No settings to update",type:"negative"});try{const{data:t}=await LNbits.api.request("PUT",this.endpoint,this.adminkey,this.settings);this.settings=t}catch(t){LNbits.utils.notifyApiError(t)}},async getSettings(){try{const{data:t}=await LNbits.api.request("GET",this.endpoint,this.adminkey);this.settings=t}catch(t){LNbits.utils.notifyApiError(t)}},async resetSettings(){LNbits.utils.confirmDialog("Are you sure you want to reset the settings?").onOk((async()=>{try{await LNbits.api.request("DELETE",this.endpoint,this.adminkey),await this.getSettings()}catch(t){LNbits.utils.notifyApiError(t)}}))}},async created(){await this.getSettings()},data:()=>({settings:void 0})}),window.app.component("lnbits-extension-settings-btn-dialog",{template:"#lnbits-extension-settings-btn-dialog",name:"lnbits-extension-settings-btn-dialog",props:["options","adminkey","endpoint"],data:()=>({show:!1})}),window.app.component("payment-list",{name:"payment-list",template:"#payment-list",props:["update","lazy","wallet"],mixins:[window.windowMixin],data(){return{denomination:LNBITS_DENOMINATION,payments:[],paymentsTable:{columns:[{name:"time",align:"left",label:this.$t("memo")+"/"+this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount")+" ("+LNBITS_DENOMINATION+")",field:"sat",sortable:!0}],pagination:{rowsPerPage:10,page:1,sortBy:"time",descending:!0,rowsNumber:10},search:"",filter:{"status[ne]":"failed"},loading:!1},searchDate:{from:null,to:null},searchStatus:{success:!0,pending:!0,failed:!1,incoming:!0,outgoing:!0},exportTagName:"",exportPaymentTagList:[],paymentsCSV:{columns:[{name:"pending",align:"left",label:"Pending",field:"pending"},{name:"memo",align:"left",label:this.$t("memo"),field:"memo"},{name:"time",align:"left",label:this.$t("date"),field:"date",sortable:!0},{name:"amount",align:"right",label:this.$t("amount")+" ("+LNBITS_DENOMINATION+")",field:"sat",sortable:!0},{name:"fee",align:"right",label:this.$t("fee")+" (m"+LNBITS_DENOMINATION+")",field:"fee"},{name:"tag",align:"right",label:this.$t("tag"),field:"tag"},{name:"payment_hash",align:"right",label:this.$t("payment_hash"),field:"payment_hash"},{name:"payment_proof",align:"right",label:this.$t("payment_proof"),field:"payment_proof"},{name:"webhook",align:"right",label:this.$t("webhook"),field:"webhook"},{name:"fiat_currency",align:"right",label:"Fiat Currency",field:t=>t.extra.wallet_fiat_currency},{name:"fiat_amount",align:"right",label:"Fiat Amount",field:t=>t.extra.wallet_fiat_amount}],loading:!1}}},computed:{currentWallet(){return this.wallet||this.g.wallet},filteredPayments(){const t=this.paymentsTable.search;return t&&""!==t?LNbits.utils.search(this.payments,t):this.payments},paymentsOmitter(){return this.$q.screen.lt.md&&this.mobileSimple?this.payments.length>0?[this.payments[0]]:[]:this.payments},pendingPaymentsExist(){return-1!==this.payments.findIndex((t=>t.pending))}},methods:{searchByDate(){"string"==typeof this.searchDate&&(this.searchDate={from:this.searchDate,to:this.searchDate}),this.searchDate.from&&(this.paymentsTable.filter["time[ge]"]=this.searchDate.from+"T00:00:00"),this.searchDate.to&&(this.paymentsTable.filter["time[le]"]=this.searchDate.to+"T23:59:59"),this.fetchPayments()},clearDateSeach(){this.searchDate={from:null,to:null},delete this.paymentsTable.filter["time[ge]"],delete this.paymentsTable.filter["time[le]"],this.fetchPayments()},fetchPayments(t){this.$emit("filter-changed",{...this.paymentsTable.filter});const e=LNbits.utils.prepareFilterQuery(this.paymentsTable,t);return LNbits.api.getPayments(this.currentWallet,e).then((t=>{this.paymentsTable.loading=!1,this.paymentsTable.pagination.rowsNumber=t.data.total,this.payments=t.data.data.map((t=>LNbits.map.payment(t)))})).catch((t=>{this.paymentsTable.loading=!1,g.user.admin?this.fetchPaymentsAsAdmin(this.currentWallet.id,e):LNbits.utils.notifyApiError(t)}))},fetchPaymentsAsAdmin(t,e){return e=(e||"")+"&wallet_id="+t,LNbits.api.request("GET","/api/v1/payments/all/paginated?"+e).then((t=>{this.paymentsTable.loading=!1,this.paymentsTable.pagination.rowsNumber=t.data.total,this.payments=t.data.data.map((t=>LNbits.map.payment(t)))})).catch((t=>{this.paymentsTable.loading=!1,LNbits.utils.notifyApiError(t)}))},checkPayment(t){LNbits.api.getPayment(this.g.wallet,t).then((t=>{this.update=!this.update,"success"==t.data.status&&Quasar.Notify.create({type:"positive",message:this.$t("payment_successful")}),"pending"==t.data.status&&Quasar.Notify.create({type:"info",message:this.$t("payment_pending")})})).catch(LNbits.utils.notifyApiError)},paymentTableRowKey:t=>t.payment_hash+t.amount,exportCSV(t=!1){const e=this.paymentsTable.pagination,i={sortby:e.sortBy??"time",direction:e.descending?"desc":"asc"},s=new URLSearchParams(i);LNbits.api.getPayments(this.g.wallet,s).then((e=>{let i=e.data.data.map(LNbits.map.payment),s=this.paymentsCSV.columns;if(t){this.exportPaymentTagList.length&&(i=i.filter((t=>this.exportPaymentTagList.includes(t.tag))));const t=Object.keys(i.reduce(((t,e)=>({...t,...e.details})),{})).map((t=>({name:t,align:"right",label:t.charAt(0).toUpperCase()+t.slice(1).replace(/([A-Z])/g," $1"),field:e=>e.details[t],format:t=>"object"==typeof t?JSON.stringify(t):t})));s=this.paymentsCSV.columns.concat(t)}LNbits.utils.exportCSV(s,i,this.g.wallet.name+"-payments")}))},addFilterTag(){if(!this.exportTagName)return;const t=this.exportTagName.trim();this.exportPaymentTagList=this.exportPaymentTagList.filter((e=>e!==t)),this.exportPaymentTagList.push(t),this.exportTagName=""},removeExportTag(t){this.exportPaymentTagList=this.exportPaymentTagList.filter((e=>e!==t))},formatCurrency(t,e){try{return LNbits.utils.formatCurrency(t,e)}catch(e){return console.error(e),`${t} ???`}},handleFilterChanged(){const{success:t,pending:e,failed:i,incoming:s,outgoing:n}=this.searchStatus;delete this.paymentsTable.filter["status[ne]"],delete this.paymentsTable.filter["status[eq]"],t&&e&&i||(t&&e?this.paymentsTable.filter["status[ne]"]="failed":t&&i?this.paymentsTable.filter["status[ne]"]="pending":i&&e?this.paymentsTable.filter["status[ne]"]="success":t?this.paymentsTable.filter["status[eq]"]="success":e?this.paymentsTable.filter["status[eq]"]="pending":i&&(this.paymentsTable.filter["status[eq]"]="failed")),delete this.paymentsTable.filter["amount[ge]"],delete this.paymentsTable.filter["amount[le]"],s&&n||(s?this.paymentsTable.filter["amount[ge]"]=0:n&&(this.paymentsTable.filter["amount[le]"]=0))}},watch:{"paymentsTable.search":{handler(){const t={};this.paymentsTable.search&&(t.search=this.paymentsTable.search),this.fetchPayments()}},lazy(t){!0===t&&this.fetchPayments()},update(){this.fetchPayments()},"g.updatePayments"(){this.fetchPayments()},"g.wallet":{handler(t){this.fetchPayments()},deep:!0}},created(){void 0===this.lazy&&this.fetchPayments()}}),window.app.component(QrcodeVue),window.app.component("lnbits-extension-rating",{template:"#lnbits-extension-rating",name:"lnbits-extension-rating",props:["rating"]}),window.app.component("lnbits-fsat",{template:"{{ fsat }}",props:{amount:{type:Number,default:0}},computed:{fsat(){return LNbits.utils.formatSat(this.amount)}}}),window.app.component("lnbits-wallet-list",{mixins:[window.windowMixin],template:"#lnbits-wallet-list",props:["balance"],data:()=>({activeWallet:null,balance:0,showForm:!1,walletName:"",LNBITS_DENOMINATION:LNBITS_DENOMINATION}),methods:{createWallet(){LNbits.api.createWallet(this.g.user.wallets[0],this.walletName)}},created(){document.addEventListener("updateWalletBalance",this.updateWalletBalance)}}),window.app.component("lnbits-extension-list",{mixins:[window.windowMixin],template:"#lnbits-extension-list",data:()=>({extensions:[],searchTerm:""}),watch:{"g.user.extensions":{handler(t){this.loadExtensions()},deep:!0}},computed:{userExtensions(){return this.updateUserExtensions(this.searchTerm)}},methods:{async loadExtensions(){try{const{data:t}=await LNbits.api.request("GET","/api/v1/extension");this.extensions=t.map((t=>LNbits.map.extension(t))).sort(((t,e)=>t.name.localeCompare(e.name)))}catch(t){LNbits.utils.notifyApiError(t)}},updateUserExtensions(t){const e=window.location.pathname,i=this.g.user.extensions;return this.extensions.filter((t=>i.includes(t.code))).filter((e=>!t||`${e.code} ${e.name} ${e.short_description} ${e.url}`.toLocaleLowerCase().includes(t.toLocaleLowerCase()))).map((t=>(t.isActive=e.startsWith(t.url),t)))}},async created(){await this.loadExtensions()}}),window.app.component("lnbits-manage",{mixins:[window.windowMixin],template:"#lnbits-manage",props:["showAdmin","showNode","showExtensions","showUsers","showAudit"],methods:{isActive:t=>window.location.pathname===t},data:()=>({extensions:[]})}),window.app.component("lnbits-payment-details",{mixins:[window.windowMixin],template:"#lnbits-payment-details",props:["payment"],mixins:[window.windowMixin],data:()=>({LNBITS_DENOMINATION:LNBITS_DENOMINATION}),computed:{hasPreimage(){return this.payment.preimage&&"0000000000000000000000000000000000000000000000000000000000000000"!==this.payment.preimage},hasExpiry(){return!!this.payment.expiry},hasSuccessAction(){return this.hasPreimage&&this.payment.extra&&this.payment.extra.success_action},webhookStatusColor(){return this.payment.webhook_status>=300||this.payment.webhook_status<0?"red-10":this.payment.webhook_status?"green-10":"cyan-7"},webhookStatusText(){return this.payment.webhook_status?this.payment.webhook_status:"not sent yet"},hasTag(){return this.payment.extra&&!!this.payment.extra.tag},extras(){if(!this.payment.extra)return[];let t=_.omit(this.payment.extra,["tag","success_action"]);return Object.keys(t).map((e=>({key:e,value:t[e]})))}}}),window.app.component("lnbits-lnurlpay-success-action",{mixins:[window.windowMixin],template:"#lnbits-lnurlpay-success-action",props:["payment","success_action"],data(){return{decryptedValue:this.success_action.ciphertext}},mounted(){if("aes"!==this.success_action.tag)return null;decryptLnurlPayAES(this.success_action,this.payment.preimage).then((t=>{this.decryptedValue=t}))}}),window.app.component("lnbits-qrcode",{mixins:[window.windowMixin],template:"#lnbits-qrcode",components:{QrcodeVue:QrcodeVue},props:{value:{type:String,required:!0},options:Object},data:()=>({custom:{margin:3,width:350,size:350,logo:LNBITS_QR_LOGO}}),created(){this.custom={...this.custom,...this.options}}}),window.app.component("lnbits-notifications-btn",{template:"#lnbits-notifications-btn",mixins:[window.windowMixin],props:["pubkey"],data:()=>({isSupported:!1,isSubscribed:!1,isPermissionGranted:!1,isPermissionDenied:!1}),methods:{urlB64ToUint8Array(t){const e=(t+"=".repeat((4-t.length%4)%4)).replace(/\-/g,"+").replace(/_/g,"/"),i=atob(e),s=new Uint8Array(i.length);for(let t=0;te!==t)),this.$q.localStorage.set("lnbits.webpush.subscribedUsers",JSON.stringify(e))},isUserSubscribed(t){return(JSON.parse(this.$q.localStorage.getItem("lnbits.webpush.subscribedUsers"))||[]).includes(t)},subscribe(){this.isSupported&&!this.isPermissionDenied&&(Notification.requestPermission().then((t=>{this.isPermissionGranted="granted"===t,this.isPermissionDenied="denied"===t})).catch(console.log),navigator.serviceWorker.ready.then((t=>{navigator.serviceWorker.getRegistration().then((t=>{t.pushManager.getSubscription().then((e=>{if(null===e||!this.isUserSubscribed(this.g.user.id)){const e={applicationServerKey:this.urlB64ToUint8Array(this.pubkey),userVisibleOnly:!0};t.pushManager.subscribe(e).then((t=>{LNbits.api.request("POST","/api/v1/webpush",null,{subscription:JSON.stringify(t)}).then((t=>{this.saveUserSubscribed(t.data.user),this.isSubscribed=!0})).catch(LNbits.utils.notifyApiError)}))}})).catch(console.log)}))})))},unsubscribe(){navigator.serviceWorker.ready.then((t=>{t.pushManager.getSubscription().then((t=>{t&&LNbits.api.request("DELETE","/api/v1/webpush?endpoint="+btoa(t.endpoint),null).then((()=>{this.removeUserSubscribed(this.g.user.id),this.isSubscribed=!1})).catch(LNbits.utils.notifyApiError)}))})).catch(console.log)},checkSupported(){let t="https:"===window.location.protocol,e="serviceWorker"in navigator,i="Notification"in window,s="PushManager"in window;return this.isSupported=t&&e&&i&&s,this.isSupported||console.log("Notifications disabled because requirements are not met:",{HTTPS:t,"Service Worker API":e,"Notification API":i,"Push API":s}),this.isSupported},async updateSubscriptionStatus(){await navigator.serviceWorker.ready.then((t=>{t.pushManager.getSubscription().then((t=>{this.isSubscribed=!!t&&this.isUserSubscribed(this.g.user.id)}))})).catch(console.log)}},created(){this.isPermissionDenied="denied"===Notification.permission,this.checkSupported()&&this.updateSubscriptionStatus()}}),window.app.component("lnbits-dynamic-fields",{template:"#lnbits-dynamic-fields",mixins:[window.windowMixin],props:["options","modelValue"],data:()=>({formData:null,rules:[t=>!!t||"Field is required"]}),methods:{applyRules(t){return t?this.rules:[]},buildData(t,e={}){return t.reduce(((t,i)=>(i.options?.length?t[i.name]=this.buildData(i.options,e[i.name]):t[i.name]=e[i.name]??i.default,t)),{})},handleValueChanged(){this.$emit("update:model-value",this.formData)}},created(){this.formData=this.buildData(this.options,this.modelValue)}}),window.app.component("lnbits-dynamic-chips",{template:"#lnbits-dynamic-chips",mixins:[window.windowMixin],props:["modelValue"],data:()=>({chip:"",chips:[]}),methods:{addChip(){this.chip&&(this.chips.push(this.chip),this.chip="",this.$emit("update:model-value",this.chips.join(",")))},removeChip(t){this.chips.splice(t,1),this.$emit("update:model-value",this.chips.join(","))}},created(){"string"==typeof this.modelValue?this.chips=this.modelValue.split(","):this.chips=[...this.modelValue]}}),window.app.component("lnbits-update-balance",{template:"#lnbits-update-balance",mixins:[window.windowMixin],props:["wallet_id","small_btn"],computed:{denomination:()=>LNBITS_DENOMINATION,admin:()=>user.super_user},data:()=>({credit:0}),methods:{updateBalance(t){LNbits.api.updateBalance(t.value,this.wallet_id).then((e=>{if(!0!==e.data.success)throw new Error(e.data);credit=parseInt(t.value),Quasar.Notify.create({type:"positive",message:this.$t("credit_ok",{amount:credit}),icon:null}),this.credit=0,t.value=0,t.set()})).catch(LNbits.utils.notifyApiError)}}}),window.app.component("user-id-only",{template:"#user-id-only",mixins:[window.windowMixin],props:{allowed_new_users:Boolean,authAction:String,authMethod:String,usr:String,wallet:String},data(){return{user:this.usr,walletName:this.wallet}},methods:{showLogin(t){this.$emit("show-login",t)},showRegister(t){this.$emit("show-register",t)},loginUsr(){this.$emit("update:usr",this.user),this.$emit("login-usr")},createWallet(){this.$emit("update:wallet",this.walletName),this.$emit("create-wallet")}},computed:{showInstantLogin(){return"username-password"!==this.authMethod||"register"!==this.authAction}},created(){}}),window.app.component("username-password",{template:"#username-password",mixins:[window.windowMixin],props:{allowed_new_users:Boolean,authMethods:Array,authAction:String,username:String,password_1:String,password_2:String,resetKey:String},data(){return{oauth:["nostr-auth-nip98","google-auth","github-auth","keycloak-auth"],username:this.userName,password:this.password_1,passwordRepeat:this.password_2,reset_key:this.resetKey,keycloakOrg:LNBITS_AUTH_KEYCLOAK_ORG||"Keycloak",keycloakIcon:LNBITS_AUTH_KEYCLOAK_ICON}},methods:{login(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("login")},register(){this.$emit("update:userName",this.username),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("register")},reset(){this.$emit("update:resetKey",this.reset_key),this.$emit("update:password_1",this.password),this.$emit("update:password_2",this.passwordRepeat),this.$emit("reset")},validateUsername:t=>new RegExp("^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$").test(t),async signInWithNostr(){try{const t=await this.createNostrToken();if(!t)return;resp=await LNbits.api.loginByProvider("nostr",{Authorization:t},{}),window.location.href="/wallet"}catch(t){console.warn(t);const e=t?.response?.data?.detail||`${t}`;Quasar.Notify.create({type:"negative",message:"Failed to sign in with Nostr.",caption:e})}},async createNostrToken(){try{if(!window.nostr?.signEvent)return void Quasar.Notify.create({type:"negative",message:"No Nostr signing app detected.",caption:'Is "window.nostr" present?'});const t=`${window.location}nostr`,e="POST",i=await NostrTools.nip98.getToken(t,e,(t=>async function(t){try{const{data:e}=await LNbits.api.getServerHealth();return t.created_at=e.server_time,await window.nostr.signEvent(t)}catch(t){console.error(t),Quasar.Notify.create({type:"negative",message:"Failed to sign nostr event.",caption:`${t}`})}}(t)),!0);if(!await NostrTools.nip98.validateToken(i,t,e))throw new Error("Invalid signed token!");return i}catch(t){console.warn(t),Quasar.Notify.create({type:"negative",message:"Failed create Nostr event.",caption:`${t}`})}}},computed:{showOauth(){return this.oauth.some((t=>this.authMethods.includes(t)))}},created(){}}),window.app.component("separator-text",{template:"#separator-text",props:{text:String,uppercase:{type:Boolean,default:!1},color:{type:String,default:"grey"}}});const DynamicComponent={props:{fetchUrl:{type:String,required:!0},scripts:{type:Array,default:()=>[]}},data:()=>({keys:[]}),async mounted(){await this.loadDynamicContent()},methods:{loadScript:async t=>new Promise(((e,i)=>{const s=document.querySelector(`script[src="${t}"]`);s&&s.remove();const n=document.createElement("script");n.src=t,n.async=!0,n.onload=e,n.onerror=()=>i(new Error(`Failed to load script: ${t}`)),document.head.appendChild(n)})),async loadDynamicContent(){this.$q.loading.show();try{const t=this.fetchUrl.split("#")[0],e=await fetch(t,{credentials:"include",headers:{Accept:"text/html","X-Requested-With":"XMLHttpRequest"}}),i=await e.text(),s=new DOMParser,n=s.parseFromString(i,"text/html").querySelector("#window-vars-script");n&&new Function(n.innerHTML)(),await this.loadScript("/static/js/base.js");for(const t of this.scripts)await this.loadScript(t);const a=this.$router.currentRoute.value.meta.previousRouteName;a&&window.app._context.components[a]&&delete window.app._context.components[a];const o=`${this.$route.name}PageLogic`,r=window[o];if(!r)throw new Error(`Component logic '${o}' not found. Ensure it is defined in the script.`);r.mixins=r.mixins||[],window.windowMixin&&r.mixins.push(window.windowMixin),window.app.component(this.$route.name,{...r,template:i}),delete window[o],this.$forceUpdate()}catch(t){console.error("Error loading dynamic content:",t)}finally{this.$q.loading.hide()}}},watch:{$route(t,e){routes.map((t=>t.name)).includes(t.name)?(this.$router.currentRoute.value.meta.previousRouteName=e.name,this.loadDynamicContent()):console.log(`Route '${t.name}' is not valid. Leave this one to Fastapi.`)}},template:'\n \n '},routes=[{path:"/wallet",name:"Wallet",component:DynamicComponent,props:t=>{let e="/wallet";if(Object.keys(t.query).length>0){e+="?";for(const[i,s]of Object.entries(t.query))e+=`${i}=${s}&`;e=e.slice(0,-1)}return{fetchUrl:e,scripts:["/static/js/wallet.js"]}}},{path:"/admin",name:"Admin",component:DynamicComponent,props:{fetchUrl:"/admin",scripts:["/static/js/admin.js"]}},{path:"/users",name:"Users",component:DynamicComponent,props:{fetchUrl:"/users",scripts:["/static/js/users.js"]}},{path:"/audit",name:"Audit",component:DynamicComponent,props:{fetchUrl:"/audit",scripts:["/static/js/audit.js"]}},{path:"/payments",name:"Payments",component:DynamicComponent,props:{fetchUrl:"/payments",scripts:["/static/js/payments.js"]}},{path:"/extensions",name:"Extensions",component:DynamicComponent,props:{fetchUrl:"/extensions",scripts:["/static/js/extensions.js"]}},{path:"/account",name:"Account",component:DynamicComponent,props:{fetchUrl:"/account",scripts:["/static/js/account.js"]}},{path:"/wallets",name:"Wallets",component:DynamicComponent,props:{fetchUrl:"/wallets",scripts:["/static/js/wallets.js"]}},{path:"/node",name:"Node",component:DynamicComponent,props:{fetchUrl:"/node",scripts:["/static/js/node.js"]}}];window.router=VueRouter.createRouter({history:VueRouter.createWebHistory(),routes:routes}),window.app.mixin({computed:{isVueRoute(){const t=window.location.pathname,e=window.router.resolve(t);return e?.matched?.length>0}}}),window.app.use(VueQrcodeReader),window.app.use(Quasar,{config:{loading:{spinner:Quasar.QSpinnerBars}}}),window.app.use(window.i18n),window.app.provide("g",g),window.app.use(window.router),window.app.component("DynamicComponent",DynamicComponent),window.app.mount("#vue");
diff --git a/lnbits/static/js/components/lnbits-funding-sources.js b/lnbits/static/js/components/lnbits-funding-sources.js
index 3d8fff65..00b02b55 100644
--- a/lnbits/static/js/components/lnbits-funding-sources.js
+++ b/lnbits/static/js/components/lnbits-funding-sources.js
@@ -219,6 +219,16 @@ window.app.component('lnbits-funding-sources', {
strike_api_endpoint: 'API Endpoint',
strike_api_key: 'API Key'
}
+ ],
+ [
+ 'BreezLiquidSdkWallet',
+ 'Breez Liquid SDK',
+ {
+ breez_liquid_api_key: 'Breez API Key (can be empty)',
+ breez_liquid_seed: 'Liquid seed phrase',
+ breez_liquid_fee_offset_sat:
+ 'Offset amount in sats to increase fee limit'
+ }
]
]
}
diff --git a/lnbits/wallets/.breez b/lnbits/wallets/.breez
new file mode 100644
index 00000000..fb71e5f8
--- /dev/null
+++ b/lnbits/wallets/.breez
@@ -0,0 +1 @@
+MIIBYzCCARWgAwIBAgIHPjlTc1IbAzAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUwNzAyMDkxODMzWhcNMzUwNjMwMDkxODMzWjAfMQ8wDQYDVQQKEwZsbmJpdHMxDDAKBgNVBAMTA2RuaTAqMAUGAytlcAMhANCD9cvfIDwcoiDKKYdT9BunHLS2/OuKzV8NS0SzqV13o38wfTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2jmj7l5rSw0yVb/vlWAYkK/YBwkwHwYDVR0jBBgwFoAU3qrWklbzjed0khb8TLYgsmsomGswHQYDVR0RBBYwFIESb2ZmaWNlQGRuaWxhYnMuY29tMAUGAytlcANBAMGS8jEfZbfNpv6mVrg328NXnjA/nG6TuGA0aAw0NyDz499aeu/0TURjF8FzmxzmdNOiffUZ6akPWCZUKFYuGgA=
diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py
index 82c18e60..7b6377a9 100644
--- a/lnbits/wallets/__init__.py
+++ b/lnbits/wallets/__init__.py
@@ -10,6 +10,7 @@ from .alby import AlbyWallet
from .blink import BlinkWallet
from .boltz import BoltzWallet
from .breez import BreezSdkWallet
+from .breez_liquid import BreezLiquidSdkWallet
from .cliche import ClicheWallet
from .corelightning import CoreLightningWallet
@@ -57,6 +58,7 @@ __all__ = [
"AlbyWallet",
"BlinkWallet",
"BoltzWallet",
+ "BreezLiquidSdkWallet",
"BreezSdkWallet",
"CLightningWallet",
"ClicheWallet",
diff --git a/lnbits/wallets/base.py b/lnbits/wallets/base.py
index 6f5c6126..c8df9f75 100644
--- a/lnbits/wallets/base.py
+++ b/lnbits/wallets/base.py
@@ -24,6 +24,7 @@ class InvoiceResponse(NamedTuple):
payment_request: str | None = None
error_message: str | None = None
preimage: str | None = None
+ fee_msat: int | None = None
@property
def success(self) -> bool:
diff --git a/lnbits/wallets/breez_liquid.py b/lnbits/wallets/breez_liquid.py
new file mode 100644
index 00000000..5397252e
--- /dev/null
+++ b/lnbits/wallets/breez_liquid.py
@@ -0,0 +1,296 @@
+# Based on breez.py
+
+try:
+ import breez_sdk_liquid as breez_sdk # type: ignore
+
+ BREEZ_SDK_INSTALLED = True
+except ImportError:
+ BREEZ_SDK_INSTALLED = False
+
+if not BREEZ_SDK_INSTALLED:
+
+ class BreezLiquidSdkWallet: # pyright: ignore
+ def __init__(self):
+ raise RuntimeError(
+ "Breez Liquid SDK is not installed. "
+ "Ask admin to run `poetry add -E breez` to install it."
+ )
+
+else:
+ import asyncio
+ from asyncio import Queue
+ from collections.abc import AsyncGenerator
+ from pathlib import Path
+ from typing import Optional
+
+ import breez_sdk_liquid as breez_sdk # type: ignore
+ from bolt11 import decode as bolt11_decode
+ from loguru import logger
+
+ from lnbits.settings import settings
+
+ from .base import (
+ InvoiceResponse,
+ PaymentFailedStatus,
+ PaymentPendingStatus,
+ PaymentResponse,
+ PaymentStatus,
+ PaymentSuccessStatus,
+ StatusResponse,
+ Wallet,
+ )
+
+ breez_incoming_queue: Queue[breez_sdk.PaymentDetails.LIGHTNING] = Queue()
+ breez_outgoing_queue: dict[str, Queue[breez_sdk.PaymentDetails.LIGHTNING]] = {}
+
+ class PaymentsListener(breez_sdk.EventListener):
+ def on_event(self, e: breez_sdk.SdkEvent) -> None:
+ logger.debug(f"received breez sdk event: {e}")
+ # TODO: when this issue is fixed:
+ # https://github.com/breez/breez-sdk-liquid/issues/961
+ # use breez_sdk.SdkEvent.PAYMENT_WAITING_CONFIRMATION
+ if not isinstance(
+ e, breez_sdk.SdkEvent.PAYMENT_SUCCEEDED
+ ) or not isinstance(e.details.details, breez_sdk.PaymentDetails.LIGHTNING):
+ return
+
+ payment = e.details
+ payment_details = e.details.details
+
+ if payment.payment_type is breez_sdk.PaymentType.RECEIVE:
+ breez_incoming_queue.put_nowait(payment_details)
+ elif (
+ payment.payment_type is breez_sdk.PaymentType.SEND
+ and payment_details.payment_hash in breez_outgoing_queue
+ ):
+ breez_outgoing_queue[payment_details.payment_hash].put_nowait(
+ payment_details
+ )
+
+ class BreezLiquidSdkWallet(Wallet): # type: ignore[no-redef]
+ def __init__(self):
+ if not settings.breez_liquid_seed:
+ raise ValueError(
+ "cannot initialize BreezLiquidSdkWallet: missing breez_liquid_seed"
+ )
+
+ if not settings.breez_liquid_api_key:
+ with open(Path("lnbits/wallets", ".breez")) as f:
+ settings.breez_liquid_api_key = f.read().strip()
+
+ self.config = breez_sdk.default_config(
+ breez_sdk.LiquidNetwork.MAINNET,
+ breez_api_key=settings.breez_liquid_api_key,
+ )
+
+ breez_sdk_working_dir = Path(
+ settings.lnbits_data_folder, "breez-liquid-sdk"
+ )
+ breez_sdk_working_dir.mkdir(parents=True, exist_ok=True)
+ self.config.working_dir = breez_sdk_working_dir.absolute().as_posix()
+
+ try:
+ mnemonic = settings.breez_liquid_seed
+ connect_request = breez_sdk.ConnectRequest(
+ config=self.config, mnemonic=mnemonic
+ )
+ self.sdk_services = breez_sdk.connect(connect_request)
+ self.sdk_services.add_event_listener(PaymentsListener())
+ except Exception as exc:
+ logger.warning(exc)
+ raise ValueError(
+ f"cannot initialize BreezLiquidSdkWallet: {exc!s}"
+ ) from exc
+
+ async def cleanup(self):
+ self.sdk_services.disconnect()
+
+ async def status(self) -> StatusResponse:
+ try:
+ info: breez_sdk.GetInfoResponse = self.sdk_services.get_info()
+ except Exception as exc:
+ logger.warning(exc)
+ return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0)
+ return StatusResponse(None, int(info.wallet_info.balance_sat * 1000))
+
+ async def create_invoice(
+ self,
+ amount: int,
+ memo: Optional[str] = None,
+ description_hash: Optional[bytes] = None,
+ unhashed_description: Optional[bytes] = None,
+ **_,
+ ) -> InvoiceResponse:
+ try:
+ # issue with breez sdk, receive_amount is of type BITCOIN
+ # not ReceiveAmount after initialisation
+ receive_amount = breez_sdk.ReceiveAmount.BITCOIN(amount)
+ req = self.sdk_services.prepare_receive_payment(
+ breez_sdk.PrepareReceiveRequest(
+ payment_method=breez_sdk.PaymentMethod.BOLT11_INVOICE,
+ amount=receive_amount, # type: ignore
+ )
+ )
+ receive_fees_sats = req.fees_sat
+
+ description = memo or (
+ unhashed_description.decode() if unhashed_description else ""
+ )
+
+ res = self.sdk_services.receive_payment(
+ breez_sdk.ReceivePaymentRequest(
+ prepare_response=req,
+ description=description,
+ use_description_hash=description_hash is not None,
+ )
+ )
+
+ bolt11 = res.destination
+ invoice_data = bolt11_decode(bolt11)
+ payment_hash = invoice_data.payment_hash
+
+ return InvoiceResponse(
+ ok=True,
+ checking_id=payment_hash,
+ payment_request=bolt11,
+ fee_msat=receive_fees_sats * 1000,
+ )
+ except Exception as e:
+ logger.warning(e)
+ return InvoiceResponse(ok=False, error_message=str(e))
+
+ async def pay_invoice(
+ self, bolt11: str, fee_limit_msat: int
+ ) -> PaymentResponse:
+ invoice_data = bolt11_decode(bolt11)
+
+ try:
+ prepare_req = breez_sdk.PrepareSendRequest(destination=bolt11)
+ req = self.sdk_services.prepare_send_payment(prepare_req)
+
+ fee_limit_sat = settings.breez_liquid_fee_offset_sat + int(
+ fee_limit_msat / 1000
+ )
+
+ if req.fees_sat and req.fees_sat > fee_limit_sat:
+ return PaymentResponse(
+ ok=False,
+ error_message=(
+ f"fee of {req.fees_sat} sat exceeds limit of "
+ f"{fee_limit_sat} sat"
+ ),
+ )
+
+ send_response = self.sdk_services.send_payment(
+ breez_sdk.SendPaymentRequest(prepare_response=req)
+ )
+
+ except Exception as exc:
+ logger.warning(exc)
+ return PaymentResponse(error_message=f"Exception while payment: {exc}")
+
+ payment: breez_sdk.Payment = send_response.payment
+ logger.debug(f"pay invoice res: {payment}")
+ checking_id = invoice_data.payment_hash
+
+ fees = req.fees_sat * 1000 if req.fees_sat and req.fees_sat > 0 else 0
+
+ if payment.status != breez_sdk.PaymentState.COMPLETE:
+ return await self._wait_for_outgoing_payment(checking_id, fees, 5)
+
+ if not isinstance(payment.details, breez_sdk.PaymentDetails.LIGHTNING):
+ return PaymentResponse(
+ error_message="lightning payment details are not available"
+ )
+
+ return PaymentResponse(
+ ok=True,
+ checking_id=checking_id,
+ fee_msat=payment.fees_sat * 1000,
+ preimage=payment.details.preimage,
+ )
+
+ async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
+ try:
+ req = breez_sdk.GetPaymentRequest.PAYMENT_HASH(checking_id)
+ payment = self.sdk_services.get_payment(req=req) # type: ignore
+ if payment is None:
+ return PaymentPendingStatus()
+ if payment.payment_type != breez_sdk.PaymentType.RECEIVE:
+ logger.warning(f"unexpected payment type: {payment.status}")
+ return PaymentPendingStatus()
+ if payment.status == breez_sdk.PaymentState.FAILED:
+ return PaymentFailedStatus()
+ if payment.status == breez_sdk.PaymentState.COMPLETE:
+ return PaymentSuccessStatus(
+ paid=True, fee_msat=int(payment.fees_sat * 1000)
+ )
+ return PaymentPendingStatus()
+ except Exception as exc:
+ logger.warning(exc)
+ return PaymentPendingStatus()
+
+ async def get_payment_status(self, checking_id: str) -> PaymentStatus:
+ try:
+ req = breez_sdk.GetPaymentRequest.PAYMENT_HASH(checking_id)
+ payment = self.sdk_services.get_payment(req=req) # type: ignore
+ if payment is None:
+ return PaymentPendingStatus()
+ if payment.payment_type != breez_sdk.PaymentType.SEND:
+ logger.warning(f"unexpected payment type: {payment.status}")
+ return PaymentPendingStatus()
+ if payment.status == breez_sdk.PaymentState.COMPLETE:
+ if not isinstance(
+ payment.details, breez_sdk.PaymentDetails.LIGHTNING
+ ):
+ logger.warning("payment details are not of type LIGHTNING")
+ return PaymentPendingStatus()
+ return PaymentSuccessStatus(
+ fee_msat=int(payment.fees_sat * 1000),
+ preimage=payment.details.preimage,
+ )
+ if payment.status == breez_sdk.PaymentState.FAILED:
+ return PaymentFailedStatus()
+ return PaymentPendingStatus()
+ except Exception as exc:
+ logger.warning(exc)
+ return PaymentPendingStatus()
+
+ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
+ while settings.lnbits_running:
+ details = await breez_incoming_queue.get()
+ logger.debug(f"breez invoice paid event: {details}")
+ if not details.invoice:
+ logger.warning(
+ "Paid invoices stream expected bolt11 invoice, got None"
+ )
+ continue
+
+ invoice_data = bolt11_decode(details.invoice)
+ yield invoice_data.payment_hash
+
+ async def _wait_for_outgoing_payment(
+ self, checking_id: str, fees: int, timeout: int
+ ) -> PaymentResponse:
+ try:
+ breez_outgoing_queue[checking_id] = Queue()
+ payment_details = await asyncio.wait_for(
+ breez_outgoing_queue[checking_id].get(), timeout
+ )
+ return PaymentResponse(
+ ok=True,
+ preimage=payment_details.preimage,
+ checking_id=checking_id,
+ fee_msat=fees,
+ )
+ except asyncio.TimeoutError:
+ logger.debug(
+ f"payment '{checking_id}' is still pending after {timeout} seconds"
+ )
+ return PaymentResponse(
+ checking_id=checking_id,
+ fee_msat=fees,
+ error_message="payment is pending",
+ )
+ finally:
+ breez_outgoing_queue.pop(checking_id, None)
diff --git a/poetry.lock b/poetry.lock
index 930f9414..4c074ca5 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -656,6 +656,47 @@ files = [
{file = "breez_sdk-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e51689ed2f2c2d49bdb53f0ee26d0dc2109a50f15b8f2129a250db275eb994bf"},
]
+[[package]]
+name = "breez-sdk-liquid"
+version = "0.9.1"
+description = "Python language bindings for the Breez Liquid SDK"
+optional = true
+python-versions = "*"
+groups = ["main"]
+markers = "extra == \"breez\""
+files = [
+ {file = "breez_sdk_liquid-0.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:414a7c04dbf869435fe234b3b9987699c15dc2f8d4660d9ca1a51af608331b45"},
+ {file = "breez_sdk_liquid-0.9.1-cp310-cp310-manylinux_2_31_aarch64.whl", hash = "sha256:7f461b6b10ddfe9cfb58119e590e5f67da4d7c93fc580a84a0116ce9fd21dffa"},
+ {file = "breez_sdk_liquid-0.9.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:c27efe35539e4092dddbe5315f5cefffccbdd699017ed8733611d11eb52f4ce0"},
+ {file = "breez_sdk_liquid-0.9.1-cp310-cp310-win32.whl", hash = "sha256:375f6322c2552d15d6ba68302a85fdc345a7671eeda8fea92c013a16c5e96505"},
+ {file = "breez_sdk_liquid-0.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:f28d107099fb7caaac4051d5950a4f11bf823e11b56e797976c5fa2a7280d597"},
+ {file = "breez_sdk_liquid-0.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:acefa49896bdb4ca06d351faacbf075646d574dda5539033df11310ec5a94446"},
+ {file = "breez_sdk_liquid-0.9.1-cp311-cp311-manylinux_2_31_aarch64.whl", hash = "sha256:d0e54fd5c8ce542dc2061d8a1ef46e6a1d430dbaea5eab369bb91ae2c4a1be7b"},
+ {file = "breez_sdk_liquid-0.9.1-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:a5d1bfd4d3286d57c110d925332416b0903dda076dadede02c4b7db67576f0bf"},
+ {file = "breez_sdk_liquid-0.9.1-cp311-cp311-win32.whl", hash = "sha256:3ce2152f0a2ba65f4425fd22efb8e1dafb419e5fcf3b93dfe06fc061b1d1c0c6"},
+ {file = "breez_sdk_liquid-0.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:8cdb4f247bccc0f4b9bbfff4451faa78d7b17e76a9dc918804880b600f1d4249"},
+ {file = "breez_sdk_liquid-0.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:c463191a89a829afcae19c94f7683dac415667755d68c212afc60d8f2b21ba63"},
+ {file = "breez_sdk_liquid-0.9.1-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:52efaeae2c278b498b37de1090e80f73987ee943743e69a363877ed93cd7a31e"},
+ {file = "breez_sdk_liquid-0.9.1-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:68aebea98990319c677bcbd7d1de258627b85063a29720879986694b9d393adb"},
+ {file = "breez_sdk_liquid-0.9.1-cp312-cp312-win32.whl", hash = "sha256:f53e4a1973dfd41df3ac3ecb42cf72acb71f49d5727a8acab25668b51a953bb2"},
+ {file = "breez_sdk_liquid-0.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:6adb1f3f3ccd4b743a11dcc0b307db604c3424f42e2547f94fa939f0afb9bb85"},
+ {file = "breez_sdk_liquid-0.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f94432a75bc848e597fb9859a77bab6c703d5e423d470519f37c402fc3c5d79e"},
+ {file = "breez_sdk_liquid-0.9.1-cp313-cp313-manylinux_2_31_aarch64.whl", hash = "sha256:8d6838e69620162c038d513f95ffade870e112ae5c6a0d713eaa2a3a31d6f212"},
+ {file = "breez_sdk_liquid-0.9.1-cp313-cp313-manylinux_2_31_x86_64.whl", hash = "sha256:d6b9a8601cef5875447e10c75ca3e1579dd1e881ec57cabe9787616b8a6de263"},
+ {file = "breez_sdk_liquid-0.9.1-cp313-cp313-win32.whl", hash = "sha256:d76fc905003ac5a0cfd2132df90ba6343b40b9948164a0be06a68f15b28789a2"},
+ {file = "breez_sdk_liquid-0.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:d6b0fc1d77b7855d9a72799d65cdaaa0bd07a3999d6d4d27050d8ef174ce0fd3"},
+ {file = "breez_sdk_liquid-0.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d0d560657c29d7e3b054ad4be07955fc41b63d4b93a2b51b29bb85d1d52341af"},
+ {file = "breez_sdk_liquid-0.9.1-cp38-cp38-manylinux_2_31_aarch64.whl", hash = "sha256:2531f788eb15bb00f4b3b505ac5b8ee3de23161dd4f65768a4aa3255ea04d68e"},
+ {file = "breez_sdk_liquid-0.9.1-cp38-cp38-manylinux_2_31_x86_64.whl", hash = "sha256:f9c9f6255999cf403bc07a9deed99f8c1625a0405ae9231e3dbecb3b6af0fdc7"},
+ {file = "breez_sdk_liquid-0.9.1-cp38-cp38-win32.whl", hash = "sha256:5232b25b7e8583100ec54423e7568a305cac7778736adc9f8c735798110350c7"},
+ {file = "breez_sdk_liquid-0.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:67d2b894b70ba9011fc98e04ed1ade2ff18f0413b6f2a0994747e71418d004e4"},
+ {file = "breez_sdk_liquid-0.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bd77d2ce43a2c0ad77a7f22226e373034ff57677338b38e4ebe3b77eb34fa224"},
+ {file = "breez_sdk_liquid-0.9.1-cp39-cp39-manylinux_2_31_aarch64.whl", hash = "sha256:14092d9d65f1b0a9009e64630614e7b8b936e9d382d0f6415f023f0b192f6b45"},
+ {file = "breez_sdk_liquid-0.9.1-cp39-cp39-manylinux_2_31_x86_64.whl", hash = "sha256:5faa4d5e9a2b47899783aa0c44ac2274d32f74a7f469506af14520deabaabf9f"},
+ {file = "breez_sdk_liquid-0.9.1-cp39-cp39-win32.whl", hash = "sha256:b5b2c4bae13655d108475f4f22efdc2b07283f72d99b9c6f99166d9e41e62942"},
+ {file = "breez_sdk_liquid-0.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f3ad7c65582558031451538c8a03caa3486a2d2c8906f2fbbccce84e9357d1ea"},
+]
+
[[package]]
name = "certifi"
version = "2025.6.15"
@@ -4373,10 +4414,10 @@ multidict = ">=4.0"
propcache = ">=0.2.1"
[extras]
-breez = ["breez-sdk"]
+breez = ["breez-sdk", "breez-sdk-liquid"]
liquid = ["wallycore"]
[metadata]
lock-version = "2.1"
python-versions = "~3.12 | ~3.11 | ~3.10"
-content-hash = "434e37e5aeb0eeb1a22e12fd1914b45f27195162ef4ebc1055071fd4718d85fc"
+content-hash = "53f582a8079540033939ccd1bbd93b8ec1e8190ee26be0c0b8d64d57edb5cdac"
diff --git a/pyproject.toml b/pyproject.toml
index 1b54074e..4d018b14 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,6 +57,7 @@ python-crontab = "3.2.0"
wallycore = {version = "1.4.0", optional = true}
# needed for breez funding source
breez-sdk = {version = "0.8.0", optional = true}
+breez-sdk-liquid = {version = "0.9.1", optional = true}
jsonpath-ng = "^1.7.0"
pynostr = "^0.6.2"
@@ -65,7 +66,7 @@ filetype = "^1.2.0"
nostr-sdk = "^0.42.1"
[tool.poetry.extras]
-breez = ["breez-sdk"]
+breez = ["breez-sdk", "breez-sdk-liquid"]
liquid = ["wallycore"]
[tool.poetry.group.dev.dependencies]