Lightning Loop initial version

pull/1/head
Joost Jager 5 years ago
parent c5eecda492
commit 21fcd8d94e
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7

63
.gitignore vendored

@ -1,12 +1,59 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
# ---> Go
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
*.dylib
# Test binary, build with `go test -c`
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
output*.log
swapcli
!swapcli/
*.key
*.hex
# vim
*.swp
*.hex
*.db
*.bin
vendor
*.idea
*.iml
profile.cov
profile.tmp
.DS_Store
.vscode
nautserver
!nautserver/
nautview
!nautview/
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
swapd
!swapd/

@ -0,0 +1,96 @@
# Swaplet
## Uncharge swap (off -> on-chain)
```
swapcli uncharge 500
|
|
v
.-----------------------------.
| Swap CLI |
| ./cmd/swapcli |
| |
| |
| .-------------------. | .--------------. .---------------.
| | Swap Client (lib) | | | LND node | | Bitcoin node |
| | ./ |<-------------| |-------------------| |
| | | | | | on-chain | |
| | |------------->| | htlc | |
| | | | off-chain | | | |
| '-------------------' | htlc '--------------' '---------------'
'-----------------|-----------' | ^
| | |
| v |
| .--. .--.
| _ -( )- _ _ -( )- _
| .--,( ),--. .--,( ),--.
initiate| _.-( )-._ _.-( )-._
swap | ( LIGHTNING NETWORK ) ( BITCOIN NETWORK )
| '-._( )_.-' '-._( )_.-'
| '__,( ),__' '__,( ),__'
| - ._(__)_. - - ._(__)_. -
| | ^
| | |
v v |
.--------------------. off-chain .--------------. .---------------.
| Swap Server | htlc | LND node | | Bitcoin node |
| |<-------------| | | |
| | | | on-chain | |
| | | | htlc | |
| |--------------| |----------------->| |
| | | | | |
'--------------------' '--------------' '---------------'
```
## Setup
LND and the swaplet are using go modules. Make sure that the `GO111MODULE` env variable is set to `on`.
In order to execute a swap, LND needs to be rebuilt with sub servers enabled.
### LND
* Checkout branch `master`
- `make install tags="signrpc walletrpc chainrpc"` to build and install lnd with required sub-servers enabled.
- Make sure there are no macaroons in the lnd dir `~/.lnd/data/chain/bitcoin/mainnet`. If there are, lnd has been started before and in that case, it could be that `admin.macaroon` doesn't contain signer permission. Delete `macaroons.db` and `*.macaroon`.
DO NOT DELETE `wallet.db` !
- Start lnd
### Swaplet
- `git clone git@gitlab.com:lightning-labs/swaplet.git`
- `cd swaplet/cmd`
- `go install ./...`
## Execute a swap
* Swaps are executed by a client daemon process. Run:
`swapd`
By default `swapd` attempts to connect to an lnd instance running on `localhost:10009` and reads the macaroon and tls certificate from `~/.lnd`. This can be altered using command line flags. See `swapd --help`.
`swapd` only listens on localhost and uses an unencrypted and unauthenticated connection.
* To initiate a swap, run:
`swapcli uncharge <amt_msat>`
When the swap is initiated successfully, `swapd` will see the process through.
* To query and track the swap status, run `swapcli` without arguments.
## Resume
When `swapd` is terminated (or killed) for whatever reason, it will pickup pending swaps after a restart.
Information about pending swaps is stored persistently in the swap database. Its location is `~/.swaplet/<network>/swapclient.db`.
## Multiple simultaneous swaps
It is possible to execute multiple swaps simultaneously.

@ -0,0 +1,322 @@
package client
import (
"context"
"encoding/hex"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/sweep"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
// ErrSwapFeeTooHigh is returned when the swap invoice amount is too
// high.
ErrSwapFeeTooHigh = errors.New("swap fee too high")
// ErrPrepayAmountTooHigh is returned when the prepay invoice amount is
// too high.
ErrPrepayAmountTooHigh = errors.New("prepay amount too high")
// ErrSwapAmountTooLow is returned when the requested swap amount is
// less than the server minimum.
ErrSwapAmountTooLow = errors.New("swap amount too low")
// ErrSwapAmountTooHigh is returned when the requested swap amount is
// more than the server maximum.
ErrSwapAmountTooHigh = errors.New("swap amount too high")
// ErrExpiryTooSoon is returned when the server proposes an expiry that
// is too soon for us.
ErrExpiryTooSoon = errors.New("swap expiry too soon")
// ErrExpiryTooFar is returned when the server proposes an expiry that
// is too soon for us.
ErrExpiryTooFar = errors.New("swap expiry too far")
serverRPCTimeout = 30 * time.Second
republishDelay = 10 * time.Second
)
// Client performs the client side part of swaps. This interface exists to
// be able to implement a stub.
type Client struct {
started uint32 // To be used atomically.
errChan chan error
lndServices *lndclient.LndServices
sweeper *sweep.Sweeper
executor *executor
resumeReady chan struct{}
wg sync.WaitGroup
clientConfig
}
// NewClient returns a new instance to initiate swaps with.
func NewClient(dbDir string, serverAddress string, insecure bool,
lnd *lndclient.LndServices) (*Client, func(), error) {
store, err := newBoltSwapClientStore(dbDir)
if err != nil {
return nil, nil, err
}
swapServerClient, err := newSwapServerClient(serverAddress, insecure)
if err != nil {
return nil, nil, err
}
config := &clientConfig{
LndServices: lnd,
Server: swapServerClient,
Store: store,
CreateExpiryTimer: func(d time.Duration) <-chan time.Time {
return time.NewTimer(d).C
},
}
sweeper := &sweep.Sweeper{
Lnd: lnd,
}
executor := newExecutor(&executorConfig{
lnd: lnd,
store: store,
sweeper: sweeper,
createExpiryTimer: config.CreateExpiryTimer,
})
client := &Client{
errChan: make(chan error),
clientConfig: *config,
lndServices: lnd,
sweeper: sweeper,
executor: executor,
resumeReady: make(chan struct{}),
}
cleanup := func() {
swapServerClient.Close()
}
return client, cleanup, nil
}
// GetUnchargeSwaps returns a list of all swaps currently in the database.
func (s *Client) GetUnchargeSwaps() ([]*PersistentUncharge, error) {
return s.Store.getUnchargeSwaps()
}
// 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 cancelling the context.
func (s *Client) Run(ctx context.Context,
statusChan chan<- SwapInfo) error {
if !atomic.CompareAndSwapUint32(&s.started, 0, 1) {
return errors.New("swap client can only be started once")
}
// Log connected node.
info, err := s.lndServices.Client.GetInfo(ctx)
if err != nil {
return fmt.Errorf("GetInfo error: %v", err)
}
logger.Infof("Connected to lnd node %v with pubkey %v",
info.Alias, hex.EncodeToString(info.IdentityPubkey[:]),
)
// Setup main context used for cancelation.
mainCtx, mainCancel := context.WithCancel(ctx)
defer mainCancel()
// Query store before starting event loop to prevent new swaps from
// being treated as swaps that need to be resumed.
pendingSwaps, err := s.Store.getUnchargeSwaps()
if err != nil {
return err
}
// Start goroutine to deliver all pending swaps to the main loop.
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.resumeSwaps(mainCtx, pendingSwaps)
// Signal that new requests can be accepted. Otherwise the new
// swap could already have been added to the store and read in
// this goroutine as being a swap that needs to be resumed.
// Resulting in two goroutines executing the same swap.
close(s.resumeReady)
}()
// Main event loop.
err = s.executor.run(mainCtx, statusChan)
// Consider canceled as happy flow.
if err == context.Canceled {
err = nil
}
if err != nil {
logger.Errorf("Swap client terminating: %v", err)
} else {
logger.Info("Swap client terminating")
}
// Cancel all remaining active goroutines.
mainCancel()
// Wait for all to finish.
logger.Debug("Wait for executor to finish")
s.executor.waitFinished()
logger.Debug("Wait for goroutines to finish")
s.wg.Wait()
logger.Info("Swap client terminated")
return err
}
// resumeSwaps restarts all pending swaps from the provided list.
func (s *Client) resumeSwaps(ctx context.Context,
swaps []*PersistentUncharge) {
for _, pend := range swaps {
if pend.State().Type() != StateTypePending {
continue
}
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}
swap, err := resumeUnchargeSwap(ctx, swapCfg, pend)
if err != nil {
logger.Errorf("resuming swap: %v", err)
continue
}
s.executor.initiateSwap(ctx, swap)
}
}
// Uncharge initiates a uncharge swap. It blocks until the swap is
// initiation with the swap server is completed (typically this takes
// only a short amount of time). From there on further status
// information can be acquired through the status channel returned from
// the Run call.
//
// When the call returns, the swap has been persisted and will be
// resumed automatically after restarts.
//
// The return value is a hash that uniquely identifies the new swap.
func (s *Client) Uncharge(globalCtx context.Context,
request *UnchargeRequest) (*lntypes.Hash, error) {
logger.Infof("Uncharge %v to %v (channel: %v)",
request.Amount, request.DestAddr,
request.UnchargeChannel,
)
if err := s.waitForInitialized(globalCtx); err != nil {
return nil, err
}
// Create a new swap object for this swap.
initiationHeight := s.executor.height()
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
server: s.Server,
}
swap, err := newUnchargeSwap(
globalCtx, swapCfg, initiationHeight, request,
)
if err != nil {
return nil, err
}
// Post swap to the main loop.
s.executor.initiateSwap(globalCtx, swap)
// Return hash so that the caller can identify this swap in the updates
// stream.
return &swap.hash, nil
}
// UnchargeQuote takes a Uncharge 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) UnchargeQuote(ctx context.Context,
request *UnchargeQuoteRequest) (*UnchargeQuote, error) {
terms, err := s.Server.GetUnchargeTerms(ctx)
if err != nil {
return nil, err
}
if request.Amount < terms.MinSwapAmount {
return nil, ErrSwapAmountTooLow
}
if request.Amount > terms.MaxSwapAmount {
return nil, ErrSwapAmountTooHigh
}
logger.Infof("Offchain swap destination: %x", terms.SwapPaymentDest)
swapFee := utils.CalcFee(
request.Amount, terms.SwapFeeBase, terms.SwapFeeRate,
)
minerFee, err := s.sweeper.GetSweepFee(
ctx, utils.QuoteHtlc.MaxSuccessWitnessSize,
request.SweepConfTarget,
)
if err != nil {
return nil, err
}
return &UnchargeQuote{
SwapFee: swapFee,
MinerFee: minerFee,
PrepayAmount: btcutil.Amount(terms.PrepayAmt),
}, nil
}
// UnchargeTerms returns the terms on which the server executes swaps.
func (s *Client) UnchargeTerms(ctx context.Context) (
*UnchargeTerms, error) {
return s.Server.GetUnchargeTerms(ctx)
}
// waitForInitialized for swaps to be resumed and executor ready.
func (s *Client) waitForInitialized(ctx context.Context) error {
select {
case <-s.executor.ready:
case <-ctx.Done():
return ctx.Err()
}
select {
case <-s.resumeReady:
case <-ctx.Done():
return ctx.Err()
}
return nil
}

@ -0,0 +1,291 @@
package client
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/test"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
testAddr, _ = btcutil.DecodeAddress(
"rbsHiPKwAgxeo1EQYiyzJTkA8XEmWSVAKx", nil)
testRequest = &UnchargeRequest{
Amount: btcutil.Amount(50000),
DestAddr: testAddr,
MaxMinerFee: 50000,
SweepConfTarget: 2,
MaxSwapFee: 1050,
MaxPrepayAmount: 100,
MaxPrepayRoutingFee: 75000,
MaxSwapRoutingFee: 70000,
}
swapInvoiceDesc = "swap"
prepayInvoiceDesc = "prepay"
)
// TestSuccess tests the uncharge happy flow.
func TestSuccess(t *testing.T) {
defer test.Guard(t)()
ctx := createClientTestContext(t, nil)
// Initiate uncharge.
hash, err := ctx.swapClient.Uncharge(context.Background(), testRequest)
if err != nil {
t.Fatal(err)
}
ctx.assertStored()
ctx.assertStatus(StateInitiated)
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
// Expect client to register for conf
confIntent := ctx.AssertRegisterConf()
testSuccess(ctx, testRequest.Amount, *hash,
signalPrepaymentResult, signalSwapPaymentResult, false,
confIntent,
)
}
// TestFailOffchain tests the handling of swap for which the server failed the
// payments.
func TestFailOffchain(t *testing.T) {
defer test.Guard(t)()
ctx := createClientTestContext(t, nil)
_, err := ctx.swapClient.Uncharge(context.Background(), testRequest)
if err != nil {
t.Fatal(err)
}
ctx.assertStored()
ctx.assertStatus(StateInitiated)
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
ctx.AssertRegisterConf()
signalSwapPaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
signalPrepaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
ctx.assertStatus(StateFailOffchainPayments)
ctx.assertStoreFinished(StateFailOffchainPayments)
ctx.finish()
}
// TestWrongAmount asserts that the client checks the server invoice amounts.
func TestFailWrongAmount(t *testing.T) {
defer test.Guard(t)()
test := func(t *testing.T, modifier func(*serverMock),
expectedErr error) {
ctx := createClientTestContext(t, nil)
// Modify mock for this subtest.
modifier(ctx.serverMock)
_, err := ctx.swapClient.Uncharge(
context.Background(), testRequest,
)
if err != expectedErr {
t.Fatalf("Expected %v, but got %v", expectedErr, err)
}
ctx.finish()
}
t.Run("swap fee too high", func(t *testing.T) {
test(t, func(m *serverMock) {
m.swapInvoiceAmt += 10
}, ErrSwapFeeTooHigh)
})
t.Run("prepay amount too high", func(t *testing.T) {
test(t, func(m *serverMock) {
// Keep total swap fee unchanged, but increase prepaid
// portion.
m.swapInvoiceAmt -= 10
m.prepayInvoiceAmt += 10
}, ErrPrepayAmountTooHigh)
})
}
// TestResume tests that swaps in various states are properly resumed after a
// restart.
func TestResume(t *testing.T) {
defer test.Guard(t)()
t.Run("not expired", func(t *testing.T) {
testResume(t, false, false, true)
})
t.Run("expired not revealed", func(t *testing.T) {
testResume(t, true, false, false)
})
t.Run("expired revealed", func(t *testing.T) {
testResume(t, true, true, true)
})
}
func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) {
defer test.Guard(t)()
preimage := testPreimage
hash := sha256.Sum256(preimage[:])
dest := test.GetDestAddr(t, 0)
amt := btcutil.Amount(50000)
swapPayReq, err := getInvoice(hash, amt, swapInvoiceDesc)
if err != nil {
t.Fatal(err)
}
prePayReq, err := getInvoice(hash, 100, prepayInvoiceDesc)
if err != nil {
t.Fatal(err)
}
_, senderPubKey := test.CreateKey(1)
var senderKey [33]byte
copy(senderKey[:], senderPubKey.SerializeCompressed())
_, receiverPubKey := test.CreateKey(2)
var receiverKey [33]byte
copy(receiverKey[:], receiverPubKey.SerializeCompressed())
state := StateInitiated
if preimageRevealed {
state = StatePreimageRevealed
}
pendingSwap := &PersistentUncharge{
Contract: &UnchargeContract{
DestAddr: dest,
SwapInvoice: swapPayReq,
SweepConfTarget: 2,
MaxSwapRoutingFee: 70000,
SwapContract: SwapContract{
Preimage: preimage,
AmountRequested: amt,
CltvExpiry: 744,
ReceiverKey: receiverKey,
SenderKey: senderKey,
MaxSwapFee: 60000,
PrepayInvoice: prePayReq,
MaxMinerFee: 50000,
},
},
Events: []*PersistentUnchargeEvent{
{
State: state,
},
},
Hash: hash,
}
if expired {
// Set cltv expiry so that it has already expired at the test
// block height.
pendingSwap.Contract.CltvExpiry = 610
}
ctx := createClientTestContext(t, []*PersistentUncharge{pendingSwap})
if preimageRevealed {
ctx.assertStatus(StatePreimageRevealed)
} else {
ctx.assertStatus(StateInitiated)
}
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
// Expect client to register for conf
confIntent := ctx.AssertRegisterConf()
signalSwapPaymentResult(nil)
signalPrepaymentResult(nil)
if !expectSuccess {
ctx.assertStatus(StateFailTimeout)
ctx.assertStoreFinished(StateFailTimeout)
ctx.finish()
return
}
// Because there is no reliable payment yet, an invoice is assumed to be
// paid after resume.
testSuccess(ctx, amt, hash,
func(r error) {},
func(r error) {},
preimageRevealed,
confIntent,
)
}
func testSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
signalPrepaymentResult, signalSwapPaymentResult func(error),
preimageRevealed bool, confIntent *test.ConfRegistration) {
htlcOutpoint := ctx.publishHtlc(confIntent.PkScript, amt)
signalPrepaymentResult(nil)
ctx.AssertRegisterSpendNtfn(confIntent.PkScript)
// Publish tick.
ctx.expiryChan <- testTime
if !preimageRevealed {
ctx.assertStatus(StatePreimageRevealed)
ctx.assertStorePreimageReveal()
}
// Expect client on-chain sweep of HTLC.
sweepTx := ctx.ReceiveTx()
if !bytes.Equal(sweepTx.TxIn[0].PreviousOutPoint.Hash[:],
htlcOutpoint.Hash[:]) {
ctx.T.Fatalf("client not sweeping from htlc tx")
}
// Check preimage.
clientPreImage := sweepTx.TxIn[0].Witness[1]
clientPreImageHash := sha256.Sum256(clientPreImage)
if clientPreImageHash != hash {
ctx.T.Fatalf("incorrect preimage")
}
// Simulate server pulling payment.
signalSwapPaymentResult(nil)
ctx.NotifySpend(sweepTx, 0)
ctx.assertStatus(StateSuccess)
ctx.assertStoreFinished(StateSuccess)
ctx.finish()
}

@ -0,0 +1,15 @@
package client
import (
"time"
"github.com/lightninglabs/nautilus/lndclient"
)
// clientConfig contains config items for the swap client.
type clientConfig struct {
LndServices *lndclient.LndServices
Server swapServerClient
Store swapClientStore
CreateExpiryTimer func(expiry time.Duration) <-chan time.Time
}

@ -0,0 +1,166 @@
package client
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/sweep"
"github.com/lightningnetwork/lnd/queue"
)
// executorConfig contains executor configuration data.
type executorConfig struct {
lnd *lndclient.LndServices
sweeper *sweep.Sweeper
store swapClientStore
createExpiryTimer func(expiry time.Duration) <-chan time.Time
}
// executor is responsible for executing swaps.
type executor struct {
wg sync.WaitGroup
newSwaps chan genericSwap
currentHeight uint32
ready chan struct{}
executorConfig
}
// newExecutor returns a new swap executor instance.
func newExecutor(cfg *executorConfig) *executor {
return &executor{
executorConfig: *cfg,
newSwaps: make(chan genericSwap),
ready: make(chan struct{}),
}
}
// run starts the executor event loop. It accepts and executes new swaps,
// providing them with required config data.
func (s *executor) run(mainCtx context.Context,
statusChan chan<- SwapInfo) error {
blockEpochChan, blockErrorChan, err :=
s.lnd.ChainNotifier.RegisterBlockEpochNtfn(mainCtx)
if err != nil {
return err
}
// Before starting, make sure we have an up to date block height.
// Otherwise we might reveal a preimage for a swap that is already
// expired.
logger.Infof("Wait for first block ntfn")
var height int32
setHeight := func(h int32) {
height = h
atomic.StoreUint32(&s.currentHeight, uint32(h))
}
select {
case h := <-blockEpochChan:
setHeight(int32(h))
case err := <-blockErrorChan:
return err
case <-mainCtx.Done():
return mainCtx.Err()
}
// Start main event loop.
logger.Infof("Starting event loop at height %v", height)
// Signal that executor being ready with an up to date block height.
close(s.ready)
// Use a map to administer the individual notification queues for the
// swaps.
blockEpochQueues := make(map[int]*queue.ConcurrentQueue)
// On exit, stop all queue goroutines.
defer func() {
for _, queue := range blockEpochQueues {
queue.Stop()
}
}()
swapDoneChan := make(chan int)
nextSwapID := 0
for {
select {
case newSwap := <-s.newSwaps:
queue := queue.NewConcurrentQueue(10)
queue.Start()
swapID := nextSwapID
blockEpochQueues[swapID] = queue
s.wg.Add(1)
go func() {
defer s.wg.Done()
newSwap.execute(mainCtx, &executeConfig{
statusChan: statusChan,
sweeper: s.sweeper,
blockEpochChan: queue.ChanOut(),
timerFactory: s.executorConfig.createExpiryTimer,
}, height)
select {
case swapDoneChan <- swapID:
case <-mainCtx.Done():
}
}()
nextSwapID++
case doneID := <-swapDoneChan:
queue, ok := blockEpochQueues[doneID]
if !ok {
return fmt.Errorf(
"swap id %v not found in queues",
doneID)
}
queue.Stop()
delete(blockEpochQueues, doneID)
case h := <-blockEpochChan:
setHeight(int32(h))
for _, queue := range blockEpochQueues {
select {
case queue.ChanIn() <- int32(h):
case <-mainCtx.Done():
return mainCtx.Err()
}
}
case err := <-blockErrorChan:
return fmt.Errorf("block error: %v", err)
case <-mainCtx.Done():
return mainCtx.Err()
}
}
}
// initiateSwap delivers a new swap to the executor main loop.
func (s *executor) initiateSwap(ctx context.Context,
swap genericSwap) {
select {
case s.newSwaps <- swap:
case <-ctx.Done():
return
}
}
// height returns the current height known to the swap server.
func (s *executor) height() int32 {
return int32(atomic.LoadUint32(&s.currentHeight))
}
// waitFinished waits for all swap goroutines to finish.
func (s *executor) waitFinished() {
s.wg.Wait()
}

@ -0,0 +1,236 @@
package client
import (
"time"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes"
)
// UnchargeRequest contains the required parameters for the swap.
type UnchargeRequest struct {
// Amount specifies the requested swap amount in sat. This does not
// include the swap and miner fee.
Amount btcutil.Amount
// Destination address for the swap.
DestAddr btcutil.Address
// MaxSwapRoutingFee is the maximum off-chain fee in msat that may be
// paid for payment to the server. This limit is applied during path
// finding. Typically this value is taken from the response of the
// UnchargeQuote call.
MaxSwapRoutingFee btcutil.Amount
// MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be
// paid for payment to the server. This limit is applied during path
// finding. Typically this value is taken from the response of the
// UnchargeQuote call.
MaxPrepayRoutingFee 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
// MaxPrepayAmount is the maximum amount of the swap fee that may be
// charged as a prepayment.
MaxPrepayAmount btcutil.Amount
// MaxMinerFee is the maximum in on-chain fees that we are willing to
// spent. If we want to sweep the on-chain htlc and the fee estimate
// turns out higher than this value, we cancel the swap. If the fee
// estimate is lower, we publish the sweep tx.
//
// If the sweep tx isn't confirmed, we are forced to ratchet up fees
// until it is swept. Possibly even exceeding MaxMinerFee if we get
// close to the htlc timeout. Because the initial publication revealed
// the preimage, we have no other choice. The server may already have
// pulled the off-chain htlc. Only when the fee becomes higher than the
// swap amount, we can only wait for fees to come down and hope - if we
// are past the timeout - that the server isn't publishing the
// revocation.
//
// MaxMinerFee is typically taken from the response of the
// UnchargeQuote call.
MaxMinerFee btcutil.Amount
// SweepConfTarget specifies the targeted confirmation target for the
// client sweep tx.
SweepConfTarget int32
// UnchargeChannel optionally specifies the short channel id of the
// channel to uncharge.
UnchargeChannel *uint64
}
// UnchargeContract contains the data that is serialized to persistent storage for
// pending swaps.
type UnchargeContract struct {
SwapContract
DestAddr btcutil.Address
SwapInvoice string
// MaxSwapRoutingFee is the maximum off-chain fee in msat that may be
// paid for the swap payment to the server.
MaxSwapRoutingFee btcutil.Amount
// SweepConfTarget specifies the targeted confirmation target for the
// client sweep tx.
SweepConfTarget int32
// UnchargeChannel is the channel to uncharge. If zero, any channel may
// be used.
UnchargeChannel *uint64
}
// UnchargeSwapInfo contains status information for a uncharge swap.
type UnchargeSwapInfo struct {
UnchargeContract
SwapInfoKit
// State where the swap is in.
State SwapState
}
// SwapCost is a breakdown of the final swap costs.
type SwapCost struct {
// Swap is the amount paid to the server.
Server btcutil.Amount
// Onchain is the amount paid to miners for the onchain tx.
Onchain btcutil.Amount
}
// UnchargeQuoteRequest specifies the swap parameters for which a quote is
// requested.
type UnchargeQuoteRequest struct {
// Amount specifies the requested swap amount in sat. This does not
// include the swap and miner fee.
Amount btcutil.Amount
// SweepConfTarget specifies the targeted confirmation target for the
// client sweep tx.
SweepConfTarget int32
// TODO: Add argument to specify confirmation target for server
// publishing htlc. This may influence the swap fee quote, because the
// server needs to pay more for faster confirmations.
//
// TODO: Add arguments to specify maximum total time locks for the
// off-chain swap payment and prepayment. This may influence the
// available routes and off-chain fee estimates. To apply these maximum
// values properly, the server needs to be queried for its required
// final cltv delta values for the off-chain payments.
}
// UnchargeQuote contains estimates for the fees making up the total swap cost
// for the client.
type UnchargeQuote struct {
// SwapFee is the fee that the swap server is charging for the swap.
SwapFee btcutil.Amount
// PrepayAmount is the part of the swap fee that is requested as a
// prepayment.
PrepayAmount btcutil.Amount
// MinerFee is an estimate of the on-chain fee that needs to be paid to
// sweep the htlc.
MinerFee btcutil.Amount
}
// UnchargeTerms are the server terms on which it executes swaps.
type UnchargeTerms struct {
// SwapFeeBase is the fixed per-swap base fee.
SwapFeeBase btcutil.Amount
// SwapFeeRate is the variable fee in parts per million.
SwapFeeRate int64
// PrepayAmt is the fixed part of the swap fee that needs to be prepaid.
PrepayAmt btcutil.Amount
// 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
// SwapPaymentDest is the node pubkey where to swap payment needs to be
// sent to.
SwapPaymentDest [33]byte
}
// SwapContract contains the base data that is serialized to persistent storage
// for pending swaps.
type SwapContract struct {
Preimage lntypes.Preimage
AmountRequested btcutil.Amount
PrepayInvoice string
SenderKey [33]byte
ReceiverKey [33]byte
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
}
// SwapInfoKit contains common swap info fields.
type SwapInfoKit struct {
// Hash is the sha256 hash of the preimage that unlocks the htlcs. It is
// used to uniquely identify this swap.
Hash lntypes.Hash
// LastUpdateTime is the time of the last update of this swap.
LastUpdateTime time.Time
}
// SwapType indicates the type of swap.
type SwapType uint8
const (
// SwapTypeCharge is a charge swap.
SwapTypeCharge SwapType = iota
// SwapTypeUncharge is an uncharge swap.
SwapTypeUncharge
)
// SwapInfo exposes common info fields for charge and uncharge swaps.
type SwapInfo struct {
LastUpdate time.Time
SwapHash lntypes.Hash
State SwapState
SwapType SwapType
SwapContract
}

@ -0,0 +1,24 @@
package client
import (
"github.com/btcsuite/btclog"
"os"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var (
backendLog = btclog.NewBackend(logWriter{})
logger = backendLog.Logger("CLIENT")
servicesLogger = backendLog.Logger("SERVICES")
)
// logWriter implements an io.Writer that outputs to both standard output and
// the write-end pipe of an initialized log rotator.
type logWriter struct{}
func (logWriter) Write(p []byte) (n int, err error) {
os.Stdout.Write(p)
return len(p), nil
}

@ -0,0 +1,125 @@
package client
import (
"context"
"errors"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
)
var (
testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
testUnchargeOnChainCltvDelta = int32(30)
testCltvDelta = 50
testSwapFeeBase = btcutil.Amount(21)
testSwapFeeRate = int64(100)
testInvoiceExpiry = 180 * time.Second
testFixedPrepayAmount = btcutil.Amount(100)
testMinSwapAmount = btcutil.Amount(10000)
testMaxSwapAmount = btcutil.Amount(1000000)
testTxConfTarget = 2
testRepublishDelay = 10 * time.Second
)
// serverMock is used in client unit tests to simulate swap server behaviour.
type serverMock struct {
t *testing.T
expectedSwapAmt btcutil.Amount
swapInvoiceAmt btcutil.Amount
prepayInvoiceAmt btcutil.Amount
height int32
swapInvoice string
swapHash lntypes.Hash
}
func newServerMock() *serverMock {
return &serverMock{
expectedSwapAmt: 50000,
// Total swap fee: 1000 + 0.01 * 50000 = 1050
swapInvoiceAmt: 50950,
prepayInvoiceAmt: 100,
height: 600,
}
}
func (s *serverMock) NewUnchargeSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount,
receiverKey [33]byte) (
*newUnchargeResponse, error) {
_, senderKey := test.CreateKey(100)
if amount != s.expectedSwapAmt {
return nil, errors.New("unexpected test swap amount")
}
swapPayReqString, err := getInvoice(swapHash, s.swapInvoiceAmt,
swapInvoiceDesc)
if err != nil {
return nil, err
}
prePayReqString, err := getInvoice(swapHash, s.prepayInvoiceAmt,
prepayInvoiceDesc)
if err != nil {
return nil, err
}
var senderKeyArray [33]byte
copy(senderKeyArray[:], senderKey.SerializeCompressed())
return &newUnchargeResponse{
senderKey: senderKeyArray,
swapInvoice: swapPayReqString,
prepayInvoice: prePayReqString,
expiry: s.height + testUnchargeOnChainCltvDelta,
}, nil
}
func (s *serverMock) GetUnchargeTerms(ctx context.Context) (
*UnchargeTerms, error) {
dest := [33]byte{1, 2, 3}
return &UnchargeTerms{
SwapFeeBase: testSwapFeeBase,
SwapFeeRate: testSwapFeeRate,
SwapPaymentDest: dest,
CltvDelta: testUnchargeOnChainCltvDelta,
MinSwapAmount: testMinSwapAmount,
MaxSwapAmount: testMaxSwapAmount,
PrepayAmt: testFixedPrepayAmount,
}, nil
}
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.MilliSatoshi(1000*amt)),
)
if err != nil {
return "", err
}
reqString, err := test.EncodePayReq(req)
if err != nil {
return "", err
}
return reqString, nil
}

@ -0,0 +1,17 @@
package client
// SwapStateType defines the types of swap states that exist. Every swap state
// defined as type SwapState above, falls into one of these SwapStateType
// categories.
type SwapStateType uint8
const (
// StateTypePending indicates that the swap is still pending.
StateTypePending SwapStateType = iota
// StateTypeSuccess indicates that the swap has completed successfully.
StateTypeSuccess
// StateTypeFail indicates that the swap has failed.
StateTypeFail
)

@ -0,0 +1,472 @@
package client
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/coreos/bbolt"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
dbFileName = "swapclient.db"
// unchargeSwapsBucketKey is a bucket that contains all swaps that are
// currently pending or completed.
//
// maps: swap_hash -> UnchargeContract
unchargeSwapsBucketKey = []byte("uncharge-swaps")
// unchargeUpdatesBucketKey is a bucket that contains all updates
// pertaining to a swap. This list only ever grows.
//
// maps: update_nr -> time | state
updatesBucketKey = []byte("updates")
// contractKey is the key that stores the serialized swap contract.
contractKey = []byte("contract")
byteOrder = binary.BigEndian
keyLength = 33
)
// boltSwapClientStore stores swap data in boltdb.
type boltSwapClientStore struct {
db *bbolt.DB
}
// newBoltSwapClientStore creates a new client swap store.
func newBoltSwapClientStore(dbPath string) (*boltSwapClientStore, error) {
if !utils.FileExists(dbPath) {
if err := os.MkdirAll(dbPath, 0700); err != nil {
return nil, err
}
}
path := filepath.Join(dbPath, dbFileName)
bdb, err := bbolt.Open(path, 0600, nil)
if err != nil {
return nil, err
}
err = bdb.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(updatesBucketKey)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(metaBucket)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
err = syncVersions(bdb)
if err != nil {
return nil, err
}
return &boltSwapClientStore{
db: bdb,
}, nil
}
// getUnchargeSwaps returns all swaps currently in the store.
func (s *boltSwapClientStore) getUnchargeSwaps() ([]*PersistentUncharge, error) {
var swaps []*PersistentUncharge
err := s.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(unchargeSwapsBucketKey)
if bucket == nil {
return errors.New("bucket does not exist")
}
err := bucket.ForEach(func(k, _ []byte) error {
swapBucket := bucket.Bucket(k)
if swapBucket == nil {
return fmt.Errorf("swap bucket %x not found",
k)
}
contractBytes := swapBucket.Get(contractKey)
if contractBytes == nil {
return errors.New("contract not found")
}
contract, err := deserializeUnchargeContract(
contractBytes,
)
if err != nil {
return err
}
stateBucket := swapBucket.Bucket(updatesBucketKey)
if stateBucket == nil {
return errors.New("updates bucket not found")
}
var updates []*PersistentUnchargeEvent
err = stateBucket.ForEach(func(k, v []byte) error {
event, err := deserializeUnchargeUpdate(v)
if err != nil {
return err
}
updates = append(updates, event)
return nil
})
if err != nil {
return err
}
var hash lntypes.Hash
copy(hash[:], k)
swap := PersistentUncharge{
Contract: contract,
Hash: hash,
Events: updates,
}
swaps = append(swaps, &swap)
return nil
})
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return swaps, nil
}
// createUncharge adds an initiated swap to the store.
func (s *boltSwapClientStore) createUncharge(hash lntypes.Hash,
swap *UnchargeContract) error {
if hash != swap.Preimage.Hash() {
return errors.New("hash and preimage do not match")
}
return s.db.Update(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey)
if err != nil {
return err
}
if bucket.Get(hash[:]) != nil {
return fmt.Errorf("swap %v already exists", swap.Preimage)
}
// Create bucket for swap.
swapBucket, err := bucket.CreateBucket(hash[:])
if err != nil {
return err
}
contract, err := serializeUnchargeContract(swap)
if err != nil {
return err
}
// Store contact.
if err := swapBucket.Put(contractKey, contract); err != nil {
return err
}
// Create empty updates bucket.
_, err = swapBucket.CreateBucket(updatesBucketKey)
return err
})
}
// updateUncharge stores a swap updateUncharge.
func (s *boltSwapClientStore) updateUncharge(hash lntypes.Hash, time time.Time,
state SwapState) error {
return s.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(unchargeSwapsBucketKey)
if bucket == nil {
return errors.New("bucket does not exist")
}
swapBucket := bucket.Bucket(hash[:])
if swapBucket == nil {
return errors.New("swap not found")
}
updateBucket := swapBucket.Bucket(updatesBucketKey)
if updateBucket == nil {
return errors.New("udpate bucket not found")
}
id, err := updateBucket.NextSequence()
if err != nil {
return err
}
updateValue, err := serializeUnchargeUpdate(time, state)
if err != nil {
return err
}
return updateBucket.Put(itob(id), updateValue)
})
}
// Close closes the underlying bolt db.
func (s *boltSwapClientStore) close() error {
return s.db.Close()
}
func deserializeUnchargeContract(value []byte) (*UnchargeContract, error) {
r := bytes.NewReader(value)
contract, err := deserializeContract(r)
if err != nil {
return nil, err
}
swap := UnchargeContract{
SwapContract: *contract,
}
addr, err := wire.ReadVarString(r, 0)
if err != nil {
return nil, err
}
swap.DestAddr, err = btcutil.DecodeAddress(addr, nil)
if err != nil {
return nil, err
}
swap.SwapInvoice, err = wire.ReadVarString(r, 0)
if err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.SweepConfTarget); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.MaxSwapRoutingFee); err != nil {
return nil, err
}
var unchargeChannel uint64
if err := binary.Read(r, byteOrder, &unchargeChannel); err != nil {
return nil, err
}
if unchargeChannel != 0 {
swap.UnchargeChannel = &unchargeChannel
}
return &swap, nil
}
func serializeUnchargeContract(swap *UnchargeContract) (
[]byte, error) {
var b bytes.Buffer
serializeContract(&swap.SwapContract, &b)
addr := swap.DestAddr.String()
if err := wire.WriteVarString(&b, 0, addr); err != nil {
return nil, err
}
if err := wire.WriteVarString(&b, 0, swap.SwapInvoice); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.SweepConfTarget); err != nil {
return nil, err
}
if err := binary.Write(&b, byteOrder, swap.MaxSwapRoutingFee); err != nil {
return nil, err
}
var unchargeChannel uint64
if swap.UnchargeChannel != nil {
unchargeChannel = *swap.UnchargeChannel
}
if err := binary.Write(&b, byteOrder, unchargeChannel); 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 {
return nil, err
}
swap.InitiationTime = time.Unix(0, unixNano)
if err := binary.Read(r, byteOrder, &swap.Preimage); err != nil {
return nil, err
}
binary.Read(r, byteOrder, &swap.AmountRequested)
swap.PrepayInvoice, err = wire.ReadVarString(r, 0)
if err != nil {
return nil, err
}
n, err := r.Read(swap.SenderKey[:])
if err != nil {
return nil, err
}
if n != keyLength {
return nil, fmt.Errorf("sender key has invalid length")
}
n, err = r.Read(swap.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, &swap.CltvExpiry); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.MaxMinerFee); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.MaxSwapFee); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.MaxPrepayRoutingFee); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &swap.InitiationHeight); err != nil {
return nil, err
}
return &swap, nil
}
func serializeContract(swap *SwapContract, b *bytes.Buffer) error {
if err := binary.Write(b, byteOrder, swap.InitiationTime.UnixNano()); err != nil {
return err
}
if err := binary.Write(b, byteOrder, swap.Preimage); err != nil {
return err
}
if err := binary.Write(b, byteOrder, swap.AmountRequested); err != nil {
return err
}
if err := wire.WriteVarString(b, 0, swap.PrepayInvoice); err != nil {
return err
}
n, err := b.Write(swap.SenderKey[:])
if err != nil {
return err
}
if n != keyLength {
return fmt.Errorf("sender key has invalid length")
}
n, err = b.Write(swap.ReceiverKey[:])
if err != nil {
return err
}
if n != keyLength {
return 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.MaxMinerFee); err != nil {
return err
}
if err := binary.Write(b, byteOrder, swap.MaxSwapFee); err != nil {
return err
}
if err := binary.Write(b, byteOrder, swap.MaxPrepayRoutingFee); err != nil {
return err
}
if err := binary.Write(b, byteOrder, swap.InitiationHeight); err != nil {
return err
}
return nil
}
func serializeUnchargeUpdate(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
}
func deserializeUnchargeUpdate(value []byte) (*PersistentUnchargeEvent, error) {
update := &PersistentUnchargeEvent{}
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
}
// itob returns an 8-byte big endian representation of v.
func itob(v uint64) []byte {
b := make([]byte, 8)
byteOrder.PutUint64(b, v)
return b
}

@ -0,0 +1,65 @@
package client
import (
"time"
"github.com/lightningnetwork/lnd/lntypes"
)
// swapClientStore provides persistent storage for swaps.
type swapClientStore interface {
// getUnchargeSwaps returns all swaps currently in the store.
getUnchargeSwaps() ([]*PersistentUncharge, error)
// createUncharge adds an initiated swap to the store.
createUncharge(hash lntypes.Hash, swap *UnchargeContract) error
// updateUncharge stores a swap updateUncharge.
updateUncharge(hash lntypes.Hash, time time.Time, state SwapState) error
}
// PersistentUnchargeEvent contains the dynamic data of a swap.
type PersistentUnchargeEvent struct {
State SwapState
Time time.Time
}
// PersistentUncharge is a combination of the contract and the updates.
type PersistentUncharge struct {
Hash lntypes.Hash
Contract *UnchargeContract
Events []*PersistentUnchargeEvent
}
// State returns the most recent state of this swap.
func (s *PersistentUncharge) State() SwapState {
lastUpdate := s.LastUpdate()
if lastUpdate == nil {
return StateInitiated
}
return lastUpdate.State
}
// LastUpdate returns the most recent update of this swap.
func (s *PersistentUncharge) LastUpdate() *PersistentUnchargeEvent {
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.
func (s *PersistentUncharge) LastUpdateTime() time.Time {
lastUpdate := s.LastUpdate()
if lastUpdate == nil {
return s.Contract.InitiationTime
}
return lastUpdate.Time
}

@ -0,0 +1,123 @@
package client
import (
"errors"
"fmt"
"github.com/coreos/bbolt"
)
var (
// metaBucket stores all the meta information concerning the state of
// the database.
metaBucket = []byte("metadata")
// dbVersionKey is a boltdb key and it's used for storing/retrieving
// current database version.
dbVersionKey = []byte("dbp")
// ErrDBReversion is returned when detecting an attempt to revert to a
// prior database version.
ErrDBReversion = fmt.Errorf("channel db cannot revert to prior version")
)
// migration is a function which takes a prior outdated version of the database
// instances and mutates the key/bucket structure to arrive at a more
// up-to-date version of the database.
type migration func(tx *bbolt.Tx) error
var (
// dbVersions is storing all versions of database. If current version
// of database don't match with latest version this list will be used
// for retrieving all migration function that are need to apply to the
// current db.
migrations = []migration{}
latestDBVersion = uint32(len(migrations))
)
// getDBVersion retrieves the current db version.
func getDBVersion(db *bbolt.DB) (uint32, error) {
var version uint32
err := db.View(func(tx *bbolt.Tx) error {
metaBucket := tx.Bucket(metaBucket)
if metaBucket == nil {
return errors.New("bucket does not exist")
}
data := metaBucket.Get(dbVersionKey)
// If no version key found, assume version is 0.
if data != nil {
version = byteOrder.Uint32(data)
}
return nil
})
if err != nil {
return 0, err
}
return version, nil
}
// getDBVersion updates the current db version.
func setDBVersion(tx *bbolt.Tx, version uint32) error {
metaBucket := tx.Bucket(metaBucket)
if metaBucket == nil {
return errors.New("bucket does not exist")
}
scratch := make([]byte, 4)
byteOrder.PutUint32(scratch, version)
return metaBucket.Put(dbVersionKey, scratch)
}
// syncVersions function is used for safe db version synchronization. It
// applies migration functions to the current database and recovers the
// previous state of db if at least one error/panic appeared during migration.
func syncVersions(db *bbolt.DB) error {
currentVersion, err := getDBVersion(db)
if err != nil {
return err
}
logger.Infof("Checking for schema update: latest_version=%v, "+
"db_version=%v", latestDBVersion, currentVersion)
switch {
// If the database reports a higher version that we are aware of, the
// user is probably trying to revert to a prior version of lnd. We fail
// here to prevent reversions and unintended corruption.
case currentVersion > latestDBVersion:
logger.Errorf("Refusing to revert from db_version=%d to "+
"lower version=%d", currentVersion,
latestDBVersion)
return ErrDBReversion
// If the current database version matches the latest version number,
// then we don't need to perform any migrations.
case currentVersion == latestDBVersion:
return nil
}
logger.Infof("Performing database schema migration")
// Otherwise we execute the migrations serially within a single database
// transaction to ensure the migration is atomic.
return db.Update(func(tx *bbolt.Tx) error {
for v := currentVersion; v < latestDBVersion; v++ {
logger.Infof("Applying migration #%v", v+1)
migration := migrations[v]
if err := migration(tx); err != nil {
logger.Infof("Unable to apply migration #%v",
v+1)
return err
}
}
return setDBVersion(tx, latestDBVersion)
})
}

@ -0,0 +1,146 @@
package client
import (
"errors"
"testing"
"time"
"github.com/lightninglabs/nautilus/test"
"github.com/lightningnetwork/lnd/lntypes"
)
// storeMock implements a mock client swap store.
type storeMock struct {
unchargeSwaps map[lntypes.Hash]*UnchargeContract
unchargeUpdates map[lntypes.Hash][]SwapState
unchargeStoreChan chan UnchargeContract
unchargeUpdateChan chan SwapState
t *testing.T
}
type finishData struct {
preimage lntypes.Hash
result SwapState
}
// NewStoreMock instantiates a new mock store.
func newStoreMock(t *testing.T) *storeMock {
return &storeMock{
unchargeStoreChan: make(chan UnchargeContract, 1),
unchargeUpdateChan: make(chan SwapState, 1),
unchargeSwaps: make(map[lntypes.Hash]*UnchargeContract),
unchargeUpdates: make(map[lntypes.Hash][]SwapState),
t: t,
}
}
// getUnchargeSwaps returns all swaps currently in the store.
func (s *storeMock) getUnchargeSwaps() ([]*PersistentUncharge, error) {
result := []*PersistentUncharge{}
for hash, contract := range s.unchargeSwaps {
updates := s.unchargeUpdates[hash]
events := make([]*PersistentUnchargeEvent, len(updates))
for i, u := range updates {
events[i] = &PersistentUnchargeEvent{
State: u,
}
}
swap := &PersistentUncharge{
Hash: hash,
Contract: contract,
Events: events,
}
result = append(result, swap)
}
return result, nil
}
// createUncharge adds an initiated swap to the store.
func (s *storeMock) createUncharge(hash lntypes.Hash,
swap *UnchargeContract) error {
_, ok := s.unchargeSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.unchargeSwaps[hash] = swap
s.unchargeUpdates[hash] = []SwapState{}
s.unchargeStoreChan <- *swap
return nil
}
// Finalize stores the final swap result.
func (s *storeMock) updateUncharge(hash lntypes.Hash, time time.Time,
state SwapState) error {
updates, ok := s.unchargeUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.unchargeUpdates[hash] = updates
s.unchargeUpdateChan <- state
return nil
}
func (s *storeMock) isDone() error {
select {
case <-s.unchargeStoreChan:
return errors.New("storeChan not empty")
default:
}
select {
case <-s.unchargeUpdateChan:
return errors.New("updateChan not empty")
default:
}
return nil
}
func (s *storeMock) assertUnchargeStored() {
s.t.Helper()
select {
case <-s.unchargeStoreChan:
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be stored")
}
}
func (s *storeMock) assertStorePreimageReveal() {
s.t.Helper()
select {
case state := <-s.unchargeUpdateChan:
if state != StatePreimageRevealed {
s.t.Fatalf("unexpected state")
}
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be marked as preimage revealed")
}
}
func (s *storeMock) assertStoreFinished(expectedResult SwapState) {
s.t.Helper()
select {
case state := <-s.unchargeUpdateChan:
if state != expectedResult {
s.t.Fatalf("expected result %v, but got %v",
expectedResult, state)
}
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be finished")
}
}

@ -0,0 +1,131 @@
package client
import (
"crypto/sha256"
"io/ioutil"
"reflect"
"testing"
"time"
"github.com/lightninglabs/nautilus/test"
)
func TestStore(t *testing.T) {
tempDirName, err := ioutil.TempDir("", "clientstore")
if err != nil {
t.Fatal(err)
}
store, err := newBoltSwapClientStore(tempDirName)
if err != nil {
t.Fatal(err)
}
swaps, err := store.getUnchargeSwaps()
if err != nil {
t.Fatal(err)
}
if len(swaps) != 0 {
t.Fatal("expected empty store")
}
destAddr := test.GetDestAddr(t, 0)
senderKey := [33]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}
receiverKey := [33]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3}
hash := sha256.Sum256(testPreimage[:])
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
pendingSwap := UnchargeContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
SenderKey: senderKey,
PrepayInvoice: "prepayinvoice",
ReceiverKey: receiverKey,
MaxMinerFee: 10,
MaxSwapFee: 20,
MaxPrepayRoutingFee: 40,
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,
}
checkSwap := func(expectedState SwapState) {
t.Helper()
swaps, err := store.getUnchargeSwaps()
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(),
)
}
}
err = store.createUncharge(hash, &pendingSwap)
if err != nil {
t.Fatal(err)
}
checkSwap(StateInitiated)
err = store.createUncharge(hash, &pendingSwap)
if err == nil {
t.Fatal("expected error on storing duplicate")
}
checkSwap(StateInitiated)
if err := store.updateUncharge(hash, testTime, StatePreimageRevealed); err != nil {
t.Fatal(err)
}
checkSwap(StatePreimageRevealed)
if err := store.updateUncharge(hash, testTime, StateFailInsufficientValue); err != nil {
t.Fatal(err)
}
checkSwap(StateFailInsufficientValue)
err = store.close()
if err != nil {
t.Fatal(err)
}
// Reopen store
store, err = newBoltSwapClientStore(tempDirName)
if err != nil {
t.Fatal(err)
}
checkSwap(StateFailInsufficientValue)
}

@ -0,0 +1,96 @@
package client
import (
"context"
"time"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/lntypes"
)
type swapKit struct {
htlc *utils.Htlc
hash lntypes.Hash
height int32
log *utils.SwapLog
lastUpdateTime time.Time
cost SwapCost
state SwapState
executeConfig
swapConfig
contract *SwapContract
swapType SwapType
}
func newSwapKit(hash lntypes.Hash, swapType SwapType, cfg *swapConfig,
contract *SwapContract) (*swapKit, error) {
// Compose expected on-chain swap script
htlc, err := utils.NewHtlc(
contract.CltvExpiry, contract.SenderKey,
contract.ReceiverKey, hash,
)
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 := &utils.SwapLog{
Hash: hash,
Logger: logger,
}
log.Infof("Htlc address: %v", htlcAddress)
return &swapKit{
swapConfig: *cfg,
hash: hash,
log: log,
htlc: htlc,
state: StateInitiated,
contract: contract,
swapType: swapType,
}, nil
}
// sendUpdate reports an update to the swap state.
func (s *swapKit) sendUpdate(ctx context.Context) error {
info := &SwapInfo{
SwapContract: *s.contract,
SwapHash: s.hash,
SwapType: s.swapType,
LastUpdate: s.lastUpdateTime,
State: s.state,
}
s.log.Infof("state %v", info.State)
select {
case s.statusChan <- *info:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
type genericSwap interface {
execute(mainCtx context.Context, cfg *executeConfig,
height int32) error
}
type swapConfig struct {
lnd *lndclient.LndServices
store swapClientStore
server swapServerClient
}

@ -0,0 +1,143 @@
package client
import (
"context"
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/rpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
type swapServerClient interface {
GetUnchargeTerms(ctx context.Context) (
*UnchargeTerms, error)
NewUnchargeSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount,
receiverKey [33]byte) (
*newUnchargeResponse, error)
}
type grpcSwapServerClient struct {
server rpc.SwapServerClient
conn *grpc.ClientConn
}
func newSwapServerClient(address string, insecure bool) (*grpcSwapServerClient, error) {
serverConn, err := getSwapServerConn(address, insecure)
if err != nil {
return nil, err
}
server := rpc.NewSwapServerClient(serverConn)
return &grpcSwapServerClient{
conn: serverConn,
server: server,
}, nil
}
func (s *grpcSwapServerClient) GetUnchargeTerms(ctx context.Context) (
*UnchargeTerms, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
defer rpcCancel()
quoteResp, err := s.server.UnchargeQuote(rpcCtx,
&rpc.ServerUnchargeQuoteRequest{},
)
if err != nil {
return nil, err
}
dest, err := hex.DecodeString(quoteResp.SwapPaymentDest)
if err != nil {
return nil, err
}
if len(dest) != 33 {
return nil, errors.New("invalid payment dest")
}
var destArray [33]byte
copy(destArray[:], dest)
return &UnchargeTerms{
MinSwapAmount: btcutil.Amount(quoteResp.MinSwapAmount),
MaxSwapAmount: btcutil.Amount(quoteResp.MaxSwapAmount),
PrepayAmt: btcutil.Amount(quoteResp.PrepayAmt),
SwapFeeBase: btcutil.Amount(quoteResp.SwapFeeBase),
SwapFeeRate: quoteResp.SwapFeeRate,
CltvDelta: quoteResp.CltvDelta,
SwapPaymentDest: destArray,
}, nil
}
func (s *grpcSwapServerClient) NewUnchargeSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, receiverKey [33]byte) (
*newUnchargeResponse, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout)
defer rpcCancel()
swapResp, err := s.server.NewUnchargeSwap(rpcCtx,
&rpc.ServerUnchargeSwapRequest{
SwapHash: swapHash[:],
Amt: uint64(amount),
ReceiverKey: receiverKey[:],
},
)
if err != nil {
return nil, err
}
var senderKey [33]byte
copy(senderKey[:], swapResp.SenderKey)
// Validate sender key.
_, err = btcec.ParsePubKey(senderKey[:], btcec.S256())
if err != nil {
return nil, fmt.Errorf("invalid sender key: %v", err)
}
return &newUnchargeResponse{
swapInvoice: swapResp.SwapInvoice,
prepayInvoice: swapResp.PrepayInvoice,
senderKey: senderKey,
expiry: swapResp.Expiry,
}, nil
}
func (s *grpcSwapServerClient) Close() {
s.conn.Close()
}
// getSwapServerConn returns a connection to the swap server.
func getSwapServerConn(address string, insecure bool) (*grpc.ClientConn, error) {
// Create a dial options array.
opts := []grpc.DialOption{}
if insecure {
opts = append(opts, grpc.WithInsecure())
} else {
creds := credentials.NewTLS(&tls.Config{})
opts = append(opts, grpc.WithTransportCredentials(creds))
}
conn, err := grpc.Dial(address, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v", err)
}
return conn, nil
}
type newUnchargeResponse struct {
swapInvoice string
prepayInvoice string
senderKey [33]byte
expiry int32
}

@ -0,0 +1,234 @@
package client
import (
"context"
"testing"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/sweep"
"github.com/lightninglabs/nautilus/test"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
testPreimage = lntypes.Preimage([32]byte{
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4,
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4,
})
testPrepayPreimage = lntypes.Preimage([32]byte{
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4,
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 5,
})
testStartingHeight = uint32(600)
)
// testContext contains functionality to support client unit tests.
type testContext struct {
test.Context
serverMock *serverMock
swapClient *Client
statusChan chan SwapInfo
store *storeMock
expiryChan chan time.Time
runErr chan error
stop func()
}
func newSwapClient(config *clientConfig) *Client {
sweeper := &sweep.Sweeper{
Lnd: config.LndServices,
}
lndServices := config.LndServices
executor := newExecutor(&executorConfig{
lnd: lndServices,
store: config.Store,
sweeper: sweeper,
createExpiryTimer: config.CreateExpiryTimer,
})
return &Client{
errChan: make(chan error),
clientConfig: *config,
lndServices: lndServices,
sweeper: sweeper,
executor: executor,
resumeReady: make(chan struct{}),
}
}
func createClientTestContext(t *testing.T,
pendingSwaps []*PersistentUncharge) *testContext {
serverMock := newServerMock()
clientLnd := test.NewMockLnd()
store := newStoreMock(t)
for _, s := range pendingSwaps {
store.unchargeSwaps[s.Hash] = s.Contract
updates := []SwapState{}
for _, e := range s.Events {
updates = append(updates, e.State)
}
store.unchargeUpdates[s.Hash] = updates
}
expiryChan := make(chan time.Time)
timerFactory := func(expiry time.Duration) <-chan time.Time {
return expiryChan
}
swapClient := newSwapClient(&clientConfig{
LndServices: &clientLnd.LndServices,
Server: serverMock,
Store: store,
CreateExpiryTimer: timerFactory,
})
statusChan := make(chan SwapInfo)
ctx := &testContext{
Context: test.NewContext(
t,
clientLnd,
),
swapClient: swapClient,
statusChan: statusChan,
expiryChan: expiryChan,
store: store,
serverMock: serverMock,
}
ctx.runErr = make(chan error)
runCtx, stop := context.WithCancel(context.Background())
ctx.stop = stop
go func() {
ctx.runErr <- swapClient.Run(
runCtx,
statusChan,
)
}()
return ctx
}
func (ctx *testContext) finish() {
ctx.stop()
select {
case err := <-ctx.runErr:
if err != nil {
ctx.T.Fatal(err)
}
case <-time.After(test.Timeout):
ctx.T.Fatal("client not stopping")
}
ctx.assertIsDone()
}
// notifyHeight notifies swap client of the arrival of a new block and waits for
// the notification to be processed by selecting on a dedicated test channel.
func (ctx *testContext) notifyHeight(height int32) {
ctx.T.Helper()
if err := ctx.Lnd.NotifyHeight(height); err != nil {
ctx.T.Fatal(err)
}
}
func (ctx *testContext) assertIsDone() {
if err := ctx.Lnd.IsDone(); err != nil {
ctx.T.Fatal(err)
}
if err := ctx.store.isDone(); err != nil {
ctx.T.Fatal(err)
}
select {
case <-ctx.statusChan:
ctx.T.Fatalf("not all status updates read")
default:
}
}
func (ctx *testContext) assertStored() {
ctx.T.Helper()
ctx.store.assertUnchargeStored()
}
func (ctx *testContext) assertStorePreimageReveal() {
ctx.T.Helper()
ctx.store.assertStorePreimageReveal()
}
func (ctx *testContext) assertStoreFinished(expectedResult SwapState) {
ctx.T.Helper()
ctx.store.assertStoreFinished(expectedResult)
}
func (ctx *testContext) assertStatus(expectedState SwapState) {
ctx.T.Helper()
for {
select {
case update := <-ctx.statusChan:
if update.SwapType != SwapTypeUncharge {
continue
}
if update.State == expectedState {
return
}
case <-time.After(test.Timeout):
ctx.T.Fatalf("expected status %v not "+
"received in time", expectedState)
}
}
}
func (ctx *testContext) publishHtlc(script []byte, amt btcutil.Amount) wire.OutPoint {
// Create the htlc tx.
htlcTx := wire.MsgTx{}
htlcTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{},
})
htlcTx.AddTxOut(&wire.TxOut{
PkScript: script,
Value: int64(amt),
})
htlcTxHash := htlcTx.TxHash()
// Signal client that script has been published.
select {
case ctx.Lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: &htlcTx,
}:
case <-time.After(test.Timeout):
ctx.T.Fatalf("htlc confirmed not consumed")
}
return wire.OutPoint{
Hash: htlcTxHash,
Index: 0,
}
}

@ -0,0 +1,675 @@
package client
import (
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/sweep"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
// MinUnchargePreimageRevealDelta configures the minimum number of remaining
// blocks before htlc expiry required to reveal preimage.
MinUnchargePreimageRevealDelta = int32(20)
)
// unchargeSwap contains all the in-memory state related to a pending uncharge
// swap.
type unchargeSwap struct {
swapKit
UnchargeContract
swapPaymentChan chan lndclient.PaymentResult
prePaymentChan chan lndclient.PaymentResult
}
// executeConfig contains extra configuration to execute the swap.
type executeConfig struct {
sweeper *sweep.Sweeper
statusChan chan<- SwapInfo
blockEpochChan <-chan interface{}
timerFactory func(d time.Duration) <-chan time.Time
}
// newUnchargeSwap initiates a new swap with the server and returns a
// corresponding swap object.
func newUnchargeSwap(globalCtx context.Context, cfg *swapConfig,
currentHeight int32, request *UnchargeRequest) (*unchargeSwap, error) {
// Generate random preimage.
var swapPreimage [32]byte
if _, err := rand.Read(swapPreimage[:]); err != nil {
logger.Error("Cannot generate preimage")
}
swapHash := lntypes.Hash(sha256.Sum256(swapPreimage[:]))
// Derive a receiver key for this swap.
keyDesc, err := cfg.lnd.WalletKit.DeriveNextKey(
globalCtx, utils.SwapKeyFamily,
)
if err != nil {
return nil, err
}
var receiverKey [33]byte
copy(receiverKey[:], keyDesc.PubKey.SerializeCompressed())
// Post the swap parameters to the swap server. The response contains
// the server revocation key and the swap and prepay invoices.
logger.Infof("Initiating swap request at height %v", currentHeight)
swapResp, err := cfg.server.NewUnchargeSwap(globalCtx, swapHash,
request.Amount, receiverKey,
)
if err != nil {
return nil, fmt.Errorf("cannot initiate swap: %v", err)
}
err = validateUnchargeContract(cfg.lnd, currentHeight, request, swapResp)
if err != nil {
return nil, err
}
// Instantie a struct that contains all required data to start the swap.
initiationTime := time.Now()
contract := UnchargeContract{
SwapInvoice: swapResp.swapInvoice,
DestAddr: request.DestAddr,
MaxSwapRoutingFee: request.MaxSwapRoutingFee,
SweepConfTarget: request.SweepConfTarget,
UnchargeChannel: request.UnchargeChannel,
SwapContract: 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,
},
}
swapKit, err := newSwapKit(
swapHash, SwapTypeUncharge, cfg, &contract.SwapContract,
)
if err != nil {
return nil, err
}
swapKit.lastUpdateTime = initiationTime
swap := &unchargeSwap{
UnchargeContract: 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.createUncharge(swapHash, &swap.UnchargeContract)
if err != nil {
return nil, fmt.Errorf("cannot store swap: %v", err)
}
return swap, nil
}
// resumeUnchargeSwap returns a swap object representing a pending swap that has
// been restored from the database.
func resumeUnchargeSwap(reqContext context.Context, cfg *swapConfig,
pend *PersistentUncharge) (*unchargeSwap, error) {
hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:]))
logger.Infof("Resuming swap %v", hash)
swapKit, err := newSwapKit(
hash, SwapTypeUncharge, cfg, &pend.Contract.SwapContract,
)
if err != nil {
return nil, err
}
swap := &unchargeSwap{
UnchargeContract: *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
}
// execute starts/resumes the swap. It is a thin wrapper around
// executeAndFinalize to conveniently handle the error case.
func (s *unchargeSwap) execute(mainCtx context.Context,
cfg *executeConfig, height int32) error {
s.executeConfig = *cfg
s.height = height
err := s.executeAndFinalize(mainCtx)
// If an unexpected error happened, report a temporary failure.
// Otherwise for example a connection error could lead to abandoning the
// swap permanently and losing funds.
if err != nil {
s.log.Errorf("Swap error: %v", err)
s.state = StateFailTemporary
// If we cannot send out this update, there is nothing we can do.
_ = s.sendUpdate(mainCtx)
}
return err
}
// executeAndFinalize executes a swap and awaits the definitive outcome of the
// offchain payments. When this method returns, the swap outcome is final.
func (s *unchargeSwap) executeAndFinalize(globalCtx context.Context) error {
// Announce swap by sending out an initial update.
err := s.sendUpdate(globalCtx)
if err != nil {
return err
}
// Execute swap. When this call returns, the swap outcome is final, but
// it may be that there are still off-chain payments pending.
err = s.executeSwap(globalCtx)
if err != nil {
return err
}
// Sanity check.
if s.state.Type() == StateTypePending {
return fmt.Errorf("swap in non-final state %v", s.state)
}
// Wait until all offchain payments have completed. If payments have
// already completed early, their channels have been set to nil.
s.log.Infof("Wait for server pulling off-chain payment(s)")
for s.swapPaymentChan != nil || s.prePaymentChan != nil {
select {
case result := <-s.swapPaymentChan:
s.swapPaymentChan = nil
if result.Err != nil {
// Server didn't pull the swap payment.
s.log.Infof("Swap payment failed: %v",
result.Err)
continue
}
s.cost.Server += result.PaidAmt
case result := <-s.prePaymentChan:
s.prePaymentChan = nil
if result.Err != nil {
// Server didn't pull the prepayment.
s.log.Infof("Prepayment failed: %v",
result.Err)
continue
}
s.cost.Server += result.PaidAmt
case <-globalCtx.Done():
return globalCtx.Err()
}
}
// Mark swap completed in store.
s.log.Infof("Swap completed: %v "+
"(final cost: server %v, onchain %v)",
s.state,
s.cost.Server,
s.cost.Onchain,
)
return s.persistState(globalCtx)
}
// executeSwap executes the swap, but returns as soon as the swap outcome is
// final. At that point, there may still be pending off-chain payment(s).
func (s *unchargeSwap) executeSwap(globalCtx context.Context) error {
// We always pay both invoices (again). This is currently the only way
// to sort of resume payments.
//
// TODO: We shouldn't pay the invoices if it is already too late to
// start the swap. But because we don't know if we already fired the
// payments in a previous run, we cannot just abandon here.
s.payInvoices(globalCtx)
// Wait for confirmation of the on-chain htlc by watching for a tx
// producing the swap script output.
txConf, err := s.waitForConfirmedHtlc(globalCtx)
if err != nil {
return err
}
// If no error and no confirmation, the swap is aborted without an
// error. The swap state has been updated to a final state.
if txConf == nil {
return nil
}
// TODO: Off-chain payments can be canceled here. Most probably the HTLC
// is accepted by the server, but in case there are not for whatever
// reason, we don't need to have mission control start another payment
// attempt.
// Retrieve outpoint for sweep.
htlcOutpoint, htlcValue, err := utils.GetScriptOutput(
txConf.Tx, s.htlc.ScriptHash,
)
if err != nil {
return err
}
s.log.Infof("Htlc value: %v", htlcValue)
// Verify amount if preimage hasn't been revealed yet.
if s.state != StatePreimageRevealed && htlcValue < s.AmountRequested {
logger.Warnf("Swap amount too low, expected %v but received %v",
s.AmountRequested, htlcValue)
s.state = StateFailInsufficientValue
return nil
}
// Try to spend htlc and continue (rbf) until a spend has confirmed.
spendDetails, err := s.waitForHtlcSpendConfirmed(globalCtx,
func() error {
return s.sweep(globalCtx, *htlcOutpoint, htlcValue)
},
)
if err != nil {
return err
}
// Inspect witness stack to see if it is a success transaction. We don't
// just try to match with the hash of our sweep tx, because it may be
// swept by a different (fee) sweep tx from a previous run.
htlcInput, err := getTxInputByOutpoint(
spendDetails.SpendingTx, htlcOutpoint,
)
if err != nil {
return err
}
sweepSuccessful := s.htlc.IsSuccessWitness(htlcInput.Witness)
if sweepSuccessful {
s.cost.Server -= htlcValue
s.cost.Onchain = htlcValue -
btcutil.Amount(spendDetails.SpendingTx.TxOut[0].Value)
s.state = StateSuccess
} else {
s.state = StateFailSweepTimeout
}
return nil
}
// persistState updates the swap state and sends out an update notification.
func (s *unchargeSwap) persistState(ctx context.Context) error {
updateTime := time.Now()
s.lastUpdateTime = updateTime
// Update state in store.
err := s.store.updateUncharge(s.hash, updateTime, s.state)
if err != nil {
return err
}
// Send out swap update
return s.sendUpdate(ctx)
}
// payInvoices pays both swap invoices.
func (s *unchargeSwap) payInvoices(ctx context.Context) {
// Pay the swap invoice.
s.log.Infof("Sending swap payment %v", s.SwapInvoice)
s.swapPaymentChan = s.lnd.Client.PayInvoice(
ctx, s.SwapInvoice, s.MaxSwapRoutingFee,
s.UnchargeContract.UnchargeChannel,
)
// Pay the prepay invoice.
s.log.Infof("Sending prepayment %v", s.PrepayInvoice)
s.prePaymentChan = s.lnd.Client.PayInvoice(
ctx, s.PrepayInvoice, s.MaxPrepayRoutingFee,
nil,
)
}
// waitForConfirmedHtlc waits for a confirmed htlc to appear on the chain. In
// case we haven't revealed the preimage yet, it also monitors block height and
// off-chain payment failure.
func (s *unchargeSwap) waitForConfirmedHtlc(globalCtx context.Context) (
*chainntnfs.TxConfirmation, error) {
// Wait for confirmation of the on-chain htlc by watching for a tx
// producing the swap script output.
s.log.Infof(
"Register conf ntfn for swap script on chain (hh=%v)",
s.InitiationHeight,
)
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
htlcConfChan, htlcErrChan, err :=
s.lnd.ChainNotifier.RegisterConfirmationsNtfn(
ctx, nil, s.htlc.ScriptHash, 1,
s.InitiationHeight,
)
if err != nil {
return nil, err
}
var txConf *chainntnfs.TxConfirmation
if s.state == StateInitiated {
// Check if it is already too late to start this swap. If we
// already revealed the preimage, this check is irrelevant and
// we need to sweep in any case.
maxPreimageRevealHeight := s.CltvExpiry -
MinUnchargePreimageRevealDelta
checkMaxRevealHeightExceeded := func() bool {
s.log.Infof("Checking preimage reveal height %v "+
"exceeded (height %v)",
maxPreimageRevealHeight, s.height)
if s.height <= maxPreimageRevealHeight {
return false
}
s.log.Infof("Max preimage reveal height %v "+
"exceeded (height %v)",
maxPreimageRevealHeight, s.height)
s.state = StateFailTimeout
return true
}
// First check, because after resume we may otherwise reveal the
// preimage after the max height (depending on order in which
// events are received in the select loop below).
if checkMaxRevealHeightExceeded() {
return nil, nil
}
s.log.Infof("Waiting for either htlc on-chain confirmation or " +
" off-chain payment failure")
loop:
for {
select {
// If the swap payment fails, abandon the swap. We may
// have lost the prepayment.
case result := <-s.swapPaymentChan:
s.swapPaymentChan = nil
if result.Err != nil {
s.state = StateFailOffchainPayments
s.log.Infof("Failed swap payment: %v",
result.Err)
return nil, nil
}
s.cost.Server += result.PaidAmt
// If the prepay fails, abandon the swap. Because we
// didn't reveal the preimage, the swap payment will be
// canceled or time out.
case result := <-s.prePaymentChan:
s.prePaymentChan = nil
if result.Err != nil {
s.state = StateFailOffchainPayments
s.log.Infof("Failed prepayment: %v",
result.Err)
return nil, nil
}
s.cost.Server += result.PaidAmt
// Unexpected error on the confirm channel happened,
// abandon the swap.
case err := <-htlcErrChan:
return nil, err
// Htlc got confirmed, continue to sweeping.
case htlcConfNtfn := <-htlcConfChan:
txConf = htlcConfNtfn
break loop
// New block is received. Recheck max reveal height.
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
logger.Infof("Received block %v", s.height)
if checkMaxRevealHeightExceeded() {
return nil, nil
}
// Client quit.
case <-globalCtx.Done():
return nil, globalCtx.Err()
}
}
s.log.Infof("Swap script confirmed on chain")
} else {
s.log.Infof("Retrieving htlc onchain")
select {
case err := <-htlcErrChan:
return nil, err
case htlcConfNtfn := <-htlcConfChan:
txConf = htlcConfNtfn
case <-globalCtx.Done():
return nil, globalCtx.Err()
}
}
s.log.Infof("Htlc tx %v at height %v", txConf.Tx.TxHash(),
txConf.BlockHeight)
return txConf, nil
}
// waitForHtlcSpendConfirmed waits for the htlc to be spent either by our own
// sweep or a server revocation tx. During this process, this function will try
// to spend the htlc every block by calling spendFunc.
//
// TODO: Improve retry/fee increase mechanism. Once in the mempool, server can
// sweep offchain. So we must make sure we sweep successfully before on-chain
// timeout.
func (s *unchargeSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
spendFunc func() error) (*chainntnfs.SpendDetail, error) {
// Register the htlc spend notification.
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
ctx, nil, s.htlc.ScriptHash, s.InitiationHeight,
)
if err != nil {
return nil, fmt.Errorf("register spend ntfn: %v", err)
}
timerChan := s.timerFactory(republishDelay)
for {
select {
// Htlc spend, break loop.
case spendDetails := <-spendChan:
s.log.Infof("Htlc spend by tx: %v", spendDetails.SpenderTxHash)
return spendDetails, nil
// Spend notification error.
case err := <-spendErr:
return nil, err
// New block arrived, update height and restart the republish
// timer.
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
timerChan = s.timerFactory(republishDelay)
// Some time after start or after arrival of a new block, try
// to spend again.
case <-timerChan:
err := spendFunc()
if err != nil {
return nil, err
}
// Context canceled.
case <-globalCtx.Done():
return nil, globalCtx.Err()
}
}
}
// sweep tries to sweep the given htlc to a destination address. It takes into
// account the max miner fee and marks the preimage as revealed when it
// published the tx.
//
// TODO: Use lnd sweeper?
func (s *unchargeSwap) sweep(ctx context.Context,
htlcOutpoint wire.OutPoint,
htlcValue btcutil.Amount) error {
witnessFunc := func(sig []byte) (wire.TxWitness, error) {
return s.htlc.GenSuccessWitness(
sig, s.Preimage,
)
}
// Calculate sweep tx fee
fee, err := s.sweeper.GetSweepFee(
ctx, s.htlc.MaxSuccessWitnessSize,
s.SweepConfTarget,
)
if err != nil {
return err
}
if fee > s.MaxMinerFee {
s.log.Warnf("Required miner fee %v exceeds max of %v",
fee, s.MaxMinerFee)
if s.state == StatePreimageRevealed {
// The currently required fee exceeds the max, but we
// already revealed the preimage. The best we can do now
// is to republish with the max fee.
fee = s.MaxMinerFee
} else {
s.log.Warnf("Not revealing preimage")
return nil
}
}
// Create sweep tx.
sweepTx, err := s.sweeper.CreateSweepTx(
ctx, s.height, s.htlc, htlcOutpoint,
s.ReceiverKey, witnessFunc,
htlcValue, fee, s.DestAddr,
)
if err != nil {
return err
}
// Before publishing the tx, already mark the preimage as revealed. This
// is a precaution in case the publish call never returns and would
// leave us thinking we didn't reveal yet.
if s.state != StatePreimageRevealed {
s.state = StatePreimageRevealed
err := s.persistState(ctx)
if err != nil {
return err
}
}
// Publish tx.
s.log.Infof("Sweep on chain HTLC to address %v with fee %v (tx %v)",
s.DestAddr, fee, sweepTx.TxHash())
err = s.lnd.WalletKit.PublishTransaction(ctx, sweepTx)
if err != nil {
s.log.Warnf("Publish sweep: %v", err)
}
return nil
}
// validateUnchargeContract validates the contract parameters against our
// request.
func validateUnchargeContract(lnd *lndclient.LndServices,
height int32,
request *UnchargeRequest,
response *newUnchargeResponse) error {
// Check invoice amounts.
chainParams := lnd.ChainParams
swapInvoiceAmt, err := utils.GetInvoiceAmt(
chainParams, response.swapInvoice,
)
if err != nil {
return err
}
prepayInvoiceAmt, err := utils.GetInvoiceAmt(
chainParams, response.prepayInvoice,
)
if err != nil {
return err
}
swapFee := swapInvoiceAmt + prepayInvoiceAmt - request.Amount
if swapFee > request.MaxSwapFee {
logger.Warnf("Swap fee %v exceeding maximum of %v",
swapFee, request.MaxSwapFee)
return ErrSwapFeeTooHigh
}
if prepayInvoiceAmt > request.MaxPrepayAmount {
logger.Warnf("Prepay amount %v exceeding maximum of %v",
prepayInvoiceAmt, request.MaxPrepayAmount)
return ErrPrepayAmountTooHigh
}
if response.expiry-height < MinUnchargePreimageRevealDelta {
logger.Warnf("Proposed expiry %v (delta %v) too soon",
response.expiry, response.expiry-height)
return ErrExpiryTooSoon
}
return nil
}

@ -0,0 +1,89 @@
package client
// SwapState indicates the current state of a swap.
type SwapState uint8
const (
// StateInitiated is the initial state of a swap. At that point, the
// initiation call to the server has been made and the payment process
// has been started for the swap and prepayment invoices.
StateInitiated SwapState = iota
// StatePreimageRevealed is reached when the sweep tx publication is
// first attempted. From that point on, we should consider the preimage
// to no longer be secret and we need to do all we can to get the sweep
// confirmed. This state will mostly coalesce with StateHtlcConfirmed,
// except in the case where we wait for fees to come down before we
// sweep.
StatePreimageRevealed
// StateSuccess is the final swap state that is reached when the sweep
// tx has the required confirmation depth (SweepConfDepth) and the
// server pulled the off-chain htlc.
StateSuccess
// 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
// StateFailTimeout indicates that the on-chain htlc wasn't confirmed before
// its expiry or confirmed too late (MinPreimageRevealDelta violated).
StateFailTimeout
// 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
// StateFailInsufficientValue indicates that the published on-chain htlc had
// a value lower than the requested amount.
StateFailInsufficientValue
// StateFailTemporary indicates that the swap cannot progress because
// of an internal error. This is not a final state. Manual intervention
// (like a restart) is required to solve this problem.
StateFailTemporary
// StateHtlcPublished means that the client published the on-chain htlc.
StateHtlcPublished
)
// Type returns the type of the SwapState it is called on.
func (s SwapState) Type() SwapStateType {
if s == StateInitiated || s == StateHtlcPublished ||
s == StatePreimageRevealed || s == StateFailTemporary {
return StateTypePending
}
if s == StateSuccess {
return StateTypeSuccess
}
return StateTypeFail
}
func (s SwapState) String() string {
switch s {
case StateInitiated:
return "Initiated"
case StatePreimageRevealed:
return "PreimageRevealed"
case StateSuccess:
return "Success"
case StateFailOffchainPayments:
return "FailOffchainPayments"
case StateFailTimeout:
return "FailTimeout"
case StateFailSweepTimeout:
return "FailSweepTimeout"
case StateFailInsufficientValue:
return "FailInsufficientValue"
case StateFailTemporary:
return "FailTemporary"
default:
return "Unknown"
}
}

@ -0,0 +1,100 @@
package client
import (
"context"
"errors"
"testing"
"time"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/sweep"
"github.com/lightninglabs/nautilus/test"
)
// TestLateHtlcPublish tests that the client is not revealing the preimage if
// there are not enough blocks left.
func TestLateHtlcPublish(t *testing.T) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx := test.NewContext(t, lnd)
server := newServerMock()
store := newStoreMock(t)
expiryChan := make(chan time.Time)
timerFactory := func(expiry time.Duration) <-chan time.Time {
return expiryChan
}
height := int32(600)
cfg := &swapConfig{
lnd: &lnd.LndServices,
store: store,
server: server,
}
swap, err := newUnchargeSwap(
context.Background(), cfg, height, testRequest,
)
if err != nil {
t.Fatal(err)
}
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
blockEpochChan := make(chan interface{})
statusChan := make(chan SwapInfo)
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), &executeConfig{
statusChan: statusChan,
sweeper: sweeper,
blockEpochChan: blockEpochChan,
timerFactory: timerFactory,
}, height)
if err != nil {
logger.Error(err)
}
errChan <- err
}()
store.assertUnchargeStored()
state := <-statusChan
if state.State != StateInitiated {
t.Fatal("unexpected state")
}
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
// Expect client to register for conf
ctx.AssertRegisterConf()
// // Wait too long before publishing htlc.
blockEpochChan <- int32(swap.CltvExpiry - 10)
signalSwapPaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
signalPrepaymentResult(
errors.New(lndclient.PaymentResultUnknownPaymentHash),
)
store.assertStoreFinished(StateFailTimeout)
status := <-statusChan
if status.State != StateFailTimeout {
t.Fatal("unexpected state")
}
err = <-errChan
if err != nil {
t.Fatal(err)
}
}

@ -0,0 +1,20 @@
package client
import (
"errors"
"github.com/btcsuite/btcd/wire"
)
// getTxInputByOutpoint returns a tx input based on a given input outpoint.
func getTxInputByOutpoint(tx *wire.MsgTx, input *wire.OutPoint) (
*wire.TxIn, error) {
for _, in := range tx.TxIn {
if in.PreviousOutPoint == *input {
return in, nil
}
}
return nil, errors.New("input not found")
}

@ -0,0 +1,300 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"time"
"github.com/lightninglabs/nautilus/utils"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/cmd/swapd/rpc"
"github.com/urfave/cli"
"google.golang.org/grpc"
)
var (
swapdAddress = "localhost:11010"
// Define route independent max routing fees. We have currently no way
// to get a reliable estimate of the routing fees. Best we can do is the
// minimum routing fees, which is not very indicative.
maxRoutingFeeBase = btcutil.Amount(10)
maxRoutingFeeRate = int64(50000)
)
var unchargeCommand = cli.Command{
Name: "uncharge",
Usage: "perform an off-chain to on-chain swap",
ArgsUsage: "amt [addr]",
Description: `
Send the amount in satoshis specified by the amt argument on-chain.
Optionally a BASE58 encoded bitcoin destination address may be
specified. If not specified, a new wallet address will be generated.`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "channel",
Usage: "the 8-byte compact channel ID of the channel to uncharge",
},
},
Action: uncharge,
}
var termsCommand = cli.Command{
Name: "terms",
Usage: "show current server swap terms",
Action: terms,
}
func main() {
app := cli.NewApp()
app.Version = "0.0.1"
app.Usage = "command line interface to swapd"
app.Commands = []cli.Command{unchargeCommand, termsCommand}
app.Action = monitor
err := app.Run(os.Args)
if err != nil {
fmt.Println(err)
}
}
func terms(ctx *cli.Context) error {
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
terms, err := client.GetUnchargeTerms(
context.Background(), &rpc.TermsRequest{},
)
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 *rpc.TermsResponse) {
fmt.Printf("Amount: %d - %d\n",
btcutil.Amount(terms.MinSwapAmount),
btcutil.Amount(terms.MaxSwapAmount),
)
fmt.Printf("Fee: %d + %.4f %% (%d prepaid)\n",
btcutil.Amount(terms.SwapFeeBase),
utils.FeeRateAsPercentage(terms.SwapFeeRate),
btcutil.Amount(terms.PrepayAmt),
)
fmt.Printf("Cltv delta: %v blocks\n", terms.CltvDelta)
}
fmt.Println("Uncharge")
fmt.Println("--------")
printTerms(terms)
return nil
}
func monitor(ctx *cli.Context) error {
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
stream, err := client.Monitor(
context.Background(), &rpc.MonitorRequest{})
if err != nil {
return err
}
for {
swap, err := stream.Recv()
if err != nil {
return fmt.Errorf("recv: %v", err)
}
logSwap(swap)
}
}
func getClient(ctx *cli.Context) (rpc.SwapClientClient, func(), error) {
conn, err := getSwapCliConn(swapdAddress)
if err != nil {
return nil, nil, err
}
cleanup := func() { conn.Close() }
swapCliClient := rpc.NewSwapClientClient(conn)
return swapCliClient, cleanup, nil
}
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
return utils.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
}
type limits struct {
maxSwapRoutingFee btcutil.Amount
maxPrepayRoutingFee btcutil.Amount
maxMinerFee btcutil.Amount
maxSwapFee btcutil.Amount
maxPrepayAmt btcutil.Amount
}
func getLimits(amt btcutil.Amount, quote *rpc.QuoteResponse) *limits {
return &limits{
maxSwapRoutingFee: getMaxRoutingFee(btcutil.Amount(amt)),
maxPrepayRoutingFee: getMaxRoutingFee(btcutil.Amount(
quote.PrepayAmt,
)),
// 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),
}
}
func displayLimits(amt btcutil.Amount, l *limits) error {
totalSuccessMax := l.maxSwapRoutingFee + l.maxPrepayRoutingFee +
l.maxMinerFee + l.maxSwapFee
fmt.Printf("Max swap fees for %d uncharge: %d\n",
btcutil.Amount(amt), totalSuccessMax,
)
fmt.Printf("CONTINUE SWAP? (y/n), expand fee detail (x): ")
var answer string
fmt.Scanln(&answer)
switch answer {
case "y":
return nil
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)
fmt.Printf("Max swap fee: %d\n", l.maxSwapFee)
fmt.Printf("Max no show penalty: %d\n",
l.maxPrepayAmt)
fmt.Printf("CONTINUE SWAP? (y/n): ")
fmt.Scanln(&answer)
if answer == "y" {
return nil
}
}
return errors.New("swap canceled")
}
func parseAmt(text string) (btcutil.Amount, error) {
amtInt64, err := strconv.ParseInt(text, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid amt value")
}
return btcutil.Amount(amtInt64), nil
}
func uncharge(ctx *cli.Context) error {
// Show command help if no arguments and flags were provided.
if ctx.NArg() < 1 {
cli.ShowCommandHelp(ctx, "uncharge")
return nil
}
args := ctx.Args()
amt, err := parseAmt(args[0])
if err != nil {
return err
}
var destAddr string
args = args.Tail()
if args.Present() {
destAddr = args.First()
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
quote, err := client.GetUnchargeQuote(
context.Background(),
&rpc.QuoteRequest{
Amt: int64(amt),
},
)
if err != nil {
return err
}
limits := getLimits(amt, quote)
if err := displayLimits(amt, limits); err != nil {
return err
}
var unchargeChannel uint64
if ctx.IsSet("channel") {
unchargeChannel = ctx.Uint64("channel")
}
resp, err := client.Uncharge(context.Background(), &rpc.UnchargeRequest{
Amt: int64(amt),
Dest: destAddr,
MaxMinerFee: int64(limits.maxMinerFee),
MaxPrepayAmt: int64(limits.maxPrepayAmt),
MaxSwapFee: int64(limits.maxSwapFee),
MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee),
UnchargeChannel: unchargeChannel,
})
if err != nil {
return err
}
fmt.Printf("Swap initiated with id: %v\n", resp.Id[:8])
fmt.Printf("Run swapcli without a command to monitor progress.\n")
return nil
}
func logSwap(swap *rpc.SwapStatus) {
fmt.Printf("%v %v %v %v - %v\n",
time.Unix(0, swap.LastUpdateTime).Format(time.RFC3339),
swap.Type, swap.State, btcutil.Amount(swap.Amt),
swap.HtlcAddress,
)
}
func getSwapCliConn(address string) (*grpc.ClientConn, error) {
opts := []grpc.DialOption{
grpc.WithInsecure(),
}
conn, err := grpc.Dial(address, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v", err)
}
return conn, nil
}

@ -0,0 +1,156 @@
package main
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"runtime/pprof"
"sync"
"time"
"github.com/lightninglabs/nautilus/client"
clientrpc "github.com/lightninglabs/nautilus/cmd/swapd/rpc"
"github.com/urfave/cli"
"google.golang.org/grpc"
)
// daemon runs swapd in daemon mode. It will listen for grpc connections,
// execute commands and pass back swap status information.
func daemon(ctx *cli.Context) error {
lnd, err := getLnd(ctx)
if err != nil {
return err
}
defer lnd.Close()
swapClient, cleanup, err := getClient(ctx, &lnd.LndServices)
if err != nil {
return err
}
defer cleanup()
// 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.GetUnchargeSwaps()
if err != nil {
return err
}
for _, swap := range storedSwaps {
swaps[swap.Hash] = client.SwapInfo{
SwapType: client.SwapTypeUncharge,
SwapContract: swap.Contract.SwapContract,
State: swap.State(),
SwapHash: swap.Hash,
LastUpdate: swap.LastUpdateTime(),
}
}
// Instantiate the swapd gRPC server.
server := swapClientServer{
impl: swapClient,
lnd: &lnd.LndServices,
}
serverOpts := []grpc.ServerOption{}
grpcServer := grpc.NewServer(serverOpts...)
clientrpc.RegisterSwapClientServer(grpcServer, &server)
// Next, Start the gRPC server listening for HTTP/2 connections.
logger.Infof("Starting RPC listener")
lis, err := net.Listen("tcp", defaultListenAddr)
if err != nil {
return fmt.Errorf("RPC server unable to listen on %s",
defaultListenAddr)
}
defer lis.Close()
statusChan := make(chan client.SwapInfo)
mainCtx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
// Start the swap client itself.
wg.Add(1)
go func() {
defer wg.Done()
logger.Infof("Starting swap client")
err := swapClient.Run(mainCtx, statusChan)
if err != nil {
logger.Error(err)
}
logger.Infof("Swap client stopped")
logger.Infof("Stopping gRPC server")
grpcServer.Stop()
cancel()
}()
// Start a goroutine that broadcasts swap updates to clients.
wg.Add(1)
go func() {
defer wg.Done()
logger.Infof("Waiting for updates")
for {
select {
case swap := <-statusChan:
swapsLock.Lock()
swaps[swap.SwapHash] = swap
for _, subscriber := range subscribers {
select {
case subscriber <- swap:
case <-mainCtx.Done():
return
}
}
swapsLock.Unlock()
case <-mainCtx.Done():
return
}
}
}()
// Start the grpc server.
wg.Add(1)
go func() {
defer wg.Done()
logger.Infof("RPC server listening on %s", lis.Addr())
err = grpcServer.Serve(lis)
if err != nil {
logger.Error(err)
}
}()
interruptChannel := make(chan os.Signal, 1)
signal.Notify(interruptChannel, os.Interrupt)
// Run until the users terminates swapd or an error occurred.
select {
case <-interruptChannel:
logger.Infof("Received SIGINT (Ctrl+C).")
// TODO: Remove debug code.
// Debug code to dump goroutines on hanging exit.
go func() {
time.Sleep(5 * time.Second)
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
}()
cancel()
case <-mainCtx.Done():
}
wg.Wait()
return nil
}

@ -0,0 +1,24 @@
package main
import (
"os"
"github.com/btcsuite/btclog"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var (
backendLog = btclog.NewBackend(logWriter{})
logger = backendLog.Logger("SWAPD")
)
// logWriter implements an io.Writer that outputs to both standard output and
// the write-end pipe of an initialized log rotator.
type logWriter struct{}
func (logWriter) Write(p []byte) (n int, err error) {
os.Stdout.Write(p)
return len(p), nil
}

@ -0,0 +1,70 @@
package main
import (
"fmt"
"os"
"sync"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/client"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/urfave/cli"
)
const (
defaultListenPort = 11010
defaultConfTarget = int32(2)
)
var (
defaultListenAddr = fmt.Sprintf("localhost:%d", defaultListenPort)
defaultSwapletDir = btcutil.AppDataDir("swaplet", false)
swaps = make(map[lntypes.Hash]client.SwapInfo)
subscribers = make(map[int]chan<- interface{})
nextSubscriberID int
swapsLock sync.Mutex
)
func main() {
app := cli.NewApp()
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "network",
Value: "mainnet",
Usage: "network to run on (regtest, testnet, mainnet)",
},
cli.StringFlag{
Name: "lnd",
Value: "localhost:10009",
Usage: "lnd instance rpc address host:port",
},
cli.StringFlag{
Name: "swapserver",
Value: "swap.lightning.today:11009",
Usage: "swap server address host:port",
},
cli.StringFlag{
Name: "macaroonpath",
Usage: "path to lnd macaroon",
},
cli.StringFlag{
Name: "tlspath",
Usage: "path to lnd tls certificate",
},
cli.BoolFlag{
Name: "insecure",
Usage: "disable tls",
},
}
app.Version = "0.0.1"
app.Usage = "swaps execution daemon"
app.Commands = []cli.Command{viewCommand}
app.Action = daemon
err := app.Run(os.Args)
if err != nil {
fmt.Println(err)
}
}

@ -0,0 +1,7 @@
#!/bin/sh
# Generate the protos.
protoc -I/usr/local/include -I. \
-I$GOPATH/src \
--go_out=plugins=grpc:. \
swapclient.proto

@ -0,0 +1,925 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: swapclient.proto
package rpc
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type SwapType int32
const (
// UNCHARGE indicates an uncharge swap (off-chain to on-chain)
SwapType_UNCHARGE SwapType = 0
)
var SwapType_name = map[int32]string{
0: "UNCHARGE",
}
var SwapType_value = map[string]int32{
"UNCHARGE": 0,
}
func (x SwapType) String() string {
return proto.EnumName(SwapType_name, int32(x))
}
func (SwapType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_swapclient_d9c5a6779b6644af, []int{0}
}
type SwapState int32
const (
// *
// INITIATED is the initial state of a swap. At that point, the initiation
// call to the server has been made and the payment process has been started
// for the swap and prepayment invoices.
SwapState_INITIATED SwapState = 0
// *
// PREIMAGE_REVEALED is reached when the sweep tx publication is first
// attempted. From that point on, we should consider the preimage to no
// longer be secret and we need to do all we can to get the sweep confirmed.
// This state will mostly coalesce with StateHtlcConfirmed, except in the
// case where we wait for fees to come down before we sweep.
SwapState_PREIMAGE_REVEALED SwapState = 1
// *
// SUCCESS is the final swap state that is reached when the sweep tx has
// the required confirmation depth.
SwapState_SUCCESS SwapState = 3
// *
// FAILED is the final swap state for a failed swap with or without loss of
// the swap amount.
SwapState_FAILED SwapState = 4
)
var SwapState_name = map[int32]string{
0: "INITIATED",
1: "PREIMAGE_REVEALED",
3: "SUCCESS",
4: "FAILED",
}
var SwapState_value = map[string]int32{
"INITIATED": 0,
"PREIMAGE_REVEALED": 1,
"SUCCESS": 3,
"FAILED": 4,
}
func (x SwapState) String() string {
return proto.EnumName(SwapState_name, int32(x))
}
func (SwapState) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_swapclient_d9c5a6779b6644af, []int{1}
}
type UnchargeRequest 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"`
// *
// Base58 encoded destination address for the swap.
Dest string `protobuf:"bytes,2,opt,name=dest,proto3" json:"dest,omitempty"`
// *
// Maximum off-chain fee in msat that may be paid for payment to the server.
// This limit is applied during path finding. Typically this value is taken
// from the response of the GetQuote call.
MaxSwapRoutingFee int64 `protobuf:"varint,3,opt,name=max_swap_routing_fee,json=maxSwapRoutingFee,proto3" json:"max_swap_routing_fee,omitempty"`
// *
// Maximum off-chain fee in msat that may be paid for payment to the server.
// This limit is applied during path finding. Typically this value is taken
// from the response of the GetQuote call.
MaxPrepayRoutingFee int64 `protobuf:"varint,4,opt,name=max_prepay_routing_fee,json=maxPrepayRoutingFee,proto3" json:"max_prepay_routing_fee,omitempty"`
// *
// Maximum we are willing to pay the server for the swap. This value is not
// disclosed in the swap initiation call, but if the server asks for a
// higher fee, we abort the swap. Typically this value is taken from the
// response of the GetQuote call. It includes the prepay amount.
MaxSwapFee int64 `protobuf:"varint,5,opt,name=max_swap_fee,json=maxSwapFee,proto3" json:"max_swap_fee,omitempty"`
// *
// Maximum amount of the swap fee that may be charged as a prepayment.
MaxPrepayAmt int64 `protobuf:"varint,6,opt,name=max_prepay_amt,json=maxPrepayAmt,proto3" json:"max_prepay_amt,omitempty"`
// *
// Maximum in on-chain fees that we are willing to spent. If we want to
// sweep the on-chain htlc and the fee estimate turns out higher than this
// value, we cancel the swap. If the fee estimate is lower, we publish the
// sweep tx.
//
// If the sweep tx isn't confirmed, we are forced to ratchet up fees until
// it is swept. Possibly even exceeding max_miner_fee if we get close to the
// htlc timeout. Because the initial publication revealed the preimage, we
// have no other choice. The server may already have pulled the off-chain
// htlc. Only when the fee becomes higher than the swap amount, we can only
// wait for fees to come down and hope - if we are past the timeout - that
// the server isn't publishing the revocation.
//
// max_miner_fee is typically taken from the response of the GetQuote call.
MaxMinerFee int64 `protobuf:"varint,7,opt,name=max_miner_fee,json=maxMinerFee,proto3" json:"max_miner_fee,omitempty"`
// *
// The channel to uncharge. If zero, the channel to uncharge is selected based
// on the lowest routing fee for the swap payment to the server.
UnchargeChannel uint64 `protobuf:"varint,8,opt,name=uncharge_channel,json=unchargeChannel,proto3" json:"uncharge_channel,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnchargeRequest) Reset() { *m = UnchargeRequest{} }
func (m *UnchargeRequest) String() string { return proto.CompactTextString(m) }
func (*UnchargeRequest) ProtoMessage() {}
func (*UnchargeRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_swapclient_d9c5a6779b6644af, []int{0}
}
func (m *UnchargeRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnchargeRequest.Unmarshal(m, b)
}
func (m *UnchargeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnchargeRequest.Marshal(b, m, deterministic)
}
func (dst *UnchargeRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnchargeRequest.Merge(dst, src)
}
func (m *UnchargeRequest) XXX_Size() int {
return xxx_messageInfo_UnchargeRequest.Size(m)
}
func (m *UnchargeRequest) XXX_DiscardUnknown() {
xxx_messageInfo_UnchargeRequest.DiscardUnknown(m)
}
var xxx_messageInfo_UnchargeRequest proto.InternalMessageInfo
func (m *UnchargeRequest) GetAmt() int64 {
if m != nil {
return m.Amt
}
return 0
}
func (m *UnchargeRequest) GetDest() string {
if m != nil {
return m.Dest
}
return ""
}
func (m *UnchargeRequest) GetMaxSwapRoutingFee() int64 {
if m != nil {
return m.MaxSwapRoutingFee
}
return 0
}
func (m *UnchargeRequest) GetMaxPrepayRoutingFee() int64 {
if m != nil {
return m.MaxPrepayRoutingFee
}
return 0
}
func (m *UnchargeRequest) GetMaxSwapFee() int64 {
if m != nil {
return m.MaxSwapFee
}
return 0
}
func (m *UnchargeRequest) GetMaxPrepayAmt() int64 {
if m != nil {
return m.MaxPrepayAmt
}
return 0
}
func (m *UnchargeRequest) GetMaxMinerFee() int64 {
if m != nil {
return m.MaxMinerFee
}
return 0
}
func (m *UnchargeRequest) GetUnchargeChannel() uint64 {
if m != nil {
return m.UnchargeChannel
}
return 0
}
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"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
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_swapclient_d9c5a6779b6644af, []int{1}
}
func (m *SwapResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapResponse.Unmarshal(m, b)
}
func (m *SwapResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SwapResponse.Marshal(b, m, deterministic)
}
func (dst *SwapResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_SwapResponse.Merge(dst, src)
}
func (m *SwapResponse) XXX_Size() int {
return xxx_messageInfo_SwapResponse.Size(m)
}
func (m *SwapResponse) XXX_DiscardUnknown() {
xxx_messageInfo_SwapResponse.DiscardUnknown(m)
}
var xxx_messageInfo_SwapResponse proto.InternalMessageInfo
func (m *SwapResponse) GetId() string {
if m != nil {
return m.Id
}
return ""
}
type MonitorRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
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_swapclient_d9c5a6779b6644af, []int{2}
}
func (m *MonitorRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_MonitorRequest.Unmarshal(m, b)
}
func (m *MonitorRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_MonitorRequest.Marshal(b, m, deterministic)
}
func (dst *MonitorRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_MonitorRequest.Merge(dst, src)
}
func (m *MonitorRequest) XXX_Size() int {
return xxx_messageInfo_MonitorRequest.Size(m)
}
func (m *MonitorRequest) XXX_DiscardUnknown() {
xxx_messageInfo_MonitorRequest.DiscardUnknown(m)
}
var xxx_messageInfo_MonitorRequest proto.InternalMessageInfo
type SwapStatus 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"`
// *
// 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,2,opt,name=id,proto3" json:"id,omitempty"`
// *
// Swap type
Type SwapType `protobuf:"varint,3,opt,name=type,proto3,enum=rpc.SwapType" json:"type,omitempty"`
// *
// State the swap is currently in, see State enum.
State SwapState `protobuf:"varint,4,opt,name=state,proto3,enum=rpc.SwapState" json:"state,omitempty"`
// *
// Initiation time of the swap.
InitiationTime int64 `protobuf:"varint,5,opt,name=initiation_time,json=initiationTime,proto3" json:"initiation_time,omitempty"`
// *
// Initiation time of the swap.
LastUpdateTime int64 `protobuf:"varint,6,opt,name=last_update_time,json=lastUpdateTime,proto3" json:"last_update_time,omitempty"`
// *
// Htlc address.
HtlcAddress string `protobuf:"bytes,7,opt,name=htlc_address,json=htlcAddress,proto3" json:"htlc_address,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
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_swapclient_d9c5a6779b6644af, []int{3}
}
func (m *SwapStatus) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapStatus.Unmarshal(m, b)
}
func (m *SwapStatus) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SwapStatus.Marshal(b, m, deterministic)
}
func (dst *SwapStatus) XXX_Merge(src proto.Message) {
xxx_messageInfo_SwapStatus.Merge(dst, src)
}
func (m *SwapStatus) XXX_Size() int {
return xxx_messageInfo_SwapStatus.Size(m)
}
func (m *SwapStatus) XXX_DiscardUnknown() {
xxx_messageInfo_SwapStatus.DiscardUnknown(m)
}
var xxx_messageInfo_SwapStatus proto.InternalMessageInfo
func (m *SwapStatus) GetAmt() int64 {
if m != nil {
return m.Amt
}
return 0
}
func (m *SwapStatus) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *SwapStatus) GetType() SwapType {
if m != nil {
return m.Type
}
return SwapType_UNCHARGE
}
func (m *SwapStatus) GetState() SwapState {
if m != nil {
return m.State
}
return SwapState_INITIATED
}
func (m *SwapStatus) GetInitiationTime() int64 {
if m != nil {
return m.InitiationTime
}
return 0
}
func (m *SwapStatus) GetLastUpdateTime() int64 {
if m != nil {
return m.LastUpdateTime
}
return 0
}
func (m *SwapStatus) GetHtlcAddress() string {
if m != nil {
return m.HtlcAddress
}
return ""
}
type TermsRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
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_swapclient_d9c5a6779b6644af, []int{4}
}
func (m *TermsRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsRequest.Unmarshal(m, b)
}
func (m *TermsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_TermsRequest.Marshal(b, m, deterministic)
}
func (dst *TermsRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_TermsRequest.Merge(dst, src)
}
func (m *TermsRequest) XXX_Size() int {
return xxx_messageInfo_TermsRequest.Size(m)
}
func (m *TermsRequest) XXX_DiscardUnknown() {
xxx_messageInfo_TermsRequest.DiscardUnknown(m)
}
var xxx_messageInfo_TermsRequest proto.InternalMessageInfo
type TermsResponse struct {
// *
// The node pubkey where the swap payment needs to be paid
// to. This can be used to test connectivity before initiating the swap.
SwapPaymentDest string `protobuf:"bytes,1,opt,name=swap_payment_dest,json=swapPaymentDest,proto3" json:"swap_payment_dest,omitempty"`
// *
// The base fee for a swap (sat)
SwapFeeBase int64 `protobuf:"varint,2,opt,name=swap_fee_base,json=swapFeeBase,proto3" json:"swap_fee_base,omitempty"`
// *
// The fee rate for a swap (parts per million)
SwapFeeRate int64 `protobuf:"varint,3,opt,name=swap_fee_rate,json=swapFeeRate,proto3" json:"swap_fee_rate,omitempty"`
// *
// Required prepay amount
PrepayAmt int64 `protobuf:"varint,4,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"`
// *
// Minimum swap amount (sat)
MinSwapAmount int64 `protobuf:"varint,5,opt,name=min_swap_amount,json=minSwapAmount,proto3" json:"min_swap_amount,omitempty"`
// *
// Maximum swap amount (sat)
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"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
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_swapclient_d9c5a6779b6644af, []int{5}
}
func (m *TermsResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsResponse.Unmarshal(m, b)
}
func (m *TermsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_TermsResponse.Marshal(b, m, deterministic)
}
func (dst *TermsResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_TermsResponse.Merge(dst, src)
}
func (m *TermsResponse) XXX_Size() int {
return xxx_messageInfo_TermsResponse.Size(m)
}
func (m *TermsResponse) XXX_DiscardUnknown() {
xxx_messageInfo_TermsResponse.DiscardUnknown(m)
}
var xxx_messageInfo_TermsResponse proto.InternalMessageInfo
func (m *TermsResponse) GetSwapPaymentDest() string {
if m != nil {
return m.SwapPaymentDest
}
return ""
}
func (m *TermsResponse) GetSwapFeeBase() int64 {
if m != nil {
return m.SwapFeeBase
}
return 0
}
func (m *TermsResponse) GetSwapFeeRate() int64 {
if m != nil {
return m.SwapFeeRate
}
return 0
}
func (m *TermsResponse) GetPrepayAmt() int64 {
if m != nil {
return m.PrepayAmt
}
return 0
}
func (m *TermsResponse) GetMinSwapAmount() int64 {
if m != nil {
return m.MinSwapAmount
}
return 0
}
func (m *TermsResponse) GetMaxSwapAmount() int64 {
if m != nil {
return m.MaxSwapAmount
}
return 0
}
func (m *TermsResponse) GetCltvDelta() int32 {
if m != nil {
return m.CltvDelta
}
return 0
}
func (m *TermsResponse) GetMaxCltv() int32 {
if m != nil {
return m.MaxCltv
}
return 0
}
type QuoteRequest struct {
// *
// Requested swap amount in sat.
Amt int64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
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_swapclient_d9c5a6779b6644af, []int{6}
}
func (m *QuoteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteRequest.Unmarshal(m, b)
}
func (m *QuoteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_QuoteRequest.Marshal(b, m, deterministic)
}
func (dst *QuoteRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_QuoteRequest.Merge(dst, src)
}
func (m *QuoteRequest) XXX_Size() int {
return xxx_messageInfo_QuoteRequest.Size(m)
}
func (m *QuoteRequest) XXX_DiscardUnknown() {
xxx_messageInfo_QuoteRequest.DiscardUnknown(m)
}
var xxx_messageInfo_QuoteRequest proto.InternalMessageInfo
func (m *QuoteRequest) GetAmt() int64 {
if m != nil {
return m.Amt
}
return 0
}
type QuoteResponse struct {
// *
// The fee that the swap server is charging for the swap.
SwapFee int64 `protobuf:"varint,1,opt,name=swap_fee,json=swapFee,proto3" json:"swap_fee,omitempty"`
// *
// The part of the swap fee that is requested as a
// prepayment.
PrepayAmt int64 `protobuf:"varint,2,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"`
// *
// An estimate of the on-chain fee that needs to be paid to
// sweep the htlc.
MinerFee int64 `protobuf:"varint,3,opt,name=miner_fee,json=minerFee,proto3" json:"miner_fee,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
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_swapclient_d9c5a6779b6644af, []int{7}
}
func (m *QuoteResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteResponse.Unmarshal(m, b)
}
func (m *QuoteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_QuoteResponse.Marshal(b, m, deterministic)
}
func (dst *QuoteResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_QuoteResponse.Merge(dst, src)
}
func (m *QuoteResponse) XXX_Size() int {
return xxx_messageInfo_QuoteResponse.Size(m)
}
func (m *QuoteResponse) XXX_DiscardUnknown() {
xxx_messageInfo_QuoteResponse.DiscardUnknown(m)
}
var xxx_messageInfo_QuoteResponse proto.InternalMessageInfo
func (m *QuoteResponse) GetSwapFee() int64 {
if m != nil {
return m.SwapFee
}
return 0
}
func (m *QuoteResponse) GetPrepayAmt() int64 {
if m != nil {
return m.PrepayAmt
}
return 0
}
func (m *QuoteResponse) GetMinerFee() int64 {
if m != nil {
return m.MinerFee
}
return 0
}
func init() {
proto.RegisterType((*UnchargeRequest)(nil), "rpc.UnchargeRequest")
proto.RegisterType((*SwapResponse)(nil), "rpc.SwapResponse")
proto.RegisterType((*MonitorRequest)(nil), "rpc.MonitorRequest")
proto.RegisterType((*SwapStatus)(nil), "rpc.SwapStatus")
proto.RegisterType((*TermsRequest)(nil), "rpc.TermsRequest")
proto.RegisterType((*TermsResponse)(nil), "rpc.TermsResponse")
proto.RegisterType((*QuoteRequest)(nil), "rpc.QuoteRequest")
proto.RegisterType((*QuoteResponse)(nil), "rpc.QuoteResponse")
proto.RegisterEnum("rpc.SwapType", SwapType_name, SwapType_value)
proto.RegisterEnum("rpc.SwapState", SwapState_name, SwapState_value)
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// SwapClientClient is the client API for SwapClient service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type SwapClientClient interface {
// *
// Uncharge initiates an uncharge 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().
Uncharge(ctx context.Context, in *UnchargeRequest, opts ...grpc.CallOption) (*SwapResponse, error)
// *
// Monitor will return a stream of swap updates for currently active swaps.
Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (SwapClient_MonitorClient, error)
// *
// GetTerms returns the terms that the server enforces for swaps.
GetUnchargeTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error)
// *
// GetQuote returns a quote for a swap with the provided parameters.
GetUnchargeQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error)
}
type swapClientClient struct {
cc *grpc.ClientConn
}
func NewSwapClientClient(cc *grpc.ClientConn) SwapClientClient {
return &swapClientClient{cc}
}
func (c *swapClientClient) Uncharge(ctx context.Context, in *UnchargeRequest, opts ...grpc.CallOption) (*SwapResponse, error) {
out := new(SwapResponse)
err := c.cc.Invoke(ctx, "/rpc.SwapClient/Uncharge", 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], "/rpc.SwapClient/Monitor", opts...)
if err != nil {
return nil, err
}
x := &swapClientMonitorClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type SwapClient_MonitorClient interface {
Recv() (*SwapStatus, error)
grpc.ClientStream
}
type swapClientMonitorClient struct {
grpc.ClientStream
}
func (x *swapClientMonitorClient) Recv() (*SwapStatus, error) {
m := new(SwapStatus)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *swapClientClient) GetUnchargeTerms(ctx context.Context, in *TermsRequest, opts ...grpc.CallOption) (*TermsResponse, error) {
out := new(TermsResponse)
err := c.cc.Invoke(ctx, "/rpc.SwapClient/GetUnchargeTerms", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) GetUnchargeQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) {
out := new(QuoteResponse)
err := c.cc.Invoke(ctx, "/rpc.SwapClient/GetUnchargeQuote", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapClientServer is the server API for SwapClient service.
type SwapClientServer interface {
// *
// Uncharge initiates an uncharge 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().
Uncharge(context.Context, *UnchargeRequest) (*SwapResponse, error)
// *
// Monitor will return a stream of swap updates for currently active swaps.
Monitor(*MonitorRequest, SwapClient_MonitorServer) error
// *
// GetTerms returns the terms that the server enforces for swaps.
GetUnchargeTerms(context.Context, *TermsRequest) (*TermsResponse, error)
// *
// GetQuote returns a quote for a swap with the provided parameters.
GetUnchargeQuote(context.Context, *QuoteRequest) (*QuoteResponse, error)
}
func RegisterSwapClientServer(s *grpc.Server, srv SwapClientServer) {
s.RegisterService(&_SwapClient_serviceDesc, srv)
}
func _SwapClient_Uncharge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UnchargeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).Uncharge(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpc.SwapClient/Uncharge",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).Uncharge(ctx, req.(*UnchargeRequest))
}
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 {
return err
}
return srv.(SwapClientServer).Monitor(m, &swapClientMonitorServer{stream})
}
type SwapClient_MonitorServer interface {
Send(*SwapStatus) error
grpc.ServerStream
}
type swapClientMonitorServer struct {
grpc.ServerStream
}
func (x *swapClientMonitorServer) Send(m *SwapStatus) error {
return x.ServerStream.SendMsg(m)
}
func _SwapClient_GetUnchargeTerms_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).GetUnchargeTerms(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpc.SwapClient/GetUnchargeTerms",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).GetUnchargeTerms(ctx, req.(*TermsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_GetUnchargeQuote_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).GetUnchargeQuote(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpc.SwapClient/GetUnchargeQuote",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).GetUnchargeQuote(ctx, req.(*QuoteRequest))
}
return interceptor(ctx, in, info, handler)
}
var _SwapClient_serviceDesc = grpc.ServiceDesc{
ServiceName: "rpc.SwapClient",
HandlerType: (*SwapClientServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Uncharge",
Handler: _SwapClient_Uncharge_Handler,
},
{
MethodName: "GetUnchargeTerms",
Handler: _SwapClient_GetUnchargeTerms_Handler,
},
{
MethodName: "GetUnchargeQuote",
Handler: _SwapClient_GetUnchargeQuote_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "Monitor",
Handler: _SwapClient_Monitor_Handler,
ServerStreams: true,
},
},
Metadata: "swapclient.proto",
}
func init() { proto.RegisterFile("swapclient.proto", fileDescriptor_swapclient_d9c5a6779b6644af) }
var fileDescriptor_swapclient_d9c5a6779b6644af = []byte{
// 744 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x54, 0x5d, 0x4f, 0xe3, 0x46,
0x14, 0x25, 0xdf, 0xf1, 0x4d, 0xe2, 0x38, 0x03, 0xad, 0x02, 0x15, 0x55, 0xb0, 0x50, 0x9b, 0xf2,
0x40, 0x5b, 0x78, 0xea, 0xa3, 0x9b, 0x18, 0x9a, 0x6a, 0x41, 0xec, 0x24, 0xd9, 0x57, 0x6b, 0x48,
0x06, 0xb0, 0x94, 0xb1, 0xbd, 0x9e, 0x31, 0x24, 0xff, 0x69, 0x1f, 0xf7, 0x57, 0xad, 0xb4, 0xff,
0x61, 0x35, 0x1f, 0x36, 0x09, 0xda, 0x7d, 0xb3, 0xce, 0x3d, 0xf7, 0x8c, 0xef, 0x99, 0x73, 0x07,
0x1c, 0xfe, 0x42, 0x92, 0xc5, 0x2a, 0xa4, 0x91, 0x38, 0x4f, 0xd2, 0x58, 0xc4, 0xa8, 0x92, 0x26,
0x0b, 0xf7, 0x73, 0x19, 0xba, 0xf3, 0x68, 0xf1, 0x44, 0xd2, 0x47, 0x8a, 0xe9, 0xc7, 0x8c, 0x72,
0x81, 0x1c, 0xa8, 0x10, 0x26, 0xfa, 0xa5, 0x41, 0x69, 0x58, 0xc1, 0xf2, 0x13, 0x21, 0xa8, 0x2e,
0x29, 0x17, 0xfd, 0xf2, 0xa0, 0x34, 0xb4, 0xb0, 0xfa, 0x46, 0x7f, 0xc2, 0x01, 0x23, 0xeb, 0x40,
0xca, 0x06, 0x69, 0x9c, 0x89, 0x30, 0x7a, 0x0c, 0x1e, 0x28, 0xed, 0x57, 0x54, 0x5b, 0x8f, 0x91,
0xf5, 0xf4, 0x85, 0x24, 0x58, 0x57, 0xae, 0x28, 0x45, 0x97, 0xf0, 0xb3, 0x6c, 0x48, 0x52, 0x9a,
0x90, 0xcd, 0x4e, 0x4b, 0x55, 0xb5, 0xec, 0x33, 0xb2, 0xbe, 0x53, 0xc5, 0xad, 0xa6, 0x01, 0xb4,
0x8b, 0x53, 0x24, 0xb5, 0xa6, 0xa8, 0x60, 0xd4, 0x25, 0xe3, 0x14, 0xec, 0x2d, 0x59, 0xf9, 0xe3,
0x75, 0xc5, 0x69, 0x17, 0x72, 0x1e, 0x13, 0xc8, 0x85, 0x8e, 0x64, 0xb1, 0x30, 0xa2, 0xa9, 0x12,
0x6a, 0x28, 0x52, 0x8b, 0x91, 0xf5, 0x8d, 0xc4, 0xa4, 0xd2, 0x1f, 0xe0, 0x64, 0xc6, 0x8a, 0x60,
0xf1, 0x44, 0xa2, 0x88, 0xae, 0xfa, 0xcd, 0x41, 0x69, 0x58, 0xc5, 0xdd, 0x1c, 0x1f, 0x69, 0xd8,
0xfd, 0x15, 0xda, 0x6a, 0x3a, 0xca, 0x93, 0x38, 0xe2, 0x14, 0xd9, 0x50, 0x0e, 0x97, 0xca, 0x31,
0x0b, 0x97, 0xc3, 0xa5, 0xeb, 0x80, 0x7d, 0x13, 0x47, 0xa1, 0x88, 0x53, 0x63, 0xaa, 0xfb, 0xb5,
0x04, 0x20, 0x5b, 0xa6, 0x82, 0x88, 0x8c, 0x7f, 0xc7, 0x63, 0x2d, 0x51, 0xce, 0x25, 0xd0, 0x09,
0x54, 0xc5, 0x26, 0xd1, 0x7e, 0xda, 0x17, 0x9d, 0xf3, 0x34, 0x59, 0x9c, 0x4b, 0x81, 0xd9, 0x26,
0xa1, 0x58, 0x95, 0xd0, 0x29, 0xd4, 0xb8, 0x20, 0x42, 0x1b, 0x68, 0x5f, 0xd8, 0x05, 0x47, 0x1e,
0x42, 0xb1, 0x2e, 0xa2, 0xdf, 0xa1, 0x1b, 0x46, 0xa1, 0x08, 0x89, 0x08, 0xe3, 0x28, 0x10, 0x21,
0xcb, 0x5d, 0xb4, 0x5f, 0xe1, 0x59, 0xc8, 0x28, 0x1a, 0x82, 0xb3, 0x22, 0x5c, 0x04, 0x59, 0xb2,
0x24, 0x82, 0x6a, 0xa6, 0xf6, 0xd2, 0x96, 0xf8, 0x5c, 0xc1, 0x8a, 0x79, 0x02, 0xed, 0x27, 0xb1,
0x5a, 0x04, 0x64, 0xb9, 0x4c, 0x29, 0xe7, 0xca, 0x4c, 0x0b, 0xb7, 0x24, 0xe6, 0x69, 0xc8, 0xb5,
0xa1, 0x3d, 0xa3, 0x29, 0xe3, 0xf9, 0xfc, 0x9f, 0xca, 0xd0, 0x31, 0x80, 0xf1, 0xec, 0x0c, 0x7a,
0xea, 0x5a, 0x13, 0xb2, 0x61, 0x34, 0x12, 0x81, 0x4a, 0x98, 0xb6, 0xb0, 0x2b, 0x0b, 0x77, 0x1a,
0x1f, 0xcb, 0xb0, 0xb9, 0xd0, 0xc9, 0x23, 0x10, 0xdc, 0x13, 0x4e, 0x95, 0x4f, 0x15, 0xdc, 0xe2,
0x3a, 0x04, 0xff, 0x12, 0x4e, 0x77, 0x38, 0xa9, 0x74, 0xa5, 0xb2, 0xc3, 0xc1, 0xd2, 0x8b, 0x63,
0x80, 0xad, 0xa0, 0xe8, 0xdc, 0x59, 0x49, 0x91, 0x92, 0xdf, 0xa0, 0xcb, 0xc2, 0x48, 0xa7, 0x8d,
0xb0, 0x38, 0x8b, 0x84, 0xb1, 0xaa, 0xc3, 0xc2, 0x48, 0x1a, 0xeb, 0x29, 0x50, 0xf1, 0xf2, 0x54,
0x1a, 0x5e, 0xdd, 0xf0, 0x74, 0x30, 0x0d, 0xef, 0x18, 0x60, 0xb1, 0x12, 0xcf, 0xc1, 0x92, 0xae,
0x04, 0x51, 0x2e, 0xd5, 0xb0, 0x25, 0x91, 0xb1, 0x04, 0xd0, 0x21, 0x34, 0xa5, 0x8c, 0x04, 0x54,
0xd0, 0x6a, 0xb8, 0xc1, 0xc8, 0x7a, 0xb4, 0x12, 0xcf, 0xee, 0x00, 0xda, 0xef, 0xb3, 0x58, 0xfc,
0x78, 0x27, 0xdd, 0x07, 0xe8, 0x18, 0x86, 0xf1, 0xf3, 0x10, 0x9a, 0xc5, 0x9a, 0x68, 0x5e, 0xc3,
0x8c, 0xfe, 0x66, 0xec, 0xf2, 0xdb, 0xb1, 0x7f, 0x01, 0xeb, 0x75, 0x31, 0xb4, 0x6b, 0x4d, 0x66,
0xb6, 0xe2, 0xac, 0x0f, 0xcd, 0x3c, 0x76, 0xa8, 0x0d, 0xcd, 0xf9, 0xed, 0xe8, 0x3f, 0x0f, 0x5f,
0xfb, 0xce, 0xde, 0xd9, 0xff, 0x60, 0x15, 0x61, 0x43, 0x1d, 0xb0, 0x26, 0xb7, 0x93, 0xd9, 0xc4,
0x9b, 0xf9, 0x63, 0x67, 0x0f, 0xfd, 0x04, 0xbd, 0x3b, 0xec, 0x4f, 0x6e, 0xbc, 0x6b, 0x3f, 0xc0,
0xfe, 0x07, 0xdf, 0x7b, 0xe7, 0x8f, 0x9d, 0x12, 0x6a, 0x41, 0x63, 0x3a, 0x1f, 0x8d, 0xfc, 0xe9,
0xd4, 0xa9, 0x20, 0x80, 0xfa, 0x95, 0x37, 0x91, 0x85, 0xea, 0xc5, 0x17, 0xb3, 0x1e, 0x23, 0xf5,
0x42, 0xa1, 0x4b, 0x68, 0xe6, 0xaf, 0x12, 0x3a, 0x50, 0xb1, 0x7e, 0xf3, 0x48, 0x1d, 0xf5, 0x8a,
0xb0, 0x17, 0x06, 0xfc, 0x0d, 0x0d, 0xb3, 0x74, 0x68, 0x5f, 0x55, 0x77, 0x57, 0xf0, 0xa8, 0xbb,
0xb3, 0x1f, 0x19, 0xff, 0xab, 0x84, 0xfe, 0x01, 0xe7, 0x9a, 0x8a, 0x5c, 0x5b, 0xe5, 0x13, 0x69,
0xe5, 0xed, 0xf0, 0x1e, 0xa1, 0x6d, 0xc8, 0x9c, 0xb6, 0xdb, 0xaa, 0xae, 0xc2, 0xb4, 0x6e, 0x5f,
0x9c, 0x69, 0xdd, 0xb9, 0xa9, 0xfb, 0xba, 0x7a, 0x80, 0x2f, 0xbf, 0x05, 0x00, 0x00, 0xff, 0xff,
0x4d, 0x46, 0x11, 0xa8, 0x94, 0x05, 0x00, 0x00,
}

@ -0,0 +1,259 @@
syntax = "proto3";
package rpc;
message UnchargeRequest {
/**
Requested swap amount in sat. This does not include the swap and miner
fee.
*/
int64 amt = 1;
/**
Base58 encoded destination address for the swap.
*/
string dest = 2;
/**
Maximum off-chain fee in msat that may be paid for payment to the server.
This limit is applied during path finding. Typically this value is taken
from the response of the GetQuote call.
*/
int64 max_swap_routing_fee = 3;
/**
Maximum off-chain fee in msat that may be paid for payment to the server.
This limit is applied during path finding. Typically this value is taken
from the response of the GetQuote call.
*/
int64 max_prepay_routing_fee = 4;
/**
Maximum we are willing to pay the server for the swap. This value is not
disclosed in the swap initiation call, but if the server asks for a
higher fee, we abort the swap. Typically this value is taken from the
response of the GetQuote call. It includes the prepay amount.
*/
int64 max_swap_fee = 5;
/**
Maximum amount of the swap fee that may be charged as a prepayment.
*/
int64 max_prepay_amt = 6;
/**
Maximum in on-chain fees that we are willing to spent. If we want to
sweep the on-chain htlc and the fee estimate turns out higher than this
value, we cancel the swap. If the fee estimate is lower, we publish the
sweep tx.
If the sweep tx isn't confirmed, we are forced to ratchet up fees until
it is swept. Possibly even exceeding max_miner_fee if we get close to the
htlc timeout. Because the initial publication revealed the preimage, we
have no other choice. The server may already have pulled the off-chain
htlc. Only when the fee becomes higher than the swap amount, we can only
wait for fees to come down and hope - if we are past the timeout - that
the server isn't publishing the revocation.
max_miner_fee is typically taken from the response of the GetQuote call.
*/
int64 max_miner_fee = 7;
/**
The channel to uncharge. If zero, the channel to uncharge is selected based
on the lowest routing fee for the swap payment to the server.
*/
uint64 uncharge_channel = 8;
}
message SwapResponse {
/**
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.
*/
string id = 1;
}
message MonitorRequest{
}
message SwapStatus {
/**
Requested swap amount in sat. This does not include the swap and miner
fee.
*/
int64 amt = 1;
/**
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.
*/
string id = 2;
/**
Swap type
*/
SwapType type = 3;
/**
State the swap is currently in, see State enum.
*/
SwapState state = 4;
/**
Initiation time of the swap.
*/
int64 initiation_time = 5;
/**
Initiation time of the swap.
*/
int64 last_update_time = 6;
/**
Htlc address.
*/
string htlc_address = 7;
}
enum SwapType {
// UNCHARGE indicates an uncharge swap (off-chain to on-chain)
UNCHARGE = 0;
}
enum SwapState {
/**
INITIATED is the initial state of a swap. At that point, the initiation
call to the server has been made and the payment process has been started
for the swap and prepayment invoices.
*/
INITIATED = 0;
/**
PREIMAGE_REVEALED is reached when the sweep tx publication is first
attempted. From that point on, we should consider the preimage to no
longer be secret and we need to do all we can to get the sweep confirmed.
This state will mostly coalesce with StateHtlcConfirmed, except in the
case where we wait for fees to come down before we sweep.
*/
PREIMAGE_REVEALED = 1;
/**
SUCCESS is the final swap state that is reached when the sweep tx has
the required confirmation depth.
*/
SUCCESS = 3;
/**
FAILED is the final swap state for a failed swap with or without loss of
the swap amount.
*/
FAILED = 4;
}
message TermsRequest {
}
message TermsResponse {
/**
The node pubkey where the swap payment needs to be paid
to. This can be used to test connectivity before initiating the swap.
*/
string swap_payment_dest = 1;
/**
The base fee for a swap (sat)
*/
int64 swap_fee_base = 2;
/**
The fee rate for a swap (parts per million)
*/
int64 swap_fee_rate = 3;
/**
Required prepay amount
*/
int64 prepay_amt = 4;
/**
Minimum swap amount (sat)
*/
int64 min_swap_amount = 5;
/**
Maximum swap amount (sat)
*/
int64 max_swap_amount = 6;
/**
On-chain cltv expiry delta
*/
int32 cltv_delta = 7;
/**
Maximum cltv expiry delta
*/
int32 max_cltv = 8;
}
message QuoteRequest {
/**
Requested swap amount in sat.
*/
int64 amt = 1;
}
message QuoteResponse {
/**
The fee that the swap server is charging for the swap.
*/
int64 swap_fee = 1;
/**
The part of the swap fee that is requested as a
prepayment.
*/
int64 prepay_amt = 2;
/**
An estimate of the on-chain fee that needs to be paid to
sweep the htlc.
*/
int64 miner_fee = 3;
}
/**
SwapClient is a service that handles the client side process of onchain/offchain
swaps. The service is designed for a single client.
*/
service SwapClient {
/**
Uncharge initiates an uncharge 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 Uncharge(UnchargeRequest) returns (SwapResponse);
/**
Monitor will return a stream of swap updates for currently active swaps.
*/
rpc Monitor(MonitorRequest) returns(stream SwapStatus);
/**
GetTerms returns the terms that the server enforces for swaps.
*/
rpc GetUnchargeTerms(TermsRequest) returns(TermsResponse);
/**
GetQuote returns a quote for a swap with the provided parameters.
*/
rpc GetUnchargeQuote(QuoteRequest) returns(QuoteResponse);
}

@ -0,0 +1,247 @@
package main
import (
"context"
"fmt"
"sort"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/utils"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/client"
clientrpc "github.com/lightninglabs/nautilus/cmd/swapd/rpc"
)
const completedSwapsCount = 5
// swapClientServer implements the grpc service exposed by swapd.
type swapClientServer struct {
impl *client.Client
lnd *lndclient.LndServices
}
// Uncharge initiates an uncharge 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 UnchargeStatus stream that is
// returned from Monitor().
func (s *swapClientServer) Uncharge(ctx context.Context,
in *clientrpc.UnchargeRequest) (
*clientrpc.SwapResponse, error) {
logger.Infof("Uncharge request received")
var sweepAddr btcutil.Address
if in.Dest == "" {
// Generate sweep address if none specified.
var err error
sweepAddr, err = s.lnd.WalletKit.NextAddr(context.Background())
if err != nil {
return nil, fmt.Errorf("NextAddr error: %v", err)
}
} else {
var err error
sweepAddr, err = btcutil.DecodeAddress(in.Dest, nil)
if err != nil {
return nil, fmt.Errorf("decode address: %v", err)
}
}
req := &client.UnchargeRequest{
Amount: btcutil.Amount(in.Amt),
DestAddr: sweepAddr,
MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt),
MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee),
MaxSwapRoutingFee: btcutil.Amount(in.MaxSwapRoutingFee),
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
SweepConfTarget: defaultConfTarget,
}
if in.UnchargeChannel != 0 {
req.UnchargeChannel = &in.UnchargeChannel
}
hash, err := s.impl.Uncharge(ctx, req)
if err != nil {
logger.Errorf("Uncharge: %v", err)
return nil, err
}
return &clientrpc.SwapResponse{
Id: hash.String(),
}, nil
}
func (s *swapClientServer) marshallSwap(swap *client.SwapInfo) (
*clientrpc.SwapStatus, error) {
var state clientrpc.SwapState
switch swap.State {
case client.StateInitiated:
state = clientrpc.SwapState_INITIATED
case client.StatePreimageRevealed:
state = clientrpc.SwapState_PREIMAGE_REVEALED
case client.StateSuccess:
state = clientrpc.SwapState_SUCCESS
default:
// Return less granular status over rpc.
state = clientrpc.SwapState_FAILED
}
htlc, err := utils.NewHtlc(swap.CltvExpiry, swap.SenderKey,
swap.ReceiverKey, swap.SwapHash,
)
if err != nil {
return nil, err
}
address, err := htlc.Address(s.lnd.ChainParams)
if err != nil {
return nil, err
}
return &clientrpc.SwapStatus{
Amt: int64(swap.AmountRequested),
Id: swap.SwapHash.String(),
State: state,
InitiationTime: swap.InitiationTime.UnixNano(),
LastUpdateTime: swap.LastUpdate.UnixNano(),
HtlcAddress: address.EncodeAddress(),
Type: clientrpc.SwapType_UNCHARGE,
}, nil
}
// Monitor will return a stream of swap updates for currently active swaps.
func (s *swapClientServer) Monitor(in *clientrpc.MonitorRequest,
server clientrpc.SwapClient_MonitorServer) error {
logger.Infof("Monitor request received")
send := func(info client.SwapInfo) error {
rpcSwap, err := s.marshallSwap(&info)
if err != nil {
return err
}
return server.Send(rpcSwap)
}
// Start a notification queue for this subscriber.
queue := queue.NewConcurrentQueue(20)
queue.Start()
// Add this subscriber to the global subscriber list. Also create a
// snapshot of all pending and completed swaps within the lock, to
// prevent subscribers from receiving duplicate updates.
swapsLock.Lock()
id := nextSubscriberID
nextSubscriberID++
subscribers[id] = queue.ChanIn()
var pendingSwaps, completedSwaps []client.SwapInfo
for _, swap := range swaps {
if swap.State.Type() == client.StateTypePending {
pendingSwaps = append(pendingSwaps, swap)
} else {
completedSwaps = append(completedSwaps, swap)
}
}
swapsLock.Unlock()
defer func() {
queue.Stop()
swapsLock.Lock()
delete(subscribers, id)
swapsLock.Unlock()
}()
// Sort completed swaps new to old.
sort.Slice(completedSwaps, func(i, j int) bool {
return completedSwaps[i].LastUpdate.After(
completedSwaps[j].LastUpdate,
)
})
// Discard all but top x latest.
if len(completedSwaps) > completedSwapsCount {
completedSwaps = completedSwaps[:completedSwapsCount]
}
// Concatenate both sets.
filteredSwaps := append(pendingSwaps, completedSwaps...)
// Sort again, but this time old to new.
sort.Slice(filteredSwaps, func(i, j int) bool {
return filteredSwaps[i].LastUpdate.Before(
filteredSwaps[j].LastUpdate,
)
})
// Return swaps to caller.
for _, swap := range filteredSwaps {
if err := send(swap); err != nil {
return err
}
}
// As long as the client is connected, keep passing through swap
// updates.
for {
select {
case queueItem, ok := <-queue.ChanOut():
if !ok {
return nil
}
swap := queueItem.(client.SwapInfo)
if err := send(swap); err != nil {
return err
}
case <-server.Context().Done():
return nil
}
}
}
// GetTerms returns the terms that the server enforces for swaps.
func (s *swapClientServer) GetUnchargeTerms(ctx context.Context, req *clientrpc.TermsRequest) (
*clientrpc.TermsResponse, error) {
logger.Infof("Terms request received")
terms, err := s.impl.UnchargeTerms(ctx)
if err != nil {
logger.Errorf("Terms request: %v", err)
return nil, err
}
return &clientrpc.TermsResponse{
MinSwapAmount: int64(terms.MinSwapAmount),
MaxSwapAmount: int64(terms.MaxSwapAmount),
PrepayAmt: int64(terms.PrepayAmt),
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) GetUnchargeQuote(ctx context.Context,
req *clientrpc.QuoteRequest) (*clientrpc.QuoteResponse, error) {
quote, err := s.impl.UnchargeQuote(ctx, &client.UnchargeQuoteRequest{
Amount: btcutil.Amount(req.Amt),
SweepConfTarget: defaultConfTarget,
})
if err != nil {
return nil, err
}
return &clientrpc.QuoteResponse{
MinerFee: int64(quote.MinerFee),
PrepayAmt: int64(quote.PrepayAmount),
SwapFee: int64(quote.SwapFee),
}, nil
}

@ -0,0 +1,49 @@
package main
import (
"os"
"path/filepath"
"github.com/lightninglabs/nautilus/client"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/urfave/cli"
)
// getLnd returns an instance of the lnd services proxy.
func getLnd(ctx *cli.Context) (*lndclient.GrpcLndServices, error) {
network := ctx.GlobalString("network")
return lndclient.NewLndServices(ctx.GlobalString("lnd"),
"client", network, ctx.GlobalString("macaroonpath"),
ctx.GlobalString("tlspath"),
)
}
// getClient returns an instance of the swap client.
func getClient(ctx *cli.Context, lnd *lndclient.LndServices) (*client.Client, func(), error) {
network := ctx.GlobalString("network")
storeDir, err := getStoreDir(network)
if err != nil {
return nil, nil, err
}
swapClient, cleanUp, err := client.NewClient(
storeDir, ctx.GlobalString("swapserver"),
ctx.GlobalBool("insecure"), lnd,
)
if err != nil {
return nil, nil, err
}
return swapClient, cleanUp, nil
}
func getStoreDir(network string) (string, error) {
dir := filepath.Join(defaultSwapletDir, network)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return "", err
}
return dir, nil
}

@ -0,0 +1,89 @@
package main
import (
"fmt"
"strconv"
"github.com/lightninglabs/nautilus/utils"
"github.com/urfave/cli"
)
var viewCommand = cli.Command{
Name: "view",
Usage: `view all swaps in the database. This command can only be
executed when swapd is not running.`,
Description: `
Show all pending and completed swaps.`,
Action: view,
}
// view prints all swaps currently in the database.
func view(ctx *cli.Context) error {
network := ctx.GlobalString("network")
chainParams, err := utils.ChainParamsFromNetwork(network)
if err != nil {
return err
}
lnd, err := getLnd(ctx)
if err != nil {
return err
}
defer lnd.Close()
swapClient, cleanup, err := getClient(ctx, &lnd.LndServices)
if err != nil {
return err
}
defer cleanup()
swaps, err := swapClient.GetUnchargeSwaps()
if err != nil {
return err
}
for _, s := range swaps {
htlc, err := utils.NewHtlc(
s.Contract.CltvExpiry,
s.Contract.SenderKey,
s.Contract.ReceiverKey,
s.Hash,
)
if err != nil {
return err
}
htlcAddress, err := htlc.Address(chainParams)
if err != nil {
return err
}
fmt.Printf("%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)
unchargeChannel := "any"
if s.Contract.UnchargeChannel != nil {
unchargeChannel = strconv.FormatUint(
*s.Contract.UnchargeChannel, 10,
)
}
fmt.Printf(" Uncharge channel: %v\n", unchargeChannel)
fmt.Printf(" Dest: %v\n", s.Contract.DestAddr)
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
}

@ -0,0 +1,44 @@
module github.com/lightninglabs/nautilus
require (
github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589
github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820
github.com/coreos/etcd v3.3.12+incompatible
github.com/coreos/go-semver v0.2.0 // indirect
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/fortytw2/leaktest v1.3.0
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gogo/protobuf v1.2.0 // indirect
github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect
github.com/golang/protobuf v1.2.0
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19
github.com/jonboulle/clockwork v0.1.0 // indirect
github.com/lightningnetwork/lnd v0.0.0
github.com/pkg/errors v0.8.0 // indirect
github.com/prometheus/client_golang v0.9.2 // indirect
github.com/sirupsen/logrus v1.2.0 // indirect
github.com/soheilhy/cmux v0.1.4 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
github.com/ugorji/go v1.1.1 // indirect
github.com/urfave/cli v1.20.0
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
go.etcd.io/etcd v3.3.12+incompatible
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a
google.golang.org/grpc v1.16.0
gopkg.in/macaroon.v2 v2.0.0
)
replace github.com/lightningnetwork/lnd => github.com/joostjager/lnd v0.4.1-beta.0.20190204111535-7159d0a5ef0a

240
go.sum

@ -0,0 +1,240 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
github.com/NebulousLabs/fastrand v0.0.0-20180208210444-3cf7173006a0/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/btcsuite/btcd v0.0.0-20180823030728-d81d8877b8f3/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
github.com/btcsuite/btcd v0.0.0-20180824064422-ed77733ec07dfc8a513741138419b8d9d3de9d2d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw=
github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589 h1:9A5pe5iQS+ll6R1EVLFv/y92IjrymihwITCU81aCIBQ=
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcwallet v0.0.0-20180904010540-284e2e0e696e33d5be388f7f3d9a26db703e0c06/go.mod h1:/d7QHZsfUAruXuBhyPITqoYOmJ+nq35qPsJjz/aSpCg=
github.com/btcsuite/btcwallet v0.0.0-20190123033236-ba03278a64bc h1:E7lDde/zAxAfvF750wMP0pUIAzF+wtwO2jQRy++q60U=
github.com/btcsuite/btcwallet v0.0.0-20190123033236-ba03278a64bc/go.mod h1:+u1ftn+QOb9qHKwsLf7rBOr0PHCo9CGA7U1WFq7VLA4=
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 h1:kij1x2aL7VE6gtx8KMIt8PGPgI5GV9LgtHFG5KaEMPY=
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8 h1:nOsAWScwueMVk/VLm/dvQQD7DuanyvAUb6B3P3eT274=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820 h1:W1bWzjKRrqKEpWlFsJ6Yef9Q4LUhdfJmS6sQrQj5L6c=
github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.12+incompatible h1:pAWNwdf7QiT1zfaWyqCtNZQWCLByQyA3JrSQyuYAqnQ=
github.com/coreos/etcd v3.3.12+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK4aZY8jz+DAF0FHjI51BXjjSwCzS1Dk=
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff h1:kOkM9whyQYodu09SJ6W3NCsHG7crFaJILQ22Gozp3lg=
github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v0.0.0-20180821051752-b27b920f9e71/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc h1:3NXdOHZ1YlN6SGP3FPbn4k73O2MeEp065abehRwGFxI=
github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jackpal/gateway v1.0.4/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19 h1:k9/LaykApavRKKlaWkunBd48Um+vMxnUNNsIjS7OJn8=
github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/joostjager/lnd v0.4.1-beta.0.20190204111535-7159d0a5ef0a h1:AExcTWAjSQSk7w94Hc15xPSTiLTft82xnAbe52NpQW0=
github.com/joostjager/lnd v0.4.1-beta.0.20190204111535-7159d0a5ef0a/go.mod h1:4axuRDteyNJN9JOK1yxIvRhtNNiWvshXk9eMnBxhbCk=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/juju/clock v0.0.0-20180808021310-bab88fc67299 h1:K9nBHQ3UNqg/HhZkQnGG2AE4YxDyNmGS9FFT2gGegLQ=
github.com/juju/clock v0.0.0-20180808021310-bab88fc67299/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 h1:rhqTjzJlm7EbkELJDKMTU7udov+Se0xZkWmugr6zGok=
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20180524022052-584905176618 h1:MK144iBQF9hTSwBW/9eJm034bVoG30IshVm688T2hi8=
github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 h1:WQM1NildKThwdP7qWrNAFGzp4ijNLw8RlgENkaI4MJs=
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885 h1:fTLuPUkaKIIV0+gA1IxiBDvDxtF8tzpSF6N6NfFGmsU=
github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885/go.mod h1:KUh15naRlx/TmUMFS/p4JJrCrE6F7RGF7rsnvuu45E4=
github.com/lightninglabs/neutrino v0.0.0-20181017011010-4d6069299130/go.mod h1:KJq43Fu9ceitbJsSXMILcT4mGDNI/crKmPIkDOZXFyM=
github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af h1:JzoYbWqwPb+PARU4LTtlohetdNa6/ocyQ0xidZQw4Hg=
github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af/go.mod h1:aR+E6cs+FTaIwIa/WLyvNsB8FZg8TiP3r0Led+4Q4gI=
github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6 h1:ONLGrYJVQdbtP6CE/ff1KNWZtygRGEh12RzonTiCzPs=
github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6/go.mod h1:8EgEt4a/NUOVQd+3kk6n9aZCJ1Ssj96Pb6lCrci+6oc=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY=
github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc=
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 h1:MPPkRncZLN9Kh4MEFmbnK4h3BD7AUmskWv2+EeZJCCs=
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.0 h1:oY10fI923Q5pVCVt1GBTZMn8LHo5M+RCInFpeMnV4QI=
go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v3.3.12+incompatible h1:V6PRYRGpU4k5EajJaaj/GL3hqIdzyPnBU8aPUp+35yw=
go.etcd.io/etcd v3.3.12+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180821023952-922f4815f713/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a h1:Weemm+oF2juintSvD0c+ZG4lDmCwgYKrM/kPI6gFINY=
google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v1 v1.0.0 h1:n+7XfCyygBFb8sEjg6692xjC6Us50TFRO54+xYUEwjE=
gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/macaroon-bakery.v2 v2.0.1 h1:0N1TlEdfLP4HXNCg7MQUMp5XwvOoxk+oe9Owr2cpvsc=
gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA=
gopkg.in/macaroon.v2 v2.0.0 h1:LVWycAfeJBUjCIqfR9gqlo7I8vmiXRr51YEOZ1suop8=
gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -0,0 +1,244 @@
package lndclient
import (
"context"
"fmt"
"sync"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/nautilus/utils"
"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.
type ChainNotifierClient interface {
RegisterBlockEpochNtfn(ctx context.Context) (
chan int32, chan error, error)
RegisterConfirmationsNtfn(ctx context.Context, txid *chainhash.Hash,
pkScript []byte, numConfs, heightHint int32) (
chan *chainntnfs.TxConfirmation, chan error, error)
RegisterSpendNtfn(ctx context.Context,
outpoint *wire.OutPoint, pkScript []byte, heightHint int32) (
chan *chainntnfs.SpendDetail, chan error, error)
}
type chainNotifierClient struct {
client chainrpc.ChainNotifierClient
wg sync.WaitGroup
}
func newChainNotifierClient(conn *grpc.ClientConn) *chainNotifierClient {
return &chainNotifierClient{
client: chainrpc.NewChainNotifierClient(conn),
}
}
func (s *chainNotifierClient) WaitForFinished() {
s.wg.Wait()
}
func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
outpoint *wire.OutPoint, pkScript []byte, heightHint int32) (
chan *chainntnfs.SpendDetail, chan error, error) {
var rpcOutpoint *chainrpc.Outpoint
if outpoint != nil {
rpcOutpoint = &chainrpc.Outpoint{
Hash: outpoint.Hash[:],
Index: outpoint.Index,
}
}
resp, err := s.client.RegisterSpendNtfn(ctx, &chainrpc.SpendRequest{
HeightHint: uint32(heightHint),
Outpoint: rpcOutpoint,
Script: pkScript,
})
if err != nil {
return nil, nil, err
}
spendChan := make(chan *chainntnfs.SpendDetail, 1)
errChan := make(chan error, 1)
processSpendDetail := func(d *chainrpc.SpendDetails) error {
outpointHash, err := chainhash.NewHash(d.SpendingOutpoint.Hash)
if err != nil {
return err
}
txHash, err := chainhash.NewHash(d.SpendingTxHash)
if err != nil {
return err
}
tx, err := utils.DecodeTx(d.RawSpendingTx)
if err != nil {
return err
}
spendChan <- &chainntnfs.SpendDetail{
SpentOutPoint: &wire.OutPoint{
Hash: *outpointHash,
Index: d.SpendingOutpoint.Index,
},
SpenderTxHash: txHash,
SpenderInputIndex: d.SpendingInputIndex,
SpendingTx: tx,
SpendingHeight: int32(d.SpendingHeight),
}
return nil
}
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
spendEvent, err := resp.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
return
}
switch c := spendEvent.Event.(type) {
case *chainrpc.SpendEvent_Spend:
err := processSpendDetail(c.Spend)
if err != nil {
errChan <- err
}
return
}
}
}()
return spendChan, errChan, nil
}
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[:]
}
confStream, err := s.client.
RegisterConfirmationsNtfn(
ctx,
&chainrpc.ConfRequest{
Script: pkScript,
NumConfs: uint32(numConfs),
HeightHint: uint32(heightHint),
Txid: txidSlice,
},
)
if err != nil {
return nil, nil, err
}
confChan := make(chan *chainntnfs.TxConfirmation, 1)
errChan := make(chan error, 1)
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
var confEvent *chainrpc.ConfEvent
confEvent, err := confStream.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
return
}
switch c := confEvent.Event.(type) {
// Script confirmed
case *chainrpc.ConfEvent_Conf:
tx, err := utils.DecodeTx(c.Conf.RawTx)
if err != nil {
errChan <- err
return
}
blockHash, err := chainhash.NewHash(
c.Conf.BlockHash,
)
if err != nil {
errChan <- err
return
}
confChan <- &chainntnfs.TxConfirmation{
BlockHeight: c.Conf.BlockHeight,
BlockHash: blockHash,
Tx: tx,
TxIndex: c.Conf.TxIndex,
}
return
// Ignore reorg events, not supported.
case *chainrpc.ConfEvent_Reorg:
continue
// Nil event, should never happen.
case nil:
errChan <- fmt.Errorf("conf event empty")
return
// Unexpected type.
default:
errChan <- fmt.Errorf(
"conf event has unexpected type",
)
return
}
}
}()
return confChan, errChan, nil
}
func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) (
chan int32, chan error, error) {
blockEpochClient, err := s.client.
RegisterBlockEpochNtfn(ctx, &chainrpc.BlockEpoch{})
if err != nil {
return nil, nil, err
}
blockErrorChan := make(chan error, 1)
blockEpochChan := make(chan int32)
// Start block epoch goroutine.
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
epoch, err := blockEpochClient.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
blockErrorChan <- err
}
return
}
select {
case blockEpochChan <- int32(epoch.Height):
case <-ctx.Done():
return
}
}
}()
return blockEpochChan, blockErrorChan, nil
}

@ -0,0 +1,154 @@
package lndclient
import (
"context"
"errors"
"sync"
"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)
SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error
CancelInvoice(ctx context.Context, hash lntypes.Hash) error
AddHoldInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) (
string, error)
}
type invoicesClient struct {
client invoicesrpc.InvoicesClient
wg sync.WaitGroup
}
func newInvoicesClient(conn *grpc.ClientConn) *invoicesClient {
return &invoicesClient{
client: invoicesrpc.NewInvoicesClient(conn),
}
}
func (s *invoicesClient) WaitForFinished() {
s.wg.Wait()
}
func (s *invoicesClient) SettleInvoice(ctx context.Context,
preimage lntypes.Preimage) error {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
_, err := s.client.SettleInvoice(rpcCtx, &invoicesrpc.SettleInvoiceMsg{
PreImage: preimage[:],
})
return err
}
func (s *invoicesClient) CancelInvoice(ctx context.Context,
hash lntypes.Hash) error {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
_, err := s.client.CancelInvoice(rpcCtx, &invoicesrpc.CancelInvoiceMsg{
PaymentHash: hash[:],
})
return err
}
func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
hash lntypes.Hash) (<-chan channeldb.ContractState,
<-chan error, error) {
invoiceStream, err := s.client.
SubscribeSingleInvoice(ctx,
&lnrpc.PaymentHash{
RHash: hash[:],
})
if err != nil {
return nil, nil, err
}
updateChan := make(chan channeldb.ContractState)
errChan := make(chan error, 1)
// Invoice updates goroutine.
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
invoice, err := invoiceStream.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
return
}
state, err := fromRPCInvoiceState(invoice.State)
if err != nil {
errChan <- err
return
}
select {
case updateChan <- state:
case <-ctx.Done():
return
}
}
}()
return updateChan, errChan, nil
}
func (s *invoicesClient) AddHoldInvoice(ctx context.Context,
in *invoicesrpc.AddInvoiceData) (string, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcIn := &invoicesrpc.AddHoldInvoiceRequest{
Memo: in.Memo,
Hash: in.Hash[:],
Value: int64(in.Value),
Expiry: in.Expiry,
CltvExpiry: in.CltvExpiry,
Private: true,
}
resp, err := s.client.AddHoldInvoice(rpcCtx, rpcIn)
if err != nil {
return "", err
}
return resp.PaymentRequest, nil
}
func fromRPCInvoiceState(state lnrpc.Invoice_InvoiceState) (
channeldb.ContractState, error) {
switch state {
case lnrpc.Invoice_OPEN:
return channeldb.ContractOpen, nil
case lnrpc.Invoice_ACCEPTED:
return channeldb.ContractAccepted, nil
case lnrpc.Invoice_SETTLED:
return channeldb.ContractSettled, nil
case lnrpc.Invoice_CANCELED:
return channeldb.ContractCanceled, nil
}
return 0, errors.New("unknown state")
}

@ -0,0 +1,330 @@
package lndclient
import (
"context"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/htlcswitch"
"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"
"google.golang.org/grpc/status"
)
// LightningClient exposes base lightning functionality.
type LightningClient interface {
PayInvoice(ctx context.Context, invoice string,
maxFee btcutil.Amount,
outgoingChannel *uint64) chan PaymentResult
GetInfo(ctx context.Context) (*Info, error)
GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) (
lnwire.MilliSatoshi, error)
ConfirmedWalletBalance(ctx context.Context) (btcutil.Amount, error)
AddInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) (
lntypes.Hash, string, error)
}
// Info contains info about the connected lnd node.
type Info struct {
BlockHeight uint32
IdentityPubkey [33]byte
Alias string
Network string
}
var (
// ErrMalformedServerResponse is returned when the swap and/or prepay
// invoice is malformed.
ErrMalformedServerResponse = errors.New(
"one or more invoices are malformed",
)
// ErrNoRouteToServer is returned if no quote can returned because there
// is no route to the server.
ErrNoRouteToServer = errors.New("no off-chain route to server")
// PaymentResultUnknownPaymentHash is the string result returned by
// SendPayment when the final node indicates the hash is unknown.
PaymentResultUnknownPaymentHash = "UnknownPaymentHash"
// PaymentResultSuccess is the string result returned by SendPayment
// when the payment was successful.
PaymentResultSuccess = ""
// PaymentResultAlreadyPaid is the string result returned by SendPayment
// when the payment was already completed in a previous SendPayment
// call.
PaymentResultAlreadyPaid = htlcswitch.ErrAlreadyPaid.Error()
// PaymentResultInFlight is the string result returned by SendPayment
// when the payment was initiated in a previous SendPayment call and
// still in flight.
PaymentResultInFlight = htlcswitch.ErrPaymentInFlight.Error()
paymentPollInterval = 3 * time.Second
)
type lightningClient struct {
client lnrpc.LightningClient
wg sync.WaitGroup
params *chaincfg.Params
}
func newLightningClient(conn *grpc.ClientConn,
params *chaincfg.Params) *lightningClient {
return &lightningClient{
client: lnrpc.NewLightningClient(conn),
params: params,
}
}
// PaymentResult signals the result of a payment.
type PaymentResult struct {
Err error
Preimage lntypes.Preimage
PaidFee btcutil.Amount
PaidAmt btcutil.Amount
}
func (s *lightningClient) WaitForFinished() {
s.wg.Wait()
}
func (s *lightningClient) ConfirmedWalletBalance(ctx context.Context) (
btcutil.Amount, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := s.client.WalletBalance(rpcCtx, &lnrpc.WalletBalanceRequest{})
if err != nil {
return 0, err
}
return btcutil.Amount(resp.ConfirmedBalance), nil
}
func (s *lightningClient) GetInfo(ctx context.Context) (*Info, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := s.client.GetInfo(rpcCtx, &lnrpc.GetInfoRequest{})
if err != nil {
return nil, err
}
pubKey, err := hex.DecodeString(resp.IdentityPubkey)
if err != nil {
return nil, err
}
var pubKeyArray [33]byte
copy(pubKeyArray[:], pubKey)
return &Info{
BlockHeight: resp.BlockHeight,
IdentityPubkey: pubKeyArray,
Alias: resp.Alias,
Network: resp.Chains[0].Network,
}, nil
}
func (s *lightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount,
dest [33]byte) (lnwire.MilliSatoshi, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
routeResp, err := s.client.QueryRoutes(
rpcCtx,
&lnrpc.QueryRoutesRequest{
Amt: int64(amt),
NumRoutes: 1,
PubKey: hex.EncodeToString(dest[:]),
},
)
if err != nil {
return 0, err
}
if len(routeResp.Routes) == 0 {
return 0, ErrNoRouteToServer
}
return lnwire.MilliSatoshi(routeResp.Routes[0].TotalFeesMsat), nil
}
// PayInvoice pays an invoice.
func (s *lightningClient) PayInvoice(ctx context.Context, invoice string,
maxFee btcutil.Amount, outgoingChannel *uint64) chan PaymentResult {
// Use buffer to prevent blocking.
paymentChan := make(chan PaymentResult, 1)
// Execute payment in parallel, because it will block until server
// discovers preimage.
s.wg.Add(1)
go func() {
defer s.wg.Done()
result := s.payInvoice(ctx, invoice, maxFee, outgoingChannel)
if result != nil {
paymentChan <- *result
}
}()
return paymentChan
}
// payInvoice tries to send a payment and returns the final result. If
// necessary, it will poll lnd for the payment result.
func (s *lightningClient) payInvoice(ctx context.Context, invoice string,
maxFee btcutil.Amount, outgoingChannel *uint64) *PaymentResult {
payReq, err := zpay32.Decode(invoice, s.params)
if err != nil {
return &PaymentResult{
Err: fmt.Errorf("invoice decode: %v", err),
}
}
if payReq.MilliSat == nil {
return &PaymentResult{
Err: errors.New("no amount in invoice"),
}
}
hash := lntypes.Hash(*payReq.PaymentHash)
for {
// Create no timeout context as this call can block for a long
// time.
req := &lnrpc.SendRequest{
FeeLimit: &lnrpc.FeeLimit{
Limit: &lnrpc.FeeLimit_Fixed{
Fixed: int64(maxFee),
},
},
PaymentRequest: invoice,
}
if outgoingChannel != nil {
req.OutgoingChannelID = *outgoingChannel
}
payResp, err := s.client.SendPaymentSync(ctx, req)
if status.Code(err) == codes.Canceled {
return nil
}
if err == nil {
// TODO: Use structured payment error when available,
// instead of this britle string matching.
switch payResp.PaymentError {
// Paid successfully.
case PaymentResultSuccess:
logger.Infof(
"Payment %v completed", hash,
)
r := payResp.PaymentRoute
preimage, err := lntypes.NewPreimage(
payResp.PaymentPreimage,
)
if err != nil {
return &PaymentResult{Err: err}
}
return &PaymentResult{
PaidFee: btcutil.Amount(r.TotalFees),
PaidAmt: btcutil.Amount(
r.TotalAmt - r.TotalFees,
),
Preimage: *preimage,
}
// Invoice was already paid on a previous run.
case PaymentResultAlreadyPaid:
logger.Infof(
"Payment %v already completed", hash,
)
// Unfortunately lnd doesn't return the route if
// the payment was successful in a previous
// call. Assume paid fees 0 and take paid amount
// from invoice.
return &PaymentResult{
PaidFee: 0,
PaidAmt: payReq.MilliSat.ToSatoshis(),
}
// If the payment is already in flight, we will poll
// again later for an outcome.
//
// TODO: Improve this when lnd expose more API to
// tracking existing payments.
case PaymentResultInFlight:
logger.Infof(
"Payment %v already in flight", hash,
)
time.Sleep(paymentPollInterval)
// Other errors are transformed into an error struct.
default:
logger.Warnf(
"Payment %v failed: %v", hash,
payResp.PaymentError,
)
return &PaymentResult{
Err: errors.New(payResp.PaymentError),
}
}
}
}
}
func (s *lightningClient) AddInvoice(ctx context.Context,
in *invoicesrpc.AddInvoiceData) (lntypes.Hash, string, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcIn := &lnrpc.Invoice{
Memo: in.Memo,
RHash: in.Hash[:],
Value: int64(in.Value),
Expiry: in.Expiry,
CltvExpiry: in.CltvExpiry,
Private: true,
}
resp, err := s.client.AddInvoice(rpcCtx, rpcIn)
if err != nil {
return lntypes.Hash{}, "", err
}
hash, err := lntypes.NewHash(resp.RHash)
if err != nil {
return lntypes.Hash{}, "", err
}
return *hash, resp.PaymentRequest, nil
}

@ -0,0 +1,186 @@
package lndclient
import (
"context"
"errors"
"fmt"
"github.com/lightninglabs/nautilus/utils"
"io/ioutil"
"path/filepath"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
macaroon "gopkg.in/macaroon.v2"
)
var rpcTimeout = 30 * time.Second
// LndServices constitutes a set of required services.
type LndServices struct {
Client LightningClient
WalletKit WalletKitClient
ChainNotifier ChainNotifierClient
Signer SignerClient
Invoices InvoicesClient
ChainParams *chaincfg.Params
}
// GrpcLndServices constitutes a set of required RPC services.
type GrpcLndServices struct {
LndServices
cleanup func()
}
// NewLndServices creates a set of required RPC services.
func NewLndServices(lndAddress string, application string,
network string, macPath, tlsPath string) (
*GrpcLndServices, error) {
// Setup connection with lnd
logger.Infof("Creating lnd connection")
conn, err := getClientConn(lndAddress, network, macPath, tlsPath)
if err != nil {
return nil, err
}
logger.Infof("Connected to lnd")
chainParams, err := utils.ChainParamsFromNetwork(network)
if err != nil {
return nil, err
}
lightningClient := newLightningClient(conn, chainParams)
info, err := lightningClient.GetInfo(context.Background())
if err != nil {
conn.Close()
return nil, err
}
if network != info.Network {
conn.Close()
return nil, errors.New(
"network mismatch with connected lnd instance",
)
}
notifierClient := newChainNotifierClient(conn)
signerClient := newSignerClient(conn)
walletKitClient := newWalletKitClient(conn)
invoicesClient := newInvoicesClient(conn)
cleanup := func() {
logger.Debugf("Closing lnd connection")
conn.Close()
logger.Debugf("Wait for client to finish")
lightningClient.WaitForFinished()
logger.Debugf("Wait for chain notifier to finish")
notifierClient.WaitForFinished()
logger.Debugf("Wait for invoices to finish")
invoicesClient.WaitForFinished()
logger.Debugf("Lnd services finished")
}
services := &GrpcLndServices{
LndServices: LndServices{
Client: lightningClient,
WalletKit: walletKitClient,
ChainNotifier: notifierClient,
Signer: signerClient,
Invoices: invoicesClient,
ChainParams: chainParams,
},
cleanup: cleanup,
}
logger.Infof("Using network %v", network)
return services, nil
}
// Close closes the lnd connection and waits for all sub server clients to
// finish their goroutines.
func (s *GrpcLndServices) Close() {
s.cleanup()
logger.Debugf("Lnd services finished")
}
var (
defaultRPCPort = "10009"
defaultLndDir = btcutil.AppDataDir("lnd", false)
defaultTLSCertFilename = "tls.cert"
defaultTLSCertPath = filepath.Join(defaultLndDir,
defaultTLSCertFilename)
defaultDataDir = "data"
defaultChainSubDir = "chain"
defaultMacaroonFilename = "admin.macaroon"
)
func getClientConn(address string, network string, macPath, tlsPath string) (
*grpc.ClientConn, error) {
// Load the specified TLS certificate and build transport credentials
// with it.
if tlsPath == "" {
tlsPath = defaultTLSCertPath
}
creds, err := credentials.NewClientTLSFromFile(tlsPath, "")
if err != nil {
return nil, err
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
if macPath == "" {
macPath = filepath.Join(
defaultLndDir, defaultDataDir, defaultChainSubDir,
"bitcoin", network, defaultMacaroonFilename,
)
}
// Load the specified macaroon file.
macBytes, err := ioutil.ReadFile(macPath)
if err == nil {
// Only if file is found
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v",
err)
}
// Now we append the macaroon credentials to the dial options.
cred := macaroons.NewMacaroonCredential(mac)
opts = append(opts, grpc.WithPerRPCCredentials(cred))
}
// We need to use a custom dialer so we can also connect to unix sockets
// and not just TCP addresses.
opts = append(
opts, grpc.WithDialer(
lncfg.ClientAddressDialer(defaultRPCPort),
),
)
conn, err := grpc.Dial(address, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v", err)
}
return conn, nil
}

@ -0,0 +1,23 @@
package lndclient
import (
"github.com/btcsuite/btclog"
"os"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var (
backendLog = btclog.NewBackend(logWriter{})
logger = backendLog.Logger("LNDCLIENT")
)
// logWriter implements an io.Writer that outputs to both standard output and
// the write-end pipe of an initialized log rotator.
type logWriter struct{}
func (logWriter) Write(p []byte) (n int, err error) {
os.Stdout.Write(p)
return len(p), nil
}

@ -0,0 +1,90 @@
package lndclient
import (
"context"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"google.golang.org/grpc"
)
// SignerClient exposes sign functionality.
type SignerClient interface {
SignOutputRaw(ctx context.Context, tx *wire.MsgTx,
signDescriptors []*input.SignDescriptor) ([][]byte, error)
}
type signerClient struct {
client signrpc.SignerClient
}
func newSignerClient(conn *grpc.ClientConn) *signerClient {
return &signerClient{
client: signrpc.NewSignerClient(conn),
}
}
func (s *signerClient) SignOutputRaw(ctx context.Context, tx *wire.MsgTx,
signDescriptors []*input.SignDescriptor) ([][]byte, error) {
txRaw, err := utils.EncodeTx(tx)
if err != nil {
return nil, err
}
rpcSignDescs := make([]*signrpc.SignDescriptor, len(signDescriptors))
for i, signDesc := range signDescriptors {
var keyBytes []byte
var keyLocator *signrpc.KeyLocator
if signDesc.KeyDesc.PubKey != nil {
keyBytes = signDesc.KeyDesc.PubKey.SerializeCompressed()
} else {
keyLocator = &signrpc.KeyLocator{
KeyFamily: int32(
signDesc.KeyDesc.KeyLocator.Family,
),
KeyIndex: int32(
signDesc.KeyDesc.KeyLocator.Index,
),
}
}
var doubleTweak []byte
if signDesc.DoubleTweak != nil {
doubleTweak = signDesc.DoubleTweak.Serialize()
}
rpcSignDescs[i] = &signrpc.SignDescriptor{
WitnessScript: signDesc.WitnessScript,
Output: &signrpc.TxOut{
PkScript: signDesc.Output.PkScript,
Value: signDesc.Output.Value,
},
Sighash: uint32(signDesc.HashType),
InputIndex: int32(signDesc.InputIndex),
KeyDesc: &signrpc.KeyDescriptor{
RawKeyBytes: keyBytes,
KeyLoc: keyLocator,
},
SingleTweak: signDesc.SingleTweak,
DoubleTweak: doubleTweak,
}
}
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := s.client.SignOutputRaw(rpcCtx,
&signrpc.SignReq{
RawTxBytes: txRaw,
SignDescs: rpcSignDescs,
},
)
if err != nil {
return nil, err
}
return resp.RawSigs, nil
}

@ -0,0 +1,180 @@
package lndclient
import (
"context"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"google.golang.org/grpc"
)
// WalletKitClient exposes wallet functionality.
type WalletKitClient interface {
DeriveNextKey(ctx context.Context, family int32) (
*keychain.KeyDescriptor, error)
DeriveKey(ctx context.Context, locator *keychain.KeyLocator) (
*keychain.KeyDescriptor, error)
NextAddr(ctx context.Context) (btcutil.Address, error)
PublishTransaction(ctx context.Context, tx *wire.MsgTx) error
SendOutputs(ctx context.Context, outputs []*wire.TxOut,
feeRate lnwallet.SatPerKWeight) (*wire.MsgTx, error)
EstimateFee(ctx context.Context, confTarget int32) (lnwallet.SatPerKWeight,
error)
}
type walletKitClient struct {
client walletrpc.WalletKitClient
}
func newWalletKitClient(conn *grpc.ClientConn) *walletKitClient {
return &walletKitClient{
client: walletrpc.NewWalletKitClient(conn),
}
}
func (m *walletKitClient) DeriveNextKey(ctx context.Context, family int32) (
*keychain.KeyDescriptor, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := m.client.DeriveNextKey(rpcCtx, &walletrpc.KeyReq{
KeyFamily: family,
})
if err != nil {
return nil, err
}
key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256())
if err != nil {
return nil, err
}
return &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(resp.KeyLoc.KeyFamily),
Index: uint32(resp.KeyLoc.KeyIndex),
},
PubKey: key,
}, nil
}
func (m *walletKitClient) DeriveKey(ctx context.Context, in *keychain.KeyLocator) (
*keychain.KeyDescriptor, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := m.client.DeriveKey(rpcCtx, &signrpc.KeyLocator{
KeyFamily: int32(in.Family),
KeyIndex: int32(in.Index),
})
if err != nil {
return nil, err
}
key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256())
if err != nil {
return nil, err
}
return &keychain.KeyDescriptor{
KeyLocator: *in,
PubKey: key,
}, nil
}
func (m *walletKitClient) NextAddr(ctx context.Context) (
btcutil.Address, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := m.client.NextAddr(rpcCtx, &walletrpc.AddrRequest{})
if err != nil {
return nil, err
}
addr, err := btcutil.DecodeAddress(resp.Addr, nil)
if err != nil {
return nil, err
}
return addr, nil
}
func (m *walletKitClient) PublishTransaction(ctx context.Context,
tx *wire.MsgTx) error {
txHex, err := utils.EncodeTx(tx)
if err != nil {
return err
}
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
_, err = m.client.PublishTransaction(rpcCtx, &walletrpc.Transaction{
TxHex: txHex,
})
return err
}
func (m *walletKitClient) SendOutputs(ctx context.Context,
outputs []*wire.TxOut, feeRate lnwallet.SatPerKWeight) (
*wire.MsgTx, error) {
rpcOutputs := make([]*signrpc.TxOut, len(outputs))
for i, output := range outputs {
rpcOutputs[i] = &signrpc.TxOut{
PkScript: output.PkScript,
Value: output.Value,
}
}
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := m.client.SendOutputs(rpcCtx, &walletrpc.SendOutputsRequest{
Outputs: rpcOutputs,
SatPerKw: int64(feeRate),
})
if err != nil {
return nil, err
}
tx, err := utils.DecodeTx(resp.RawTx)
if err != nil {
return nil, err
}
return tx, nil
}
func (m *walletKitClient) EstimateFee(ctx context.Context, confTarget int32) (
lnwallet.SatPerKWeight, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := m.client.EstimateFee(rpcCtx, &walletrpc.EstimateFeeRequest{
ConfTarget: int32(confTarget),
})
if err != nil {
return 0, err
}
return lnwallet.SatPerKWeight(resp.SatPerKw), nil
}

@ -0,0 +1,7 @@
#!/bin/sh
# Generate the protos.
protoc -I/usr/local/include -I.\
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=plugins=grpc,paths=source_relative:. \
server.proto

@ -0,0 +1,405 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: server.proto
package rpc // import "github.com/lightninglabs/nautilus/rpc"
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "google.golang.org/genproto/googleapis/api/annotations"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type ServerUnchargeSwapRequest struct {
ReceiverKey []byte `protobuf:"bytes,1,opt,name=receiver_key,json=receiverKey,proto3" json:"receiver_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"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerUnchargeSwapRequest) Reset() { *m = ServerUnchargeSwapRequest{} }
func (m *ServerUnchargeSwapRequest) String() string { return proto.CompactTextString(m) }
func (*ServerUnchargeSwapRequest) ProtoMessage() {}
func (*ServerUnchargeSwapRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_server_a93cbdd892155ac8, []int{0}
}
func (m *ServerUnchargeSwapRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerUnchargeSwapRequest.Unmarshal(m, b)
}
func (m *ServerUnchargeSwapRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerUnchargeSwapRequest.Marshal(b, m, deterministic)
}
func (dst *ServerUnchargeSwapRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerUnchargeSwapRequest.Merge(dst, src)
}
func (m *ServerUnchargeSwapRequest) XXX_Size() int {
return xxx_messageInfo_ServerUnchargeSwapRequest.Size(m)
}
func (m *ServerUnchargeSwapRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ServerUnchargeSwapRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ServerUnchargeSwapRequest proto.InternalMessageInfo
func (m *ServerUnchargeSwapRequest) GetReceiverKey() []byte {
if m != nil {
return m.ReceiverKey
}
return nil
}
func (m *ServerUnchargeSwapRequest) GetSwapHash() []byte {
if m != nil {
return m.SwapHash
}
return nil
}
func (m *ServerUnchargeSwapRequest) GetAmt() uint64 {
if m != nil {
return m.Amt
}
return 0
}
type ServerUnchargeSwapResponse struct {
SwapInvoice string `protobuf:"bytes,1,opt,name=swap_invoice,json=swapInvoice,proto3" json:"swap_invoice,omitempty"`
PrepayInvoice string `protobuf:"bytes,2,opt,name=prepay_invoice,json=prepayInvoice,proto3" json:"prepay_invoice,omitempty"`
SenderKey []byte `protobuf:"bytes,3,opt,name=sender_key,json=senderKey,proto3" json:"sender_key,omitempty"`
Expiry int32 `protobuf:"varint,4,opt,name=expiry,proto3" json:"expiry,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerUnchargeSwapResponse) Reset() { *m = ServerUnchargeSwapResponse{} }
func (m *ServerUnchargeSwapResponse) String() string { return proto.CompactTextString(m) }
func (*ServerUnchargeSwapResponse) ProtoMessage() {}
func (*ServerUnchargeSwapResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_server_a93cbdd892155ac8, []int{1}
}
func (m *ServerUnchargeSwapResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerUnchargeSwapResponse.Unmarshal(m, b)
}
func (m *ServerUnchargeSwapResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerUnchargeSwapResponse.Marshal(b, m, deterministic)
}
func (dst *ServerUnchargeSwapResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerUnchargeSwapResponse.Merge(dst, src)
}
func (m *ServerUnchargeSwapResponse) XXX_Size() int {
return xxx_messageInfo_ServerUnchargeSwapResponse.Size(m)
}
func (m *ServerUnchargeSwapResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ServerUnchargeSwapResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ServerUnchargeSwapResponse proto.InternalMessageInfo
func (m *ServerUnchargeSwapResponse) GetSwapInvoice() string {
if m != nil {
return m.SwapInvoice
}
return ""
}
func (m *ServerUnchargeSwapResponse) GetPrepayInvoice() string {
if m != nil {
return m.PrepayInvoice
}
return ""
}
func (m *ServerUnchargeSwapResponse) GetSenderKey() []byte {
if m != nil {
return m.SenderKey
}
return nil
}
func (m *ServerUnchargeSwapResponse) GetExpiry() int32 {
if m != nil {
return m.Expiry
}
return 0
}
type ServerUnchargeQuoteRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerUnchargeQuoteRequest) Reset() { *m = ServerUnchargeQuoteRequest{} }
func (m *ServerUnchargeQuoteRequest) String() string { return proto.CompactTextString(m) }
func (*ServerUnchargeQuoteRequest) ProtoMessage() {}
func (*ServerUnchargeQuoteRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_server_a93cbdd892155ac8, []int{2}
}
func (m *ServerUnchargeQuoteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerUnchargeQuoteRequest.Unmarshal(m, b)
}
func (m *ServerUnchargeQuoteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerUnchargeQuoteRequest.Marshal(b, m, deterministic)
}
func (dst *ServerUnchargeQuoteRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerUnchargeQuoteRequest.Merge(dst, src)
}
func (m *ServerUnchargeQuoteRequest) XXX_Size() int {
return xxx_messageInfo_ServerUnchargeQuoteRequest.Size(m)
}
func (m *ServerUnchargeQuoteRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ServerUnchargeQuoteRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ServerUnchargeQuoteRequest proto.InternalMessageInfo
type ServerUnchargeQuoteResponse struct {
SwapPaymentDest string `protobuf:"bytes,1,opt,name=swap_payment_dest,json=swapPaymentDest,proto3" json:"swap_payment_dest,omitempty"`
SwapFeeBase int64 `protobuf:"varint,2,opt,name=swap_fee_base,json=swapFeeBase,proto3" json:"swap_fee_base,omitempty"`
SwapFeeRate int64 `protobuf:"varint,3,opt,name=swap_fee_rate,json=swapFeeRate,proto3" json:"swap_fee_rate,omitempty"`
PrepayAmt uint64 `protobuf:"varint,4,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"`
MinSwapAmount uint64 `protobuf:"varint,5,opt,name=min_swap_amount,json=minSwapAmount,proto3" json:"min_swap_amount,omitempty"`
MaxSwapAmount uint64 `protobuf:"varint,6,opt,name=max_swap_amount,json=maxSwapAmount,proto3" json:"max_swap_amount,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:"-"`
}
func (m *ServerUnchargeQuoteResponse) Reset() { *m = ServerUnchargeQuoteResponse{} }
func (m *ServerUnchargeQuoteResponse) String() string { return proto.CompactTextString(m) }
func (*ServerUnchargeQuoteResponse) ProtoMessage() {}
func (*ServerUnchargeQuoteResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_server_a93cbdd892155ac8, []int{3}
}
func (m *ServerUnchargeQuoteResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerUnchargeQuoteResponse.Unmarshal(m, b)
}
func (m *ServerUnchargeQuoteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerUnchargeQuoteResponse.Marshal(b, m, deterministic)
}
func (dst *ServerUnchargeQuoteResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerUnchargeQuoteResponse.Merge(dst, src)
}
func (m *ServerUnchargeQuoteResponse) XXX_Size() int {
return xxx_messageInfo_ServerUnchargeQuoteResponse.Size(m)
}
func (m *ServerUnchargeQuoteResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ServerUnchargeQuoteResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ServerUnchargeQuoteResponse proto.InternalMessageInfo
func (m *ServerUnchargeQuoteResponse) GetSwapPaymentDest() string {
if m != nil {
return m.SwapPaymentDest
}
return ""
}
func (m *ServerUnchargeQuoteResponse) GetSwapFeeBase() int64 {
if m != nil {
return m.SwapFeeBase
}
return 0
}
func (m *ServerUnchargeQuoteResponse) GetSwapFeeRate() int64 {
if m != nil {
return m.SwapFeeRate
}
return 0
}
func (m *ServerUnchargeQuoteResponse) GetPrepayAmt() uint64 {
if m != nil {
return m.PrepayAmt
}
return 0
}
func (m *ServerUnchargeQuoteResponse) GetMinSwapAmount() uint64 {
if m != nil {
return m.MinSwapAmount
}
return 0
}
func (m *ServerUnchargeQuoteResponse) GetMaxSwapAmount() uint64 {
if m != nil {
return m.MaxSwapAmount
}
return 0
}
func (m *ServerUnchargeQuoteResponse) GetCltvDelta() int32 {
if m != nil {
return m.CltvDelta
}
return 0
}
func init() {
proto.RegisterType((*ServerUnchargeSwapRequest)(nil), "rpc.ServerUnchargeSwapRequest")
proto.RegisterType((*ServerUnchargeSwapResponse)(nil), "rpc.ServerUnchargeSwapResponse")
proto.RegisterType((*ServerUnchargeQuoteRequest)(nil), "rpc.ServerUnchargeQuoteRequest")
proto.RegisterType((*ServerUnchargeQuoteResponse)(nil), "rpc.ServerUnchargeQuoteResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// SwapServerClient is the client API for SwapServer service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type SwapServerClient interface {
NewUnchargeSwap(ctx context.Context, in *ServerUnchargeSwapRequest, opts ...grpc.CallOption) (*ServerUnchargeSwapResponse, error)
UnchargeQuote(ctx context.Context, in *ServerUnchargeQuoteRequest, opts ...grpc.CallOption) (*ServerUnchargeQuoteResponse, error)
}
type swapServerClient struct {
cc *grpc.ClientConn
}
func NewSwapServerClient(cc *grpc.ClientConn) SwapServerClient {
return &swapServerClient{cc}
}
func (c *swapServerClient) NewUnchargeSwap(ctx context.Context, in *ServerUnchargeSwapRequest, opts ...grpc.CallOption) (*ServerUnchargeSwapResponse, error) {
out := new(ServerUnchargeSwapResponse)
err := c.cc.Invoke(ctx, "/rpc.SwapServer/NewUnchargeSwap", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapServerClient) UnchargeQuote(ctx context.Context, in *ServerUnchargeQuoteRequest, opts ...grpc.CallOption) (*ServerUnchargeQuoteResponse, error) {
out := new(ServerUnchargeQuoteResponse)
err := c.cc.Invoke(ctx, "/rpc.SwapServer/UnchargeQuote", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwapServerServer is the server API for SwapServer service.
type SwapServerServer interface {
NewUnchargeSwap(context.Context, *ServerUnchargeSwapRequest) (*ServerUnchargeSwapResponse, error)
UnchargeQuote(context.Context, *ServerUnchargeQuoteRequest) (*ServerUnchargeQuoteResponse, error)
}
func RegisterSwapServerServer(s *grpc.Server, srv SwapServerServer) {
s.RegisterService(&_SwapServer_serviceDesc, srv)
}
func _SwapServer_NewUnchargeSwap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerUnchargeSwapRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapServerServer).NewUnchargeSwap(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpc.SwapServer/NewUnchargeSwap",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapServerServer).NewUnchargeSwap(ctx, req.(*ServerUnchargeSwapRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapServer_UnchargeQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerUnchargeQuoteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapServerServer).UnchargeQuote(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpc.SwapServer/UnchargeQuote",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapServerServer).UnchargeQuote(ctx, req.(*ServerUnchargeQuoteRequest))
}
return interceptor(ctx, in, info, handler)
}
var _SwapServer_serviceDesc = grpc.ServiceDesc{
ServiceName: "rpc.SwapServer",
HandlerType: (*SwapServerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "NewUnchargeSwap",
Handler: _SwapServer_NewUnchargeSwap_Handler,
},
{
MethodName: "UnchargeQuote",
Handler: _SwapServer_UnchargeQuote_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "server.proto",
}
func init() { proto.RegisterFile("server.proto", fileDescriptor_server_a93cbdd892155ac8) }
var fileDescriptor_server_a93cbdd892155ac8 = []byte{
// 471 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x93, 0xc1, 0x8e, 0xd3, 0x30,
0x10, 0x86, 0x95, 0xb6, 0x5b, 0xe8, 0x6c, 0x4b, 0x21, 0x07, 0x14, 0xba, 0xbb, 0x50, 0x2a, 0x2d,
0x54, 0x1c, 0x1a, 0x09, 0x9e, 0x60, 0x57, 0x2b, 0x04, 0x42, 0x42, 0x90, 0x15, 0x17, 0x2e, 0xd1,
0x34, 0x1d, 0x12, 0x8b, 0xc4, 0x36, 0xb6, 0xd3, 0x36, 0x0f, 0x83, 0x78, 0x09, 0x1e, 0x10, 0xd9,
0xce, 0x42, 0x8b, 0xda, 0x5b, 0xf2, 0xcf, 0xe7, 0x99, 0x7f, 0x7e, 0x27, 0x30, 0xd4, 0xa4, 0xd6,
0xa4, 0x16, 0x52, 0x09, 0x23, 0xc2, 0xae, 0x92, 0xd9, 0xe4, 0x3c, 0x17, 0x22, 0x2f, 0x29, 0x46,
0xc9, 0x62, 0xe4, 0x5c, 0x18, 0x34, 0x4c, 0x70, 0xed, 0x91, 0x59, 0x05, 0x4f, 0x6e, 0xdd, 0x91,
0x2f, 0x3c, 0x2b, 0x50, 0xe5, 0x74, 0xbb, 0x41, 0x99, 0xd0, 0x8f, 0x9a, 0xb4, 0x09, 0x9f, 0xc3,
0x50, 0x51, 0x46, 0x6c, 0x4d, 0x2a, 0xfd, 0x4e, 0x4d, 0x14, 0x4c, 0x83, 0xf9, 0x30, 0x39, 0xbd,
0xd3, 0x3e, 0x50, 0x13, 0x9e, 0xc1, 0x40, 0x6f, 0x50, 0xa6, 0x05, 0xea, 0x22, 0xea, 0xb8, 0xfa,
0x7d, 0x2b, 0xbc, 0x43, 0x5d, 0x84, 0x0f, 0xa1, 0x8b, 0x95, 0x89, 0xba, 0xd3, 0x60, 0xde, 0x4b,
0xec, 0xe3, 0xec, 0x67, 0x00, 0x93, 0x43, 0xf3, 0xb4, 0x14, 0x5c, 0x93, 0x1d, 0xe8, 0xba, 0x31,
0xbe, 0x16, 0x2c, 0x23, 0x37, 0x70, 0x90, 0x9c, 0x5a, 0xed, 0xbd, 0x97, 0xc2, 0x4b, 0x78, 0x20,
0x15, 0x49, 0x6c, 0xfe, 0x42, 0x1d, 0x07, 0x8d, 0xbc, 0x7a, 0x87, 0x5d, 0x00, 0x68, 0xe2, 0xab,
0xd6, 0x78, 0xd7, 0x19, 0x1b, 0x78, 0xc5, 0xda, 0x7e, 0x0c, 0x7d, 0xda, 0x4a, 0xa6, 0x9a, 0xa8,
0x37, 0x0d, 0xe6, 0x27, 0x49, 0xfb, 0x36, 0x3b, 0xff, 0xdf, 0xde, 0xe7, 0x5a, 0x18, 0x6a, 0xf3,
0x98, 0xfd, 0xea, 0xc0, 0xd9, 0xc1, 0x72, 0x6b, 0xff, 0x15, 0x3c, 0x72, 0xf6, 0x25, 0x36, 0x15,
0x71, 0x93, 0xae, 0x48, 0x9b, 0x76, 0x87, 0xb1, 0x2d, 0x7c, 0xf2, 0xfa, 0x8d, 0xcd, 0x76, 0x06,
0x23, 0xc7, 0x7e, 0x23, 0x4a, 0x97, 0xa8, 0xfd, 0x1a, 0x5d, 0xbf, 0xeb, 0x5b, 0xa2, 0x6b, 0xd4,
0xb4, 0xc7, 0x28, 0x34, 0xe4, 0xf6, 0xf8, 0xc7, 0x24, 0x68, 0xdc, 0xa2, 0x6d, 0x1e, 0x36, 0xea,
0x9e, 0x8b, 0x7a, 0xe0, 0x95, 0xab, 0xca, 0x84, 0x2f, 0x60, 0x5c, 0x31, 0x9e, 0xba, 0x36, 0x58,
0x89, 0x9a, 0x9b, 0xe8, 0xc4, 0x31, 0xa3, 0x8a, 0x71, 0x9b, 0xfd, 0x95, 0x13, 0x1d, 0x87, 0xdb,
0x3d, 0xae, 0xdf, 0x72, 0xb8, 0xdd, 0xe1, 0x2e, 0x00, 0xb2, 0xd2, 0xac, 0xd3, 0x15, 0x95, 0x06,
0xa3, 0x7b, 0x2e, 0xbc, 0x81, 0x55, 0x6e, 0xac, 0xf0, 0xfa, 0x77, 0x00, 0x60, 0x69, 0x9f, 0x52,
0x98, 0xc0, 0xf8, 0x23, 0x6d, 0x76, 0xaf, 0x3a, 0x7c, 0xba, 0x50, 0x32, 0x5b, 0x1c, 0xfd, 0xe6,
0x26, 0xcf, 0x8e, 0xd6, 0xdb, 0x90, 0x13, 0x18, 0xed, 0xa5, 0x1f, 0x1e, 0x3a, 0xb1, 0x7b, 0x6d,
0x93, 0xe9, 0x71, 0xc0, 0xf7, 0xbc, 0x7e, 0xf9, 0xf5, 0x32, 0x67, 0xa6, 0xa8, 0x97, 0x8b, 0x4c,
0x54, 0x71, 0xc9, 0xf2, 0xc2, 0x70, 0xc6, 0xf3, 0x12, 0x97, 0x3a, 0xe6, 0x58, 0x1b, 0x56, 0xd6,
0x3a, 0x56, 0x32, 0x5b, 0xf6, 0xdd, 0x5f, 0xf3, 0xe6, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xeb,
0xb5, 0x41, 0x9b, 0x68, 0x03, 0x00, 0x00,
}

@ -0,0 +1,40 @@
syntax = "proto3";
import "google/api/annotations.proto";
package rpc;
option go_package = "github.com/lightninglabs/nautilus/rpc";
service SwapServer {
rpc NewUnchargeSwap(ServerUnchargeSwapRequest) returns (ServerUnchargeSwapResponse);
rpc UnchargeQuote(ServerUnchargeQuoteRequest) returns (ServerUnchargeQuoteResponse);
}
message ServerUnchargeSwapRequest {
bytes receiver_key = 1;
bytes swap_hash = 2;
uint64 amt = 3;
}
message ServerUnchargeSwapResponse {
string swap_invoice= 1;
string prepay_invoice = 2;
bytes sender_key = 3;
int32 expiry = 4;
}
message ServerUnchargeQuoteRequest {
}
message ServerUnchargeQuoteResponse {
string swap_payment_dest = 1;
int64 swap_fee_base = 2;
int64 swap_fee_rate = 3;
uint64 prepay_amt = 4;
uint64 min_swap_amount = 5;
uint64 max_swap_amount = 6;
int32 cltv_delta = 7;
}

@ -0,0 +1,106 @@
package sweep
import (
"context"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
// Sweeper creates htlc sweep txes.
type Sweeper struct {
Lnd *lndclient.LndServices
}
// CreateSweepTx creates an htlc sweep tx.
func (s *Sweeper) CreateSweepTx(
globalCtx context.Context, height int32,
htlc *utils.Htlc, htlcOutpoint wire.OutPoint,
keyBytes [33]byte,
witnessFunc func(sig []byte) (wire.TxWitness, error),
amount, fee btcutil.Amount,
destAddr btcutil.Address) (*wire.MsgTx, error) {
// Compose tx.
sweepTx := wire.NewMsgTx(2)
sweepTx.LockTime = uint32(height)
// Add HTLC input.
sweepTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: htlcOutpoint,
})
// Add output for the destination address.
sweepPkScript, err := txscript.PayToAddrScript(destAddr)
if err != nil {
return nil, err
}
sweepTx.AddTxOut(&wire.TxOut{
PkScript: sweepPkScript,
Value: int64(amount - fee),
})
// Generate a signature for the swap htlc transaction.
key, err := btcec.ParsePubKey(keyBytes[:], btcec.S256())
if err != nil {
return nil, err
}
signDesc := input.SignDescriptor{
WitnessScript: htlc.Script,
Output: &wire.TxOut{
Value: int64(amount),
},
HashType: txscript.SigHashAll,
InputIndex: 0,
KeyDesc: keychain.KeyDescriptor{
PubKey: key,
},
}
rawSigs, err := s.Lnd.Signer.SignOutputRaw(
globalCtx, sweepTx, []*input.SignDescriptor{&signDesc},
)
if err != nil {
return nil, fmt.Errorf("signing: %v", err)
}
sig := rawSigs[0]
// Add witness stack to the tx input.
sweepTx.TxIn[0].Witness, err = witnessFunc(sig)
if err != nil {
return nil, err
}
return sweepTx, nil
}
// GetSweepFee calculates the required tx fee.
func (s *Sweeper) GetSweepFee(ctx context.Context,
htlcSuccessWitnessSize int, sweepConfTarget int32) (
btcutil.Amount, error) {
// Get fee estimate from lnd.
feeRate, err := s.Lnd.WalletKit.EstimateFee(ctx, sweepConfTarget)
if err != nil {
return 0, fmt.Errorf("estimate fee: %v", err)
}
// Calculate weight for this tx.
var weightEstimate input.TxWeightEstimator
weightEstimate.AddP2WKHOutput()
weightEstimate.AddWitnessInput(htlcSuccessWitnessSize)
weight := weightEstimate.Weight()
return feeRate.FeeForWeight(int64(weight)), nil
}

@ -0,0 +1,134 @@
package test
import (
"sync"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainntnfs"
"golang.org/x/net/context"
)
type mockChainNotifier struct {
lnd *LndMockServices
wg sync.WaitGroup
}
// SpendRegistration contains registration details.
type SpendRegistration struct {
Outpoint *wire.OutPoint
PkScript []byte
HeightHint int32
}
// ConfRegistration contains registration details.
type ConfRegistration struct {
TxID *chainhash.Hash
PkScript []byte
HeightHint int32
NumConfs int32
}
func (c *mockChainNotifier) RegisterSpendNtfn(ctx context.Context,
outpoint *wire.OutPoint, pkScript []byte, heightHint int32) (
chan *chainntnfs.SpendDetail, chan error, error) {
c.lnd.RegisterSpendChannel <- &SpendRegistration{
HeightHint: heightHint,
Outpoint: outpoint,
PkScript: pkScript,
}
spendChan := make(chan *chainntnfs.SpendDetail, 1)
errChan := make(chan error, 1)
c.wg.Add(1)
go func() {
defer c.wg.Done()
select {
case m := <-c.lnd.SpendChannel:
select {
case spendChan <- m:
case <-ctx.Done():
}
case <-ctx.Done():
}
}()
return spendChan, errChan, nil
}
func (c *mockChainNotifier) WaitForFinished() {
c.wg.Wait()
}
func (c *mockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) (
chan int32, chan error, error) {
blockErrorChan := make(chan error, 1)
blockEpochChan := make(chan int32)
c.wg.Add(1)
go func() {
defer c.wg.Done()
// Send initial block height
select {
case blockEpochChan <- c.lnd.Height:
case <-ctx.Done():
return
}
for {
select {
case m := <-c.lnd.epochChannel:
select {
case blockEpochChan <- m:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
return blockEpochChan, blockErrorChan, nil
}
func (c *mockChainNotifier) RegisterConfirmationsNtfn(ctx context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) (
chan *chainntnfs.TxConfirmation, chan error, error) {
confChan := make(chan *chainntnfs.TxConfirmation, 1)
errChan := make(chan error, 1)
c.wg.Add(1)
go func() {
defer c.wg.Done()
select {
case m := <-c.lnd.ConfChannel:
select {
case confChan <- m:
case <-ctx.Done():
}
case <-ctx.Done():
}
}()
select {
case c.lnd.RegisterConfChannel <- &ConfRegistration{
PkScript: pkScript,
TxID: txid,
HeightHint: heightHint,
NumConfs: numConfs,
}:
case <-time.After(Timeout):
return nil, nil, ErrTimeout
}
return confChan, errChan, nil
}

@ -0,0 +1,240 @@
package test
import (
"bytes"
"crypto/sha256"
"testing"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32"
)
// Context contains shared test context functions.
type Context struct {
T *testing.T
Lnd *LndMockServices
FailedInvoices map[lntypes.Hash]struct{}
PaidInvoices map[string]func(error)
}
// NewContext instanties a new common test context.
func NewContext(t *testing.T,
lnd *LndMockServices) Context {
return Context{
T: t,
Lnd: lnd,
FailedInvoices: make(map[lntypes.Hash]struct{}),
PaidInvoices: make(map[string]func(error)),
}
}
// ReceiveTx receives and decodes a published tx.
func (ctx *Context) ReceiveTx() *wire.MsgTx {
ctx.T.Helper()
select {
case tx := <-ctx.Lnd.TxPublishChannel:
return tx
case <-time.After(Timeout):
ctx.T.Fatalf("sweep not published")
return nil
}
}
// NotifySpend simulates a spend.
func (ctx *Context) NotifySpend(tx *wire.MsgTx, inputIndex uint32) {
ctx.T.Helper()
txHash := tx.TxHash()
select {
case ctx.Lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: tx,
SpenderTxHash: &txHash,
SpenderInputIndex: inputIndex,
}:
case <-time.After(Timeout):
ctx.T.Fatalf("htlc spend not consumed")
}
}
// NotifyConf simulates a conf.
func (ctx *Context) NotifyConf(tx *wire.MsgTx) {
ctx.T.Helper()
select {
case ctx.Lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: tx,
}:
case <-time.After(Timeout):
ctx.T.Fatalf("htlc spend not consumed")
}
}
// AssertRegisterSpendNtfn asserts that a register for spend has been received.
func (ctx *Context) AssertRegisterSpendNtfn(script []byte) {
ctx.T.Helper()
select {
case spendIntent := <-ctx.Lnd.RegisterSpendChannel:
if !bytes.Equal(spendIntent.PkScript, script) {
ctx.T.Fatalf("server not listening for published htlc script")
}
case <-time.After(Timeout):
DumpGoroutines()
ctx.T.Fatalf("spend not subscribed to")
}
}
// AssertRegisterConf asserts that a register for conf has been received.
func (ctx *Context) AssertRegisterConf() *ConfRegistration {
ctx.T.Helper()
// Expect client to register for conf
var confIntent *ConfRegistration
select {
case confIntent = <-ctx.Lnd.RegisterConfChannel:
if confIntent.TxID != nil {
ctx.T.Fatalf("expected script only registration")
}
case <-time.After(Timeout):
ctx.T.Fatalf("htlc confirmed not subscribed to")
}
return confIntent
}
// AssertPaid asserts that the expected payment request has been paid. This
// function returns a complete function to signal the final payment result.
func (ctx *Context) AssertPaid(
expectedMemo string) func(error) {
ctx.T.Helper()
if done, ok := ctx.PaidInvoices[expectedMemo]; ok {
return done
}
// Assert that client pays swap invoice.
for {
var swapPayment PaymentChannelMessage
select {
case swapPayment = <-ctx.Lnd.SendPaymentChannel:
case <-time.After(Timeout):
ctx.T.Fatalf("no payment sent for invoice: %v",
expectedMemo)
}
payReq := ctx.DecodeInvoice(swapPayment.PaymentRequest)
if _, ok := ctx.PaidInvoices[*payReq.Description]; ok {
ctx.T.Fatalf("duplicate invoice paid: %v",
*payReq.Description)
}
done := func(result error) {
select {
case swapPayment.Done <- result:
case <-time.After(Timeout):
ctx.T.Fatalf("payment result not consumed")
}
}
ctx.PaidInvoices[*payReq.Description] = done
if *payReq.Description == expectedMemo {
return done
}
}
}
// AssertSettled asserts that an invoice with the given hash is settled.
func (ctx *Context) AssertSettled(
expectedHash lntypes.Hash) lntypes.Preimage {
ctx.T.Helper()
select {
case preimage := <-ctx.Lnd.SettleInvoiceChannel:
hash := sha256.Sum256(preimage[:])
if expectedHash != hash {
ctx.T.Fatalf("server claims with wrong preimage")
}
return preimage
case <-time.After(Timeout):
}
ctx.T.Fatalf("invoice not settled")
return lntypes.Preimage{}
}
// AssertFailed asserts that an invoice with the given hash is failed.
func (ctx *Context) AssertFailed(expectedHash lntypes.Hash) {
ctx.T.Helper()
if _, ok := ctx.FailedInvoices[expectedHash]; ok {
return
}
for {
select {
case hash := <-ctx.Lnd.FailInvoiceChannel:
ctx.FailedInvoices[expectedHash] = struct{}{}
if expectedHash == hash {
return
}
case <-time.After(Timeout):
ctx.T.Fatalf("invoice not failed")
}
}
}
// DecodeInvoice decodes a payment request string.
func (ctx *Context) DecodeInvoice(request string) *zpay32.Invoice {
ctx.T.Helper()
payReq, err := ctx.Lnd.DecodeInvoice(request)
if err != nil {
ctx.T.Fatal(err)
}
return payReq
}
func (ctx *Context) GetOutputIndex(tx *wire.MsgTx,
script []byte) int {
for idx, out := range tx.TxOut {
if bytes.Equal(out.PkScript, script) {
return idx
}
}
ctx.T.Fatal("htlc not present in tx")
return 0
}
// NotifyServerHeight notifies the server of the arrival of a new block and
// waits for the notification to be processed by selecting on a
// dedicated test channel.
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")
// }
}

@ -0,0 +1,111 @@
package test
import (
"context"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
)
type mockInvoices struct {
lnd *LndMockServices
wg sync.WaitGroup
}
func (s *mockInvoices) SettleInvoice(ctx context.Context,
preimage lntypes.Preimage) error {
logger.Infof("Settle invoice %v with preimage %v", preimage.Hash(),
preimage)
s.lnd.SettleInvoiceChannel <- preimage
return nil
}
func (s *mockInvoices) WaitForFinished() {
s.wg.Wait()
}
func (s *mockInvoices) CancelInvoice(ctx context.Context,
hash lntypes.Hash) error {
s.lnd.FailInvoiceChannel <- hash
return nil
}
func (s *mockInvoices) SubscribeSingleInvoice(ctx context.Context,
hash lntypes.Hash) (<-chan channeldb.ContractState,
<-chan error, error) {
updateChan := make(chan channeldb.ContractState, 2)
errChan := make(chan error)
select {
case s.lnd.SingleInvoiceSubcribeChannel <- &SingleInvoiceSubscription{
Update: updateChan,
Err: errChan,
Hash: hash,
}:
case <-ctx.Done():
return nil, nil, ctx.Err()
}
return updateChan, errChan, nil
}
func (s *mockInvoices) AddHoldInvoice(ctx context.Context,
in *invoicesrpc.AddInvoiceData) (string, error) {
s.lnd.lock.Lock()
defer s.lnd.lock.Unlock()
hash := in.Hash
// Create and encode the payment request as a bech32 (zpay32) string.
creationDate := time.Now()
payReq, err := zpay32.NewInvoice(
s.lnd.ChainParams, *hash, creationDate,
zpay32.Description(in.Memo),
zpay32.CLTVExpiry(in.CltvExpiry),
zpay32.Amount(lnwire.MilliSatoshi(in.Value)),
)
if err != nil {
return "", err
}
privKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
return "", err
}
payReqString, err := payReq.Encode(
zpay32.MessageSigner{
SignCompact: func(hash []byte) ([]byte, error) {
// btcec.SignCompact returns a pubkey-recoverable signature
sig, err := btcec.SignCompact(
btcec.S256(), privKey, hash, true,
)
if err != nil {
return nil, fmt.Errorf("can't sign the hash: %v", err)
}
return sig, nil
},
},
)
if err != nil {
return "", err
}
return payReqString, nil
}

@ -0,0 +1,17 @@
package test
import (
"github.com/btcsuite/btcd/btcec"
)
// CreateKey returns a deterministically generated key pair.
func CreateKey(index int32) (*btcec.PrivateKey, *btcec.PublicKey) {
// Avoid all zeros, because it results in an invalid key.
privKey, pubKey := btcec.PrivKeyFromBytes(btcec.S256(),
[]byte{0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, byte(index + 1)})
return privKey, pubKey
}

@ -0,0 +1,152 @@
package test
import (
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightninglabs/nautilus/utils"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
"golang.org/x/net/context"
)
type mockLightningClient struct {
lnd *LndMockServices
wg sync.WaitGroup
}
// PayInvoice pays an invoice.
func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string,
maxFee btcutil.Amount,
outgoingChannel *uint64) chan lndclient.PaymentResult {
done := make(chan lndclient.PaymentResult, 1)
mockChan := make(chan error)
h.wg.Add(1)
go func() {
defer h.wg.Done()
amt, err := utils.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,
}
return done
}
func (h *mockLightningClient) WaitForFinished() {
h.wg.Wait()
}
func (h *mockLightningClient) ConfirmedWalletBalance(ctx context.Context) (
btcutil.Amount, error) {
return 1000000, nil
}
func (h *mockLightningClient) GetInfo(ctx context.Context) (*lndclient.Info,
error) {
var pubKey [33]byte
return &lndclient.Info{
BlockHeight: 600,
IdentityPubkey: pubKey,
}, nil
}
func (h *mockLightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) (
lnwire.MilliSatoshi, error) {
return 0, nil
}
func (h *mockLightningClient) AddInvoice(ctx context.Context,
in *invoicesrpc.AddInvoiceData) (lntypes.Hash, string, error) {
h.lnd.lock.Lock()
defer h.lnd.lock.Unlock()
var hash lntypes.Hash
if in.Hash != nil {
hash = *in.Hash
} else {
hash = (*in.Preimage).Hash()
}
// Create and encode the payment request as a bech32 (zpay32) string.
creationDate := time.Now()
payReq, err := zpay32.NewInvoice(
h.lnd.ChainParams, hash, creationDate,
zpay32.Description(in.Memo),
zpay32.CLTVExpiry(in.CltvExpiry),
zpay32.Amount(lnwire.MilliSatoshi(in.Value)),
)
if err != nil {
return lntypes.Hash{}, "", err
}
privKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
return lntypes.Hash{}, "", err
}
payReqString, err := payReq.Encode(
zpay32.MessageSigner{
SignCompact: func(hash []byte) ([]byte, error) {
// btcec.SignCompact returns a pubkey-recoverable signature
sig, err := btcec.SignCompact(
btcec.S256(), privKey, hash, true,
)
if err != nil {
return nil, fmt.Errorf("can't sign the hash: %v", err)
}
return sig, nil
},
},
)
if err != nil {
return lntypes.Hash{}, "", err
}
return hash, payReqString, nil
}

@ -0,0 +1,176 @@
package test
import (
"errors"
"sync"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/nautilus/lndclient"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
)
var testStartingHeight = int32(600)
// NewMockLnd returns a new instance of LndMockServices that can be used in unit
// tests.
func NewMockLnd() *LndMockServices {
lightningClient := &mockLightningClient{}
walletKit := &mockWalletKit{}
chainNotifier := &mockChainNotifier{}
signer := &mockSigner{}
invoices := &mockInvoices{}
lnd := LndMockServices{
LndServices: lndclient.LndServices{
WalletKit: walletKit,
Client: lightningClient,
ChainNotifier: chainNotifier,
Signer: signer,
Invoices: invoices,
ChainParams: &chaincfg.TestNet3Params,
},
SendPaymentChannel: make(chan PaymentChannelMessage),
ConfChannel: make(chan *chainntnfs.TxConfirmation),
RegisterConfChannel: make(chan *ConfRegistration),
RegisterSpendChannel: make(chan *SpendRegistration),
SpendChannel: make(chan *chainntnfs.SpendDetail),
TxPublishChannel: make(chan *wire.MsgTx),
SendOutputsChannel: make(chan wire.MsgTx),
SettleInvoiceChannel: make(chan lntypes.Preimage),
SingleInvoiceSubcribeChannel: make(chan *SingleInvoiceSubscription),
FailInvoiceChannel: make(chan lntypes.Hash, 2),
epochChannel: make(chan int32),
Height: testStartingHeight,
}
lightningClient.lnd = &lnd
chainNotifier.lnd = &lnd
walletKit.lnd = &lnd
invoices.lnd = &lnd
lnd.WaitForFinished = func() {
chainNotifier.WaitForFinished()
lightningClient.WaitForFinished()
invoices.WaitForFinished()
}
return &lnd
}
// PaymentChannelMessage is the data that passed through SendPaymentChannel.
type PaymentChannelMessage struct {
PaymentRequest string
Done chan error
}
// SingleInvoiceSubscription contains the single invoice subscribers
type SingleInvoiceSubscription struct {
Hash lntypes.Hash
Update chan channeldb.ContractState
Err chan error
}
// LndMockServices provides a full set of mocked lnd services.
type LndMockServices struct {
lndclient.LndServices
SendPaymentChannel chan PaymentChannelMessage
SpendChannel chan *chainntnfs.SpendDetail
TxPublishChannel chan *wire.MsgTx
SendOutputsChannel chan wire.MsgTx
SettleInvoiceChannel chan lntypes.Preimage
FailInvoiceChannel chan lntypes.Hash
epochChannel chan int32
ConfChannel chan *chainntnfs.TxConfirmation
RegisterConfChannel chan *ConfRegistration
RegisterSpendChannel chan *SpendRegistration
SingleInvoiceSubcribeChannel chan *SingleInvoiceSubscription
Height int32
WaitForFinished func()
lock sync.Mutex
}
// NotifyHeight notifies a new block height.
func (s *LndMockServices) NotifyHeight(height int32) error {
s.Height = height
select {
case s.epochChannel <- height:
case <-time.After(Timeout):
return ErrTimeout
}
return nil
}
// IsDone checks whether all channels have been fully emptied. If not this may
// indicate unexpected behaviour of the code under test.
func (s *LndMockServices) IsDone() error {
select {
case <-s.SendPaymentChannel:
return errors.New("SendPaymentChannel not empty")
default:
}
select {
case <-s.SpendChannel:
return errors.New("SpendChannel not empty")
default:
}
select {
case <-s.TxPublishChannel:
return errors.New("TxPublishChannel not empty")
default:
}
select {
case <-s.SendOutputsChannel:
return errors.New("SendOutputsChannel not empty")
default:
}
select {
case <-s.SettleInvoiceChannel:
return errors.New("SettleInvoiceChannel not empty")
default:
}
select {
case <-s.ConfChannel:
return errors.New("ConfChannel not empty")
default:
}
select {
case <-s.RegisterConfChannel:
return errors.New("RegisterConfChannel not empty")
default:
}
select {
case <-s.RegisterSpendChannel:
return errors.New("RegisterSpendChannel not empty")
default:
}
return nil
}
// DecodeInvoice decodes a payment request string.
func (s *LndMockServices) DecodeInvoice(request string) (*zpay32.Invoice,
error) {
return zpay32.Decode(request, s.ChainParams)
}

@ -0,0 +1,23 @@
package test
import (
"github.com/btcsuite/btclog"
"os"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var (
backendLog = btclog.NewBackend(logWriter{})
logger = backendLog.Logger("TEST")
)
// logWriter implements an io.Writer that outputs to both standard output and
// the write-end pipe of an initialized log rotator.
type logWriter struct{}
func (logWriter) Write(p []byte) (n int, err error) {
os.Stdout.Write(p)
return len(p), nil
}

@ -0,0 +1,19 @@
package test
import (
"context"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
)
type mockSigner struct {
}
func (s *mockSigner) SignOutputRaw(ctx context.Context, tx *wire.MsgTx,
signDescriptors []*input.SignDescriptor) ([][]byte, error) {
rawSigs := [][]byte{{1, 2, 3}}
return rawSigs, nil
}

@ -0,0 +1,69 @@
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"
)
var (
// Timeout is the default timeout when tests wait for something to
// happen.
Timeout = time.Second * 5
// ErrTimeout is returned on timeout.
ErrTimeout = errors.New("test timeout")
)
// GetDestAddr deterministically generates a sweep address for testing.
func GetDestAddr(t *testing.T, nr byte) btcutil.Address {
destAddr, err := btcutil.NewAddressScriptHash([]byte{nr},
&chaincfg.MainNetParams)
if err != nil {
t.Fatal(err)
}
return destAddr
}
// EncodePayReq encodes a zpay32 invoice with a fixed key.
func EncodePayReq(payReq *zpay32.Invoice) (string, error) {
privKey, _ := CreateKey(5)
reqString, err := payReq.Encode(
zpay32.MessageSigner{
SignCompact: func(hash []byte) ([]byte, error) {
// btcec.SignCompact returns a
// pubkey-recoverable signature
sig, err := btcec.SignCompact(
btcec.S256(),
privKey,
payReq.PaymentHash[:],
true,
)
if err != nil {
return nil, fmt.Errorf(
"can't sign the hash: %v", err)
}
return sig, nil
},
},
)
if err != nil {
return "", err
}
return reqString, nil
}
// DumpGoroutines dumps all currently running goroutines.
func DumpGoroutines() {
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
}

@ -0,0 +1,31 @@
package test
import (
"os"
"runtime/pprof"
"testing"
"time"
"github.com/fortytw2/leaktest"
)
// Guard implements a test level timeout.
func Guard(t *testing.T) func() {
done := make(chan struct{})
go func() {
select {
case <-time.After(5 * time.Second):
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
panic("test timeout")
case <-done:
}
}()
fn := leaktest.Check(t)
return func() {
close(done)
fn()
}
}

@ -0,0 +1,95 @@
package test
import (
"context"
"errors"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
)
type mockWalletKit struct {
lnd *LndMockServices
keyIndex int32
}
func (m *mockWalletKit) DeriveNextKey(ctx context.Context, family int32) (
*keychain.KeyDescriptor, error) {
index := m.keyIndex
_, pubKey := CreateKey(index)
m.keyIndex++
return &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(family),
Index: uint32(index),
},
PubKey: pubKey,
}, nil
}
func (m *mockWalletKit) DeriveKey(ctx context.Context, in *keychain.KeyLocator) (
*keychain.KeyDescriptor, error) {
_, pubKey := CreateKey(int32(in.Index))
return &keychain.KeyDescriptor{
KeyLocator: *in,
PubKey: pubKey,
}, nil
}
func (m *mockWalletKit) NextAddr(ctx context.Context) (btcutil.Address, error) {
addr, err := btcutil.NewAddressWitnessPubKeyHash(
make([]byte, 20), &chaincfg.TestNet3Params,
)
if err != nil {
return nil, err
}
return addr, nil
}
func (m *mockWalletKit) PublishTransaction(ctx context.Context, tx *wire.MsgTx) error {
m.lnd.TxPublishChannel <- tx
return nil
}
func (m *mockWalletKit) SendOutputs(ctx context.Context, outputs []*wire.TxOut,
feeRate lnwallet.SatPerKWeight) (*wire.MsgTx, error) {
var inputTxHash chainhash.Hash
tx := wire.MsgTx{}
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: inputTxHash,
Index: 0,
},
})
for _, out := range outputs {
tx.AddTxOut(&wire.TxOut{
PkScript: out.PkScript,
Value: out.Value,
})
}
m.lnd.SendOutputsChannel <- tx
return &tx, nil
}
func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) (
lnwallet.SatPerKWeight, error) {
if confTarget <= 1 {
return 0, errors.New("conf target must be greater than 1")
}
return 10000, nil
}

