Overview
The LNURL implementation in libwallet provides support for LNURL-withdraw, enabling users to withdraw funds from LNURL-enabled services. The protocol converts LNURL codes into actionable Lightning invoice requests.
LNURL is a protocol specification for Lightning Network services defined at github.com/lnurl/luds. LNURL-withdraw is specified in LUD-03.
Event System
LNURL operations use an event-driven architecture with status updates and error reporting:
LNURLEvent Structure
type LNURLEvent struct {
Code int // Status or error code
Message string // Human-readable message
Metadata *LNURLEventMetadata // Additional event data
}
type LNURLEventMetadata struct {
Host string // LNURL service host
Invoice string // Generated invoice
}
Event Listener Interface
type LNURLListener interface {
OnUpdate(e *LNURLEvent) // Called for status updates
OnError(e *LNURLEvent) // Called when errors occur
}
Status Codes
The implementation defines the following status and error codes:
Status Codes (>= 100)
LNURLStatusContacting = 110 // Contacting LNURL service
LNURLStatusInvoiceCreated = 120 // Invoice created successfully
LNURLStatusReceiving = 130 // Receiving payment
Error Codes (< 100)
LNURLErrDecode // Failed to decode LNURL
LNURLErrUnsafeURL // URL is not HTTPS or uses Tor (on mainnet)
LNURLErrUnreachable // Service is unreachable
LNURLErrInvalidResponse // Invalid response from service
LNURLErrResponse // Error response from service
LNURLErrUnknown // Unknown error
LNURLErrWrongTag // Wrong LNURL tag (not withdraw)
LNURLErrNoAvailableBalance // Insufficient balance at service
LNURLErrRequestExpired // Request has expired
LNURLErrNoRoute // No route to destination
LNURLErrTorNotSupported // Tor URLs not supported
LNURLErrAlreadyUsed // LNURL already used
LNURLErrForbidden // Access forbidden
LNURLErrCountryNotSupported // Service not available in country
LNURL Validation
Validate an LNURL code before processing:
func validateLNURL(qr string) bool {
isValid := LNURLValidate(qr)
return isValid
}
// Example usage
lnurlCode := "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS"
if LNURLValidate(lnurlCode) {
// Valid LNURL code
}
LNURL Withdraw
Process an LNURL-withdraw request:
// Create invoice builder
network := Mainnet()
invoiceBuilder := NewInvoiceBuilder()
.Network(network)
.UserKey(userKey)
.MuunKey(muunKey)
// Create listener
listener := &MyLNURLListener{}
// Start withdraw process
LNURLWithdraw(invoiceBuilder, lnurlCode, listener)
// Listener implementation
type MyLNURLListener struct{}
func (l *MyLNURLListener) OnUpdate(e *LNURLEvent) {
switch e.Code {
case LNURLStatusContacting:
fmt.Printf("Contacting service: %s\n", e.Metadata.Host)
case LNURLStatusInvoiceCreated:
fmt.Printf("Invoice created: %s\n", e.Metadata.Invoice)
case LNURLStatusReceiving:
fmt.Println("Receiving payment...")
}
}
func (l *MyLNURLListener) OnError(e *LNURLEvent) {
fmt.Printf("Error %d: %s\n", e.Code, e.Message)
}
Implementation Details
Invoice Creation
The withdraw process automatically creates a Lightning invoice with the appropriate amount:
// Source: libwallet/lnurl.go:53-62
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
metadata := &OperationMetadata{
LnurlSender: host,
}
return invoiceBuilder.AmountMSat(int64(amt)).
Description(desc).
Metadata(metadata).
Build()
}
Security Considerations
The implementation enforces HTTPS for mainnet operations:
// Source: libwallet/lnurl.go:64
allowUnsafe := !reflect.DeepEqual(invoiceBuilder.net, Mainnet())
On mainnet, LNURL services must use HTTPS. Unsafe URLs (HTTP or Tor) are only allowed on testnet and regtest networks.
Asynchronous Processing
LNURL withdraw runs asynchronously in a goroutine:
// Source: libwallet/lnurl.go:66
go lnurl.Withdraw(qr, createInvoiceFunc, allowUnsafe, func(e *lnurl.Event) {
// Event handling
})
Usage Examples
Complete Withdraw Flow
type WithdrawHandler struct {
invoiceReceived chan string
errorOccurred chan error
}
func (h *WithdrawHandler) OnUpdate(e *LNURLEvent) {
switch e.Code {
case LNURLStatusContacting:
log.Printf("Contacting %s", e.Metadata.Host)
case LNURLStatusInvoiceCreated:
log.Printf("Invoice created: %s", e.Metadata.Invoice)
h.invoiceReceived <- e.Metadata.Invoice
case LNURLStatusReceiving:
log.Println("Waiting for payment...")
}
}
func (h *WithdrawHandler) OnError(e *LNURLEvent) {
h.errorOccurred <- fmt.Errorf("LNURL error %d: %s", e.Code, e.Message)
}
func withdrawFunds(invoiceBuilder *InvoiceBuilder, lnurl string) error {
handler := &WithdrawHandler{
invoiceReceived: make(chan string, 1),
errorOccurred: make(chan error, 1),
}
LNURLWithdraw(invoiceBuilder, lnurl, handler)
select {
case invoice := <-handler.invoiceReceived:
log.Printf("Success! Invoice: %s", invoice)
return nil
case err := <-handler.errorOccurred:
return err
}
}
Validate Before Processing
func processLNURL(qr string, invoiceBuilder *InvoiceBuilder) error {
// Validate first
if !LNURLValidate(qr) {
return errors.New("invalid LNURL code")
}
// Create listener
listener := &MyLNURLListener{}
// Process
LNURLWithdraw(invoiceBuilder, qr, listener)
return nil
}
Error Handling by Code
func (l *MyLNURLListener) OnError(e *LNURLEvent) {
switch e.Code {
case LNURLErrUnsafeURL:
// Handle unsafe URL error
log.Println("Service must use HTTPS")
case LNURLErrNoAvailableBalance:
// Handle insufficient balance
log.Println("Service has no available balance")
case LNURLErrAlreadyUsed:
// Handle already used LNURL
log.Println("This LNURL has already been used")
case LNURLErrRequestExpired:
// Handle expired request
log.Println("LNURL request has expired")
default:
log.Printf("Error %d: %s", e.Code, e.Message)
}
}
Protocol Flow
- Decode LNURL: Extract URL from bech32-encoded LNURL
- Contact Service: HTTP GET request to decoded URL
- Parse Response: Validate withdraw parameters (min/max amount)
- Create Invoice: Generate Lightning invoice with requested amount
- Submit Invoice: POST invoice back to service callback URL
- Wait for Payment: Service pays the invoice
Error Recovery
Common errors and solutions:
| Error Code | Cause | Solution |
|---|
ErrUnsafeURL | HTTP or Tor URL on mainnet | Only use HTTPS services |
ErrUnreachable | Network connectivity | Check internet connection |
ErrAlreadyUsed | LNURL already redeemed | Get a new LNURL |
ErrRequestExpired | LNURL expired | Request a new LNURL |
ErrNoAvailableBalance | Service has no funds | Contact service provider |
See Also