Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: image build
on:
push:
branches:
- master
- docker

env:
REGISTRY: ghcr.io

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to the Container registry
uses: docker/login-action@6d4b68b490aef8836e8fb5e50ee7b3bdfa5894f0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@418e4b98bf2841bd337d0b24fe63cb36dc8afa55
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}

- name: Build and push Docker image
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM golang:1-alpine AS builder

WORKDIR /app

# Copy go mod files first for better caching
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o lnc-server ./cmd/lnc-server

# Create a minimal runtime image
FROM alpine:3

RUN apk --no-cache add ca-certificates tzdata && \
adduser -D -h /app lnproxy

WORKDIR /app
USER lnproxy

# Copy the binary from the builder stage
COPY --from=builder --chown=lnproxy:lnproxy /app/lnc-server /app/

# Expose the default port
EXPOSE 4747

# Set the entrypoint
ENTRYPOINT ["/app/lnc-server"]
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ node's pubkey.
Automatically checks the payment hashes, amount, and destination, on wrapped invoices,
so you get privacy without trusting anyone with your funds.

## Usage

```
usage: address [flags] address.macaroon https://example.com
address.macaroon
Expand All @@ -34,4 +36,46 @@ usage: address [flags] address.macaroon https://example.com
http port over which to expose api (default "4747")
-username string
lud6 username (default "_")
-log-level string
Log level (DEBUG, INFO, WARNING, ERROR, FATAL) (default "DEBUG")
-log-file string
Log file path (logs to stderr if not specified)
```

## Logging

The application includes a comprehensive logging system with the following features:

- **Log Levels**: Supports DEBUG, INFO, WARNING, ERROR, and FATAL log levels
- **Component-based Logging**: Different components log with their specific tags
- **Colored Output**: Terminal output includes colors for better readability
- **File Logging**: Option to log to a file instead of stderr
- **Customizable**: Log level can be adjusted via command-line flag

### Examples

Basic usage with default DEBUG logging to console:

```bash
./address -username satoshi address.macaroon https://example.com
```

Using INFO level logging to a file:

```bash
./address -log-level INFO -log-file server.log -username satoshi address.macaroon https://example.com
```

## Sample Log Output

```
[INFO][main] Starting lnproxy-address server
[INFO][main] Using domain: https://example.com
[INFO][main] Using LND host: https://127.0.0.1:8080
[INFO][main] Using LNProxy at https://lnproxy.org/spec/ (base fee: 2000 msat, ppm: 10000)
[INFO][main] LNURL-pay configured for user satoshi@example.com
[INFO][main] Starting HTTP server on 0.0.0.0:4747
[INFO][main] Lightning Address server is ready at satoshi@example.com
[DEBUG][lnurl-handler] Received request: /.well-known/lnurlp/satoshi
[DEBUG][lnurl-handler] No amount provided, returning LNURL-pay info
```
77 changes: 70 additions & 7 deletions address.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strconv"

"github.com/lnproxy/lnproxy-address/logger"
"github.com/lnproxy/lnproxy-client"
)

Expand Down Expand Up @@ -48,37 +48,100 @@ func (lnurl *LNURLP) JSONResponse() []byte {
}

func MakeLUD6Handler(lnurl *LNURLP, invoiceMaker InvoiceMaker, lnproxy *client.LNProxy) func(w http.ResponseWriter, r *http.Request) {
log := logger.WithComponent("lnurl-handler")

return func(w http.ResponseWriter, r *http.Request) {
log.Debug("Received request: %s", r.URL.String())

// Extract requested username from path
path := r.URL.Path
username := ""
if len(path) > len("/.well-known/lnurlp/") {
username = path[len("/.well-known/lnurlp/"):]
log.Debug("Requested username: %s", username)
}

// Check if username matches or is empty
if username != "" && username != lnurl.UserName {
log.Warning("Invalid username requested: %s (expected: %s)", username, lnurl.UserName)
fmt.Fprintf(w, `{"status": "ERROR", "reason": "unknown user"}`)
return
}

amount_string := r.URL.Query().Get("amount")
if amount_string == "" {
log.Debug("No amount provided, returning LNURL-pay info")
w.Header().Set("Content-Type", "application/json")
w.Write(lnurl.JSONResponse())
return
}

log.Debug("Payment request with amount: %s", amount_string)
amount_msat, err := strconv.ParseUint(amount_string, 10, 64)
if err != nil || amount_msat < lnurl.MinAmtMsat || amount_msat > lnurl.MaxAmtMsat {
fmt.Fprintf(w, `{"status": "ERROR", "reason": "invalid amount"}`)
if err != nil {
log.Warning("Invalid amount format: %s", amount_string)
fmt.Fprintf(w, `{"status": "ERROR", "reason": "invalid amount format"}`)
return
}

if amount_msat < lnurl.MinAmtMsat || amount_msat > lnurl.MaxAmtMsat {
log.Warning("Amount out of range: %d (min: %d, max: %d)",
amount_msat, lnurl.MinAmtMsat, lnurl.MaxAmtMsat)
fmt.Fprintf(w, `{"status": "ERROR", "reason": "amount out of allowed range"}`)
return
}

log.Debug("Calculating description hash from metadata")
h := sha256.New()
h.Write([]byte(lnurl.Metadata()))
description_hash := h.Sum(nil)

var routing_msat uint64
if lnproxy != nil {
// Calculate routing_msat ensuring final_amount is a multiple of 1000 msat (1 sat)
routing_msat = lnproxy.BaseMsat + (lnproxy.Ppm*amount_msat)/1_000_000
remainder := (amount_msat - routing_msat) % 1000
if remainder != 0 {
// Adjust routing_msat to make final_amount a whole satoshi
routing_msat += remainder // This increases the fee slightly to round down final_amount
}
}
inv, err := invoiceMaker.MakeInvoice(amount_msat-routing_msat, h.Sum(nil))


final_amount := amount_msat - routing_msat
log.Debug("Creating invoice for %d msat (original amount: %d msat)", final_amount, amount_msat)

inv, err := invoiceMaker.MakeInvoice(final_amount, description_hash)
if err != nil {
log.Println("error requesting invoice:", err)
log.Error("Error requesting invoice: %v", err)
fmt.Fprintf(w, `{"status": "ERROR", "reason": "error requesting invoice"}`)
return
}
log.Debug("Invoice created successfully")

if lnproxy != nil {
inv, err = lnproxy.RequestProxy(inv, routing_msat)
log.Debug("Proxying invoice through lnproxy")
originalInv := inv // Save the original invoice before proxying
inv, err = lnproxy.RequestProxy(originalInv, routing_msat)
if err != nil {
log.Println("error wrapping invoice:", err)
log.Error("Error wrapping invoice: %v", err)
fmt.Fprintf(w, `{"status": "ERROR", "reason": "error wrapping invoice"}`)
return
}
log.Debug("Successfully proxied invoice")

// Validate the proxy invoice against the original
valid, validateErr := client.ValidateProxyInvoice(originalInv, inv, routing_msat)
if validateErr != nil || !valid {
log.Error("Proxy invoice validation failed: %v", validateErr)
fmt.Fprintf(w, `{"status": "ERROR", "reason": "proxy invoice validation failed: %v"}`, validateErr)
return
}
log.Debug("Proxy invoice validated successfully")
}

log.Info("Successfully created invoice for %d msat", amount_msat)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"pr": "%s", "routes": []}`, inv)
}
}
Loading