diff --git a/.env.example b/.env.example index cc3585963..1464bc5f7 100644 --- a/.env.example +++ b/.env.example @@ -40,7 +40,7 @@ PORT=5000 ###################################### # which fundingsources are allowed in the admin ui -# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet" +# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet" LNBITS_BACKEND_WALLET_CLASS=VoidWallet # VoidWallet is just a fallback that works without any actual Lightning capabilities, @@ -105,6 +105,10 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroon" # or HEXSTRING BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" # or HEXSTRING BOLTZ_CLIENT_WALLET="lnbits" +# StrikeWallet +STRIKE_API_ENDPOINT=https://api.strike.me/v1 +STRIKE_API_KEY=YOUR_STRIKE_API_KEY + # ZBDWallet ZBD_API_ENDPOINT=https://api.zebedee.io/v0/ ZBD_API_KEY=ZBD_ACCESS_TOKEN diff --git a/lnbits/settings.py b/lnbits/settings.py index ca92bf32c..6c7e26c75 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -556,6 +556,13 @@ class BoltzFundingSource(LNbitsSettings): boltz_client_cert: str | None = Field(default=None) +class StrikeFundingSource(LNbitsSettings): + strike_api_endpoint: str | None = Field( + default="https://api.strike.me/v1", env="STRIKE_API_ENDPOINT" + ) + strike_api_key: str | None = Field(default=None, env="STRIKE_API_KEY") + + class LightningSettings(LNbitsSettings): lightning_invoice_expiry: int = Field(default=3600, gt=0) @@ -580,6 +587,7 @@ class FundingSourcesSettings( LnTipsFundingSource, NWCFundingSource, BreezSdkFundingSource, + StrikeFundingSource, ): lnbits_backend_wallet_class: str = Field(default="VoidWallet") # How long to wait for the payment to be confirmed before returning a pending status @@ -863,6 +871,7 @@ class SuperUserSettings(LNbitsSettings): "VoidWallet", "ZBDWallet", "NWCWallet", + "StrikeWallet", ] ) diff --git a/lnbits/static/bundle-components.min.js b/lnbits/static/bundle-components.min.js index 07702df07..619f6c45e 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}},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]={label:e,value:null};t.push([e,i])}return new Map(t)},sortedAllowedFundingSources(){return this.allowedFundingSources.sort()}},data:()=>({hideInput:!0,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"}],["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"}]]})}),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","showPayments"],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:1,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}},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:"/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}},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]={label:e,value:null};t.push([e,i])}return new Map(t)},sortedAllowedFundingSources(){return this.allowedFundingSources.sort()}},data:()=>({hideInput:!0,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"}],["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","showPayments"],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:1,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}},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:"/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 06a074958..7a53108f4 100644 --- a/lnbits/static/js/components/lnbits-funding-sources.js +++ b/lnbits/static/js/components/lnbits-funding-sources.js @@ -197,6 +197,14 @@ window.app.component('lnbits-funding-sources', { 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' + } ] ] } diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 5bf0095de..a1076a484 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -28,6 +28,7 @@ from .nwc import NWCWallet from .opennode import OpenNodeWallet from .phoenixd import PhoenixdWallet from .spark import SparkWallet +from .strike import StrikeWallet from .void import VoidWallet from .zbd import ZBDWallet @@ -72,6 +73,7 @@ __all__ = [ "OpenNodeWallet", "PhoenixdWallet", "SparkWallet", + "StrikeWallet", "VoidWallet", "ZBDWallet", ] diff --git a/lnbits/wallets/strike.py b/lnbits/wallets/strike.py new file mode 100644 index 000000000..4d53351bc --- /dev/null +++ b/lnbits/wallets/strike.py @@ -0,0 +1,546 @@ +import asyncio +import time +from decimal import Decimal +from typing import Any, AsyncGenerator, Dict, Optional + +import httpx +from loguru import logger + +from lnbits.settings import settings + +from .base import ( + InvoiceResponse, + PaymentFailedStatus, + PaymentPendingStatus, + PaymentResponse, + PaymentStatus, + PaymentSuccessStatus, + StatusResponse, + Wallet, +) + + +class TokenBucket: + """ + Token bucket rate limiter for Strike API endpoints. + """ + + def __init__(self, rate: int, period_seconds: int): + """ + Initialize a token bucket. + + Args: + rate: Max requests allowed in the period + period_seconds: Time period in seconds. + """ + self.rate = rate + self.period = period_seconds + self.tokens = rate + self.last_refill = time.monotonic() + self.lock = asyncio.Lock() + + async def consume(self) -> None: + """Wait until a token is available and consume it.""" + async with self.lock: + # Refill tokens based on elapsed time + now = time.monotonic() + elapsed = now - self.last_refill + + if elapsed > 0: + new_tokens = int((elapsed / self.period) * self.rate) + self.tokens = min(self.rate, self.tokens + new_tokens) + self.last_refill = now + + # If no tokens available, calculate wait time and wait for a token + if self.tokens < 1: + # Calculate time needed for one token + wait_time = (self.period / self.rate) * (1 - self.tokens) + await asyncio.sleep(wait_time) + + # After waiting, update time and add one token + self.last_refill = time.monotonic() + self.tokens = 1 + + # Consume a token (will be 0 or more after consumption) + self.tokens -= 1 + + +class StrikeWallet(Wallet): + """ + https://developer.strike.me/api + A minimal LNbits wallet backend for Strike. + """ + + # --------------------------------------------------------------------- # + # construction / teardown # + # --------------------------------------------------------------------- # + + def __init__(self): + if not settings.strike_api_endpoint: + raise ValueError("Missing strike_api_endpoint") + if not settings.strike_api_key: + raise ValueError("Missing strike_api_key") + + super().__init__() + + # throttle + self._sem = asyncio.Semaphore(value=20) + + # Rate limiters for different API endpoints + # Invoice/payment operations: 250 requests / 1 minute + self._invoice_limiter = TokenBucket(250, 60) + self._payment_limiter = TokenBucket(250, 60) + # All other operations: 1,000 requests / 10 minutes + self._general_limiter = TokenBucket(1000, 600) + + self.client = httpx.AsyncClient( + base_url=self.normalize_endpoint(settings.strike_api_endpoint), + headers={ + "Authorization": f"Bearer {settings.strike_api_key}", + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": settings.user_agent, + }, + timeout=httpx.Timeout(connect=5.0, read=40.0, write=10.0, pool=None), + transport=httpx.AsyncHTTPTransport( + limits=httpx.Limits(max_connections=20, max_keepalive_connections=10), + retries=0, # we handle retries ourselves + ), + ) + + # runtime state + self.pending_invoices: list[str] = [] # Keep it as a list + self.pending_payments: Dict[str, str] = {} + self.failed_payments: Dict[str, str] = {} + + # balance cache + self._cached_balance: Optional[int] = None + self._cached_balance_ts: float = 0.0 + self._cache_ttl = 30 # seconds + + async def cleanup(self): + try: + await self.client.aclose() + except Exception: + logger.warning("Error closing Strike client") + + # --------------------------------------------------------------------- # + # low-level request helpers # + # --------------------------------------------------------------------- # + + async def _req(self, method: str, path: str, /, **kw) -> httpx.Response: + """Make a Strike HTTP call with: + One Strike HTTP call with + • rate limiting based on endpoint type + • concurrency throttle + • exponential back-off + jitter + • explicit retry on 429/5xx + • latency logging + """ + # Apply the appropriate rate limiter based on the endpoint path. + if path.startswith("/invoices") or path.startswith("/receive-requests"): + await self._invoice_limiter.consume() + elif path.startswith("/payment-quotes"): + await self._payment_limiter.consume() + else: + await self._general_limiter.consume() + + async with self._sem: + return await self.client.request(method, path, **kw) + + # Typed wrappers - so call-sites stay tidy. + async def _get(self, path: str, **kw) -> httpx.Response: # GET request. + return await self._req("GET", path, **kw) + + async def _post(self, path: str, **kw) -> httpx.Response: + return await self._req("POST", path, **kw) + + async def _patch(self, path: str, **kw) -> httpx.Response: + return await self._req("PATCH", path, **kw) + + # --------------------------------------------------------------------- # + # LNbits wallet API implementation # + # --------------------------------------------------------------------- # + + async def status(self) -> StatusResponse: + """ + Return wallet balance (millisatoshis) with a 30-second cache. + """ + now = time.time() + if ( + self._cached_balance is not None + and now - self._cached_balance_ts < self._cache_ttl + ): + return StatusResponse(None, self._cached_balance) + + try: + r = await self._get("/balances") + r.raise_for_status() + data = r.json() + balances = data.get("data", []) if isinstance(data, dict) else data + btc = next((b for b in balances if b.get("currency") == "BTC"), None) + if btc and "available" in btc: + available_btc = Decimal(btc["available"]) # Get available BTC amount. + msats = int( + available_btc * Decimal(1e11) + ) # Convert BTC to millisatoshis. + self._cached_balance = msats + self._cached_balance_ts = now + return StatusResponse(None, msats) + + return StatusResponse(None, 0) + + except Exception as e: + logger.warning(e) + return StatusResponse("Connection error", 0) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: bytes | None = None, # Add this parameter + **kwargs, + ) -> InvoiceResponse: + try: + btc_amt = (Decimal(amount) / Decimal(1e8)).quantize( + Decimal("0.00000001") + ) # Convert amount from millisatoshis to BTC. + payload: Dict[str, Any] = { + "bolt11": { + "amount": { + "currency": "BTC", + "amount": str(btc_amt), + }, + "description": memo or "", + }, + "targetCurrency": "BTC", + } + if description_hash: + payload["bolt11"]["descriptionHash"] = description_hash.hex() + + r = await self._post( + "/receive-requests", + json=payload, + ) + r.raise_for_status() + resp = r.json() + invoice_id = resp.get("receiveRequestId") + bolt11 = resp.get("bolt11", {}).get("invoice") + if not invoice_id or not bolt11: + return InvoiceResponse( + ok=False, error_message="Invalid invoice response" + ) + + self.pending_invoices.append(invoice_id) + return InvoiceResponse( + ok=True, checking_id=invoice_id, payment_request=bolt11 + ) + except httpx.HTTPStatusError as e: + logger.warning(e) + msg = e.response.json().get("message", e.response.text) + return InvoiceResponse(ok=False, error_message=f"Strike API error: {msg}") + except Exception as e: + logger.warning(e) + return InvoiceResponse(ok=False, error_message="Connection error") + + async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + # 1) Create a payment quote. + q = await self._post( + "/payment-quotes/lightning", + json={"lnInvoice": bolt11}, + ) + q.raise_for_status() + quote_id = q.json().get("paymentQuoteId") + if not quote_id: + return PaymentResponse( + ok=False, error_message="Strike: missing payment quote Id" + ) + + # 2) Execute the payment quote. + e = await self._patch(f"/payment-quotes/{quote_id}/execute") + e.raise_for_status() + + data = e.json() if e.content else {} + payment_id = data.get("paymentId") + state = data.get("state", "").upper() + + # Network fee → msat. + fee_obj = data.get("lightningNetworkFee") or data.get("totalFee") or {} + fee_btc = Decimal(fee_obj.get("amount", "0")) + fee_msat = int(fee_btc * Decimal(1e11)) # millisatoshis. + + if state in {"SUCCEEDED", "COMPLETED"}: + preimage = data.get("preimage") or data.get("preImage") + return PaymentResponse( + ok=True, + checking_id=payment_id, + fee_msat=fee_msat, + preimage=preimage, + ) + + failed_states = { + "CANCELED", + "FAILED", + "TIMED_OUT", + } + if state in failed_states: + return PaymentResponse( + ok=False, checking_id=payment_id, error_message=f"State: {state}" + ) + + # Store mapping for later polling. + if payment_id: + # todo: this will be lost on server restart + self.pending_payments[payment_id] = quote_id + + # Treat all other states as pending (including unknown states). + return PaymentResponse(ok=None, checking_id=payment_id) + + except Exception as e: + logger.warning(e) + # Keep pending. Not sure if the payment went trough or not. + return PaymentResponse(ok=None, error_message="Connection error") + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + try: + r = await self._get(f"/receive-requests/{checking_id}/receives") + + if r.status_code == 200: + data = r.json() + items = data.get("items", []) + + if not items: + # Still pending. + return PaymentPendingStatus() + + for item in items: + if item.get("state") == "COMPLETED": + preimage = None + lightning_data = item.get("lightning") + if lightning_data: + preimage = lightning_data.get( + "preimage" + ) or lightning_data.get("preImage") + + return PaymentSuccessStatus(fee_msat=0, preimage=preimage) + + return PaymentPendingStatus() + + if r.status_code == 404: + logger.warning(f"Payment '{checking_id}' not found. Marking as failed.") + return PaymentFailedStatus(False) + + r.raise_for_status() + return PaymentPendingStatus() + + except httpx.HTTPStatusError as e: + logger.warning( + f"HTTPStatusError in get_invoice_status for checking_id {checking_id} " + f"on URL {e.request.url}: {e.response.status_code} - {e.response.text}" + ) + # Default to Pending to allow retries by paid_invoices_stream. + return PaymentPendingStatus() + except Exception as e: + logger.warning(e) + return PaymentPendingStatus() + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + quote_id = self.pending_payments.get(checking_id) + + try: + # Attempt 1: Use quote_id if available (from in-memory store) + if quote_id: + status = await self._get_payment_status_by_quote_id( + checking_id, quote_id + ) + if status: + return status + except Exception as e: + logger.warning(e) + logger.debug(f"Error while fetching payment by quote id {checking_id}.") + + try: + # Attempt 2: Fallback - Use paymentId (checking_id) directly. + return await self._get_payment_status_by_checking_id(checking_id) + except Exception as e: + logger.warning(e) + logger.debug(f"Error while fetching payment {checking_id}.") + return PaymentPendingStatus() + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + """ + Poll Strike for invoice settlement while respecting the documented API limits. + + Uses dynamic adjustment of polling frequency based on activity. + """ + min_poll, max_poll = 1, 15 + # 1,000 requests / 10 minutes = ~100 requests/minute. + rate_limit = 100 + sleep_s = min_poll + # Main loop for polling invoices. + self._running = True + + while self._running and settings.lnbits_running: + loop_start = time.time() + had_activity = False + + req_budget = max( + 1, rate_limit * sleep_s // 60 + ) # Calculate request budget based on sleep time. + processed = 0 + + for inv in list(self.pending_invoices): + if processed >= req_budget: # If request budget is exhausted. + break + status = await self.get_invoice_status(inv) + processed += 1 + + if status.success or status.failed: + self.pending_invoices.remove(inv) + if status.success: + had_activity = True + yield inv + + # Dynamic adjustment of polling frequency based on activity. + sleep_s = ( + max(min_poll, sleep_s // 2) + if had_activity + else min(max_poll, sleep_s * 2) + ) + + # Sleep to respect rate limits. + elapsed = time.time() - loop_start + # Ensure we respect the rate limit, even with dynamic adjustment. + min_sleep_for_rate = processed * 60 / rate_limit - elapsed + await asyncio.sleep(max(sleep_s, min_sleep_for_rate, 0)) + + # ------------------------------------------------------------------ # + # misc Strike helpers # + # ------------------------------------------------------------------ # + + async def get_invoices( + self, + filters: Optional[str] = None, + orderby: Optional[str] = None, + skip: Optional[int] = None, + top: Optional[int] = None, + ) -> Dict[str, Any]: + try: + params: Dict[str, Any] = {} + if filters: + params["$filter"] = filters + if orderby: + params["$orderby"] = orderby + if skip is not None: + params["$skip"] = skip + if top is not None: + params["$top"] = top + r = await self._get( + "/invoices", params=params + ) # Get invoices from Strike API. + r.raise_for_status() + return r.json() + except Exception: + logger.warning("Error in get_invoices()") + return {"error": "unable to fetch invoices"} + + async def _get_payment_status_by_quote_id( + self, checking_id: str, quote_id: str + ) -> Optional[PaymentStatus]: + resp = await self._get(f"/payment-quotes/{quote_id}") + resp.raise_for_status() + + data = resp.json() + state = data.get("state", "").upper() + preimage = data.get("preimage") or data.get("preImage") + + fee_msat = 0 + fee_obj = data.get("lightningNetworkFee") or data.get("totalFee") + if fee_obj and fee_obj.get("amount") and fee_obj.get("currency"): + amount_str = fee_obj.get("amount") + currency_str = fee_obj.get("currency").upper() + try: + if currency_str == "BTC": + fee_btc_decimal = Decimal(amount_str) + fee_msat = int(fee_btc_decimal * Decimal(1e11)) + elif currency_str == "SAT": + fee_sat_decimal = Decimal(amount_str) + fee_msat = int(fee_sat_decimal * 1000) + except Exception as e: + logger.warning(e) + logger.warning( + f"Fee parse error. Quote: '{quote_id}'. " + f"Payment '{checking_id}'." + ) + fee_msat = 0 + + if state in {"SUCCEEDED", "COMPLETED"}: + self.pending_payments.pop(checking_id, None) + return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) + if state == "FAILED": + self.pending_payments.pop(checking_id, None) + return PaymentFailedStatus() + + return None + + async def _get_payment_status_by_checking_id( + self, checking_id: str + ) -> PaymentStatus: + r_payment = await self._get(f"/payments/{checking_id}") + + if r_payment.status_code == 200: + data = r_payment.json() + state = data.get("state", "").upper() + preimage = None + fee_msat = 0 + + if state in {"SUCCEEDED", "COMPLETED"}: + self.pending_payments.pop(checking_id, None) + return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage) + if state == "FAILED": + self.pending_payments.pop(checking_id, None) + return PaymentFailedStatus() + + return PaymentPendingStatus() + + if r_payment.status_code == 400: + try: + error_data = r_payment.json() + # Check for Strike's specific validation + # error structure for paymentId format + if error_data.get("data", {}).get("code") == "INVALID_DATA": + validation_errors = error_data.get("data", {}).get( + "validationErrors", {} + ) + if "paymentId" in validation_errors: + for err_detail in validation_errors["paymentId"]: + is_invalid = err_detail.get( + "code" + ) == "INVALID_DATA" and "is not valid." in err_detail.get( + "message", "" + ) + if not is_invalid: + continue + logger.error( + f"Payment '{checking_id}' not a valid Strike payment. " + f"Marked as failed. Response: {r_payment.text}" + ) + self.pending_payments.pop(checking_id, None) + return PaymentFailedStatus() + except Exception as e: + logger.warning(e) + + return PaymentPendingStatus() + + if r_payment.status_code == 404: + logger.warning(f"Payment {checking_id} not found. Marking as failed.") + self.pending_payments.pop(checking_id, None) + return PaymentFailedStatus() + + logger.debug( + f"Error fetching payment {checking_id} directly: " + f"{r_payment.status_code} - {r_payment.text}" + ) + return PaymentPendingStatus()