diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ceb01..3a3a44c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project are documented in this file. - docs: split the README into `docs/` (getting started, configuration, HTTP API, architecture, development) - chore: drop Scrutinizer (`.scrutinizer.yml`, badges) — GitHub Actions is the only CI gate +- docs: publish a project page on GitHub Pages, with an animated LNURL-pay flow diagram + and an address-anatomy graphic **Full Changelog**: https://github.com/php-lightning/lnaddress/compare/0.9.0...main diff --git a/docs/index.html b/docs/index.html index d09305a..3d9e71d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -55,6 +55,7 @@ border-radius: 5px; padding: 1px 5px; font-size: 0.9em; + overflow-wrap: anywhere; } h1, h2, h3 { line-height: 1.25; letter-spacing: -0.015em; } h2 { margin-top: 3.2rem; font-size: 1.6rem; } @@ -122,6 +123,119 @@ @media (prefers-color-scheme: dark) { .steps li::before { color: #14161a; } } .steps h3 { margin: 0 0 4px; } + /* ---- figures: address anatomy + payment flow ---- */ + .fig { + margin: 1.8rem 0 0; + padding: 18px 16px 14px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--bg-soft); + } + .fig svg { display: block; width: 100%; height: auto; } + .fig text { font-family: var(--sans); fill: var(--fg); } + .fig-note { font-size: 13px; fill: var(--fg-soft); } + + .addr-part { font-family: var(--mono); font-size: 34px; font-weight: 700; } + .addr-at { font-family: var(--mono); font-size: 34px; fill: var(--fg-soft); } + .addr-part.user { fill: var(--accent); } + .addr-part.host { fill: #2f7d6d; } + .hint { fill: none; stroke-width: 1.5; stroke-dasharray: 4 4; } + .hint.user { stroke: var(--accent); } + .hint.host { stroke: #2f7d6d; } + + .part rect { fill: var(--bg); stroke-width: 1.5; } + .part.user rect { stroke: var(--accent); } + .part.host rect { stroke: #2f7d6d; } + .part-label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; } + .part.user .part-label { fill: var(--accent); } + .part.host .part-label { fill: #2f7d6d; } + .part-value { font-family: var(--mono); font-size: 15px; fill: var(--fg); } + + @media (prefers-color-scheme: dark) { + .addr-part.host { fill: #62c3ab; } + .hint.host { stroke: #62c3ab; } + .part.host rect { stroke: #62c3ab; } + .part.host .part-label { fill: #62c3ab; } + } + + .flow .lifeline { stroke: var(--line); stroke-width: 2; } + .flow .actor rect { fill: var(--bg); stroke: var(--line); stroke-width: 1.5; } + .flow .actor text { font-size: 14px; font-weight: 600; text-anchor: middle; } + .flow .actor .sub { font-size: 11px; font-weight: 400; fill: var(--fg-soft); } + .flow .actor.mine rect { fill: var(--accent-soft); stroke: var(--accent); } + + .flow .msg path { + fill: none; + stroke: var(--line); + stroke-width: 2; + marker-end: url(#arrow-idle); + } + .flow .msg text { + font-size: 12.5px; + fill: var(--fg-soft); + text-anchor: middle; + } + .flow .msg.pay path { stroke-dasharray: 6 5; } + .flow .msg.done path { stroke: var(--fg-soft); opacity: 0.55; } + .flow .msg.active path { stroke: var(--accent); stroke-width: 3; marker-end: url(#arrow-active); } + .flow .msg.active text { fill: var(--fg); font-weight: 600; } + .flow .head.idle { fill: var(--line); } + .flow .head.active { fill: var(--accent); } + + .flow .packet { + fill: var(--accent); + stroke: var(--bg); + stroke-width: 2; + transition: transform 900ms cubic-bezier(.45,.05,.35,1); + } + .flow.paused .packet { opacity: 0.45; } + + .flow-caption { + min-height: 3.2em; + margin-top: 12px; + padding: 12px 14px; + border-radius: 9px; + background: var(--bg); + border: 1px solid var(--line); + font-size: 0.95rem; + } + .flow-caption b { color: var(--accent); } + + .flow-controls { display: flex; align-items: center; gap: 8px; margin-top: 12px; flex-wrap: wrap; } + .flow-controls button { + font: inherit; + font-size: 0.9rem; + min-width: 38px; + padding: 5px 12px; + border-radius: 7px; + border: 1px solid var(--line); + background: var(--bg); + color: var(--fg); + cursor: pointer; + } + .flow-controls button:hover { border-color: var(--accent); color: var(--accent); } + .flow-dots { display: flex; gap: 6px; list-style: none; margin: 0 0 0 6px; padding: 0; } + .flow-dots button { + min-width: 0; + width: 26px; + height: 26px; + padding: 0; + border-radius: 50%; + font-size: 0.75rem; + color: var(--fg-soft); + } + .flow-dots button[aria-current="true"] { + background: var(--accent); + border-color: var(--accent); + color: #fff; + font-weight: 700; + } + @media (prefers-color-scheme: dark) { .flow-dots button[aria-current="true"] { color: #14161a; } } + + @media (prefers-reduced-motion: reduce) { + .flow .packet { transition: none; } + } + footer { margin-top: 4rem; border-top: 1px solid var(--line); @@ -193,19 +307,131 @@

Why self-host it

  • Multiple identities, one deployment. Family, team, or project addresses can share a domain while each keeps its own backend.
  • -

    How it works

    -
      -
    1. -

      The wallet resolves the address

      -

      - bob@example.com becomes - GET https://example.com/.well-known/lnurlp/bob, which your server - routes to this library. -

      -
    2. -
    3. -

      The server returns pay parameters

      -

      Minimum and maximum sendable amounts, a description, and the callback URL to use next.

      +

      Anatomy of an address

      +

      + Nothing about a Lightning Address is new protocol — it is a naming convention on top + of plain HTTPS. The two halves of the address tell a wallet exactly which URL to open. +

      + +
      + + The address bob@example.com maps to https://example.com/.well-known/lnurlp/bob + + bob@example.com + + + + + + + your domain + https://example.com + + + + username → path + /.well-known/lnurlp/bob + + + + Join the two and you have the URL the wallet opens — this library answers it. + + +
      + +

      How a payment happens

      +

      + Two HTTP round trips, then a normal Lightning payment. Step through it: +

      + +
      + + Sequence diagram of the LNURL-pay flow between the payer's wallet, this library and your node + + + + + + + + + + + + + + + + + + + Payer's wallet + phone or browser + + + + Your domain + this library + + + + Your node + LNbits wallet + + + + + + GET /.well-known/lnurlp/bob + + + + pay params: min, max, callback + + + + GET /bob?amount=2000 + + + + POST /api/v1/payments + + + + bolt11 invoice + + + + { "pr": "lnbc20n1p…" } + + + + ⚡ pays the invoice over Lightning + + + + + +
      + +
      + + + +
        +
        +
        + +

        + Steps 1, 2, 3 and 6 are what this library answers. Step 4 and 5 happen against your + own node — the sats in step 7 travel from the payer straight to your wallet, never + through the library. +

        + +

        What travels on the wire

        +

        The pay parameters (step 2), straight from a running instance:

        {
           "callback": "https://example.com",
           "maxSendable": 10000000000,
        @@ -214,21 +440,8 @@ 

        The server returns pay parameters

        "tag": "payRequest", "commentAllowed": false }
        -
      1. -
      2. -

        The wallet asks for an invoice

        -

        - It calls the callback with the chosen amount in millisatoshis: - GET /bob?amount=2000. -

        -
      3. -
      4. -

        Your node issues it

        -

        - The library requests a bolt11 invoice from the backend configured for that - username and hands it back under pr. The wallet pays it directly — - the library never touches funds. -

        + +

        And the invoice (step 6), with the bolt11 string under pr as LUD-06 requires:

        {
           "pr": "lnbc20n1p…",
           "status": "OK",
        @@ -237,8 +450,6 @@ 

        Your node issues it

        "disposable": false, "error": null }
        -
      5. -

      Quick start

      Requires PHP 8.3 or newer and an LNbits wallet API key.

      @@ -377,5 +588,117 @@

      What is not implemented?

      + +