Browse Source

Merge pull request #387 from bhandras/loop_in_probe

loop-in:  allow clients to request server probes and extend loop-in quote with additional parameters for more accurate swap fees
pull/410/head
András Bánki-Horváth 4 months ago
committed by GitHub
parent
commit
91ad53a811
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 2218 additions and 864 deletions
  1. +17
    -1
      client.go
  2. +18
    -14
      cmd/loop/loopin.go
  3. +24
    -3
      cmd/loop/quote.go
  4. +22
    -0
      interface.go
  5. +7
    -0
      loopd/macaroons.go
  6. +100
    -0
      loopd/swapclient_server.go
  7. +8
    -1
      loopdb/protocol_version.go
  8. +2
    -0
      loopdb/protocol_version_test.go
  9. +9
    -2
      loopin.go
  10. +667
    -486
      looprpc/client.pb.go
  11. +117
    -0
      looprpc/client.pb.gw.go
  12. +40
    -0
      looprpc/client.proto
  13. +101
    -0
      looprpc/client.swagger.json
  14. +2
    -0
      looprpc/client.yaml
  15. +42
    -0
      looprpc/client_grpc.pb.go
  16. +261
    -0
      looprpc/common.pb.go
  17. +33
    -0
      looprpc/common.proto
  18. +562
    -346
      looprpc/server.pb.go
  19. +40
    -0
      looprpc/server.proto
  20. +36
    -0
      looprpc/server_grpc.pb.go
  21. +3
    -0
      release_notes.md
  22. +9
    -2
      server_mock_test.go
  23. +98
    -9
      swap_server_client.go

+ 17
- 1
client.go View File

@ -48,6 +48,9 @@ var (
// and pay for an LSAT token.
globalCallTimeout = serverRPCTimeout + lsat.PaymentTimeout
// probeTimeout is the maximum time until a probe is allowed to take.
probeTimeout = 3 * time.Minute
republishDelay = 10 * time.Second
// MinerFeeEstimationFailed is a magic number that is returned in a
@ -560,7 +563,10 @@ func (s *Client) LoopInQuote(ctx context.Context,
return nil, ErrSwapAmountTooHigh
}
quote, err := s.Server.GetLoopInQuote(ctx, request.Amount)
quote, err := s.Server.GetLoopInQuote(
ctx, request.Amount, s.lndServices.NodePubkey, request.LastHop,
request.RouteHints,
)
if err != nil {
return nil, err
}
@ -625,3 +631,13 @@ func wrapGrpcError(message string, err error) error {
grpcStatus.Message()),
)
}
// Probe asks the server to probe a route to us given a requested amount and
// last hop. The server is free to discard frequent request to avoid abuse or if
// there's been a recent probe to us for the same amount.
func (s *Client) Probe(ctx context.Context, req *ProbeRequest) error {
return s.Server.Probe(
ctx, req.Amount, s.lndServices.NodePubkey, req.LastHop,
req.RouteHints,
)
}

+ 18
- 14
cmd/loop/loopin.go View File

@ -109,11 +109,25 @@ func loopIn(ctx *cli.Context) error {
return err
}
var lastHop []byte
if ctx.IsSet(lastHopFlag.Name) {
lastHopVertex, err := route.NewVertexFromStr(
ctx.String(lastHopFlag.Name),
)
if err != nil {
return err
}
lastHop = lastHopVertex[:]
}
quoteReq := &looprpc.QuoteRequest{
Amt: int64(amt),
ConfTarget: htlcConfTarget,
ExternalHtlc: external,
Amt: int64(amt),
ConfTarget: htlcConfTarget,
ExternalHtlc: external,
LoopInLastHop: lastHop,
}
quote, err := client.GetLoopInQuote(context.Background(), quoteReq)
if err != nil {
return err
@ -147,17 +161,7 @@ func loopIn(ctx *cli.Context) error {
HtlcConfTarget: htlcConfTarget,
Label: label,
Initiator: defaultInitiator,
}
if ctx.IsSet(lastHopFlag.Name) {
lastHop, err := route.NewVertexFromStr(
ctx.String(lastHopFlag.Name),
)
if err != nil {
return err
}
req.LastHop = lastHop[:]
LastHop: lastHop,
}
resp, err := client.LoopIn(context.Background(), req)

+ 24
- 3
cmd/loop/quote.go View File

@ -8,6 +8,7 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
)
@ -22,8 +23,16 @@ var quoteInCommand = cli.Command{
Usage: "get a quote for the cost of a loop in swap",
ArgsUsage: "amt",
Description: "Allows to determine the cost of a swap up front",
Flags: []cli.Flag{confTargetFlag, verboseFlag},
Action: quoteIn,
Flags: []cli.Flag{
cli.StringFlag{
Name: lastHopFlag.Name,
Usage: "the pubkey of the last hop to use for the " +
"quote",
},
confTargetFlag,
verboseFlag,
},
Action: quoteIn,
}
func quoteIn(ctx *cli.Context) error {
@ -44,11 +53,23 @@ func quoteIn(ctx *cli.Context) error {
}
defer cleanup()
ctxb := context.Background()
quoteReq := &looprpc.QuoteRequest{
Amt: int64(amt),
ConfTarget: int32(ctx.Uint64("conf_target")),
}
if ctx.IsSet(lastHopFlag.Name) {
lastHopVertex, err := route.NewVertexFromStr(
ctx.String(lastHopFlag.Name),
)
if err != nil {
return err
}
quoteReq.LoopInLastHop = lastHopVertex[:]
}
ctxb := context.Background()
quoteResp, err := client.GetLoopInQuote(ctxb, quoteReq)
if err != nil {
return err

+ 22
- 0
interface.go View File

@ -8,6 +8,7 @@ import (
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
)
// OutRequest contains the required parameters for a loop out swap.
@ -243,6 +244,15 @@ type LoopInQuoteRequest struct {
// ExternalHtlc specifies whether the htlc is published by an external
// source.
ExternalHtlc bool
// LastHop is an optional last hop to use. This last hop is used when
// the client has already requested a server probe for more accurate
// routing fee estimation.
LastHop *route.Vertex
// RouteHints are optional route hints to reach the destination through
// private channels.
RouteHints [][]zpay32.HopHint
}
// LoopInQuote contains estimates for the fees making up the total swap cost
@ -340,3 +350,15 @@ func (s *In) LastUpdate() time.Time {
func (s *In) SwapHash() lntypes.Hash {
return s.Hash
}
// ProbeRequest specifies probe parameters for the server probe.
type ProbeRequest struct {
// Amount is the amount that will be probed.
Amount btcutil.Amount
// LastHop is the last hop along the route.
LastHop *route.Vertex
// Optional hop hints.
RouteHints [][]zpay32.HopHint
}

+ 7
- 0
loopd/macaroons.go View File

@ -95,6 +95,13 @@ var (
Entity: "suggestions",
Action: "write",
}},
"/looprpc.SwapClient/Probe": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "in",
}},
}
// allPermissions is the list of all existing permissions that exist

+ 100
- 0
loopd/swapclient_server.go View File

@ -2,12 +2,14 @@ package loopd
import (
"context"
"encoding/hex"
"errors"
"fmt"
"sort"
"sync"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/lndclient"
@ -22,6 +24,7 @@ import (
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@ -480,10 +483,29 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
return nil, err
}
var lastHop *route.Vertex
if req.LoopInLastHop != nil {
lastHopVertex, err := route.NewVertexFromBytes(
req.LoopInLastHop,
)
if err != nil {
return nil, err
}
lastHop = &lastHopVertex
}
routeHints, err := unmarshallRouteHints(req.LoopInRouteHints)
if err != nil {
return nil, err
}
quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
Amount: btcutil.Amount(req.Amt),
HtlcConfTarget: htlcConfTarget,
ExternalHtlc: req.ExternalHtlc,
LastHop: lastHop,
RouteHints: routeHints,
})
if err != nil {
return nil, err
@ -495,6 +517,84 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
}, nil
}
// unmarshallRouteHints unmarshalls a list of route hints.
func unmarshallRouteHints(rpcRouteHints []*looprpc.RouteHint) (
[][]zpay32.HopHint, error) {
routeHints := make([][]zpay32.HopHint, 0, len(rpcRouteHints))
for _, rpcRouteHint := range rpcRouteHints {
routeHint := make(
[]zpay32.HopHint, 0, len(rpcRouteHint.HopHints),
)
for _, rpcHint := range rpcRouteHint.HopHints {
hint, err := unmarshallHopHint(rpcHint)
if err != nil {
return nil, err
}
routeHint = append(routeHint, hint)
}
routeHints = append(routeHints, routeHint)
}
return routeHints, nil
}
// unmarshallHopHint unmarshalls a single hop hint.
func unmarshallHopHint(rpcHint *looprpc.HopHint) (zpay32.HopHint, error) {
pubBytes, err := hex.DecodeString(rpcHint.NodeId)
if err != nil {
return zpay32.HopHint{}, err
}
pubkey, err := btcec.ParsePubKey(pubBytes, btcec.S256())
if err != nil {
return zpay32.HopHint{}, err
}
return zpay32.HopHint{
NodeID: pubkey,
ChannelID: rpcHint.ChanId,
FeeBaseMSat: rpcHint.FeeBaseMsat,
FeeProportionalMillionths: rpcHint.FeeProportionalMillionths,
CLTVExpiryDelta: uint16(rpcHint.CltvExpiryDelta),
}, nil
}
// Probe requests the server to probe the client's node to test inbound
// liquidity.
func (s *swapClientServer) Probe(ctx context.Context,
req *looprpc.ProbeRequest) (*looprpc.ProbeResponse, error) {
log.Infof("Probe request received")
var lastHop *route.Vertex
if req.LastHop != nil {
lastHopVertex, err := route.NewVertexFromBytes(req.LastHop)
if err != nil {
return nil, err
}
lastHop = &lastHopVertex
}
routeHints, err := unmarshallRouteHints(req.RouteHints)
if err != nil {
return nil, err
}
err = s.impl.Probe(ctx, &loop.ProbeRequest{
Amount: btcutil.Amount(req.Amt),
LastHop: lastHop,
RouteHints: routeHints,
})
if err != nil {
return nil, err
}
return &looprpc.ProbeResponse{}, nil
}
func (s *swapClientServer) LoopIn(ctx context.Context,
in *looprpc.LoopInRequest) (
*looprpc.SwapResponse, error) {

+ 8
- 1
loopdb/protocol_version.go View File

@ -43,13 +43,17 @@ const (
// canceling loop out swaps.
ProtocolVersionLoopOutCancel = 7
// ProtocolVerionProbe indicates that the client is able to request
// the server to perform a probe to test inbound liquidty.
ProtocolVersionProbe ProtocolVersion = 8
// ProtocolVersionUnrecorded is set for swaps were created before we
// started saving protocol version with swaps.
ProtocolVersionUnrecorded ProtocolVersion = math.MaxUint32
// CurrentRPCProtocolVersion defines the version of the RPC protocol
// that is currently supported by the loop client.
CurrentRPCProtocolVersion = looprpc.ProtocolVersion_LOOP_OUT_CANCEL
CurrentRPCProtocolVersion = looprpc.ProtocolVersion_PROBE
// CurrentInternalProtocolVersion defines the RPC current protocol in
// the internal representation.
@ -88,6 +92,9 @@ func (p ProtocolVersion) String() string {
case ProtocolVersionLoopOutCancel:
return "Loop Out Cancel"
case ProtocolVersionProbe:
return "Probe"
default:
return "Unknown"
}

+ 2
- 0
loopdb/protocol_version_test.go View File

@ -22,6 +22,7 @@ func TestProtocolVersionSanity(t *testing.T) {
ProtocolVersionHtlcV2,
ProtocolVersionMultiLoopIn,
ProtocolVersionLoopOutCancel,
ProtocolVersionProbe,
}
rpcVersions := [...]looprpc.ProtocolVersion{
@ -33,6 +34,7 @@ func TestProtocolVersionSanity(t *testing.T) {
looprpc.ProtocolVersion_HTLC_V2,
looprpc.ProtocolVersion_MULTI_LOOP_IN,
looprpc.ProtocolVersion_LOOP_OUT_CANCEL,
looprpc.ProtocolVersion_PROBE,
}
require.Equal(t, len(versions), len(rpcVersions))

+ 9
- 2
loopin.go View File

@ -82,8 +82,15 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
// Request current server loop in terms and use these to calculate the
// swap fee that we should subtract from the swap amount in the payment
// request that we send to the server.
quote, err := cfg.server.GetLoopInQuote(globalCtx, request.Amount)
// request that we send to the server. We pass nil as optional route
// hints as hop hint selection when generating invoices with private
// channels is an LND side black box feaure. Advanced users will quote
// directly anyway and there they have the option to add specific
// route hints.
quote, err := cfg.server.GetLoopInQuote(
globalCtx, request.Amount, cfg.lnd.NodePubkey, request.LastHop,
nil,
)
if err != nil {
return nil, wrapGrpcError("loop in terms", err)
}

+ 667
- 486
looprpc/client.pb.go
File diff suppressed because it is too large
View File


+ 117
- 0
looprpc/client.pb.gw.go View File

@ -345,6 +345,76 @@ func local_request_SwapClient_GetLoopInQuote_0(ctx context.Context, marshaler ru
}
var (
filter_SwapClient_Probe_0 = &utilities.DoubleArray{Encoding: map[string]int{"amt": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_SwapClient_Probe_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ProbeRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["amt"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "amt")
}
protoReq.Amt, err = runtime.Int64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "amt", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SwapClient_Probe_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Probe(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_SwapClient_Probe_0(ctx context.Context, marshaler runtime.Marshaler, server SwapClientServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ProbeRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["amt"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "amt")
}
protoReq.Amt, err = runtime.Int64(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "amt", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SwapClient_Probe_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Probe(ctx, &protoReq)
return msg, metadata, err
}
func request_SwapClient_GetLsatTokens_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TokensRequest
var metadata runtime.ServerMetadata
@ -623,6 +693,29 @@ func RegisterSwapClientHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("GET", pattern_SwapClient_Probe_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/looprpc.SwapClient/Probe", runtime.WithHTTPPathPattern("/v1/loop/in/probe/{amt}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_SwapClient_Probe_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_SwapClient_Probe_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_SwapClient_GetLsatTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -916,6 +1009,26 @@ func RegisterSwapClientHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("GET", pattern_SwapClient_Probe_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/looprpc.SwapClient/Probe", runtime.WithHTTPPathPattern("/v1/loop/in/probe/{amt}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_SwapClient_Probe_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_SwapClient_Probe_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_SwapClient_GetLsatTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1016,6 +1129,8 @@ var (
pattern_SwapClient_GetLoopInQuote_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "loop", "in", "quote", "amt"}, ""))
pattern_SwapClient_Probe_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "loop", "in", "probe", "amt"}, ""))
pattern_SwapClient_GetLsatTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "lsat", "tokens"}, ""))
pattern_SwapClient_GetLiquidityParams_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "liquidity", "params"}, ""))
@ -1042,6 +1157,8 @@ var (
forward_SwapClient_GetLoopInQuote_0 = runtime.ForwardResponseMessage
forward_SwapClient_Probe_0 = runtime.ForwardResponseMessage
forward_SwapClient_GetLsatTokens_0 = runtime.ForwardResponseMessage
forward_SwapClient_GetLiquidityParams_0 = runtime.ForwardResponseMessage

+ 40
- 0
looprpc/client.proto View File

@ -1,5 +1,7 @@
syntax = "proto3";
import "common.proto";
package looprpc;
option go_package = "github.com/lightninglabs/loop/looprpc";
@ -62,6 +64,12 @@ service SwapClient {
*/
rpc GetLoopInQuote (QuoteRequest) returns (InQuoteResponse);
/*
Probe asks he sever to probe the route to us to have a better upfront
estimate about routing fees when loopin-in.
*/
rpc Probe (ProbeRequest) returns (ProbeResponse);
/* loop: `listauth`
GetLsatTokens returns all LSAT tokens the daemon ever paid for.
*/
@ -560,6 +568,18 @@ message QuoteRequest {
lower total swap fee. This only has an effect on loop out quotes.
*/
uint64 swap_publication_deadline = 4;
/*
Optionally the client can specify the last hop pubkey when requesting a
loop-in quote. This is useful to get better off-chain routing fee from the
server.
*/
bytes loop_in_last_hop = 5;
/*
Optional route hints to reach the destination through private channels.
*/
repeated RouteHint loop_in_route_hints = 6;
}
message InQuoteResponse {
@ -625,6 +645,26 @@ message OutQuoteResponse {
int32 conf_target = 6;
}
message ProbeRequest {
/*
The amount to probe.
*/
int64 amt = 1;
/*
Optional last hop of the route to probe.
*/
bytes last_hop = 2;
/*
Optional route hints to reach the destination through private channels.
*/
repeated RouteHint route_hints = 3;
}
message ProbeResponse {
}
message TokensRequest {
}

+ 101
- 0
looprpc/client.swagger.json View File

@ -126,6 +126,47 @@
]
}
},
"/v1/loop/in/probe/{amt}": {
"get": {
"summary": "Probe asks he sever to probe the route to us to have a better upfront\nestimate about routing fees when loopin-in.",
"operationId": "SwapClient_Probe",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcProbeResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "amt",
"description": "The amount to probe.",
"in": "path",
"required": true,
"type": "string",
"format": "int64"
},
{
"name": "last_hop",
"description": "Optional last hop of the route to probe.",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
}
],
"tags": [
"SwapClient"
]
}
},
"/v1/loop/in/quote/{amt}": {
"get": {
"summary": "loop: `quote`\nGetQuote returns a quote for a swap with the provided parameters.",
@ -175,6 +216,14 @@
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "loop_in_last_hop",
"description": "Optionally the client can specify the last hop pubkey when requesting a\nloop-in quote. This is useful to get better off-chain routing fee from the\nserver.",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
}
],
"tags": [
@ -287,6 +336,14 @@
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "loop_in_last_hop",
"description": "Optionally the client can specify the last hop pubkey when requesting a\nloop-in quote. This is useful to get better off-chain routing fee from the\nserver.",
"in": "query",
"required": false,
"type": "string",
"format": "byte"
}
],
"tags": [
@ -452,6 +509,35 @@
"default": "FAILURE_REASON_NONE",
"description": " - FAILURE_REASON_NONE: FAILURE_REASON_NONE is set when the swap did not fail, it is either in\nprogress or succeeded.\n - FAILURE_REASON_OFFCHAIN: FAILURE_REASON_OFFCHAIN indicates that a loop out failed because it wasn't\npossible to find a route for one or both off chain payments that met the fee\nand timelock limits required.\n - FAILURE_REASON_TIMEOUT: FAILURE_REASON_TIMEOUT indicates that the swap failed because on chain htlc\ndid not confirm before its expiry, or it confirmed too late for us to reveal\nour preimage and claim.\n - FAILURE_REASON_SWEEP_TIMEOUT: FAILURE_REASON_SWEEP_TIMEOUT indicates that a loop out permanently failed\nbecause the on chain htlc wasn't swept before the server revoked the\nhtlc.\n - FAILURE_REASON_INSUFFICIENT_VALUE: FAILURE_REASON_INSUFFICIENT_VALUE indicates that a loop out has failed\nbecause the on chain htlc had a lower value than requested.\n - FAILURE_REASON_TEMPORARY: FAILURE_REASON_TEMPORARY indicates that a swap cannot continue due to an\ninternal error. Manual intervention such as a restart is required.\n - FAILURE_REASON_INCORRECT_AMOUNT: FAILURE_REASON_INCORRECT_AMOUNT indicates that a loop in permanently failed\nbecause the amount extended by an external loop in htlc is insufficient."
},
"looprpcHopHint": {
"type": "object",
"properties": {
"node_id": {
"type": "string",
"description": "The public key of the node at the start of the channel."
},
"chan_id": {
"type": "string",
"format": "uint64",
"description": "The unique identifier of the channel."
},
"fee_base_msat": {
"type": "integer",
"format": "int64",
"description": "The base fee of the channel denominated in millisatoshis."
},
"fee_proportional_millionths": {
"type": "integer",
"format": "int64",
"description": "The fee rate of the channel for sending one satoshi across it denominated in\nmillionths of a satoshi."
},
"cltv_expiry_delta": {
"type": "integer",
"format": "int64",
"description": "The time-lock delta of the channel."
}
}
},
"looprpcInQuoteResponse": {
"type": "object",
"properties": {
@ -847,6 +933,21 @@
}
}
},
"looprpcProbeResponse": {
"type": "object"
},
"looprpcRouteHint": {
"type": "object",
"properties": {
"hop_hints": {
"type": "array",
"items": {
"$ref": "#/definitions/looprpcHopHint"
},
"description": "A list of hop hints that when chained together can assist in reaching a\nspecific destination."
}
}
},
"looprpcSetLiquidityParamsRequest": {
"type": "object",
"properties": {

+ 2
- 0
looprpc/client.yaml View File

@ -22,6 +22,8 @@ http:
get: "/v1/loop/in/terms"
- selector: looprpc.SwapClient.GetLoopInQuote
get: "/v1/loop/in/quote/{amt}"
- selector: looprpc.SwapClient.Probe
get: "/v1/loop/in/probe/{amt}"
- selector: looprpc.SwapClient.GetLsatTokens
get: "/v1/lsat/tokens"
- selector: looprpc.SwapClient.GetLiquidityParams

+ 42
- 0
looprpc/client_grpc.pb.go View File

@ -53,6 +53,10 @@ type SwapClientClient interface {
// loop: `quote`
//GetQuote returns a quote for a swap with the provided parameters.
GetLoopInQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*InQuoteResponse, error)
//
//Probe asks he sever to probe the route to us to have a better upfront
//estimate about routing fees when loopin-in.
Probe(ctx context.Context, in *ProbeRequest, opts ...grpc.CallOption) (*ProbeResponse, error)
// loop: `listauth`
//GetLsatTokens returns all LSAT tokens the daemon ever paid for.
GetLsatTokens(ctx context.Context, in *TokensRequest, opts ...grpc.CallOption) (*TokensResponse, error)
@ -187,6 +191,15 @@ func (c *swapClientClient) GetLoopInQuote(ctx context.Context, in *QuoteRequest,
return out, nil
}
func (c *swapClientClient) Probe(ctx context.Context, in *ProbeRequest, opts ...grpc.CallOption) (*ProbeResponse, error) {
out := new(ProbeResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/Probe", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) GetLsatTokens(ctx context.Context, in *TokensRequest, opts ...grpc.CallOption) (*TokensResponse, error) {
out := new(TokensResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetLsatTokens", in, out, opts...)
@ -262,6 +275,10 @@ type SwapClientServer interface {
// loop: `quote`
//GetQuote returns a quote for a swap with the provided parameters.
GetLoopInQuote(context.Context, *QuoteRequest) (*InQuoteResponse, error)
//
//Probe asks he sever to probe the route to us to have a better upfront
//estimate about routing fees when loopin-in.
Probe(context.Context, *ProbeRequest) (*ProbeResponse, error)
// loop: `listauth`
//GetLsatTokens returns all LSAT tokens the daemon ever paid for.
GetLsatTokens(context.Context, *TokensRequest) (*TokensResponse, error)
@ -316,6 +333,9 @@ func (UnimplementedSwapClientServer) GetLoopInTerms(context.Context, *TermsReque
func (UnimplementedSwapClientServer) GetLoopInQuote(context.Context, *QuoteRequest) (*InQuoteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetLoopInQuote not implemented")
}
func (UnimplementedSwapClientServer) Probe(context.Context, *ProbeRequest) (*ProbeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Probe not implemented")
}
func (UnimplementedSwapClientServer) GetLsatTokens(context.Context, *TokensRequest) (*TokensResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetLsatTokens not implemented")
}
@ -506,6 +526,24 @@ func _SwapClient_GetLoopInQuote_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _SwapClient_Probe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ProbeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).Probe(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/Probe",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).Probe(ctx, req.(*ProbeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_GetLsatTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TokensRequest)
if err := dec(in); err != nil {
@ -617,6 +655,10 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetLoopInQuote",
Handler: _SwapClient_GetLoopInQuote_Handler,
},
{
MethodName: "Probe",
Handler: _SwapClient_Probe_Handler,
},
{
MethodName: "GetLsatTokens",
Handler: _SwapClient_GetLsatTokens_Handler,

+ 261
- 0
looprpc/common.pb.go View File

@ -0,0 +1,261 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.6.1
// source: common.proto
package looprpc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HopHint struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The public key of the node at the start of the channel.
NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"`
// The unique identifier of the channel.
ChanId uint64 `protobuf:"varint,2,opt,name=chan_id,json=chanId,proto3" json:"chan_id,omitempty"`
// The base fee of the channel denominated in millisatoshis.
FeeBaseMsat uint32 `protobuf:"varint,3,opt,name=fee_base_msat,json=feeBaseMsat,proto3" json:"fee_base_msat,omitempty"`
//
//The fee rate of the channel for sending one satoshi across it denominated in
//millionths of a satoshi.
FeeProportionalMillionths uint32 `protobuf:"varint,4,opt,name=fee_proportional_millionths,json=feeProportionalMillionths,proto3" json:"fee_proportional_millionths,omitempty"`
// The time-lock delta of the channel.
CltvExpiryDelta uint32 `protobuf:"varint,5,opt,name=cltv_expiry_delta,json=cltvExpiryDelta,proto3" json:"cltv_expiry_delta,omitempty"`
}
func (x *HopHint) Reset() {
*x = HopHint{}
if protoimpl.UnsafeEnabled {
mi := &file_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HopHint) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HopHint) ProtoMessage() {}
func (x *HopHint) ProtoReflect() protoreflect.Message {
mi := &file_common_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HopHint.ProtoReflect.Descriptor instead.
func (*HopHint) Descriptor() ([]byte, []int) {
return file_common_proto_rawDescGZIP(), []int{0}
}
func (x *HopHint) GetNodeId() string {
if x != nil {
return x.NodeId
}
return ""
}
func (x *HopHint) GetChanId() uint64 {
if x != nil {
return x.ChanId
}
return 0
}
func (x *HopHint) GetFeeBaseMsat() uint32 {
if x != nil {
return x.FeeBaseMsat
}
return 0
}
func (x *HopHint) GetFeeProportionalMillionths() uint32 {
if x != nil {
return x.FeeProportionalMillionths
}
return 0
}
func (x *HopHint) GetCltvExpiryDelta() uint32 {
if x != nil {
return x.CltvExpiryDelta
}
return 0
}
type RouteHint struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
//
//A list of hop hints that when chained together can assist in reaching a
//specific destination.
HopHints []*HopHint `protobuf:"bytes,1,rep,name=hop_hints,json=hopHints,proto3" json:"hop_hints,omitempty"`
}
func (x *RouteHint) Reset() {
*x = RouteHint{}
if protoimpl.UnsafeEnabled {
mi := &file_common_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RouteHint) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RouteHint) ProtoMessage() {}
func (x *RouteHint) ProtoReflect() protoreflect.Message {
mi := &file_common_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RouteHint.ProtoReflect.Descriptor instead.
func (*RouteHint) Descriptor() ([]byte, []int) {
return file_common_proto_rawDescGZIP(), []int{1}
}
func (x *RouteHint) GetHopHints() []*HopHint {
if x != nil {
return x.HopHints
}
return nil
}
var File_common_proto protoreflect.FileDescriptor
var file_common_proto_rawDesc = []byte{
0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07,
0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x22, 0xcb, 0x01, 0x0a, 0x07, 0x48, 0x6f, 0x70, 0x48,
0x69, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07,
0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x63,
0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x66, 0x65, 0x65, 0x5f, 0x62, 0x61, 0x73,
0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x66, 0x65,
0x65, 0x42, 0x61, 0x73, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x3e, 0x0a, 0x1b, 0x66, 0x65, 0x65,
0x5f, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6d, 0x69,
0x6c, 0x6c, 0x69, 0x6f, 0x6e, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x19,
0x66, 0x65, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x4d,
0x69, 0x6c, 0x6c, 0x69, 0x6f, 0x6e, 0x74, 0x68, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x63, 0x6c, 0x74,
0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x63, 0x6c, 0x74, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79,
0x44, 0x65, 0x6c, 0x74, 0x61, 0x22, 0x3a, 0x0a, 0x09, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x48, 0x69,
0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x09, 0x68, 0x6f, 0x70, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e,
0x48, 0x6f, 0x70, 0x48, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x68, 0x6f, 0x70, 0x48, 0x69, 0x6e, 0x74,
0x73, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f,
0x6f, 0x70, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (
file_common_proto_rawDescOnce sync.Once
file_common_proto_rawDescData = file_common_proto_rawDesc
)
func file_common_proto_rawDescGZIP() []byte {
file_common_proto_rawDescOnce.Do(func() {
file_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_proto_rawDescData)
})
return file_common_proto_rawDescData
}
var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_common_proto_goTypes = []interface{}{
(*HopHint)(nil), // 0: looprpc.HopHint
(*RouteHint)(nil), // 1: looprpc.RouteHint
}
var file_common_proto_depIdxs = []int32{
0, // 0: looprpc.RouteHint.hop_hints:type_name -> looprpc.HopHint
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_common_proto_init() }
func file_common_proto_init() {
if File_common_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_common_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HopHint); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_common_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RouteHint); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_common_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_common_proto_goTypes,
DependencyIndexes: file_common_proto_depIdxs,
MessageInfos: file_common_proto_msgTypes,
}.Build()
File_common_proto = out.File
file_common_proto_rawDesc = nil
file_common_proto_goTypes = nil
file_common_proto_depIdxs = nil
}

+ 33
- 0
looprpc/common.proto View File

@ -0,0 +1,33 @@
syntax = "proto3";
package looprpc;
option go_package = "github.com/lightninglabs/loop/looprpc";
message HopHint {
// The public key of the node at the start of the channel.
string node_id = 1;
// The unique identifier of the channel.
uint64 chan_id = 2;
// The base fee of the channel denominated in millisatoshis.
uint32 fee_base_msat = 3;
/*
The fee rate of the channel for sending one satoshi across it denominated in
millionths of a satoshi.
*/
uint32 fee_proportional_millionths = 4;
// The time-lock delta of the channel.
uint32 cltv_expiry_delta = 5;
}
message RouteHint {
/*
A list of hop hints that when chained together can assist in reaching a
specific destination.
*/
repeated HopHint hop_hints = 1;
}

+ 562
- 346
looprpc/server.pb.go
File diff suppressed because it is too large
View File


+ 40
- 0
looprpc/server.proto View File

@ -1,5 +1,7 @@
syntax = "proto3";
import "common.proto";
package looprpc;
option go_package = "github.com/lightninglabs/loop/looprpc";
@ -29,6 +31,8 @@ service SwapServer {
rpc CancelLoopOutSwap (CancelLoopOutSwapRequest)
returns (CancelLoopOutSwapResponse);
rpc Probe (ServerProbeRequest) returns (ServerProbeResponse);
}
/**
@ -70,6 +74,10 @@ enum ProtocolVersion {
// The client supports loop out swap cancelation.
LOOP_OUT_CANCEL = 7;
// The client is able to ask the server to probe to test inbound
// liquidity.
PROBE = 8;
}
message ServerLoopOutRequest {
@ -201,6 +209,16 @@ message ServerLoopInQuoteRequest {
/// The swap amount. If zero, a quote for a maximum amt swap will be given.
uint64 amt = 1;
// The destination pubkey. Will be used to retrieve cached probed routing
// fee.
bytes pubkey = 3;
// The last hop to use. Will be used to retrieve cached probed routing fee.
bytes last_hop = 4;
// Optional route hints to reach the destination through private channels.
repeated RouteHint route_hints = 5;
/// The protocol version that the client adheres to.
ProtocolVersion protocol_version = 2;
}
@ -414,3 +432,25 @@ message CancelLoopOutSwapRequest {
message CancelLoopOutSwapResponse {
}
message ServerProbeRequest {
/// The protocol version that the client adheres to.
ProtocolVersion protocol_version = 1;
// The probe amount.
uint64 amt = 2;
// The target node for the probe.
bytes target = 3;
// Optional last hop to use when probing the client.
bytes last_hop = 4;
/*
Optional route hints to reach the destination through private channels.
*/
repeated RouteHint route_hints = 5;
}
message ServerProbeResponse {
}

+ 36
- 0
looprpc/server_grpc.pb.go View File

@ -28,6 +28,7 @@ type SwapServerClient interface {
SubscribeLoopOutUpdates(ctx context.Context, in *SubscribeUpdatesRequest, opts ...grpc.CallOption) (SwapServer_SubscribeLoopOutUpdatesClient, error)
SubscribeLoopInUpdates(ctx context.Context, in *SubscribeUpdatesRequest, opts ...grpc.CallOption) (SwapServer_SubscribeLoopInUpdatesClient, error)
CancelLoopOutSwap(ctx context.Context, in *CancelLoopOutSwapRequest, opts ...grpc.CallOption) (*CancelLoopOutSwapResponse, error)
Probe(ctx context.Context, in *ServerProbeRequest, opts ...grpc.CallOption) (*ServerProbeResponse, error)
}
type swapServerClient struct {
@ -174,6 +175,15 @@ func (c *swapServerClient) CancelLoopOutSwap(ctx context.Context, in *CancelLoop
return out, nil
}
func (c *swapServerClient) Probe(ctx context.Context, in *ServerProbeRequest, opts ...grpc.CallOption) (*ServerProbeResponse, error) {
out := new(ServerProbeResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapServer/Probe", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapServerServer is the server API for SwapServer service.
// All implementations must embed UnimplementedSwapServerServer
// for forward compatibility
@ -188,6 +198,7 @@ type SwapServerServer interface {
SubscribeLoopOutUpdates(*SubscribeUpdatesRequest, SwapServer_SubscribeLoopOutUpdatesServer) error
SubscribeLoopInUpdates(*SubscribeUpdatesRequest, SwapServer_SubscribeLoopInUpdatesServer) error
CancelLoopOutSwap(context.Context, *CancelLoopOutSwapRequest) (*CancelLoopOutSwapResponse, error)
Probe(context.Context, *ServerProbeRequest) (*ServerProbeResponse, error)
mustEmbedUnimplementedSwapServerServer()
}
@ -225,6 +236,9 @@ func (UnimplementedSwapServerServer) SubscribeLoopInUpdates(*SubscribeUpdatesReq
func (UnimplementedSwapServerServer) CancelLoopOutSwap(context.Context, *CancelLoopOutSwapRequest) (*CancelLoopOutSwapResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CancelLoopOutSwap not implemented")
}
func (UnimplementedSwapServerServer) Probe(context.Context, *ServerProbeRequest) (*ServerProbeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Probe not implemented")
}
func (UnimplementedSwapServerServer) mustEmbedUnimplementedSwapServerServer() {}
// UnsafeSwapServerServer may be embedded to opt out of forward compatibility for this service.
@ -424,6 +438,24 @@ func _SwapServer_CancelLoopOutSwap_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _SwapServer_Probe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerProbeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapServerServer).Probe(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapServer/Probe",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapServerServer).Probe(ctx, req.(*ServerProbeRequest))
}
return interceptor(ctx, in, info, handler)
}
// SwapServer_ServiceDesc is the grpc.ServiceDesc for SwapServer service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -463,6 +495,10 @@ var SwapServer_ServiceDesc = grpc.ServiceDesc{
MethodName: "CancelLoopOutSwap",
Handler: _SwapServer_CancelLoopOutSwap_Handler,
},
{
MethodName: "Probe",
Handler: _SwapServer_Probe_Handler,
},
},
Streams: []grpc.StreamDesc{
{

+ 3
- 0
release_notes.md View File

@ -15,6 +15,9 @@ This file tracks release notes for the loop client.
## Next release
#### New Features
* Loop-in quote now asks the server to optionally probe the client to test
inbound liquidity. The server may use this information to give more accurate
quotes.
#### Breaking Changes

+ 9
- 2
server_mock_test.go View File

@ -212,8 +212,8 @@ func (s *serverMock) GetLoopInTerms(ctx context.Context) (
}, nil
}
func (s *serverMock) GetLoopInQuote(ctx context.Context, amt btcutil.Amount) (
*LoopInQuote, error) {
func (s *serverMock) GetLoopInQuote(context.Context, btcutil.Amount,
route.Vertex, *route.Vertex, [][]zpay32.HopHint) (*LoopInQuote, error) {
return &LoopInQuote{
SwapFee: testSwapFee,
@ -235,3 +235,10 @@ func (s *serverMock) SubscribeLoopInUpdates(_ context.Context,
return nil, nil, nil
}
func (s *serverMock) Probe(ctx context.Context, amt btcutil.Amount,
pubKey route.Vertex, lastHop *route.Vertex,
routeHints [][]zpay32.HopHint) error {
return nil
}

+ 98
- 9
swap_server_client.go View File

@ -21,8 +21,11 @@ import (
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tor"
"github.com/lightningnetwork/lnd/zpay32"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
)
var (
@ -51,8 +54,12 @@ type swapServerClient interface {
GetLoopInTerms(ctx context.Context) (
*LoopInTerms, error)
GetLoopInQuote(ctx context.Context, amt btcutil.Amount) (
*LoopInQuote, error)
GetLoopInQuote(ctx context.Context, amt btcutil.Amount,
pubKey route.Vertex, lastHop *route.Vertex,
routeHints [][]zpay32.HopHint) (*LoopInQuote, error)
Probe(ctx context.Context, amt btcutil.Amount, target route.Vertex,
lastHop *route.Vertex, routeHints [][]zpay32.HopHint) error
NewLoopOutSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, expiry int32,
@ -203,16 +210,28 @@ func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context) (
}
func (s *grpcSwapServerClient) GetLoopInQuote(ctx context.Context,
amt btcutil.Amount) (*LoopInQuote, error) {
amt btcutil.Amount, pubKey route.Vertex, lastHop *route.Vertex,
routeHints [][]zpay32.HopHint) (*LoopInQuote, error) {
err := s.Probe(ctx, amt, pubKey, lastHop, routeHints)
if err != nil && status.Code(err) != codes.Unavailable {
log.Warnf("Server probe error: %v", err)
}
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
defer rpcCancel()
quoteResp, err := s.server.LoopInQuote(rpcCtx,
&looprpc.ServerLoopInQuoteRequest{
Amt: uint64(amt),
ProtocolVersion: loopdb.CurrentRPCProtocolVersion,
},
)
req := &looprpc.ServerLoopInQuoteRequest{
Amt: uint64(amt),
ProtocolVersion: loopdb.CurrentRPCProtocolVersion,
Pubkey: pubKey[:],
}
if lastHop != nil {
req.LastHop = lastHop[:]
}
quoteResp, err := s.server.LoopInQuote(rpcCtx, req)
if err != nil {
return nil, err
}
@ -223,6 +242,76 @@ func (s *grpcSwapServerClient) GetLoopInQuote(ctx context.Context,
}, nil
}
// marshallRouteHints marshalls a list of route hints.
func marshallRouteHints(routeHints [][]zpay32.HopHint) (
[]*looprpc.RouteHint, error) {
rpcRouteHints := make([]*looprpc.RouteHint, 0, len(routeHints))
for _, routeHint := range routeHints {
rpcRouteHint := make(
[]*looprpc.HopHint, 0, len(routeHint),
)
for _, hint := range routeHint {
rpcHint, err := marshallHopHint(hint)
if err != nil {
return nil, err
}
rpcRouteHint = append(rpcRouteHint, rpcHint)
}
rpcRouteHints = append(rpcRouteHints, &looprpc.RouteHint{
HopHints: rpcRouteHint,
})
}
return rpcRouteHints, nil
}
// marshallHopHint marshalls a single hop hint.
func marshallHopHint(hint zpay32.HopHint) (*looprpc.HopHint, error) {
nodeID, err := route.NewVertexFromBytes(
hint.NodeID.SerializeCompressed(),
)
if err != nil {
return nil, err
}
return &looprpc.HopHint{
ChanId: hint.ChannelID,
CltvExpiryDelta: uint32(hint.CLTVExpiryDelta),
FeeBaseMsat: hint.FeeBaseMSat,
FeeProportionalMillionths: hint.FeeProportionalMillionths,
NodeId: nodeID.String(),
}, nil
}
func (s *grpcSwapServerClient) Probe(ctx context.Context, amt btcutil.Amount,
target route.Vertex,