multi: loop in swap

pull/34/head
Joost Jager 5 years ago
parent 6a0a9556a0
commit 3e960b8b54
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7

@ -118,6 +118,11 @@ func (s *Client) FetchLoopOutSwaps() ([]*loopdb.LoopOut, error) {
return s.Store.FetchLoopOutSwaps() return s.Store.FetchLoopOutSwaps()
} }
// FetchLoopInSwaps returns a list of all swaps currently in the database.
func (s *Client) FetchLoopInSwaps() ([]*loopdb.LoopIn, error) {
return s.Store.FetchLoopInSwaps()
}
// Run is a blocking call that executes all swaps. Any pending swaps are // Run is a blocking call that executes all swaps. Any pending swaps are
// restored from persistent storage and resumed. Subsequent updates will be // restored from persistent storage and resumed. Subsequent updates will be
// sent through the passed in statusChan. The function can be terminated by // sent through the passed in statusChan. The function can be terminated by
@ -144,7 +149,12 @@ func (s *Client) Run(ctx context.Context,
// Query store before starting event loop to prevent new swaps from // Query store before starting event loop to prevent new swaps from
// being treated as swaps that need to be resumed. // being treated as swaps that need to be resumed.
pendingSwaps, err := s.Store.FetchLoopOutSwaps() pendingLoopOutSwaps, err := s.Store.FetchLoopOutSwaps()
if err != nil {
return err
}
pendingLoopInSwaps, err := s.Store.FetchLoopInSwaps()
if err != nil { if err != nil {
return err return err
} }
@ -154,7 +164,7 @@ func (s *Client) Run(ctx context.Context,
go func() { go func() {
defer s.wg.Done() defer s.wg.Done()
s.resumeSwaps(mainCtx, pendingSwaps) s.resumeSwaps(mainCtx, pendingLoopOutSwaps, pendingLoopInSwaps)
// Signal that new requests can be accepted. Otherwise the new // Signal that new requests can be accepted. Otherwise the new
// swap could already have been added to the store and read in // swap could already have been added to the store and read in
@ -194,19 +204,33 @@ func (s *Client) Run(ctx context.Context,
// resumeSwaps restarts all pending swaps from the provided list. // resumeSwaps restarts all pending swaps from the provided list.
func (s *Client) resumeSwaps(ctx context.Context, func (s *Client) resumeSwaps(ctx context.Context,
swaps []*loopdb.LoopOut) { loopOutSwaps []*loopdb.LoopOut, loopInSwaps []*loopdb.LoopIn) {
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}
for _, pend := range swaps { for _, pend := range loopOutSwaps {
if pend.State().Type() != loopdb.StateTypePending { if pend.State().Type() != loopdb.StateTypePending {
continue continue
} }
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}
swap, err := resumeLoopOutSwap(ctx, swapCfg, pend) swap, err := resumeLoopOutSwap(ctx, swapCfg, pend)
if err != nil { if err != nil {
logger.Errorf("resuming swap: %v", err) logger.Errorf("resuming loop out swap: %v", err)
continue
}
s.executor.initiateSwap(ctx, swap)
}
for _, pend := range loopInSwaps {
if pend.State().Type() != loopdb.StateTypePending {
continue
}
swap, err := resumeLoopInSwap(ctx, swapCfg, pend)
if err != nil {
logger.Errorf("resuming loop in swap: %v", err)
continue continue
} }
@ -320,3 +344,85 @@ func (s *Client) waitForInitialized(ctx context.Context) error {
return nil return nil
} }
// LoopIn initiates a loop in swap.
func (s *Client) LoopIn(globalCtx context.Context,
request *LoopInRequest) (*lntypes.Hash, error) {
logger.Infof("Loop in %v (channel: %v)",
request.Amount,
request.LoopInChannel,
)
if err := s.waitForInitialized(globalCtx); err != nil {
return nil, err
}
// Create a new swap object for this swap.
initiationHeight := s.executor.height()
swapCfg := swapConfig{
lnd: s.lndServices,
store: s.Store,
server: s.Server,
}
swap, err := newLoopInSwap(
globalCtx, &swapCfg, initiationHeight, request,
)
if err != nil {
return nil, err
}
// Post swap to the main loop.
s.executor.initiateSwap(globalCtx, swap)
// Return hash so that the caller can identify this swap in the updates
// stream.
return &swap.hash, nil
}
// LoopInQuote takes an amount and returns a break down of estimated
// costs for the client. Both the swap server and the on-chain fee estimator are
// queried to get to build the quote response.
func (s *Client) LoopInQuote(ctx context.Context,
request *LoopInQuoteRequest) (*LoopInQuote, error) {
// Retrieve current server terms to calculate swap fee.
terms, err := s.Server.GetLoopInTerms(ctx)
if err != nil {
return nil, err
}
// Check amount limits.
if request.Amount < terms.MinSwapAmount {
return nil, ErrSwapAmountTooLow
}
if request.Amount > terms.MaxSwapAmount {
return nil, ErrSwapAmountTooHigh
}
// Calculate swap fee.
swapFee := terms.SwapFeeBase +
request.Amount*btcutil.Amount(terms.SwapFeeRate)/
btcutil.Amount(swap.FeeRateTotalParts)
// Get estimate for miner fee.
minerFee, err := s.lndServices.Client.EstimateFeeToP2WSH(
ctx, request.Amount, request.HtlcConfTarget,
)
if err != nil {
return nil, err
}
return &LoopInQuote{
SwapFee: swapFee,
MinerFee: minerFee,
}, nil
}
// LoopInTerms returns the terms on which the server executes swaps.
func (s *Client) LoopInTerms(ctx context.Context) (
*LoopInTerms, error) {
return s.Server.GetLoopInTerms(ctx)
}

@ -0,0 +1,102 @@
package main
import (
"context"
"fmt"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)
var loopInCommand = cli.Command{
Name: "in",
Usage: "perform an on-chain to off-chain swap (loop in)",
ArgsUsage: "amt",
Description: `
Send the amount in satoshis specified by the amt argument off-chain.`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "channel",
Usage: "the 8-byte compact channel ID of the channel to loop in",
},
cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis to loop out",
},
},
Action: loopIn,
}
func loopIn(ctx *cli.Context) error {
args := ctx.Args()
var amtStr string
switch {
case ctx.IsSet("amt"):
amtStr = ctx.String("amt")
case ctx.NArg() > 0:
amtStr = args[0]
args = args.Tail()
default:
// Show command help if no arguments and flags were provided.
cli.ShowCommandHelp(ctx, "in")
return nil
}
amt, err := parseAmt(amtStr)
if err != nil {
return err
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
quote, err := client.GetLoopInQuote(
context.Background(),
&looprpc.QuoteRequest{
Amt: int64(amt),
},
)
if err != nil {
return err
}
limits := getInLimits(amt, quote)
if err := displayLimits(amt, limits); err != nil {
return err
}
var loopInChannel uint64
if ctx.IsSet("channel") {
loopInChannel = ctx.Uint64("channel")
}
resp, err := client.LoopIn(context.Background(), &looprpc.LoopInRequest{
Amt: int64(amt),
MaxMinerFee: int64(limits.maxMinerFee),
MaxSwapFee: int64(limits.maxSwapFee),
LoopInChannel: loopInChannel,
})
if err != nil {
return err
}
fmt.Printf("Swap initiated with id: %v\n", resp.Id[:8])
fmt.Printf("Run swapcli without a command to monitor progress.\n")
return nil
}
func getInLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits {
return &limits{
// Apply a multiplier to the estimated miner fee, to not get
// the swap canceled because fees increased in the mean time.
maxMinerFee: btcutil.Amount(quote.MinerFee) * 3,
maxSwapFee: btcutil.Amount(quote.SwapFee),
}
}

@ -97,10 +97,10 @@ func loopOut(ctx *cli.Context) error {
Amt: int64(amt), Amt: int64(amt),
Dest: destAddr, Dest: destAddr,
MaxMinerFee: int64(limits.maxMinerFee), MaxMinerFee: int64(limits.maxMinerFee),
MaxPrepayAmt: int64(limits.maxPrepayAmt), MaxPrepayAmt: int64(*limits.maxPrepayAmt),
MaxSwapFee: int64(limits.maxSwapFee), MaxSwapFee: int64(limits.maxSwapFee),
MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee), MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee), MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee),
LoopOutChannel: unchargeChannel, LoopOutChannel: unchargeChannel,
}) })
if err != nil { if err != nil {

@ -62,7 +62,8 @@ func main() {
}, },
} }
app.Commands = []cli.Command{ app.Commands = []cli.Command{
loopOutCommand, termsCommand, monitorCommand, quoteCommand, loopOutCommand, loopInCommand, termsCommand,
monitorCommand, quoteCommand,
} }
err := app.Run(os.Args) err := app.Run(os.Args)
@ -88,32 +89,41 @@ func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
} }
type limits struct { type limits struct {
maxSwapRoutingFee btcutil.Amount maxSwapRoutingFee *btcutil.Amount
maxPrepayRoutingFee btcutil.Amount maxPrepayRoutingFee *btcutil.Amount
maxMinerFee btcutil.Amount maxMinerFee btcutil.Amount
maxSwapFee btcutil.Amount maxSwapFee btcutil.Amount
maxPrepayAmt btcutil.Amount maxPrepayAmt *btcutil.Amount
} }
func getLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits { func getLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits {
maxSwapRoutingFee := getMaxRoutingFee(btcutil.Amount(amt))
maxPrepayRoutingFee := getMaxRoutingFee(btcutil.Amount(
quote.PrepayAmt,
))
maxPrepayAmt := btcutil.Amount(quote.PrepayAmt)
return &limits{ return &limits{
maxSwapRoutingFee: getMaxRoutingFee(btcutil.Amount(amt)), maxSwapRoutingFee: &maxSwapRoutingFee,
maxPrepayRoutingFee: getMaxRoutingFee(btcutil.Amount( maxPrepayRoutingFee: &maxPrepayRoutingFee,
quote.PrepayAmt,
)),
// Apply a multiplier to the estimated miner fee, to not get // Apply a multiplier to the estimated miner fee, to not get
// the swap canceled because fees increased in the mean time. // the swap canceled because fees increased in the mean time.
maxMinerFee: btcutil.Amount(quote.MinerFee) * 3, maxMinerFee: btcutil.Amount(quote.MinerFee) * 3,
maxSwapFee: btcutil.Amount(quote.SwapFee), maxSwapFee: btcutil.Amount(quote.SwapFee),
maxPrepayAmt: btcutil.Amount(quote.PrepayAmt), maxPrepayAmt: &maxPrepayAmt,
} }
} }
func displayLimits(amt btcutil.Amount, l *limits) error { func displayLimits(amt btcutil.Amount, l *limits) error {
totalSuccessMax := l.maxSwapRoutingFee + l.maxPrepayRoutingFee + totalSuccessMax := l.maxMinerFee + l.maxSwapFee
l.maxMinerFee + l.maxSwapFee if l.maxSwapRoutingFee != nil {
totalSuccessMax += *l.maxSwapRoutingFee
}
if l.maxPrepayRoutingFee != nil {
totalSuccessMax += *l.maxPrepayRoutingFee
}
fmt.Printf("Max swap fees for %d loop out: %d\n", fmt.Printf("Max swap fees for %d loop out: %d\n",
btcutil.Amount(amt), totalSuccessMax, btcutil.Amount(amt), totalSuccessMax,
@ -130,13 +140,22 @@ func displayLimits(amt btcutil.Amount, l *limits) error {
case "x": case "x":
fmt.Println() fmt.Println()
fmt.Printf("Max on-chain fee: %d\n", l.maxMinerFee) fmt.Printf("Max on-chain fee: %d\n", l.maxMinerFee)
fmt.Printf("Max off-chain swap routing fee: %d\n",
l.maxSwapRoutingFee) if l.maxSwapRoutingFee != nil {
fmt.Printf("Max off-chain prepay routing fee: %d\n", fmt.Printf("Max off-chain swap routing fee: %d\n",
l.maxPrepayRoutingFee) *l.maxSwapRoutingFee)
}
if l.maxPrepayRoutingFee != nil {
fmt.Printf("Max off-chain prepay routing fee: %d\n",
*l.maxPrepayRoutingFee)
}
fmt.Printf("Max swap fee: %d\n", l.maxSwapFee) fmt.Printf("Max swap fee: %d\n", l.maxSwapFee)
fmt.Printf("Max no show penalty: %d\n",
l.maxPrepayAmt) if l.maxPrepayAmt != nil {
fmt.Printf("Max no show penalty: %d\n",
*l.maxPrepayAmt)
}
fmt.Printf("CONTINUE SWAP? (y/n): ") fmt.Printf("CONTINUE SWAP? (y/n): ")
fmt.Scanln(&answer) fmt.Scanln(&answer)

@ -4,11 +4,11 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/btcsuite/btcutil" "github.com/urfave/cli"
"github.com/lightninglabs/loop/swap"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli" "github.com/lightninglabs/loop/swap"
) )
var termsCommand = cli.Command{ var termsCommand = cli.Command{
@ -25,14 +25,13 @@ func terms(ctx *cli.Context) error {
defer cleanup() defer cleanup()
req := &looprpc.TermsRequest{} req := &looprpc.TermsRequest{}
terms, err := client.LoopOutTerms(context.Background(), req) loopOutTerms, err := client.LoopOutTerms(context.Background(), req)
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("Amount: %d - %d\n", loopInTerms, err := client.GetLoopInTerms(
btcutil.Amount(terms.MinSwapAmount), context.Background(), &looprpc.TermsRequest{},
btcutil.Amount(terms.MaxSwapAmount),
) )
if err != nil { if err != nil {
return err return err
@ -54,7 +53,13 @@ func terms(ctx *cli.Context) error {
fmt.Println("Loop Out") fmt.Println("Loop Out")
fmt.Println("--------") fmt.Println("--------")
printTerms(terms) printTerms(loopOutTerms)
fmt.Println()
fmt.Println("Loop In")
fmt.Println("------")
printTerms(loopInTerms)
return nil return nil
} }

@ -43,11 +43,11 @@ func daemon(config *config) error {
// Before starting the client, build an in-memory view of all swaps. // Before starting the client, build an in-memory view of all swaps.
// This view is used to update newly connected clients with the most // This view is used to update newly connected clients with the most
// recent swaps. // recent swaps.
storedSwaps, err := swapClient.FetchLoopOutSwaps() loopOutSwaps, err := swapClient.FetchLoopOutSwaps()
if err != nil { if err != nil {
return err return err
} }
for _, swap := range storedSwaps { for _, swap := range loopOutSwaps {
swaps[swap.Hash] = loop.SwapInfo{ swaps[swap.Hash] = loop.SwapInfo{
SwapType: loop.TypeOut, SwapType: loop.TypeOut,
SwapContract: swap.Contract.SwapContract, SwapContract: swap.Contract.SwapContract,
@ -57,6 +57,20 @@ func daemon(config *config) error {
} }
} }
loopInSwaps, err := swapClient.FetchLoopInSwaps()
if err != nil {
return err
}
for _, swap := range loopInSwaps {
swaps[swap.Hash] = loop.SwapInfo{
SwapType: loop.TypeIn,
SwapContract: swap.Contract.SwapContract,
State: swap.State(),
SwapHash: swap.Hash,
LastUpdate: swap.LastUpdateTime(),
}
}
// Instantiate the loopd gRPC server. // Instantiate the loopd gRPC server.
server := swapClientServer{ server := swapClientServer{
impl: swapClient, impl: swapClient,

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sort" "sort"
@ -32,7 +33,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
in *looprpc.LoopOutRequest) ( in *looprpc.LoopOutRequest) (
*looprpc.SwapResponse, error) { *looprpc.SwapResponse, error) {
logger.Infof("LoopOut request received") logger.Infof("Loop out request received")
var sweepAddr btcutil.Address var sweepAddr btcutil.Address
if in.Dest == "" { if in.Dest == "" {
@ -85,6 +86,8 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
state = looprpc.SwapState_INITIATED state = looprpc.SwapState_INITIATED
case loopdb.StatePreimageRevealed: case loopdb.StatePreimageRevealed:
state = looprpc.SwapState_PREIMAGE_REVEALED state = looprpc.SwapState_PREIMAGE_REVEALED
case loopdb.StateHtlcPublished:
state = looprpc.SwapState_HTLC_PUBLISHED
case loopdb.StateSuccess: case loopdb.StateSuccess:
state = looprpc.SwapState_SUCCESS state = looprpc.SwapState_SUCCESS
default: default:
@ -105,6 +108,16 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
return nil, err return nil, err
} }
var swapType looprpc.SwapType
switch loopSwap.SwapType {
case loop.TypeIn:
swapType = looprpc.SwapType_LOOP_IN
case loop.TypeOut:
swapType = looprpc.SwapType_LOOP_OUT
default:
return nil, errors.New("unknown swap type")
}
return &looprpc.SwapStatus{ return &looprpc.SwapStatus{
Amt: int64(loopSwap.AmountRequested), Amt: int64(loopSwap.AmountRequested),
Id: loopSwap.SwapHash.String(), Id: loopSwap.SwapHash.String(),
@ -112,7 +125,7 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
InitiationTime: loopSwap.InitiationTime.UnixNano(), InitiationTime: loopSwap.InitiationTime.UnixNano(),
LastUpdateTime: loopSwap.LastUpdate.UnixNano(), LastUpdateTime: loopSwap.LastUpdate.UnixNano(),
HtlcAddress: address.EncodeAddress(), HtlcAddress: address.EncodeAddress(),
Type: looprpc.SwapType_LOOP_OUT, Type: swapType,
}, nil }, nil
} }
@ -214,7 +227,7 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
func (s *swapClientServer) LoopOutTerms(ctx context.Context, func (s *swapClientServer) LoopOutTerms(ctx context.Context,
req *looprpc.TermsRequest) (*looprpc.TermsResponse, error) { req *looprpc.TermsRequest) (*looprpc.TermsResponse, error) {
logger.Infof("Terms request received") logger.Infof("Loop out terms request received")
terms, err := s.impl.LoopOutTerms(ctx) terms, err := s.impl.LoopOutTerms(ctx)
if err != nil { if err != nil {
@ -250,3 +263,69 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
SwapFee: int64(quote.SwapFee), SwapFee: int64(quote.SwapFee),
}, nil }, nil
} }
// GetTerms returns the terms that the server enforces for swaps.
func (s *swapClientServer) GetLoopInTerms(ctx context.Context, req *looprpc.TermsRequest) (
*looprpc.TermsResponse, error) {
logger.Infof("Loop in terms request received")
terms, err := s.impl.LoopInTerms(ctx)
if err != nil {
logger.Errorf("Terms request: %v", err)
return nil, err
}
return &looprpc.TermsResponse{
MinSwapAmount: int64(terms.MinSwapAmount),
MaxSwapAmount: int64(terms.MaxSwapAmount),
SwapFeeBase: int64(terms.SwapFeeBase),
SwapFeeRate: int64(terms.SwapFeeRate),
CltvDelta: int32(terms.CltvDelta),
}, nil
}
// GetQuote returns a quote for a swap with the provided parameters.
func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
req *looprpc.QuoteRequest) (*looprpc.QuoteResponse, error) {
logger.Infof("Loop in quote request received")
quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
Amount: btcutil.Amount(req.Amt),
HtlcConfTarget: defaultConfTarget,
})
if err != nil {
return nil, err
}
return &looprpc.QuoteResponse{
MinerFee: int64(quote.MinerFee),
SwapFee: int64(quote.SwapFee),
}, nil
}
func (s *swapClientServer) LoopIn(ctx context.Context,
in *looprpc.LoopInRequest) (
*looprpc.SwapResponse, error) {
logger.Infof("Loop in request received")
req := &loop.LoopInRequest{
Amount: btcutil.Amount(in.Amt),
MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
HtlcConfTarget: defaultConfTarget,
}
if in.LoopInChannel != 0 {
req.LoopInChannel = &in.LoopInChannel
}
hash, err := s.impl.LoopIn(ctx, req)
if err != nil {
logger.Errorf("Loop in: %v", err)
return nil, err
}
return &looprpc.SwapResponse{
Id: hash.String(),
}, nil
}

@ -158,6 +158,90 @@ type LoopOutTerms struct {
SwapPaymentDest [33]byte SwapPaymentDest [33]byte
} }
// LoopInRequest contains the required parameters for the swap.
type LoopInRequest struct {
// Amount specifies the requested swap amount in sat. This does not
// include the swap and miner fee.
Amount btcutil.Amount
// MaxSwapFee is the maximum we are willing to pay the server for the
// swap. This value is not disclosed in the swap initiation call, but if
// the server asks for a higher fee, we abort the swap. Typically this
// value is taken from the response of the UnchargeQuote call. It
// includes the prepay amount.
MaxSwapFee btcutil.Amount
// MaxMinerFee is the maximum in on-chain fees that we are willing to
// spent. If we publish the on-chain htlc and the fee estimate turns out
// higher than this value, we cancel the swap.
//
// MaxMinerFee is typically taken from the response of the UnchargeQuote
// call.
MaxMinerFee btcutil.Amount
// HtlcConfTarget specifies the targeted confirmation target for the
// client htlc tx.
HtlcConfTarget int32
// LoopInChannel optionally specifies the short channel id of the
// channel to charge.
LoopInChannel *uint64
}
// LoopInTerms are the server terms on which it executes charge swaps.
type LoopInTerms struct {
// SwapFeeBase is the fixed per-swap base fee.
SwapFeeBase btcutil.Amount
// SwapFeeRate is the variable fee in parts per million.
SwapFeeRate int64
// MinSwapAmount is the minimum amount that the server requires for a
// swap.
MinSwapAmount btcutil.Amount
// MaxSwapAmount is the maximum amount that the server accepts for a
// swap.
MaxSwapAmount btcutil.Amount
// Time lock delta relative to current block height that swap server
// will accept on the swap initiation call.
CltvDelta int32
}
// In contains status information for a loop in swap.
type In struct {
loopdb.LoopInContract
SwapInfoKit
// State where the swap is in.
State loopdb.SwapState
}
// LoopInQuoteRequest specifies the swap parameters for which a quote is
// requested.
type LoopInQuoteRequest struct {
// Amount specifies the requested swap amount in sat. This does not
// include the swap and miner fee.
Amount btcutil.Amount
// HtlcConfTarget specifies the targeted confirmation target for the
// client sweep tx.
HtlcConfTarget int32
}
// LoopInQuote contains estimates for the fees making up the total swap cost
// for the client.
type LoopInQuote struct {
// SwapFee is the fee that the swap server is charging for the swap.
SwapFee btcutil.Amount
// MinerFee is an estimate of the on-chain fee that needs to be paid to
// sweep the htlc.
MinerFee btcutil.Amount
}
// SwapInfoKit contains common swap info fields. // SwapInfoKit contains common swap info fields.
type SwapInfoKit struct { type SwapInfoKit struct {
// Hash is the sha256 hash of the preimage that unlocks the htlcs. It // Hash is the sha256 hash of the preimage that unlocks the htlcs. It
@ -191,3 +275,13 @@ type SwapInfo struct {
loopdb.SwapContract loopdb.SwapContract
} }
// LastUpdate returns the last update time of the swap
func (s *In) LastUpdate() time.Time {
return s.LastUpdateTime
}
// SwapHash returns the swap hash.
func (s *In) SwapHash() lntypes.Hash {
return s.Hash
}

@ -11,8 +11,6 @@ import (
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc" "github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
// ChainNotifierClient exposes base lightning functionality. // ChainNotifierClient exposes base lightning functionality.
@ -101,9 +99,7 @@ func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
for { for {
spendEvent, err := resp.Recv() spendEvent, err := resp.Recv()
if err != nil { if err != nil {
if status.Code(err) != codes.Canceled { errChan <- err
errChan <- err
}
return return
} }
@ -125,7 +121,6 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) ( txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) (
chan *chainntnfs.TxConfirmation, chan error, error) { chan *chainntnfs.TxConfirmation, chan error, error) {
// TODO: Height hint
var txidSlice []byte var txidSlice []byte
if txid != nil { if txid != nil {
txidSlice = txid[:] txidSlice = txid[:]
@ -155,9 +150,7 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
var confEvent *chainrpc.ConfEvent var confEvent *chainrpc.ConfEvent
confEvent, err := confStream.Recv() confEvent, err := confStream.Recv()
if err != nil { if err != nil {
if status.Code(err) != codes.Canceled { errChan <- err
errChan <- err
}
return return
} }
@ -226,9 +219,7 @@ func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) (
for { for {
epoch, err := blockEpochClient.Recv() epoch, err := blockEpochClient.Recv()
if err != nil { if err != nil {
if status.Code(err) != codes.Canceled { blockErrorChan <- err
blockErrorChan <- err
}
return return
} }

@ -5,19 +5,18 @@ import (
"errors" "errors"
"sync" "sync"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
// InvoicesClient exposes invoice functionality. // InvoicesClient exposes invoice functionality.
type InvoicesClient interface { type InvoicesClient interface {
SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) ( SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) (
<-chan channeldb.ContractState, <-chan error, error) <-chan InvoiceUpdate, <-chan error, error)
SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error
@ -27,6 +26,12 @@ type InvoicesClient interface {
string, error) string, error)
} }
// InvoiceUpdate contains a state update for an invoice.
type InvoiceUpdate struct {
State channeldb.ContractState
AmtPaid btcutil.Amount
}
type invoicesClient struct { type invoicesClient struct {
client invoicesrpc.InvoicesClient client invoicesrpc.InvoicesClient
wg sync.WaitGroup wg sync.WaitGroup
@ -69,7 +74,7 @@ func (s *invoicesClient) CancelInvoice(ctx context.Context,
} }
func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context, func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
hash lntypes.Hash) (<-chan channeldb.ContractState, hash lntypes.Hash) (<-chan InvoiceUpdate,
<-chan error, error) { <-chan error, error) {
invoiceStream, err := s.client. invoiceStream, err := s.client.
@ -81,7 +86,7 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
return nil, nil, err return nil, nil, err
} }
updateChan := make(chan channeldb.ContractState) updateChan := make(chan InvoiceUpdate)
errChan := make(chan error, 1) errChan := make(chan error, 1)
// Invoice updates goroutine. // Invoice updates goroutine.
@ -91,9 +96,7 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
for { for {
invoice, err := invoiceStream.Recv() invoice, err := invoiceStream.Recv()
if err != nil { if err != nil {
if status.Code(err) != codes.Canceled { errChan <- err
errChan <- err
}
return return
} }
@ -104,7 +107,10 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
} }
select { select {
case updateChan <- state: case updateChan <- InvoiceUpdate{
State: state,
AmtPaid: btcutil.Amount(invoice.AmtPaidSat),
}:
case <-ctx.Done(): case <-ctx.Done():
return return
} }

@ -14,7 +14,6 @@ import (
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32" "github.com/lightningnetwork/lnd/zpay32"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@ -29,8 +28,8 @@ type LightningClient interface {
GetInfo(ctx context.Context) (*Info, error) GetInfo(ctx context.Context) (*Info, error)
GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) ( EstimateFeeToP2WSH(ctx context.Context, amt btcutil.Amount,
lnwire.MilliSatoshi, error) confTarget int32) (btcutil.Amount, error)
ConfirmedWalletBalance(ctx context.Context) (btcutil.Amount, error) ConfirmedWalletBalance(ctx context.Context) (btcutil.Amount, error)
@ -144,28 +143,35 @@ func (s *lightningClient) GetInfo(ctx context.Context) (*Info, error) {
}, nil }, nil
} }
func (s *lightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, func (s *lightningClient) EstimateFeeToP2WSH(ctx context.Context,
dest [33]byte) (lnwire.MilliSatoshi, error) { amt btcutil.Amount, confTarget int32) (btcutil.Amount,
error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout) rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel() defer cancel()
routeResp, err := s.client.QueryRoutes( // Generate dummy p2wsh address for fee estimation.
wsh := [32]byte{}
p2wshAddress, err := btcutil.NewAddressWitnessScriptHash(
wsh[:], s.params,
)
if err != nil {
return 0, err
}
resp, err := s.client.EstimateFee(
rpcCtx, rpcCtx,
&lnrpc.QueryRoutesRequest{ &lnrpc.EstimateFeeRequest{
Amt: int64(amt), TargetConf: confTarget,
NumRoutes: 1, AddrToAmount: map[string]int64{
PubKey: hex.EncodeToString(dest[:]), p2wshAddress.String(): int64(amt),
},
}, },
) )
if err != nil { if err != nil {
return 0, err return 0, err
} }
if len(routeResp.Routes) == 0 { return btcutil.Amount(resp.FeeSat), nil
return 0, ErrNoRouteToServer
}
return lnwire.MilliSatoshi(routeResp.Routes[0].TotalFeesMsat), nil
} }
// PayInvoice pays an invoice. // PayInvoice pays an invoice.
@ -310,13 +316,19 @@ func (s *lightningClient) AddInvoice(ctx context.Context,
rpcIn := &lnrpc.Invoice{ rpcIn := &lnrpc.Invoice{
Memo: in.Memo, Memo: in.Memo,
RHash: in.Hash[:],
Value: int64(in.Value), Value: int64(in.Value),
Expiry: in.Expiry, Expiry: in.Expiry,
CltvExpiry: in.CltvExpiry, CltvExpiry: in.CltvExpiry,
Private: true, Private: true,
} }
if in.Preimage != nil {
rpcIn.RPreimage = in.Preimage[:]
}
if in.Hash != nil {
rpcIn.RHash = in.Hash[:]
}
resp, err := s.client.AddInvoice(rpcCtx, rpcIn) resp, err := s.client.AddInvoice(rpcCtx, rpcIn)
if err != nil { if err != nil {
return lntypes.Hash{}, "", err return lntypes.Hash{}, "", err

@ -0,0 +1,585 @@
package loop
import (
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
"time"
"github.com/lightninglabs/loop/swap"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
// MaxLoopInAcceptDelta configures the maximum acceptable number of
// remaining blocks until the on-chain htlc expires. This value is used
// to decide whether we want to continue with the swap parameters as
// proposed by the server. It is a protection to prevent the server from
// getting us to lock up our funds to an arbitrary point in the future.
MaxLoopInAcceptDelta = int32(1500)
// MinLoopInPublishDelta defines the minimum number of remaining blocks
// until on-chain htlc expiry required to proceed to publishing the htlc
// tx. This value isn't critical, as we could even safely publish the
// htlc after expiry. The reason we do implement this check is to
// prevent us from publishing an htlc that the server surely wouldn't
// follow up to.
MinLoopInPublishDelta = int32(10)
// TimeoutTxConfTarget defines the confirmation target for the loop in
// timeout tx.
TimeoutTxConfTarget = int32(2)
)
// loopInSwap contains all the in-memory state related to a pending loop in
// swap.
type loopInSwap struct {
swapKit
loopdb.LoopInContract
timeoutAddr btcutil.Address
}
// newLoopInSwap initiates a new loop in swap.
func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
currentHeight int32, request *LoopInRequest) (*loopInSwap, error) {
// 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.GetLoopInTerms(globalCtx)
if err != nil {
return nil, fmt.Errorf("loop in terms: %v", err)
}
swapFee := swap.CalcFee(
request.Amount, quote.SwapFeeBase, quote.SwapFeeRate,
)
if swapFee > request.MaxSwapFee {
logger.Warnf("Swap fee %v exceeding maximum of %v",
swapFee, request.MaxSwapFee)
return nil, ErrSwapFeeTooHigh
}
// Calculate the swap invoice amount. The prepay is added which
// effectively forces the server to pay us back our prepayment on a
// successful swap.
swapInvoiceAmt := request.Amount - swapFee
// Generate random preimage.
var swapPreimage lntypes.Preimage
if _, err := rand.Read(swapPreimage[:]); err != nil {
logger.Error("Cannot generate preimage")
}
swapHash := lntypes.Hash(sha256.Sum256(swapPreimage[:]))
// Derive a sender key for this swap.
keyDesc, err := cfg.lnd.WalletKit.DeriveNextKey(
globalCtx, swap.KeyFamily,
)
if err != nil {
return nil, err
}
var senderKey [33]byte
copy(senderKey[:], keyDesc.PubKey.SerializeCompressed())
// Create the swap invoice in lnd.
_, swapInvoice, err := cfg.lnd.Client.AddInvoice(
globalCtx, &invoicesrpc.AddInvoiceData{
Preimage: &swapPreimage,
Value: swapInvoiceAmt,
Memo: "swap",
Expiry: 3600 * 24 * 365,
},
)
if err != nil {
return nil, err
}
// Post the swap parameters to the swap server. The response contains
// the server success key and the expiry height of the on-chain swap
// htlc.
logger.Infof("Initiating swap request at height %v", currentHeight)
swapResp, err := cfg.server.NewLoopInSwap(globalCtx, swapHash,
request.Amount, senderKey, swapInvoice,
)
if err != nil {
return nil, fmt.Errorf("cannot initiate swap: %v", err)
}
// Validate the response parameters the prevent us continuing with a
// swap that is based on parameters outside our allowed range.
err = validateLoopInContract(cfg.lnd, currentHeight, request, swapResp)
if err != nil {
return nil, err
}
// Instantiate a struct that contains all required data to start the
// swap.
initiationTime := time.Now()
contract := loopdb.LoopInContract{
HtlcConfTarget: request.HtlcConfTarget,
LoopInChannel: request.LoopInChannel,
SwapContract: loopdb.SwapContract{
InitiationHeight: currentHeight,
InitiationTime: initiationTime,
ReceiverKey: swapResp.receiverKey,
SenderKey: senderKey,
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
},
}
swapKit, err := newSwapKit(
swapHash, TypeIn, cfg, &contract.SwapContract,
)
if err != nil {
return nil, err
}
swapKit.lastUpdateTime = initiationTime
swap := &loopInSwap{
LoopInContract: contract,
swapKit: *swapKit,
}
// Persist the data before exiting this function, so that the caller can
// trust that this swap will be resumed on restart.
err = cfg.store.CreateLoopIn(swapHash, &swap.LoopInContract)
if err != nil {
return nil, fmt.Errorf("cannot store swap: %v", err)
}
return swap, nil
}
// resumeLoopInSwap returns a swap object representing a pending swap that has
// been restored from the database.
func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig,
pend *loopdb.LoopIn) (*loopInSwap, error) {
hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:]))
logger.Infof("Resuming loop in swap %v", hash)
swapKit, err := newSwapKit(
hash, TypeIn, cfg, &pend.Contract.SwapContract,
)
if err != nil {
return nil, err
}
swap := &loopInSwap{
LoopInContract: *pend.Contract,
swapKit: *swapKit,
}
lastUpdate := pend.LastUpdate()
if lastUpdate == nil {
swap.lastUpdateTime = pend.Contract.InitiationTime
} else {
swap.state = lastUpdate.State
swap.lastUpdateTime = lastUpdate.Time
}
return swap, nil
}
// validateLoopInContract validates the contract parameters against our
// request.
func validateLoopInContract(lnd *lndclient.LndServices,
height int32,
request *LoopInRequest,
response *newLoopInResponse) error {
// Verify that we are not forced to publish an htlc that locks up our
// funds for too long in case the server doesn't follow through.
if response.expiry-height > MaxLoopInAcceptDelta {
return ErrExpiryTooFar
}
return nil
}
// execute starts/resumes the swap. It is a thin wrapper around executeSwap to
// conveniently handle the error case.
func (s *loopInSwap) execute(mainCtx context.Context,
cfg *executeConfig, height int32) error {
s.executeConfig = *cfg
s.height = height
// Announce swap by sending out an initial update.
err := s.sendUpdate(mainCtx)
if err != nil {
return err
}
// Execute the swap until it either reaches a final state or a temporary
// error occurs.
err = s.executeSwap(mainCtx)
// Sanity check. If there is no error, the swap must be in a final
// state.
if err == nil && s.state.Type() == loopdb.StateTypePending {
err = fmt.Errorf("swap in non-final state %v", s.state)
}
// If an unexpected error happened, report a temporary failure
// but don't persist the error. Otherwise for example a
// connection error could lead to abandoning the swap
// permanently and losing funds.
if err != nil {
s.log.Errorf("Swap error: %v", err)
s.state = loopdb.StateFailTemporary
// If we cannot send out this update, there is nothing we can do.
_ = s.sendUpdate(mainCtx)
return err
}
s.log.Infof("Loop in swap completed: %v "+
"(final cost: server %v, onchain %v)",
s.state,
s.cost.Server,
s.cost.Onchain,
)
return nil
}
// executeSwap executes the swap.
func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
var err error
// For loop in, the client takes the first step by publishing the
// on-chain htlc. Only do this is we haven't already done so in a
// previous run.
if s.state == loopdb.StateInitiated {
published, err := s.publishOnChainHtlc(globalCtx)
if err != nil {
return err
}
if !published {
return nil
}
}
// Wait for the htlc to confirm. After a restart this will pick up a
// previously published tx.
conf, err := s.waitForHtlcConf(globalCtx)
if err != nil {
return err
}
// Determine the htlc outpoint by inspecting the htlc tx.
htlcOutpoint, htlcValue, err := swap.GetScriptOutput(
conf.Tx, s.htlc.ScriptHash,
)
if err != nil {
return err
}
// TODO: Add miner fee of htlc tx to swap cost balance.
// The server is expected to see the htlc on-chain and knowing that it
// can sweep that htlc with the preimage, it should pay our swap
// invoice, receive the preimage and sweep the htlc. We are waiting for
// this to happen and simultaneously watch the htlc expiry height. When
// the htlc expires, we will publish a timeout tx to reclaim the funds.
spend, err := s.waitForHtlcSpend(globalCtx, htlcOutpoint)
if err != nil {
return err
}
// Determine the htlc input of the spending tx and inspect the witness
// to findout whether a success or a timeout tx spend the htlc.
htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
s.state = loopdb.StateSuccess
// Server swept the htlc. The htlc value can be added to the
// server cost balance.
s.cost.Server += htlcValue
} else {
s.state = loopdb.StateFailTimeout
// Now that the timeout tx confirmed, we can safely cancel the
// swap invoice. We still need to query the final invoice state.
// This is not a hodl invoice, so it may be that the invoice was
// already settled. This means that the server didn't succeed in
// sweeping the htlc after paying the invoice.
err := s.lnd.Invoices.CancelInvoice(globalCtx, s.hash)
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
return err
}
// TODO: Add miner fee of timeout tx to swap cost balance.
}
// Wait for a final state of the swap invoice. It should either be
// settled because the server successfully paid it or canceled because
// we canceled after our timeout tx confirmed.
err = s.waitForSwapInvoiceResult(globalCtx)
if err != nil {
return err
}
// Persist swap outcome.
if err := s.persistState(globalCtx); err != nil {
return err
}
return nil
}
// waitForHtlcConf watches the chain until the htlc confirms.
func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
*chainntnfs.TxConfirmation, error) {
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
confChan, confErr, err := s.lnd.ChainNotifier.RegisterConfirmationsNtfn(
ctx, nil, s.htlc.ScriptHash, 1, s.InitiationHeight,
)
if err != nil {
return nil, err
}
for {
select {
// Htlc confirmed.
case conf := <-confChan:
return conf, nil
// Conf ntfn error.
case err := <-confErr:
return nil, err
// Keep up with block height.
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
// Cancel.
case <-globalCtx.Done():
return nil, globalCtx.Err()
}
}
}
// publishOnChainHtlc checks whether there are still enough blocks left and if
// so, it publishes the htlc and advances the swap state.
func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
var err error
blocksRemaining := s.CltvExpiry - s.height
s.log.Infof("Blocks left until on-chain expiry: %v", blocksRemaining)
// Verify whether it still makes sense to publish the htlc.
if blocksRemaining < MinLoopInPublishDelta {
s.state = loopdb.StateFailTimeout
return false, s.persistState(ctx)
}
// Get fee estimate from lnd.
feeRate, err := s.lnd.WalletKit.EstimateFee(
ctx, s.LoopInContract.HtlcConfTarget,
)
if err != nil {
return false, fmt.Errorf("estimate fee: %v", err)
}
// Transition to state HtlcPublished before calling SendOutputs to
// prevent us from ever paying multiple times after a crash.
s.state = loopdb.StateHtlcPublished
err = s.persistState(ctx)
if err != nil {
return false, err
}
s.log.Infof("Publishing on chain HTLC with fee rate %v", feeRate)
tx, err := s.lnd.WalletKit.SendOutputs(ctx,
[]*wire.TxOut{{
PkScript: s.htlc.ScriptHash,
Value: int64(s.LoopInContract.AmountRequested),
}},
feeRate,
)
if err != nil {
return false, fmt.Errorf("send outputs: %v", err)
}
s.log.Infof("Published on chain HTLC tx %v", tx.TxHash())
return true, nil
}
// waitForHtlcSpend waits until a spending tx of the htlc gets confirmed and
// returns the spend details.
func (s *loopInSwap) waitForHtlcSpend(ctx context.Context,
htlc *wire.OutPoint) (*chainntnfs.SpendDetail, error) {
// Register the htlc spend notification.
rpcCtx, cancel := context.WithCancel(ctx)
defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
rpcCtx, nil, s.htlc.ScriptHash, s.InitiationHeight,
)
if err != nil {
return nil, fmt.Errorf("register spend ntfn: %v", err)
}
for {
select {
// Spend notification error.
case err := <-spendErr:
return nil, err
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
if s.height >= s.LoopInContract.CltvExpiry {
err := s.publishTimeoutTx(ctx, htlc)
if err != nil {
return nil, err
}
}
// Htlc spend, break loop.
case spendDetails := <-spendChan:
s.log.Infof("Htlc spend by tx: %v",
spendDetails.SpenderTxHash)
return spendDetails, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// waitForSwapPaid waits until our swap invoice gets paid by the server.
func (s *loopInSwap) waitForSwapInvoiceResult(ctx context.Context) error {
// Wait for swap invoice to be paid.
rpcCtx, cancel := context.WithCancel(ctx)
defer cancel()
s.log.Infof("Subscribing to swap invoice %v", s.hash)
swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
rpcCtx, s.hash,
)
if err != nil {
return err
}
for {
select {
// Swap invoice ntfn error.
case err := <-swapInvoiceErr:
return err
case update := <-swapInvoiceChan:
s.log.Infof("Received swap invoice update: %v",
update.State)
switch update.State {
// Swap invoice was paid, so update server cost balance.
case channeldb.ContractSettled:
s.cost.Server -= update.AmtPaid
return nil
// Canceled invoice has no effect on server cost
// balance.
case channeldb.ContractCanceled:
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
}
// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired.
// The swap failed and we are reclaiming our funds.
func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
htlc *wire.OutPoint) error {
if s.timeoutAddr == nil {
var err error
s.timeoutAddr, err = s.lnd.WalletKit.NextAddr(ctx)
if err != nil {
return err
}
}
// Calculate sweep tx fee
fee, err := s.sweeper.GetSweepFee(
ctx, s.htlc.MaxTimeoutWitnessSize, TimeoutTxConfTarget,
)
if err != nil {
return err
}
witnessFunc := func(sig []byte) (wire.TxWitness, error) {
return s.htlc.GenTimeoutWitness(sig)
}
timeoutTx, err := s.sweeper.CreateSweepTx(
ctx, s.height, s.htlc, *htlc, s.SenderKey, witnessFunc,
s.LoopInContract.AmountRequested, fee, s.timeoutAddr,
)
if err != nil {
return err
}
timeoutTxHash := timeoutTx.TxHash()
s.log.Infof("Publishing timeout tx %v with fee %v to addr %v",
timeoutTxHash, fee, s.timeoutAddr)
err = s.lnd.WalletKit.PublishTransaction(ctx, timeoutTx)
if err != nil {
s.log.Warnf("publish timeout: %v", err)
}
return nil
}
// persistState updates the swap state and sends out an update notification.
func (s *loopInSwap) persistState(ctx context.Context) error {
updateTime := time.Now()
s.lastUpdateTime = updateTime
// Update state in store.
err := s.store.UpdateLoopIn(s.hash, updateTime, s.state)
if err != nil {
return err
}
// Send out swap update
return s.sendUpdate(ctx)
}

@ -0,0 +1,370 @@
package loop
import (
"context"
"testing"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
)
var (
testLoopInRequest = LoopInRequest{
Amount: btcutil.Amount(50000),
MaxSwapFee: btcutil.Amount(1000),
HtlcConfTarget: 2,
}
)
// TestLoopInSuccess tests the success scenario where the swap completes the
// happy flow.
func TestLoopInSuccess(t *testing.T) {
defer test.Guard(t)()
ctx := newLoopInTestContext(t)
height := int32(600)
cfg := &swapConfig{
lnd: &ctx.lnd.LndServices,
store: ctx.store,
server: ctx.server,
}
swap, err := newLoopInSwap(
context.Background(), cfg,
height, &testLoopInRequest,
)
if err != nil {
t.Fatal(err)
}
ctx.store.assertLoopInStored()
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), ctx.cfg, height)
if err != nil {
logger.Error(err)
}
errChan <- err
}()
ctx.assertState(loopdb.StateInitiated)
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published.
htlcTx := <-ctx.lnd.SendOutputsChannel
// Expect register for htlc conf.
<-ctx.lnd.RegisterConfChannel
// Confirm htlc.
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: &htlcTx,
}
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderInputIndex: 0,
}
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != ctx.server.swapHash {
t.Fatal("client subscribing to wrong invoice")
}
// Server has already paid invoice before spending the htlc. Signal
// settled.
subscription.Update <- lndclient.InvoiceUpdate{
State: channeldb.ContractSettled,
AmtPaid: 49000,
}
ctx.assertState(loopdb.StateSuccess)
ctx.store.assertLoopInState(loopdb.StateSuccess)
err = <-errChan
if err != nil {
t.Fatal(err)
}
}
// TestLoopInTimeout tests the scenario where the server doesn't sweep the htlc
// and the client is forced to reclaim the funds using the timeout tx.
func TestLoopInTimeout(t *testing.T) {
defer test.Guard(t)()
ctx := newLoopInTestContext(t)
height := int32(600)
cfg := &swapConfig{
lnd: &ctx.lnd.LndServices,
store: ctx.store,
server: ctx.server,
}
swap, err := newLoopInSwap(
context.Background(), cfg,
height, &testLoopInRequest,
)
if err != nil {
t.Fatal(err)
}
ctx.store.assertLoopInStored()
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), ctx.cfg, height)
if err != nil {
logger.Error(err)
}
errChan <- err
}()
ctx.assertState(loopdb.StateInitiated)
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published.
htlcTx := <-ctx.lnd.SendOutputsChannel
// Expect register for htlc conf.
<-ctx.lnd.RegisterConfChannel
// Confirm htlc.
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: &htlcTx,
}
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
// Let htlc expire.
ctx.blockEpochChan <- swap.LoopInContract.CltvExpiry
// Expect timeout tx to be published.
timeoutTx := <-ctx.lnd.TxPublishChannel
// Confirm timeout tx.
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: timeoutTx,
SpenderInputIndex: 0,
}
// Now that timeout tx has confirmed, the client should be able to
// safely cancel the swap invoice.
<-ctx.lnd.FailInvoiceChannel
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != ctx.server.swapHash {
t.Fatal("client subscribing to wrong invoice")
}
// Signal the the invoice was canceled.
subscription.Update <- lndclient.InvoiceUpdate{
State: channeldb.ContractCanceled,
}
ctx.assertState(loopdb.StateFailTimeout)
ctx.store.assertLoopInState(loopdb.StateFailTimeout)
err = <-errChan
if err != nil {
t.Fatal(err)
}
}
// TestLoopInResume tests resuming swaps in various states.
func TestLoopInResume(t *testing.T) {
t.Run("initiated", func(t *testing.T) {
testLoopInResume(t, loopdb.StateInitiated, false)
})
t.Run("initiated expired", func(t *testing.T) {
testLoopInResume(t, loopdb.StateInitiated, true)
})
t.Run("htlc published", func(t *testing.T) {
testLoopInResume(t, loopdb.StateHtlcPublished, false)
})
}
func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) {
defer test.Guard(t)()
ctx := newLoopInTestContext(t)
cfg := &swapConfig{
lnd: &ctx.lnd.LndServices,
store: ctx.store,
server: ctx.server,
}
senderKey := [33]byte{4}
receiverKey := [33]byte{5}
contract := &loopdb.LoopInContract{
HtlcConfTarget: 2,
SwapContract: loopdb.SwapContract{
Preimage: testPreimage,
AmountRequested: 100000,
CltvExpiry: 744,
ReceiverKey: receiverKey,
SenderKey: senderKey,
MaxSwapFee: 60000,
MaxMinerFee: 50000,
},
}
pendSwap := &loopdb.LoopIn{
Contract: contract,
Loop: loopdb.Loop{
Events: []*loopdb.LoopEvent{
{
State: state,
},
},
Hash: testPreimage.Hash(),
},
}
htlc, err := swap.NewHtlc(
contract.CltvExpiry, contract.SenderKey, contract.ReceiverKey,
testPreimage.Hash(),
)
if err != nil {
t.Fatal(err)
}
err = ctx.store.CreateLoopIn(testPreimage.Hash(), contract)
if err != nil {
t.Fatal(err)
}
swap, err := resumeLoopInSwap(
context.Background(), cfg,
pendSwap,
)
if err != nil {
t.Fatal(err)
}
var height int32
if expired {
height = 740
} else {
height = 600
}
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), ctx.cfg, height)
if err != nil {
logger.Error(err)
}
errChan <- err
}()
defer func() {
err = <-errChan
if err != nil {
t.Fatal(err)
}
select {
case <-ctx.lnd.SendPaymentChannel:
t.Fatal("unexpected payment sent")
default:
}
select {
case <-ctx.lnd.SendOutputsChannel:
t.Fatal("unexpected tx published")
default:
}
}()
var htlcTx wire.MsgTx
if state == loopdb.StateInitiated {
ctx.assertState(loopdb.StateInitiated)
if expired {
ctx.assertState(loopdb.StateFailTimeout)
return
}
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published.
htlcTx = <-ctx.lnd.SendOutputsChannel
} else {
ctx.assertState(loopdb.StateHtlcPublished)
htlcTx.AddTxOut(&wire.TxOut{
PkScript: htlc.ScriptHash,
})
}
// Expect register for htlc conf.
<-ctx.lnd.RegisterConfChannel
// Confirm htlc.
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: &htlcTx,
}
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
successTxHash := successTx.TxHash()
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderTxHash: &successTxHash,
SpenderInputIndex: 0,
}
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != testPreimage.Hash() {
t.Fatal("client subscribing to wrong invoice")
}
// Server has already paid invoice before spending the htlc. Signal
// settled.
subscription.Update <- lndclient.InvoiceUpdate{
State: channeldb.ContractSettled,
AmtPaid: 49000,
}
ctx.assertState(loopdb.StateSuccess)
}

