Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fe7dcb4
feat: add CLN as a lnclient backend
daywalker90 Jan 29, 2026
257235d
feat: add hold invoice support for CLN backend
daywalker90 Jan 29, 2026
bcaa738
fix: reduce CLN form to just addresses and lightning dir
daywalker90 Feb 3, 2026
291a710
feat: add README for CLN grpc go code generation
daywalker90 Feb 3, 2026
5674e99
fix: cln backend does not support keysend with given preimages
daywalker90 Feb 4, 2026
882dd2b
fix: hold invoice notifications in CLN backend
daywalker90 Feb 5, 2026
ebb81b7
fix: remove dead code in CLN backend from ListTransactions
daywalker90 Feb 9, 2026
1a0f8ec
fix: env example CLN_ADDRESS_HOLD with different port to show it's a …
daywalker90 Feb 16, 2026
3a7fea0
fix: cleanup of CLN ressources in all cases
daywalker90 Feb 16, 2026
b8ddad8
fix: cln backend's GetNetworkGraph only fetches specified nodeId's
daywalker90 Feb 16, 2026
ad22bb0
fix: cln backend: only advertise hold methods for nip47 if hold plugi…
daywalker90 Feb 16, 2026
6642d3b
fix: cln's Shutdown should not stop CLN itself
daywalker90 Feb 16, 2026
10f039a
fix: relax the LND README line regarding env configuration
daywalker90 Feb 16, 2026
3af02db
fix: more nil checks in clnInvoiceToTransaction
daywalker90 Feb 16, 2026
4aec969
fix: prevent feerate overflow in CLN's RedeemOnchainFunds
daywalker90 Feb 16, 2026
acf11bd
fix: don't access nil errors for empty reponses of certain CLN methods
daywalker90 Feb 16, 2026
43b6b2b
fix: nil instead of empty string in cln's GetNetworkGraph return types
daywalker90 Feb 16, 2026
efcd091
fix: nil checks for created_at in cln's clnInvoiceToTransaction
daywalker90 Feb 16, 2026
bae7f10
fix: set minimum tls version to 1.2 for cln backend grpc connections
daywalker90 Feb 17, 2026
081c687
fix: cln's subscribeOpenHoldInvoices doesn't give up as fast
daywalker90 Feb 17, 2026
70d007a
fix: deduplicate graph edges in cln's GetNetworkGraph
daywalker90 Feb 17, 2026
3956b19
fix: print the error string, not pointer address, in cln's ListChannels
daywalker90 Feb 17, 2026
157293c
fix: remove cln's ListTransactions completely
daywalker90 Feb 17, 2026
bcfc82e
fix: use ListPeers instead of ListPeerChannels in cln's ListPeers
daywalker90 Feb 17, 2026
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
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,13 @@ FRONTEND_URL=http://localhost:5173

# Boltz API
#BOLTZ_API=https://api.testnet.boltz.exchange
#NETWORK=testnet
#NETWORK=testnet

# CLN Backend
#LN_BACKEND_TYPE=CLN
# CLN's grpc-host:grpc-port
#CLN_ADDRESS=127.0.0.1:9737
# CLN's lightning directory containing the grpc certificates, usually ~/.lightning/<network>/
#CLN_LIGHTNING_DIR=/path/to/.lightning/bitcoin
# CLN's hold plugin https://github.com/BoltzExchange/hold gRPC address
#CLN_ADDRESS_HOLD=127.0.0.1:9738
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ By default Alby Hub uses the embedded LDK based lightning node. Optionally it ca
- LND
- Phoenixd
- Cashu
- CLN
- want more? please open an issue.

## Development
Expand Down Expand Up @@ -210,9 +211,22 @@ Migration of the database is currently experimental. Please make a backup before

- `ENABLE_ADVANCED_SETUP`: set to `false` to force a specific backend type (combined with backend parameters below)

### CLN Backend parameters

Can be configured via env or the UI

- `LN_BACKEND_TYPE`: CLN
- `CLN_ADDRESS`: the CLN grpc address (grpc-host and grpc-port), e.g. `127.0.0.1:9737`
- `CLN_LIGHTNING_DIR`: CLN's lightning directory containing the grpc certificates, usually `~/.lightning/<network>`

Optional for hold invoice methods support:
- `CLN_ADDRESS_HOLD`: the CLN hold plugin grpc address (grpc-host and grpc-port), e.g. `127.0.0.1:9738`

If you are copying the certificates to another machine make sure you get the `ca.pem`, `client.pem` and `client-key.pem` from the lightning directory and optionally from the `hold` directory inside the lightning directory and keep the sub-directory structure of the hold directory.

### LND Backend parameters

Currently only LND can be configured via env. Other node types must be configured via the UI.
LND can be configured via env. Other node types may need to be configured via the UI.

_To configure via env, the following parameters must be provided:_

Expand Down
24 changes: 24 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,30 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error {
}
}

if setupRequest.CLNAddress != "" {
err = api.cfg.SetUpdate("CLNAddress", setupRequest.CLNAddress, setupRequest.UnlockPassword)
if err != nil {
logger.Logger.WithError(err).Error("Failed to save CLN address")
return err
}
}

if setupRequest.CLNLightningDir != "" {
err = api.cfg.SetUpdate("CLNLightningDir", setupRequest.CLNLightningDir, setupRequest.UnlockPassword)
if err != nil {
logger.Logger.WithError(err).Error("Failed to save CLN Lightning directory path")
return err
}
}

if setupRequest.CLNAddressHold != "" {
err = api.cfg.SetUpdate("CLNAddressHold", setupRequest.CLNAddressHold, setupRequest.UnlockPassword)
if err != nil {
logger.Logger.WithError(err).Error("Failed to save cln hold plugin address")
return err
}
}

return nil
}

Expand Down
5 changes: 5 additions & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ type SetupRequest struct {

// Cashu fields
CashuMintUrl string `json:"cashuMintUrl"`

// CLN fields
CLNAddress string `json:"clnAddress"`
CLNLightningDir string `json:"clnLightningDir"`
CLNAddressHold string `json:"clnAddressHold"`
}

type CreateAppResponse struct {
Expand Down
20 changes: 20 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ func (cfg *config) init(env *AppConfig) error {
}
}

// CLN specific to support env variables
if cfg.Env.CLNAddress != "" {
err := cfg.SetUpdate("CLNAddress", cfg.Env.CLNAddress, "")
if err != nil {
return err
}
}
if cfg.Env.CLNLightningDir != "" {
err := cfg.SetUpdate("CLNLightningDir", cfg.Env.CLNLightningDir, "")
if err != nil {
return err
}
}
if cfg.Env.CLNAddressHold != "" {
err := cfg.SetUpdate("CLNAddressHold", cfg.Env.CLNAddressHold, "")
if err != nil {
return err
}
}

return nil
}

Expand Down
4 changes: 4 additions & 0 deletions config/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const (
LDKBackendType = "LDK"
PhoenixBackendType = "PHOENIX"
CashuBackendType = "CASHU"
CLNBackendType = "CLN"
)

const (
Expand Down Expand Up @@ -59,6 +60,9 @@ type AppConfig struct {
LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"`
BoltzApi string `envconfig:"BOLTZ_API" default:"https://api.boltz.exchange"`
HideUpdateBanner bool `envconfig:"HIDE_UPDATE_BANNER" default:"false"`
CLNAddress string `envconfig:"CLN_ADDRESS"`
CLNLightningDir string `envconfig:"CLN_LIGHTNING_DIR"`
CLNAddressHold string `envconfig:"CLN_ADDRESS_HOLD"`
}

func (c *AppConfig) IsDefaultClientId() bool {
Expand Down
Binary file added frontend/src/assets/images/node/cln.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frontend/src/lib/backendType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ export const backendTypeConfigs: Record<BackendType, BackendTypeConfig> = {
hasChannelManagement: false,
hasNodeBackup: false,
},
CLN: {
hasMnemonic: false,
hasChannelManagement: true,
hasNodeBackup: false,
},
};
5 changes: 5 additions & 0 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { SetupFinish } from "src/screens/setup/SetupFinish";
import { SetupNode } from "src/screens/setup/SetupNode";
import { SetupPassword } from "src/screens/setup/SetupPassword";
import { SetupSecurity } from "src/screens/setup/SetupSecurity";
import { CLNForm } from "src/screens/setup/node/CLNForm";
import { CashuForm } from "src/screens/setup/node/CashuForm";
import { LDKForm } from "src/screens/setup/node/LDKForm";
import { LNDForm } from "src/screens/setup/node/LNDForm";
Expand Down Expand Up @@ -536,6 +537,10 @@ const routes: RouteObject[] = [
path: "ldk",
element: <LDKForm />,
},
{
path: "cln",
element: <CLNForm />,
},
{
path: "preset",
element: <PresetNodeForm />,
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/screens/setup/SetupNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { cn } from "src/lib/utils";
import { BackendType } from "src/types";

import cashu from "src/assets/images/node/cashu.png";
import cln from "src/assets/images/node/cln.png";
import lnd from "src/assets/images/node/lnd.png";
import { backendTypeConfigs } from "src/lib/backendType";
import useSetupStore from "src/state/SetupStore";
Expand Down Expand Up @@ -37,6 +38,10 @@ const backendTypeDisplayConfigs: Partial<
title: "Cashu Mint",
icon: <img src={cashu} />,
},
CLN: {
title: "CLN",
icon: <img src={cln} />,
},
};

