thunderhub/docs/ADDING_A_PROVIDER.md
2026-03-17 18:15:21 -06:00

8.7 KiB

Adding a New Lightning Provider

This guide walks through adding a new Lightning implementation (e.g., CLN, Eclair) to ThunderHub.

Architecture Overview

GraphQL Resolvers
       │
       ▼
   NodeService          ← dispatches by account.type
       │
       ▼
ProviderRegistryService ← maps NodeType → LightningProvider
       │
       ├── LndService (type: 'lnd')
       ├── YourService (type: 'your-type')
       └── ...

Each provider implements the LightningProvider interface from lightning.types.ts. The NodeService looks up the correct provider for each account via ProviderRegistryService and delegates all calls to it. Resolvers never interact with providers directly.

Steps

1. Add the node type constant

In src/server/modules/node/lightning.types.ts, add your type to the NodeType object:

export const NodeType = {
  LND: 'lnd',
  YOUR_TYPE: 'your-type',       // ← add this
} as const;

2. Create the provider directory and service

Create a new directory under src/server/modules/node/:

src/server/modules/node/your-type/
├── your-type.module.ts
├── your-type.service.ts
└── your-type.helpers.ts     (optional, for error handling etc.)

your-type.service.ts

Your service must implement LightningProvider. Use LndService (src/server/modules/node/lnd/lnd.service.ts) as a reference.

import { Injectable } from '@nestjs/common';
import EventEmitter from 'events';
import {
  Capability,
  LightningProvider,
  GetChannelsOptions,
  OpenChannelOptions,
  CloseChannelOptions,
  PayOptions,
  CreateInvoiceOptions,
  // ... import all option types you need
} from '../lightning.types';

@Injectable()
export class YourTypeService implements LightningProvider {
  // Return only the capabilities your implementation supports.
  // The UI can query this to hide unsupported features.
  getCapabilities(): Set<Capability> {
    return new Set([
      Capability.WALLET_INFO,
      Capability.CHANNELS,
      Capability.CHAIN,
      Capability.INVOICES,
      Capability.PAYMENTS,
      Capability.PEERS,
      // omit capabilities you don't support
    ]);
  }

  // Create and return a connection object from account config.
  // This is called once at startup per account.
  // The returned object is stored as `account.connection` and passed
  // as the first argument to every other method.
  connect(config: {
    socket: string;
    cert?: string;
    macaroon?: string;
    authToken?: string;
  }): YourConnectionType {
    // e.g., create an axios instance, gRPC client, etc.
    return createYourClient({
      url: config.socket,
      token: config.authToken,
    });
  }

  // Implement each method from the LightningProvider interface.
  // The first parameter is always the connection object returned by connect().
  //
  // For methods your implementation doesn't support, throw an error:
  //
  //   async grantAccess(connection: YourConnectionType): Promise<any> {
  //     throw new Error('grantAccess is not supported by your-type');
  //   }

  async getWalletInfo(connection: YourConnectionType) {
    // Call your node's API and return the result.
    // The return shape should match what the GraphQL resolvers expect.
    // See the LND provider for the expected shapes.
    const info = await connection.getInfo();
    return {
      public_key: info.pubkey,
      alias: info.alias,
      chains: [info.chain],
      // ...
    };
  }

  // ... implement remaining methods
}

Key points about the connection parameter:

  • It's the object returned by your connect() method
  • It can be anything: an axios instance, a gRPC client, a WebSocket connection, etc.
  • The LightningProvider interface types it as any so each provider can use its own connection type
  • Within your service, type it concretely (e.g., connection: AxiosInstance)

Key points about return values:

  • Return shapes should match what the existing GraphQL resolvers expect
  • Look at the LND provider's return values (which come from the lightning npm package) as the reference
  • The resolvers access fields like result.public_key, result.channels, result.tokens, etc.
  • If your implementation returns data in a different shape, map it in your service

your-type.module.ts

import { Module } from '@nestjs/common';
import { YourTypeService } from './your-type.service';

@Module({
  providers: [YourTypeService],
  exports: [YourTypeService],
})
export class YourTypeModule {}

3. Register the provider

In src/server/modules/node/provider-registry.service.ts, inject your service and register it:

import { Injectable } from '@nestjs/common';
import { LightningProvider, NodeType } from './lightning.types';
import { LndService } from './lnd/lnd.service';
import { YourTypeService } from './your-type/your-type.service';

@Injectable()
export class ProviderRegistryService {
  private providers = new Map<string, LightningProvider>();

  constructor(
    lndService: LndService,
    yourTypeService: YourTypeService,     // ← add this
  ) {
    this.providers.set(NodeType.LND, lndService);
    this.providers.set(NodeType.YOUR_TYPE, yourTypeService);  // ← add this
  }

  // ... rest unchanged
}

In src/server/modules/node/provider-registry.module.ts, import your module:

import { Module } from '@nestjs/common';
import { LndModule } from './lnd/lnd.module';
import { YourTypeModule } from './your-type/your-type.module';
import { ProviderRegistryService } from './provider-registry.service';

@Module({
  imports: [LndModule, YourTypeModule],   // ← add YourTypeModule
  providers: [ProviderRegistryService],
  exports: [ProviderRegistryService],
})
export class ProviderRegistryModule {}

4. Account configuration

Users configure accounts in their YAML config file. Your new type is selected via the type field:

masterPassword: 'your-password'
accounts:
  - name: 'My Node'
    type: your-type
    serverUrl: 'https://localhost:3001'
    authToken: 'your-auth-token'
    password: 'account-password'

The type field defaults to lnd when omitted, so existing configs are backwards compatible.

Account fields available to all types: name, serverUrl, password, type, authToken.

LND-specific fields (ignored for other types): lndDir, macaroonPath, macaroon, certificatePath, certificate, network, encrypted.

If your implementation needs additional config fields, add them to AccountType and UnresolvedAccountType in src/server/modules/files/files.types.ts, and to ParsedAccount if they need to be passed through to the service.

5. Subscriptions (optional)

The subscription service (src/server/modules/sub/sub.service.ts) currently only supports LND nodes — it filters accounts by account.type === NodeType.LND. If your implementation supports event subscriptions, you'll need to:

  1. Add subscription methods to the LightningProvider interface (or handle them separately)
  2. Update sub.service.ts to handle your node type

Full list of LightningProvider methods

These are grouped by capability. If your implementation doesn't support a group, omit those capabilities from getCapabilities() and throw in the method bodies.

Capability Methods
WALLET_INFO getWalletInfo, getIdentity, getWalletVersion, getHeight
CHANNELS getChannels, getClosedChannels, getPendingChannels, getChannel, openChannel, closeChannel, getChannelBalance
CHAIN getChainBalance, getPendingChainBalance, getChainTransactions, getUtxos, createChainAddress, sendToChainAddress
PAYMENTS pay, payViaPaymentDetails, decodePaymentRequest, getPayments
INVOICES createInvoice, getInvoices, subscribeToInvoice
PEERS getPeers, addPeer, removePeer
FORWARDS getForwards
BACKUPS getBackups, verifyBackup, verifyBackups, recoverFundsFromChannels
MACAROONS grantAccess
SIGN_MESSAGE signMessage, verifyMessage
NETWORK_INFO getNetworkInfo, getNode
ROUTING_FEES updateRoutingFees
DIFFIE_HELLMAN diffieHellmanComputeSecret

Files you'll touch

File Change
src/server/modules/node/lightning.types.ts Add to NodeType
src/server/modules/node/your-type/your-type.service.ts New — implement LightningProvider
src/server/modules/node/your-type/your-type.module.ts New — NestJS module
src/server/modules/node/provider-registry.service.ts Inject and register new service
src/server/modules/node/provider-registry.module.ts Import new module
src/server/modules/files/files.types.ts Add config fields if needed

Files you do not need to touch: NodeService, AccountsService, any resolver, any frontend code (for basic support).