mirror of https://github.com/lightninglabs/loop
commit
1166691522
@ -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
|
@ -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…
Reference in New Issue