feat(extensions): add extension loader infrastructure (#3)
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run

## Summary

- Adds a modular extension system for Lightning.Pub enabling third-party plugins
- Provides isolated SQLite databases per extension for data safety
- Implements ExtensionContext API for accessing Lightning.Pub services (payments, Nostr, storage)
- Supports RPC method registration with automatic namespacing
- Includes HTTP route handling for protocols like LNURL
- Event routing for payment receipts and Nostr events
- Comprehensive documentation with architecture overview and working examples

## Key Components

- `src/extensions/types.ts` - Core extension interfaces
- `src/extensions/loader.ts` - Extension discovery, loading, and lifecycle management
- `src/extensions/context.ts` - Bridge between extensions and Lightning.Pub services
- `src/extensions/database.ts` - SQLite isolation with WAL mode
- `src/extensions/README.md` - Full documentation with examples

## ExtensionContext API

| Method | Description |
|--------|-------------|
| `getApplication()` | Get application info |
| `createInvoice()` | Create Lightning invoice |
| `payInvoice()` | Pay Lightning invoice |
| `getLnurlPayInfo()` | Get LNURL-pay info for a user (enables Lightning Address/zaps) |
| `sendEncryptedDM()` | Send Nostr DM (NIP-44) |
| `publishNostrEvent()` | Publish Nostr event |
| `registerMethod()` | Register RPC method |
| `onPaymentReceived()` | Subscribe to payment callbacks |
| `onNostrEvent()` | Subscribe to Nostr events |

## Test plan

- [x] Review extension loader code for correctness
- [x] Verify TypeScript compilation succeeds
- [x] Test extension discovery from `src/extensions/` directory
- [x] Test RPC method registration and routing
- [x] Test database isolation between extensions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: boufni95 <boufni95@gmail.com>
Co-authored-by: Patrick Mulligan <patjmulligan@protonmail.com>
Reviewed-on: #3
This commit is contained in:
padreug 2026-04-02 18:47:55 +00:00
parent 72c9872b23
commit 77e5772afd
47 changed files with 10187 additions and 4828 deletions

View file

@ -1,22 +1,43 @@
syntax = "proto3";
import "lightning.proto";
package routerrpc;
import "lightning.proto";
option go_package = "github.com/lightningnetwork/lnd/lnrpc/routerrpc";
/*
* Comments in this file will be directly parsed into the API
* Documentation as descriptions of the associated method, message, or field.
* These descriptions should go right above the definition of the object, and
* can be in either block or // comment format.
*
* An RPC method can be matched to an lncli command by placing a line in the
* beginning of the description in exactly the following format:
* lncli: `methodname`
*
* Failure to specify the exact name of the command will cause documentation
* generation to fail.
*
* More information on how exactly the gRPC documentation is generated from
* this proto file can be found here:
* https://github.com/lightninglabs/lightning-api
*/
// Router is a service that offers advanced interaction with the router
// subsystem of the daemon.
service Router {
/*
SendPaymentV2 attempts to route a payment described by the passed
PaymentRequest to the final destination. The call returns a stream of
payment updates.
payment updates. When using this RPC, make sure to set a fee limit, as the
default routing fee limit is 0 sats. Without a non-zero fee limit only
routes without fees will be attempted which often fails with
FAILURE_REASON_NO_ROUTE.
*/
rpc SendPaymentV2 (SendPaymentRequest) returns (stream lnrpc.Payment);
/*
/* lncli: `trackpayment`
TrackPaymentV2 returns an update stream for the payment identified by the
payment hash.
*/
@ -57,21 +78,21 @@ service Router {
*/
rpc SendToRouteV2 (SendToRouteRequest) returns (lnrpc.HTLCAttempt);
/*
/* lncli: `resetmc`
ResetMissionControl clears all mission control state and starts with a clean
slate.
*/
rpc ResetMissionControl (ResetMissionControlRequest)
returns (ResetMissionControlResponse);
/*
/* lncli: `querymc`
QueryMissionControl exposes the internal mission control state to callers.
It is a development feature.
*/
rpc QueryMissionControl (QueryMissionControlRequest)
returns (QueryMissionControlResponse);
/*
/* lncli: `importmc`
XImportMissionControl is an experimental API that imports the state provided
to the internal mission control's state, using all results which are more
recent than our existing values. These values will only be imported
@ -80,30 +101,36 @@ service Router {
rpc XImportMissionControl (XImportMissionControlRequest)
returns (XImportMissionControlResponse);
/*
/* lncli: `getmccfg`
GetMissionControlConfig returns mission control's current config.
*/
rpc GetMissionControlConfig (GetMissionControlConfigRequest)
returns (GetMissionControlConfigResponse);
/*
/* lncli: `setmccfg`
SetMissionControlConfig will set mission control's config, if the config
provided is valid.
*/
rpc SetMissionControlConfig (SetMissionControlConfigRequest)
returns (SetMissionControlConfigResponse);
/*
QueryProbability returns the current success probability estimate for a
given node pair and amount.
/* lncli: `queryprob`
Deprecated. QueryProbability returns the current success probability
estimate for a given node pair and amount. The call returns a zero success
probability if no channel is available or if the amount violates min/max
HTLC constraints.
*/
rpc QueryProbability (QueryProbabilityRequest)
returns (QueryProbabilityResponse);
/*
/* lncli: `buildroute`
BuildRoute builds a fully specified route based on a list of hop public
keys. It retrieves the relevant channel policies from the graph in order to
calculate the correct fees and time locks.
Note that LND will use its default final_cltv_delta if no value is supplied.
Make sure to add the correct final_cltv_delta depending on the invoice
restriction. Moreover the caller has to make sure to provide the
payment_addr if the route is paying an invoice which signaled it.
*/
rpc BuildRoute (BuildRouteRequest) returns (BuildRouteResponse);
@ -141,7 +168,7 @@ service Router {
rpc HtlcInterceptor (stream ForwardHtlcInterceptResponse)
returns (stream ForwardHtlcInterceptRequest);
/*
/* lncli: `updatechanstatus`
UpdateChanStatus attempts to manually set the state of a channel
(enabled, disabled, or auto). A manual "disable" request will cause the
channel to stay disabled until a subsequent manual request of either
@ -149,6 +176,25 @@ service Router {
*/
rpc UpdateChanStatus (UpdateChanStatusRequest)
returns (UpdateChanStatusResponse);
/*
XAddLocalChanAliases is an experimental API that creates a set of new
channel SCID alias mappings. The final total set of aliases in the manager
after the add operation is returned. This is only a locally stored alias,
and will not be communicated to the channel peer via any message. Therefore,
routing over such an alias will only work if the peer also calls this same
RPC on their end. If an alias already exists, an error is returned
*/
rpc XAddLocalChanAliases (AddAliasesRequest) returns (AddAliasesResponse);
/*
XDeleteLocalChanAliases is an experimental API that deletes a set of alias
mappings. The final total set of aliases in the manager after the delete
operation is returned. The deletion will not be communicated to the channel
peer via any message.
*/
rpc XDeleteLocalChanAliases (DeleteAliasesRequest)
returns (DeleteAliasesResponse);
}
message SendPaymentRequest {
@ -162,13 +208,6 @@ message SendPaymentRequest {
*/
int64 amt = 2;
/*
Number of millisatoshis to send.
The fields amt and amt_msat are mutually exclusive.
*/
int64 amt_msat = 12;
// The hash to use within the payment's HTLC
bytes payment_hash = 3;
@ -178,9 +217,6 @@ message SendPaymentRequest {
*/
int32 final_cltv_delta = 4;
// An optional payment addr to be included within the last hop of the route.
bytes payment_addr = 20;
/*
A bare-bones invoice for a payment within the Lightning Network. With the
details of the invoice, the sender has all the data necessary to send a
@ -191,10 +227,11 @@ message SendPaymentRequest {
string payment_request = 5;
/*
An upper limit on the amount of time we should spend when attempting to
fulfill the payment. This is expressed in seconds. If we cannot make a
successful payment within this time frame, an error will be returned.
This field must be non-zero.
An optional limit, expressed in seconds, on the time to wait before
attempting the first HTLC. Once HTLCs are in flight, the payment will
not be aborted until the HTLCs are either settled or failed. If the field
is not set or is explicitly set to zero, the default value of 60 seconds
will be applied.
*/
int32 timeout_seconds = 6;
@ -208,17 +245,6 @@ message SendPaymentRequest {
*/
int64 fee_limit_sat = 7;
/*
The maximum number of millisatoshis that will be paid as a fee of the
payment. If this field is left to the default value of 0, only zero-fee
routes will be considered. This usually means single hop routes connecting
directly to the destination. To send the payment without a fee limit, use
max int here.
The fields fee_limit_sat and fee_limit_msat are mutually exclusive.
*/
int64 fee_limit_msat = 13;
/*
Deprecated, use outgoing_chan_ids. The channel id of the channel that must
be taken to the first hop. If zero, any channel may be used (unless
@ -227,19 +253,8 @@ message SendPaymentRequest {
uint64 outgoing_chan_id = 8 [jstype = JS_STRING, deprecated = true];
/*
The channel ids of the channels are allowed for the first hop. If empty,
any channel may be used.
*/
repeated uint64 outgoing_chan_ids = 19;
/*
The pubkey of the last hop of the route. If empty, any hop may be used.
*/
bytes last_hop_pubkey = 14;
/*
An optional maximum total time lock for the route. This should not exceed
lnd's `--max-cltv-expiry` setting. If zero, then the value of
An optional maximum total time lock for the route. This should not
exceed lnd's `--max-cltv-expiry` setting. If zero, then the value of
`--max-cltv-expiry` is enforced.
*/
int32 cltv_limit = 9;
@ -258,6 +273,29 @@ message SendPaymentRequest {
*/
map<uint64, bytes> dest_custom_records = 11;
/*
Number of millisatoshis to send.
The fields amt and amt_msat are mutually exclusive.
*/
int64 amt_msat = 12;
/*
The maximum number of millisatoshis that will be paid as a fee of the
payment. If this field is left to the default value of 0, only zero-fee
routes will be considered. This usually means single hop routes connecting
directly to the destination. To send the payment without a fee limit, use
max int here.
The fields fee_limit_sat and fee_limit_msat are mutually exclusive.
*/
int64 fee_limit_msat = 13;
/*
The pubkey of the last hop of the route. If empty, any hop may be used.
*/
bytes last_hop_pubkey = 14;
// If set, circular payments to self are permitted.
bool allow_self_payment = 15;
@ -282,6 +320,18 @@ message SendPaymentRequest {
*/
bool no_inflight_updates = 18;
/*
The channel ids of the channels are allowed for the first hop. If empty,
any channel may be used.
*/
repeated uint64 outgoing_chan_ids = 19;
/*
An optional payment addr to be included within the last hop of the route.
This is also called payment secret in specifications (e.g. BOLT 11).
*/
bytes payment_addr = 20;
/*
The largest payment split that should be attempted when making a payment if
splitting is necessary. Setting this value will effectively cause lnd to
@ -300,6 +350,24 @@ message SendPaymentRequest {
only, to 1 to optimize for reliability only or a value inbetween for a mix.
*/
double time_pref = 23;
/*
If set, the payment loop can be interrupted by manually canceling the
payment context, even before the payment timeout is reached. Note that the
payment may still succeed after cancellation, as in-flight attempts can
still settle afterwards. Canceling will only prevent further attempts from
being sent.
*/
bool cancelable = 24;
/*
An optional field that can be used to pass an arbitrary set of TLV records
to the first hop peer of this payment. This can be used to pass application
specific data during the payment attempt. Record types are required to be in
the custom range >= 65536. When using REST, the values must be encoded as
base64.
*/
map<uint64, bytes> first_hop_custom_records = 25;
}
message TrackPaymentRequest {
@ -323,14 +391,39 @@ message TrackPaymentsRequest {
message RouteFeeRequest {
/*
The destination once wishes to obtain a routing fee quote to.
The destination one wishes to obtain a routing fee quote to. If set, this
parameter requires the amt_sat parameter also to be set. This parameter
combination triggers a graph based routing fee estimation as opposed to a
payment probe based estimate in case a payment request is provided. The
graph based estimation is an algorithm that is executed on the in memory
graph. Hence its runtime is significantly shorter than a payment probe
estimation that sends out actual payments to the network.
*/
bytes dest = 1;
/*
The amount one wishes to send to the target destination.
The amount one wishes to send to the target destination. It is only to be
used in combination with the dest parameter.
*/
int64 amt_sat = 2;
/*
A payment request of the target node that the route fee request is intended
for. Its parameters are input to probe payments that estimate routing fees.
The timeout parameter can be specified to set a maximum time on the probing
attempt. Cannot be used in combination with dest and amt_sat.
*/
string payment_request = 3;
/*
A user preference of how long a probe payment should maximally be allowed to
take, denoted in seconds. The probing payment loop is aborted if this
timeout is reached. Note that the probing process itself can take longer
than the timeout if the HTLC becomes delayed or stuck. Canceling the context
of this call will not cancel the payment loop, the duration is only
controlled by the timeout parameter.
*/
uint32 timeout = 4;
}
message RouteFeeResponse {
@ -346,6 +439,12 @@ message RouteFeeResponse {
value.
*/
int64 time_lock_delay = 2;
/*
An indication whether a probing payment succeeded or whether and why it
failed. FAILURE_REASON_NONE indicates success.
*/
lnrpc.PaymentFailureReason failure_reason = 5;
}
message SendToRouteRequest {
@ -362,6 +461,15 @@ message SendToRouteRequest {
routes, incorrect payment details, or insufficient funds.
*/
bool skip_temp_err = 3;
/*
An optional field that can be used to pass an arbitrary set of TLV records
to the first hop peer of this payment. This can be used to pass application
specific data during the payment attempt. Record types are required to be in
the custom range >= 65536. When using REST, the values must be encoded as
base64.
*/
map<uint64, bytes> first_hop_custom_records = 4;
}
message SendToRouteResponse {
@ -465,6 +573,93 @@ message SetMissionControlConfigResponse {
}
message MissionControlConfig {
/*
Deprecated, use AprioriParameters. The amount of time mission control will
take to restore a penalized node or channel back to 50% success probability,
expressed in seconds. Setting this value to a higher value will penalize
failures for longer, making mission control less likely to route through
nodes and channels that we have previously recorded failures for.
*/
uint64 half_life_seconds = 1 [deprecated = true];
/*
Deprecated, use AprioriParameters. The probability of success mission
control should assign to hop in a route where it has no other information
available. Higher values will make mission control more willing to try hops
that we have no information about, lower values will discourage trying these
hops.
*/
float hop_probability = 2 [deprecated = true];
/*
Deprecated, use AprioriParameters. The importance that mission control
should place on historical results, expressed as a value in [0;1]. Setting
this value to 1 will ignore all historical payments and just use the hop
probability to assess the probability of success for each hop. A zero value
ignores hop probability completely and relies entirely on historical
results, unless none are available.
*/
float weight = 3 [deprecated = true];
/*
The maximum number of payment results that mission control will store.
*/
uint32 maximum_payment_results = 4;
/*
The minimum time that must have passed since the previously recorded failure
before we raise the failure amount.
*/
uint64 minimum_failure_relax_interval = 5;
enum ProbabilityModel {
APRIORI = 0;
BIMODAL = 1;
}
/*
ProbabilityModel defines which probability estimator should be used in
pathfinding. Note that the bimodal estimator is experimental.
*/
ProbabilityModel model = 6;
/*
EstimatorConfig is populated dependent on the estimator type.
*/
oneof EstimatorConfig {
AprioriParameters apriori = 7;
BimodalParameters bimodal = 8;
}
}
message BimodalParameters {
/*
NodeWeight defines how strongly other previous forwardings on channels of a
router should be taken into account when computing a channel's probability
to route. The allowed values are in the range [0, 1], where a value of 0
means that only direct information about a channel is taken into account.
*/
double node_weight = 1;
/*
ScaleMsat describes the scale over which channels statistically have some
liquidity left. The value determines how quickly the bimodal distribution
drops off from the edges of a channel. A larger value (compared to typical
channel capacities) means that the drop off is slow and that channel
balances are distributed more uniformly. A small value leads to the
assumption of very unbalanced channels.
*/
uint64 scale_msat = 2;
/*
DecayTime describes the information decay of knowledge about previous
successes and failures in channels. The smaller the decay time, the quicker
we forget about past forwardings.
*/
uint64 decay_time = 3;
}
message AprioriParameters {
/*
The amount of time mission control will take to restore a penalized node
or channel back to 50% success probability, expressed in seconds. Setting
@ -480,7 +675,7 @@ message MissionControlConfig {
control more willing to try hops that we have no information about, lower
values will discourage trying these hops.
*/
float hop_probability = 2;
double hop_probability = 2;
/*
The importance that mission control should place on historical results,
@ -490,18 +685,15 @@ message MissionControlConfig {
completely and relies entirely on historical results, unless none are
available.
*/
float weight = 3;
double weight = 3;
/*
The maximum number of payment results that mission control will store.
The fraction of a channel's capacity that we consider to have liquidity. For
amounts that come close to or exceed the fraction, an additional penalty is
applied. A value of 1.0 disables the capacity factor. Allowed values are in
[0.75, 1.0].
*/
uint32 maximum_payment_results = 4;
/*
The minimum time that must have passed since the previously recorded failure
before we raise the failure amount.
*/
uint64 minimum_failure_relax_interval = 5;
double capacity_fraction = 4;
}
message QueryProbabilityRequest {
@ -548,8 +740,20 @@ message BuildRouteRequest {
*/
repeated bytes hop_pubkeys = 4;
// An optional payment addr to be included within the last hop of the route.
/*
An optional payment addr to be included within the last hop of the route.
This is also called payment secret in specifications (e.g. BOLT 11).
*/
bytes payment_addr = 5;
/*
An optional field that can be used to pass an arbitrary set of TLV records
to the first hop peer of this payment. This can be used to pass application
specific data during the payment attempt. Record types are required to be in
the custom range >= 65536. When using REST, the values must be encoded as
base64.
*/
map<uint64, bytes> first_hop_custom_records = 6;
}
message BuildRouteResponse {
@ -806,12 +1010,17 @@ message ForwardHtlcInterceptRequest {
// The block height at which this htlc will be auto-failed to prevent the
// channel from force-closing.
int32 auto_fail_height = 10;
// The custom records of the peer's incoming p2p wire message.
map<uint64, bytes> in_wire_custom_records = 11;
}
/**
ForwardHtlcInterceptResponse enables the caller to resolve a previously hold
forward. The caller can choose either to:
- `Resume`: Execute the default behavior (usually forward).
- `ResumeModified`: Execute the default behavior (usually forward) with HTLC
field modifications.
- `Reject`: Fail the htlc backwards.
- `Settle`: Settle this htlc with a given preimage.
*/
@ -842,12 +1051,40 @@ message ForwardHtlcInterceptResponse {
// For backwards-compatibility reasons, TEMPORARY_CHANNEL_FAILURE is the
// default value for this field.
lnrpc.Failure.FailureCode failure_code = 5;
// The amount that was set on the p2p wire message of the incoming HTLC.
// This field is ignored if the action is not RESUME_MODIFIED or the amount
// is zero.
uint64 in_amount_msat = 6;
// The amount to set on the p2p wire message of the resumed HTLC. This field
// is ignored if the action is not RESUME_MODIFIED or the amount is zero.
uint64 out_amount_msat = 7;
// Any custom records that should be set on the p2p wire message message of
// the resumed HTLC. This field is ignored if the action is not
// RESUME_MODIFIED.
//
// This map will merge with the existing set of custom records (if any),
// replacing any conflicting types. Note that there currently is no support
// for deleting existing custom records (they can only be replaced).
map<uint64, bytes> out_wire_custom_records = 8;
}
enum ResolveHoldForwardAction {
// SETTLE is an action that is used to settle an HTLC instead of forwarding
// it.
SETTLE = 0;
// FAIL is an action that is used to fail an HTLC backwards.
FAIL = 1;
// RESUME is an action that is used to resume a forward HTLC.
RESUME = 2;
// RESUME_MODIFIED is an action that is used to resume a hold forward HTLC
// with modifications specified during interception.
RESUME_MODIFIED = 3;
}
message UpdateChanStatusRequest {
@ -863,4 +1100,20 @@ enum ChanStatusAction {
}
message UpdateChanStatusResponse {
}
message AddAliasesRequest {
repeated lnrpc.AliasMap alias_maps = 1;
}
message AddAliasesResponse {
repeated lnrpc.AliasMap alias_maps = 1;
}
message DeleteAliasesRequest {
repeated lnrpc.AliasMap alias_maps = 1;
}
message DeleteAliasesResponse {
repeated lnrpc.AliasMap alias_maps = 1;
}