@lightninglabs/lnc-web
Version:
Lightning Node Connect npm module for web
480 lines (351 loc) • 15.9 kB
Markdown
# /lnc-web
A JavaScript library for connecting to Lightning Network nodes via [Lightning Node Connect](https://docs.lightning.engineering/lightning-network-tools/lightning-terminal/lightning-node-connect). Supports password and passkey authentication, automatic session management, and typed RPC access to LND, Loop, Pool, Faraday, Taproot Assets, and Lightning Terminal.
## Install
```bash
npm i /lnc-web
```
## Quick Start
```typescript
import { LightningNodeConnect } from '/lnc-web';
const lnc = new LightningNodeConnect({
namespace: 'my-app',
allowPasskeys: true,
enableSessions: true,
});
// First-time pairing (connects and persists in one step)
await lnc.pair('artefact morning piano photo consider light', {
method: 'password',
password: 'my-secure-password',
});
// Returning user login
await lnc.login({ method: 'password', password: 'my-secure-password' });
// Access services
const info = await lnc.lnd.lightning.getInfo();
const channels = await lnc.lnd.lightning.listChannels();
```
## Configuration
The constructor accepts a `LightningNodeConnectConfig` object:
```typescript
const lnc = new LightningNodeConnect({
// LNC proxy server (default: mailbox.terminal.lightning.today:443)
serverHost: 'mailbox.terminal.lightning.today:443',
// Custom WASM binary URL - local or remote (default: Lightning Engineering CDN)
wasmClientCode: 'https://lightning.engineering/lnc-v0.3.5-alpha.wasm',
// Unique namespace for this connection (default: 'default')
namespace: 'my-app',
// Enable passkey-based authentication (default: true)
allowPasskeys: true,
// Display name shown during passkey creation (default: 'LNC User ({namespace})')
passkeyDisplayName: 'My App',
// Enable session-based authentication (default: true)
enableSessions: true,
// Session timing (see Sessions section below)
session: {
sessionDurationMs: 24 * 60 * 60 * 1000, // 24 hours (default)
enableActivityRefresh: true, // auto-extend on activity (default)
maxRefreshes: 10, // max extensions per session (default)
maxSessionAgeMs: 7 * 24 * 60 * 60 * 1000, // absolute max age: 7 days (default)
},
});
```
### Custom Proxy Server
Some consumer apps construct the `LightningNodeConnect` instance on page load but allow users to specify a custom proxy server on the pairing screen. Use the `serverHost` setter:
```typescript
const lnc = new LightningNodeConnect({ namespace: 'my-app' });
// Later, when the user provides a custom host...
lnc.serverHost = 'custom.proxy-server.host:443';
await lnc.pair(pairingPhrase, { method: 'password', password });
```
## Pairing
Pairing is a one-time operation that connects your app to a Lightning node using a pairing phrase generated by LND or Lightning Terminal. After pairing and persisting credentials, the connection keys are stored locally so the user doesn't need to pair again.
### One-step pairing (recommended)
Connect and persist credentials in a single call:
```typescript
// With password
await lnc.pair('artefact morning piano photo consider light', {
method: 'password',
password: 'my-secure-password',
});
// With passkey (biometric / hardware key)
await lnc.pair('artefact morning piano photo consider light', {
method: 'passkey',
});
```
### Two-step pairing
For more control (e.g., verifying the connection before persisting):
```typescript
// Step 1: pair and connect
await lnc.pair('artefact morning piano photo consider light');
// Step 2: verify the connection works
await lnc.lnd.lightning.listChannels();
// Step 3: persist credentials
await lnc.persistWithPassword('my-secure-password');
// or: await lnc.persistWithPasskey();
```
## Detecting Saved Credentials
Use `getAuthenticationInfo()` to check whether the user has previously paired and how they should log in:
```typescript
const auth = await lnc.getAuthenticationInfo();
if (!auth.hasStoredCredentials) {
// New user - show pairing screen
showPairingScreen();
} else if (auth.hasActiveSession) {
// Active session - can auto-connect (see Sessions below)
await lnc.login({ method: 'session' });
} else if (auth.hasPasskey) {
// Returning user with passkey - prompt for biometric
await lnc.login({ method: 'passkey' });
} else {
// Returning user with password - prompt for password
await lnc.login({ method: 'password', password: userInput });
}
```
The `AuthenticationInfo` object contains:
| Field | Type | Description |
| ----------------------- | --------- | ------------------------------------------------------- |
| `isUnlocked` | `boolean` | True if credentials have been decrypted into memory |
| `hasStoredCredentials` | `boolean` | True if long-term credentials exist (password or passkey) |
| `hasActiveSession` | `boolean` | True if a valid session exists for passwordless login |
| `sessionTimeRemaining` | `number` | Milliseconds until the current session expires |
| `supportsPasskeys` | `boolean` | True if passkeys are enabled in config and supported by the browser |
| `hasPasskey` | `boolean` | True if a passkey credential has been stored |
| `preferredUnlockMethod` | `string` | `'session'`, `'passkey'`, or `'password'` - recommended method based on current state |
| `passkeyCredentialId` | `string?` | The stored passkey credential ID, if any (for reusing across namespaces) |
## Login
Returning users authenticate with one of three methods:
```typescript
// Password
await lnc.login({ method: 'password', password: 'my-secure-password' });
// Passkey (triggers browser biometric / hardware key prompt)
await lnc.login({ method: 'passkey' });
// Session (no user interaction needed)
await lnc.login({ method: 'session' });
```
`login()` is a convenience that calls `unlock()` (decrypts credentials) followed by `connect()` (connects to the proxy). It throws if unlock fails, so wrap in try/catch. You can also call them separately if you need to inspect state between the two steps:
```typescript
const success = await lnc.unlock({ method: 'password', password });
if (success) {
await lnc.connect();
}
```
## Accessing Services
All Lightning services are available as typed properties on the `lnc` object. Sub-services are nested, and all names are camelCased.
```typescript
const { lnd, loop, pool, faraday, tapd, lit } = lnc;
// LND
const info = await lnd.lightning.getInfo();
const channels = await lnd.lightning.listChannels();
const invoices = await lnd.lightning.listInvoices();
await lnd.lightning.connectPeer({
addr: { pubkey: '03aa49c1...', host: 'host:9735' },
});
const signature = await lnd.signer.signMessage({ msg: myBuffer });
// Loop
const swaps = await loop.swapClient.listSwaps();
// Pool
const account = await pool.trader.initAccount({
accountValue: 100000000,
relativeHeight: 1000,
});
// Faraday
const insights = await faraday.faradayServer.channelInsights();
// Taproot Assets
const assets = await tapd.taprootAssets.listAssets();
// Lightning Terminal
const sessions = await lit.sessions.listSessions();
```
### Subscriptions
For streaming RPC endpoints, pass a message handler and an optional error handler:
```typescript
// Subscribe to on-chain transactions
lnc.lnd.lightning.subscribeTransactions(
{},
(transaction) => console.log('New tx:', transaction),
(error) => console.error('Stream error:', error),
);
// Subscribe to channel events
lnc.lnd.lightning.subscribeChannelEvents(
{},
(event) => console.log('Channel event:', event),
(error) => console.error('Stream error:', error),
);
```
## Sessions
When `enableSessions` is `true` (the default), a session is automatically created after each successful connection. Sessions allow returning users to reconnect without re-entering their password or triggering a passkey prompt, as long as the browser tab/window remains open.
Sessions are stored in `sessionStorage` and are scoped to the browser tab. They expire based on the `session` config.
### How sessions work
1. After a successful `pair()` or `login()`, the library encrypts the connection keys and stores them in `sessionStorage`.
2. On the next page load (within the same tab), `getAuthenticationInfo()` will report `hasActiveSession: true`.
3. Calling `login({ method: 'session' })` restores the keys without any user interaction.
4. If `enableActivityRefresh` is `true`, the session is automatically extended on activity, up to `maxRefreshes` times.
5. Once `maxSessionAgeMs` is reached or the tab is closed, the session expires.
### Auto-restore on page load
You can use `tryAutoRestore()` to pre-check whether a session can be restored, without connecting:
```typescript
const restored = await lnc.tryAutoRestore();
if (restored) {
// Credentials are in memory - connect when ready
await lnc.connect();
}
```
### Session configuration
```typescript
const lnc = new LightningNodeConnect({
enableSessions: true,
session: {
sessionDurationMs: 30 * 60 * 1000, // 30 minutes
enableActivityRefresh: true, // extend session on activity
maxRefreshes: 5, // allow up to 5 extensions
maxSessionAgeMs: 4 * 60 * 60 * 1000, // hard cap at 4 hours
},
});
```
To disable sessions entirely:
```typescript
const lnc = new LightningNodeConnect({ enableSessions: false });
```
## Passkeys
Passkeys use the [WebAuthn](https://webauthn.guide/) standard to encrypt stored credentials with biometric authentication (fingerprint, Face ID) or a hardware security key. This provides a passwordless experience for returning users.
### How passkeys work
1. During pairing, calling `persistWithPasskey()` (or using `pair(phrase, { method: 'passkey' })`) creates a WebAuthn credential and uses it to encrypt the connection keys.
2. The encrypted keys are stored in IndexedDB (not localStorage).
3. On subsequent visits, calling `login({ method: 'passkey' })` triggers the browser's biometric prompt. The resulting WebAuthn assertion is used to decrypt the keys.
### Checking passkey support
```typescript
// Instance method - checks config AND browser support
if (lnc.supportsPasskeys()) {
// Show passkey option in UI
}
// Static method - checks browser support only (no instance needed)
if (await LightningNodeConnect.isPasskeySupported()) {
// Browser supports passkeys
}
```
### Passkey configuration
```typescript
const lnc = new LightningNodeConnect({
allowPasskeys: true, // enable passkey support (default: true)
passkeyDisplayName: 'My Lightning App', // name shown during passkey creation
});
```
To disable passkeys:
```typescript
const lnc = new LightningNodeConnect({ allowPasskeys: false });
```
## Clearing Credentials
The `clear()` method supports two levels of cleanup:
```typescript
// Logout - clears session only (default). User can still log back in with password/passkey.
lnc.clear();
// Forget this node - clears session AND long-term stored credentials.
// User will need to pair again with a new phrase.
lnc.clear({ persisted: true });
```
`clear()` options:
| Option | Default | Description |
| ----------- | ------- | -------------------------------------------------------- |
| `session` | `true` | Clear the short-term session from `sessionStorage` |
| `persisted` | `false` | Clear long-term credentials from `localStorage` / IndexedDB |
> **Note:** `clear()` only removes stored credentials — it does not tear down the active WASM connection. To fully disconnect, perform a page reload after clearing (e.g. `window.location.reload()`).
## Status Properties
```typescript
lnc.isReady; // true when WASM client is loaded and ready
lnc.isConnected; // true when connected to the LNC proxy server
lnc.status; // current status string
lnc.expiry; // Date when the LNC session token expires
lnc.isReadOnly; // true if the connection has read-only permissions
lnc.hasPerms('lnrpc.Lightning.SendPaymentSync'); // check a specific permission
```
## Preloading WASM
The WASM binary (~5 MB) is downloaded on first connection. To improve perceived performance, preload it on your pairing/login screens while waiting for user input:
```typescript
const lnc = new LightningNodeConnect({ namespace: 'my-app' });
// Start downloading WASM immediately (e.g., in a useEffect or on app mount)
lnc.preload();
// Later, when the user is ready to pair or log in, the WASM is already cached
await lnc.pair(phrase, { method: 'password', password });
```
## Multiple Connections
Use unique `namespace` values to maintain simultaneous connections to different nodes:
```typescript
const alice = new LightningNodeConnect({ namespace: 'alice' });
const bob = new LightningNodeConnect({ namespace: 'bob' });
await alice.pair(alicePhrase, { method: 'password', password: alicePassword });
await bob.pair(bobPhrase, { method: 'password', password: bobPassword });
// Each instance has its own credentials, sessions, and service objects
const aliceChannels = await alice.lnd.lightning.listChannels();
const bobChannels = await bob.lnd.lightning.listChannels();
```
Credentials, sessions, and passkeys are all scoped to the namespace, so they won't collide.
## React Integration
A common pattern is to create a singleton instance in a custom hook:
```typescript
import { useCallback, useEffect, useState } from 'react';
import {
AuthenticationInfo,
LightningNodeConnect,
PersistOptions,
UnlockOptions,
} from '/lnc-web';
const lnc = new LightningNodeConnect({
namespace: 'my-app',
allowPasskeys: true,
enableSessions: true,
});
const useLNC = () => {
const [auth, setAuth] = useState<AuthenticationInfo | null>(null);
useEffect(() => {
lnc.getAuthenticationInfo().then(setAuth);
}, []);
const pair = useCallback(
async (phrase: string, options: PersistOptions) => {
await lnc.pair(phrase);
await lnc.lnd.lightning.listChannels(); // verify connection
if (options.method === 'password') {
await lnc.persistWithPassword(options.password);
} else if (options.method === 'passkey') {
await lnc.persistWithPasskey();
}
},
[],
);
const login = useCallback(async (options: UnlockOptions) => {
await lnc.login(options);
}, []);
const logout = useCallback(() => {
lnc.clear();
window.location.reload();
}, []);
return { lnc, pair, login, logout, auth };
};
export default useLNC;
```
### Auto-connect with sessions
Restore sessions automatically when the page loads:
```typescript
import { useEffect, useState } from 'react';
import useLNC from './useLNC';
export const useAutoConnect = () => {
const { lnc, login, auth } = useLNC();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
if (auth?.hasActiveSession && !lnc.isConnected) {
setLoading(true);
login({ method: 'session' })
.catch((err) => setError((err as Error).message))
.finally(() => setLoading(false));
}
}, [auth?.hasActiveSession, lnc.isConnected, login]);
return { loading, error };
};
```
## Legacy API
The original `LNC` class is still available as the default export for backwards compatibility:
```typescript
import LNC from '/lnc-web';
```
See [LEGACY_LNC.md](LEGACY_LNC.md) for the legacy API documentation and [LEGACY_MIGRATE.md](LEGACY_MIGRATE.md) for migration instructions.
## Further Documentation
- [Lightning Node Connect](https://docs.lightning.engineering/lightning-network-tools/lightning-terminal/lightning-node-connect)
- [LNC npm package docs](https://docs.lightning.engineering/lightning-network-tools/lightning-terminal/lnc-npm)