Merge remote-tracking branch 'origin/master' into Jukebox
This commit is contained in:
commit
3faf426945
22 changed files with 356 additions and 96 deletions
|
|
@ -44,7 +44,9 @@ LND_REST_MACAROON="HEXSTRING"
|
||||||
|
|
||||||
# LNPayWallet
|
# LNPayWallet
|
||||||
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/
|
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/
|
||||||
|
# Secret API Key under developers tab
|
||||||
LNPAY_API_KEY=LNPAY_API_KEY
|
LNPAY_API_KEY=LNPAY_API_KEY
|
||||||
|
# Wallet Admin in Wallet Access Keys
|
||||||
LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
|
LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
|
||||||
|
|
||||||
# LntxbotWallet
|
# LntxbotWallet
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ core_app: Blueprint = Blueprint(
|
||||||
|
|
||||||
from .views.api import * # noqa
|
from .views.api import * # noqa
|
||||||
from .views.generic import * # noqa
|
from .views.generic import * # noqa
|
||||||
|
from .views.public_api import * # noqa
|
||||||
from .tasks import register_listeners
|
from .tasks import register_listeners
|
||||||
|
|
||||||
from lnbits.tasks import record_async
|
from lnbits.tasks import record_async
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from . import db
|
||||||
from .crud import get_balance_notify
|
from .crud import get_balance_notify
|
||||||
from .models import Payment
|
from .models import Payment
|
||||||
|
|
||||||
sse_listeners: List[trio.MemorySendChannel] = []
|
api_invoice_listeners: List[trio.MemorySendChannel] = []
|
||||||
|
|
||||||
|
|
||||||
async def register_listeners():
|
async def register_listeners():
|
||||||
|
|
@ -20,7 +20,7 @@ async def register_listeners():
|
||||||
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
async for payment in invoice_paid_chan:
|
async for payment in invoice_paid_chan:
|
||||||
# send information to sse channel
|
# send information to sse channel
|
||||||
await dispatch_sse(payment)
|
await dispatch_invoice_listener(payment)
|
||||||
|
|
||||||
# dispatch webhook
|
# dispatch webhook
|
||||||
if payment.webhook and not payment.webhook_status:
|
if payment.webhook and not payment.webhook_status:
|
||||||
|
|
@ -40,13 +40,13 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_sse(payment: Payment):
|
async def dispatch_invoice_listener(payment: Payment):
|
||||||
for send_channel in sse_listeners:
|
for send_channel in api_invoice_listeners:
|
||||||
try:
|
try:
|
||||||
send_channel.send_nowait(payment)
|
send_channel.send_nowait(payment)
|
||||||
except trio.WouldBlock:
|
except trio.WouldBlock:
|
||||||
print("removing sse listener", send_channel)
|
print("removing sse listener", send_channel)
|
||||||
sse_listeners.remove(send_channel)
|
api_invoice_listeners.remove(send_channel)
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_webhook(payment: Payment):
|
async def dispatch_webhook(payment: Payment):
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from ..services import (
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
perform_lnurlauth,
|
perform_lnurlauth,
|
||||||
)
|
)
|
||||||
from ..tasks import sse_listeners
|
from ..tasks import api_invoice_listeners
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/wallet", methods=["GET"])
|
@core_app.route("/api/v1/wallet", methods=["GET"])
|
||||||
|
|
@ -295,7 +295,7 @@ async def api_payments_sse():
|
||||||
send_payment, receive_payment = trio.open_memory_channel(0)
|
send_payment, receive_payment = trio.open_memory_channel(0)
|
||||||
|
|
||||||
print("adding sse listener", send_payment)
|
print("adding sse listener", send_payment)
|
||||||
sse_listeners.append(send_payment)
|
api_invoice_listeners.append(send_payment)
|
||||||
|
|
||||||
send_event, event_to_send = trio.open_memory_channel(0)
|
send_event, event_to_send = trio.open_memory_channel(0)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,10 @@ async def lnurl_full_withdraw_callback():
|
||||||
pr = request.args.get("pr")
|
pr = request.args.get("pr")
|
||||||
|
|
||||||
async def pay():
|
async def pay():
|
||||||
|
try:
|
||||||
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
|
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
current_app.nursery.start_soon(pay)
|
current_app.nursery.start_soon(pay)
|
||||||
|
|
||||||
|
|
|
||||||
37
lnbits/core/views/public_api.py
Normal file
37
lnbits/core/views/public_api.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import trio # type: ignore
|
||||||
|
import datetime
|
||||||
|
from http import HTTPStatus
|
||||||
|
from quart import jsonify
|
||||||
|
|
||||||
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
from .. import core_app
|
||||||
|
from ..crud import get_standalone_payment
|
||||||
|
from ..tasks import api_invoice_listeners
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.route("/public/v1/payment/<payment_hash>", methods=["GET"])
|
||||||
|
async def api_public_payment_longpolling(payment_hash):
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
|
||||||
|
if not payment:
|
||||||
|
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
|
||||||
|
elif not payment.pending:
|
||||||
|
return jsonify({"status": "paid"}), HTTPStatus.OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
invoice = bolt11.decode(payment.bolt11)
|
||||||
|
expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
||||||
|
if expiration < datetime.datetime.now():
|
||||||
|
return jsonify({"status": "expired"}), HTTPStatus.OK
|
||||||
|
except:
|
||||||
|
return jsonify({"message": "Invalid bolt11 invoice."}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
send_payment, receive_payment = trio.open_memory_channel(0)
|
||||||
|
|
||||||
|
print("adding standalone invoice listener", payment_hash, send_payment)
|
||||||
|
api_invoice_listeners.append(send_payment)
|
||||||
|
|
||||||
|
async for payment in receive_payment:
|
||||||
|
if payment.payment_hash == payment_hash:
|
||||||
|
return jsonify({"status": "paid"}), HTTPStatus.OK
|
||||||
|
|
@ -1,3 +1,33 @@
|
||||||
<h1>Events</h1>
|
# Events
|
||||||
<h2>Events: Sell and register event tickets</h2>
|
|
||||||
Events alows you to make a wave of tickets for an event, each ticket is in the form of a unqiue QRcode, which the user presents at registration. Events comes with a shareable ticket scanner, which can be used to register attendees.
|
## Sell tickets for events and use the built-in scanner for registering attendants
|
||||||
|
|
||||||
|
Events alows you to make tickets for an event. Each ticket is in the form of a uniqque QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance.
|
||||||
|
|
||||||
|
Events includes a shareable ticket scanner, which can be used to register attendees.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create an event\
|
||||||
|

|
||||||
|
2. Fill out the event information:
|
||||||
|
|
||||||
|
- event name
|
||||||
|
- wallet (normally there's only one)
|
||||||
|
- event information
|
||||||
|
- closing date for event registration
|
||||||
|
- begin and end date of the event
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Share the event registration link\
|
||||||
|

|
||||||
|
|
||||||
|
- ticket example\
|
||||||
|

|
||||||
|
|
||||||
|
- QR code ticket, presented after invoice paid, to present at registration\
|
||||||
|

|
||||||
|
|
||||||
|
4. Use the built-in ticket scanner to validate registered, and paid, attendees\
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
<script>
|
<script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
Vue.use(VueQrcodeReader)
|
Vue.use(VueQrcodeReader)
|
||||||
var mapEvents = function (obj) {
|
var mapEvents = function(obj) {
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
new Date(obj.time * 1000),
|
new Date(obj.time * 1000),
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
tickets: [],
|
tickets: [],
|
||||||
ticketsTable: {
|
ticketsTable: {
|
||||||
|
|
@ -119,35 +119,35 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hoverEmail: function (tmp) {
|
hoverEmail: function(tmp) {
|
||||||
this.tickets.data.emailtemp = tmp
|
this.tickets.data.emailtemp = tmp
|
||||||
},
|
},
|
||||||
closeCamera: function () {
|
closeCamera: function() {
|
||||||
this.sendCamera.show = false
|
this.sendCamera.show = false
|
||||||
},
|
},
|
||||||
showCamera: function () {
|
showCamera: function() {
|
||||||
this.sendCamera.show = true
|
this.sendCamera.show = true
|
||||||
},
|
},
|
||||||
decodeQR: function (res) {
|
decodeQR: function(res) {
|
||||||
this.sendCamera.show = false
|
this.sendCamera.show = false
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/events/api/v1/register/ticket/' + res)
|
.request('GET', '/events/api/v1/register/ticket/' + res)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.$q.notify({
|
self.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Registered!'
|
message: 'Registered!'
|
||||||
})
|
})
|
||||||
setTimeout(function () {
|
setTimeout(function() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getEventTickets: function () {
|
getEventTickets: function() {
|
||||||
var self = this
|
var self = this
|
||||||
console.log('obj')
|
console.log('obj')
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
|
@ -155,17 +155,17 @@
|
||||||
'GET',
|
'GET',
|
||||||
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
|
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.tickets = response.data.map(function (obj) {
|
self.tickets = response.data.map(function(obj) {
|
||||||
return mapEvents(obj)
|
return mapEvents(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function() {
|
||||||
this.getEventTickets()
|
this.getEventTickets()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,9 @@ async def api_event_register_ticket(ticket_id):
|
||||||
if not ticket:
|
if not ticket:
|
||||||
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
|
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
|
if not ticket.paid:
|
||||||
|
return jsonify({"message": "Ticket not paid for."}), HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
if ticket.registered == True:
|
if ticket.registered == True:
|
||||||
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
|
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,45 @@
|
||||||
# DJ Livestream
|
# DJ Livestream
|
||||||
|
|
||||||
An extension to help DJs to conduct music livestreams.
|
## Help DJ's and music producers conduct music livestreams
|
||||||
|
|
||||||
It produces a single static QR code that can be shown on screen. Once someone scans that QR code with an lnurl-pay capable wallet they will see the name of the track being played at that time and the name of the producer.
|
LNBits Livestream extension produces a static QR code that can be shown on screen while livestreaming a DJ set for example. If someone listening to the livestream likes a song and want to support the DJ and/or the producer he can scan the QR code with a LNURL-pay capable wallet.
|
||||||
|
|
||||||
They will then be given the opportunity to send a tip and a message related to that specific track and if they pay an amount over a specific threshold they will be given a link to download it (optional).
|
When scanned, the QR code sends up information about the song playing at the moment (name and the producer of that song). Also, if the user likes the song and would like to support the producer, he can send a tip and a message for that specific track. If the user sends an amount over a specific threshold they will be given a link to download it (optional).
|
||||||
|
|
||||||
The revenue will be sent to a wallet created specifically for that producer, with optional revenue splitting between the DJ and the producer.
|
The revenue will be sent to a wallet created specifically for that producer, with optional revenue splitting between the DJ and the producer.
|
||||||
|
|
||||||
|
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||||
|
|
||||||
|
[](https://youtu.be/zDrSWShKz7k 'video tutorial offline shop')
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Start by adding a track\
|
||||||
|

|
||||||
|
- set the producer, or choose an existing one
|
||||||
|
- set the track name
|
||||||
|
- define a minimum price where a user can download the track
|
||||||
|
- set the download URL, where user will be redirected if he tips the livestream and the tip is equal or above the set price\
|
||||||
|

|
||||||
|
2. Adjust the percentage of the pay you want to take from the user's tips. 10%, the default, means that the DJ will keep 10% of all the tips sent by users. The other 90% will go to an auto generated producer wallet\
|
||||||
|

|
||||||
|
3. For every different producer added, when adding tracks, a wallet is generated for them\
|
||||||
|

|
||||||
|
4. On the bottom of the LNBits DJ Livestream extension you'll find the static QR code ([LNURL-pay](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) you can add to the livestream or if you're a street performer you can print it and have it displayed
|
||||||
|
5. After all tracks and producers are added, you can start "playing" songs\
|
||||||
|

|
||||||
|
6. You'll see the current track playing and a green icon indicating active track also\
|
||||||
|

|
||||||
|
7. When a user scans the QR code, and sends a tip, you'll receive 10%, in the example case, in your wallet and the producer's wallet will get the rest. For example someone tips 100 sats, you'll get 10 sats and the producer will get 90 sats
|
||||||
|
- producer's wallet receiving 18 sats from 20 sats tips\
|
||||||
|

|
||||||
|
|
||||||
|
## Use cases
|
||||||
|
|
||||||
|
You can print the QR code and display it on a live gig, a street performance, etc... OR you can use the QR as an overlay in an online stream of you playing music, doing a DJ set, making a podcast.
|
||||||
|
|
||||||
|
You can use the extension's API to trigger updates for the current track, update fees, add tracks...
|
||||||
|
|
||||||
## Sponsored by
|
## Sponsored by
|
||||||
|
|
||||||
[](https://cryptograffiti.com/)
|
[](https://cryptograffiti.com/)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ new Vue({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getTrackLabel(trackId) {
|
getTrackLabel(trackId) {
|
||||||
|
if (!trackId) return
|
||||||
let track = this.tracksMap[trackId]
|
let track = this.tracksMap[trackId]
|
||||||
return `${track.name}, ${this.producersMap[track.producer].name}`
|
return `${track.name}, ${this.producersMap[track.producer].name}`
|
||||||
},
|
},
|
||||||
|
|
@ -162,6 +163,7 @@ new Vue({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateCurrentTrack(track) {
|
updateCurrentTrack(track) {
|
||||||
|
console.log(this.nextCurrentTrack, this.livestream)
|
||||||
if (this.livestream.current_track === track) {
|
if (this.livestream.current_track === track) {
|
||||||
// if clicking the same, stop it
|
// if clicking the same, stop it
|
||||||
track = 0
|
track = 0
|
||||||
|
|
@ -175,6 +177,7 @@ new Vue({
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.livestream.current_track = track
|
this.livestream.current_track = track
|
||||||
|
this.nextCurrentTrack = track
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
message: `Current track updated.`,
|
message: `Current track updated.`,
|
||||||
timeout: 700
|
timeout: 700
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<q-btn unelevated color="deep-purple" type="submit">
|
<q-btn unelevated color="deep-purple" type="submit">
|
||||||
{{ nextCurrentTrack === livestream.current_track ? 'Stop' : 'Set'
|
{{ nextCurrentTrack && nextCurrentTrack ===
|
||||||
}} current track
|
livestream.current_track ? 'Stop' : 'Set' }} current track
|
||||||
</q-btn>
|
</q-btn>
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,29 @@
|
||||||
<h1>Support Tickets</h1>
|
# Support Tickets
|
||||||
<h2>Get paid sats to answer questions</h2>
|
|
||||||
Charge people per word for contacting you. Possible applications include, paid support ticketing, PAYG language services, contact spam protection.
|
## Get paid sats to answer questions
|
||||||
|
|
||||||
|
Charge a per word amount for people to contact you.
|
||||||
|
|
||||||
|
Possible applications include, paid support ticketing, PAYG language services, contact spam protection.
|
||||||
|
|
||||||
|
1. Click "NEW FORM" to create a new contact form\
|
||||||
|

|
||||||
|
2. Fill out the contact form
|
||||||
|
- set the wallet to use
|
||||||
|
- give your form a name
|
||||||
|
- set an optional webhook that will get called when the form receives a payment
|
||||||
|
- give it a small description
|
||||||
|
- set the amount you want to charge, per **word**, for people to contact you\
|
||||||
|

|
||||||
|
3. Your new contact form will appear on the _Forms_ section. Note that you can create various forms with different rates per word, for different purposes\
|
||||||
|

|
||||||
|
4. When a user wants to reach out to you, they will get to the contact form. They can fill out some information:
|
||||||
|
- a name
|
||||||
|
- an optional email if they want you to reply
|
||||||
|
- and the actual message
|
||||||
|
- at the bottom, a value in satoshis, will display how much it will cost them to send this message\
|
||||||
|

|
||||||
|
- after submiting the Lightning Network invoice will pop up and after payment the message will be sent to you\
|
||||||
|

|
||||||
|
5. Back in "Support ticket" extension you'll get the messages your fans, users, haters, etc, sent you on the _Tickets_ section\
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script>
|
||||||
var mapLNTicket = function (obj) {
|
var mapLNTicket = function(obj) {
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
new Date(obj.time * 1000),
|
new Date(obj.time * 1000),
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
|
|
@ -243,7 +243,7 @@
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
forms: [],
|
forms: [],
|
||||||
tickets: [],
|
tickets: [],
|
||||||
|
|
@ -294,7 +294,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getTickets: function () {
|
getTickets: function() {
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
|
@ -303,40 +303,40 @@
|
||||||
'/lnticket/api/v1/tickets?all_wallets',
|
'/lnticket/api/v1/tickets?all_wallets',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.tickets = response.data.map(function (obj) {
|
self.tickets = response.data.map(function(obj) {
|
||||||
return mapLNTicket(obj)
|
return mapLNTicket(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteTicket: function (ticketId) {
|
deleteTicket: function(ticketId) {
|
||||||
var self = this
|
var self = this
|
||||||
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||||
|
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this ticket')
|
.confirmDialog('Are you sure you want to delete this ticket')
|
||||||
.onOk(function () {
|
.onOk(function() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/lnticket/api/v1/tickets/' + ticketId,
|
'/lnticket/api/v1/tickets/' + ticketId,
|
||||||
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.tickets = _.reject(self.tickets, function (obj) {
|
self.tickets = _.reject(self.tickets, function(obj) {
|
||||||
return obj.id == ticketId
|
return obj.id == ticketId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
exportticketsCSV: function () {
|
exportticketsCSV: function() {
|
||||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||||
},
|
},
|
||||||
|
|
||||||
getForms: function () {
|
getForms: function() {
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
|
@ -345,13 +345,13 @@
|
||||||
'/lnticket/api/v1/forms?all_wallets',
|
'/lnticket/api/v1/forms?all_wallets',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.forms = response.data.map(function (obj) {
|
self.forms = response.data.map(function(obj) {
|
||||||
return mapLNTicket(obj)
|
return mapLNTicket(obj)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sendFormData: function () {
|
sendFormData: function() {
|
||||||
var wallet = _.findWhere(this.g.user.wallets, {
|
var wallet = _.findWhere(this.g.user.wallets, {
|
||||||
id: this.formDialog.data.wallet
|
id: this.formDialog.data.wallet
|
||||||
})
|
})
|
||||||
|
|
@ -364,20 +364,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createForm: function (wallet, data) {
|
createForm: function(wallet, data) {
|
||||||
var self = this
|
var self = this
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
|
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.forms.push(mapLNTicket(response.data))
|
self.forms.push(mapLNTicket(response.data))
|
||||||
self.formDialog.show = false
|
self.formDialog.show = false
|
||||||
self.formDialog.data = {}
|
self.formDialog.data = {}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateformDialog: function (formId) {
|
updateformDialog: function(formId) {
|
||||||
var link = _.findWhere(this.forms, {id: formId})
|
var link = _.findWhere(this.forms, {id: formId})
|
||||||
console.log(link.id)
|
console.log(link.id)
|
||||||
this.formDialog.data.id = link.id
|
this.formDialog.data.id = link.id
|
||||||
|
|
@ -387,7 +387,7 @@
|
||||||
this.formDialog.data.costpword = link.costpword
|
this.formDialog.data.costpword = link.costpword
|
||||||
this.formDialog.show = true
|
this.formDialog.show = true
|
||||||
},
|
},
|
||||||
updateForm: function (wallet, data) {
|
updateForm: function(wallet, data) {
|
||||||
var self = this
|
var self = this
|
||||||
console.log(data)
|
console.log(data)
|
||||||
|
|
||||||
|
|
@ -398,47 +398,47 @@
|
||||||
wallet.inkey,
|
wallet.inkey,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.forms = _.reject(self.forms, function (obj) {
|
self.forms = _.reject(self.forms, function(obj) {
|
||||||
return obj.id == data.id
|
return obj.id == data.id
|
||||||
})
|
})
|
||||||
self.forms.push(mapLNTicket(response.data))
|
self.forms.push(mapLNTicket(response.data))
|
||||||
self.formDialog.show = false
|
self.formDialog.show = false
|
||||||
self.formDialog.data = {}
|
self.formDialog.data = {}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteForm: function (formsId) {
|
deleteForm: function(formsId) {
|
||||||
var self = this
|
var self = this
|
||||||
var forms = _.findWhere(this.forms, {id: formsId})
|
var forms = _.findWhere(this.forms, {id: formsId})
|
||||||
|
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this form link?')
|
.confirmDialog('Are you sure you want to delete this form link?')
|
||||||
.onOk(function () {
|
.onOk(function() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/lnticket/api/v1/forms/' + formsId,
|
'/lnticket/api/v1/forms/' + formsId,
|
||||||
_.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey
|
_.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function(response) {
|
||||||
self.forms = _.reject(self.forms, function (obj) {
|
self.forms = _.reject(self.forms, function(obj) {
|
||||||
return obj.id == formsId
|
return obj.id == formsId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
exportformsCSV: function () {
|
exportformsCSV: function() {
|
||||||
LNbits.utils.exportCSV(this.formsTable.columns, this.forms)
|
LNbits.utils.exportCSV(this.formsTable.columns, this.forms)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
created: function() {
|
||||||
if (this.g.user.wallets.length) {
|
if (this.g.user.wallets.length) {
|
||||||
this.getTickets()
|
this.getTickets()
|
||||||
this.getForms()
|
this.getForms()
|
||||||
|
|
|
||||||
|
|
@ -1 +1,27 @@
|
||||||
# LNURLp
|
# LNURLp
|
||||||
|
|
||||||
|
## Create a static QR code people can use to pay over Lightning Network
|
||||||
|
|
||||||
|
LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL-pay is a link that wallets use to fetch an invoice from a server on-demand. The link or QR code is fixed, but each time it is read by a compatible wallet a new invoice is issued by the service and sent to the wallet.
|
||||||
|
|
||||||
|
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create an LNURLp (New Pay link)\
|
||||||
|

|
||||||
|
|
||||||
|
- select your wallets
|
||||||
|
- make a small description
|
||||||
|
- enter amount
|
||||||
|
- if _Fixed amount_ is unchecked you'll have the option to configure a Max and Min amount
|
||||||
|
- you can set the currency to something different than sats. For example if you choose EUR, the satoshi amount will be calculated when a user scans the LNURLp
|
||||||
|
- You can ask the user to send a comment that will be sent along with the payment (for example a comment to a blog post)
|
||||||
|
- Webhook URL allows to call an URL when the LNURLp is paid
|
||||||
|
- Success mesage, will send a message back to the user after a successful payment, for example a thank you note
|
||||||
|
- Success URL, will send back a clickable link to the user. Access to some hidden content, or a download link
|
||||||
|
|
||||||
|
2. Use the shareable link or view the LNURLp you just created\
|
||||||
|

|
||||||
|
- you can now open your LNURLp and copy the LNURL, get the shareable link or print it\
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -95,10 +95,13 @@ async def api_lnurl_callback(link_id):
|
||||||
extra={"tag": "lnurlp", "link": link.id, "comment": comment},
|
extra={"tag": "lnurlp", "link": link.id, "comment": comment},
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = LnurlPayActionResponse(
|
resp = {
|
||||||
pr=payment_request,
|
"routes": [],
|
||||||
success_action=link.success_action(payment_hash),
|
"pr": payment_request,
|
||||||
routes=[],
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(resp.dict()), HTTPStatus.OK
|
success_action = link.success_action(payment_hash)
|
||||||
|
if success_action:
|
||||||
|
resp["success_action"] = success_action
|
||||||
|
|
||||||
|
return jsonify(), HTTPStatus.OK
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ from typing import NamedTuple, Optional, Dict
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from lnurl import Lnurl, encode as lnurl_encode # type: ignore
|
from lnurl import Lnurl, encode as lnurl_encode # type: ignore
|
||||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||||
from lnurl.models import LnurlPaySuccessAction, MessageAction, UrlAction # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class PayLink(NamedTuple):
|
class PayLink(NamedTuple):
|
||||||
|
|
@ -36,15 +35,21 @@ class PayLink(NamedTuple):
|
||||||
def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
def lnurlpay_metadata(self) -> LnurlPayMetadata:
|
||||||
return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))
|
return LnurlPayMetadata(json.dumps([["text/plain", self.description]]))
|
||||||
|
|
||||||
def success_action(self, payment_hash: str) -> Optional[LnurlPaySuccessAction]:
|
def success_action(self, payment_hash: str) -> Optional[Dict]:
|
||||||
if self.success_url:
|
if self.success_url:
|
||||||
url: ParseResult = urlparse(self.success_url)
|
url: ParseResult = urlparse(self.success_url)
|
||||||
qs: Dict = parse_qs(url.query)
|
qs: Dict = parse_qs(url.query)
|
||||||
qs["payment_hash"] = payment_hash
|
qs["payment_hash"] = payment_hash
|
||||||
url = url._replace(query=urlencode(qs, doseq=True))
|
url = url._replace(query=urlencode(qs, doseq=True))
|
||||||
raw: str = urlunparse(url)
|
return {
|
||||||
return UrlAction(url=raw, description=self.success_text)
|
"tag": "url",
|
||||||
|
"description": self.success_text or "~",
|
||||||
|
"url": urlunparse(url),
|
||||||
|
}
|
||||||
elif self.success_text:
|
elif self.success_text:
|
||||||
return MessageAction(message=self.success_text)
|
return {
|
||||||
|
"tag": "message",
|
||||||
|
"message": self.success_text,
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,11 @@ async def api_link_create_or_update(link_id=None):
|
||||||
):
|
):
|
||||||
return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST
|
return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
if g.data["success_url"][:8] != "https://":
|
if "success_url" in g.data and g.data["success_url"][:8] != "https://":
|
||||||
return jsonify({"message": "Success URL must be secure https://..."}), HTTPStatus.BAD_REQUEST
|
return (
|
||||||
|
jsonify({"message": "Success URL must be secure https://..."}),
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if link_id:
|
if link_id:
|
||||||
link = await get_pay_link(link_id)
|
link = await get_pay_link(link_id)
|
||||||
|
|
|
||||||
|
|
@ -1 +1,36 @@
|
||||||
# Offline Shop
|
# Offline Shop
|
||||||
|
|
||||||
|
## Create QR codes for each product and display them on your store for receiving payments Offline
|
||||||
|
|
||||||
|
[](https://youtu.be/_XAvM_LNsoo 'video tutorial offline shop')
|
||||||
|
|
||||||
|
LNBits Offline Shop allows for merchants to receive Bitcoin payments while offline and without any electronic device.
|
||||||
|
|
||||||
|
Merchant will create items and associate a QR code ([a LNURLp](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) with a price. He can then print the QR codes and display them on their shop. When a costumer chooses an item, scans the QR code, gets the description and price. After payment, the costumer gets a confirmation code that the merchant can validate to be sure the payment was successful.
|
||||||
|
|
||||||
|
Costumers must use an LNURL pay capable wallet.
|
||||||
|
|
||||||
|
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Entering the Offline shop extension you'll see an Items list, the Shop wallet and a Wordslist\
|
||||||
|

|
||||||
|
2. Begin by creating an item, click "ADD NEW ITEM"
|
||||||
|
- set the item name and a small description
|
||||||
|
- you can set an optional, preferably square image, that will show up on the costumer wallet - _depending on wallet_
|
||||||
|
- set the item price, if you choose a fiat currency the bitcoin conversion will happen at the time costumer scans to pay\
|
||||||
|

|
||||||
|
3. After creating some products, click on "PRINT QR CODES"\
|
||||||
|

|
||||||
|
4. You'll see a QR code for each product in your LNBits Offline Shop with a title and price ready for printing\
|
||||||
|

|
||||||
|
5. Place the printed QR codes on your shop, or at the fair stall, or have them as a menu style laminated sheet
|
||||||
|
6. Choose what type of confirmation do you want costumers to report to merchant after a successful payment\
|
||||||
|

|
||||||
|
|
||||||
|
- Wordlist is the default option: after a successful payment the costumer will receive a word from this list, **sequentially**. Starting in _albatross_ as costumers pay for the items they will get the next word in the list until _zebra_, then it starts at the top again. The list can be changed, for example if you think A-Z is a big list to track, you can use _apple_, _banana_, _coconut_\
|
||||||
|

|
||||||
|
- TOTP (time-based one time password) can be used instead. If you use Google Authenticator just scan the presented QR with the app and after a successful payment the user will get the password that you can check with GA\
|
||||||
|

|
||||||
|
- Nothing, disables the need for confirmation of payment, click the "DISABLE CONFIRMATION CODES"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
<h1>Example Extension</h1>
|
# Paywall
|
||||||
<h2>*tagline*</h2>
|
|
||||||
This is an example extension to help you organise and build you own.
|
|
||||||
|
|
||||||
Try to include an image
|
## Hide content behind a paywall, a user has to pay some amount to access your hidden content
|
||||||
<img src="https://i.imgur.com/9i4xcQB.png">
|
|
||||||
|
|
||||||
|
A Paywall is a way of restricting to content via a purchase or paid subscription. For example to read a determined blog post, or to continue reading further, to access a downloads area, etc...
|
||||||
|
|
||||||
<h2>If your extension has API endpoints, include useful ones here</h2>
|
## Usage
|
||||||
|
|
||||||
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
1. Create a paywall by clicking "NEW PAYWALL"\
|
||||||
|

|
||||||
|
2. Fill the options for your PAYWALL
|
||||||
|
- select the wallet
|
||||||
|
- set the link that will be unlocked after a successful payment
|
||||||
|
- give your paywall a _Title_
|
||||||
|
- an optional small description
|
||||||
|
- and set an amount a user must pay to access the hidden content. Note this is the minimum amount, a user can over pay if they wish
|
||||||
|
- if _Remember payments_ is checked, a returning paying user won't have to pay again for the same content.\
|
||||||
|

|
||||||
|
3. You can then use your paywall link to secure your awesome content\
|
||||||
|

|
||||||
|
4. When a user wants to access your hidden content, he can use the minimum amount or increase and click the "_Check icon_" to generate an invoice, user will then be redirected to the content page\
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
<h1> TPOS</h1>
|
# TPoS
|
||||||
<h2>A Shareable PoS that doesnt need to be installed and can run in phones browser!</h2>
|
|
||||||
<p>If someone drops your Quickening in a beer at your bar, staff can scan a QR and open/use a browser based PoS, linked to the same self-hosted lnbits instance!</p>
|
|
||||||
|
|
||||||
<img src="https://i.imgur.com/8wgTWDn.jpg">
|
## A Shareable PoS (Point of Sale) that doesn't need to be installed and can run in the browser!
|
||||||
|
|
||||||
|
An easy, fast and secure way to accept Bitcoin, over Lightning Network, at your business. The PoS is isolated from the wallet, so it's safe for any employee to use. You can create as many TPOS's as you need, for example one for each employee, or one for each branch of your business.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Enable extension
|
||||||
|
2. Create a TPOS\
|
||||||
|

|
||||||
|
3. Open TPOS on the browser\
|
||||||
|

|
||||||
|
4. Present invoice QR to costumer\
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,46 @@
|
||||||
# LNURLw
|
# LNURLw
|
||||||
## Withdraw link maker
|
|
||||||
LNURL withdraw is a very powerful tool and should not have his use limited to just faucet applications. With LNURL withdraw, you have the ability to give someone the right to spend a range, once or multiple times. This functionality has not existed in money before.
|
|
||||||
https://github.com/btcontract/lnurl-rfc/blob/master/spec.md#3-lnurl-withdraw
|
|
||||||
|
|
||||||
With this extension to can create/edit LNURL withdraws, set a min/max amount, set time (useful for subscription services)
|
## Create a static QR code people can use to withdraw funds from a Lightning Network wallet
|
||||||
|
|
||||||

|
LNURL is a range of lightning-network standards that allow us to use lightning-network differently. An LNURL withdraw is the permission for someone to pull a certain amount of funds from a lightning wallet.
|
||||||
|
|
||||||
|
The most common use case for an LNURL withdraw is a faucet, although it is a very powerful technology, with much further reaching implications. For example, an LNURL withdraw could be minted to pay for a subscription service. Or you can have a LNURLw as an offline Lightning wallet (a pre paid "card"), you use to pay for something without having to even reach your smartphone.
|
||||||
|
|
||||||
## API endpoint - /withdraw/api/v1/lnurlmaker
|
LNURL withdraw is a **very powerful tool** and should not have his use limited to just faucet applications. With LNURL withdraw, you have the ability to give someone the right to spend a range, once or multiple times. **This functionality has not existed in money before**.
|
||||||
Easily fetch one-off LNURLw
|
|
||||||
|
|
||||||
curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/withdraw/api/v1/lnurlmaker -d '{"amount":"100","memo":"ATM"}' -H "X-Api-Key: YOUR-WALLET-ADMIN-KEY"
|
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
#### Quick Vouchers
|
||||||
|
|
||||||
|
LNBits Quick Vouchers allows you to easily create a batch of LNURLw's QR codes that you can print and distribute as rewards, onboarding people into Lightning Network, gifts, etc...
|
||||||
|
|
||||||
|
1. Create Quick Vouchers\
|
||||||
|

|
||||||
|
- select wallet
|
||||||
|
- set the amount each voucher will allow someone to withdraw
|
||||||
|
- set the amount of vouchers you want to create - _have in mind you need to have a balance on the wallet that supports the amount \* number of vouchers_
|
||||||
|
2. You can now print, share, display your LNURLw links or QR codes\
|
||||||
|

|
||||||
|
- on details you can print the vouchers\
|
||||||
|

|
||||||
|
- every printed LNURLw QR code is unique, it can only be used once
|
||||||
|
|
||||||
|
#### Advanced
|
||||||
|
|
||||||
|
1. Create the Advanced LNURLw\
|
||||||
|

|
||||||
|
- set the wallet
|
||||||
|
- set a title for the LNURLw (it will show up in users wallet)
|
||||||
|
- define the minimum and maximum a user can withdraw, if you want a fixed amount set them both to an equal value
|
||||||
|
- set how many times can the LNURLw be scanned, if it's a one time use or it can be scanned 100 times
|
||||||
|
- LNBits has the "_Time between withdraws_" setting, you can define how long the LNURLw will be unavailable between scans
|
||||||
|
- you can set the time in _seconds, minutes or hours_
|
||||||
|
- the "_Use unique withdraw QR..._" reduces the chance of your LNURL withdraw being exploited and depleted by one person, by generating a new QR code every time it's scanned
|
||||||
|
2. Print, share or display your LNURLw link or it's QR code\
|
||||||
|

|
||||||
|
|
||||||
|
**LNBits bonus:** If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with their smartphone camera, or a QR scanner app, they can follow the link provided to claim their satoshis and get an instant LNBits wallet!
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue