Merge pull request #113 from lnbits/lnurl
This commit is contained in:
commit
3a56aaa3ad
11 changed files with 729 additions and 233 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import httpx
|
import httpx
|
||||||
from typing import Optional, Tuple, Dict
|
from typing import Optional, Tuple, Dict
|
||||||
from quart import g
|
from quart import g
|
||||||
from lnurl import LnurlWithdrawResponse
|
from lnurl import LnurlWithdrawResponse # type: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import TypedDict # type: ignore
|
from typing import TypedDict # type: ignore
|
||||||
|
|
@ -51,7 +51,12 @@ def create_invoice(
|
||||||
|
|
||||||
|
|
||||||
def pay_invoice(
|
def pay_invoice(
|
||||||
*, wallet_id: str, payment_request: str, max_sat: Optional[int] = None, extra: Optional[Dict] = None
|
*,
|
||||||
|
wallet_id: str,
|
||||||
|
payment_request: str,
|
||||||
|
max_sat: Optional[int] = None,
|
||||||
|
extra: Optional[Dict] = None,
|
||||||
|
description: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
temp_id = f"temp_{urlsafe_short_hash()}"
|
temp_id = f"temp_{urlsafe_short_hash()}"
|
||||||
internal_id = f"internal_{urlsafe_short_hash()}"
|
internal_id = f"internal_{urlsafe_short_hash()}"
|
||||||
|
|
@ -79,7 +84,7 @@ def pay_invoice(
|
||||||
payment_request=payment_request,
|
payment_request=payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
amount=-invoice.amount_msat,
|
amount=-invoice.amount_msat,
|
||||||
memo=invoice.description or "",
|
memo=description or invoice.description or "",
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -111,7 +116,7 @@ def pay_invoice(
|
||||||
else:
|
else:
|
||||||
# actually pay the external invoice
|
# actually pay the external invoice
|
||||||
payment: PaymentResponse = WALLET.pay_invoice(payment_request)
|
payment: PaymentResponse = WALLET.pay_invoice(payment_request)
|
||||||
if payment.ok:
|
if payment.ok and payment.checking_id:
|
||||||
create_payment(
|
create_payment(
|
||||||
checking_id=payment.checking_id,
|
checking_id=payment.checking_id,
|
||||||
fee=payment.fee_msat,
|
fee=payment.fee_msat,
|
||||||
|
|
@ -127,13 +132,10 @@ def pay_invoice(
|
||||||
|
|
||||||
|
|
||||||
async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo: Optional[str] = None) -> None:
|
async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo: Optional[str] = None) -> None:
|
||||||
if not memo:
|
|
||||||
memo = res.default_description
|
|
||||||
|
|
||||||
_, payment_request = create_invoice(
|
_, payment_request = create_invoice(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
amount=res.max_sats,
|
amount=res.max_sats,
|
||||||
memo=memo,
|
memo=memo or res.default_description or "",
|
||||||
extra={"tag": "lnurlwallet"},
|
extra={"tag": "lnurlwallet"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* globals moment, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart */
|
/* globals windowMixin, decode, Vue, VueQrcodeReader, VueQrcode, Quasar, LNbits, _, EventHub, Chart, decryptLnurlPayAES */
|
||||||
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
Vue.use(VueQrcodeReader)
|
Vue.use(VueQrcodeReader)
|
||||||
|
|
@ -14,12 +14,8 @@ function generateChart(canvas, payments) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_.each(
|
_.each(
|
||||||
payments
|
payments.filter(p => !p.pending).sort((a, b) => a.time - b.time),
|
||||||
.filter(p => !p.pending)
|
tx => {
|
||||||
.sort(function (a, b) {
|
|
||||||
return a.time - b.time
|
|
||||||
}),
|
|
||||||
function (tx) {
|
|
||||||
txs.push({
|
txs.push({
|
||||||
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
|
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
|
||||||
sat: tx.sat
|
sat: tx.sat
|
||||||
|
|
@ -27,19 +23,15 @@ function generateChart(canvas, payments) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_.each(_.groupBy(txs, 'hour'), function (value, day) {
|
_.each(_.groupBy(txs, 'hour'), (value, day) => {
|
||||||
var income = _.reduce(
|
var income = _.reduce(
|
||||||
value,
|
value,
|
||||||
function (memo, tx) {
|
(memo, tx) => (tx.sat >= 0 ? memo + tx.sat : memo),
|
||||||
return tx.sat >= 0 ? memo + tx.sat : memo
|
|
||||||
},
|
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
var outcome = _.reduce(
|
var outcome = _.reduce(
|
||||||
value,
|
value,
|
||||||
function (memo, tx) {
|
(memo, tx) => (tx.sat < 0 ? memo + Math.abs(tx.sat) : memo),
|
||||||
return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo
|
|
||||||
},
|
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
n = n + income - outcome
|
n = n + income - outcome
|
||||||
|
|
@ -124,21 +116,27 @@ new Vue({
|
||||||
show: false,
|
show: false,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
paymentReq: null,
|
paymentReq: null,
|
||||||
|
minMax: [0, 2100000000000000],
|
||||||
|
lnurl: null,
|
||||||
data: {
|
data: {
|
||||||
amount: null,
|
amount: null,
|
||||||
memo: ''
|
memo: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
send: {
|
parse: {
|
||||||
show: false,
|
show: false,
|
||||||
invoice: null,
|
invoice: null,
|
||||||
|
lnurlpay: null,
|
||||||
data: {
|
data: {
|
||||||
bolt11: ''
|
request: '',
|
||||||
}
|
amount: 0,
|
||||||
|
comment: ''
|
||||||
},
|
},
|
||||||
sendCamera: {
|
paymentChecker: null,
|
||||||
|
camera: {
|
||||||
show: false,
|
show: false,
|
||||||
camera: 'auto'
|
camera: 'auto'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
payments: [],
|
payments: [],
|
||||||
paymentsTable: {
|
paymentsTable: {
|
||||||
|
|
@ -196,8 +194,8 @@ new Vue({
|
||||||
return LNbits.utils.search(this.payments, q)
|
return LNbits.utils.search(this.payments, q)
|
||||||
},
|
},
|
||||||
canPay: function () {
|
canPay: function () {
|
||||||
if (!this.send.invoice) return false
|
if (!this.parse.invoice) return false
|
||||||
return this.send.invoice.sat <= this.balance
|
return this.parse.invoice.sat <= this.balance
|
||||||
},
|
},
|
||||||
pendingPaymentsExist: function () {
|
pendingPaymentsExist: function () {
|
||||||
return this.payments
|
return this.payments
|
||||||
|
|
@ -205,105 +203,184 @@ new Vue({
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
msatoshiFormat: function (value) {
|
||||||
|
return LNbits.utils.formatSat(value / 1000)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeCamera: function () {
|
closeCamera: function () {
|
||||||
this.sendCamera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
showCamera: function () {
|
showCamera: function () {
|
||||||
this.sendCamera.show = true
|
this.parse.camera.show = true
|
||||||
},
|
},
|
||||||
showChart: function () {
|
showChart: function () {
|
||||||
this.paymentsChart.show = true
|
this.paymentsChart.show = true
|
||||||
this.$nextTick(function () {
|
this.$nextTick(() => {
|
||||||
generateChart(this.$refs.canvas, this.payments)
|
generateChart(this.$refs.canvas, this.payments)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
showReceiveDialog: function () {
|
showReceiveDialog: function () {
|
||||||
this.receive = {
|
this.receive.show = true
|
||||||
show: true,
|
this.receive.status = 'pending'
|
||||||
status: 'pending',
|
this.receive.paymentReq = null
|
||||||
paymentReq: null,
|
this.receive.data.amount = null
|
||||||
data: {
|
this.receive.data.memo = null
|
||||||
amount: null,
|
this.receive.paymentChecker = null
|
||||||
memo: ''
|
this.receive.minMax = [0, 2100000000000000]
|
||||||
|
this.receive.lnurl = null
|
||||||
},
|
},
|
||||||
paymentChecker: null
|
showParseDialog: function () {
|
||||||
}
|
this.parse.show = true
|
||||||
},
|
this.parse.invoice = null
|
||||||
showSendDialog: function () {
|
this.parse.lnurlpay = null
|
||||||
this.send = {
|
this.parse.data.request = ''
|
||||||
show: true,
|
this.parse.data.comment = ''
|
||||||
invoice: null,
|
this.parse.data.paymentChecker = null
|
||||||
data: {
|
this.parse.camera.show = false
|
||||||
bolt11: ''
|
|
||||||
},
|
|
||||||
paymentChecker: null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
closeReceiveDialog: function () {
|
closeReceiveDialog: function () {
|
||||||
var checker = this.receive.paymentChecker
|
var checker = this.receive.paymentChecker
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
clearInterval(checker)
|
clearInterval(checker)
|
||||||
}, 10000)
|
}, 10000)
|
||||||
},
|
},
|
||||||
closeSendDialog: function () {
|
closeParseDialog: function () {
|
||||||
this.sendCamera.show = false
|
var checker = this.parse.paymentChecker
|
||||||
var checker = this.send.paymentChecker
|
setTimeout(() => {
|
||||||
setTimeout(function () {
|
|
||||||
clearInterval(checker)
|
clearInterval(checker)
|
||||||
}, 1000)
|
}, 10000)
|
||||||
},
|
},
|
||||||
createInvoice: function () {
|
createInvoice: function () {
|
||||||
var self = this
|
|
||||||
this.receive.status = 'loading'
|
this.receive.status = 'loading'
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.createInvoice(
|
.createInvoice(
|
||||||
this.g.wallet,
|
this.g.wallet,
|
||||||
this.receive.data.amount,
|
this.receive.data.amount,
|
||||||
this.receive.data.memo
|
this.receive.data.memo,
|
||||||
|
this.receive.lnurl && this.receive.lnurl.callback
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.receive.status = 'success'
|
this.receive.status = 'success'
|
||||||
self.receive.paymentReq = response.data.payment_request
|
this.receive.paymentReq = response.data.payment_request
|
||||||
|
|
||||||
self.receive.paymentChecker = setInterval(function () {
|
if (response.data.lnurl_response !== null) {
|
||||||
|
if (response.data.lnurl_response === false) {
|
||||||
|
response.data.lnurl_response = `Unable to connect`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof response.data.lnurl_response === 'string') {
|
||||||
|
// failure
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'negative',
|
||||||
|
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
|
||||||
|
caption: response.data.lnurl_response
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else if (response.data.lnurl_response === true) {
|
||||||
|
// success
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
||||||
|
spinner: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.receive.paymentChecker = setInterval(() => {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.getPayment(self.g.wallet, response.data.payment_hash)
|
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
if (response.data.paid) {
|
if (response.data.paid) {
|
||||||
self.fetchPayments()
|
this.fetchPayments()
|
||||||
self.receive.show = false
|
this.fetchBalance()
|
||||||
clearInterval(self.receive.paymentChecker)
|
this.receive.show = false
|
||||||
|
clearInterval(this.receive.paymentChecker)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(err => {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(err)
|
||||||
self.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
decodeQR: function (res) {
|
decodeQR: function (res) {
|
||||||
this.send.data.bolt11 = res
|
this.parse.data.request = res
|
||||||
this.decodeInvoice()
|
this.decodeRequest()
|
||||||
this.sendCamera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
decodeInvoice: function () {
|
decodeRequest: function () {
|
||||||
if (this.send.data.bolt11.startsWith('lightning:')) {
|
this.parse.show = true
|
||||||
this.send.data.bolt11 = this.send.data.bolt11.slice(10)
|
|
||||||
|
if (this.parse.data.request.startsWith('lightning:')) {
|
||||||
|
this.parse.data.request = this.parse.data.request.slice(10)
|
||||||
|
}
|
||||||
|
if (this.parse.data.request.startsWith('lnurl:')) {
|
||||||
|
this.parse.data.request = this.parse.data.request.slice(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.parse.data.request.toLowerCase().startsWith('lnurl1')) {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/api/v1/lnurlscan/' + this.parse.data.request,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
let data = response.data
|
||||||
|
|
||||||
|
if (data.status === 'ERROR') {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'warning',
|
||||||
|
message: `${data.domain} lnurl call failed.`,
|
||||||
|
caption: data.reason
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.kind === 'pay') {
|
||||||
|
this.parse.lnurlpay = Object.freeze(data)
|
||||||
|
this.parse.data.amount = data.minSendable / 1000
|
||||||
|
} else if (data.kind === 'withdraw') {
|
||||||
|
this.parse.show = false
|
||||||
|
this.receive.show = true
|
||||||
|
this.receive.status = 'pending'
|
||||||
|
this.receive.paymentReq = null
|
||||||
|
this.receive.data.amount = data.maxWithdrawable / 1000
|
||||||
|
this.receive.data.memo = data.defaultDescription
|
||||||
|
this.receive.minMax = [
|
||||||
|
data.minWithdrawable / 1000,
|
||||||
|
data.maxWithdrawable / 1000
|
||||||
|
]
|
||||||
|
this.receive.lnurl = {
|
||||||
|
domain: data.domain,
|
||||||
|
callback: data.callback,
|
||||||
|
fixed: data.fixed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let invoice
|
let invoice
|
||||||
try {
|
try {
|
||||||
invoice = decode(this.send.data.bolt11)
|
invoice = decode(this.parse.data.request)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: error + '.',
|
message: error + '.',
|
||||||
caption: '400 BAD REQUEST',
|
caption: '400 BAD REQUEST'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
|
this.parse.show = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,7 +390,7 @@ new Vue({
|
||||||
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
|
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
_.each(invoice.data.tags, function (tag) {
|
_.each(invoice.data.tags, tag => {
|
||||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||||
if (tag.description === 'payment_hash') {
|
if (tag.description === 'payment_hash') {
|
||||||
cleanInvoice.hash = tag.value
|
cleanInvoice.hash = tag.value
|
||||||
|
|
@ -332,78 +409,154 @@ new Vue({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.send.invoice = Object.freeze(cleanInvoice)
|
this.parse.invoice = Object.freeze(cleanInvoice)
|
||||||
},
|
},
|
||||||
payInvoice: function () {
|
payInvoice: function () {
|
||||||
var self = this
|
|
||||||
|
|
||||||
let dismissPaymentMsg = this.$q.notify({
|
let dismissPaymentMsg = this.$q.notify({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Processing payment...',
|
message: 'Processing payment...'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.payInvoice(this.g.wallet, this.send.data.bolt11)
|
.payInvoice(this.g.wallet, this.parse.data.request)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.send.paymentChecker = setInterval(function () {
|
this.parse.paymentChecker = setInterval(() => {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.getPayment(self.g.wallet, response.data.payment_hash)
|
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||||
.then(function (res) {
|
.then(res => {
|
||||||
if (res.data.paid) {
|
if (res.data.paid) {
|
||||||
self.send.show = false
|
this.parse.show = false
|
||||||
clearInterval(self.send.paymentChecker)
|
clearInterval(this.parse.paymentChecker)
|
||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
self.fetchPayments()
|
this.fetchPayments()
|
||||||
|
this.fetchBalance()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(err => {
|
||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
payLnurl: function () {
|
||||||
|
let dismissPaymentMsg = this.$q.notify({
|
||||||
|
timeout: 0,
|
||||||
|
message: 'Processing payment...'
|
||||||
|
})
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.payLnurl(
|
||||||
|
this.g.wallet,
|
||||||
|
this.parse.lnurlpay.callback,
|
||||||
|
this.parse.lnurlpay.description_hash,
|
||||||
|
this.parse.data.amount * 1000,
|
||||||
|
this.parse.lnurlpay.description.slice(0, 120),
|
||||||
|
this.parse.data.comment
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
this.parse.show = false
|
||||||
|
|
||||||
|
this.parse.paymentChecker = setInterval(() => {
|
||||||
|
LNbits.api
|
||||||
|
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||||
|
.then(res => {
|
||||||
|
if (res.data.paid) {
|
||||||
|
dismissPaymentMsg()
|
||||||
|
clearInterval(this.parse.paymentChecker)
|
||||||
|
this.fetchPayments()
|
||||||
|
this.fetchBalance()
|
||||||
|
|
||||||
|
// show lnurlpay success action
|
||||||
|
if (response.data.success_action) {
|
||||||
|
switch (response.data.success_action.tag) {
|
||||||
|
case 'url':
|
||||||
|
this.$q.notify({
|
||||||
|
message: `<a target="_blank" style="color: inherit" href="${response.data.success_action.url}">${response.data.success_action.url}</a>`,
|
||||||
|
caption: response.data.success_action.description,
|
||||||
|
html: true,
|
||||||
|
type: 'info',
|
||||||
|
timeout: 0,
|
||||||
|
closeBtn: true
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'message':
|
||||||
|
this.$q.notify({
|
||||||
|
message: response.data.success_action.message,
|
||||||
|
type: 'info',
|
||||||
|
timeout: 0,
|
||||||
|
closeBtn: true
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'aes':
|
||||||
|
LNbits.api
|
||||||
|
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||||
|
.then(
|
||||||
|
({data: payment}) =>
|
||||||
|
console.log(payment) ||
|
||||||
|
decryptLnurlPayAES(
|
||||||
|
response.data.success_action,
|
||||||
|
payment.preimage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(value => {
|
||||||
|
this.$q.notify({
|
||||||
|
message: value,
|
||||||
|
caption: response.data.success_action.description,
|
||||||
|
html: true,
|
||||||
|
type: 'info',
|
||||||
|
timeout: 0,
|
||||||
|
closeBtn: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dismissPaymentMsg()
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteWallet: function (walletId, user) {
|
deleteWallet: function (walletId, user) {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||||
.onOk(function () {
|
.onOk(() => {
|
||||||
LNbits.href.deleteWallet(walletId, user)
|
LNbits.href.deleteWallet(walletId, user)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchPayments: function (checkPending) {
|
fetchPayments: function (checkPending) {
|
||||||
var self = this
|
|
||||||
|
|
||||||
return LNbits.api
|
return LNbits.api
|
||||||
.getPayments(this.g.wallet, checkPending)
|
.getPayments(this.g.wallet, checkPending)
|
||||||
.then(function (response) {
|
.then(response => {
|
||||||
self.payments = response.data
|
this.payments = response.data
|
||||||
.map(function (obj) {
|
.map(obj => {
|
||||||
return LNbits.map.payment(obj)
|
return LNbits.map.payment(obj)
|
||||||
})
|
})
|
||||||
.sort(function (a, b) {
|
.sort((a, b) => {
|
||||||
return b.time - a.time
|
return b.time - a.time
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchBalance: function () {
|
fetchBalance: function () {
|
||||||
var self = this
|
LNbits.api.getWallet(this.g.wallet).then(response => {
|
||||||
LNbits.api.getWallet(self.g.wallet).then(function (response) {
|
this.balance = Math.round(response.data.balance / 1000)
|
||||||
self.balance = Math.round(response.data.balance / 1000)
|
|
||||||
EventHub.$emit('update-wallet-balance', [
|
EventHub.$emit('update-wallet-balance', [
|
||||||
self.g.wallet.id,
|
this.g.wallet.id,
|
||||||
self.balance
|
this.balance
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
checkPendingPayments: function () {
|
checkPendingPayments: function () {
|
||||||
var dismissMsg = this.$q.notify({
|
var dismissMsg = this.$q.notify({
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
message: 'Checking pending transactions...',
|
message: 'Checking pending transactions...'
|
||||||
icon: null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.fetchPayments(true).then(function () {
|
this.fetchPayments(true).then(() => {
|
||||||
dismissMsg()
|
dismissMsg()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="purple"
|
color="deep-purple"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
@click="showSendDialog"
|
@click="showParseDialog"
|
||||||
>Send</q-btn
|
>Paste Request</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
@ -26,9 +26,19 @@
|
||||||
color="deep-purple"
|
color="deep-purple"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
@click="showReceiveDialog"
|
@click="showReceiveDialog"
|
||||||
>Receive</q-btn
|
>Create Invoice</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="purple"
|
||||||
|
icon="photo_camera"
|
||||||
|
@click="showCamera"
|
||||||
|
>scan
|
||||||
|
<q-tooltip>Use camera to scan an invoice/QR</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
|
@ -120,7 +130,7 @@
|
||||||
<q-td auto-width key="sat" :props="props">
|
<q-td auto-width key="sat" :props="props">
|
||||||
{{ props.row.fsat }}
|
{{ props.row.fsat }}
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td auto-width key="sat" :props="props">
|
<q-td auto-width key="fee" :props="props">
|
||||||
{{ props.row.fee }}
|
{{ props.row.fee }}
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
|
|
@ -131,7 +141,9 @@
|
||||||
<div v-if="props.row.isIn && props.row.pending">
|
<div v-if="props.row.isIn && props.row.pending">
|
||||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||||
Invoice waiting to be paid
|
Invoice waiting to be paid
|
||||||
<lnbits-payment-details :payment="props.row"></lnbits-payment-details>
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
<div v-if="props.row.bolt11" class="text-center q-mb-lg">
|
<div v-if="props.row.bolt11" class="text-center q-mb-lg">
|
||||||
<a :href="'lightning:' + props.row.bolt11">
|
<a :href="'lightning:' + props.row.bolt11">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
<q-responsive :ratio="1" class="q-mx-xl">
|
||||||
|
|
@ -162,7 +174,9 @@
|
||||||
:color="'green'"
|
:color="'green'"
|
||||||
></q-icon>
|
></q-icon>
|
||||||
Payment Received
|
Payment Received
|
||||||
<lnbits-payment-details :payment="props.row"></lnbits-payment-details>
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="props.row.isPaid && props.row.isOut">
|
<div v-else-if="props.row.isPaid && props.row.isOut">
|
||||||
<q-icon
|
<q-icon
|
||||||
|
|
@ -171,12 +185,16 @@
|
||||||
:color="'pink'"
|
:color="'pink'"
|
||||||
></q-icon>
|
></q-icon>
|
||||||
Payment Sent
|
Payment Sent
|
||||||
<lnbits-payment-details :payment="props.row"></lnbits-payment-details>
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="props.row.isOut && props.row.pending">
|
<div v-else-if="props.row.isOut && props.row.pending">
|
||||||
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
<q-icon name="settings_ethernet" color="grey"></q-icon>
|
||||||
Outgoing payment pending
|
Outgoing payment pending
|
||||||
<lnbits-payment-details :payment="props.row"></lnbits-payment-details>
|
<lnbits-payment-details
|
||||||
|
:payment="props.row"
|
||||||
|
></lnbits-payment-details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -187,6 +205,7 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
|
|
@ -229,20 +248,27 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
|
||||||
|
{% raw %}
|
||||||
<q-card
|
<q-card
|
||||||
v-if="!receive.paymentReq"
|
v-if="!receive.paymentReq"
|
||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||||
>
|
>
|
||||||
<q-form @submit="createInvoice" class="q-gutter-md">
|
<q-form @submit="createInvoice" class="q-gutter-md">
|
||||||
|
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
||||||
|
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
|
||||||
|
</p>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.number="receive.data.amount"
|
v-model.number="receive.data.amount"
|
||||||
type="number"
|
type="number"
|
||||||
label="Amount (sat) *"
|
label="Amount (sat) *"
|
||||||
|
:min="receive.minMax[0]"
|
||||||
|
:max="receive.minMax[1]"
|
||||||
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
|
|
@ -257,8 +283,12 @@
|
||||||
color="deep-purple"
|
color="deep-purple"
|
||||||
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Create invoice</q-btn
|
|
||||||
>
|
>
|
||||||
|
<span v-if="receive.lnurl">
|
||||||
|
Withdraw from {{receive.lnurl.domain}}
|
||||||
|
</span>
|
||||||
|
<span v-else> Create invoice </span>
|
||||||
|
</q-btn>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<q-spinner
|
<q-spinner
|
||||||
|
|
@ -287,36 +317,117 @@
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
{% endraw %}
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="send.show" position="top" @hide="closeSendDialog">
|
<q-dialog v-model="parse.show" @hide="closeParseDialog">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div v-if="!send.invoice">
|
<div v-if="parse.invoice">
|
||||||
|
{% raw %}
|
||||||
|
<h6 class="q-my-none">{{ parse.invoice.fsat }} sat</h6>
|
||||||
|
<q-separator class="q-my-sm"></q-separator>
|
||||||
|
<p style="word-break: break-all">
|
||||||
|
<strong>Description:</strong> {{ parse.invoice.description }}<br />
|
||||||
|
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
|
||||||
|
<strong>Hash:</strong> {{ parse.invoice.hash }}
|
||||||
|
</p>
|
||||||
|
{% endraw %}
|
||||||
|
<div v-if="canPay" class="row q-mt-lg">
|
||||||
|
<q-btn unelevated color="deep-purple" @click="payInvoice">Pay</q-btn>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||||
|
</div>
|
||||||
|
<div v-else class="row q-mt-lg">
|
||||||
|
<q-btn unelevated disabled color="yellow" text-color="black"
|
||||||
|
>Not enough funds!</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="parse.lnurlpay">
|
||||||
|
{% raw %}
|
||||||
|
<q-form @submit="payLnurl" class="q-gutter-md">
|
||||||
|
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
||||||
|
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
|
||||||
|
parse.lnurlpay.maxSendable | msatoshiFormat }} sat
|
||||||
|
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||||
|
<br />
|
||||||
|
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p v-else class="q-my-none text-h6 text-center">
|
||||||
|
<b>{{ parse.lnurlpay.domain }}</b> is requesting <br />
|
||||||
|
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
|
||||||
|
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b> sat
|
||||||
|
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||||
|
<br />
|
||||||
|
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<q-separator class="q-my-sm"></q-separator>
|
||||||
|
<div class="row">
|
||||||
|
<p class="col text-justify text-italic">
|
||||||
|
{{ parse.lnurlpay.description }}
|
||||||
|
</p>
|
||||||
|
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
|
||||||
|
<q-img :src="parse.lnurlpay.image" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="parse.data.amount"
|
||||||
|
type="number"
|
||||||
|
label="Amount (sat) *"
|
||||||
|
:min="parse.lnurlpay.minSendable / 1000"
|
||||||
|
:max="parse.lnurlpay.maxSendable / 1000"
|
||||||
|
:readonly="parse.lnurlpay.fixed"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="parse.data.comment"
|
||||||
|
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
|
||||||
|
label="Comment (optional)"
|
||||||
|
:maxlength="parse.lnurlpay.commentAllowed"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn unelevated color="deep-purple" type="submit"
|
||||||
|
>Send satoshis</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
{% endraw %}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<q-form
|
<q-form
|
||||||
v-if="!sendCamera.show"
|
v-if="!parse.camera.show"
|
||||||
@submit="decodeInvoice"
|
@submit="decodeRequest"
|
||||||
class="q-gutter-md"
|
class="q-gutter-md"
|
||||||
>
|
>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="send.data.bolt11"
|
v-model.trim="parse.data.request"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
label="Paste an invoice *"
|
label="Paste an invoice, payment request or lnurl code *"
|
||||||
>
|
>
|
||||||
<template v-slot:after>
|
|
||||||
<q-btn round dense flat icon="photo_camera" @click="showCamera">
|
|
||||||
<q-tooltip>Use camera to scan an invoice</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
</q-input>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="deep-purple"
|
color="deep-purple"
|
||||||
:disable="send.data.bolt11 == ''"
|
:disable="parse.data.request == ''"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Read invoice</q-btn
|
>Read</q-btn
|
||||||
>
|
>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
>Cancel</q-btn
|
>Cancel</q-btn
|
||||||
|
|
@ -331,39 +442,29 @@
|
||||||
></qrcode-stream>
|
></qrcode-stream>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
|
||||||
>Cancel</q-btn
|
Cancel
|
||||||
>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
|
||||||
{% raw %}
|
|
||||||
<h6 class="q-my-none">{{ send.invoice.fsat }} sat</h6>
|
|
||||||
<q-separator class="q-my-sm"></q-separator>
|
|
||||||
<p style="word-break: break-all">
|
|
||||||
<strong>Memo:</strong> {{ send.invoice.description }}<br />
|
|
||||||
<strong>Expire date:</strong> {{ send.invoice.expireDate }}<br />
|
|
||||||
<strong>Hash:</strong> {{ send.invoice.hash }}
|
|
||||||
</p>
|
|
||||||
{% endraw %}
|
|
||||||
<div v-if="canPay" class="row q-mt-lg">
|
|
||||||
<q-btn unelevated color="deep-purple" @click="payInvoice"
|
|
||||||
>Send satoshis</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
|
||||||
</div>
|
|
||||||
<div v-else class="row q-mt-lg">
|
|
||||||
<q-btn unelevated disabled color="yellow" text-color="black"
|
|
||||||
>Not enough funds!</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="paymentsChart.show" position="top">
|
<q-dialog v-model="parse.camera.show">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl">
|
||||||
|
<div class="text-center q-mb-lg">
|
||||||
|
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="paymentsChart.show">
|
||||||
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<canvas ref="canvas" width="600" height="400"></canvas>
|
<canvas ref="canvas" width="600" height="400"></canvas>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import trio # type: ignore
|
import trio # type: ignore
|
||||||
import json
|
import json
|
||||||
|
import lnurl # type: ignore
|
||||||
|
import httpx
|
||||||
import traceback
|
import traceback
|
||||||
|
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
||||||
from quart import g, jsonify, request, make_response
|
from quart import g, jsonify, request, make_response
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
|
|
@ -47,6 +51,7 @@ async def api_payments():
|
||||||
"amount": {"type": "integer", "min": 1, "required": True},
|
"amount": {"type": "integer", "min": 1, "required": True},
|
||||||
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
|
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"},
|
||||||
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
||||||
|
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_payments_create_invoice():
|
async def api_payments_create_invoice():
|
||||||
|
|
@ -66,6 +71,22 @@ async def api_payments_create_invoice():
|
||||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
invoice = bolt11.decode(payment_request)
|
invoice = bolt11.decode(payment_request)
|
||||||
|
|
||||||
|
lnurl_response: Union[None, bool, str] = None
|
||||||
|
if g.data.get("lnurl_callback"):
|
||||||
|
try:
|
||||||
|
r = httpx.get(g.data["lnurl_callback"], params={"pr": payment_request}, timeout=10)
|
||||||
|
if r.is_error:
|
||||||
|
lnurl_response = r.text
|
||||||
|
else:
|
||||||
|
resp = json.loads(r.text)
|
||||||
|
if resp["status"] != "OK":
|
||||||
|
lnurl_response = resp["reason"]
|
||||||
|
else:
|
||||||
|
lnurl_response = True
|
||||||
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
|
lnurl_response = False
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
|
|
@ -73,6 +94,7 @@ async def api_payments_create_invoice():
|
||||||
"payment_request": payment_request,
|
"payment_request": payment_request,
|
||||||
# maintain backwards compatibility with API clients:
|
# maintain backwards compatibility with API clients:
|
||||||
"checking_id": invoice.payment_hash,
|
"checking_id": invoice.payment_hash,
|
||||||
|
"lnurl_response": lnurl_response,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
HTTPStatus.CREATED,
|
HTTPStatus.CREATED,
|
||||||
|
|
@ -113,6 +135,79 @@ async def api_payments_create():
|
||||||
return await api_payments_create_invoice()
|
return await api_payments_create_invoice()
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/api/v1/payments/lnurl", methods=["POST"])
|
||||||
|
@api_check_wallet_key("admin")
|
||||||
|
@api_validate_post_request(
|
||||||
|
schema={
|
||||||
|
"description_hash": {"type": "string", "empty": False, "required": True},
|
||||||
|
"callback": {"type": "string", "empty": False, "required": True},
|
||||||
|
"amount": {"type": "number", "empty": False, "required": True},
|
||||||
|
"comment": {"type": "string", "nullable": True, "empty": True, "required": False},
|
||||||
|
"description": {"type": "string", "nullable": True, "empty": True, "required": False},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def api_payments_pay_lnurl():
|
||||||
|
try:
|
||||||
|
r = httpx.get(
|
||||||
|
g.data["callback"],
|
||||||
|
params={"amount": g.data["amount"], "comment": g.data["comment"]},
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
if r.is_error:
|
||||||
|
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
|
||||||
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
|
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
params = json.loads(r.text)
|
||||||
|
if params.get("status") == "ERROR":
|
||||||
|
domain = urlparse(g.data["callback"]).netloc
|
||||||
|
return jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
invoice = bolt11.decode(params["pr"])
|
||||||
|
if invoice.amount_msat != g.data["amount"]:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
|
||||||
|
}
|
||||||
|
),
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if invoice.description_hash != g.data["description_hash"]:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
|
||||||
|
}
|
||||||
|
),
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payment_hash = pay_invoice(
|
||||||
|
wallet_id=g.wallet.id,
|
||||||
|
payment_request=params["pr"],
|
||||||
|
description=g.data.get("description", ""),
|
||||||
|
extra={"success_action": params.get("successAction")},
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
traceback.print_exc(7)
|
||||||
|
g.db.rollback()
|
||||||
|
return jsonify({"message": str(exc)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success_action": params.get("successAction"),
|
||||||
|
"payment_hash": payment_hash,
|
||||||
|
# maintain backwards compatibility with API clients:
|
||||||
|
"checking_id": payment_hash,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
HTTPStatus.CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
|
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_payment(payment_hash):
|
async def api_payment(payment_hash):
|
||||||
|
|
@ -121,14 +216,14 @@ async def api_payment(payment_hash):
|
||||||
if not payment:
|
if not payment:
|
||||||
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
|
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
|
||||||
elif not payment.pending:
|
elif not payment.pending:
|
||||||
return jsonify({"paid": True}), HTTPStatus.OK
|
return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payment.check_pending()
|
payment.check_pending()
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"paid": False}), HTTPStatus.OK
|
return jsonify({"paid": False}), HTTPStatus.OK
|
||||||
|
|
||||||
return jsonify({"paid": not payment.pending}), HTTPStatus.OK
|
return jsonify({"paid": not payment.pending, "preimage": payment.preimage}), HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/payments/sse", methods=["GET"])
|
@core_app.route("/api/v1/payments/sse", methods=["GET"])
|
||||||
|
|
@ -183,3 +278,55 @@ async def api_payments_sse():
|
||||||
)
|
)
|
||||||
response.timeout = None
|
response.timeout = None
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/api/v1/lnurlscan/<code>", methods=["GET"])
|
||||||
|
@api_check_wallet_key("invoice")
|
||||||
|
async def api_lnurlscan(code: str):
|
||||||
|
try:
|
||||||
|
url = lnurl.Lnurl(code)
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({"error": "invalid lnurl"}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
domain = urlparse(url.url).netloc
|
||||||
|
if url.is_login:
|
||||||
|
return jsonify({"domain": domain, "kind": "auth", "error": "unsupported"})
|
||||||
|
|
||||||
|
r = httpx.get(url.url)
|
||||||
|
if r.is_error:
|
||||||
|
return jsonify({"domain": domain, "error": "failed to get parameters"})
|
||||||
|
|
||||||
|
try:
|
||||||
|
jdata = json.loads(r.text)
|
||||||
|
data: lnurl.LnurlResponseModel = lnurl.LnurlResponse.from_dict(jdata)
|
||||||
|
except (json.decoder.JSONDecodeError, lnurl.exceptions.LnurlResponseException):
|
||||||
|
return jsonify({"domain": domain, "error": f"got invalid response '{r.text[:200]}'"})
|
||||||
|
|
||||||
|
if type(data) is lnurl.LnurlChannelResponse:
|
||||||
|
return jsonify({"domain": domain, "kind": "channel", "error": "unsupported"})
|
||||||
|
|
||||||
|
params: Dict = data.dict()
|
||||||
|
if type(data) is lnurl.LnurlWithdrawResponse:
|
||||||
|
params.update(kind="withdraw")
|
||||||
|
params.update(fixed=data.min_withdrawable == data.max_withdrawable)
|
||||||
|
|
||||||
|
# callback with k1 already in it
|
||||||
|
parsed_callback: ParseResult = urlparse(data.callback)
|
||||||
|
qs: Dict = parse_qs(parsed_callback.query)
|
||||||
|
qs["k1"] = data.k1
|
||||||
|
parsed_callback = parsed_callback._replace(query=urlencode(qs, doseq=True))
|
||||||
|
params.update(callback=urlunparse(parsed_callback))
|
||||||
|
|
||||||
|
if type(data) is lnurl.LnurlPayResponse:
|
||||||
|
params.update(kind="pay")
|
||||||
|
params.update(fixed=data.min_sendable == data.max_sendable)
|
||||||
|
params.update(description_hash=data.metadata.h)
|
||||||
|
params.update(description=data.metadata.text)
|
||||||
|
if data.metadata.images:
|
||||||
|
image = min(data.metadata.images, key=lambda image: len(image[1]))
|
||||||
|
data_uri = "data:" + image[0] + "," + image[1]
|
||||||
|
params.update(image=data_uri)
|
||||||
|
params.update(commentAllowed=jdata.get("commentAllowed", 0))
|
||||||
|
|
||||||
|
params.update(domain=domain)
|
||||||
|
return jsonify(params)
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,7 @@
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits LNURL-pay link</h6>
|
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits LNURL-pay link</h6>
|
||||||
<p class="q-my-none">
|
<p class="q-my-none">Use an LNURL compatible bitcoin wallet to pay.</p>
|
||||||
Use an LNURL compatible bitcoin wallet to pay.
|
|
||||||
</p>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
/* globals moment, Vue, EventHub, axios, Quasar, _ */
|
/* globals crypto, moment, Vue, axios, Quasar, _ */
|
||||||
|
|
||||||
var LOCALE = 'en'
|
window.LOCALE = 'en'
|
||||||
|
window.EventHub = new Vue()
|
||||||
var EventHub = new Vue()
|
window.LNbits = {
|
||||||
|
|
||||||
var LNbits = {
|
|
||||||
api: {
|
api: {
|
||||||
request: function (method, url, apiKey, data) {
|
request: function (method, url, apiKey, data) {
|
||||||
return axios({
|
return axios({
|
||||||
|
|
@ -16,11 +14,12 @@ var LNbits = {
|
||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createInvoice: function (wallet, amount, memo) {
|
createInvoice: function (wallet, amount, memo, lnurlCallback = null) {
|
||||||
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
||||||
out: false,
|
out: false,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
memo: memo
|
memo: memo,
|
||||||
|
lnurl_callback: lnurlCallback
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payInvoice: function (wallet, bolt11) {
|
payInvoice: function (wallet, bolt11) {
|
||||||
|
|
@ -29,6 +28,22 @@ var LNbits = {
|
||||||
bolt11: bolt11
|
bolt11: bolt11
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
payLnurl: function (
|
||||||
|
wallet,
|
||||||
|
callback,
|
||||||
|
description_hash,
|
||||||
|
amount,
|
||||||
|
description = '',
|
||||||
|
comment = ''
|
||||||
|
) {
|
||||||
|
return this.request('post', '/api/v1/payments/lnurl', wallet.adminkey, {
|
||||||
|
callback,
|
||||||
|
description_hash,
|
||||||
|
amount,
|
||||||
|
comment,
|
||||||
|
description
|
||||||
|
})
|
||||||
|
},
|
||||||
getWallet: function (wallet) {
|
getWallet: function (wallet) {
|
||||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||||
},
|
},
|
||||||
|
|
@ -91,7 +106,7 @@ var LNbits = {
|
||||||
)
|
)
|
||||||
obj.msat = obj.balance
|
obj.msat = obj.balance
|
||||||
obj.sat = Math.round(obj.balance / 1000)
|
obj.sat = Math.round(obj.balance / 1000)
|
||||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
|
obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat)
|
||||||
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('')
|
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('')
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
|
|
@ -119,7 +134,7 @@ var LNbits = {
|
||||||
obj.msat = obj.amount
|
obj.msat = obj.amount
|
||||||
obj.sat = obj.msat / 1000
|
obj.sat = obj.msat / 1000
|
||||||
obj.tag = obj.extra.tag
|
obj.tag = obj.extra.tag
|
||||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.sat)
|
obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat)
|
||||||
obj.isIn = obj.amount > 0
|
obj.isIn = obj.amount > 0
|
||||||
obj.isOut = obj.amount < 0
|
obj.isOut = obj.amount < 0
|
||||||
obj.isPaid = obj.pending === 0
|
obj.isPaid = obj.pending === 0
|
||||||
|
|
@ -142,13 +157,13 @@ var LNbits = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
formatCurrency: function (value, currency) {
|
formatCurrency: function (value, currency) {
|
||||||
return new Intl.NumberFormat(LOCALE, {
|
return new Intl.NumberFormat(window.LOCALE, {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: currency
|
currency: currency
|
||||||
}).format(value)
|
}).format(value)
|
||||||
},
|
},
|
||||||
formatSat: function (value) {
|
formatSat: function (value) {
|
||||||
return new Intl.NumberFormat(LOCALE).format(value)
|
return new Intl.NumberFormat(window.LOCALE).format(value)
|
||||||
},
|
},
|
||||||
notifyApiError: function (error) {
|
notifyApiError: function (error) {
|
||||||
var types = {
|
var types = {
|
||||||
|
|
@ -231,7 +246,7 @@ var LNbits = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var windowMixin = {
|
window.windowMixin = {
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
g: {
|
g: {
|
||||||
|
|
@ -261,17 +276,17 @@ var windowMixin = {
|
||||||
created: function () {
|
created: function () {
|
||||||
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
|
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
|
||||||
if (window.user) {
|
if (window.user) {
|
||||||
this.g.user = Object.freeze(LNbits.map.user(window.user))
|
this.g.user = Object.freeze(window.LNbits.map.user(window.user))
|
||||||
}
|
}
|
||||||
if (window.wallet) {
|
if (window.wallet) {
|
||||||
this.g.wallet = Object.freeze(LNbits.map.wallet(window.wallet))
|
this.g.wallet = Object.freeze(window.LNbits.map.wallet(window.wallet))
|
||||||
}
|
}
|
||||||
if (window.extensions) {
|
if (window.extensions) {
|
||||||
var user = this.g.user
|
var user = this.g.user
|
||||||
this.g.extensions = Object.freeze(
|
this.g.extensions = Object.freeze(
|
||||||
window.extensions
|
window.extensions
|
||||||
.map(function (data) {
|
.map(function (data) {
|
||||||
return LNbits.map.extension(data)
|
return window.LNbits.map.extension(data)
|
||||||
})
|
})
|
||||||
.map(function (obj) {
|
.map(function (obj) {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
@ -288,3 +303,27 @@ var windowMixin = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.decryptLnurlPayAES = function (success_action, preimage) {
|
||||||
|
let keyb = new Uint8Array(
|
||||||
|
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
||||||
|
)
|
||||||
|
|
||||||
|
return crypto.subtle
|
||||||
|
.importKey('raw', keyb, {name: 'AES-CBC', length: 256}, false, ['decrypt'])
|
||||||
|
.then(key => {
|
||||||
|
let ivb = Uint8Array.from(window.atob(success_action.iv), c =>
|
||||||
|
c.charCodeAt(0)
|
||||||
|
)
|
||||||
|
let ciphertextb = Uint8Array.from(
|
||||||
|
window.atob(success_action.ciphertext),
|
||||||
|
c => c.charCodeAt(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
return crypto.subtle.decrypt({name: 'AES-CBC', iv: ivb}, key, ciphertextb)
|
||||||
|
})
|
||||||
|
.then(valueb => {
|
||||||
|
let decoder = new TextDecoder('utf-8')
|
||||||
|
return decoder.decode(valueb)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* global Vue, moment, LNbits, EventHub */
|
/* global Vue, moment, LNbits, EventHub, decryptLnurlPayAES */
|
||||||
|
|
||||||
Vue.component('lnbits-fsat', {
|
Vue.component('lnbits-fsat', {
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -199,10 +199,64 @@ Vue.component('lnbits-payment-details', {
|
||||||
<div class="col-3"><b>Payment hash</b>:</div>
|
<div class="col-3"><b>Payment hash</b>:</div>
|
||||||
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
|
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" v-if="payment.preimage">
|
<div class="row" v-if="hasPreimage">
|
||||||
<div class="col-3"><b>Payment proof</b>:</div>
|
<div class="col-3"><b>Payment proof</b>:</div>
|
||||||
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
|
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" v-if="hasSuccessAction">
|
||||||
|
<div class="col-3"><b>Success action</b>:</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<lnbits-lnurlpay-success-action
|
||||||
|
:payment="payment"
|
||||||
|
:success_action="payment.extra.success_action"
|
||||||
|
></lnbits-lnurlpay-success-action>
|
||||||
</div>
|
</div>
|
||||||
`
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
computed: {
|
||||||
|
hasPreimage() {
|
||||||
|
return (
|
||||||
|
this.payment.preimage &&
|
||||||
|
this.payment.preimage !==
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hasSuccessAction() {
|
||||||
|
return (
|
||||||
|
this.hasPreimage &&
|
||||||
|
this.payment.extra &&
|
||||||
|
this.payment.extra.success_action
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Vue.component('lnbits-lnurlpay-success-action', {
|
||||||
|
props: ['payment', 'success_action'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
decryptedValue: this.success_action.ciphertext
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<p class="q-mb-sm">{{ success_action.message || success_action.description }}</p>
|
||||||
|
<code v-if="decryptedValue" class="text-h6 q-mt-sm q-mb-none">
|
||||||
|
{{ decryptedValue }}
|
||||||
|
</code>
|
||||||
|
<p v-else-if="success_action.url" class="text-h6 q-mt-sm q-mb-none">
|
||||||
|
<a target="_blank" style="color: inherit;" :href="success_action.url">{{ success_action.url }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mounted: function () {
|
||||||
|
if (this.success_action.tag !== 'aes') return null
|
||||||
|
|
||||||
|
decryptLnurlPayAES(this.success_action, this.payment.preimage).then(
|
||||||
|
value => {
|
||||||
|
this.decryptedValue = value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class PaymentStatus(NamedTuple):
|
||||||
|
|
||||||
class Wallet(ABC):
|
class Wallet(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def status() -> StatusResponse:
|
def status(self) -> StatusResponse:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class LNPayWallet(Wallet):
|
||||||
try:
|
try:
|
||||||
r = httpx.get(url, headers=self.auth)
|
r = httpx.get(url, headers=self.auth)
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
return StatusResponse(f"Unable to connect to '{url}'")
|
return StatusResponse(f"Unable to connect to '{url}'", 0)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return StatusResponse(r.text[:250], 0)
|
return StatusResponse(r.text[:250], 0)
|
||||||
|
|
@ -34,7 +34,7 @@ class LNPayWallet(Wallet):
|
||||||
f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}", 0
|
f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}", 0
|
||||||
)
|
)
|
||||||
|
|
||||||
return StatusResponse(None, data["balance"] / 1000)
|
return StatusResponse(None, data["balance"] * 1000)
|
||||||
|
|
||||||
def create_invoice(
|
def create_invoice(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class OpenNodeWallet(Wallet):
|
||||||
try:
|
try:
|
||||||
r = httpx.get(f"{self.endpoint}/v1/account/balance", headers=self.auth)
|
r = httpx.get(f"{self.endpoint}/v1/account/balance", headers=self.auth)
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
return StatusResponse(f"Unable to connect to '{self.endpoint}'")
|
return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0)
|
||||||
|
|
||||||
data = r.json()["message"]
|
data = r.json()["message"]
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ class SparkWallet(Wallet):
|
||||||
params = args
|
params = args
|
||||||
elif kwargs:
|
elif kwargs:
|
||||||
params = kwargs
|
params = kwargs
|
||||||
|
else:
|
||||||
|
params = {}
|
||||||
|
|
||||||
r = httpx.post(self.url + "/rpc", headers={"X-Access": self.token}, json={"method": key, "params": params})
|
r = httpx.post(self.url + "/rpc", headers={"X-Access": self.token}, json={"method": key, "params": params})
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue