diff --git a/macaroon_recipes.go b/macaroon_recipes.go index 371788a..4677ee4 100644 --- a/macaroon_recipes.go +++ b/macaroon_recipes.go @@ -28,24 +28,25 @@ var ( // implemented in lndclient and the value is the original name of the // RPC method defined in the proto. renames = map[string]string{ - "ChannelBackup": "ExportChannelBackup", - "ChannelBackups": "ExportAllChannelBackups", - "ConfirmedWalletBalance": "WalletBalance", - "Connect": "ConnectPeer", - "DecodePaymentRequest": "DecodePayReq", - "ListTransactions": "GetTransactions", - "PayInvoice": "SendPaymentSync", - "UpdateChanPolicy": "UpdateChannelPolicy", - "NetworkInfo": "GetNetworkInfo", - "SubscribeGraph": "SubscribeChannelGraph", - "InterceptHtlcs": "HtlcInterceptor", - "ImportMissionControl": "XImportMissionControl", - "EstimateFeeRate": "EstimateFee", - "EstimateFeeToP2WSH": "EstimateFee", - "OpenChannelStream": "OpenChannel", - "ListSweepsVerbose": "ListSweeps", - "MinRelayFee": "EstimateFee", - "SignOutputRawKeyLocator": "SignOutputRaw", + "ChannelBackup": "ExportChannelBackup", + "ChannelBackups": "ExportAllChannelBackups", + "ConfirmedWalletBalance": "WalletBalance", + "Connect": "ConnectPeer", + "DecodePaymentRequest": "DecodePayReq", + "ListTransactions": "GetTransactions", + "PayInvoice": "SendPaymentSync", + "UpdateChanPolicy": "UpdateChannelPolicy", + "NetworkInfo": "GetNetworkInfo", + "SubscribeGraph": "SubscribeChannelGraph", + "InterceptHtlcs": "HtlcInterceptor", + "ImportMissionControl": "XImportMissionControl", + "EstimateFeeRate": "EstimateFee", + "EstimateFeeToP2WSH": "EstimateFee", + "EstimateRouteFeeWithRequest": "EstimateRouteFee", + "OpenChannelStream": "OpenChannel", + "ListSweepsVerbose": "ListSweeps", + "MinRelayFee": "EstimateFee", + "SignOutputRawKeyLocator": "SignOutputRaw", } // ignores is a list of method names on the client implementations that diff --git a/router_client.go b/router_client.go index d603be9..98f87e3 100644 --- a/router_client.go +++ b/router_client.go @@ -45,9 +45,19 @@ type RouterClient interface { // EstimateRouteFee uses the channel router's internal state to estimate // the routing cost of the given amount to the destination node. + // + // Deprecated: use EstimateRouteFeeWithRequest for full request and + // response support. EstimateRouteFee(ctx context.Context, dest route.Vertex, amt btcutil.Amount) (lnwire.MilliSatoshi, error) + // EstimateRouteFeeWithRequest estimates routing costs either by using + // the channel router's internal graph, or by probing with a payment + // request. + EstimateRouteFeeWithRequest(ctx context.Context, + request EstimateRouteFeeRequest) (*EstimateRouteFeeResponse, + error) + // SubscribeHtlcEvents subscribes to a stream of htlc events from the // router. SubscribeHtlcEvents(ctx context.Context) (<-chan *routerrpc.HtlcEvent, @@ -318,6 +328,43 @@ type SendPaymentRequest struct { FirstHopCustomRecords map[uint64][]byte } +// EstimateRouteFeeRequest defines the parameters for a route fee estimate. +type EstimateRouteFeeRequest struct { + // Dest is the destination one wishes to obtain a routing fee quote to. + // If set, Amount must also be set. This triggers a graph-based fee + // estimation and cannot be used with PaymentRequest. + Dest *route.Vertex + + // Amount is the amount one wishes to send to the target destination in + // satoshis. It is only used in combination with Dest. + Amount btcutil.Amount + + // PaymentRequest is an encoded payment request of the target node. If + // set, lnd sends probe payments to estimate routing fees. It cannot be + // used with Dest and Amount. + PaymentRequest string + + // Timeout is the maximum time that a probe payment should be allowed to + // take. It is only used in combination with PaymentRequest. + Timeout time.Duration +} + +// EstimateRouteFeeResponse is the response of a route fee estimate. +type EstimateRouteFeeResponse struct { + // RoutingFee is a lower bound of the estimated fee to the target + // destination within the network. + RoutingFee lnwire.MilliSatoshi + + // TimeLockDelay is an estimate of the worst-case time delay that can + // occur. Callers still need to factor in the final CLTV delta of the + // last hop into this value. + TimeLockDelay int64 + + // FailureReason indicates whether a probing payment succeeded or + // whether and why it failed. FAILURE_REASON_NONE indicates success. + FailureReason lnrpc.PaymentFailureReason +} + // InterceptedHtlc contains information about a htlc that was intercepted in // lnd's switch. type InterceptedHtlc struct { @@ -604,21 +651,64 @@ func (r *routerClient) trackPayment(ctx context.Context, // EstimateRouteFee uses the channel router's internal state to estimate the // routing cost of the given amount to the destination node. +// +// Deprecated: use EstimateRouteFeeWithRequest for full request and response +// support. func (r *routerClient) EstimateRouteFee(ctx context.Context, dest route.Vertex, amt btcutil.Amount) (lnwire.MilliSatoshi, error) { + res, err := r.EstimateRouteFeeWithRequest(ctx, EstimateRouteFeeRequest{ + Dest: &dest, + Amount: amt, + }) + if err != nil { + return 0, err + } + + return res.RoutingFee, nil +} + +// EstimateRouteFeeWithRequest estimates routing costs either by using the +// channel router's internal graph, or by probing with a payment request. +func (r *routerClient) EstimateRouteFeeWithRequest(ctx context.Context, + request EstimateRouteFeeRequest) (*EstimateRouteFeeResponse, error) { + rpcCtx := r.routerKitMac.WithMacaroonAuth(ctx) rpcReq := &routerrpc.RouteFeeRequest{ - Dest: dest[:], - AmtSat: int64(amt), + AmtSat: int64(request.Amount), + PaymentRequest: request.PaymentRequest, + } + + if request.Dest != nil { + rpcReq.Dest = request.Dest[:] + } + + if request.Timeout < 0 { + return nil, fmt.Errorf("timeout must not be negative") + } + + if request.Timeout > 0 { + const maxTimeout = time.Duration(^uint32(0)) * time.Second + if request.Timeout > maxTimeout { + return nil, fmt.Errorf("timeout exceeds maximum of %v", + maxTimeout) + } + + rpcReq.Timeout = uint32( + (request.Timeout + time.Second - 1) / time.Second, + ) } rpcRes, err := r.client.EstimateRouteFee(rpcCtx, rpcReq) if err != nil { - return 0, err + return nil, err } - return lnwire.MilliSatoshi(rpcRes.RoutingFeeMsat), nil + return &EstimateRouteFeeResponse{ + RoutingFee: lnwire.MilliSatoshi(rpcRes.RoutingFeeMsat), + TimeLockDelay: rpcRes.TimeLockDelay, + FailureReason: rpcRes.FailureReason, + }, nil } // unmarshallPaymentStatus converts an rpc status update to the PaymentStatus diff --git a/router_client_test.go b/router_client_test.go new file mode 100644 index 0000000..83f7102 --- /dev/null +++ b/router_client_test.go @@ -0,0 +1,151 @@ +package lndclient + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type mockRouterRPCClient struct { + routerrpc.RouterClient + + request *routerrpc.RouteFeeRequest + response *routerrpc.RouteFeeResponse + err error +} + +func (m *mockRouterRPCClient) EstimateRouteFee(_ context.Context, + request *routerrpc.RouteFeeRequest, _ ...grpc.CallOption) ( + *routerrpc.RouteFeeResponse, error) { + + m.request = request + return m.response, m.err +} + +func TestEstimateRouteFeeWithRequestGraph(t *testing.T) { + t.Parallel() + + dest := testVertex() + mock := &mockRouterRPCClient{ + response: &routerrpc.RouteFeeResponse{ + RoutingFeeMsat: 123, + TimeLockDelay: 456, + FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, + }, + } + client := &routerClient{ + client: mock, + } + + resp, err := client.EstimateRouteFeeWithRequest( + context.Background(), + EstimateRouteFeeRequest{ + Dest: &dest, + Amount: 1000, + }, + ) + require.NoError(t, err) + + require.Equal(t, dest[:], mock.request.Dest) + require.Equal(t, int64(1000), mock.request.AmtSat) + require.Empty(t, mock.request.PaymentRequest) + require.Zero(t, mock.request.Timeout) + require.Equal(t, lnwire.MilliSatoshi(123), resp.RoutingFee) + require.Equal(t, int64(456), resp.TimeLockDelay) + require.Equal( + t, lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, + resp.FailureReason, + ) +} + +func TestEstimateRouteFeeWithRequestPaymentRequest(t *testing.T) { + t.Parallel() + + mock := &mockRouterRPCClient{ + response: &routerrpc.RouteFeeResponse{ + RoutingFeeMsat: 987, + TimeLockDelay: 654, + FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, + }, + } + client := &routerClient{ + client: mock, + } + + resp, err := client.EstimateRouteFeeWithRequest( + context.Background(), + EstimateRouteFeeRequest{ + PaymentRequest: "lnbc1...", + Timeout: 1500 * time.Millisecond, + }, + ) + require.NoError(t, err) + + require.Empty(t, mock.request.Dest) + require.Zero(t, mock.request.AmtSat) + require.Equal(t, "lnbc1...", mock.request.PaymentRequest) + require.Equal(t, uint32(2), mock.request.Timeout) + require.Equal(t, lnwire.MilliSatoshi(987), resp.RoutingFee) + require.Equal(t, int64(654), resp.TimeLockDelay) + require.Equal( + t, lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, + resp.FailureReason, + ) +} + +func TestEstimateRouteFee(t *testing.T) { + t.Parallel() + + dest := testVertex() + mock := &mockRouterRPCClient{ + response: &routerrpc.RouteFeeResponse{ + RoutingFeeMsat: 4321, + }, + } + client := &routerClient{ + client: mock, + } + + fee, err := client.EstimateRouteFee( + context.Background(), dest, btcutil.Amount(1000), + ) + require.NoError(t, err) + + require.Equal(t, dest[:], mock.request.Dest) + require.Equal(t, int64(1000), mock.request.AmtSat) + require.Equal(t, lnwire.MilliSatoshi(4321), fee) +} + +func TestEstimateRouteFeeWithRequestRejectsInvalidTimeout(t *testing.T) { + t.Parallel() + + client := &routerClient{ + client: &mockRouterRPCClient{}, + } + + _, err := client.EstimateRouteFeeWithRequest( + context.Background(), + EstimateRouteFeeRequest{ + PaymentRequest: "lnbc1...", + Timeout: -time.Second, + }, + ) + require.ErrorContains(t, err, "timeout must not be negative") +} + +func testVertex() route.Vertex { + var vertex route.Vertex + for i := range vertex { + vertex[i] = byte(i + 1) + } + + return vertex +}