Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ var/
*.cache
composer.lock
lightning-config.php
nostr.json
backends.json
.phpunit.cache
.phpunit.result.cache
169 changes: 153 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# PHP Lightning Address

PHP Lightning Address is an easy way to get a [lightning address](https://lightningaddress.com/) in PHP.

<p align="center">
<a href="https://github.com/php-lightning/lnaddress/actions">
<img src="https://github.com/php-lightning/lnaddress/workflows/CI/badge.svg" alt="GitHub Build Status">
Expand All @@ -20,43 +18,182 @@ PHP Lightning Address is an easy way to get a [lightning address](https://lightn
</a>
</p>

## Usage / Development
Self-host your own [Lightning Address](https://lightningaddress.com) in PHP: a human-readable identifier like `you@yourdomain.com` that any Lightning wallet can pay. It implements [LNURL-pay (LUD-06)](https://github.com/lnurl/luds/blob/luds/06.md) and is backend-agnostic — [LNbits](https://lnbits.com) is the backend available today. Built on the [Gacela](https://gacela-project.com) framework.

## Requirements

Set up your custom config:
- PHP >= 8.2

## Install

```bash
composer require php-lightning/lnaddress
```

`composer install` runs a post-install step that copies `backends.dist.json` → `backends.json` if the latter does not exist yet.

Prefer starting from a working project? Use the ready-made [demo template](https://github.com/php-lightning/demo-lnaddress). It depends on this library, so a `composer update` pulls in new features and fixes as they land here.

## Configure

There are two config files: `lightning-config.php` (settings) and `backends.json` (per-user invoice backends).

### 1. Settings — `lightning-config.php`

```bash
cp lightning-config.dist.php lightning-config.php
# or just simply the nostr.json to define the backends/user-settings
cp nostr.dist.json nostr.json
```

You can customize the invoice description and the success message by editing
`lightning-config.php`:
`LightningConfig` has a fluent API:

```php
use PhpLightning\Config\LightningConfig;

return (new LightningConfig())
->setDescriptionTemplate('Pay to %s on mynode')
->setSuccessMessage('Thanks for the payment!');
->setDomain('yourdomain.com')
->setReceiver('default-receiver')
->setDescriptionTemplate('Pay to %s') // %s = the lightning address
->setSuccessMessage('Thanks for the payment!')
->setInvoiceMemo('')
->setSendableRange(min: 100_000, max: 10_000_000_000) // millisats
->setCallbackUrl('https://yourdomain.com')
->addBackendsFile(getcwd() . '/backends.json');
```

Run a local PHP server listening `public/index.php`
### 2. Backends — `backends.json`

```bash
cp backends.dist.json backends.json
```

Each username maps to its own invoice backend:

```json
{
"bob": { "type": "lnbits", "api_key": "abc...123", "api_endpoint": "http://localhost:5000" },
"alice": { "type": "lnbits", "api_key": "def...456", "api_endpoint": "http://localhost:5000" }
}
```

### Register backends programmatically (no JSON file)

You can skip `backends.json` and register backends directly in `lightning-config.php`:

```php
use PhpLightning\Config\Backend\LnBitsBackendConfig;

$config->addBackend('bob', LnBitsBackendConfig::withEndpointAndKey('http://localhost:5000', 'abc...123'));
```

## Run the server

```bash
composer serve
```

### Demo template
This starts `php -S localhost:8080 public/index.php`.

## HTTP API

One route serves the full LNURL-pay flow: `GET /{username?}`. The username is optional — when omitted, the request resolves to the default `receiver@domain` from your config.

Every response carries permissive CORS headers (`Access-Control-Allow-Origin: *`) so browser-based wallets can call it, and `OPTIONS` preflight requests are answered directly. Uncaught errors are turned into the LNURL error object by a global handler.

### Step 1 — pay params

`GET /bob` (no `amount`) returns the LNURL-pay parameters:

```json
{
"callback": "https://yourdomain.com",
"maxSendable": 10000000000,
"minSendable": 100000,
"metadata": "[[\"text/plain\",\"Pay to bob@yourdomain.com\"],[\"text/identifier\",\"bob@yourdomain.com\"]]",
"tag": "payRequest",
"commentAllowed": false
}
```

### Step 2 — invoice

`GET /bob?amount=<millisats>` returns a bolt11 invoice for that amount:

```json
{
"pr": "lnbc20n1p...",
"status": "OK",
"memo": "",
"successAction": { "tag": "message", "message": "Thanks for the payment!" },
"routes": [],
"disposable": false,
"error": null
}
```

### Errors

Failures return an LNURL error object, for example when the amount falls outside the sendable range or the backend is unreachable:

```json
{ "status": "ERROR", "reason": "Amount is not between minimum and maximum sendable amount" }
```

> **Units:** the sendable range and the `amount` query param are in **millisats**. The backend is billed in **sats** (`millisats / 1000`).

## Use as a library (programmatic)

You can call the facade directly instead of going over HTTP:

```php
use Gacela\Framework\Gacela;
use PhpLightning\Invoice\InvoiceFacade;

Gacela::bootstrap(__DIR__);

$facade = new InvoiceFacade();
$payParams = $facade->getCallbackUrl('bob'); // LNURL-pay params
$invoice = $facade->generateInvoice('bob', 2_000); // millisats
```

## Configuration reference

| Setter | Purpose | Default |
| --- | --- | --- |
| `setDomain(string)` | Your domain (URL scheme is stripped) | — |
| `setReceiver(string)` | Default username when none is in the URL | — |
| `setSendableRange(int $min, int $max)` | Allowed amounts, in millisats | `100_000` – `10_000_000_000` |
| `setCallbackUrl(string)` | Public callback base URL wallets call back to | — |
| `setDescriptionTemplate(string)` | LNURL metadata description (`%s` = the address) | `Pay to %s` |
| `setSuccessMessage(string)` | Message shown after a successful payment | `Payment received!` |
| `setInvoiceMemo(string)` | Memo attached to the invoice | `''` |
| `addBackendsFile(string $path)` / `addBackend(string $username, ...)` | Register invoice backends | — |

## Adding a new backend

Backends are keyed by a `type` string, resolved through the `PhpLightning\Config\Backend\BackendType` enum. To add one:

- Add a case to `PhpLightning\Config\Backend\BackendType`.
- Handle that case in `LightningConfig::createBackendConfig()`.
- Implement `PhpLightning\Invoice\Domain\BackendInvoice\BackendInvoiceInterface`.

## Development / Testing

```bash
composer test-all # quality + phpunit + rector (dry-run)
```

Other useful scripts:

We prepared a demo template, so you can use this project as a dependency. The benefits from this approach is that you can update your project with `composer update` whenever there are new features or improvements on this `lnaddress` repository.
- `composer test-phpunit` — run the PHPUnit suite
- `composer quality` — php-cs-fixer (dry-run), psalm, phpstan
- `composer fix` — php-cs-fixer + rector (apply fixes)

> [https://github.com/php-lightning/demo-lnaddress](https://github.com/php-lightning/demo-lnaddress)
See [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) before opening a PR.

## Wiki

Check the wiki for more details: [https://github.com/php-lightning/lnaddress/wiki](https://github.com/php-lightning/lnaddress/wiki)
More details in the [wiki](https://github.com/php-lightning/lnaddress/wiki).

## Contributions

Feel free to open issues & PR if you want to contribute to this project.
Issues and pull requests are welcome. Licensed under [MIT](LICENSE).
File renamed without changes.
15 changes: 7 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
"license": "MIT",
"require": {
"php": ">=8.2",
"gacela-project/gacela": "^1.9",
"gacela-project/router": "^0.12",
"gacela-project/gacela": "^1.19",
"gacela-project/router": "^0.13",
"symfony/http-client": "^7.2"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75",
"gacela-project/phpstan-extension": "^0.3",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^9.6",
"gacela-project/phpstan-extension": "^0.4",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^10.5",
"psalm/plugin-phpunit": "^0.19",
"rector/rector": "^1.2",
"symfony/var-dumper": "^7.2",
"rector/rector": "^2.0",
"vimeo/psalm": "^6.11"
},
"config": {
Expand All @@ -41,7 +40,7 @@
},
"scripts": {
"post-install-cmd": [
"[ ! -f nostr.json ] && cp nostr.dist.json nostr.json || true"
"[ ! -f backends.json ] && cp backends.dist.json backends.json || true"
],
"ctal": [
"@static-clear-cache",
Expand Down
7 changes: 5 additions & 2 deletions lightning-config.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
return (new LightningConfig())
->setDomain('localhost')
->setReceiver('default-receiver')
// %s is replaced with the payer-facing lightning address
->setDescriptionTemplate('Pay to %s')
->setSuccessMessage('Payment received!')
->setInvoiceMemo('')
// min/max are in millisats (sat * 1000)
->setSendableRange(min: 100_000, max: 10_000_000_000)
->setCallbackUrl('localhost:8000/callback')
->addBackendsFile(getcwd() . DIRECTORY_SEPARATOR . 'nostr.json');
// public URL wallets call back to request the invoice
->setCallbackUrl('http://localhost:8080')
->addBackendsFile(getcwd() . DIRECTORY_SEPARATOR . 'backends.json');
50 changes: 18 additions & 32 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
executionOrder="random"
processIsolation="false"
resolveDependencies="true"
stopOnFailure="false"
verbose="true"
>
<php>
<ini name="error_reporting" value="-1" />
<ini name="memory_limit" value="-1" />
</php>

<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="feature">
<directory suffix="Test.php">tests/Feature</directory>
</testsuite>
</testsuites>

<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" backupGlobals="false" colors="true" executionOrder="random" processIsolation="false" resolveDependencies="true" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<php>
<ini name="error_reporting" value="-1"/>
<ini name="memory_limit" value="-1"/>
</php>
<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="feature">
<directory suffix="Test.php">tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>
26 changes: 26 additions & 0 deletions src/Config/Backend/BackendType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace PhpLightning\Config\Backend;

use RuntimeException;

use function array_map;
use function implode;
use function sprintf;

enum BackendType: string
{
case Lnbits = 'lnbits';

public static function fromString(string $type): self
{
return self::tryFrom($type)
?? throw new RuntimeException(sprintf(
'Unknown backend type "%s". Supported types: %s',
$type,
implode(', ', array_map(static fn (self $t): string => $t->value, self::cases())),
));
}
}
Loading
Loading