const backendTypeDisplayConfigList = Object.entries(
Expand Down
1 change: 1 addition & 0 deletions frontend/src/screens/setup/SetupSecurity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function SetupSecurity() {
</span>
</div>
{store.nodeInfo.backendType === "LND" ||
store.nodeInfo.backendType === "CLN" ||
store.nodeInfo.backendType === "PHOENIX" ? (
<div className="flex gap-3 items-center">
<div className="shrink-0">
Expand Down
86 changes: 86 additions & 0 deletions frontend/src/screens/setup/node/CLNForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import Container from "src/components/Container";
import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader";
import { Button } from "src/components/ui/button";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import useSetupStore from "src/state/SetupStore";

export function CLNForm() {
const navigate = useNavigate();
const setupStore = useSetupStore();
const [clnAddress, setClnAddress] = React.useState<string>(
setupStore.nodeInfo.clnAddress || ""
);
const [clnLightningDir, setClnLightningDir] = React.useState<string>(
setupStore.nodeInfo.clnLightningDir || ""
);
const [clnAddressHold, setClnAddressHold] = React.useState<string>(
setupStore.nodeInfo.clnAddressHold || ""
);

// TODO: proper onboarding
function onSubmit(e: React.FormEvent) {
e.preventDefault();
handleSubmit({
clnAddress,
clnLightningDir,
clnAddressHold,
});
}

async function handleSubmit(data: object) {
setupStore.updateNodeInfo({
backendType: "CLN",
...data,
});
navigate("/setup/security");
}

return (
<Container>
<TwoColumnLayoutHeader
title="Configure CLN"
description="Fill out wallet details to finish setup."
/>
<form className="w-full grid gap-5 mt-6" onSubmit={onSubmit}>
<div className="grid gap-1.5">
<Label htmlFor="cln-address">CLN Address (GRPC)</Label>
<Input
required
name="cln-address"
onChange={(e) => setClnAddress(e.target.value)}
value={clnAddress}
id="cln-address"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cln-lightning-dir">
CLN Lightning directory (full path)
</Label>
<Input
required
name="cln-lightning-dir"
onChange={(e) => setClnLightningDir(e.target.value)}
value={clnLightningDir}
type="text"
id="cln-lightning-dir"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cln-address-hold">
(optional) CLN hold plugin Address (GRPC)
</Label>
<Input
name="cln-address-hold"
onChange={(e) => setClnAddressHold(e.target.value)}
value={clnAddressHold}
id="cln-address-hold"
/>
</div>
<Button>Next</Button>
</form>
</Container>
);
}
6 changes: 5 additions & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
WalletMinimalIcon,
} from "lucide-react";

export type BackendType = "LND" | "LDK" | "PHOENIX" | "CASHU";
export type BackendType = "LND" | "LDK" | "PHOENIX" | "CASHU" | "CLN";

export type Nip47RequestMethod =
| "get_info"
Expand Down Expand Up @@ -443,6 +443,10 @@ export type SetupNodeInfo = Partial<{

phoenixdAddress?: string;
phoenixdAuthorization?: string;

clnAddress?: string;
clnLightningDir?: string;
clnAddressHold?: string;
}>;

export type LSPType = "LSPS1";
Expand Down
75 changes: 75 additions & 0 deletions lnclient/cln/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Generating Go gRPC Code

The Go gRPC bindings for Core Lightning (CLN) are generated from proto files that live
in the **lightning** repository (`cln-grpc`) and in the **hold** plugin repository.

The generated Go files are written into the **hub** repository under `lnclient/cln/clngrpc` and `lnclient/cln/clngrpc_hold`.

### Prerequisites

Make sure the following tools are installed:

```bash
# protoc (>= 3.20 recommended)
protoc --version

# Go plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```

Ensure `$GOPATH/bin` is in your `PATH`:

```bash
export PATH="$PATH:$(go env GOPATH)/bin"
```

This guide assumes the following directory structure:

```
~/dev/
├── lightning/
│ └── cln-grpc/
│ └── proto/
│ ├── node.proto
│ ├── primitives.proto
│ └── ...
├── hub/
| └── lnclient/
| └── cln/
| └── clngrpc/
| └── clngrpc_hold/
└── hold/
└── protos/
└── hold.proto
```

### Generating Go code

From the hub repository root, run:

```bash
protoc \
--proto_path=../lightning/cln-grpc/proto \
--go_out=./lnclient/cln/clngrpc \
--go_opt=paths=source_relative \
--go_opt=Mprimitives.proto=github.com/getAlby/hub/lnclient/cln/clngrpc \
--go_opt=Mnode.proto=github.com/getAlby/hub/lnclient/cln/clngrpc \
--go-grpc_out=./lnclient/cln/clngrpc \
--go-grpc_opt=paths=source_relative \
../lightning/cln-grpc/proto/node.proto \
../lightning/cln-grpc/proto/primitives.proto
```

and if you have the hold plugin repo:

```bash
protoc \
--proto_path=../hold/protos \
--go_out=./lnclient/cln/clngrpc_hold \
--go_opt=paths=source_relative \
--go_opt=Mhold.proto=github.com/getAlby/hub/lnclient/cln/clngrpc_hold \
--go-grpc_out=./lnclient/cln/clngrpc_hold \
--go-grpc_opt=paths=source_relative \
../hold/protos/hold.proto
```
Loading