Merge pull request #34 from joostjager/loopin-merge

Loop In
pull/39/head
Olaoluwa Osuntokun 5 years ago committed by GitHub
commit e8005d095a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -65,7 +65,7 @@ tags that enable the swap. This enables the required lnd rpc services.
```
cd lnd
make install tags="signrpc walletrpc chainrpc"
make install tags="signrpc walletrpc chainrpc invoicesrpc"
```
Check to see if you have already installed lnd. If you have, you will need to

@ -118,6 +118,11 @@ func (s *Client) FetchLoopOutSwaps() ([]*loopdb.LoopOut, error) {
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
// restored from persistent storage and resumed. Subsequent updates will be
// 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
// 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 {
return err
}
@ -154,7 +164,7 @@ func (s *Client) Run(ctx context.Context,
go func() {
defer s.wg.Done()
s.resumeSwaps(mainCtx, pendingSwaps)
s.resumeSwaps(mainCtx, pendingLoopOutSwaps, pendingLoopInSwaps)
// Signal that new requests can be accepted. Otherwise the new
// 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.
func (s *Client) resumeSwaps(ctx context.Context,
swaps []*loopdb.LoopOut) {
loopOutSwaps []*loopdb.LoopOut, loopInSwaps []*loopdb.LoopIn) {
for _, pend := range swaps {
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}
for _, pend := range loopOutSwaps {
if pend.State().Type() != loopdb.StateTypePending {
continue
}
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}
swap, err := resumeLoopOutSwap(ctx, swapCfg, pend)
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
}
@ -224,7 +248,7 @@ func (s *Client) resumeSwaps(ctx context.Context,
//
// The return value is a hash that uniquely identifies the new swap.
func (s *Client) LoopOut(globalCtx context.Context,
request *OutRequest) (*lntypes.Hash, error) {
request *OutRequest) (*lntypes.Hash, btcutil.Address, error) {
logger.Infof("LoopOut %v to %v (channel: %v)",
request.Amount, request.DestAddr,
@ -232,7 +256,7 @@ func (s *Client) LoopOut(globalCtx context.Context,
)
if err := s.waitForInitialized(globalCtx); err != nil {
return nil, err
return nil, nil, err
}
// Create a new swap object for this swap.
@ -246,7 +270,7 @@ func (s *Client) LoopOut(globalCtx context.Context,
globalCtx, swapCfg, initiationHeight, request,
)
if err != nil {
return nil, err
return nil, nil, err
}
// Post swap to the main loop.
@ -254,7 +278,7 @@ func (s *Client) LoopOut(globalCtx context.Context,
// Return hash so that the caller can identify this swap in the updates
// stream.
return &swap.hash, nil
return &swap.hash, swap.htlc.Address, nil
}
// LoopOutQuote takes a LoopOut amount and returns a break down of estimated
@ -283,7 +307,7 @@ func (s *Client) LoopOutQuote(ctx context.Context,
)
minerFee, err := s.sweeper.GetSweepFee(
ctx, swap.QuoteHtlc.MaxSuccessWitnessSize,
ctx, swap.QuoteHtlc.AddSuccessToEstimator,
request.SweepConfTarget,
)
if err != nil {
@ -320,3 +344,85 @@ func (s *Client) waitForInitialized(ctx context.Context) error {
return nil
}
// LoopIn initiates a loop in swap.
func (s *Client) LoopIn(globalCtx context.Context,
request *LoopInRequest) (*lntypes.Hash, btcutil.Address, error) {
logger.Infof("Loop in %v (channel: %v)",
request.Amount,
request.LoopInChannel,
)
if err := s.waitForInitialized(globalCtx); err != nil {
return nil, 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, 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, swap.htlc.Address, 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)
}

@ -43,7 +43,7 @@ func TestSuccess(t *testing.T) {
// Initiate uncharge.
hash, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
hash, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
if err != nil {
t.Fatal(err)
}
@ -70,7 +70,7 @@ func TestFailOffchain(t *testing.T) {
ctx := createClientTestContext(t, nil)
_, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
_, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
if err != nil {
t.Fatal(err)
}
@ -108,7 +108,7 @@ func TestFailWrongAmount(t *testing.T) {
// Modify mock for this subtest.
modifier(ctx.serverMock)
_, err := ctx.swapClient.LoopOut(
_, _, err := ctx.swapClient.LoopOut(
context.Background(), testRequest,
)
if err != expectedErr {
@ -188,6 +188,7 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) {
SwapInvoice: swapPayReq,
SweepConfTarget: 2,
MaxSwapRoutingFee: 70000,
PrepayInvoice: prePayReq,
SwapContract: loopdb.SwapContract{
Preimage: preimage,
AmountRequested: amt,
@ -195,16 +196,17 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) {
ReceiverKey: receiverKey,
SenderKey: senderKey,
MaxSwapFee: 60000,
PrepayInvoice: prePayReq,
MaxMinerFee: 50000,
},
},
Events: []*loopdb.LoopOutEvent{
{
State: state,
Loop: loopdb.Loop{
Events: []*loopdb.LoopEvent{
{
State: state,
},
},
Hash: hash,
},
Hash: hash,
}
if expired {

@ -0,0 +1,101 @@
package main
import (
"context"
"fmt"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop"
"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: "amt",
Usage: "the amount in satoshis to loop in",
},
cli.BoolFlag{
Name: "external",
Usage: "expect htlc to be published externally",
},
},
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(loop.TypeIn, amt, limits); err != nil {
return err
}
resp, err := client.LoopIn(context.Background(), &looprpc.LoopInRequest{
Amt: int64(amt),
MaxMinerFee: int64(limits.maxMinerFee),
MaxSwapFee: int64(limits.maxSwapFee),
ExternalHtlc: ctx.Bool("external"),
})
if err != nil {
return err
}
fmt.Printf("Swap initiated\n")
fmt.Printf("ID: %v\n", resp.Id)
fmt.Printf("HTLC address: %v\n", resp.HtlcAddress)
fmt.Println()
fmt.Printf("Run `loop monitor` 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),
}
}

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)
@ -84,7 +85,7 @@ func loopOut(ctx *cli.Context) error {
limits := getLimits(amt, quote)
if err := displayLimits(amt, limits); err != nil {
if err := displayLimits(loop.TypeOut, amt, limits); err != nil {
return err
}
@ -97,17 +98,20 @@ func loopOut(ctx *cli.Context) error {
Amt: int64(amt),
Dest: destAddr,
MaxMinerFee: int64(limits.maxMinerFee),
MaxPrepayAmt: int64(limits.maxPrepayAmt),
MaxPrepayAmt: int64(*limits.maxPrepayAmt),
MaxSwapFee: int64(limits.maxSwapFee),
MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee),
MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee),
LoopOutChannel: unchargeChannel,
})
if err != nil {
return err
}
fmt.Printf("Swap initiated with id: %v\n", resp.Id[:8])
fmt.Printf("Swap initiated\n")
fmt.Printf("ID: %v\n", resp.Id)
fmt.Printf("HTLC address: %v\n", resp.HtlcAddress)
fmt.Println()
fmt.Printf("Run `loop monitor` to monitor progress.\n")
return nil

@ -62,7 +62,8 @@ func main() {
},
}
app.Commands = []cli.Command{
loopOutCommand, termsCommand, monitorCommand, quoteCommand,
loopOutCommand, loopInCommand, termsCommand,
monitorCommand, quoteCommand,
}
err := app.Run(os.Args)
@ -88,35 +89,44 @@ func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
}
type limits struct {
maxSwapRoutingFee btcutil.Amount
maxPrepayRoutingFee btcutil.Amount
maxSwapRoutingFee *btcutil.Amount
maxPrepayRoutingFee *btcutil.Amount
maxMinerFee btcutil.Amount
maxSwapFee btcutil.Amount
maxPrepayAmt btcutil.Amount
maxPrepayAmt *btcutil.Amount
}
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{
maxSwapRoutingFee: getMaxRoutingFee(btcutil.Amount(amt)),
maxPrepayRoutingFee: getMaxRoutingFee(btcutil.Amount(
quote.PrepayAmt,
)),
maxSwapRoutingFee: &maxSwapRoutingFee,
maxPrepayRoutingFee: &maxPrepayRoutingFee,
// 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),
maxPrepayAmt: btcutil.Amount(quote.PrepayAmt),
maxPrepayAmt: &maxPrepayAmt,
}
}
func displayLimits(amt btcutil.Amount, l *limits) error {
totalSuccessMax := l.maxSwapRoutingFee + l.maxPrepayRoutingFee +
l.maxMinerFee + l.maxSwapFee
func displayLimits(swapType loop.Type, amt btcutil.Amount, l *limits) error {
totalSuccessMax := 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",
btcutil.Amount(amt), totalSuccessMax,
fmt.Printf("Max swap fees for %d Loop %v: %d\n",
btcutil.Amount(amt), swapType, totalSuccessMax,
)
fmt.Printf("CONTINUE SWAP? (y/n), expand fee detail (x): ")
@ -130,13 +140,22 @@ func displayLimits(amt btcutil.Amount, l *limits) error {
case "x":
fmt.Println()
fmt.Printf("Max on-chain fee: %d\n", l.maxMinerFee)
fmt.Printf("Max off-chain swap routing fee: %d\n",
l.maxSwapRoutingFee)
fmt.Printf("Max off-chain prepay routing fee: %d\n",
l.maxPrepayRoutingFee)
if l.maxSwapRoutingFee != nil {
fmt.Printf("Max off-chain swap routing fee: %d\n",
*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 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.Scanln(&answer)

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

@ -43,11 +43,11 @@ func daemon(config *config) error {
// Before starting the client, build an in-memory view of all swaps.
// This view is used to update newly connected clients with the most
// recent swaps.
storedSwaps, err := swapClient.FetchLoopOutSwaps()
loopOutSwaps, err := swapClient.FetchLoopOutSwaps()
if err != nil {
return err
}
for _, swap := range storedSwaps {
for _, swap := range loopOutSwaps {
swaps[swap.Hash] = loop.SwapInfo{
SwapType: loop.TypeOut,
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.
server := swapClientServer{
impl: swapClient,

@ -2,15 +2,17 @@ package main
import (
"context"
"errors"
"fmt"
"sort"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/looprpc"
@ -18,6 +20,10 @@ import (
const completedSwapsCount = 5
var (
errNoMainnet = errors.New("function not available on mainnet")
)
// swapClientServer implements the grpc service exposed by loopd.
type swapClientServer struct {
impl *loop.Client
@ -32,7 +38,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
in *looprpc.LoopOutRequest) (
*looprpc.SwapResponse, error) {
logger.Infof("LoopOut request received")
logger.Infof("Loop out request received")
var sweepAddr btcutil.Address
if in.Dest == "" {
@ -65,14 +71,15 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
if in.LoopOutChannel != 0 {
req.LoopOutChannel = &in.LoopOutChannel
}
hash, err := s.impl.LoopOut(ctx, req)
hash, htlc, err := s.impl.LoopOut(ctx, req)
if err != nil {
logger.Errorf("LoopOut: %v", err)
return nil, err
}
return &looprpc.SwapResponse{
Id: hash.String(),
Id: hash.String(),
HtlcAddress: htlc.String(),
}, nil
}
@ -85,6 +92,10 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
state = looprpc.SwapState_INITIATED
case loopdb.StatePreimageRevealed:
state = looprpc.SwapState_PREIMAGE_REVEALED
case loopdb.StateHtlcPublished:
state = looprpc.SwapState_HTLC_PUBLISHED
case loopdb.StateInvoiceSettled:
state = looprpc.SwapState_INVOICE_SETTLED
case loopdb.StateSuccess:
state = looprpc.SwapState_SUCCESS
default:
@ -92,17 +103,14 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
state = looprpc.SwapState_FAILED
}
htlc, err := swap.NewHtlc(
loopSwap.CltvExpiry, loopSwap.SenderKey, loopSwap.ReceiverKey,
loopSwap.SwapHash,
)
if err != nil {
return nil, err
}
address, err := htlc.Address(s.lnd.ChainParams)
if err != nil {
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{
@ -111,8 +119,8 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
State: state,
InitiationTime: loopSwap.InitiationTime.UnixNano(),
LastUpdateTime: loopSwap.LastUpdate.UnixNano(),
HtlcAddress: address.EncodeAddress(),
Type: looprpc.SwapType_LOOP_OUT,
HtlcAddress: loopSwap.HtlcAddress.EncodeAddress(),
Type: swapType,
}, nil
}
@ -214,7 +222,7 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
func (s *swapClientServer) LoopOutTerms(ctx context.Context,
req *looprpc.TermsRequest) (*looprpc.TermsResponse, error) {
logger.Infof("Terms request received")
logger.Infof("Loop out terms request received")
terms, err := s.impl.LoopOutTerms(ctx)
if err != nil {
@ -250,3 +258,83 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
SwapFee: int64(quote.SwapFee),
}, 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")
if s.lnd.ChainParams.Name == chaincfg.MainNetParams.Name {
return nil, errNoMainnet
}
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")
if s.lnd.ChainParams.Name == chaincfg.MainNetParams.Name {
return nil, errNoMainnet
}
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")
if s.lnd.ChainParams.Name == chaincfg.MainNetParams.Name {
return nil, errNoMainnet
}
req := &loop.LoopInRequest{
Amount: btcutil.Amount(in.Amt),
MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
HtlcConfTarget: defaultConfTarget,
ExternalHtlc: in.ExternalHtlc,
}
if in.LoopInChannel != 0 {
req.LoopInChannel = &in.LoopInChannel
}
hash, htlc, err := s.impl.LoopIn(ctx, req)
if err != nil {
logger.Errorf("Loop in: %v", err)
return nil, err
}
return &looprpc.SwapResponse{
Id: hash.String(),
HtlcAddress: htlc.String(),
}, nil
}

@ -4,6 +4,8 @@ import (
"fmt"
"strconv"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/swap"
)
@ -28,13 +30,21 @@ func view(config *config) error {
}
defer cleanup()
swaps, err := swapClient.FetchLoopOutSwaps()
if err != nil {
if err := viewOut(swapClient, chainParams); err != nil {
return err
}
if err := viewIn(swapClient, chainParams); err != nil {
return err
}
if len(swaps) == 0 {
fmt.Printf("No swaps\n")
return nil
}
func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error {
swaps, err := swapClient.FetchLoopOutSwaps()
if err != nil {
return err
}
for _, s := range swaps {
@ -42,23 +52,18 @@ func view(config *config) error {
s.Contract.CltvExpiry,
s.Contract.SenderKey,
s.Contract.ReceiverKey,
s.Hash,
s.Hash, swap.HtlcP2WSH, chainParams,
)
if err != nil {
return err
}
htlcAddress, err := htlc.Address(chainParams)
if err != nil {
return err
}
fmt.Printf("%v\n", s.Hash)
fmt.Printf("OUT %v\n", s.Hash)
fmt.Printf(" Created: %v (height %v)\n",
s.Contract.InitiationTime, s.Contract.InitiationHeight,
)
fmt.Printf(" Preimage: %v\n", s.Contract.Preimage)
fmt.Printf(" Htlc address: %v\n", htlcAddress)
fmt.Printf(" Htlc address: %v\n", htlc.Address)
unchargeChannel := "any"
if s.Contract.UnchargeChannel != nil {
@ -81,3 +86,40 @@ func view(config *config) error {
return nil
}
func viewIn(swapClient *loop.Client, chainParams *chaincfg.Params) error {
swaps, err := swapClient.FetchLoopInSwaps()
if err != nil {
return err
}
for _, s := range swaps {
htlc, err := swap.NewHtlc(
s.Contract.CltvExpiry,
s.Contract.SenderKey,
s.Contract.ReceiverKey,
s.Hash, swap.HtlcNP2WSH, chainParams,
)
if err != nil {
return err
}
fmt.Printf("IN %v\n", s.Hash)
fmt.Printf(" Created: %v (height %v)\n",
s.Contract.InitiationTime, s.Contract.InitiationHeight,
)
fmt.Printf(" Preimage: %v\n", s.Contract.Preimage)
fmt.Printf(" Htlc address: %v\n", htlc.Address)
fmt.Printf(" Amt: %v, Expiry: %v\n",
s.Contract.AmountRequested, s.Contract.CltvExpiry,
)
for i, e := range s.Events {
fmt.Printf(" Update %v, Time %v, State: %v\n",
i, e.Time, e.State,
)
}
fmt.Println()
}
return nil
}

@ -158,6 +158,94 @@ type LoopOutTerms struct {
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
// ExternalHtlc specifies whether the htlc is published by an external
// source.
ExternalHtlc bool
}
// 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.
type SwapInfoKit struct {
// Hash is the sha256 hash of the preimage that unlocks the htlcs. It
@ -179,6 +267,17 @@ const (
TypeOut
)
func (t Type) String() string {
switch t {
case TypeIn:
return "In"
case TypeOut:
return "Out"
default:
return "Unknown"
}
}
// SwapInfo exposes common info fields for loop in and loop out swaps.
type SwapInfo struct {
LastUpdate time.Time
@ -190,4 +289,16 @@ type SwapInfo struct {
SwapType Type
loopdb.SwapContract
HtlcAddress btcutil.Address
}
// 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/lnrpc/chainrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ChainNotifierClient exposes base lightning functionality.
@ -101,9 +99,7 @@ func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
for {
spendEvent, err := resp.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
errChan <- err
return
}
@ -125,7 +121,6 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) (
chan *chainntnfs.TxConfirmation, chan error, error) {
// TODO: Height hint
var txidSlice []byte
if txid != nil {
txidSlice = txid[:]
@ -155,9 +150,7 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
var confEvent *chainrpc.ConfEvent
confEvent, err := confStream.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
errChan <- err
return
}
@ -226,9 +219,7 @@ func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) (
for {
epoch, err := blockEpochClient.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
blockErrorChan <- err
}
blockErrorChan <- err
return
}

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

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

@ -20,6 +20,17 @@ type SwapStore interface {
// the various stages in its lifetime.
UpdateLoopOut(hash lntypes.Hash, time time.Time, state SwapState) error
// FetchLoopInSwaps returns all swaps currently in the store.
FetchLoopInSwaps() ([]*LoopIn, error)
// CreateLoopIn adds an initiated swap to the store.
CreateLoopIn(hash lntypes.Hash, swap *LoopInContract) error
// UpdateLoopIn stores a new event for a target loop in swap. This
// appends to the event log for a particular swap as it goes through
// the various stages in its lifetime.
UpdateLoopIn(hash lntypes.Hash, time time.Time, state SwapState) error
// Close closes the underlying database.
Close() error
}

@ -0,0 +1,121 @@
package loopdb
import (
"bytes"
"encoding/binary"
"time"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes"
)
// SwapContract contains the base data that is serialized to persistent storage
// for pending swaps.
type SwapContract struct {
// Preimage is the preimage for the swap.
Preimage lntypes.Preimage
// AmountRequested is the total amount of the swap.
AmountRequested btcutil.Amount
// SenderKey is the key of the sender that will be used in the on-chain
// HTLC.
SenderKey [33]byte
// ReceiverKey is the of the receiver that will be used in the on-chain
// HTLC.
ReceiverKey [33]byte
// CltvExpiry is the total absolute CLTV expiry of the swap.
CltvExpiry int32
// MaxSwapFee is the maximum we are willing to pay the server for the
// swap.
MaxSwapFee btcutil.Amount
// MaxMinerFee is the maximum in on-chain fees that we are willing to
// spend.
MaxMinerFee btcutil.Amount
// InitiationHeight is the block height at which the swap was
// initiated.
InitiationHeight int32
// InitiationTime is the time at which the swap was initiated.
InitiationTime time.Time
}
// Loop contains fields shared between LoopIn and LoopOut
type Loop struct {
Hash lntypes.Hash
Events []*LoopEvent
}
// LoopEvent contains the dynamic data of a swap.
type LoopEvent struct {
// State is the new state for this swap as a result of this event.
State SwapState
// Time is the time that this swap had its state changed.
Time time.Time
}
// State returns the most recent state of this swap.
func (s *Loop) State() SwapState {
lastUpdate := s.LastUpdate()
if lastUpdate == nil {
return StateInitiated
}
return lastUpdate.State
}
// LastUpdate returns the most recent update of this swap.
func (s *Loop) LastUpdate() *LoopEvent {
eventCount := len(s.Events)
if eventCount == 0 {
return nil
}
lastEvent := s.Events[eventCount-1]
return lastEvent
}
// serializeLoopEvent serializes a state update of a swap. This is used for both
// in and out swaps.
func serializeLoopEvent(time time.Time, state SwapState) (
[]byte, error) {
var b bytes.Buffer
if err := binary.Write(&b, byteOrder, time.UnixNano()); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, state); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// deserializeLoopEvent deserializes a state update of a swap. This is used for
// both in and out swaps.
func deserializeLoopEvent(value []byte) (*LoopEvent, error) {
update := &LoopEvent{}
r := bytes.NewReader(value)
var unixNano int64
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
return nil, err
}
update.Time = time.Unix(0, unixNano)
if err := binary.Read(r, byteOrder, &update.State); err != nil {
return nil, err
}
return update, nil
}

@ -0,0 +1,180 @@
package loopdb
import (
"bytes"
"encoding/binary"
"fmt"
"time"
)
// LoopInContract contains the data that is serialized to persistent storage for
// pending loop in swaps.
type LoopInContract struct {
SwapContract
// SweepConfTarget specifies the targeted confirmation target for the
// client sweep tx.
HtlcConfTarget int32
// LoopInChannel is the channel to charge. If zero, any channel may
// be used.
LoopInChannel *uint64
// ExternalHtlc specifies whether the htlc is published by an external
// source.
ExternalHtlc bool
}
// LoopIn is a combination of the contract and the updates.
type LoopIn struct {
Loop
Contract *LoopInContract
}
// LastUpdateTime returns the last update time of this swap.
func (s *LoopIn) LastUpdateTime() time.Time {
lastUpdate := s.LastUpdate()
if lastUpdate == nil {
return s.Contract.InitiationTime
}
return lastUpdate.Time
}
// serializeLoopInContract serialize the loop in contract into a byte slice.
func serializeLoopInContract(swap *LoopInContract) (
[]byte, error) {
var b bytes.Buffer
if err := binary.Write(&b, byteOrder, swap.InitiationTime.UnixNano()); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.Preimage); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.AmountRequested); err != nil {
return nil, err
}
n, err := b.Write(swap.SenderKey[:])
if err != nil {
return nil, err
}
if n != keyLength {
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = b.Write(swap.ReceiverKey[:])
if err != nil {
return nil, err
}
if n != keyLength {
return nil, fmt.Errorf("receiver key has invalid length")
}
if err := binary.Write(&b, byteOrder, swap.CltvExpiry); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.MaxMinerFee); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.MaxSwapFee); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.InitiationHeight); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.HtlcConfTarget); err != nil {
return nil, err
}
var chargeChannel uint64
if swap.LoopInChannel != nil {
chargeChannel = *swap.LoopInChannel
}
if err := binary.Write(&b, byteOrder, chargeChannel); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.ExternalHtlc); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// deserializeLoopInContract deserializes the loop in contract from a byte slice.
func deserializeLoopInContract(value []byte) (*LoopInContract, error) {
r := bytes.NewReader(value)
contract := LoopInContract{}
var err error
var unixNano int64
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
return nil, err
}
contract.InitiationTime = time.Unix(0, unixNano)
if err := binary.Read(r, byteOrder, &contract.Preimage); err != nil {
return nil, err
}
binary.Read(r, byteOrder, &contract.AmountRequested)
n, err := r.Read(contract.SenderKey[:])
if err != nil {
return nil, err
}
if n != keyLength {
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = r.Read(contract.ReceiverKey[:])
if err != nil {
return nil, err
}
if n != keyLength {
return nil, fmt.Errorf("receiver key has invalid length")
}
if err := binary.Read(r, byteOrder, &contract.CltvExpiry); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &contract.MaxMinerFee); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &contract.MaxSwapFee); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &contract.InitiationHeight); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &contract.HtlcConfTarget); err != nil {
return nil, err
}
var loopInChannel uint64
if err := binary.Read(r, byteOrder, &loopInChannel); err != nil {
return nil, err
}
if loopInChannel != 0 {
contract.LoopInChannel = &loopInChannel
}
if err := binary.Read(r, byteOrder, &contract.ExternalHtlc); err != nil {
return nil, err
}
return &contract, nil
}

@ -4,59 +4,13 @@ import (
"bytes"
"encoding/binary"
"fmt"
"io"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes"
)
// SwapContract contains the base data that is serialized to persistent storage
// for pending swaps.
type SwapContract struct {
// Preimage is the preimage for the swap.
Preimage lntypes.Preimage
// AmountRequested is the total amount of the swap.
AmountRequested btcutil.Amount
// PrepayInvoice is the invoice that the client should pay to the
// server that will be returned if the swap is complete.
PrepayInvoice string
// SenderKey is the key of the sender that will be used in the on-chain
// HTLC.
SenderKey [33]byte
// ReceiverKey is the of the receiver that will be used in the on-chain
// HTLC.
ReceiverKey [33]byte
// CltvExpiry is the total absolute CLTV expiry of the swap.
CltvExpiry int32
// MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be
// paid for the prepayment to the server.
MaxPrepayRoutingFee btcutil.Amount
// MaxSwapFee is the maximum we are willing to pay the server for the
// swap.
MaxSwapFee btcutil.Amount
// MaxMinerFee is the maximum in on-chain fees that we are willing to
// spend.
MaxMinerFee btcutil.Amount
// InitiationHeight is the block height at which the swap was
// initiated.
InitiationHeight int32
// InitiationTime is the time at which the swap was initiated.
InitiationTime time.Time
}
// LoopOutContract contains the data that is serialized to persistent storage
// for pending swaps.
type LoopOutContract struct {
@ -83,51 +37,24 @@ type LoopOutContract struct {
// TargetChannel is the channel to loop out. If zero, any channel may
// be used.
UnchargeChannel *uint64
}
// LoopOutEvent contains the dynamic data of a swap.
type LoopOutEvent struct {
// State is the new state for this swap as a result of this event.
State SwapState
// PrepayInvoice is the invoice that the client should pay to the
// server that will be returned if the swap is complete.
PrepayInvoice string
// Time is the time that this swap had its state changed.
Time time.Time
// MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be
// paid for the prepayment to the server.
MaxPrepayRoutingFee btcutil.Amount
}
// LoopOut is a combination of the contract and the updates.
type LoopOut struct {
// Hash is the hash that uniquely identifies this swap.
Hash lntypes.Hash
Loop
// Contract is the active contract for this swap. It describes the
// precise details of the swap including the final fee, CLTV value,
// etc.
Contract *LoopOutContract
// Events are each of the state transitions that this swap underwent.
Events []*LoopOutEvent
}
// State returns the most recent state of this swap.
func (s *LoopOut) State() SwapState {
lastUpdate := s.LastUpdate()
if lastUpdate == nil {
return StateInitiated
}
return lastUpdate.State
}
// LastUpdate returns the most recent update of this swap.
func (s *LoopOut) LastUpdate() *LoopOutEvent {
eventCount := len(s.Events)
if eventCount == 0 {
return nil
}
lastEvent := s.Events[eventCount-1]
return lastEvent
}
// LastUpdateTime returns the last update time of this swap.
@ -145,226 +72,173 @@ func deserializeLoopOutContract(value []byte, chainParams *chaincfg.Params) (
r := bytes.NewReader(value)
contract, err := deserializeContract(r)
if err != nil {
return nil, err
}
swap := LoopOutContract{
SwapContract: *contract,
}
addr, err := wire.ReadVarString(r, 0)
if err != nil {
return nil, err
}
swap.DestAddr, err = btcutil.DecodeAddress(addr, chainParams)
if err != nil {
contract := LoopOutContract{}
var err error
var unixNano int64
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
return nil, err
}
contract.InitiationTime = time.Unix(0, unixNano)
swap.SwapInvoice, err = wire.ReadVarString(r, 0)
if err != nil {
if err := binary.Read(r, byteOrder, &contract.Preimage); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.SweepConfTarget); err != nil {
return nil, err
}
binary.Read(r, byteOrder, &contract.AmountRequested)
if err := binary.Read(r, byteOrder, &swap.MaxSwapRoutingFee); err != nil {
contract.PrepayInvoice, err = wire.ReadVarString(r, 0)
if err != nil {
return nil, err
}
var unchargeChannel uint64
if err := binary.Read(r, byteOrder, &unchargeChannel); err != nil {
n, err := r.Read(contract.SenderKey[:])
if err != nil {
return nil, err
}
if unchargeChannel != 0 {
swap.UnchargeChannel = &unchargeChannel
if n != keyLength {
return nil, fmt.Errorf("sender key has invalid length")
}
return &swap, nil
}
func serializeLoopOutContract(swap *LoopOutContract) (
[]byte, error) {
var b bytes.Buffer
serializeContract(&swap.SwapContract, &b)
addr := swap.DestAddr.String()
if err := wire.WriteVarString(&b, 0, addr); err != nil {
n, err = r.Read(contract.ReceiverKey[:])
if err != nil {
return nil, err
}
if err := wire.WriteVarString(&b, 0, swap.SwapInvoice); err != nil {
return nil, err
if n != keyLength {
return nil, fmt.Errorf("receiver key has invalid length")
}
if err := binary.Write(&b, byteOrder, swap.SweepConfTarget); err != nil {
if err := binary.Read(r, byteOrder, &contract.CltvExpiry); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.MaxSwapRoutingFee); err != nil {
if err := binary.Read(r, byteOrder, &contract.MaxMinerFee); err != nil {
return nil, err
}
var unchargeChannel uint64
if swap.UnchargeChannel != nil {
unchargeChannel = *swap.UnchargeChannel
}
if err := binary.Write(&b, byteOrder, unchargeChannel); err != nil {
if err := binary.Read(r, byteOrder, &contract.MaxSwapFee); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func deserializeContract(r io.Reader) (*SwapContract, error) {
swap := SwapContract{}
var err error
var unixNano int64
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
if err := binary.Read(r, byteOrder, &contract.MaxPrepayRoutingFee); err != nil {
return nil, err
}
swap.InitiationTime = time.Unix(0, unixNano)
if err := binary.Read(r, byteOrder, &swap.Preimage); err != nil {
if err := binary.Read(r, byteOrder, &contract.InitiationHeight); err != nil {
return nil, err
}
binary.Read(r, byteOrder, &swap.AmountRequested)
swap.PrepayInvoice, err = wire.ReadVarString(r, 0)
addr, err := wire.ReadVarString(r, 0)
if err != nil {
return nil, err
}
n, err := r.Read(swap.SenderKey[:])
contract.DestAddr, err = btcutil.DecodeAddress(addr, chainParams)
if err != nil {
return nil, err
}
if n != keyLength {
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = r.Read(swap.ReceiverKey[:])
contract.SwapInvoice, err = wire.ReadVarString(r, 0)
if err != nil {
return nil, err
}
if n != keyLength {
return nil, fmt.Errorf("receiver key has invalid length")
}
if err := binary.Read(r, byteOrder, &swap.CltvExpiry); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.MaxMinerFee); err != nil {
if err := binary.Read(r, byteOrder, &contract.SweepConfTarget); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.MaxSwapFee); err != nil {
if err := binary.Read(r, byteOrder, &contract.MaxSwapRoutingFee); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.MaxPrepayRoutingFee); err != nil {
var unchargeChannel uint64
if err := binary.Read(r, byteOrder, &unchargeChannel); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.InitiationHeight); err != nil {
return nil, err
if unchargeChannel != 0 {
contract.UnchargeChannel = &unchargeChannel
}
return &swap, nil
return &contract, nil
}
func serializeContract(swap *SwapContract, b *bytes.Buffer) error {
if err := binary.Write(b, byteOrder, swap.InitiationTime.UnixNano()); err != nil {
return err
func serializeLoopOutContract(swap *LoopOutContract) (
[]byte, error) {
var b bytes.Buffer
if err := binary.Write(&b, byteOrder, swap.InitiationTime.UnixNano()); err != nil {
return nil, err
}
if err := binary.Write(b, byteOrder, swap.Preimage); err != nil {
return err
if err := binary.Write(&b, byteOrder, swap.Preimage); err != nil {
return nil, err
}
if err := binary.Write(b, byteOrder, swap.AmountRequested); err != nil {
return err
if err := binary.Write(&b, byteOrder, swap.AmountRequested); err != nil {
return nil, err
}
if err := wire.WriteVarString(b, 0, swap.PrepayInvoice); err != nil {
return err
if err := wire.WriteVarString(&b, 0, swap.PrepayInvoice); err != nil {
return nil, err
}
n, err := b.Write(swap.SenderKey[:])
if err != nil {
return err
return nil, err
}
if n != keyLength {
return fmt.Errorf("sender key has invalid length")
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = b.Write(swap.ReceiverKey[:])
if err != nil {
return err
return nil, err
}
if n != keyLength {
return fmt.Errorf("receiver key has invalid length")
return nil, fmt.Errorf("receiver key has invalid length")
}
if err := binary.Write(b, byteOrder, swap.CltvExpiry); err != nil {
return err
if err := binary.Write(&b, byteOrder, swap.CltvExpiry); err != nil {
return nil, err
}
if err := binary.Write(b, byteOrder, swap.MaxMinerFee); err != nil {
return err
if err := binary.Write(&b, byteOrder, swap.MaxMinerFee); err != nil {
return nil, err
}
if err := binary.Write(b, byteOrder, swap.MaxSwapFee); err != nil {
return err
if err := binary.Write(&b, byteOrder, swap.MaxSwapFee); err != nil {
return nil, err
}
if err := binary.Write(b, byteOrder, swap.MaxPrepayRoutingFee); err != nil {
return err
if err := binary.Write(&b, byteOrder, swap.MaxPrepayRoutingFee); err != nil {
return nil, err
}
if err := binary.Write(b, byteOrder, swap.InitiationHeight); err != nil {
return err
if err := binary.Write(&b, byteOrder, swap.InitiationHeight); err != nil {
return nil, err
}
return nil
}
func serializeLoopOutEvent(time time.Time, state SwapState) (
[]byte, error) {
var b bytes.Buffer
if err := binary.Write(&b, byteOrder, time.UnixNano()); err != nil {
addr := swap.DestAddr.String()
if err := wire.WriteVarString(&b, 0, addr); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, state); err != nil {
if err := wire.WriteVarString(&b, 0, swap.SwapInvoice); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func deserializeLoopOutEvent(value []byte) (*LoopOutEvent, error) {
update := &LoopOutEvent{}
r := bytes.NewReader(value)
if err := binary.Write(&b, byteOrder, swap.SweepConfTarget); err != nil {
return nil, err
}
var unixNano int64
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
if err := binary.Write(&b, byteOrder, swap.MaxSwapRoutingFee); err != nil {
return nil, err
}
update.Time = time.Unix(0, unixNano)
if err := binary.Read(r, byteOrder, &update.State); err != nil {
var unchargeChannel uint64
if swap.UnchargeChannel != nil {
unchargeChannel = *swap.UnchargeChannel
}
if err := binary.Write(&b, byteOrder, unchargeChannel); err != nil {
return nil, err
}
return update, nil
return b.Bytes(), nil
}

@ -18,19 +18,27 @@ var (
// database.
dbFileName = "loop.db"
// unchargeSwapsBucketKey is a bucket that contains all swaps that are
// currently pending or completed. This bucket is keyed by the
// swaphash, and leads to a nested sub-bucket that houses information
// for that swap.
// loopOutBucketKey is a bucket that contains all out swaps that are
// currently pending or completed. This bucket is keyed by the swaphash,
// and leads to a nested sub-bucket that houses information for that
// swap.
//
// maps: swapHash -> swapBucket
unchargeSwapsBucketKey = []byte("uncharge-swaps")
loopOutBucketKey = []byte("uncharge-swaps")
// unchargeUpdatesBucketKey is a bucket that contains all updates
// pertaining to a swap. This is a sub-bucket of the swap bucket for a
// particular swap. This list only ever grows.
// loopInBucketKey is a bucket that contains all in swaps that are
// currently pending or completed. This bucket is keyed by the swaphash,
// and leads to a nested sub-bucket that houses information for that
// swap.
//
// path: unchargeUpdatesBucket -> swapBucket[hash] -> updateBucket
// maps: swapHash -> swapBucket
loopInBucketKey = []byte("loop-in")
// updatesBucketKey is a bucket that contains all updates pertaining to
// a swap. This is a sub-bucket of the swap bucket for a particular
// swap. This list only ever grows.
//
// path: loopInBucket/loopOutBucket -> swapBucket[hash] -> updatesBucket
//
// maps: updateNumber -> time || state
updatesBucketKey = []byte("updates")
@ -38,7 +46,7 @@ var (
// contractKey is the key that stores the serialized swap contract. It
// is nested within the sub-bucket for each active swap.
//
// path: unchargeUpdatesBucket -> swapBucket[hash]
// path: loopInBucket/loopOutBucket -> swapBucket[hash] -> contractKey
//
// value: time || rawSwapState
contractKey = []byte("contract")
@ -92,11 +100,11 @@ func NewBoltSwapStore(dbPath string, chainParams *chaincfg.Params) (
// We'll create all the buckets we need if this is the first time we're
// starting up. If they already exist, then these calls will be noops.
err = bdb.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey)
_, err := tx.CreateBucketIfNotExists(loopOutBucketKey)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(updatesBucketKey)
_, err = tx.CreateBucketIfNotExists(loopInBucketKey)
if err != nil {
return err
}
@ -123,15 +131,12 @@ func NewBoltSwapStore(dbPath string, chainParams *chaincfg.Params) (
}, nil
}
// FetchLoopOutSwaps returns all swaps currently in the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
var swaps []*LoopOut
func (s *boltSwapStore) fetchSwaps(bucketKey []byte,
callback func([]byte, Loop) error) error {
err := s.db.View(func(tx *bbolt.Tx) error {
// First, we'll grab our main loop out swap bucket key.
rootBucket := tx.Bucket(unchargeSwapsBucketKey)
return s.db.View(func(tx *bbolt.Tx) error {
// First, we'll grab our main loop in bucket key.
rootBucket := tx.Bucket(bucketKey)
if rootBucket == nil {
return errors.New("bucket does not exist")
}
@ -159,12 +164,6 @@ func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
if contractBytes == nil {
return errors.New("contract not found")
}
contract, err := deserializeLoopOutContract(
contractBytes, s.chainParams,
)
if err != nil {
return err
}
// Once we have the raw swap, we'll also need to decode
// each of the past updates to the swap itself.
@ -175,9 +174,9 @@ func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
// De serialize and collect each swap update into our
// slice of swap events.
var updates []*LoopOutEvent
err = stateBucket.ForEach(func(k, v []byte) error {
event, err := deserializeLoopOutEvent(v)
var updates []*LoopEvent
err := stateBucket.ForEach(func(k, v []byte) error {
event, err := deserializeLoopEvent(v)
if err != nil {
return err
}
@ -192,16 +191,39 @@ func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
var hash lntypes.Hash
copy(hash[:], swapHash)
swap := LoopOut{
Contract: contract,
Hash: hash,
Events: updates,
loop := Loop{
Hash: hash,
Events: updates,
}
swaps = append(swaps, &swap)
return nil
return callback(contractBytes, loop)
})
})
}
// FetchLoopOutSwaps returns all loop out swaps currently in the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
var swaps []*LoopOut
err := s.fetchSwaps(loopOutBucketKey,
func(contractBytes []byte, loop Loop) error {
contract, err := deserializeLoopOutContract(
contractBytes, s.chainParams,
)
if err != nil {
return err
}
swaps = append(swaps, &LoopOut{
Contract: contract,
Loop: loop,
})
return nil
},
)
if err != nil {
return nil, err
}
@ -209,24 +231,47 @@ func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
return swaps, nil
}
// CreateLoopOut adds an initiated swap to the store.
// FetchLoopInSwaps returns all loop in swaps currently in the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
swap *LoopOutContract) error {
func (s *boltSwapStore) FetchLoopInSwaps() ([]*LoopIn, error) {
var swaps []*LoopIn
// If the hash doesn't match the pre-image, then this is an invalid
// swap so we'll bail out early.
if hash != swap.Preimage.Hash() {
return errors.New("hash and preimage do not match")
err := s.fetchSwaps(loopInBucketKey,
func(contractBytes []byte, loop Loop) error {
contract, err := deserializeLoopInContract(
contractBytes,
)
if err != nil {
return err
}
swaps = append(swaps, &LoopIn{
Contract: contract,
Loop: loop,
})
return nil
},
)
if err != nil {
return nil, err
}
return swaps, nil
}
// createLoop creates a swap in the store. It requires that the contract is
// already serialized to be able to use this function for both in and out swaps.
func (s *boltSwapStore) createLoop(bucketKey []byte, hash lntypes.Hash,
contractBytes []byte) error {
// Otherwise, we'll create a new swap within the database.
return s.db.Update(func(tx *bbolt.Tx) error {
// First, we'll grab the root bucket that houses all of our
// main swaps.
rootBucket, err := tx.CreateBucketIfNotExists(
unchargeSwapsBucketKey,
bucketKey,
)
if err != nil {
return err
@ -235,8 +280,7 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
// If the swap already exists, then we'll exit as we don't want
// to override a swap.
if rootBucket.Get(hash[:]) != nil {
return fmt.Errorf("swap %v already exists",
swap.Preimage)
return fmt.Errorf("swap %v already exists", hash)
}
// From the root bucket, we'll make a new sub swap bucket using
@ -246,15 +290,11 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
return err
}
// With out swap bucket created, we'll serialize and store the
// swap itself.
contract, err := serializeLoopOutContract(swap)
// With the swap bucket created, we'll store the swap itself.
err = swapBucket.Put(contractKey, contractBytes)
if err != nil {
return err
}
if err := swapBucket.Put(contractKey, contract); err != nil {
return err
}
// Finally, we'll create an empty updates bucket for this swap
// to track any future updates to the swap itself.
@ -263,18 +303,56 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
})
}
// UpdateLoopOut stores a swap updateLoopOut. This appends to the event log for
// a particular swap as it goes through the various stages in its lifetime.
// CreateLoopOut adds an initiated swap to the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *boltSwapStore) UpdateLoopOut(hash lntypes.Hash, time time.Time,
state SwapState) error {
func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
swap *LoopOutContract) error {
// If the hash doesn't match the pre-image, then this is an invalid
// swap so we'll bail out early.
if hash != swap.Preimage.Hash() {
return errors.New("hash and preimage do not match")
}
contractBytes, err := serializeLoopOutContract(swap)
if err != nil {
return err
}
return s.createLoop(loopOutBucketKey, hash, contractBytes)
}
// CreateLoopIn adds an initiated swap to the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *boltSwapStore) CreateLoopIn(hash lntypes.Hash,
swap *LoopInContract) error {
// If the hash doesn't match the pre-image, then this is an invalid
// swap so we'll bail out early.
if hash != swap.Preimage.Hash() {
return errors.New("hash and preimage do not match")
}
contractBytes, err := serializeLoopInContract(swap)
if err != nil {
return err
}
return s.createLoop(loopInBucketKey, hash, contractBytes)
}
// updateLoop saves a new swap state transition to the store. It takes in a
// bucket key so that this function can be used for both in and out swaps.
func (s *boltSwapStore) updateLoop(bucketKey []byte, hash lntypes.Hash,
time time.Time, state SwapState) error {
return s.db.Update(func(tx *bbolt.Tx) error {
// Starting from the root bucket, we'll traverse the bucket
// hierarchy all the way down to the swap bucket, and the
// update sub-bucket within that.
rootBucket := tx.Bucket(unchargeSwapsBucketKey)
rootBucket := tx.Bucket(bucketKey)
if rootBucket == nil {
return errors.New("bucket does not exist")
}
@ -295,7 +373,7 @@ func (s *boltSwapStore) UpdateLoopOut(hash lntypes.Hash, time time.Time,
}
// With the ID obtained, we'll write out this new update value.
updateValue, err := serializeLoopOutEvent(time, state)
updateValue, err := serializeLoopEvent(time, state)
if err != nil {
return err
}
@ -303,6 +381,26 @@ func (s *boltSwapStore) UpdateLoopOut(hash lntypes.Hash, time time.Time,
})
}
// UpdateLoopOut stores a swap update. This appends to the event log for
// a particular swap as it goes through the various stages in its lifetime.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *boltSwapStore) UpdateLoopOut(hash lntypes.Hash, time time.Time,
state SwapState) error {
return s.updateLoop(loopOutBucketKey, hash, time, state)
}
// UpdateLoopIn stores a swap update. This appends to the event log for
// a particular swap as it goes through the various stages in its lifetime.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *boltSwapStore) UpdateLoopIn(hash lntypes.Hash, time time.Time,
state SwapState) error {
return s.updateLoop(loopInBucketKey, hash, time, state)
}
// Close closes the underlying database.
//
// NOTE: Part of the loopdb.SwapStore interface.

@ -34,9 +34,9 @@ var (
testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
)
// TestBoltSwapStore tests all the basic functionality of the current bbolt
// TestLoopOutStore tests all the basic functionality of the current bbolt
// swap store.
func TestBoltSwapStore(t *testing.T) {
func TestLoopOutStore(t *testing.T) {
tempDirName, err := ioutil.TempDir("", "clientstore")
if err != nil {
t.Fatal(err)
@ -65,25 +65,27 @@ func TestBoltSwapStore(t *testing.T) {
// database shortly.
pendingSwap := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
SenderKey: senderKey,
PrepayInvoice: "prepayinvoice",
ReceiverKey: receiverKey,
MaxMinerFee: 10,
MaxSwapFee: 20,
MaxPrepayRoutingFee: 40,
InitiationHeight: 99,
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
SenderKey: senderKey,
ReceiverKey: receiverKey,
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
// Convert to/from unix to remove timezone, so that it
// doesn't interfere with DeepEqual.
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
},
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
}
// checkSwap is a test helper function that'll assert the state of a
@ -157,3 +159,125 @@ func TestBoltSwapStore(t *testing.T) {
}
checkSwap(StateFailInsufficientValue)
}
// TestLoopInStore tests all the basic functionality of the current bbolt
// swap store.
func TestLoopInStore(t *testing.T) {
tempDirName, err := ioutil.TempDir("", "clientstore")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDirName)
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
if err != nil {
t.Fatal(err)
}
// First, verify that an empty database has no active swaps.
swaps, err := store.FetchLoopInSwaps()
if err != nil {
t.Fatal(err)
}
if len(swaps) != 0 {
t.Fatal("expected empty store")
}
hash := sha256.Sum256(testPreimage[:])
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
// Next, we'll make a new pending swap that we'll insert into the
// database shortly.
loopInChannel := uint64(123)
pendingSwap := LoopInContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
SenderKey: senderKey,
ReceiverKey: receiverKey,
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
// Convert to/from unix to remove timezone, so that it
// doesn't interfere with DeepEqual.
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
},
HtlcConfTarget: 2,
LoopInChannel: &loopInChannel,
ExternalHtlc: true,
}
// checkSwap is a test helper function that'll assert the state of a
// swap.
checkSwap := func(expectedState SwapState) {
t.Helper()
swaps, err := store.FetchLoopInSwaps()
if err != nil {
t.Fatal(err)
}
if len(swaps) != 1 {
t.Fatal("expected pending swap in store")
}
swap := swaps[0].Contract
if !reflect.DeepEqual(swap, &pendingSwap) {
t.Fatal("invalid pending swap data")
}
if swaps[0].State() != expectedState {
t.Fatalf("expected state %v, but got %v",
expectedState, swaps[0].State(),
)
}
}
// If we create a new swap, then it should show up as being initialized
// right after.
if err := store.CreateLoopIn(hash, &pendingSwap); err != nil {
t.Fatal(err)
}
checkSwap(StateInitiated)
// Trying to make the same swap again should result in an error.
if err := store.CreateLoopIn(hash, &pendingSwap); err == nil {
t.Fatal("expected error on storing duplicate")
}
checkSwap(StateInitiated)
// Next, we'll update to the next state of the pre-image being
// revealed. The state should be reflected here again.
err = store.UpdateLoopIn(
hash, testTime, StatePreimageRevealed,
)
if err != nil {
t.Fatal(err)
}
checkSwap(StatePreimageRevealed)
// Next, we'll update to the final state to ensure that the state is
// properly updated.
err = store.UpdateLoopIn(
hash, testTime, StateFailInsufficientValue,
)
if err != nil {
t.Fatal(err)
}
checkSwap(StateFailInsufficientValue)
if err := store.Close(); err != nil {
t.Fatal(err)
}
// If we re-open the same store, then the state of the current swap
// should be the same.
store, err = NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
if err != nil {
t.Fatal(err)
}
checkSwap(StateFailInsufficientValue)
}

@ -1,6 +1,8 @@
package loopdb
// SwapState indicates the current state of a swap.
// SwapState indicates the current state of a swap. This enumeration is the
// union of loop in and loop out states. A single type is used for both swap
// types to be able to reduce code duplication that would otherwise be required.
type SwapState uint8
const (
@ -22,23 +24,24 @@ const (
// server pulled the off-chain htlc.
StateSuccess = 2
// StateFailOffchainPayments indicates that it wasn't possible to find routes
// for one or both of the off-chain payments to the server that
// StateFailOffchainPayments indicates that it wasn't possible to find
// routes for one or both of the off-chain payments to the server that
// satisfied the payment restrictions (fee and timelock limits).
StateFailOffchainPayments = 3
// StateFailTimeout indicates that the on-chain htlc wasn't confirmed before
// its expiry or confirmed too late (MinPreimageRevealDelta violated).
// StateFailTimeout indicates that the on-chain htlc wasn't confirmed
// before its expiry or confirmed too late (MinPreimageRevealDelta
// violated).
StateFailTimeout = 4
// StateFailSweepTimeout indicates that the on-chain htlc wasn't swept before
// the server revoked the htlc. The server didn't pull the off-chain
// htlc (even though it could have) and we timed out the off-chain htlc
// ourselves. No funds lost.
// StateFailSweepTimeout indicates that the on-chain htlc wasn't swept
// before the server revoked the htlc. The server didn't pull the
// off-chain htlc (even though it could have) and we timed out the
// off-chain htlc ourselves. No funds lost.
StateFailSweepTimeout = 5
// StateFailInsufficientValue indicates that the published on-chain htlc had
// a value lower than the requested amount.
// StateFailInsufficientValue indicates that the published on-chain htlc
// had a value lower than the requested amount.
StateFailInsufficientValue = 6
// StateFailTemporary indicates that the swap cannot progress because
@ -48,6 +51,10 @@ const (
// StateHtlcPublished means that the client published the on-chain htlc.
StateHtlcPublished = 8
// StateInvoiceSettled means that the swap invoice has been paid by the
// server.
StateInvoiceSettled = 9
)
// SwapStateType defines the types of swap states that exist. Every swap state
@ -90,6 +97,9 @@ func (s SwapState) String() string {
case StatePreimageRevealed:
return "PreimageRevealed"
case StateHtlcPublished:
return "HtlcPublished"
case StateSuccess:
return "Success"
@ -108,6 +118,9 @@ func (s SwapState) String() string {
case StateFailTemporary:
return "FailTemporary"
case StateInvoiceSettled:
return "InvoiceSettled"
default:
return "Unknown"
}

@ -0,0 +1,632 @@
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,
ExternalHtlc: request.ExternalHtlc,
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, swap.HtlcNP2WSH,
)
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, swap.HtlcNP2WSH,
)
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.setState(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 {
if s.ExternalHtlc {
// If an external htlc was indicated, we can move to the
// HtlcPublished state directly and wait for
// confirmation.
s.setState(loopdb.StateHtlcPublished)
err = s.persistState(globalCtx)
if err != nil {
return err
}
} else {
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.PkScript,
)
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.
err = s.waitForSwapComplete(globalCtx, htlcOutpoint, htlcValue)
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.PkScript, 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.setState(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.setState(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.PkScript,
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
}
// waitForSwapComplete waits until a spending tx of the htlc gets confirmed and
// the swap invoice is either settled or canceled. If the htlc times out, the
// timeout tx will be published.
func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
htlc *wire.OutPoint, htlcValue btcutil.Amount) error {
// Register the htlc spend notification.
rpcCtx, cancel := context.WithCancel(ctx)
defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
rpcCtx, nil, s.htlc.PkScript, s.InitiationHeight,
)
if err != nil {
return fmt.Errorf("register spend ntfn: %v", err)
}
// Register for swap invoice updates.
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 fmt.Errorf("subscribe to swap invoice: %v", err)
}
// checkTimeout publishes the timeout tx if the contract has expired.
checkTimeout := func() error {
if s.height >= s.LoopInContract.CltvExpiry {
return s.publishTimeoutTx(ctx, htlc)
}
return nil
}
// Check timeout at current height. After a restart we may want to
// publish the tx immediately.
err = checkTimeout()
if err != nil {
return err
}
htlcSpend := false
invoiceFinalized := false
for !htlcSpend || !invoiceFinalized {
select {
// Spend notification error.
case err := <-spendErr:
return err
// Receive block epochs and start publishing the timeout tx
// whenever possible.
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
err := checkTimeout()
if err != nil {
return err
}
// The htlc spend is confirmed. Inspect the spending tx to
// determine the final swap state.
case spendDetails := <-spendChan:
s.log.Infof("Htlc spend by tx: %v",
spendDetails.SpenderTxHash)
err := s.processHtlcSpend(
ctx, spendDetails, htlcValue,
)
if err != nil {
return err
}
htlcSpend = true
// Swap invoice ntfn error.
case err := <-swapInvoiceErr:
return err
// An update to the swap invoice occured. Check the new state
// and update the swap state accordingly.
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
// If invoice settlement and htlc spend happen
// in the expected order, move the swap to an
// intermediate state that indicates that the
// swap is complete from the user point of view,
// but still incomplete with regards to
// accounting data.
if s.state == loopdb.StateHtlcPublished {
s.setState(loopdb.StateInvoiceSettled)
err := s.persistState(ctx)
if err != nil {
return err
}
}
invoiceFinalized = true
// Canceled invoice has no effect on server cost
// balance.
case channeldb.ContractCanceled:
invoiceFinalized = true
}
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func (s *loopInSwap) processHtlcSpend(ctx context.Context,
spend *chainntnfs.SpendDetail, htlcValue btcutil.Amount) error {
// 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.setState(loopdb.StateSuccess)
// Server swept the htlc. The htlc value can be added to the
// server cost balance.
s.cost.Server += htlcValue
} else {
s.setState(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(ctx, s.hash)
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
return err
}
// TODO: Add miner fee of timeout tx to swap cost balance.
}
return nil
}
// 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.AddTimeoutToEstimator, 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 {
// Update state in store.
err := s.store.UpdateLoopIn(s.hash, s.lastUpdateTime, s.state)
if err != nil {
return err
}
// Send out swap update
return s.sendUpdate(ctx)
}
// setState updates the swap state and last update timestamp.
func (s *loopInSwap) setState(state loopdb.SwapState) {
s.lastUpdateTime = time.Now()
s.state = state
}

@ -0,0 +1,378 @@
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
// 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,
}
// Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderInputIndex: 0,
}
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
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != ctx.server.swapHash {
t.Fatal("client subscribing to wrong invoice")
}
// 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
// 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(), swap.HtlcNP2WSH, cfg.lnd.ChainParams,
)
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.PkScript,
})
}
// 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
// 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,
}
// Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
// 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,
}
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)
}
}

@ -84,28 +84,28 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
initiationTime := time.Now()
contract := loopdb.LoopOutContract{
SwapInvoice: swapResp.swapInvoice,
DestAddr: request.DestAddr,
MaxSwapRoutingFee: request.MaxSwapRoutingFee,
SweepConfTarget: request.SweepConfTarget,
UnchargeChannel: request.LoopOutChannel,
SwapInvoice: swapResp.swapInvoice,
DestAddr: request.DestAddr,
MaxSwapRoutingFee: request.MaxSwapRoutingFee,
SweepConfTarget: request.SweepConfTarget,
UnchargeChannel: request.LoopOutChannel,
PrepayInvoice: swapResp.prepayInvoice,
MaxPrepayRoutingFee: request.MaxPrepayRoutingFee,
SwapContract: loopdb.SwapContract{
InitiationHeight: currentHeight,
InitiationTime: initiationTime,
PrepayInvoice: swapResp.prepayInvoice,
ReceiverKey: receiverKey,
SenderKey: swapResp.senderKey,
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
MaxPrepayRoutingFee: request.MaxPrepayRoutingFee,
InitiationHeight: currentHeight,
InitiationTime: initiationTime,
ReceiverKey: receiverKey,
SenderKey: swapResp.senderKey,
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
},
}
swapKit, err := newSwapKit(
swapHash, TypeOut, cfg, &contract.SwapContract,
swapHash, TypeOut, cfg, &contract.SwapContract, swap.HtlcP2WSH,
)
if err != nil {
return nil, err
@ -135,10 +135,10 @@ func resumeLoopOutSwap(reqContext context.Context, cfg *swapConfig,
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(
hash, TypeOut, cfg, &pend.Contract.SwapContract,
hash, TypeOut, cfg, &pend.Contract.SwapContract, swap.HtlcP2WSH,
)
if err != nil {
return nil, err
@ -168,6 +168,7 @@ func (s *loopOutSwap) execute(mainCtx context.Context,
s.executeConfig = *cfg
s.height = height
// Execute swap.
err := s.executeAndFinalize(mainCtx)
// If an unexpected error happened, report a temporary failure.
@ -189,6 +190,7 @@ func (s *loopOutSwap) execute(mainCtx context.Context,
// executeAndFinalize executes a swap and awaits the definitive outcome of the
// offchain payments. When this method returns, the swap outcome is final.
func (s *loopOutSwap) executeAndFinalize(globalCtx context.Context) error {
// Announce swap by sending out an initial update.
err := s.sendUpdate(globalCtx)
if err != nil {
@ -281,7 +283,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
// Retrieve outpoint for sweep.
htlcOutpoint, htlcValue, err := swap.GetScriptOutput(
txConf.Tx, s.htlc.ScriptHash,
txConf.Tx, s.htlc.PkScript,
)
if err != nil {
return err
@ -383,7 +385,7 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) (
defer cancel()
htlcConfChan, htlcErrChan, err :=
s.lnd.ChainNotifier.RegisterConfirmationsNtfn(
ctx, nil, s.htlc.ScriptHash, 1,
ctx, nil, s.htlc.PkScript, 1,
s.InitiationHeight,
)
if err != nil {
@ -514,7 +516,7 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
ctx, nil, s.htlc.ScriptHash, s.InitiationHeight,
ctx, nil, s.htlc.PkScript, s.InitiationHeight,
)
if err != nil {
return nil, fmt.Errorf("register spend ntfn: %v", err)
@ -571,7 +573,7 @@ func (s *loopOutSwap) sweep(ctx context.Context,
// Calculate sweep tx fee
fee, err := s.sweeper.GetSweepFee(
ctx, s.htlc.MaxSuccessWitnessSize,
ctx, s.htlc.AddSuccessToEstimator,
s.SweepConfTarget,
)
if err != nil {

@ -29,20 +29,24 @@ type SwapType int32
const (
// LOOP_OUT indicates an loop out swap (off-chain to on-chain)
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{
0: "LOOP_OUT",
1: "LOOP_IN",
}
var SwapType_value = map[string]int32{
"LOOP_OUT": 0,
"LOOP_IN": 1,
}
func (x SwapType) String() string {
return proto.EnumName(SwapType_name, int32(x))
}
func (SwapType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{0}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{0}
}
type SwapState int32
@ -61,6 +65,10 @@ const (
// case where we wait for fees to come down before we sweep.
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
// the required confirmation depth.
SwapState_SUCCESS SwapState = 3
@ -68,26 +76,34 @@ const (
// FAILED is the final swap state for a failed swap with or without loss of
// the swap amount.
SwapState_FAILED SwapState = 4
// *
// INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been
// paid, but we are still waiting for the htlc spend to confirm.
SwapState_INVOICE_SETTLED SwapState = 5
)
var SwapState_name = map[int32]string{
0: "INITIATED",
1: "PREIMAGE_REVEALED",
2: "HTLC_PUBLISHED",
3: "SUCCESS",
4: "FAILED",
5: "INVOICE_SETTLED",
}
var SwapState_value = map[string]int32{
"INITIATED": 0,
"PREIMAGE_REVEALED": 1,
"HTLC_PUBLISHED": 2,
"SUCCESS": 3,
"FAILED": 4,
"INVOICE_SETTLED": 5,
}
func (x SwapState) String() string {
return proto.EnumName(SwapState_name, int32(x))
}
func (SwapState) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{1}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{1}
}
type LoopOutRequest struct {
@ -145,7 +161,7 @@ func (m *LoopOutRequest) Reset() { *m = LoopOutRequest{} }
func (m *LoopOutRequest) String() string { return proto.CompactTextString(m) }
func (*LoopOutRequest) ProtoMessage() {}
func (*LoopOutRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{0}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{0}
}
func (m *LoopOutRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_LoopOutRequest.Unmarshal(m, b)
@ -221,11 +237,106 @@ func (m *LoopOutRequest) GetLoopOutChannel() uint64 {
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.
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.
//
// Note: NOT YET IMPLEMENTED
LoopInChannel uint64 `protobuf:"varint,4,opt,name=loop_in_channel,json=loopInChannel,proto3" json:"loop_in_channel,omitempty"`
// *
// If external_htlc is true, we expect the htlc to be published by an external
// actor.
ExternalHtlc bool `protobuf:"varint,5,opt,name=external_htlc,json=externalHtlc,proto3" json:"external_htlc,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_ba4b73c10b9bbc2a, []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
}
func (m *LoopInRequest) GetExternalHtlc() bool {
if m != nil {
return m.ExternalHtlc
}
return false
}
type SwapResponse struct {
// *
// Swap identifier to track status in the update stream that is returned from
// the Start() call. Currently this is the hash that locks the htlcs.
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// *
// The address of the on-chain htlc.
HtlcAddress string `protobuf:"bytes,2,opt,name=htlc_address,json=htlcAddress,proto3" json:"htlc_address,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -235,7 +346,7 @@ func (m *SwapResponse) Reset() { *m = SwapResponse{} }
func (m *SwapResponse) String() string { return proto.CompactTextString(m) }
func (*SwapResponse) ProtoMessage() {}
func (*SwapResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{1}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{2}
}
func (m *SwapResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapResponse.Unmarshal(m, b)
@ -262,6 +373,13 @@ func (m *SwapResponse) GetId() string {
return ""
}
func (m *SwapResponse) GetHtlcAddress() string {
if m != nil {
return m.HtlcAddress
}
return ""
}
type MonitorRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
@ -272,7 +390,7 @@ func (m *MonitorRequest) Reset() { *m = MonitorRequest{} }
func (m *MonitorRequest) String() string { return proto.CompactTextString(m) }
func (*MonitorRequest) ProtoMessage() {}
func (*MonitorRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{2}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{3}
}
func (m *MonitorRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_MonitorRequest.Unmarshal(m, b)
@ -325,7 +443,7 @@ func (m *SwapStatus) Reset() { *m = SwapStatus{} }
func (m *SwapStatus) String() string { return proto.CompactTextString(m) }
func (*SwapStatus) ProtoMessage() {}
func (*SwapStatus) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{3}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{4}
}
func (m *SwapStatus) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapStatus.Unmarshal(m, b)
@ -404,7 +522,7 @@ func (m *TermsRequest) Reset() { *m = TermsRequest{} }
func (m *TermsRequest) String() string { return proto.CompactTextString(m) }
func (*TermsRequest) ProtoMessage() {}
func (*TermsRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{4}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{5}
}
func (m *TermsRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsRequest.Unmarshal(m, b)
@ -446,10 +564,7 @@ type TermsResponse struct {
MaxSwapAmount int64 `protobuf:"varint,6,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,omitempty"`
// *
// On-chain cltv expiry delta
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"`
CltvDelta int32 `protobuf:"varint,7,opt,name=cltv_delta,json=cltvDelta,proto3" json:"cltv_delta,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -459,7 +574,7 @@ func (m *TermsResponse) Reset() { *m = TermsResponse{} }
func (m *TermsResponse) String() string { return proto.CompactTextString(m) }
func (*TermsResponse) ProtoMessage() {}
func (*TermsResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{5}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{6}
}
func (m *TermsResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsResponse.Unmarshal(m, b)
@ -528,13 +643,6 @@ func (m *TermsResponse) GetCltvDelta() int32 {
return 0
}
func (m *TermsResponse) GetMaxCltv() int32 {
if m != nil {
return m.MaxCltv
}
return 0
}
type QuoteRequest struct {
// *
// The amount to swap in satoshis.
@ -548,7 +656,7 @@ func (m *QuoteRequest) Reset() { *m = QuoteRequest{} }
func (m *QuoteRequest) String() string { return proto.CompactTextString(m) }
func (*QuoteRequest) ProtoMessage() {}
func (*QuoteRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{6}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{7}
}
func (m *QuoteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteRequest.Unmarshal(m, b)
@ -594,7 +702,7 @@ func (m *QuoteResponse) Reset() { *m = QuoteResponse{} }
func (m *QuoteResponse) String() string { return proto.CompactTextString(m) }
func (*QuoteResponse) ProtoMessage() {}
func (*QuoteResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_ecf1f0d12250b1cb, []int{7}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{8}
}
func (m *QuoteResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteResponse.Unmarshal(m, b)
@ -637,6 +745,7 @@ func (m *QuoteResponse) GetMinerFee() int64 {
func init() {
proto.RegisterType((*LoopOutRequest)(nil), "looprpc.LoopOutRequest")
proto.RegisterType((*LoopInRequest)(nil), "looprpc.LoopInRequest")
proto.RegisterType((*SwapResponse)(nil), "looprpc.SwapResponse")
proto.RegisterType((*MonitorRequest)(nil), "looprpc.MonitorRequest")
proto.RegisterType((*SwapStatus)(nil), "looprpc.SwapStatus")
@ -666,6 +775,12 @@ type SwapClientClient interface {
// point onwards, progress can be tracked via the SwapStatus stream that is
// returned from Monitor().
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`
// Monitor will return a stream of swap updates for currently active swaps.
// TODO: add MonitorSync version for REST clients.
@ -677,6 +792,12 @@ type SwapClientClient interface {
// LoopOutQuote returns a quote for a loop out swap with the provided
// parameters.
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 {
@ -696,6 +817,15 @@ func (c *swapClientClient) LoopOut(ctx context.Context, in *LoopOutRequest, opts
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) {
stream, err := c.cc.NewStream(ctx, &_SwapClient_serviceDesc.Streams[0], "/looprpc.SwapClient/Monitor", opts...)
if err != nil {
@ -746,6 +876,24 @@ func (c *swapClientClient) LoopOutQuote(ctx context.Context, in *QuoteRequest, o
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.
type SwapClientServer interface {
// * loop: `out`
@ -754,6 +902,12 @@ type SwapClientServer interface {
// point onwards, progress can be tracked via the SwapStatus stream that is
// returned from Monitor().
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`
// Monitor will return a stream of swap updates for currently active swaps.
// TODO: add MonitorSync version for REST clients.
@ -765,6 +919,12 @@ type SwapClientServer interface {
// LoopOutQuote returns a quote for a loop out swap with the provided
// parameters.
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) {
@ -789,6 +949,24 @@ func _SwapClient_LoopOut_Handler(srv interface{}, ctx context.Context, dec func(
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 {
m := new(MonitorRequest)
if err := stream.RecvMsg(m); err != nil {
@ -846,6 +1024,42 @@ func _SwapClient_LoopOutQuote_Handler(srv interface{}, ctx context.Context, dec
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{
ServiceName: "looprpc.SwapClient",
HandlerType: (*SwapClientServer)(nil),
@ -854,6 +1068,10 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
MethodName: "LoopOut",
Handler: _SwapClient_LoopOut_Handler,
},
{
MethodName: "LoopIn",
Handler: _SwapClient_LoopIn_Handler,
},
{
MethodName: "LoopOutTerms",
Handler: _SwapClient_LoopOutTerms_Handler,
@ -862,6 +1080,14 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
MethodName: "LoopOutQuote",
Handler: _SwapClient_LoopOutQuote_Handler,
},
{
MethodName: "GetLoopInTerms",
Handler: _SwapClient_GetLoopInTerms_Handler,
},
{
MethodName: "GetLoopInQuote",
Handler: _SwapClient_GetLoopInQuote_Handler,
},
},
Streams: []grpc.StreamDesc{
{
@ -873,60 +1099,67 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
Metadata: "client.proto",
}
func init() { proto.RegisterFile("client.proto", fileDescriptor_client_ecf1f0d12250b1cb) }
var fileDescriptor_client_ecf1f0d12250b1cb = []byte{
// 825 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x54, 0xcb, 0x72, 0xe3, 0x44,
0x14, 0x1d, 0xc9, 0x4e, 0x6c, 0xdd, 0xc8, 0x8a, 0xdd, 0xf3, 0xf2, 0x18, 0x86, 0x32, 0x2a, 0x1e,
0xae, 0x2c, 0x22, 0xc8, 0xac, 0x60, 0x67, 0x1c, 0x0f, 0x65, 0x2a, 0x21, 0xa6, 0xed, 0x50, 0xc5,
0x4a, 0xd5, 0x63, 0xf7, 0x64, 0x54, 0xa5, 0x56, 0x6b, 0xd4, 0xad, 0x8c, 0x5d, 0x14, 0x1b, 0x7e,
0x81, 0xdf, 0x60, 0xc5, 0xaf, 0xb0, 0xe0, 0x07, 0xf8, 0x05, 0xf6, 0x54, 0x3f, 0xec, 0x48, 0x81,
0xd9, 0x75, 0x9d, 0x3e, 0xf7, 0x74, 0xf7, 0xb9, 0xe7, 0x36, 0xf8, 0xab, 0x34, 0xa1, 0x99, 0x3c,
0xcd, 0x0b, 0x2e, 0x39, 0x6a, 0xa5, 0x9c, 0xe7, 0x45, 0xbe, 0x1a, 0x7c, 0x78, 0xc3, 0xf9, 0x4d,
0x4a, 0x23, 0x92, 0x27, 0x11, 0xc9, 0x32, 0x2e, 0x89, 0x4c, 0x78, 0x26, 0x0c, 0x2d, 0xfc, 0xc3,
0x85, 0xe0, 0x82, 0xf3, 0xfc, 0xaa, 0x94, 0x98, 0xbe, 0x2d, 0xa9, 0x90, 0xa8, 0x0b, 0x0d, 0xc2,
0x64, 0xdf, 0x19, 0x3a, 0xa3, 0x06, 0x56, 0x4b, 0x84, 0xa0, 0xb9, 0xa6, 0x42, 0xf6, 0xdd, 0xa1,
0x33, 0xf2, 0xb0, 0x5e, 0xa3, 0x08, 0x1e, 0x31, 0xb2, 0x89, 0xc5, 0x3b, 0x92, 0xc7, 0x05, 0x2f,
0x65, 0x92, 0xdd, 0xc4, 0xaf, 0x29, 0xed, 0x37, 0x74, 0x59, 0x8f, 0x91, 0xcd, 0xe2, 0x1d, 0xc9,
0xb1, 0xd9, 0x79, 0x49, 0x29, 0x7a, 0x01, 0x4f, 0x54, 0x41, 0x5e, 0xd0, 0x9c, 0x6c, 0x6b, 0x25,
0x4d, 0x5d, 0xf2, 0x90, 0x91, 0xcd, 0x5c, 0x6f, 0x56, 0x8a, 0x86, 0xe0, 0xef, 0x4f, 0x51, 0xd4,
0x03, 0x4d, 0x05, 0xab, 0xae, 0x18, 0x9f, 0x40, 0x50, 0x91, 0x55, 0x17, 0x3f, 0xd4, 0x1c, 0x7f,
0x2f, 0x37, 0x66, 0x12, 0x85, 0xd0, 0x51, 0x2c, 0x96, 0x64, 0xb4, 0xd0, 0x42, 0x2d, 0x4d, 0x3a,
0x62, 0x64, 0x73, 0xa9, 0x30, 0xa5, 0x34, 0x82, 0xae, 0xf2, 0x2c, 0xe6, 0xa5, 0x8c, 0x57, 0x6f,
0x48, 0x96, 0xd1, 0xb4, 0xdf, 0x1e, 0x3a, 0xa3, 0x26, 0x0e, 0x52, 0xe3, 0xd0, 0xc4, 0xa0, 0xe1,
0x47, 0xe0, 0xeb, 0xc7, 0x51, 0x91, 0xf3, 0x4c, 0x50, 0x14, 0x80, 0x9b, 0xac, 0xb5, 0x61, 0x1e,
0x76, 0x93, 0x75, 0xd8, 0x85, 0xe0, 0x92, 0x67, 0x89, 0xe4, 0x85, 0xf5, 0x34, 0xfc, 0xc7, 0x01,
0x50, 0x25, 0x0b, 0x49, 0x64, 0x29, 0xfe, 0xc7, 0x62, 0x23, 0xe1, 0xee, 0x24, 0xd0, 0xa7, 0xd0,
0x94, 0xdb, 0xdc, 0xd8, 0x19, 0x9c, 0xf5, 0x4e, 0x6d, 0x37, 0x4f, 0x95, 0xc8, 0x72, 0x9b, 0x53,
0xac, 0xb7, 0xd1, 0x08, 0x0e, 0x84, 0x24, 0xd2, 0x78, 0x18, 0x9c, 0xa1, 0x1a, 0x4f, 0x1d, 0x46,
0xb1, 0x21, 0xa0, 0xcf, 0xe1, 0x38, 0xc9, 0x12, 0x99, 0xe8, 0xee, 0xc7, 0x32, 0x61, 0x3b, 0x33,
0x83, 0x3b, 0x78, 0x99, 0x30, 0x63, 0x03, 0x11, 0x32, 0x2e, 0xf3, 0x35, 0x91, 0xd4, 0x30, 0x8d,
0xa5, 0x81, 0xc2, 0xaf, 0x35, 0xac, 0x99, 0x1f, 0x83, 0xff, 0x46, 0xa6, 0xab, 0x98, 0xac, 0xd7,
0x05, 0x15, 0x42, 0x7b, 0xea, 0xe1, 0x23, 0x85, 0x8d, 0x0d, 0x14, 0x06, 0xe0, 0x2f, 0x69, 0xc1,
0xc4, 0xce, 0x87, 0xdf, 0x5d, 0xe8, 0x58, 0xc0, 0x7a, 0x77, 0x02, 0x3d, 0xdd, 0xdd, 0x9c, 0x6c,
0x19, 0xcd, 0x64, 0xac, 0x83, 0x66, 0xac, 0x3c, 0x56, 0x1b, 0x73, 0x83, 0x9f, 0xab, 0xcc, 0x85,
0xd0, 0xd9, 0x25, 0x21, 0x7e, 0x45, 0x04, 0xd5, 0x7e, 0x35, 0xf0, 0x91, 0x30, 0x59, 0xf8, 0x86,
0x08, 0x5a, 0xe3, 0x14, 0xca, 0x99, 0x46, 0x8d, 0x83, 0x95, 0x17, 0xcf, 0x01, 0x2a, 0x79, 0x31,
0xf1, 0xf3, 0xf2, 0x7d, 0x58, 0x3e, 0x83, 0x63, 0x96, 0x64, 0x26, 0x74, 0x84, 0xf1, 0x32, 0x93,
0xd6, 0xaa, 0x0e, 0x4b, 0x32, 0x65, 0xec, 0x58, 0x83, 0x9a, 0xb7, 0x0b, 0xa7, 0xe5, 0x1d, 0x5a,
0x9e, 0xc9, 0xa7, 0xe5, 0x3d, 0x07, 0x58, 0xa5, 0xf2, 0x36, 0x5e, 0xd3, 0x54, 0x12, 0xed, 0xd2,
0x01, 0xf6, 0x14, 0x72, 0xae, 0x00, 0xf4, 0x0c, 0xda, 0x4a, 0x46, 0x01, 0x3a, 0x6f, 0x07, 0xb8,
0xc5, 0xc8, 0x66, 0x92, 0xca, 0xdb, 0x70, 0x08, 0xfe, 0x0f, 0x25, 0x97, 0xf4, 0xbd, 0xa3, 0x19,
0xbe, 0x86, 0x8e, 0x65, 0x58, 0x3f, 0x9f, 0x41, 0x7b, 0x3f, 0x2d, 0x86, 0xd7, 0xb2, 0x4f, 0xbf,
0xf7, 0x6c, 0xf7, 0xfe, 0xb3, 0x3f, 0x00, 0xef, 0x6e, 0x3e, 0x8c, 0x6b, 0x6d, 0x66, 0x87, 0xe3,
0xa4, 0x0f, 0xed, 0x5d, 0xf4, 0x90, 0x0f, 0xed, 0x8b, 0xab, 0xab, 0x79, 0x7c, 0x75, 0xbd, 0xec,
0x3e, 0x38, 0xf9, 0x0e, 0xbc, 0x7d, 0xd8, 0x50, 0x07, 0xbc, 0xd9, 0xf7, 0xb3, 0xe5, 0x6c, 0xbc,
0x9c, 0x9e, 0x77, 0x1f, 0xa0, 0xc7, 0xd0, 0x9b, 0xe3, 0xe9, 0xec, 0x72, 0xfc, 0xed, 0x34, 0xc6,
0xd3, 0x1f, 0xa7, 0xe3, 0x8b, 0xe9, 0x79, 0xd7, 0x41, 0x47, 0xd0, 0x5a, 0x5c, 0x4f, 0x26, 0xd3,
0xc5, 0xa2, 0xdb, 0x40, 0x00, 0x87, 0x2f, 0xc7, 0x33, 0xb5, 0xd1, 0x3c, 0xfb, 0xcb, 0x35, 0x63,
0x32, 0xd1, 0x3f, 0x19, 0xc2, 0xd0, 0xb2, 0x7f, 0x13, 0x7a, 0xba, 0x4f, 0x76, 0xfd, 0xb7, 0x1a,
0x3c, 0xae, 0x45, 0x7e, 0x67, 0x43, 0xf8, 0xf4, 0xd7, 0x3f, 0xff, 0xfe, 0xcd, 0xed, 0x85, 0x7e,
0x74, 0xfb, 0x65, 0xa4, 0x18, 0x11, 0x2f, 0xe5, 0xd7, 0xce, 0x09, 0xfa, 0x0a, 0x5a, 0x76, 0x36,
0x2b, 0x9a, 0xf5, 0x69, 0x1d, 0x3c, 0xfc, 0xcf, 0x18, 0x95, 0xe2, 0x0b, 0x07, 0xfd, 0x04, 0xbe,
0x3d, 0x5c, 0x47, 0x18, 0xdd, 0x1d, 0x5d, 0xcd, 0xf8, 0xe0, 0xc9, 0x7d, 0xd8, 0x5e, 0x69, 0xa0,
0xaf, 0xf4, 0x08, 0xa1, 0xea, 0x95, 0x22, 0xa9, 0xa5, 0xe2, 0xbd, 0xb4, 0xee, 0x66, 0x45, 0xba,
0xda, 0xff, 0x8a, 0x74, 0xad, 0xe9, 0xe1, 0x50, 0x4b, 0x0f, 0x50, 0xbf, 0x26, 0xfd, 0x56, 0x71,
0xa2, 0x9f, 0x09, 0x93, 0xbf, 0xbc, 0x3a, 0xd4, 0xdf, 0xfd, 0x8b, 0x7f, 0x03, 0x00, 0x00, 0xff,
0xff, 0x2d, 0xba, 0xce, 0x65, 0x25, 0x06, 0x00, 0x00,
func init() { proto.RegisterFile("client.proto", fileDescriptor_client_ba4b73c10b9bbc2a) }
var fileDescriptor_client_ba4b73c10b9bbc2a = []byte{
// 941 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x8e, 0xda, 0x46,
0x18, 0x8d, 0x0d, 0xbb, 0xc0, 0xb7, 0xc6, 0xc0, 0x6c, 0xb2, 0xa1, 0xb4, 0x91, 0xa8, 0xdb, 0xa4,
0x68, 0x2f, 0x96, 0x76, 0x73, 0x51, 0xb5, 0x37, 0x15, 0x01, 0x27, 0x6b, 0x89, 0x5d, 0xa8, 0x61,
0x23, 0xf5, 0xca, 0x9a, 0xc0, 0x64, 0x63, 0xc9, 0xf6, 0x38, 0xf6, 0x38, 0x61, 0x55, 0xf5, 0xa6,
0x6f, 0xd0, 0xf6, 0x4d, 0xaa, 0xbe, 0x49, 0x5f, 0xa1, 0xaf, 0xd0, 0xfb, 0x6a, 0x7e, 0x70, 0x6c,
0xe8, 0xde, 0xe4, 0xce, 0x9c, 0x39, 0x73, 0xe6, 0xfb, 0xce, 0x9c, 0x6f, 0x00, 0x63, 0x15, 0xf8,
0x24, 0x62, 0x67, 0x71, 0x42, 0x19, 0x45, 0xb5, 0x80, 0xd2, 0x38, 0x89, 0x57, 0xbd, 0xcf, 0x6e,
0x28, 0xbd, 0x09, 0xc8, 0x10, 0xc7, 0xfe, 0x10, 0x47, 0x11, 0x65, 0x98, 0xf9, 0x34, 0x4a, 0x25,
0xcd, 0xfa, 0x53, 0x07, 0x73, 0x4a, 0x69, 0x3c, 0xcb, 0x98, 0x4b, 0xde, 0x66, 0x24, 0x65, 0xa8,
0x0d, 0x15, 0x1c, 0xb2, 0xae, 0xd6, 0xd7, 0x06, 0x15, 0x97, 0x7f, 0x22, 0x04, 0xd5, 0x35, 0x49,
0x59, 0x57, 0xef, 0x6b, 0x83, 0x86, 0x2b, 0xbe, 0xd1, 0x10, 0xee, 0x87, 0x78, 0xe3, 0xa5, 0xef,
0x71, 0xec, 0x25, 0x34, 0x63, 0x7e, 0x74, 0xe3, 0xbd, 0x26, 0xa4, 0x5b, 0x11, 0xdb, 0x3a, 0x21,
0xde, 0x2c, 0xde, 0xe3, 0xd8, 0x95, 0x2b, 0xcf, 0x09, 0x41, 0x4f, 0xe1, 0x84, 0x6f, 0x88, 0x13,
0x12, 0xe3, 0xdb, 0xd2, 0x96, 0xaa, 0xd8, 0x72, 0x1c, 0xe2, 0xcd, 0x5c, 0x2c, 0x16, 0x36, 0xf5,
0xc1, 0xc8, 0x4f, 0xe1, 0xd4, 0x03, 0x41, 0x05, 0xa5, 0xce, 0x19, 0x5f, 0x82, 0x59, 0x90, 0xe5,
0x85, 0x1f, 0x0a, 0x8e, 0x91, 0xcb, 0x8d, 0x42, 0x86, 0x2c, 0x68, 0x72, 0x56, 0xe8, 0x47, 0x24,
0x11, 0x42, 0x35, 0x41, 0x3a, 0x0a, 0xf1, 0xe6, 0x92, 0x63, 0x5c, 0x69, 0x00, 0x6d, 0xee, 0x99,
0x47, 0x33, 0xe6, 0xad, 0xde, 0xe0, 0x28, 0x22, 0x41, 0xb7, 0xde, 0xd7, 0x06, 0x55, 0xd7, 0x0c,
0xa4, 0x43, 0x63, 0x89, 0x5a, 0x7f, 0x69, 0xd0, 0xe4, 0xa6, 0x39, 0xd1, 0xdd, 0x9e, 0xed, 0x56,
0xae, 0xef, 0x55, 0xbe, 0x57, 0x53, 0x65, 0xbf, 0xa6, 0x27, 0xd0, 0x12, 0x35, 0xf9, 0x51, 0x5e,
0x52, 0x55, 0x94, 0xd4, 0x0c, 0xc4, 0xf9, 0xaa, 0x22, 0xf4, 0x05, 0x34, 0xc9, 0x86, 0x91, 0x24,
0xc2, 0x81, 0xf7, 0x86, 0x05, 0x2b, 0x61, 0x54, 0xdd, 0x35, 0xb6, 0xe0, 0x05, 0x0b, 0x56, 0xd6,
0x08, 0x0c, 0x71, 0x27, 0x24, 0x8d, 0x69, 0x94, 0x12, 0x64, 0x82, 0xee, 0xaf, 0x45, 0xcd, 0x0d,
0x57, 0xf7, 0xd7, 0xe8, 0x73, 0x30, 0xf8, 0x5e, 0x0f, 0xaf, 0xd7, 0x09, 0x49, 0x53, 0x75, 0xdd,
0x47, 0x1c, 0x1b, 0x49, 0xc8, 0x6a, 0x83, 0x79, 0x49, 0x23, 0x9f, 0xd1, 0x44, 0x75, 0x6e, 0xfd,
0xab, 0x01, 0x70, 0xd5, 0x05, 0xc3, 0x2c, 0x4b, 0xff, 0xc7, 0x08, 0x79, 0x8a, 0x9e, 0x9f, 0xf2,
0x18, 0xaa, 0xec, 0x36, 0x96, 0xdd, 0x9a, 0xe7, 0x9d, 0x33, 0x95, 0xd3, 0x33, 0x2e, 0xb2, 0xbc,
0x8d, 0x89, 0x2b, 0x96, 0xd1, 0x00, 0x0e, 0x52, 0x86, 0x99, 0x4c, 0x87, 0x79, 0x8e, 0x4a, 0x3c,
0x7e, 0x18, 0x71, 0x25, 0x01, 0x7d, 0x05, 0x2d, 0x3f, 0xf2, 0x99, 0x2f, 0x72, 0xed, 0x31, 0x3f,
0xdc, 0xc6, 0xc4, 0xfc, 0x00, 0x2f, 0xfd, 0x50, 0x5e, 0x30, 0x4e, 0x99, 0x97, 0xc5, 0x6b, 0xcc,
0x88, 0x64, 0xca, 0xb0, 0x98, 0x1c, 0xbf, 0x16, 0xb0, 0x60, 0xee, 0x3a, 0x51, 0xdb, 0x77, 0xc2,
0x04, 0x63, 0x49, 0x92, 0x30, 0xdd, 0xfa, 0xf0, 0x9b, 0x0e, 0x4d, 0x05, 0x28, 0x7b, 0x4f, 0xa1,
0x23, 0x6e, 0x3f, 0xc6, 0xb7, 0x21, 0x89, 0x98, 0x27, 0x46, 0x48, 0xba, 0xdd, 0xe2, 0x0b, 0x73,
0x89, 0x4f, 0x78, 0x7e, 0x2c, 0x68, 0x6e, 0x93, 0xe2, 0xbd, 0xc2, 0xe9, 0x36, 0x2e, 0x47, 0xa9,
0xcc, 0xca, 0x33, 0x9c, 0x92, 0x12, 0x27, 0xe1, 0xce, 0x54, 0x4a, 0x1c, 0x97, 0x7b, 0xf1, 0x08,
0xa0, 0x30, 0x09, 0x72, 0xb0, 0x1a, 0x71, 0x3e, 0x06, 0x4f, 0xa0, 0x15, 0xfa, 0x91, 0x0c, 0x25,
0x0e, 0x69, 0x16, 0x31, 0x65, 0x55, 0x33, 0xf4, 0x23, 0x6e, 0xec, 0x48, 0x80, 0x82, 0xb7, 0x0d,
0xaf, 0xe2, 0x1d, 0x2a, 0x9e, 0xcc, 0xaf, 0xe2, 0x3d, 0x02, 0x58, 0x05, 0xec, 0x9d, 0xb7, 0x26,
0x01, 0xc3, 0xc2, 0xa5, 0x03, 0xb7, 0xc1, 0x91, 0x09, 0x07, 0xac, 0x3e, 0x18, 0x3f, 0x66, 0x94,
0x91, 0x3b, 0xa7, 0xc4, 0x7a, 0x0d, 0x4d, 0xc5, 0x50, 0xa6, 0x7d, 0x02, 0xf5, 0x7c, 0x64, 0x24,
0xaf, 0xa6, 0xfa, 0xdb, 0xe9, 0x4d, 0xdf, 0xed, 0xed, 0x53, 0x68, 0xec, 0x8e, 0x52, 0x3d, 0x54,
0x73, 0x74, 0xfa, 0x18, 0xea, 0xdb, 0x7c, 0x21, 0x03, 0xea, 0xd3, 0xd9, 0x6c, 0xee, 0xcd, 0xae,
0x97, 0xed, 0x7b, 0xe8, 0x08, 0x6a, 0xe2, 0x97, 0x73, 0xd5, 0xd6, 0x4e, 0x53, 0x68, 0xe4, 0xf1,
0x42, 0x4d, 0x68, 0x38, 0x57, 0xce, 0xd2, 0x19, 0x2d, 0xed, 0x49, 0xfb, 0x1e, 0x7a, 0x00, 0x9d,
0xb9, 0x6b, 0x3b, 0x97, 0xa3, 0x17, 0xb6, 0xe7, 0xda, 0x2f, 0xed, 0xd1, 0xd4, 0x9e, 0xb4, 0x35,
0x84, 0xc0, 0xbc, 0x58, 0x4e, 0xc7, 0xde, 0xfc, 0xfa, 0xd9, 0xd4, 0x59, 0x5c, 0xd8, 0x93, 0xb6,
0xce, 0x35, 0x17, 0xd7, 0xe3, 0xb1, 0xbd, 0x58, 0xb4, 0x2b, 0x08, 0xe0, 0xf0, 0xf9, 0xc8, 0xe1,
0xe4, 0x2a, 0x3a, 0x86, 0x96, 0x73, 0xf5, 0x72, 0xe6, 0x8c, 0x6d, 0x6f, 0x61, 0x2f, 0x97, 0x1c,
0x3c, 0x38, 0xff, 0xbd, 0x2a, 0x27, 0x68, 0x2c, 0x9e, 0x6f, 0xe4, 0x42, 0x4d, 0x3d, 0xc8, 0xe8,
0x61, 0x1e, 0xfa, 0xf2, 0x13, 0xdd, 0x7b, 0x50, 0x9a, 0x86, 0xad, 0x79, 0xd6, 0xc3, 0x5f, 0xff,
0xfe, 0xe7, 0x0f, 0xbd, 0x63, 0x19, 0xc3, 0x77, 0xdf, 0x0c, 0x39, 0x63, 0x48, 0x33, 0xf6, 0xbd,
0x76, 0x8a, 0xbe, 0x85, 0x43, 0xf9, 0x5e, 0xa1, 0x93, 0x92, 0x64, 0xfe, 0x80, 0xdd, 0xa1, 0x88,
0xbe, 0x83, 0x9a, 0x9a, 0xf7, 0x42, 0x31, 0xe5, 0x17, 0xa0, 0x77, 0xbc, 0x37, 0x9a, 0x59, 0xfa,
0xb5, 0x86, 0x7e, 0x02, 0x43, 0x55, 0x2d, 0xc6, 0x02, 0x7d, 0x38, 0xa1, 0x38, 0x37, 0xbd, 0x93,
0x5d, 0x58, 0xf5, 0xd2, 0x13, 0xbd, 0xdc, 0x47, 0xa8, 0xd8, 0xcb, 0x90, 0x09, 0x29, 0x2f, 0x97,
0x16, 0xe1, 0x29, 0x48, 0x17, 0xe3, 0x56, 0x90, 0x2e, 0x65, 0xcc, 0xea, 0x0b, 0xe9, 0x1e, 0xea,
0x96, 0xa4, 0xdf, 0x72, 0xce, 0xf0, 0x67, 0x1c, 0xb2, 0x5f, 0xd0, 0x0f, 0x60, 0xbe, 0x20, 0x4c,
0x3a, 0xf4, 0x31, 0xd5, 0x97, 0x04, 0x3e, 0xa6, 0xc6, 0x57, 0x87, 0xe2, 0xef, 0xf9, 0xe9, 0x7f,
0x01, 0x00, 0x00, 0xff, 0xff, 0xc4, 0x2d, 0xd7, 0x0b, 0xd5, 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`
Monitor will return a stream of swap updates for currently active swaps.
TODO: add MonitorSync version for REST clients.
@ -46,6 +54,16 @@ service SwapClient {
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 {
@ -111,6 +129,44 @@ message LoopOutRequest {
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.
Note: NOT YET IMPLEMENTED
*/
uint64 loop_in_channel = 4;
/**
If external_htlc is true, we expect the htlc to be published by an external
actor.
*/
bool external_htlc = 5;
}
message SwapResponse {
/**
@ -118,6 +174,11 @@ message SwapResponse {
the Start() call. Currently this is the hash that locks the htlcs.
*/
string id = 1;
/**
The address of the on-chain htlc.
*/
string htlc_address = 2;
}
message MonitorRequest{
@ -165,6 +226,9 @@ message SwapStatus {
enum SwapType {
// LOOP_OUT indicates an loop out swap (off-chain to on-chain)
LOOP_OUT = 0;
// LOOP_IN indicates a loop in swap (on-chain to off-chain)
LOOP_IN = 1;
}
enum SwapState {
@ -184,6 +248,12 @@ enum SwapState {
*/
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
the required confirmation depth.
@ -195,6 +265,12 @@ enum SwapState {
the swap amount.
*/
FAILED = 4;
/**
INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been
paid, but we are still waiting for the htlc spend to confirm.
*/
INVOICE_SETTLED = 5;
}
message TermsRequest {
@ -236,11 +312,6 @@ message TermsResponse {
On-chain cltv expiry delta
*/
int32 cltv_delta = 7;
/**
Maximum cltv expiry delta
*/
int32 max_cltv = 8;
}
message QuoteRequest {

@ -157,6 +157,10 @@
"id": {
"type": "string",
"description": "*\nSwap identifier to track status in the update stream that is returned from\nthe Start() call. Currently this is the hash that locks the htlcs."
},
"htlc_address": {
"type": "string",
"description": "*\nThe address of the on-chain htlc."
}
}
},
@ -165,11 +169,13 @@
"enum": [
"INITIATED",
"PREIMAGE_REVEALED",
"HTLC_PUBLISHED",
"SUCCESS",
"FAILED"
"FAILED",
"INVOICE_SETTLED"
],
"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.\n - INVOICE_SETTLED: *\nINVOICE_SETTLED is reached when the swap invoice in a loop in swap has been\npaid, but we are still waiting for the htlc spend to confirm."
},
"looprpcSwapStatus": {
"type": "object",
@ -210,10 +216,11 @@
"looprpcSwapType": {
"type": "string",
"enum": [
"LOOP_OUT"
"LOOP_OUT",
"LOOP_IN"
],
"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": {
"type": "object",
@ -251,11 +258,6 @@
"type": "integer",
"format": "int32",
"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 (*ServerLoopOutRequest) ProtoMessage() {}
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 {
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 (*ServerLoopOutResponse) ProtoMessage() {}
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 {
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 (*ServerLoopOutQuoteRequest) ProtoMessage() {}
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 {
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 (*ServerLoopOutQuote) ProtoMessage() {}
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 {
return xxx_messageInfo_ServerLoopOutQuote.Unmarshal(m, b)
@ -256,11 +256,223 @@ func (m *ServerLoopOutQuote) GetCltvDelta() int32 {
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() {
proto.RegisterType((*ServerLoopOutRequest)(nil), "looprpc.ServerLoopOutRequest")
proto.RegisterType((*ServerLoopOutResponse)(nil), "looprpc.ServerLoopOutResponse")
proto.RegisterType((*ServerLoopOutQuoteRequest)(nil), "looprpc.ServerLoopOutQuoteRequest")
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.
@ -277,6 +489,8 @@ const _ = grpc.SupportPackageIsVersion4
type SwapServerClient interface {
NewLoopOutSwap(ctx context.Context, in *ServerLoopOutRequest, opts ...grpc.CallOption) (*ServerLoopOutResponse, 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 {
@ -305,10 +519,30 @@ func (c *swapServerClient) LoopOutQuote(ctx context.Context, in *ServerLoopOutQu
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.
type SwapServerServer interface {
NewLoopOutSwap(context.Context, *ServerLoopOutRequest) (*ServerLoopOutResponse, 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) {
@ -351,6 +585,42 @@ func _SwapServer_LoopOutQuote_Handler(srv interface{}, ctx context.Context, dec
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{
ServiceName: "looprpc.SwapServer",
HandlerType: (*SwapServerServer)(nil),
@ -363,43 +633,58 @@ var _SwapServer_serviceDesc = grpc.ServiceDesc{
MethodName: "LoopOutQuote",
Handler: _SwapServer_LoopOutQuote_Handler,
},
{
MethodName: "NewLoopInSwap",
Handler: _SwapServer_NewLoopInSwap_Handler,
},
{
MethodName: "LoopInQuote",
Handler: _SwapServer_LoopInQuote_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "server.proto",
}
func init() { proto.RegisterFile("server.proto", fileDescriptor_server_76a33eec530f39a2) }
var fileDescriptor_server_76a33eec530f39a2 = []byte{
// 467 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0xd1, 0x6e, 0xd3, 0x30,
0x14, 0x86, 0x95, 0xb6, 0xeb, 0xe8, 0x59, 0xbb, 0x81, 0x05, 0x28, 0xb4, 0x14, 0x95, 0x48, 0x83,
0x8a, 0x8b, 0x56, 0x82, 0x27, 0xd8, 0x34, 0x21, 0x10, 0x88, 0x42, 0x76, 0xc7, 0x4d, 0x74, 0xda,
0x1e, 0x12, 0x8b, 0xc4, 0x36, 0xb1, 0xd3, 0x36, 0x2f, 0x82, 0x78, 0x0a, 0x9e, 0x11, 0xd9, 0xf1,
0x60, 0x85, 0xb1, 0xbb, 0xe4, 0x3f, 0xdf, 0xf1, 0xf9, 0xfd, 0xe7, 0x04, 0xfa, 0x9a, 0xca, 0x0d,
0x95, 0x33, 0x55, 0x4a, 0x23, 0xd9, 0x61, 0x2e, 0xa5, 0x2a, 0xd5, 0x6a, 0xf8, 0x38, 0x95, 0x32,
0xcd, 0x69, 0x8e, 0x8a, 0xcf, 0x51, 0x08, 0x69, 0xd0, 0x70, 0x29, 0x74, 0x83, 0x45, 0x19, 0xdc,
0xbf, 0x74, 0x6d, 0xef, 0xa5, 0x54, 0x8b, 0xca, 0xc4, 0xf4, 0xad, 0x22, 0x6d, 0xd8, 0x53, 0xe8,
0x97, 0xb4, 0x22, 0xbe, 0xa1, 0x32, 0xf9, 0x4a, 0x75, 0x18, 0x4c, 0x82, 0x69, 0x3f, 0x3e, 0xba,
0xd2, 0xde, 0x51, 0xcd, 0x46, 0xd0, 0xd3, 0x5b, 0x54, 0x49, 0x86, 0x3a, 0x0b, 0x5b, 0xae, 0x7e,
0xc7, 0x0a, 0x6f, 0x50, 0x67, 0xec, 0x2e, 0xb4, 0xb1, 0x30, 0x61, 0x7b, 0x12, 0x4c, 0x3b, 0xb1,
0x7d, 0x8c, 0x7e, 0x04, 0xf0, 0xe0, 0xaf, 0x51, 0x5a, 0x49, 0xa1, 0xc9, 0xce, 0x72, 0x07, 0x71,
0xb1, 0x91, 0x7c, 0x45, 0x6e, 0x56, 0x2f, 0x3e, 0xb2, 0xda, 0xdb, 0x46, 0x62, 0xa7, 0x70, 0xac,
0x4a, 0x52, 0x58, 0xff, 0x86, 0x5a, 0x0e, 0x1a, 0x34, 0xea, 0x15, 0x36, 0x06, 0xd0, 0x24, 0xd6,
0xde, 0x73, 0xdb, 0x79, 0xea, 0x35, 0x8a, 0x75, 0xfc, 0x10, 0xba, 0xb4, 0x53, 0xbc, 0xac, 0xc3,
0xce, 0x24, 0x98, 0x1e, 0xc4, 0xfe, 0x2d, 0x1a, 0xc1, 0xa3, 0x3d, 0x67, 0x9f, 0x2a, 0x69, 0xc8,
0x27, 0x11, 0x7d, 0x6f, 0x01, 0xfb, 0xb7, 0xca, 0x5e, 0xc0, 0x3d, 0x67, 0x5a, 0x61, 0x5d, 0x90,
0x30, 0xc9, 0x9a, 0xb4, 0xf1, 0xce, 0x4f, 0x6c, 0xe1, 0x63, 0xa3, 0x5f, 0xd8, 0x30, 0x23, 0x18,
0x38, 0xf6, 0x0b, 0x51, 0xb2, 0x44, 0xdd, 0x98, 0x6f, 0x37, 0x37, 0x7c, 0x4d, 0x74, 0x8e, 0x9a,
0xf6, 0x98, 0x12, 0x0d, 0x39, 0xf7, 0x7f, 0x98, 0x18, 0x8d, 0xbb, 0x9e, 0x4f, 0xc1, 0x66, 0xdb,
0x71, 0xd9, 0xf6, 0x1a, 0xe5, 0xac, 0x30, 0xec, 0x19, 0x9c, 0x14, 0x5c, 0x24, 0xee, 0x18, 0x2c,
0x64, 0x25, 0x4c, 0x78, 0xe0, 0x98, 0x41, 0xc1, 0xc5, 0xe5, 0x16, 0xd5, 0x99, 0x13, 0x1d, 0x87,
0xbb, 0x3d, 0xae, 0xeb, 0x39, 0xdc, 0x5d, 0xe3, 0xc6, 0x00, 0xab, 0xdc, 0x6c, 0x92, 0x35, 0xe5,
0x06, 0xc3, 0x43, 0x17, 0x59, 0xcf, 0x2a, 0x17, 0x56, 0x78, 0xf9, 0x33, 0x00, 0xb0, 0x74, 0x13,
0x0e, 0x5b, 0xc0, 0xf1, 0x07, 0xda, 0xfa, 0x8c, 0xac, 0xce, 0xc6, 0x33, 0xbf, 0x83, 0xb3, 0x9b,
0x56, 0x6c, 0xf8, 0xe4, 0x7f, 0x65, 0xbf, 0x16, 0x0b, 0xe8, 0xef, 0x25, 0x1e, 0xdd, 0xcc, 0x5f,
0xff, 0x58, 0xc3, 0xd1, 0x2d, 0xcc, 0xf9, 0xf3, 0xcf, 0xa7, 0x29, 0x37, 0x59, 0xb5, 0x9c, 0xad,
0x64, 0x31, 0xcf, 0x79, 0x9a, 0x19, 0xc1, 0x45, 0x9a, 0xe3, 0x52, 0xcf, 0x6d, 0xdb, 0xdc, 0xf7,
0x2e, 0xbb, 0xee, 0xdf, 0x78, 0xf5, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x94, 0x23, 0x1c, 0x6e, 0x52,
0x03, 0x00, 0x00,
func init() { proto.RegisterFile("server.proto", fileDescriptor_server_1f6c9db5f7136644) }
var fileDescriptor_server_1f6c9db5f7136644 = []byte{
// 589 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xdd, 0x6e, 0xd3, 0x4c,
0x14, 0x94, 0x9d, 0x34, 0xfd, 0x72, 0x9a, 0xb4, 0x1f, 0xcb, 0x8f, 0x4c, 0xda, 0xa0, 0xd6, 0x52,
0xa1, 0xe2, 0x22, 0x91, 0xe0, 0x09, 0x5a, 0x55, 0x88, 0x8a, 0x8a, 0x52, 0x97, 0x2b, 0x6e, 0xac,
0x4d, 0x72, 0x88, 0x2d, 0xec, 0xdd, 0xc5, 0xbb, 0xf9, 0x7b, 0x01, 0x1e, 0x01, 0xf1, 0x5c, 0x7d,
0x22, 0xb4, 0xeb, 0x4d, 0x62, 0x27, 0xe9, 0x0f, 0x77, 0xed, 0x9c, 0xf1, 0xd9, 0xd9, 0x99, 0xc9,
0x42, 0x43, 0x62, 0x36, 0xc6, 0xac, 0x23, 0x32, 0xae, 0x38, 0xd9, 0x4e, 0x38, 0x17, 0x99, 0xe8,
0xb7, 0x0e, 0x86, 0x9c, 0x0f, 0x13, 0xec, 0x52, 0x11, 0x77, 0x29, 0x63, 0x5c, 0x51, 0x15, 0x73,
0x26, 0x73, 0x9a, 0x1f, 0xc1, 0xb3, 0x1b, 0xf3, 0xd9, 0x25, 0xe7, 0xe2, 0x6a, 0xa4, 0x02, 0xfc,
0x39, 0x42, 0xa9, 0xc8, 0x11, 0x34, 0x32, 0xec, 0x63, 0x3c, 0xc6, 0x2c, 0xfc, 0x81, 0x33, 0xcf,
0x39, 0x74, 0x4e, 0x1a, 0xc1, 0xce, 0x1c, 0xfb, 0x84, 0x33, 0xb2, 0x0f, 0x75, 0x39, 0xa1, 0x22,
0x8c, 0xa8, 0x8c, 0x3c, 0xd7, 0xcc, 0xff, 0xd3, 0xc0, 0x47, 0x2a, 0x23, 0xf2, 0x3f, 0x54, 0x68,
0xaa, 0xbc, 0xca, 0xa1, 0x73, 0x52, 0x0d, 0xf4, 0x9f, 0xfe, 0x1f, 0x07, 0x9e, 0xaf, 0x1c, 0x25,
0x05, 0x67, 0x12, 0xf5, 0x59, 0x66, 0x51, 0xcc, 0xc6, 0x3c, 0xee, 0xa3, 0x39, 0xab, 0x1e, 0xec,
0x68, 0xec, 0x22, 0x87, 0xc8, 0x31, 0xec, 0x8a, 0x0c, 0x05, 0x9d, 0x2d, 0x48, 0xae, 0x21, 0x35,
0x73, 0x74, 0x4e, 0x6b, 0x03, 0x48, 0x64, 0x03, 0xab, 0xb9, 0x62, 0x34, 0xd5, 0x73, 0x44, 0x2b,
0x7e, 0x01, 0x35, 0x9c, 0x8a, 0x38, 0x9b, 0x79, 0xd5, 0x43, 0xe7, 0x64, 0x2b, 0xb0, 0xff, 0xf9,
0xfb, 0xf0, 0xb2, 0xa4, 0xec, 0x7a, 0xc4, 0x15, 0x5a, 0x27, 0xfc, 0xdf, 0x2e, 0x90, 0xf5, 0x29,
0x79, 0x0b, 0x4f, 0x8c, 0x68, 0x41, 0x67, 0x29, 0x32, 0x15, 0x0e, 0x50, 0x2a, 0xab, 0x7c, 0x4f,
0x0f, 0xbe, 0xe4, 0xf8, 0xb9, 0x36, 0xd3, 0x87, 0xa6, 0xe1, 0x7e, 0x47, 0x0c, 0x7b, 0x54, 0xe6,
0xe2, 0x2b, 0xf9, 0x0d, 0x3f, 0x20, 0x9e, 0x51, 0x89, 0x25, 0x4e, 0x46, 0x15, 0x1a, 0xf5, 0x4b,
0x4e, 0x40, 0x95, 0xb9, 0x9e, 0x75, 0x41, 0x7b, 0x5b, 0x35, 0xde, 0xd6, 0x73, 0xe4, 0x34, 0x55,
0xe4, 0x35, 0xec, 0xa5, 0x31, 0x0b, 0xcd, 0x1a, 0x9a, 0xf2, 0x11, 0x53, 0xde, 0x96, 0xe1, 0x34,
0xd3, 0x98, 0xdd, 0x4c, 0xa8, 0x38, 0x35, 0xa0, 0xe1, 0xd1, 0x69, 0x89, 0x57, 0xb3, 0x3c, 0x3a,
0x2d, 0xf0, 0xda, 0x00, 0xfd, 0x44, 0x8d, 0xc3, 0x01, 0x26, 0x8a, 0x7a, 0xdb, 0xc6, 0xb2, 0xba,
0x46, 0xce, 0x35, 0xe0, 0xff, 0x72, 0xe0, 0xe9, 0xd2, 0x98, 0x0b, 0x36, 0xaf, 0x4e, 0x39, 0x04,
0x67, 0x35, 0x84, 0x7f, 0xab, 0xcd, 0x5a, 0x39, 0xaa, 0x6b, 0xe5, 0xf0, 0xaf, 0x8b, 0x1d, 0xd6,
0x3a, 0x96, 0xbd, 0x7a, 0xa8, 0xc3, 0xcb, 0x46, 0xb8, 0xa5, 0x46, 0xb4, 0xc0, 0x2b, 0xae, 0x2c,
0x15, 0xe2, 0xd6, 0x29, 0xd6, 0x65, 0x31, 0xb4, 0x87, 0xae, 0x65, 0xed, 0x3c, 0x22, 0x6b, 0x77,
0x3d, 0xeb, 0x0d, 0x61, 0x56, 0x1f, 0x19, 0xe6, 0xd6, 0xc3, 0x61, 0xd6, 0x56, 0xc2, 0x7c, 0x77,
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 LoopOutQuote(ServerLoopOutQuoteRequest) returns (ServerLoopOutQuote);
rpc NewLoopInSwap(ServerLoopInRequest) returns (ServerLoopInResponse);
rpc LoopInQuote(ServerLoopInQuoteRequest) returns (ServerLoopInQuoteResponse);
}
message ServerLoopOutRequest {
@ -48,3 +52,26 @@ message ServerLoopOutQuote {
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)
testLoopOutOnChainCltvDelta = int32(30)
testChargeOnChainCltvDelta = int32(100)
testCltvDelta = 50
testSwapFeeBase = btcutil.Amount(21)
testSwapFeeRate = int64(100)
@ -123,3 +124,40 @@ func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, err
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
}

@ -17,6 +17,11 @@ type storeMock struct {
loopOutStoreChan chan loopdb.LoopOutContract
loopOutUpdateChan chan loopdb.SwapState
loopInSwaps map[lntypes.Hash]*loopdb.LoopInContract
loopInUpdates map[lntypes.Hash][]loopdb.SwapState
loopInStoreChan chan loopdb.LoopInContract
loopInUpdateChan chan loopdb.SwapState
t *testing.T
}
@ -33,7 +38,11 @@ func newStoreMock(t *testing.T) *storeMock {
loopOutSwaps: make(map[lntypes.Hash]*loopdb.LoopOutContract),
loopOutUpdates: make(map[lntypes.Hash][]loopdb.SwapState),
t: t,
loopInStoreChan: make(chan loopdb.LoopInContract, 1),
loopInUpdateChan: make(chan loopdb.SwapState, 1),
loopInSwaps: make(map[lntypes.Hash]*loopdb.LoopInContract),
loopInUpdates: make(map[lntypes.Hash][]loopdb.SwapState),
t: t,
}
}
@ -45,17 +54,19 @@ func (s *storeMock) FetchLoopOutSwaps() ([]*loopdb.LoopOut, error) {
for hash, contract := range s.loopOutSwaps {
updates := s.loopOutUpdates[hash]
events := make([]*loopdb.LoopOutEvent, len(updates))
events := make([]*loopdb.LoopEvent, len(updates))
for i, u := range updates {
events[i] = &loopdb.LoopOutEvent{
events[i] = &loopdb.LoopEvent{
State: u,
}
}
swap := &loopdb.LoopOut{
Hash: hash,
Loop: loopdb.Loop{
Hash: hash,
Events: events,
},
Contract: contract,
Events: events,
}
result = append(result, swap)
}
@ -81,6 +92,50 @@ func (s *storeMock) CreateLoopOut(hash lntypes.Hash,
return nil
}
// FetchLoopInSwaps returns all in swaps currently in the store.
func (s *storeMock) FetchLoopInSwaps() ([]*loopdb.LoopIn, error) {
result := []*loopdb.LoopIn{}
for hash, contract := range s.loopInSwaps {
updates := s.loopInUpdates[hash]
events := make([]*loopdb.LoopEvent, len(updates))
for i, u := range updates {
events[i] = &loopdb.LoopEvent{
State: u,
}
}
swap := &loopdb.LoopIn{
Loop: loopdb.Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
result = append(result, swap)
}
return result, nil
}
// CreateLoopIn adds an initiated loop in swap to the store.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) CreateLoopIn(hash lntypes.Hash,
swap *loopdb.LoopInContract) error {
_, ok := s.loopInSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.loopInSwaps[hash] = swap
s.loopInUpdates[hash] = []loopdb.SwapState{}
s.loopInStoreChan <- *swap
return nil
}
// UpdateLoopOut stores a new event for a target loop out swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
@ -101,6 +156,26 @@ func (s *storeMock) UpdateLoopOut(hash lntypes.Hash, time time.Time,
return nil
}
// UpdateLoopIn stores a new event for a target loop in swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
//
// NOTE: Part of the loopdb.SwapStore interface.
func (s *storeMock) UpdateLoopIn(hash lntypes.Hash, time time.Time,
state loopdb.SwapState) error {
updates, ok := s.loopInUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.loopOutUpdates[hash] = updates
s.loopOutUpdateChan <- state
return nil
}
func (s *storeMock) Close() error {
return nil
}
@ -130,6 +205,21 @@ func (s *storeMock) assertLoopOutStored() {
}
}
func (s *storeMock) assertLoopInStored() {
s.t.Helper()
<-s.loopInStoreChan
}
func (s *storeMock) assertLoopInState(expectedState loopdb.SwapState) {
s.t.Helper()
state := <-s.loopOutUpdateChan
if state != expectedState {
s.t.Fatalf("unexpected state")
}
}
func (s *storeMock) assertStorePreimageReveal() {
s.t.Helper()

@ -29,29 +29,26 @@ type swapKit struct {
}
func newSwapKit(hash lntypes.Hash, swapType Type, cfg *swapConfig,
contract *loopdb.SwapContract) (*swapKit, error) {
contract *loopdb.SwapContract, outputType swap.HtlcOutputType) (
*swapKit, error) {
// Compose expected on-chain swap script
htlc, err := swap.NewHtlc(
contract.CltvExpiry, contract.SenderKey,
contract.ReceiverKey, hash,
contract.ReceiverKey, hash, outputType,
cfg.lnd.ChainParams,
)
if err != nil {
return nil, err
}
// Log htlc address for debugging.
htlcAddress, err := htlc.Address(cfg.lnd.ChainParams)
if err != nil {
return nil, err
}
log := &SwapLog{
Hash: hash,
Logger: logger,
}
log.Infof("Htlc address: %v", htlcAddress)
// Log htlc address for debugging.
log.Infof("Htlc address: %v", htlc.Address)
return &swapKit{
swapConfig: *cfg,
@ -72,6 +69,7 @@ func (s *swapKit) sendUpdate(ctx context.Context) error {
SwapType: s.swapType,
LastUpdate: s.lastUpdateTime,
State: s.state,
HtlcAddress: s.htlc.Address,
}
s.log.Infof("state %v", info.State)

@ -2,6 +2,7 @@ package swap
import (
"bytes"
"crypto/sha256"
"errors"
"github.com/btcsuite/btcd/chaincfg"
@ -12,13 +13,27 @@ import (
"github.com/lightningnetwork/lnd/lntypes"
)
// HtlcOutputType defines the output type of the htlc that is published.
type HtlcOutputType uint8
const (
// HtlcP2WSH is a pay-to-witness-script-hash output (segwit only)
HtlcP2WSH HtlcOutputType = iota
// HtlcNP2WSH is a nested pay-to-witness-script-hash output that can be
// paid to be legacy wallets.
HtlcNP2WSH
)
// Htlc contains relevant htlc information from the receiver perspective.
type Htlc struct {
Script []byte
ScriptHash []byte
Hash lntypes.Hash
MaxSuccessWitnessSize int
MaxTimeoutWitnessSize int
Script []byte
PkScript []byte
Hash lntypes.Hash
OutputType HtlcOutputType
ChainParams *chaincfg.Params
Address btcutil.Address
SigScript []byte
}
var (
@ -30,13 +45,15 @@ var (
// the maximum value for cltv expiry to get the maximum (worst case)
// script size.
QuoteHtlc, _ = NewHtlc(
^int32(0), quoteKey, quoteKey, quoteHash,
^int32(0), quoteKey, quoteKey, quoteHash, HtlcP2WSH,
&chaincfg.MainNetParams,
)
)
// NewHtlc returns a new instance.
func NewHtlc(cltvExpiry int32, senderKey, receiverKey [33]byte,
hash lntypes.Hash) (*Htlc, error) {
hash lntypes.Hash, outputType HtlcOutputType,
chainParams *chaincfg.Params) (*Htlc, error) {
script, err := swapHTLCScript(
cltvExpiry, senderKey, receiverKey, hash,
@ -45,39 +62,73 @@ func NewHtlc(cltvExpiry int32, senderKey, receiverKey [33]byte,
return nil, err
}
scriptHash, err := input.WitnessScriptHash(script)
p2wshPkScript, err := input.WitnessScriptHash(script)
if err != nil {
return nil, err
}
// Calculate maximum success witness size
//
// - number_of_witness_elements: 1 byte
// - receiver_sig_length: 1 byte
// - receiver_sig: 73 bytes
// - preimage_length: 1 byte
// - preimage: 33 bytes
// - witness_script_length: 1 byte
// - witness_script: len(script) bytes
maxSuccessWitnessSize := 1 + 1 + 73 + 1 + 33 + 1 + len(script)
// Calculate maximum timeout witness size
//
// - number_of_witness_elements: 1 byte
// - sender_sig_length: 1 byte
// - sender_sig: 73 bytes
// - zero_length: 1 byte
// - zero: 1 byte
// - witness_script_length: 1 byte
// - witness_script: len(script) bytes
maxTimeoutWitnessSize := 1 + 1 + 73 + 1 + 1 + 1 + len(script)
p2wshPkScriptHash := sha256.Sum256(p2wshPkScript)
var pkScript, sigScript []byte
var address btcutil.Address
switch outputType {
case HtlcNP2WSH:
// Generate p2sh script for p2wsh (nested).
hash160 := input.Ripemd160H(p2wshPkScriptHash[:])
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_HASH160)
builder.AddData(hash160)
builder.AddOp(txscript.OP_EQUAL)
pkScript, err = builder.Script()
if err != nil {
return nil, err
}
// Generate a valid sigScript that will allow us to spend the
// p2sh output. The sigScript will contain only a single push of
// the p2wsh witness program corresponding to the matching
// public key of this address.
sigScript, err = txscript.NewScriptBuilder().
AddData(p2wshPkScript).
Script()
if err != nil {
return nil, err
}
address, err = btcutil.NewAddressScriptHash(
p2wshPkScript, chainParams,
)
if err != nil {
return nil, err
}
case HtlcP2WSH:
pkScript = p2wshPkScript
address, err = btcutil.NewAddressWitnessScriptHash(
p2wshPkScriptHash[:],
chainParams,
)
if err != nil {
return nil, err
}
default:
return nil, errors.New("unknown output type")
}
return &Htlc{
Hash: hash,
Script: script,
ScriptHash: scriptHash,
MaxSuccessWitnessSize: maxSuccessWitnessSize,
MaxTimeoutWitnessSize: maxTimeoutWitnessSize,
Hash: hash,
Script: script,
PkScript: pkScript,
OutputType: outputType,
ChainParams: chainParams,
Address: address,
SigScript: sigScript,
}, nil
}
@ -127,17 +178,6 @@ func swapHTLCScript(cltvExpiry int32, senderHtlcKey,
return builder.Script()
}
// Address returns the p2wsh address of the htlc.
func (h *Htlc) Address(chainParams *chaincfg.Params) (
btcutil.Address, error) {
// Skip OP_0 and data length.
return btcutil.NewAddressWitnessScriptHash(
h.ScriptHash[2:],
chainParams,
)
}
// GenSuccessWitness returns the success script to spend this htlc with the
// preimage.
func (h *Htlc) GenSuccessWitness(receiverSig []byte,
@ -178,3 +218,47 @@ func (h *Htlc) GenTimeoutWitness(senderSig []byte) (wire.TxWitness, error) {
return witnessStack, nil
}
// AddSuccessToEstimator adds a successful spend to a weight estimator.
func (h *Htlc) AddSuccessToEstimator(estimator *input.TxWeightEstimator) {
// Calculate maximum success witness size
//
// - number_of_witness_elements: 1 byte
// - receiver_sig_length: 1 byte
// - receiver_sig: 73 bytes
// - preimage_length: 1 byte
// - preimage: 33 bytes
// - witness_script_length: 1 byte
// - witness_script: len(script) bytes
maxSuccessWitnessSize := 1 + 1 + 73 + 1 + 33 + 1 + len(h.Script)
switch h.OutputType {
case HtlcP2WSH:
estimator.AddWitnessInput(maxSuccessWitnessSize)
case HtlcNP2WSH:
estimator.AddNestedP2WSHInput(maxSuccessWitnessSize)
}
}
// AddTimeoutToEstimator adds a timeout spend to a weight estimator.
func (h *Htlc) AddTimeoutToEstimator(estimator *input.TxWeightEstimator) {
// Calculate maximum timeout witness size
//
// - number_of_witness_elements: 1 byte
// - sender_sig_length: 1 byte
// - sender_sig: 73 bytes
// - zero_length: 1 byte
// - zero: 1 byte
// - witness_script_length: 1 byte
// - witness_script: len(script) bytes
maxTimeoutWitnessSize := 1 + 1 + 73 + 1 + 1 + 1 + len(h.Script)
switch h.OutputType {
case HtlcP2WSH:
estimator.AddWitnessInput(maxTimeoutWitnessSize)
case HtlcNP2WSH:
estimator.AddNestedP2WSHInput(maxTimeoutWitnessSize)
}
}

@ -20,10 +20,18 @@ type swapServerClient interface {
GetLoopOutTerms(ctx context.Context) (
*LoopOutTerms, error)
GetLoopInTerms(ctx context.Context) (
*LoopInTerms, error)
NewLoopOutSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount,
receiverKey [33]byte) (
*newLoopOutResponse, error)
NewLoopInSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount,
senderKey [33]byte, swapInvoice string) (
*newLoopInResponse, error)
}
type grpcSwapServerClient struct {
@ -80,6 +88,27 @@ func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) (
}, 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,
swapHash lntypes.Hash, amount btcutil.Amount,
receiverKey [33]byte) (*newLoopOutResponse, error) {
@ -114,6 +143,39 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
}, 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() {
s.conn.Close()
}
@ -143,3 +205,8 @@ type newLoopOutResponse struct {
senderKey [33]byte
expiry int32
}
type newLoopInResponse struct {
receiverKey [33]byte
expiry int32
}

@ -36,6 +36,7 @@ func (s *Sweeper) CreateSweepTx(
// Add HTLC input.
sweepTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: htlcOutpoint,
SignatureScript: htlc.SigScript,
})
// Add output for the destination address.
@ -85,9 +86,12 @@ func (s *Sweeper) CreateSweepTx(
return sweepTx, nil
}
// GetSweepFee calculates the required tx fee.
// GetSweepFee calculates the required tx fee to spend to P2WKH. It takes a
// function that is expected to add the weight of the input to the weight
// estimator.
func (s *Sweeper) GetSweepFee(ctx context.Context,
htlcSuccessWitnessSize int, sweepConfTarget int32) (
addInputEstimate func(*input.TxWeightEstimator),
sweepConfTarget int32) (
btcutil.Amount, error) {
// Get fee estimate from lnd.
@ -99,7 +103,7 @@ func (s *Sweeper) GetSweepFee(ctx context.Context,
// Calculate weight for this tx.
var weightEstimate input.TxWeightEstimator
weightEstimate.AddP2WKHOutput()
weightEstimate.AddWitnessInput(htlcSuccessWitnessSize)
addInputEstimate(&weightEstimate)
weight := weightEstimate.Weight()
return feeRate.FeeForWeight(int64(weight)), nil

@ -7,6 +7,7 @@ import (
"time"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32"
@ -140,7 +141,9 @@ func (ctx *Context) AssertPaid(
done := func(result error) {
select {
case swapPayment.Done <- result:
case swapPayment.Done <- lndclient.PaymentResult{
Err: result,
}:
case <-time.After(Timeout):
ctx.T.Fatalf("payment result not consumed")
}
@ -206,6 +209,7 @@ func (ctx *Context) DecodeInvoice(request string) *zpay32.Invoice {
return payReq
}
// GetOutputIndex returns the index in the tx outs of the given script hash.
func (ctx *Context) GetOutputIndex(tx *wire.MsgTx,
script []byte) int {
@ -226,15 +230,4 @@ func (ctx *Context) NotifyServerHeight(height int32) {
if err := ctx.Lnd.NotifyHeight(height); err != nil {
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"
"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/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
@ -43,10 +43,10 @@ func (s *mockInvoices) CancelInvoice(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) {
updateChan := make(chan channeldb.ContractState, 2)
updateChan := make(chan lndclient.InvoiceUpdate, 2)
errChan := make(chan error)
select {

@ -1,15 +1,14 @@
package test
import (
"crypto/rand"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
@ -29,44 +28,9 @@ func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string,
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{
PaymentRequest: invoice,
Done: mockChan,
Done: done,
}
return done
@ -92,10 +56,11 @@ func (h *mockLightningClient) GetInfo(ctx context.Context) (*lndclient.Info,
}, nil
}
func (h *mockLightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) (
lnwire.MilliSatoshi, error) {
func (h *mockLightningClient) EstimateFeeToP2WSH(ctx context.Context,
amt btcutil.Amount, confTarget int32) (btcutil.Amount,
error) {
return 0, nil
return 3000, nil
}
func (h *mockLightningClient) AddInvoice(ctx context.Context,
@ -105,10 +70,15 @@ func (h *mockLightningClient) AddInvoice(ctx context.Context,
defer h.lnd.lock.Unlock()
var hash lntypes.Hash
if in.Hash != nil {
switch {
case in.Hash != nil:
hash = *in.Hash
} else {
case in.Preimage != nil:
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.

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

@ -3,14 +3,17 @@ package test
import (
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/zpay32"
"os"
"runtime/pprof"
"testing"
"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 (
@ -20,6 +23,8 @@ var (
// ErrTimeout is returned on 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.
@ -63,6 +68,27 @@ func EncodePayReq(payReq *zpay32.Invoice) (string, error) {
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.
func DumpGoroutines() {
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)

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

Loading…
Cancel
Save