Merge remote-tracking branch 'origin/master' into Jukebox

This commit is contained in:
Ben Arc 2021-06-06 22:14:36 +01:00
commit 3faf426945
22 changed files with 356 additions and 96 deletions

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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)

View 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

View file

@ -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\
![create event](https://i.imgur.com/dadK1dp.jpg)
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
![event info](https://imgur.com/KAv68Yr.jpg)
3. Share the event registration link\
![event ticket](https://imgur.com/AQWUOBY.jpg)
- ticket example\
![ticket example](https://i.imgur.com/trAVSLd.jpg)
- QR code ticket, presented after invoice paid, to present at registration\
![event ticket](https://i.imgur.com/M0ROM82.jpg)
4. Use the built-in ticket scanner to validate registered, and paid, attendees\
![ticket scanner](https://i.imgur.com/zrm9202.jpg)

View file

@ -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()
} }
}) })

View file

@ -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

View file

@ -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)
[![video tutorial livestream](http://img.youtube.com/vi/zDrSWShKz7k/0.jpg)](https://youtu.be/zDrSWShKz7k 'video tutorial offline shop')
## Usage
1. Start by adding a track\
![add new track](https://i.imgur.com/Cu0eGrW.jpg)
- 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\
![track settings](https://i.imgur.com/HTJYwcW.jpg)
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\
![adjust percentage](https://i.imgur.com/9weHKAB.jpg)
3. For every different producer added, when adding tracks, a wallet is generated for them\
![producer wallet](https://i.imgur.com/YFIZ7Tm.jpg)
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\
![play tracks](https://i.imgur.com/7ytiBkq.jpg)
6. You'll see the current track playing and a green icon indicating active track also\
![active track](https://i.imgur.com/W1vBz54.jpg)
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\
![producer wallet](https://i.imgur.com/OM9LawA.jpg)
## 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://cdn.shopify.com/s/files/1/0826/9235/files/cryptograffiti_logo_clear_background.png?v=1504730421)](https://cryptograffiti.com/) [![](https://cdn.shopify.com/s/files/1/0826/9235/files/cryptograffiti_logo_clear_background.png?v=1504730421)](https://cryptograffiti.com/)

View file

@ -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

View file

@ -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>

View file

@ -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\
![new contact form](https://i.imgur.com/kZqWGPe.png)
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\
![form settings](https://i.imgur.com/AsXeVet.png)
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\
![forms section](https://i.imgur.com/gg71HhM.png)
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\
![user view of form](https://i.imgur.com/DWGJWQz.png)
- after submiting the Lightning Network invoice will pop up and after payment the message will be sent to you\
![contact form payment](https://i.imgur.com/7heGsiO.png)
5. Back in "Support ticket" extension you'll get the messages your fans, users, haters, etc, sent you on the _Tickets_ section\
![tickets](https://i.imgur.com/dGhJ6Ok.png)

View file

@ -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()

View file

@ -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)\
![create lnurlp](https://i.imgur.com/rhUBJFy.jpg)
- 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\
![LNURLp](https://i.imgur.com/C8s1P0Q.jpg)
- you can now open your LNURLp and copy the LNURL, get the shareable link or print it\
![view lnurlp](https://i.imgur.com/4n41S7T.jpg)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -1 +1,36 @@
# Offline Shop # Offline Shop
## Create QR codes for each product and display them on your store for receiving payments Offline
[![video tutorial offline shop](http://img.youtube.com/vi/_XAvM_LNsoo/0.jpg)](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\
![offline shop back office](https://i.imgur.com/Ei7cxj9.png)
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\
![add new item](https://i.imgur.com/pkZqRgj.png)
3. After creating some products, click on "PRINT QR CODES"\
![print qr codes](https://i.imgur.com/2GAiSTe.png)
4. You'll see a QR code for each product in your LNBits Offline Shop with a title and price ready for printing\
![qr codes sheet](https://i.imgur.com/faEqOcd.png)
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](https://i.imgur.com/9aM6NUL.png)
- 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 authenticator](https://i.imgur.com/MrJXFxz.png)
- 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\
![disable confirmations](https://i.imgur.com/2OFs4yi.png)
- Nothing, disables the need for confirmation of payment, click the "DISABLE CONFIRMATION CODES"

View file

@ -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"\
![create new paywall](https://i.imgur.com/q0ZIekC.png)
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.\
![paywall config](https://i.imgur.com/CBW48F6.png)
3. You can then use your paywall link to secure your awesome content\
![paywall link](https://i.imgur.com/hDQmCDf.png)
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\
![user paywall view](https://i.imgur.com/3pLywkZ.png)

View file

@ -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\
![create](https://imgur.com/8jNj8Zq.jpg)
3. Open TPOS on the browser\
![open](https://imgur.com/LZuoWzb.jpg)
4. Present invoice QR to costumer\
![pay](https://imgur.com/tOwxn77.jpg)

View file

@ -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](https://i.imgur.com/qHi7ExL.png) 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\
![quick vouchers](https://i.imgur.com/IUfwdQz.jpg)
- 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\
![lnurlw created](https://i.imgur.com/X00twiX.jpg)
- on details you can print the vouchers\
![printable vouchers](https://i.imgur.com/2xLHbob.jpg)
- every printed LNURLw QR code is unique, it can only be used once
#### Advanced
1. Create the Advanced LNURLw\
![create advanced lnurlw](https://i.imgur.com/OR0f885.jpg)
- 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\
![lnurlw created](https://i.imgur.com/X00twiX.jpg)
**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!
![](https://i.imgur.com/2zZ7mi8.jpg)