@ -0,0 +1,62 @@
package loop
import (
"testing"
"time"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/test"
)
type loopInTestContext struct {
t *testing.T
lnd *test.LndMockServices
server *serverMock
store *storeMock
sweeper *sweep.Sweeper
cfg *executeConfig
statusChan chan SwapInfo
blockEpochChan chan interface{}
}
func newLoopInTestContext(t *testing.T) *loopInTestContext {
lnd := test.NewMockLnd()
server := newServerMock()
store := newStoreMock(t)
sweeper := sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
expiryChan := make(chan time.Time)
timerFactory := func(expiry time.Duration) <-chan time.Time {
return expiryChan
}
cfg := executeConfig{
statusChan: statusChan,
sweeper: &sweeper,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
}
return &loopInTestContext{
t: t,
lnd: lnd,
server: server,
store: store,
sweeper: &sweeper,
cfg: &cfg,
statusChan: statusChan,
blockEpochChan: blockEpochChan,
}
}
func (c *loopInTestContext) assertState(expectedState loopdb.SwapState) {
state := <-c.statusChan
if state.State != expectedState {
c.t.Fatalf("expected state %v but got %v", expectedState,
state.State)
}
}

@ -135,7 +135,7 @@ func resumeLoopOutSwap(reqContext context.Context, cfg *swapConfig,
hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:])) hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:]))
logger.Infof("Resuming swap %v", hash) logger.Infof("Resuming loop out swap %v", hash)
swapKit, err := newSwapKit( swapKit, err := newSwapKit(
hash, TypeOut, cfg, &pend.Contract.SwapContract, hash, TypeOut, cfg, &pend.Contract.SwapContract,

@ -29,20 +29,24 @@ type SwapType int32
const ( const (
// LOOP_OUT indicates an loop out swap (off-chain to on-chain) // LOOP_OUT indicates an loop out swap (off-chain to on-chain)
SwapType_LOOP_OUT SwapType = 0 SwapType_LOOP_OUT SwapType = 0
// LOOP_IN indicates a loop in swap (on-chain to off-chain)
SwapType_LOOP_IN SwapType = 1
) )
var SwapType_name = map[int32]string{ var SwapType_name = map[int32]string{
0: "LOOP_OUT", 0: "LOOP_OUT",
1: "LOOP_IN",
} }
var SwapType_value = map[string]int32{ var SwapType_value = map[string]int32{
"LOOP_OUT": 0, "LOOP_OUT": 0,
"LOOP_IN": 1,
} }
func (x SwapType) String() string { func (x SwapType) String() string {
return proto.EnumName(SwapType_name, int32(x)) return proto.EnumName(SwapType_name, int32(x))
} }
func (SwapType) EnumDescriptor() ([]byte, []int) { func (SwapType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{0} return fileDescriptor_client_e75b35e98d3b3b20, []int{0}
} }
type SwapState int32 type SwapState int32
@ -61,6 +65,10 @@ const (
// case where we wait for fees to come down before we sweep. // case where we wait for fees to come down before we sweep.
SwapState_PREIMAGE_REVEALED SwapState = 1 SwapState_PREIMAGE_REVEALED SwapState = 1
// * // *
// HTLC_PUBLISHED is reached when the htlc tx has been published in a loop in
// swap.
SwapState_HTLC_PUBLISHED SwapState = 2
// *
// SUCCESS is the final swap state that is reached when the sweep tx has // SUCCESS is the final swap state that is reached when the sweep tx has
// the required confirmation depth. // the required confirmation depth.
SwapState_SUCCESS SwapState = 3 SwapState_SUCCESS SwapState = 3
@ -73,12 +81,14 @@ const (
var SwapState_name = map[int32]string{ var SwapState_name = map[int32]string{
0: "INITIATED", 0: "INITIATED",
1: "PREIMAGE_REVEALED", 1: "PREIMAGE_REVEALED",
2: "HTLC_PUBLISHED",
3: "SUCCESS", 3: "SUCCESS",
4: "FAILED", 4: "FAILED",
} }
var SwapState_value = map[string]int32{ var SwapState_value = map[string]int32{
"INITIATED": 0, "INITIATED": 0,
"PREIMAGE_REVEALED": 1, "PREIMAGE_REVEALED": 1,
"HTLC_PUBLISHED": 2,
"SUCCESS": 3, "SUCCESS": 3,
"FAILED": 4, "FAILED": 4,
} }
@ -87,7 +97,7 @@ func (x SwapState) String() string {
return proto.EnumName(SwapState_name, int32(x)) return proto.EnumName(SwapState_name, int32(x))
} }
func (SwapState) EnumDescriptor() ([]byte, []int) { func (SwapState) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{1} return fileDescriptor_client_e75b35e98d3b3b20, []int{1}
} }
type LoopOutRequest struct { type LoopOutRequest struct {
@ -145,7 +155,7 @@ func (m *LoopOutRequest) Reset() { *m = LoopOutRequest{} }
func (m *LoopOutRequest) String() string { return proto.CompactTextString(m) } func (m *LoopOutRequest) String() string { return proto.CompactTextString(m) }
func (*LoopOutRequest) ProtoMessage() {} func (*LoopOutRequest) ProtoMessage() {}
func (*LoopOutRequest) Descriptor() ([]byte, []int) { func (*LoopOutRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{0} return fileDescriptor_client_e75b35e98d3b3b20, []int{0}
} }
func (m *LoopOutRequest) XXX_Unmarshal(b []byte) error { func (m *LoopOutRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_LoopOutRequest.Unmarshal(m, b) return xxx_messageInfo_LoopOutRequest.Unmarshal(m, b)
@ -221,6 +231,85 @@ func (m *LoopOutRequest) GetLoopOutChannel() uint64 {
return 0 return 0
} }
type LoopInRequest struct {
// *
// Requested swap amount in sat. This does not include the swap and miner
// fee.
Amt int64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"`
// *
// Maximum we are willing to pay the server for the swap. This value is not
// disclosed in the swap initiation call, but if the server asks for a
// higher fee, we abort the swap. Typically this value is taken from the
// response of the GetQuote call. It includes the prepay amount.
MaxSwapFee int64 `protobuf:"varint,2,opt,name=max_swap_fee,json=maxSwapFee,proto3" json:"max_swap_fee,omitempty"`
// *
// Maximum in on-chain fees that we are willing to spent. If we want to
// publish the on-chain htlc and the fee estimate turns out higher than this
// value, we cancel the swap.
//
// max_miner_fee is typically taken from the response of the GetQuote call.
MaxMinerFee int64 `protobuf:"varint,3,opt,name=max_miner_fee,json=maxMinerFee,proto3" json:"max_miner_fee,omitempty"`
// *
// The channel to loop in. If zero, the channel to loop in is selected based
// on the lowest routing fee for the swap payment from the server.
LoopInChannel uint64 `protobuf:"varint,4,opt,name=loop_in_channel,json=loopInChannel,proto3" json:"loop_in_channel,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *LoopInRequest) Reset() { *m = LoopInRequest{} }
func (m *LoopInRequest) String() string { return proto.CompactTextString(m) }
func (*LoopInRequest) ProtoMessage() {}
func (*LoopInRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e75b35e98d3b3b20, []int{1}
}
func (m *LoopInRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_LoopInRequest.Unmarshal(m, b)
}
func (m *LoopInRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_LoopInRequest.Marshal(b, m, deterministic)
}
func (dst *LoopInRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_LoopInRequest.Merge(dst, src)
}
func (m *LoopInRequest) XXX_Size() int {
return xxx_messageInfo_LoopInRequest.Size(m)
}
func (m *LoopInRequest) XXX_DiscardUnknown() {
xxx_messageInfo_LoopInRequest.DiscardUnknown(m)
}
var xxx_messageInfo_LoopInRequest proto.InternalMessageInfo
func (m *LoopInRequest) GetAmt() int64 {
if m != nil {
return m.Amt
}
return 0
}
func (m *LoopInRequest) GetMaxSwapFee() int64 {
if m != nil {
return m.MaxSwapFee
}
return 0
}
func (m *LoopInRequest) GetMaxMinerFee() int64 {
if m != nil {
return m.MaxMinerFee
}
return 0
}
func (m *LoopInRequest) GetLoopInChannel() uint64 {
if m != nil {
return m.LoopInChannel
}
return 0
}
type SwapResponse struct { type SwapResponse struct {
// * // *
// Swap identifier to track status in the update stream that is returned from // Swap identifier to track status in the update stream that is returned from
@ -235,7 +324,7 @@ func (m *SwapResponse) Reset() { *m = SwapResponse{} }
func (m *SwapResponse) String() string { return proto.CompactTextString(m) } func (m *SwapResponse) String() string { return proto.CompactTextString(m) }
func (*SwapResponse) ProtoMessage() {} func (*SwapResponse) ProtoMessage() {}
func (*SwapResponse) Descriptor() ([]byte, []int) { func (*SwapResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{1} return fileDescriptor_client_e75b35e98d3b3b20, []int{2}
} }
func (m *SwapResponse) XXX_Unmarshal(b []byte) error { func (m *SwapResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapResponse.Unmarshal(m, b) return xxx_messageInfo_SwapResponse.Unmarshal(m, b)
@ -272,7 +361,7 @@ func (m *MonitorRequest) Reset() { *m = MonitorRequest{} }
func (m *MonitorRequest) String() string { return proto.CompactTextString(m) } func (m *MonitorRequest) String() string { return proto.CompactTextString(m) }
func (*MonitorRequest) ProtoMessage() {} func (*MonitorRequest) ProtoMessage() {}
func (*MonitorRequest) Descriptor() ([]byte, []int) { func (*MonitorRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{2} return fileDescriptor_client_e75b35e98d3b3b20, []int{3}
} }
func (m *MonitorRequest) XXX_Unmarshal(b []byte) error { func (m *MonitorRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_MonitorRequest.Unmarshal(m, b) return xxx_messageInfo_MonitorRequest.Unmarshal(m, b)
@ -325,7 +414,7 @@ func (m *SwapStatus) Reset() { *m = SwapStatus{} }
func (m *SwapStatus) String() string { return proto.CompactTextString(m) } func (m *SwapStatus) String() string { return proto.CompactTextString(m) }
func (*SwapStatus) ProtoMessage() {} func (*SwapStatus) ProtoMessage() {}
func (*SwapStatus) Descriptor() ([]byte, []int) { func (*SwapStatus) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{3} return fileDescriptor_client_e75b35e98d3b3b20, []int{4}
} }
func (m *SwapStatus) XXX_Unmarshal(b []byte) error { func (m *SwapStatus) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapStatus.Unmarshal(m, b) return xxx_messageInfo_SwapStatus.Unmarshal(m, b)
@ -404,7 +493,7 @@ func (m *TermsRequest) Reset() { *m = TermsRequest{} }
func (m *TermsRequest) String() string { return proto.CompactTextString(m) } func (m *TermsRequest) String() string { return proto.CompactTextString(m) }
func (*TermsRequest) ProtoMessage() {} func (*TermsRequest) ProtoMessage() {}
func (*TermsRequest) Descriptor() ([]byte, []int) { func (*TermsRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{4} return fileDescriptor_client_e75b35e98d3b3b20, []int{5}
} }
func (m *TermsRequest) XXX_Unmarshal(b []byte) error { func (m *TermsRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsRequest.Unmarshal(m, b) return xxx_messageInfo_TermsRequest.Unmarshal(m, b)
@ -446,10 +535,7 @@ type TermsResponse struct {
MaxSwapAmount int64 `protobuf:"varint,6,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"` MaxSwapAmount int64 `protobuf:"varint,6,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"`
// * // *
// On-chain cltv expiry delta // On-chain cltv expiry delta
CltvDelta int32 `protobuf:"varint,7,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"` CltvDelta int32 `protobuf:"varint,7,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"`
// *
// Maximum cltv expiry delta
MaxCltv int32 `protobuf:"varint,8,opt,name=max_cltv,json=maxCltv,proto3" json:"max_cltv,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@ -459,7 +545,7 @@ func (m *TermsResponse) Reset() { *m = TermsResponse{} }
func (m *TermsResponse) String() string { return proto.CompactTextString(m) } func (m *TermsResponse) String() string { return proto.CompactTextString(m) }
func (*TermsResponse) ProtoMessage() {} func (*TermsResponse) ProtoMessage() {}
func (*TermsResponse) Descriptor() ([]byte, []int) { func (*TermsResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{5} return fileDescriptor_client_e75b35e98d3b3b20, []int{6}
} }
func (m *TermsResponse) XXX_Unmarshal(b []byte) error { func (m *TermsResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsResponse.Unmarshal(m, b) return xxx_messageInfo_TermsResponse.Unmarshal(m, b)
@ -528,13 +614,6 @@ func (m *TermsResponse) GetCltvDelta() int32 {
return 0 return 0
} }
func (m *TermsResponse) GetMaxCltv() int32 {
if m != nil {
return m.MaxCltv
}
return 0
}
type QuoteRequest struct { type QuoteRequest struct {
// * // *
// The amount to swap in satoshis. // The amount to swap in satoshis.
@ -548,7 +627,7 @@ func (m *QuoteRequest) Reset() { *m = QuoteRequest{} }
func (m *QuoteRequest) String() string { return proto.CompactTextString(m) } func (m *QuoteRequest) String() string { return proto.CompactTextString(m) }
func (*QuoteRequest) ProtoMessage() {} func (*QuoteRequest) ProtoMessage() {}
func (*QuoteRequest) Descriptor() ([]byte, []int) { func (*QuoteRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{6} return fileDescriptor_client_e75b35e98d3b3b20, []int{7}
} }
func (m *QuoteRequest) XXX_Unmarshal(b []byte) error { func (m *QuoteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteRequest.Unmarshal(m, b) return xxx_messageInfo_QuoteRequest.Unmarshal(m, b)
@ -594,7 +673,7 @@ func (m *QuoteResponse) Reset() { *m = QuoteResponse{} }
func (m *QuoteResponse) String() string { return proto.CompactTextString(m) } func (m *QuoteResponse) String() string { return proto.CompactTextString(m) }
func (*QuoteResponse) ProtoMessage() {} func (*QuoteResponse) ProtoMessage() {}
func (*QuoteResponse) Descriptor() ([]byte, []int) { func (*QuoteResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{7} return fileDescriptor_client_e75b35e98d3b3b20, []int{8}
} }
func (m *QuoteResponse) XXX_Unmarshal(b []byte) error { func (m *QuoteResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteResponse.Unmarshal(m, b) return xxx_messageInfo_QuoteResponse.Unmarshal(m, b)
@ -637,6 +716,7 @@ func (m *QuoteResponse) GetMinerFee() int64 {
func init() { func init() {
proto.RegisterType((*LoopOutRequest)(nil), "looprpc.LoopOutRequest") proto.RegisterType((*LoopOutRequest)(nil), "looprpc.LoopOutRequest")
proto.RegisterType((*LoopInRequest)(nil), "looprpc.LoopInRequest")
proto.RegisterType((*SwapResponse)(nil), "looprpc.SwapResponse") proto.RegisterType((*SwapResponse)(nil), "looprpc.SwapResponse")
proto.RegisterType((*MonitorRequest)(nil), "looprpc.MonitorRequest") proto.RegisterType((*MonitorRequest)(nil), "looprpc.MonitorRequest")
proto.RegisterType((*SwapStatus)(nil), "looprpc.SwapStatus") proto.RegisterType((*SwapStatus)(nil), "looprpc.SwapStatus")
@ -666,6 +746,12 @@ type SwapClientClient interface {
// point onwards, progress can be tracked via the SwapStatus stream that is // point onwards, progress can be tracked via the SwapStatus stream that is
// returned from Monitor(). // returned from Monitor().
LoopOut(ctx context.Context, in *LoopOutRequest, opts ...grpc.CallOption) (*SwapResponse, error) LoopOut(ctx context.Context, in *LoopOutRequest, opts ...grpc.CallOption) (*SwapResponse, error)
// *
// LoopIn initiates a loop in swap with the given parameters. The call
// returns after the swap has been set up with the swap server. From that
// point onwards, progress can be tracked via the SwapStatus stream
// that is returned from Monitor().
LoopIn(ctx context.Context, in *LoopInRequest, opts ...grpc.CallOption) (*SwapResponse, error)
// * loop: `monitor` // * loop: `monitor`
// Monitor will return a stream of swap updates for currently active swaps. // Monitor will return a stream of swap updates for currently active swaps.
// TODO: add MonitorSync version for REST clients. // TODO: add MonitorSync version for REST clients.
@ -677,6 +763,12 @@ type SwapClientClient interface {
// LoopOutQuote returns a quote for a loop out swap with the provided // LoopOutQuote returns a quote for a loop out swap with the provided
// parameters. // parameters.
LoopOutQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) LoopOutQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error)
// *
// GetTerms returns the terms that the server enforces for swaps.
GetLoopInTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error)
// *
// GetQuote returns a quote for a swap with the provided parameters.
GetLoopInQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error)
} }
type swapClientClient struct { type swapClientClient struct {
@ -696,6 +788,15 @@ func (c *swapClientClient) LoopOut(ctx context.Context, in *LoopOutRequest, opts
return out, nil return out, nil
} }
func (c *swapClientClient) LoopIn(ctx context.Context, in *LoopInRequest, opts ...grpc.CallOption) (*SwapResponse, error) {
out := new(SwapResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/LoopIn", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (SwapClient_MonitorClient, error) { func (c *swapClientClient) Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (SwapClient_MonitorClient, error) {
stream, err := c.cc.NewStream(ctx, &_SwapClient_serviceDesc.Streams[0], "/looprpc.SwapClient/Monitor", opts...) stream, err := c.cc.NewStream(ctx, &_SwapClient_serviceDesc.Streams[0], "/looprpc.SwapClient/Monitor", opts...)
if err != nil { if err != nil {
@ -746,6 +847,24 @@ func (c *swapClientClient) LoopOutQuote(ctx context.Context, in *QuoteRequest, o
return out, nil return out, nil
} }
func (c *swapClientClient) GetLoopInTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) {
out := new(TermsResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetLoopInTerms", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) GetLoopInQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) {
out := new(QuoteResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetLoopInQuote", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapClientServer is the server API for SwapClient service. // SwapClientServer is the server API for SwapClient service.
type SwapClientServer interface { type SwapClientServer interface {
// * loop: `out` // * loop: `out`
@ -754,6 +873,12 @@ type SwapClientServer interface {
// point onwards, progress can be tracked via the SwapStatus stream that is // point onwards, progress can be tracked via the SwapStatus stream that is
// returned from Monitor(). // returned from Monitor().
LoopOut(context.Context, *LoopOutRequest) (*SwapResponse, error) LoopOut(context.Context, *LoopOutRequest) (*SwapResponse, error)
// *
// LoopIn initiates a loop in swap with the given parameters. The call
// returns after the swap has been set up with the swap server. From that
// point onwards, progress can be tracked via the SwapStatus stream
// that is returned from Monitor().
LoopIn(context.Context, *LoopInRequest) (*SwapResponse, error)
// * loop: `monitor` // * loop: `monitor`
// Monitor will return a stream of swap updates for currently active swaps. // Monitor will return a stream of swap updates for currently active swaps.
// TODO: add MonitorSync version for REST clients. // TODO: add MonitorSync version for REST clients.
@ -765,6 +890,12 @@ type SwapClientServer interface {
// LoopOutQuote returns a quote for a loop out swap with the provided // LoopOutQuote returns a quote for a loop out swap with the provided
// parameters. // parameters.
LoopOutQuote(context.Context, *QuoteRequest) (*QuoteResponse, error) LoopOutQuote(context.Context, *QuoteRequest) (*QuoteResponse, error)
// *
// GetTerms returns the terms that the server enforces for swaps.
GetLoopInTerms(context.Context, *TermsRequest) (*TermsResponse, error)
// *
// GetQuote returns a quote for a swap with the provided parameters.
GetLoopInQuote(context.Context, *QuoteRequest) (*QuoteResponse, error)
} }
func RegisterSwapClientServer(s *grpc.Server, srv SwapClientServer) { func RegisterSwapClientServer(s *grpc.Server, srv SwapClientServer) {
@ -789,6 +920,24 @@ func _SwapClient_LoopOut_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _SwapClient_LoopIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoopInRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).LoopIn(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/LoopIn",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).LoopIn(ctx, req.(*LoopInRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_Monitor_Handler(srv interface{}, stream grpc.ServerStream) error { func _SwapClient_Monitor_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(MonitorRequest) m := new(MonitorRequest)
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
@ -846,6 +995,42 @@ func _SwapClient_LoopOutQuote_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _SwapClient_GetLoopInTerms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TermsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).GetLoopInTerms(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/GetLoopInTerms",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).GetLoopInTerms(ctx, req.(*TermsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_GetLoopInQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QuoteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).GetLoopInQuote(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/GetLoopInQuote",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).GetLoopInQuote(ctx, req.(*QuoteRequest))
}
return interceptor(ctx, in, info, handler)
}
var _SwapClient_serviceDesc = grpc.ServiceDesc{ var _SwapClient_serviceDesc = grpc.ServiceDesc{
ServiceName: "looprpc.SwapClient", ServiceName: "looprpc.SwapClient",
HandlerType: (*SwapClientServer)(nil), HandlerType: (*SwapClientServer)(nil),
@ -854,6 +1039,10 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
MethodName: "LoopOut", MethodName: "LoopOut",
Handler: _SwapClient_LoopOut_Handler, Handler: _SwapClient_LoopOut_Handler,
}, },
{
MethodName: "LoopIn",
Handler: _SwapClient_LoopIn_Handler,
},
{ {
MethodName: "LoopOutTerms", MethodName: "LoopOutTerms",
Handler: _SwapClient_LoopOutTerms_Handler, Handler: _SwapClient_LoopOutTerms_Handler,
@ -862,6 +1051,14 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
MethodName: "LoopOutQuote", MethodName: "LoopOutQuote",
Handler: _SwapClient_LoopOutQuote_Handler, Handler: _SwapClient_LoopOutQuote_Handler,
}, },
{
MethodName: "GetLoopInTerms",
Handler: _SwapClient_GetLoopInTerms_Handler,
},
{
MethodName: "GetLoopInQuote",
Handler: _SwapClient_GetLoopInQuote_Handler,
},
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
{ {
@ -873,60 +1070,65 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
Metadata: "client.proto", Metadata: "client.proto",
} }
func init() { proto.RegisterFile("client.proto", fileDescriptor_client_ecf1f0d12250b1cb) } func init() { proto.RegisterFile("client.proto", fileDescriptor_client_e75b35e98d3b3b20) }
var fileDescriptor_client_ecf1f0d12250b1cb = []byte{ var fileDescriptor_client_e75b35e98d3b3b20 = []byte{
// 825 bytes of a gzipped FileDescriptorProto // 901 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x54, 0xcb, 0x72, 0xe3, 0x44, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x72, 0xdb, 0x44,
0x14, 0x1d, 0xc9, 0x4e, 0x6c, 0xdd, 0xc8, 0x8a, 0xdd, 0xf3, 0xf2, 0x18, 0x86, 0x32, 0x2a, 0x1e, 0x18, 0xad, 0x64, 0xc7, 0x3f, 0x5f, 0x64, 0x45, 0xde, 0xb6, 0xa9, 0x31, 0x94, 0x31, 0x1a, 0x5a,
0xae, 0x2c, 0x22, 0xc8, 0xac, 0x60, 0x67, 0x1c, 0x0f, 0x65, 0x2a, 0x21, 0xa6, 0xed, 0x50, 0xc5, 0x3c, 0xb9, 0x88, 0x21, 0xbd, 0x60, 0xe0, 0x86, 0x71, 0x6d, 0xb7, 0xf5, 0x8c, 0xd3, 0x18, 0xd9,
0x4a, 0xd5, 0x63, 0xf7, 0x64, 0x54, 0xa5, 0x56, 0x6b, 0xd4, 0xad, 0x8c, 0x5d, 0x14, 0x1b, 0x7e, 0x61, 0x86, 0x1b, 0x34, 0x5b, 0x7b, 0x9b, 0x6a, 0x46, 0xda, 0x55, 0xa5, 0x55, 0x9b, 0x0c, 0xc3,
0x81, 0xdf, 0x60, 0xc5, 0xaf, 0xb0, 0xe0, 0x07, 0xf8, 0x05, 0xf6, 0x54, 0x3f, 0xec, 0x48, 0x81, 0x0d, 0x2f, 0xc0, 0x00, 0x6f, 0xc2, 0xab, 0xf0, 0x0a, 0xbc, 0x02, 0xf7, 0xcc, 0xfe, 0x48, 0x95,
0xd9, 0x75, 0x9d, 0x3e, 0xf7, 0x74, 0xf7, 0xb9, 0xe7, 0x36, 0xf8, 0xab, 0x34, 0xa1, 0x99, 0x3c, 0x1c, 0x72, 0x93, 0x3b, 0xf9, 0xdb, 0xb3, 0x67, 0xcf, 0x9e, 0xef, 0x7c, 0x6b, 0xb0, 0x36, 0x61,
0xcd, 0x0b, 0x2e, 0x39, 0x6a, 0xa5, 0x9c, 0xe7, 0x45, 0xbe, 0x1a, 0x7c, 0x78, 0xc3, 0xf9, 0x4d, 0x40, 0x28, 0x3f, 0x8e, 0x13, 0xc6, 0x19, 0x6a, 0x86, 0x8c, 0xc5, 0x49, 0xbc, 0xe9, 0x7f, 0x72,
0x4a, 0x23, 0x92, 0x27, 0x11, 0xc9, 0x32, 0x2e, 0x89, 0x4c, 0x78, 0x26, 0x0c, 0x2d, 0xfc, 0xc3, 0xc1, 0xd8, 0x45, 0x48, 0x46, 0x38, 0x0e, 0x46, 0x98, 0x52, 0xc6, 0x31, 0x0f, 0x18, 0x4d, 0x15,
0x85, 0xe0, 0x82, 0xf3, 0xfc, 0xaa, 0x94, 0x98, 0xbe, 0x2d, 0xa9, 0x90, 0xa8, 0x0b, 0x0d, 0xc2, 0xcc, 0xfd, 0xcb, 0x04, 0x7b, 0xc1, 0x58, 0x7c, 0x96, 0x71, 0x8f, 0xbc, 0xcd, 0x48, 0xca, 0x91,
0x64, 0xdf, 0x19, 0x3a, 0xa3, 0x06, 0x56, 0x4b, 0x84, 0xa0, 0xb9, 0xa6, 0x42, 0xf6, 0xdd, 0xa1, 0x03, 0x35, 0x1c, 0xf1, 0x9e, 0x31, 0x30, 0x86, 0x35, 0x4f, 0x7c, 0x22, 0x04, 0xf5, 0x2d, 0x49,
0x33, 0xf2, 0xb0, 0x5e, 0xa3, 0x08, 0x1e, 0x31, 0xb2, 0x89, 0xc5, 0x3b, 0x92, 0xc7, 0x05, 0x2f, 0x79, 0xcf, 0x1c, 0x18, 0xc3, 0xb6, 0x27, 0xbf, 0xd1, 0x08, 0xee, 0x45, 0xf8, 0xd2, 0x4f, 0xdf,
0x65, 0x92, 0xdd, 0xc4, 0xaf, 0x29, 0xed, 0x37, 0x74, 0x59, 0x8f, 0x91, 0xcd, 0xe2, 0x1d, 0xc9, 0xe3, 0xd8, 0x4f, 0x58, 0xc6, 0x03, 0x7a, 0xe1, 0xbf, 0x26, 0xa4, 0x57, 0x93, 0xdb, 0xba, 0x11,
0xb1, 0xd9, 0x79, 0x49, 0x29, 0x7a, 0x01, 0x4f, 0x54, 0x41, 0x5e, 0xd0, 0x9c, 0x6c, 0x6b, 0x25, 0xbe, 0x5c, 0xbd, 0xc7, 0xb1, 0xa7, 0x56, 0x9e, 0x11, 0x82, 0x9e, 0xc0, 0xa1, 0xd8, 0x10, 0x27,
0x4d, 0x5d, 0xf2, 0x90, 0x91, 0xcd, 0x5c, 0x6f, 0x56, 0x8a, 0x86, 0xe0, 0xef, 0x4f, 0x51, 0xd4, 0x24, 0xc6, 0x57, 0x95, 0x2d, 0x75, 0xb9, 0xe5, 0x6e, 0x84, 0x2f, 0x97, 0x72, 0xb1, 0xb4, 0x69,
0x03, 0x4d, 0x05, 0xab, 0xae, 0x18, 0x9f, 0x40, 0x50, 0x91, 0x55, 0x17, 0x3f, 0xd4, 0x1c, 0x7f, 0x00, 0x56, 0x71, 0x8a, 0x80, 0xee, 0x49, 0x28, 0x68, 0x76, 0x81, 0xf8, 0x1c, 0xec, 0x12, 0xad,
0x2f, 0x37, 0x66, 0x12, 0x85, 0xd0, 0x51, 0x2c, 0x96, 0x64, 0xb4, 0xd0, 0x42, 0x2d, 0x4d, 0x3a, 0x10, 0xde, 0x90, 0x18, 0xab, 0xa0, 0x1b, 0x47, 0x1c, 0xb9, 0xd0, 0x11, 0xa8, 0x28, 0xa0, 0x24,
0x62, 0x64, 0x73, 0xa9, 0x30, 0xa5, 0x34, 0x82, 0xae, 0xf2, 0x2c, 0xe6, 0xa5, 0x8c, 0x57, 0x6f, 0x91, 0x44, 0x4d, 0x09, 0xda, 0x8f, 0xf0, 0xe5, 0xa9, 0xa8, 0x09, 0xa6, 0x21, 0x38, 0xc2, 0x33,
0x48, 0x96, 0xd1, 0xb4, 0xdf, 0x1e, 0x3a, 0xa3, 0x26, 0x0e, 0x52, 0xe3, 0xd0, 0xc4, 0xa0, 0xe1, 0x9f, 0x65, 0xdc, 0xdf, 0xbc, 0xc1, 0x94, 0x92, 0xb0, 0xd7, 0x1a, 0x18, 0xc3, 0xba, 0x67, 0x87,
0x47, 0xe0, 0xeb, 0xc7, 0x51, 0x91, 0xf3, 0x4c, 0x50, 0x14, 0x80, 0x9b, 0xac, 0xb5, 0x61, 0x1e, 0xca, 0xa1, 0x89, 0xaa, 0xba, 0xbf, 0x19, 0xd0, 0x11, 0xa6, 0xcd, 0xe9, 0xcd, 0x9e, 0xed, 0x2a,
0x76, 0x93, 0x75, 0xd8, 0x85, 0xe0, 0x92, 0x67, 0x89, 0xe4, 0x85, 0xf5, 0x34, 0xfc, 0xc7, 0x01, 0x37, 0xaf, 0x29, 0xbf, 0xa6, 0xa9, 0x76, 0x5d, 0xd3, 0x63, 0x38, 0x90, 0x9a, 0x02, 0x5a, 0x48,
0x50, 0x25, 0x0b, 0x49, 0x64, 0x29, 0xfe, 0xc7, 0x62, 0x23, 0xe1, 0xee, 0x24, 0xd0, 0xa7, 0xd0, 0xaa, 0x4b, 0x49, 0x9d, 0x50, 0x9e, 0x9f, 0x2b, 0xfa, 0x14, 0x2c, 0x69, 0x37, 0x49, 0x63, 0x46,
0x94, 0xdb, 0xdc, 0xd8, 0x19, 0x9c, 0xf5, 0x4e, 0x6d, 0x37, 0x4f, 0x95, 0xc8, 0x72, 0x9b, 0x53, 0x53, 0x82, 0x6c, 0x30, 0x83, 0xad, 0x94, 0xd3, 0xf6, 0xcc, 0x60, 0xeb, 0x3a, 0x60, 0x9f, 0x32,
0xac, 0xb7, 0xd1, 0x08, 0x0e, 0x84, 0x24, 0xd2, 0x78, 0x18, 0x9c, 0xa1, 0x1a, 0x4f, 0x1d, 0x46, 0x1a, 0x70, 0x96, 0x68, 0xc5, 0xee, 0xbf, 0x06, 0x80, 0xd8, 0xb2, 0xe2, 0x98, 0x67, 0xe9, 0xff,
0xb1, 0x21, 0xa0, 0xcf, 0xe1, 0x38, 0xc9, 0x12, 0x99, 0xe8, 0xee, 0xc7, 0x32, 0x61, 0x3b, 0x33, 0x5c, 0x40, 0x51, 0x98, 0x39, 0x05, 0x7a, 0x04, 0x75, 0x7e, 0x15, 0x2b, 0x95, 0xf6, 0x49, 0xf7,
0x83, 0x3b, 0x78, 0x99, 0x30, 0x63, 0x03, 0x11, 0x32, 0x2e, 0xf3, 0x35, 0x91, 0xd4, 0x30, 0x8d, 0x58, 0xe7, 0xeb, 0x58, 0x90, 0xac, 0xaf, 0x62, 0xe2, 0xc9, 0x65, 0x34, 0x84, 0xbd, 0x94, 0x63,
0xa5, 0x81, 0xc2, 0xaf, 0x35, 0xac, 0x99, 0x1f, 0x83, 0xff, 0x46, 0xa6, 0xab, 0x98, 0xac, 0xd7, 0xae, 0xba, 0x6a, 0x9f, 0xa0, 0x0a, 0x4e, 0x1c, 0x46, 0x3c, 0x05, 0x40, 0x5f, 0xc0, 0x41, 0x40,
0x05, 0x15, 0x42, 0x7b, 0xea, 0xe1, 0x23, 0x85, 0x8d, 0x0d, 0x14, 0x06, 0xe0, 0x2f, 0x69, 0xc1, 0x03, 0x1e, 0xc8, 0x3c, 0xfa, 0x3c, 0x88, 0xf2, 0xf6, 0xda, 0x1f, 0xca, 0xeb, 0x20, 0x52, 0x8d,
0xc4, 0xce, 0x87, 0xdf, 0x5d, 0xe8, 0x58, 0xc0, 0x7a, 0x77, 0x02, 0x3d, 0xdd, 0xdd, 0x9c, 0x6c, 0xc1, 0x29, 0xf7, 0xb3, 0x78, 0x8b, 0x39, 0x51, 0x48, 0xd5, 0x64, 0x5b, 0xd4, 0xcf, 0x65, 0x59,
0x19, 0xcd, 0x64, 0xac, 0x83, 0x66, 0xac, 0x3c, 0x56, 0x1b, 0x73, 0x83, 0x9f, 0xab, 0xcc, 0x85, 0x22, 0x3f, 0x03, 0xeb, 0x0d, 0x0f, 0x37, 0x3e, 0xde, 0x6e, 0x13, 0x92, 0xa6, 0xb2, 0xcb, 0x6d,
0xd0, 0xd9, 0x25, 0x21, 0x7e, 0x45, 0x04, 0xd5, 0x7e, 0x35, 0xf0, 0x91, 0x30, 0x59, 0xf8, 0x86, 0x6f, 0x5f, 0xd4, 0xc6, 0xaa, 0xe4, 0xda, 0x60, 0xad, 0x49, 0x12, 0xa5, 0xb9, 0x0f, 0xbf, 0x9b,
0x08, 0x5a, 0xe3, 0x14, 0xca, 0x99, 0x46, 0x8d, 0x83, 0x95, 0x17, 0xcf, 0x01, 0x2a, 0x79, 0x31, 0xd0, 0xd1, 0x05, 0xed, 0xdd, 0x11, 0x74, 0x65, 0xd7, 0x62, 0x7c, 0x15, 0x11, 0xca, 0x7d, 0x19,
0xf1, 0xf3, 0xf2, 0x7d, 0x58, 0x3e, 0x83, 0x63, 0x96, 0x64, 0x26, 0x74, 0x84, 0xf1, 0x32, 0x93, 0x7d, 0x65, 0xe5, 0x81, 0x58, 0x58, 0xaa, 0xfa, 0x54, 0xf4, 0xdd, 0x85, 0x4e, 0xde, 0x61, 0xff,
0xd6, 0xaa, 0x0e, 0x4b, 0x32, 0x65, 0xec, 0x58, 0x83, 0x9a, 0xb7, 0x0b, 0xa7, 0xe5, 0x1d, 0x5a, 0x15, 0x4e, 0xf3, 0x36, 0xef, 0xa7, 0xaa, 0xc7, 0x4f, 0x71, 0x4a, 0x2a, 0x98, 0x44, 0x38, 0x53,
0x9e, 0xc9, 0xa7, 0xe5, 0x3d, 0x07, 0x58, 0xa5, 0xf2, 0x36, 0x5e, 0xd3, 0x54, 0x12, 0xed, 0xd2, 0xab, 0x60, 0x3c, 0xe1, 0xc5, 0x43, 0x80, 0x52, 0x82, 0xd5, 0x40, 0xb4, 0xe3, 0x22, 0xbe, 0x8f,
0x01, 0xf6, 0x14, 0x72, 0xae, 0x00, 0xf4, 0x0c, 0xda, 0x4a, 0x46, 0x01, 0x3a, 0x6f, 0x07, 0xb8, 0xe1, 0x20, 0x0a, 0xa8, 0x0a, 0x13, 0x8e, 0x58, 0x46, 0xb9, 0xb6, 0xaa, 0x13, 0x05, 0x54, 0x18,
0xc5, 0xc8, 0x66, 0x92, 0xca, 0xdb, 0x70, 0x08, 0xfe, 0x0f, 0x25, 0x97, 0xf4, 0xbd, 0xa3, 0x19, 0x3b, 0x96, 0x45, 0x89, 0xcb, 0x43, 0xa7, 0x71, 0x0d, 0x8d, 0x53, 0xb9, 0xd3, 0xb8, 0x87, 0x00,
0xbe, 0x86, 0x8e, 0x65, 0x58, 0x3f, 0x9f, 0x41, 0x7b, 0x3f, 0x2d, 0x86, 0xd7, 0xb2, 0x4f, 0xbf, 0x9b, 0x90, 0xbf, 0xf3, 0xb7, 0x24, 0xe4, 0x58, 0xba, 0xb4, 0xe7, 0xb5, 0x45, 0x65, 0x2a, 0x0a,
0xf7, 0x6c, 0xf7, 0xfe, 0xb3, 0x3f, 0x00, 0xef, 0x6e, 0x3e, 0x8c, 0x6b, 0x6d, 0x66, 0x87, 0xe3, 0xee, 0x00, 0xac, 0xef, 0x33, 0xc6, 0xc9, 0x8d, 0xe9, 0x76, 0x5f, 0x43, 0x47, 0x23, 0xb4, 0x69,
0xa4, 0x0f, 0xed, 0x5d, 0xf4, 0x90, 0x0f, 0xed, 0x8b, 0xab, 0xab, 0x79, 0x7c, 0x75, 0xbd, 0xec, 0x1f, 0x41, 0xab, 0x88, 0xba, 0xc2, 0x35, 0xf5, 0xfd, 0x76, 0xee, 0x66, 0xee, 0xde, 0xed, 0x63,
0x3e, 0x38, 0xf9, 0x0e, 0xbc, 0x7d, 0xd8, 0x50, 0x07, 0xbc, 0xd9, 0xf7, 0xb3, 0xe5, 0x6c, 0xbc, 0x68, 0xef, 0x8e, 0x40, 0x2b, 0xd2, 0xf9, 0x3f, 0x7a, 0x04, 0xad, 0x3c, 0x5f, 0xc8, 0x82, 0xd6,
0x9c, 0x9e, 0x77, 0x1f, 0xa0, 0xc7, 0xd0, 0x9b, 0xe3, 0xe9, 0xec, 0x72, 0xfc, 0xed, 0x34, 0xc6, 0xe2, 0xec, 0x6c, 0xe9, 0x9f, 0x9d, 0xaf, 0x9d, 0x3b, 0x68, 0x1f, 0x9a, 0xf2, 0xd7, 0xfc, 0xa5,
0xd3, 0x1f, 0xa7, 0xe3, 0x8b, 0xe9, 0x79, 0xd7, 0x41, 0x47, 0xd0, 0x5a, 0x5c, 0x4f, 0x26, 0xd3, 0x63, 0x1c, 0xfd, 0x04, 0xed, 0x22, 0x5e, 0xa8, 0x03, 0xed, 0xf9, 0xcb, 0xf9, 0x7a, 0x3e, 0x5e,
0xc5, 0xa2, 0xdb, 0x40, 0x00, 0x87, 0x2f, 0xc7, 0x33, 0xb5, 0xd1, 0x3c, 0xfb, 0xcb, 0x35, 0x63, 0xcf, 0xa6, 0xce, 0x1d, 0x74, 0x1f, 0xba, 0x4b, 0x6f, 0x36, 0x3f, 0x1d, 0x3f, 0x9f, 0xf9, 0xde,
0x32, 0xd1, 0x3f, 0x19, 0xc2, 0xd0, 0xb2, 0x7f, 0x13, 0x7a, 0xba, 0x4f, 0x76, 0xfd, 0xb7, 0x1a, 0xec, 0x87, 0xd9, 0x78, 0x31, 0x9b, 0x3a, 0x06, 0x42, 0x60, 0xbf, 0x58, 0x2f, 0x26, 0xfe, 0xf2,
0x3c, 0xae, 0x45, 0x7e, 0x67, 0x43, 0xf8, 0xf4, 0xd7, 0x3f, 0xff, 0xfe, 0xcd, 0xed, 0x85, 0x7e, 0xfc, 0xe9, 0x62, 0xbe, 0x7a, 0x31, 0x9b, 0x3a, 0xa6, 0xe0, 0x5c, 0x9d, 0x4f, 0x26, 0xb3, 0xd5,
0x74, 0xfb, 0x65, 0xa4, 0x18, 0x11, 0x2f, 0xe5, 0xd7, 0xce, 0x09, 0xfa, 0x0a, 0x5a, 0x76, 0x36, 0xca, 0xa9, 0x21, 0x80, 0xc6, 0xb3, 0xf1, 0x5c, 0x80, 0xeb, 0x27, 0x7f, 0xd4, 0xd5, 0xb0, 0x4c,
0x2b, 0x9a, 0xf5, 0x69, 0x1d, 0x3c, 0xfc, 0xcf, 0x18, 0x95, 0xe2, 0x0b, 0x07, 0xfd, 0x04, 0xbe, 0xe4, 0x0b, 0x8b, 0x3c, 0x68, 0xea, 0x37, 0x13, 0x3d, 0x28, 0xf2, 0x5d, 0x7d, 0x45, 0xfb, 0xf7,
0x3d, 0x5c, 0x47, 0x18, 0xdd, 0x1d, 0x5d, 0xcd, 0xf8, 0xe0, 0xc9, 0x7d, 0xd8, 0x5e, 0x69, 0xa0, 0x2b, 0xc1, 0xcf, 0x7d, 0x72, 0x1f, 0xfc, 0xfa, 0xf7, 0x3f, 0x7f, 0x9a, 0x5d, 0xd7, 0x1a, 0xbd,
0xaf, 0xf4, 0x08, 0xa1, 0xea, 0x95, 0x22, 0xa9, 0xa5, 0xe2, 0xbd, 0xb4, 0xee, 0x66, 0x45, 0xba, 0xfb, 0x6a, 0x24, 0x10, 0x23, 0x96, 0xf1, 0x6f, 0x8d, 0x23, 0xf4, 0x35, 0x34, 0xd4, 0x93, 0x82,
0xda, 0xff, 0x8a, 0x74, 0xad, 0xe9, 0xe1, 0x50, 0x4b, 0x0f, 0x50, 0xbf, 0x26, 0xfd, 0x56, 0x71, 0x0e, 0x2b, 0x94, 0xc5, 0x1b, 0x73, 0x03, 0x23, 0xfa, 0x06, 0x9a, 0x7a, 0xb4, 0x4b, 0x62, 0xaa,
0xa2, 0x9f, 0x09, 0x93, 0xbf, 0xbc, 0x3a, 0xd4, 0xdf, 0xfd, 0x8b, 0x7f, 0x03, 0x00, 0x00, 0xff, 0xc3, 0xde, 0xbf, 0x7b, 0x6d, 0x0a, 0xb3, 0xf4, 0x4b, 0x03, 0xfd, 0x08, 0x96, 0x56, 0x2d, 0x27,
0xff, 0x2d, 0xba, 0xce, 0x65, 0x25, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x38, 0xa1, 0x3c, 0x22, 0xfd, 0xc3, 0xdd, 0xb2, 0xbe, 0x4b, 0x5f, 0xde, 0xe5, 0x1e,
0x42, 0xe5, 0xbb, 0x8c, 0xb8, 0xa4, 0xf2, 0x0b, 0x6a, 0x99, 0x93, 0x12, 0x75, 0x39, 0x59, 0x25,
0xea, 0x4a, 0x9c, 0xdc, 0x81, 0xa4, 0xee, 0xa3, 0x5e, 0x85, 0xfa, 0xad, 0xc0, 0x8c, 0x7e, 0xc6,
0x11, 0xff, 0x05, 0x7d, 0x07, 0xf6, 0x73, 0xc2, 0x95, 0x43, 0xb7, 0x51, 0x5f, 0x21, 0xb8, 0x8d,
0xc6, 0x57, 0x0d, 0xf9, 0x0f, 0xfa, 0xe4, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x17, 0x23, 0x70,
0x9a, 0x78, 0x07, 0x00, 0x00,
} }

@ -22,6 +22,14 @@ service SwapClient {
}; };
} }
/**
LoopIn initiates a loop in swap with the given parameters. The call
returns after the swap has been set up with the swap server. From that
point onwards, progress can be tracked via the SwapStatus stream
that is returned from Monitor().
*/
rpc LoopIn(LoopInRequest) returns (SwapResponse);
/** loop: `monitor` /** loop: `monitor`
Monitor will return a stream of swap updates for currently active swaps. Monitor will return a stream of swap updates for currently active swaps.
TODO: add MonitorSync version for REST clients. TODO: add MonitorSync version for REST clients.
@ -46,6 +54,16 @@ service SwapClient {
get: "/v1/loop/out/quote/{amt}" get: "/v1/loop/out/quote/{amt}"
}; };
} }
/**
GetTerms returns the terms that the server enforces for swaps.
*/
rpc GetLoopInTerms(TermsRequest) returns(TermsResponse);
/**
GetQuote returns a quote for a swap with the provided parameters.
*/
rpc GetLoopInQuote(QuoteRequest) returns(QuoteResponse);
} }
message LoopOutRequest { message LoopOutRequest {
@ -111,6 +129,36 @@ message LoopOutRequest {
uint64 loop_out_channel = 8; uint64 loop_out_channel = 8;
} }
message LoopInRequest {
/**
Requested swap amount in sat. This does not include the swap and miner
fee.
*/
int64 amt = 1;
/**
Maximum we are willing to pay the server for the swap. This value is not
disclosed in the swap initiation call, but if the server asks for a
higher fee, we abort the swap. Typically this value is taken from the
response of the GetQuote call.
*/
int64 max_swap_fee = 2;
/**
Maximum in on-chain fees that we are willing to spent. If we want to
publish the on-chain htlc and the fee estimate turns out higher than this
value, we cancel the swap.
max_miner_fee is typically taken from the response of the GetQuote call.
*/
int64 max_miner_fee = 3;
/**
The channel to loop in. If zero, the channel to loop in is selected based
on the lowest routing fee for the swap payment from the server.
*/
uint64 loop_in_channel = 4;
}
message SwapResponse { message SwapResponse {
/** /**
@ -165,6 +213,9 @@ message SwapStatus {
enum SwapType { enum SwapType {
// LOOP_OUT indicates an loop out swap (off-chain to on-chain) // LOOP_OUT indicates an loop out swap (off-chain to on-chain)
LOOP_OUT = 0; LOOP_OUT = 0;
// LOOP_IN indicates a loop in swap (on-chain to off-chain)
LOOP_IN = 1;
} }
enum SwapState { enum SwapState {
@ -184,6 +235,12 @@ enum SwapState {
*/ */
PREIMAGE_REVEALED = 1; PREIMAGE_REVEALED = 1;
/**
HTLC_PUBLISHED is reached when the htlc tx has been published in a loop in
swap.
*/
HTLC_PUBLISHED = 2;
/** /**
SUCCESS is the final swap state that is reached when the sweep tx has SUCCESS is the final swap state that is reached when the sweep tx has
the required confirmation depth. the required confirmation depth.
@ -236,11 +293,6 @@ message TermsResponse {
On-chain cltv expiry delta On-chain cltv expiry delta
*/ */
int32 cltv_delta = 7; int32 cltv_delta = 7;
/**
Maximum cltv expiry delta
*/
int32 max_cltv = 8;
} }
message QuoteRequest { message QuoteRequest {

@ -165,11 +165,12 @@
"enum": [ "enum": [
"INITIATED", "INITIATED",
"PREIMAGE_REVEALED", "PREIMAGE_REVEALED",
"HTLC_PUBLISHED",
"SUCCESS", "SUCCESS",
"FAILED" "FAILED"
], ],
"default": "INITIATED", "default": "INITIATED",
"description": " - INITIATED: *\nINITIATED is the initial state of a swap. At that point, the initiation\ncall to the server has been made and the payment process has been started\nfor the swap and prepayment invoices.\n - PREIMAGE_REVEALED: *\nPREIMAGE_REVEALED is reached when the sweep tx publication is first\nattempted. From that point on, we should consider the preimage to no\nlonger be secret and we need to do all we can to get the sweep confirmed.\nThis state will mostly coalesce with StateHtlcConfirmed, except in the\ncase where we wait for fees to come down before we sweep.\n - SUCCESS: *\nSUCCESS is the final swap state that is reached when the sweep tx has\nthe required confirmation depth.\n - FAILED: *\nFAILED is the final swap state for a failed swap with or without loss of\nthe swap amount." "description": " - INITIATED: *\nINITIATED is the initial state of a swap. At that point, the initiation\ncall to the server has been made and the payment process has been started\nfor the swap and prepayment invoices.\n - PREIMAGE_REVEALED: *\nPREIMAGE_REVEALED is reached when the sweep tx publication is first\nattempted. From that point on, we should consider the preimage to no\nlonger be secret and we need to do all we can to get the sweep confirmed.\nThis state will mostly coalesce with StateHtlcConfirmed, except in the\ncase where we wait for fees to come down before we sweep.\n - HTLC_PUBLISHED: *\nHTLC_PUBLISHED is reached when the htlc tx has been published in a loop in\nswap.\n - SUCCESS: *\nSUCCESS is the final swap state that is reached when the sweep tx has\nthe required confirmation depth.\n - FAILED: *\nFAILED is the final swap state for a failed swap with or without loss of\nthe swap amount."
}, },
"looprpcSwapStatus": { "looprpcSwapStatus": {
"type": "object", "type": "object",
@ -210,10 +211,11 @@
"looprpcSwapType": { "looprpcSwapType": {
"type": "string", "type": "string",
"enum": [ "enum": [
"LOOP_OUT" "LOOP_OUT",
"LOOP_IN"
], ],
"default": "LOOP_OUT", "default": "LOOP_OUT",
"title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)" "title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)"
}, },
"looprpcTermsResponse": { "looprpcTermsResponse": {
"type": "object", "type": "object",
@ -251,11 +253,6 @@
"type": "integer", "type": "integer",
"format": "int32", "format": "int32",
"title": "*\nOn-chain cltv expiry delta" "title": "*\nOn-chain cltv expiry delta"
},
"max_cltv": {
"type": "integer",
"format": "int32",
"title": "*\nMaximum cltv expiry delta"
} }
} }
} }

@ -37,7 +37,7 @@ func (m *ServerLoopOutRequest) Reset() { *m = ServerLoopOutRequest{} }
func (m *ServerLoopOutRequest) String() string { return proto.CompactTextString(m) } func (m *ServerLoopOutRequest) String() string { return proto.CompactTextString(m) }
func (*ServerLoopOutRequest) ProtoMessage() {} func (*ServerLoopOutRequest) ProtoMessage() {}
func (*ServerLoopOutRequest) Descriptor() ([]byte, []int) { func (*ServerLoopOutRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_server_76a33eec530f39a2, []int{0} return fileDescriptor_server_1f6c9db5f7136644, []int{0}
} }
func (m *ServerLoopOutRequest) XXX_Unmarshal(b []byte) error { func (m *ServerLoopOutRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopOutRequest.Unmarshal(m, b) return xxx_messageInfo_ServerLoopOutRequest.Unmarshal(m, b)
@ -92,7 +92,7 @@ func (m *ServerLoopOutResponse) Reset() { *m = ServerLoopOutResponse{} }
func (m *ServerLoopOutResponse) String() string { return proto.CompactTextString(m) } func (m *ServerLoopOutResponse) String() string { return proto.CompactTextString(m) }
func (*ServerLoopOutResponse) ProtoMessage() {} func (*ServerLoopOutResponse) ProtoMessage() {}
func (*ServerLoopOutResponse) Descriptor() ([]byte, []int) { func (*ServerLoopOutResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_server_76a33eec530f39a2, []int{1} return fileDescriptor_server_1f6c9db5f7136644, []int{1}
} }
func (m *ServerLoopOutResponse) XXX_Unmarshal(b []byte) error { func (m *ServerLoopOutResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopOutResponse.Unmarshal(m, b) return xxx_messageInfo_ServerLoopOutResponse.Unmarshal(m, b)
@ -150,7 +150,7 @@ func (m *ServerLoopOutQuoteRequest) Reset() { *m = ServerLoopOutQuoteReq
func (m *ServerLoopOutQuoteRequest) String() string { return proto.CompactTextString(m) } func (m *ServerLoopOutQuoteRequest) String() string { return proto.CompactTextString(m) }
func (*ServerLoopOutQuoteRequest) ProtoMessage() {} func (*ServerLoopOutQuoteRequest) ProtoMessage() {}
func (*ServerLoopOutQuoteRequest) Descriptor() ([]byte, []int) { func (*ServerLoopOutQuoteRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_server_76a33eec530f39a2, []int{2} return fileDescriptor_server_1f6c9db5f7136644, []int{2}
} }
func (m *ServerLoopOutQuoteRequest) XXX_Unmarshal(b []byte) error { func (m *ServerLoopOutQuoteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopOutQuoteRequest.Unmarshal(m, b) return xxx_messageInfo_ServerLoopOutQuoteRequest.Unmarshal(m, b)
@ -187,7 +187,7 @@ func (m *ServerLoopOutQuote) Reset() { *m = ServerLoopOutQuote{} }
func (m *ServerLoopOutQuote) String() string { return proto.CompactTextString(m) } func (m *ServerLoopOutQuote) String() string { return proto.CompactTextString(m) }
func (*ServerLoopOutQuote) ProtoMessage() {} func (*ServerLoopOutQuote) ProtoMessage() {}
func (*ServerLoopOutQuote) Descriptor() ([]byte, []int) { func (*ServerLoopOutQuote) Descriptor() ([]byte, []int) {
return fileDescriptor_server_76a33eec530f39a2, []int{3} return fileDescriptor_server_1f6c9db5f7136644, []int{3}
} }
func (m *ServerLoopOutQuote) XXX_Unmarshal(b []byte) error { func (m *ServerLoopOutQuote) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopOutQuote.Unmarshal(m, b) return xxx_messageInfo_ServerLoopOutQuote.Unmarshal(m, b)
@ -256,11 +256,223 @@ func (m *ServerLoopOutQuote) GetCltvDelta() int32 {
return 0 return 0
} }
type ServerLoopInRequest struct {
SenderKey []byte `protobuf:"bytes,1,opt,name=sender_key,json=senderKey,proto3" json:"sender_key,omitempty"`
SwapHash []byte `protobuf:"bytes,2,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
Amt uint64 `protobuf:"varint,3,opt,name=amt,proto3" json:"amt,omitempty"`
SwapInvoice string `protobuf:"bytes,4,opt,name=swap_invoice,json=swapInvoice,proto3" json:"swap_invoice,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerLoopInRequest) Reset() { *m = ServerLoopInRequest{} }
func (m *ServerLoopInRequest) String() string { return proto.CompactTextString(m) }
func (*ServerLoopInRequest) ProtoMessage() {}
func (*ServerLoopInRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_server_1f6c9db5f7136644, []int{4}
}
func (m *ServerLoopInRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopInRequest.Unmarshal(m, b)
}
func (m *ServerLoopInRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerLoopInRequest.Marshal(b, m, deterministic)
}
func (dst *ServerLoopInRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerLoopInRequest.Merge(dst, src)
}
func (m *ServerLoopInRequest) XXX_Size() int {
return xxx_messageInfo_ServerLoopInRequest.Size(m)
}
func (m *ServerLoopInRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ServerLoopInRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ServerLoopInRequest proto.InternalMessageInfo
func (m *ServerLoopInRequest) GetSenderKey() []byte {
if m != nil {
return m.SenderKey
}
return nil
}
func (m *ServerLoopInRequest) GetSwapHash() []byte {
if m != nil {
return m.SwapHash
}
return nil
}
func (m *ServerLoopInRequest) GetAmt() uint64 {
if m != nil {
return m.Amt
}
return 0
}
func (m *ServerLoopInRequest) GetSwapInvoice() string {
if m != nil {
return m.SwapInvoice
}
return ""
}
type ServerLoopInResponse struct {
ReceiverKey []byte `protobuf:"bytes,1,opt,name=receiver_key,json=receiverKey,proto3" json:"receiver_key,omitempty"`
Expiry int32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerLoopInResponse) Reset() { *m = ServerLoopInResponse{} }
func (m *ServerLoopInResponse) String() string { return proto.CompactTextString(m) }
func (*ServerLoopInResponse) ProtoMessage() {}
func (*ServerLoopInResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_server_1f6c9db5f7136644, []int{5}
}
func (m *ServerLoopInResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopInResponse.Unmarshal(m, b)
}
func (m *ServerLoopInResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerLoopInResponse.Marshal(b, m, deterministic)
}
func (dst *ServerLoopInResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerLoopInResponse.Merge(dst, src)
}
func (m *ServerLoopInResponse) XXX_Size() int {
return xxx_messageInfo_ServerLoopInResponse.Size(m)
}
func (m *ServerLoopInResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ServerLoopInResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ServerLoopInResponse proto.InternalMessageInfo
func (m *ServerLoopInResponse) GetReceiverKey() []byte {
if m != nil {
return m.ReceiverKey
}
return nil
}
func (m *ServerLoopInResponse) GetExpiry() int32 {
if m != nil {
return m.Expiry
}
return 0
}
type ServerLoopInQuoteRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerLoopInQuoteRequest) Reset() { *m = ServerLoopInQuoteRequest{} }
func (m *ServerLoopInQuoteRequest) String() string { return proto.CompactTextString(m) }
func (*ServerLoopInQuoteRequest) ProtoMessage() {}
func (*ServerLoopInQuoteRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_server_1f6c9db5f7136644, []int{6}
}
func (m *ServerLoopInQuoteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopInQuoteRequest.Unmarshal(m, b)
}
func (m *ServerLoopInQuoteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerLoopInQuoteRequest.Marshal(b, m, deterministic)
}
func (dst *ServerLoopInQuoteRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerLoopInQuoteRequest.Merge(dst, src)
}
func (m *ServerLoopInQuoteRequest) XXX_Size() int {
return xxx_messageInfo_ServerLoopInQuoteRequest.Size(m)
}
func (m *ServerLoopInQuoteRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ServerLoopInQuoteRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ServerLoopInQuoteRequest proto.InternalMessageInfo
type ServerLoopInQuoteResponse struct {
SwapFeeBase int64 `protobuf:"varint,1,opt,name=swap_fee_base,json=swapFeeBase,proto3" json:"swap_fee_base,omitempty"`
SwapFeeRate int64 `protobuf:"varint,2,opt,name=swap_fee_rate,json=swapFeeRate,proto3" json:"swap_fee_rate,omitempty"`
MinSwapAmount uint64 `protobuf:"varint,4,opt,name=min_swap_amount,json=minSwapAmount,proto3" json:"min_swap_amount,omitempty"`
MaxSwapAmount uint64 `protobuf:"varint,5,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"`
CltvDelta int32 `protobuf:"varint,6,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerLoopInQuoteResponse) Reset() { *m = ServerLoopInQuoteResponse{} }
func (m *ServerLoopInQuoteResponse) String() string { return proto.CompactTextString(m) }
func (*ServerLoopInQuoteResponse) ProtoMessage() {}
func (*ServerLoopInQuoteResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_server_1f6c9db5f7136644, []int{7}
}
func (m *ServerLoopInQuoteResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerLoopInQuoteResponse.Unmarshal(m, b)
}
func (m *ServerLoopInQuoteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerLoopInQuoteResponse.Marshal(b, m, deterministic)
}
func (dst *ServerLoopInQuoteResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerLoopInQuoteResponse.Merge(dst, src)
}
func (m *ServerLoopInQuoteResponse) XXX_Size() int {
return xxx_messageInfo_ServerLoopInQuoteResponse.Size(m)
}
func (m *ServerLoopInQuoteResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ServerLoopInQuoteResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ServerLoopInQuoteResponse proto.InternalMessageInfo
func (m *ServerLoopInQuoteResponse) GetSwapFeeBase() int64 {
if m != nil {
return m.SwapFeeBase
}
return 0
}
func (m *ServerLoopInQuoteResponse) GetSwapFeeRate() int64 {
if m != nil {
return m.SwapFeeRate
}
return 0
}
func (m *ServerLoopInQuoteResponse) GetMinSwapAmount() uint64 {
if m != nil {
return m.MinSwapAmount
}
return 0
}
func (m *ServerLoopInQuoteResponse) GetMaxSwapAmount() uint64 {
if m != nil {
return m.MaxSwapAmount
}
return 0
}
func (m *ServerLoopInQuoteResponse) GetCltvDelta() int32 {
if m != nil {
return m.CltvDelta
}
return 0
}
func init() { func init() {
proto.RegisterType((*ServerLoopOutRequest)(nil), "looprpc.ServerLoopOutRequest") proto.RegisterType((*ServerLoopOutRequest)(nil), "looprpc.ServerLoopOutRequest")
proto.RegisterType((*ServerLoopOutResponse)(nil), "looprpc.ServerLoopOutResponse") proto.RegisterType((*ServerLoopOutResponse)(nil), "looprpc.ServerLoopOutResponse")
proto.RegisterType((*ServerLoopOutQuoteRequest)(nil), "looprpc.ServerLoopOutQuoteRequest") proto.RegisterType((*ServerLoopOutQuoteRequest)(nil), "looprpc.ServerLoopOutQuoteRequest")
proto.RegisterType((*ServerLoopOutQuote)(nil), "looprpc.ServerLoopOutQuote") proto.RegisterType((*ServerLoopOutQuote)(nil), "looprpc.ServerLoopOutQuote")
proto.RegisterType((*ServerLoopInRequest)(nil), "looprpc.ServerLoopInRequest")
proto.RegisterType((*ServerLoopInResponse)(nil), "looprpc.ServerLoopInResponse")
proto.RegisterType((*ServerLoopInQuoteRequest)(nil), "looprpc.ServerLoopInQuoteRequest")
proto.RegisterType((*ServerLoopInQuoteResponse)(nil), "looprpc.ServerLoopInQuoteResponse")
} }
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.
@ -277,6 +489,8 @@ const _ = grpc.SupportPackageIsVersion4
type SwapServerClient interface { type SwapServerClient interface {
NewLoopOutSwap(ctx context.Context, in *ServerLoopOutRequest, opts ...grpc.CallOption) (*ServerLoopOutResponse, error) NewLoopOutSwap(ctx context.Context, in *ServerLoopOutRequest, opts ...grpc.CallOption) (*ServerLoopOutResponse, error)
LoopOutQuote(ctx context.Context, in *ServerLoopOutQuoteRequest, opts ...grpc.CallOption) (*ServerLoopOutQuote, error) LoopOutQuote(ctx context.Context, in *ServerLoopOutQuoteRequest, opts ...grpc.CallOption) (*ServerLoopOutQuote, error)
NewLoopInSwap(ctx context.Context, in *ServerLoopInRequest, opts ...grpc.CallOption) (*ServerLoopInResponse, error)
LoopInQuote(ctx context.Context, in *ServerLoopInQuoteRequest, opts ...grpc.CallOption) (*ServerLoopInQuoteResponse, error)
} }
type swapServerClient struct { type swapServerClient struct {
@ -305,10 +519,30 @@ func (c *swapServerClient) LoopOutQuote(ctx context.Context, in *ServerLoopOutQu
return out, nil return out, nil
} }
func (c *swapServerClient) NewLoopInSwap(ctx context.Context, in *ServerLoopInRequest, opts ...grpc.CallOption) (*ServerLoopInResponse, error) {
out := new(ServerLoopInResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapServer/NewLoopInSwap", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapServerClient) LoopInQuote(ctx context.Context, in *ServerLoopInQuoteRequest, opts ...grpc.CallOption) (*ServerLoopInQuoteResponse, error) {
out := new(ServerLoopInQuoteResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapServer/LoopInQuote", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapServerServer is the server API for SwapServer service. // SwapServerServer is the server API for SwapServer service.
type SwapServerServer interface { type SwapServerServer interface {
NewLoopOutSwap(context.Context, *ServerLoopOutRequest) (*ServerLoopOutResponse, error) NewLoopOutSwap(context.Context, *ServerLoopOutRequest) (*ServerLoopOutResponse, error)
LoopOutQuote(context.Context, *ServerLoopOutQuoteRequest) (*ServerLoopOutQuote, error) LoopOutQuote(context.Context, *ServerLoopOutQuoteRequest) (*ServerLoopOutQuote, error)
NewLoopInSwap(context.Context, *ServerLoopInRequest) (*ServerLoopInResponse, error)
LoopInQuote(context.Context, *ServerLoopInQuoteRequest) (*ServerLoopInQuoteResponse, error)
} }
func RegisterSwapServerServer(s *grpc.Server, srv SwapServerServer) { func RegisterSwapServerServer(s *grpc.Server, srv SwapServerServer) {
@ -351,6 +585,42 @@ func _SwapServer_LoopOutQuote_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _SwapServer_NewLoopInSwap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerLoopInRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapServerServer).NewLoopInSwap(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapServer/NewLoopInSwap",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapServerServer).NewLoopInSwap(ctx, req.(*ServerLoopInRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapServer_LoopInQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerLoopInQuoteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapServerServer).LoopInQuote(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapServer/LoopInQuote",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapServerServer).LoopInQuote(ctx, req.(*ServerLoopInQuoteRequest))
}
return interceptor(ctx, in, info, handler)
}
var _SwapServer_serviceDesc = grpc.ServiceDesc{ var _SwapServer_serviceDesc = grpc.ServiceDesc{
ServiceName: "looprpc.SwapServer", ServiceName: "looprpc.SwapServer",
HandlerType: (*SwapServerServer)(nil), HandlerType: (*SwapServerServer)(nil),
@ -363,43 +633,58 @@ var _SwapServer_serviceDesc = grpc.ServiceDesc{
MethodName: "LoopOutQuote", MethodName: "LoopOutQuote",
Handler: _SwapServer_LoopOutQuote_Handler, Handler: _SwapServer_LoopOutQuote_Handler,
}, },
{
MethodName: "NewLoopInSwap",
Handler: _SwapServer_NewLoopInSwap_Handler,
},
{
MethodName: "LoopInQuote",
Handler: _SwapServer_LoopInQuote_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "server.proto", Metadata: "server.proto",
} }
func init() { proto.RegisterFile("server.proto", fileDescriptor_server_76a33eec530f39a2) } func init() { proto.RegisterFile("server.proto", fileDescriptor_server_1f6c9db5f7136644) }
var fileDescriptor_server_76a33eec530f39a2 = []byte{ var fileDescriptor_server_1f6c9db5f7136644 = []byte{
// 467 bytes of a gzipped FileDescriptorProto // 589 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0xd1, 0x6e, 0xd3, 0x30, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xdd, 0x6e, 0xd3, 0x4c,
0x14, 0x86, 0x95, 0xb6, 0xeb, 0xe8, 0x59, 0xbb, 0x81, 0x05, 0x28, 0xb4, 0x14, 0x95, 0x48, 0x83, 0x14, 0x94, 0x9d, 0x34, 0xfd, 0x72, 0x9a, 0xb4, 0x1f, 0xcb, 0x8f, 0x4c, 0xda, 0xa0, 0xd6, 0x52,
0x8a, 0x8b, 0x56, 0x82, 0x27, 0xd8, 0x34, 0x21, 0x10, 0x88, 0x42, 0x76, 0xc7, 0x4d, 0x74, 0xda, 0xa1, 0xe2, 0x22, 0x91, 0xe0, 0x09, 0x5a, 0x55, 0x88, 0x8a, 0x8a, 0x52, 0x97, 0x2b, 0x6e, 0xac,
0x1e, 0x12, 0x8b, 0xc4, 0x36, 0xb1, 0xd3, 0x36, 0x2f, 0x82, 0x78, 0x0a, 0x9e, 0x11, 0xd9, 0xf1, 0x4d, 0x72, 0x88, 0x2d, 0xec, 0xdd, 0xc5, 0xbb, 0xf9, 0x7b, 0x01, 0x1e, 0x01, 0xf1, 0x5c, 0x7d,
0x60, 0x85, 0xb1, 0xbb, 0xe4, 0x3f, 0xdf, 0xf1, 0xf9, 0xfd, 0xe7, 0x04, 0xfa, 0x9a, 0xca, 0x0d, 0x22, 0xb4, 0xeb, 0x4d, 0x62, 0x27, 0xe9, 0x0f, 0x77, 0xed, 0x9c, 0xf1, 0xd9, 0xd9, 0x99, 0xc9,
0x95, 0x33, 0x55, 0x4a, 0x23, 0xd9, 0x61, 0x2e, 0xa5, 0x2a, 0xd5, 0x6a, 0xf8, 0x38, 0x95, 0x32, 0x42, 0x43, 0x62, 0x36, 0xc6, 0xac, 0x23, 0x32, 0xae, 0x38, 0xd9, 0x4e, 0x38, 0x17, 0x99, 0xe8,
0xcd, 0x69, 0x8e, 0x8a, 0xcf, 0x51, 0x08, 0x69, 0xd0, 0x70, 0x29, 0x74, 0x83, 0x45, 0x19, 0xdc, 0xb7, 0x0e, 0x86, 0x9c, 0x0f, 0x13, 0xec, 0x52, 0x11, 0x77, 0x29, 0x63, 0x5c, 0x51, 0x15, 0x73,
0xbf, 0x74, 0x6d, 0xef, 0xa5, 0x54, 0x8b, 0xca, 0xc4, 0xf4, 0xad, 0x22, 0x6d, 0xd8, 0x53, 0xe8, 0x26, 0x73, 0x9a, 0x1f, 0xc1, 0xb3, 0x1b, 0xf3, 0xd9, 0x25, 0xe7, 0xe2, 0x6a, 0xa4, 0x02, 0xfc,
0x97, 0xb4, 0x22, 0xbe, 0xa1, 0x32, 0xf9, 0x4a, 0x75, 0x18, 0x4c, 0x82, 0x69, 0x3f, 0x3e, 0xba, 0x39, 0x42, 0xa9, 0xc8, 0x11, 0x34, 0x32, 0xec, 0x63, 0x3c, 0xc6, 0x2c, 0xfc, 0x81, 0x33, 0xcf,
0xd2, 0xde, 0x51, 0xcd, 0x46, 0xd0, 0xd3, 0x5b, 0x54, 0x49, 0x86, 0x3a, 0x0b, 0x5b, 0xae, 0x7e, 0x39, 0x74, 0x4e, 0x1a, 0xc1, 0xce, 0x1c, 0xfb, 0x84, 0x33, 0xb2, 0x0f, 0x75, 0x39, 0xa1, 0x22,
0xc7, 0x0a, 0x6f, 0x50, 0x67, 0xec, 0x2e, 0xb4, 0xb1, 0x30, 0x61, 0x7b, 0x12, 0x4c, 0x3b, 0xb1, 0x8c, 0xa8, 0x8c, 0x3c, 0xd7, 0xcc, 0xff, 0xd3, 0xc0, 0x47, 0x2a, 0x23, 0xf2, 0x3f, 0x54, 0x68,
0x7d, 0x8c, 0x7e, 0x04, 0xf0, 0xe0, 0xaf, 0x51, 0x5a, 0x49, 0xa1, 0xc9, 0xce, 0x72, 0x07, 0x71, 0xaa, 0xbc, 0xca, 0xa1, 0x73, 0x52, 0x0d, 0xf4, 0x9f, 0xfe, 0x1f, 0x07, 0x9e, 0xaf, 0x1c, 0x25,
0xb1, 0x91, 0x7c, 0x45, 0x6e, 0x56, 0x2f, 0x3e, 0xb2, 0xda, 0xdb, 0x46, 0x62, 0xa7, 0x70, 0xac, 0x05, 0x67, 0x12, 0xf5, 0x59, 0x66, 0x51, 0xcc, 0xc6, 0x3c, 0xee, 0xa3, 0x39, 0xab, 0x1e, 0xec,
0x4a, 0x52, 0x58, 0xff, 0x86, 0x5a, 0x0e, 0x1a, 0x34, 0xea, 0x15, 0x36, 0x06, 0xd0, 0x24, 0xd6, 0x68, 0xec, 0x22, 0x87, 0xc8, 0x31, 0xec, 0x8a, 0x0c, 0x05, 0x9d, 0x2d, 0x48, 0xae, 0x21, 0x35,
0xde, 0x73, 0xdb, 0x79, 0xea, 0x35, 0x8a, 0x75, 0xfc, 0x10, 0xba, 0xb4, 0x53, 0xbc, 0xac, 0xc3, 0x73, 0x74, 0x4e, 0x6b, 0x03, 0x48, 0x64, 0x03, 0xab, 0xb9, 0x62, 0x34, 0xd5, 0x73, 0x44, 0x2b,
0xce, 0x24, 0x98, 0x1e, 0xc4, 0xfe, 0x2d, 0x1a, 0xc1, 0xa3, 0x3d, 0x67, 0x9f, 0x2a, 0x69, 0xc8, 0x7e, 0x01, 0x35, 0x9c, 0x8a, 0x38, 0x9b, 0x79, 0xd5, 0x43, 0xe7, 0x64, 0x2b, 0xb0, 0xff, 0xf9,
0x27, 0x11, 0x7d, 0x6f, 0x01, 0xfb, 0xb7, 0xca, 0x5e, 0xc0, 0x3d, 0x67, 0x5a, 0x61, 0x5d, 0x90, 0xfb, 0xf0, 0xb2, 0xa4, 0xec, 0x7a, 0xc4, 0x15, 0x5a, 0x27, 0xfc, 0xdf, 0x2e, 0x90, 0xf5, 0x29,
0x30, 0xc9, 0x9a, 0xb4, 0xf1, 0xce, 0x4f, 0x6c, 0xe1, 0x63, 0xa3, 0x5f, 0xd8, 0x30, 0x23, 0x18, 0x79, 0x0b, 0x4f, 0x8c, 0x68, 0x41, 0x67, 0x29, 0x32, 0x15, 0x0e, 0x50, 0x2a, 0xab, 0x7c, 0x4f,
0x38, 0xf6, 0x0b, 0x51, 0xb2, 0x44, 0xdd, 0x98, 0x6f, 0x37, 0x37, 0x7c, 0x4d, 0x74, 0x8e, 0x9a, 0x0f, 0xbe, 0xe4, 0xf8, 0xb9, 0x36, 0xd3, 0x87, 0xa6, 0xe1, 0x7e, 0x47, 0x0c, 0x7b, 0x54, 0xe6,
0xf6, 0x98, 0x12, 0x0d, 0x39, 0xf7, 0x7f, 0x98, 0x18, 0x8d, 0xbb, 0x9e, 0x4f, 0xc1, 0x66, 0xdb, 0xe2, 0x2b, 0xf9, 0x0d, 0x3f, 0x20, 0x9e, 0x51, 0x89, 0x25, 0x4e, 0x46, 0x15, 0x1a, 0xf5, 0x4b,
0x71, 0xd9, 0xf6, 0x1a, 0xe5, 0xac, 0x30, 0xec, 0x19, 0x9c, 0x14, 0x5c, 0x24, 0xee, 0x18, 0x2c, 0x4e, 0x40, 0x95, 0xb9, 0x9e, 0x75, 0x41, 0x7b, 0x5b, 0x35, 0xde, 0xd6, 0x73, 0xe4, 0x34, 0x55,
0x64, 0x25, 0x4c, 0x78, 0xe0, 0x98, 0x41, 0xc1, 0xc5, 0xe5, 0x16, 0xd5, 0x99, 0x13, 0x1d, 0x87, 0xe4, 0x35, 0xec, 0xa5, 0x31, 0x0b, 0xcd, 0x1a, 0x9a, 0xf2, 0x11, 0x53, 0xde, 0x96, 0xe1, 0x34,
0xbb, 0x3d, 0xae, 0xeb, 0x39, 0xdc, 0x5d, 0xe3, 0xc6, 0x00, 0xab, 0xdc, 0x6c, 0x92, 0x35, 0xe5, 0xd3, 0x98, 0xdd, 0x4c, 0xa8, 0x38, 0x35, 0xa0, 0xe1, 0xd1, 0x69, 0x89, 0x57, 0xb3, 0x3c, 0x3a,
0x06, 0xc3, 0x43, 0x17, 0x59, 0xcf, 0x2a, 0x17, 0x56, 0x78, 0xf9, 0x33, 0x00, 0xb0, 0x74, 0x13, 0x2d, 0xf0, 0xda, 0x00, 0xfd, 0x44, 0x8d, 0xc3, 0x01, 0x26, 0x8a, 0x7a, 0xdb, 0xc6, 0xb2, 0xba,
0x0e, 0x5b, 0xc0, 0xf1, 0x07, 0xda, 0xfa, 0x8c, 0xac, 0xce, 0xc6, 0x33, 0xbf, 0x83, 0xb3, 0x9b, 0x46, 0xce, 0x35, 0xe0, 0xff, 0x72, 0xe0, 0xe9, 0xd2, 0x98, 0x0b, 0x36, 0xaf, 0x4e, 0x39, 0x04,
0x56, 0x6c, 0xf8, 0xe4, 0x7f, 0x65, 0xbf, 0x16, 0x0b, 0xe8, 0xef, 0x25, 0x1e, 0xdd, 0xcc, 0x5f, 0x67, 0x35, 0x84, 0x7f, 0xab, 0xcd, 0x5a, 0x39, 0xaa, 0x6b, 0xe5, 0xf0, 0xaf, 0x8b, 0x1d, 0xd6,
0xff, 0x58, 0xc3, 0xd1, 0x2d, 0xcc, 0xf9, 0xf3, 0xcf, 0xa7, 0x29, 0x37, 0x59, 0xb5, 0x9c, 0xad, 0x3a, 0x96, 0xbd, 0x7a, 0xa8, 0xc3, 0xcb, 0x46, 0xb8, 0xa5, 0x46, 0xb4, 0xc0, 0x2b, 0xae, 0x2c,
0x64, 0x31, 0xcf, 0x79, 0x9a, 0x19, 0xc1, 0x45, 0x9a, 0xe3, 0x52, 0xcf, 0x6d, 0xdb, 0xdc, 0xf7, 0x15, 0xe2, 0xd6, 0x29, 0xd6, 0x65, 0x31, 0xb4, 0x87, 0xae, 0x65, 0xed, 0x3c, 0x22, 0x6b, 0x77,
0x2e, 0xbb, 0xee, 0xdf, 0x78, 0xf5, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x94, 0x23, 0x1c, 0x6e, 0x52, 0x3d, 0xeb, 0x0d, 0x61, 0x56, 0x1f, 0x19, 0xe6, 0xd6, 0xc3, 0x61, 0xd6, 0x56, 0xc2, 0x7c, 0x77,
0x03, 0x00, 0x00, 0xeb, 0x02, 0x68, 0x76, 0x7e, 0x31, 0x72, 0x05, 0xbb, 0x9f, 0x71, 0x62, 0x0b, 0xaf, 0x71, 0xd2,
0xee, 0xd8, 0x07, 0xa5, 0xb3, 0xe9, 0xbd, 0x68, 0xbd, 0xba, 0x6b, 0x6c, 0x6d, 0xb9, 0x82, 0x46,
0xe9, 0xe7, 0xe3, 0x6f, 0xe6, 0x17, 0x8d, 0x6e, 0xed, 0xdf, 0xc3, 0x21, 0x97, 0xd0, 0xb4, 0x0a,
0x2f, 0x8c, 0x1d, 0xe4, 0x60, 0x03, 0x7b, 0x51, 0xca, 0x56, 0xfb, 0x8e, 0xa9, 0x95, 0xf7, 0x15,
0x76, 0x0a, 0x61, 0x92, 0xa3, 0x8d, 0xec, 0x92, 0x38, 0xff, 0x3e, 0x4a, 0xbe, 0xf5, 0xec, 0xcd,
0xb7, 0xe3, 0x61, 0xac, 0xa2, 0x51, 0xaf, 0xd3, 0xe7, 0x69, 0x37, 0x89, 0x87, 0x91, 0x62, 0x31,
0x1b, 0x26, 0xb4, 0x27, 0xbb, 0xfa, 0xeb, 0xae, 0x5d, 0xd1, 0xab, 0x99, 0xc7, 0xf8, 0xfd, 0xdf,
0x00, 0x00, 0x00, 0xff, 0xff, 0x94, 0xad, 0x2a, 0x29, 0xc3, 0x05, 0x00, 0x00,
} }

@ -10,6 +10,10 @@ service SwapServer {
rpc NewLoopOutSwap(ServerLoopOutRequest) returns (ServerLoopOutResponse); rpc NewLoopOutSwap(ServerLoopOutRequest) returns (ServerLoopOutResponse);
rpc LoopOutQuote(ServerLoopOutQuoteRequest) returns (ServerLoopOutQuote); rpc LoopOutQuote(ServerLoopOutQuoteRequest) returns (ServerLoopOutQuote);
rpc NewLoopInSwap(ServerLoopInRequest) returns (ServerLoopInResponse);
rpc LoopInQuote(ServerLoopInQuoteRequest) returns (ServerLoopInQuoteResponse);
} }
message ServerLoopOutRequest { message ServerLoopOutRequest {
@ -48,3 +52,26 @@ message ServerLoopOutQuote {
int32 cltv_delta = 7; int32 cltv_delta = 7;
} }
message ServerLoopInRequest {
bytes sender_key = 1;
bytes swap_hash = 2;
uint64 amt = 3;
string swap_invoice = 4;
}
message ServerLoopInResponse {
bytes receiver_key = 1;
int32 expiry = 2;
}
message ServerLoopInQuoteRequest {
}
message ServerLoopInQuoteResponse {
int64 swap_fee_base = 1;
int64 swap_fee_rate = 2;
uint64 min_swap_amount = 4;
uint64 max_swap_amount = 5;
int32 cltv_delta = 6;
}

@ -19,6 +19,7 @@ var (
testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
testLoopOutOnChainCltvDelta = int32(30) testLoopOutOnChainCltvDelta = int32(30)
testChargeOnChainCltvDelta = int32(100)
testCltvDelta = 50 testCltvDelta = 50
testSwapFeeBase = btcutil.Amount(21) testSwapFeeBase = btcutil.Amount(21)
testSwapFeeRate = int64(100) testSwapFeeRate = int64(100)
@ -123,3 +124,40 @@ func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, err
return reqString, nil return reqString, nil
} }
func (s *serverMock) NewLoopInSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount,
senderKey [33]byte, swapInvoice string) (
*newLoopInResponse, error) {
_, receiverKey := test.CreateKey(101)
if amount != s.expectedSwapAmt {
return nil, errors.New("unexpected test swap amount")
}
var receiverKeyArray [33]byte
copy(receiverKeyArray[:], receiverKey.SerializeCompressed())
s.swapInvoice = swapInvoice
s.swapHash = swapHash
resp := &newLoopInResponse{
expiry: s.height + testChargeOnChainCltvDelta,
receiverKey: receiverKeyArray,
}
return resp, nil
}
func (s *serverMock) GetLoopInTerms(ctx context.Context) (
*LoopInTerms, error) {
return &LoopInTerms{
SwapFeeBase: testSwapFeeBase,
SwapFeeRate: testSwapFeeRate,
CltvDelta: testChargeOnChainCltvDelta,
MinSwapAmount: testMinSwapAmount,
MaxSwapAmount: testMaxSwapAmount,
}, nil
}

@ -20,10 +20,18 @@ type swapServerClient interface {
GetLoopOutTerms(ctx context.Context) ( GetLoopOutTerms(ctx context.Context) (
*LoopOutTerms, error) *LoopOutTerms, error)
GetLoopInTerms(ctx context.Context) (
*LoopInTerms, error)
NewLoopOutSwap(ctx context.Context, NewLoopOutSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, swapHash lntypes.Hash, amount btcutil.Amount,
receiverKey [33]byte) ( receiverKey [33]byte) (
*newLoopOutResponse, error) *newLoopOutResponse, error)
NewLoopInSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount,
senderKey [33]byte, swapInvoice string) (
*newLoopInResponse, error)
} }
type grpcSwapServerClient struct { type grpcSwapServerClient struct {
@ -80,6 +88,27 @@ func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) (
}, nil }, nil
} }
func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context) (
*LoopInTerms, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
defer rpcCancel()
quoteResp, err := s.server.LoopInQuote(rpcCtx,
&looprpc.ServerLoopInQuoteRequest{},
)
if err != nil {
return nil, err
}
return &LoopInTerms{
MinSwapAmount: btcutil.Amount(quoteResp.MinSwapAmount),
MaxSwapAmount: btcutil.Amount(quoteResp.MaxSwapAmount),
SwapFeeBase: btcutil.Amount(quoteResp.SwapFeeBase),
SwapFeeRate: quoteResp.SwapFeeRate,
CltvDelta: quoteResp.CltvDelta,
}, nil
}
func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context, func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, swapHash lntypes.Hash, amount btcutil.Amount,
receiverKey [33]byte) (*newLoopOutResponse, error) { receiverKey [33]byte) (*newLoopOutResponse, error) {
@ -114,6 +143,39 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
}, nil }, nil
} }
func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, senderKey [33]byte,
swapInvoice string) (*newLoopInResponse, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
defer rpcCancel()
swapResp, err := s.server.NewLoopInSwap(rpcCtx,
&looprpc.ServerLoopInRequest{
SwapHash: swapHash[:],
Amt: uint64(amount),
SenderKey: senderKey[:],
SwapInvoice: swapInvoice,
},
)
if err != nil {
return nil, err
}
var receiverKey [33]byte
copy(receiverKey[:], swapResp.ReceiverKey)
// Validate receiver key.
_, err = btcec.ParsePubKey(receiverKey[:], btcec.S256())
if err != nil {
return nil, fmt.Errorf("invalid sender key: %v", err)
}
return &newLoopInResponse{
receiverKey: receiverKey,
expiry: swapResp.Expiry,
}, nil
}
func (s *grpcSwapServerClient) Close() { func (s *grpcSwapServerClient) Close() {
s.conn.Close() s.conn.Close()
} }
@ -143,3 +205,8 @@ type newLoopOutResponse struct {
senderKey [33]byte senderKey [33]byte
expiry int32 expiry int32
} }
type newLoopInResponse struct {
receiverKey [33]byte
expiry int32
}

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32" "github.com/lightningnetwork/lnd/zpay32"
@ -140,7 +141,9 @@ func (ctx *Context) AssertPaid(
done := func(result error) { done := func(result error) {
select { select {
case swapPayment.Done <- result: case swapPayment.Done <- lndclient.PaymentResult{
Err: result,
}:
case <-time.After(Timeout): case <-time.After(Timeout):
ctx.T.Fatalf("payment result not consumed") ctx.T.Fatalf("payment result not consumed")
} }
@ -206,6 +209,7 @@ func (ctx *Context) DecodeInvoice(request string) *zpay32.Invoice {
return payReq return payReq
} }
// GetOutputIndex returns the index in the tx outs of the given script hash.
func (ctx *Context) GetOutputIndex(tx *wire.MsgTx, func (ctx *Context) GetOutputIndex(tx *wire.MsgTx,
script []byte) int { script []byte) int {
@ -226,15 +230,4 @@ func (ctx *Context) NotifyServerHeight(height int32) {
if err := ctx.Lnd.NotifyHeight(height); err != nil { if err := ctx.Lnd.NotifyHeight(height); err != nil {
ctx.T.Fatal(err) ctx.T.Fatal(err)
} }
// TODO: Fix race condition with height not processed yet.
// select {
// case h := <-ctx.swapServer.testEpochChan:
// if h != height {
// ctx.T.Fatal("height not set")
// }
// case <-time.After(test.Timeout):
// ctx.T.Fatal("no height response")
// }
} }

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -43,10 +43,10 @@ func (s *mockInvoices) CancelInvoice(ctx context.Context,
} }
func (s *mockInvoices) SubscribeSingleInvoice(ctx context.Context, func (s *mockInvoices) SubscribeSingleInvoice(ctx context.Context,
hash lntypes.Hash) (<-chan channeldb.ContractState, hash lntypes.Hash) (<-chan lndclient.InvoiceUpdate,
<-chan error, error) { <-chan error, error) {
updateChan := make(chan channeldb.ContractState, 2) updateChan := make(chan lndclient.InvoiceUpdate, 2)
errChan := make(chan error) errChan := make(chan error)
select { select {

@ -1,15 +1,14 @@
package test package test
import ( import (
"crypto/rand"
"fmt" "fmt"
"sync" "sync"
"time" "time"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -29,44 +28,9 @@ func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string,
done := make(chan lndclient.PaymentResult, 1) done := make(chan lndclient.PaymentResult, 1)
mockChan := make(chan error)
h.wg.Add(1)
go func() {
defer h.wg.Done()
amt, err := swap.GetInvoiceAmt(&chaincfg.TestNet3Params, invoice)
if err != nil {
select {
case done <- lndclient.PaymentResult{
Err: err,
}:
case <-ctx.Done():
}
return
}
var paidFee btcutil.Amount
err = <-mockChan
if err != nil {
amt = 0
} else {
paidFee = 1
}
select {
case done <- lndclient.PaymentResult{
Err: err,
PaidFee: paidFee,
PaidAmt: amt,
}:
case <-ctx.Done():
}
}()
h.lnd.SendPaymentChannel <- PaymentChannelMessage{ h.lnd.SendPaymentChannel <- PaymentChannelMessage{
PaymentRequest: invoice, PaymentRequest: invoice,
Done: mockChan, Done: done,
} }
return done return done
@ -92,10 +56,11 @@ func (h *mockLightningClient) GetInfo(ctx context.Context) (*lndclient.Info,
}, nil }, nil
} }
func (h *mockLightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) ( func (h *mockLightningClient) EstimateFeeToP2WSH(ctx context.Context,
lnwire.MilliSatoshi, error) { amt btcutil.Amount, confTarget int32) (btcutil.Amount,
error) {
return 0, nil return 3000, nil
} }
func (h *mockLightningClient) AddInvoice(ctx context.Context, func (h *mockLightningClient) AddInvoice(ctx context.Context,
@ -105,10 +70,15 @@ func (h *mockLightningClient) AddInvoice(ctx context.Context,
defer h.lnd.lock.Unlock() defer h.lnd.lock.Unlock()
var hash lntypes.Hash var hash lntypes.Hash
if in.Hash != nil { switch {
case in.Hash != nil:
hash = *in.Hash hash = *in.Hash
} else { case in.Preimage != nil:
hash = (*in.Preimage).Hash() hash = (*in.Preimage).Hash()
default:
if _, err := rand.Read(hash[:]); err != nil {
return lntypes.Hash{}, "", err
}
} }
// Create and encode the payment request as a bech32 (zpay32) string. // Create and encode the payment request as a bech32 (zpay32) string.

@ -12,7 +12,6 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
) )
var testStartingHeight = int32(600) var testStartingHeight = int32(600)
@ -67,13 +66,13 @@ func NewMockLnd() *LndMockServices {
// PaymentChannelMessage is the data that passed through SendPaymentChannel. // PaymentChannelMessage is the data that passed through SendPaymentChannel.
type PaymentChannelMessage struct { type PaymentChannelMessage struct {
PaymentRequest string PaymentRequest string
Done chan error Done chan lndclient.PaymentResult
} }
// SingleInvoiceSubscription contains the single invoice subscribers // SingleInvoiceSubscription contains the single invoice subscribers
type SingleInvoiceSubscription struct { type SingleInvoiceSubscription struct {
Hash lntypes.Hash Hash lntypes.Hash
Update chan channeldb.ContractState Update chan lndclient.InvoiceUpdate
Err chan error Err chan error
} }

@ -3,14 +3,17 @@ package test
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/zpay32"
"os" "os"
"runtime/pprof" "runtime/pprof"
"testing" "testing"
"time" "time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
) )
var ( var (
@ -20,6 +23,8 @@ var (
// ErrTimeout is returned on timeout. // ErrTimeout is returned on timeout.
ErrTimeout = errors.New("test timeout") ErrTimeout = errors.New("test timeout")
testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
) )
// GetDestAddr deterministically generates a sweep address for testing. // GetDestAddr deterministically generates a sweep address for testing.
@ -63,6 +68,27 @@ func EncodePayReq(payReq *zpay32.Invoice) (string, error) {
return reqString, nil return reqString, nil
} }
// GetInvoice creates a testnet payment request with the given parameters.
func GetInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (
string, error) {
req, err := zpay32.NewInvoice(
&chaincfg.TestNet3Params, hash, testTime,
zpay32.Description(memo),
zpay32.Amount(lnwire.NewMSatFromSatoshis(amt)),
)
if err != nil {
return "", err
}
reqString, err := EncodePayReq(req)
if err != nil {
return "", err
}
return reqString, nil
}
// DumpGoroutines dumps all currently running goroutines. // DumpGoroutines dumps all currently running goroutines.
func DumpGoroutines() { func DumpGoroutines() {
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)

@ -117,10 +117,12 @@ func createClientTestContext(t *testing.T,
ctx.stop = stop ctx.stop = stop
go func() { go func() {
ctx.runErr <- swapClient.Run( err := swapClient.Run(
runCtx, runCtx,
statusChan, statusChan,
) )
logger.Errorf("client run: %v", err)
ctx.runErr <- err
}() }()
return ctx return ctx

Loading…
Cancel
Save