@ -0,0 +1,9 @@
package utils
// SwapKeyFamily is the key family used to generate keys that allow spending
// of the htlc.
//
// TODO: Decide on actual value.
var (
SwapKeyFamily = int32(99)
)

@ -0,0 +1,180 @@
package utils
import (
"bytes"
"errors"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
)
// Htlc contains relevant htlc information from the receiver perspective.
type Htlc struct {
Script []byte
ScriptHash []byte
Hash lntypes.Hash
MaxSuccessWitnessSize int
MaxTimeoutWitnessSize int
}
var (
quoteKey [33]byte
quoteHash lntypes.Hash
// QuoteHtlc is a template script just used for fee estimation. It uses
// the maximum value for cltv expiry to get the maximum (worst case)
// script size.
QuoteHtlc, _ = NewHtlc(
^int32(0), quoteKey, quoteKey, quoteHash,
)
)
// NewHtlc returns a new instance.
func NewHtlc(cltvExpiry int32, senderKey, receiverKey [33]byte,
hash lntypes.Hash) (*Htlc, error) {
script, err := swapHTLCScript(
cltvExpiry, senderKey, receiverKey, hash,
)
if err != nil {
return nil, err
}
scriptHash, 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)
return &Htlc{
Hash: hash,
Script: script,
ScriptHash: scriptHash,
MaxSuccessWitnessSize: maxSuccessWitnessSize,
MaxTimeoutWitnessSize: maxTimeoutWitnessSize,
}, nil
}
// SwapHTLCScript returns the on-chain HTLC witness script.
//
// OP_SIZE 32 OP_EQUAL
// OP_IF
// OP_HASH160 <ripemd160(swap_hash)> OP_EQUALVERIFY
// <recvr key>
// OP_ELSE
// OP_DROP
// <cltv timeout> OP_CHECKLOCKTIMEVERIFY OP_DROP
// <sender key>
// OP_ENDIF
// OP_CHECKSIG
func swapHTLCScript(cltvExpiry int32, senderHtlcKey,
receiverHtlcKey [33]byte, swapHash lntypes.Hash) ([]byte, error) {
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_SIZE)
builder.AddInt64(32)
builder.AddOp(txscript.OP_EQUAL)
builder.AddOp(txscript.OP_IF)
builder.AddOp(txscript.OP_HASH160)
builder.AddData(input.Ripemd160H(swapHash[:]))
builder.AddOp(txscript.OP_EQUALVERIFY)
builder.AddData(receiverHtlcKey[:])
builder.AddOp(txscript.OP_ELSE)
builder.AddOp(txscript.OP_DROP)
builder.AddInt64(int64(cltvExpiry))
builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
builder.AddOp(txscript.OP_DROP)
builder.AddData(senderHtlcKey[:])
builder.AddOp(txscript.OP_ENDIF)
builder.AddOp(txscript.OP_CHECKSIG)
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,
preimage lntypes.Preimage) (wire.TxWitness, error) {
if h.Hash != preimage.Hash() {
return nil, errors.New("preimage doesn't match hash")
}
witnessStack := make(wire.TxWitness, 3)
witnessStack[0] = append(receiverSig, byte(txscript.SigHashAll))
witnessStack[1] = preimage[:]
witnessStack[2] = h.Script
return witnessStack, nil
}
// IsSuccessWitness checks whether the given stack is valid for redeeming the
// htlc.
func (h *Htlc) IsSuccessWitness(witness wire.TxWitness) bool {
if len(witness) != 3 {
return false
}
isTimeoutTx := bytes.Equal([]byte{0}, witness[1])
return !isTimeoutTx
}
// GenTimeoutWitness returns the timeout script to spend this htlc after
// timeout.
func (h *Htlc) GenTimeoutWitness(senderSig []byte) (wire.TxWitness, error) {
witnessStack := make(wire.TxWitness, 3)
witnessStack[0] = append(senderSig, byte(txscript.SigHashAll))
witnessStack[1] = []byte{0}
witnessStack[2] = h.Script
return witnessStack, nil
}

@ -0,0 +1,41 @@
package utils
import (
"fmt"
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/lntypes"
)
// SwapLog logs with a short swap hash prefix.
type SwapLog struct {
Logger btclog.Logger
Hash lntypes.Hash
}
// Infof formats message according to format specifier and writes to
// log with LevelInfo.
func (s *SwapLog) Infof(format string, params ...interface{}) {
s.Logger.Infof(
fmt.Sprintf("%v %s", ShortHash(&s.Hash), format),
params...,
)
}
// Warnf formats message according to format specifier and writes to
// to log with LevelError.
func (s *SwapLog) Warnf(format string, params ...interface{}) {
s.Logger.Warnf(
fmt.Sprintf("%v %s", ShortHash(&s.Hash), format),
params...,
)
}
// Errorf formats message according to format specifier and writes to
// to log with LevelError.
func (s *SwapLog) Errorf(format string, params ...interface{}) {
s.Logger.Errorf(
fmt.Sprintf("%v %s", ShortHash(&s.Hash), format),
params...,
)
}

@ -0,0 +1,123 @@
package utils
import (
"bytes"
"errors"
"fmt"
"os"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/btcsuite/btcd/wire"
)
const (
// FeeRateTotalParts defines the granularity of the fee rate.
FeeRateTotalParts = 1e6
)
// ShortHash returns a shortened version of the hash suitable for use in
// logging.
func ShortHash(hash *lntypes.Hash) string {
return hash.String()[:6]
}
// EncodeTx encodes a tx to raw bytes.
func EncodeTx(tx *wire.MsgTx) ([]byte, error) {
var buffer bytes.Buffer
err := tx.BtcEncode(&buffer, 0, wire.WitnessEncoding)
if err != nil {
return nil, err
}
rawTx := buffer.Bytes()
return rawTx, nil
}
// DecodeTx decodes raw tx bytes.
func DecodeTx(rawTx []byte) (*wire.MsgTx, error) {
tx := wire.MsgTx{}
r := bytes.NewReader(rawTx)
err := tx.BtcDecode(r, 0, wire.WitnessEncoding)
if err != nil {
return nil, err
}
return &tx, nil
}
// GetInvoiceAmt gets the invoice amount. It requires an amount to be specified.
func GetInvoiceAmt(params *chaincfg.Params,
payReq string) (btcutil.Amount, error) {
swapPayReq, err := zpay32.Decode(
payReq, params,
)
if err != nil {
return 0, err
}
if swapPayReq.MilliSat == nil {
return 0, errors.New("no amount in invoice")
}
return swapPayReq.MilliSat.ToSatoshis(), nil
}
// FileExists returns true if the file exists, and false otherwise.
func FileExists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// ChainParamsFromNetwork returns chain parameters based on a network name.
func ChainParamsFromNetwork(network string) (*chaincfg.Params, error) {
switch network {
case "mainnet":
return &chaincfg.MainNetParams, nil
case "testnet":
return &chaincfg.TestNet3Params, nil
case "regtest":
return &chaincfg.RegressionNetParams, nil
case "simnet":
return &chaincfg.SimNetParams, nil
default:
return nil, errors.New("unknown network")
}
}
// GetScriptOutput locates the given script in the outputs of a transaction and
// returns its outpoint and value.
func GetScriptOutput(htlcTx *wire.MsgTx, scriptHash []byte) (
*wire.OutPoint, btcutil.Amount, error) {
for idx, output := range htlcTx.TxOut {
if bytes.Equal(output.PkScript, scriptHash) {
return &wire.OutPoint{
Hash: htlcTx.TxHash(),
Index: uint32(idx),
}, btcutil.Amount(output.Value), nil
}
}
return nil, 0, fmt.Errorf("cannot determine outpoint")
}
// CalcFee returns the swap fee for a given swap amount.
func CalcFee(amount, feeBase btcutil.Amount, feeRate int64) btcutil.Amount {
return feeBase + amount*btcutil.Amount(feeRate)/
btcutil.Amount(FeeRateTotalParts)
}
// FeeRateAsPercentage converts a feerate to a percentage.
func FeeRateAsPercentage(feeRate int64) float64 {
return float64(feeRate) / (FeeRateTotalParts / 100)
}
Loading…
Cancel
Save