From a2bef1178b819bbe94cdb72c0d5447f1fa6c2a01 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Mon, 13 May 2024 09:15:18 +0900 Subject: [PATCH] lwk: add TLS support for Electrum Add TLS support for Electrum. In default, the scheme is a tls connection, which allows connection to electrs. context timeout for wallet operations to enhance reliability. --- Makefile | 2 +- cmd/peerswap-plugin/main.go | 14 ++-- cmd/peerswaplnd/peerswapd/main.go | 16 ++-- electrum/client.go | 86 +++++++++++++++++++ electrum/electrum.go | 2 + electrum/mock/electrum.go | 116 +++++++++++++++++++++++++ lwk/conf_builder.go | 4 +- lwk/config.go | 17 ++++ lwk/electrumtxwatcher.go | 35 +++++--- lwk/electrumtxwatcher_test.go | 6 +- lwk/lwkwallet.go | 135 ++++++++++++++++++------------ lwk/lwkwallet_test.go | 32 +++++++ lwk/mock/electrumRPC.go | 86 ------------------- 13 files changed, 376 insertions(+), 175 deletions(-) create mode 100644 electrum/client.go create mode 100644 electrum/mock/electrum.go create mode 100644 lwk/lwkwallet_test.go delete mode 100644 lwk/mock/electrumRPC.go diff --git a/Makefile b/Makefile index eadd0d3f..c89d44c4 100644 --- a/Makefile +++ b/Makefile @@ -224,4 +224,4 @@ mockgen: mockgen/lwk .PHONY: mockgen/lwk mockgen/lwk: - $(TOOLS_DIR)/bin/mockgen -source=lwk/electrumRPC.go -destination=lwk/mock/electrumRPC.go \ No newline at end of file + $(TOOLS_DIR)/bin/mockgen -source=electrum/electrum.go -destination=electrum/mock/electrum.go \ No newline at end of file diff --git a/cmd/peerswap-plugin/main.go b/cmd/peerswap-plugin/main.go index b065d6cb..31da8fd2 100644 --- a/cmd/peerswap-plugin/main.go +++ b/cmd/peerswap-plugin/main.go @@ -9,7 +9,6 @@ import ( "path/filepath" "time" - "github.com/checksum0/go-electrum/electrum" "github.com/elementsproject/peerswap/elements" "github.com/elementsproject/peerswap/isdev" "github.com/elementsproject/peerswap/log" @@ -209,22 +208,19 @@ func run(ctx context.Context, lightningPlugin *clightning.ClightningClient) erro log.Infof("Liquid swaps enabled") } else if config.LWK != nil && config.LWK.Enabled() { liquidEnabled = true - ec, err2 := electrum.NewClientTCP(ctx, config.LWK.GetElectrumEndpoint()) + lc, err2 := lwk.NewLWKRpcWallet(ctx, config.LWK) if err2 != nil { return err2 } - liquidRpcWallet, err2 = lwk.NewLWKRpcWallet(lwk.NewLwk(config.LWK.GetLWKEndpoint()), - ec, config.LWK.GetWalletName(), config.LWK.GetSignerName()) - if err2 != nil { - return err2 - } - liquidTxWatcher, err = lwk.NewElectrumTxWatcher(ec) + liquidTxWatcher, err = lwk.NewElectrumTxWatcher(lc.GetElectrumClient()) if err != nil { return err } + liquidRpcWallet = lc liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, config.LWK.GetChain()) supportedAssets = append(supportedAssets, "lbtc") - log.Infof("Liquid swaps enabled") + log.Infof("Liquid swaps enabled with LWK. Network: %s, wallet: %s", + config.LWK.GetNetwork(), config.LWK.GetWalletName()) } else { log.Infof("Liquid swaps disabled") } diff --git a/cmd/peerswaplnd/peerswapd/main.go b/cmd/peerswaplnd/peerswapd/main.go index 45c9a784..04554373 100644 --- a/cmd/peerswaplnd/peerswapd/main.go +++ b/cmd/peerswaplnd/peerswapd/main.go @@ -16,7 +16,6 @@ import ( "syscall" "time" - "github.com/checksum0/go-electrum/electrum" "github.com/elementsproject/peerswap/elements" "github.com/elementsproject/peerswap/isdev" "github.com/elementsproject/peerswap/lnd" @@ -231,22 +230,17 @@ func run() error { liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, liquidChain) } else if cfg.LWKConfig.Enabled() { log.Infof("Liquid swaps enabled with LWK. Network: %s, wallet: %s", cfg.LWKConfig.GetNetwork(), cfg.LWKConfig.GetWalletName()) - ec, err := electrum.NewClientTCP(ctx, cfg.LWKConfig.GetElectrumEndpoint()) - if err != nil { - return err - } - // This call is blocking, waiting for elements to come alive and sync. - liquidRpcWallet, err = lwk.NewLWKRpcWallet(lwk.NewLwk(cfg.LWKConfig.GetLWKEndpoint()), - ec, cfg.LWKConfig.GetWalletName(), cfg.LWKConfig.GetSignerName()) - if err != nil { - return err + lc, err2 := lwk.NewLWKRpcWallet(ctx, cfg.LWKConfig) + if err2 != nil { + return err2 } cfg.LiquidEnabled = true - liquidTxWatcher, err = lwk.NewElectrumTxWatcher(ec) + liquidTxWatcher, err = lwk.NewElectrumTxWatcher(lc.GetElectrumClient()) if err != nil { return err } + liquidRpcWallet = lc liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, cfg.LWKConfig.GetChain()) supportedAssets = append(supportedAssets, "lbtc") } else { diff --git a/electrum/client.go b/electrum/client.go new file mode 100644 index 00000000..fac23ed9 --- /dev/null +++ b/electrum/client.go @@ -0,0 +1,86 @@ +package electrum + +import ( + "context" + "crypto/tls" + + "github.com/checksum0/go-electrum/electrum" + "github.com/elementsproject/peerswap/log" +) + +type electrumClient struct { + client *electrum.Client + endpoint string + isTLS bool +} + +func NewElectrumClient(ctx context.Context, endpoint string, isTLS bool) (RPC, error) { + ec, err := newClient(ctx, endpoint, isTLS) + if err != nil { + return nil, err + } + client := &electrumClient{ + client: ec, + endpoint: endpoint, + isTLS: isTLS, + } + return client, nil +} + +// reconnect reconnects to the electrum server if the connection is lost. +func (c *electrumClient) reconnect(ctx context.Context) error { + if err := c.client.Ping(ctx); err != nil { + log.Infof("failed to ping electrum server: %v", err) + log.Infof("reconnecting to electrum server") + client, err := newClient(ctx, c.endpoint, c.isTLS) + if err != nil { + return err + } + c.client = client + } + return nil +} + +func newClient(ctx context.Context, endpoint string, isTLS bool) (*electrum.Client, error) { + if isTLS { + return electrum.NewClientSSL(ctx, endpoint, &tls.Config{ + MinVersion: tls.VersionTLS12, + }) + } + return electrum.NewClientTCP(ctx, endpoint) +} + +func (c *electrumClient) SubscribeHeaders(ctx context.Context) (<-chan *electrum.SubscribeHeadersResult, error) { + if err := c.reconnect(ctx); err != nil { + return nil, err + } + return c.client.SubscribeHeaders(ctx) +} + +func (c *electrumClient) GetHistory(ctx context.Context, scripthash string) ([]*electrum.GetMempoolResult, error) { + if err := c.reconnect(ctx); err != nil { + return nil, err + } + return c.client.GetHistory(ctx, scripthash) +} + +func (c *electrumClient) GetRawTransaction(ctx context.Context, txHash string) (string, error) { + if err := c.reconnect(ctx); err != nil { + return "", err + } + return c.client.GetRawTransaction(ctx, txHash) +} + +func (c *electrumClient) BroadcastTransaction(ctx context.Context, rawTx string) (string, error) { + if err := c.reconnect(ctx); err != nil { + return "", err + } + return c.client.BroadcastTransaction(ctx, rawTx) +} + +func (c *electrumClient) GetFee(ctx context.Context, target uint32) (float32, error) { + if err := c.reconnect(ctx); err != nil { + return 0, err + } + return c.client.GetFee(ctx, target) +} diff --git a/electrum/electrum.go b/electrum/electrum.go index 4a224ff8..43dd0c60 100644 --- a/electrum/electrum.go +++ b/electrum/electrum.go @@ -10,4 +10,6 @@ type RPC interface { SubscribeHeaders(ctx context.Context) (<-chan *electrum.SubscribeHeadersResult, error) GetHistory(ctx context.Context, scripthash string) ([]*electrum.GetMempoolResult, error) GetRawTransaction(ctx context.Context, txHash string) (string, error) + BroadcastTransaction(ctx context.Context, rawTx string) (string, error) + GetFee(ctx context.Context, target uint32) (float32, error) } diff --git a/electrum/mock/electrum.go b/electrum/mock/electrum.go new file mode 100644 index 00000000..b6ff9a0a --- /dev/null +++ b/electrum/mock/electrum.go @@ -0,0 +1,116 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: electrum/electrum.go +// +// Generated by this command: +// +// mockgen -source=electrum/electrum.go -destination=electrum/mock/electrum.go +// + +// Package mock_electrum is a generated GoMock package. +package mock_electrum + +import ( + context "context" + reflect "reflect" + + electrum "github.com/checksum0/go-electrum/electrum" + gomock "go.uber.org/mock/gomock" +) + +// MockRPC is a mock of RPC interface. +type MockRPC struct { + ctrl *gomock.Controller + recorder *MockRPCMockRecorder +} + +// MockRPCMockRecorder is the mock recorder for MockRPC. +type MockRPCMockRecorder struct { + mock *MockRPC +} + +// NewMockRPC creates a new mock instance. +func NewMockRPC(ctrl *gomock.Controller) *MockRPC { + mock := &MockRPC{ctrl: ctrl} + mock.recorder = &MockRPCMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRPC) EXPECT() *MockRPCMockRecorder { + return m.recorder +} + +// BroadcastTransaction mocks base method. +func (m *MockRPC) BroadcastTransaction(ctx context.Context, rawTx string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastTransaction", ctx, rawTx) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastTransaction indicates an expected call of BroadcastTransaction. +func (mr *MockRPCMockRecorder) BroadcastTransaction(ctx, rawTx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastTransaction", reflect.TypeOf((*MockRPC)(nil).BroadcastTransaction), ctx, rawTx) +} + +// GetFee mocks base method. +func (m *MockRPC) GetFee(ctx context.Context, target uint32) (float32, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFee", ctx, target) + ret0, _ := ret[0].(float32) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFee indicates an expected call of GetFee. +func (mr *MockRPCMockRecorder) GetFee(ctx, target any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFee", reflect.TypeOf((*MockRPC)(nil).GetFee), ctx, target) +} + +// GetHistory mocks base method. +func (m *MockRPC) GetHistory(ctx context.Context, scripthash string) ([]*electrum.GetMempoolResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHistory", ctx, scripthash) + ret0, _ := ret[0].([]*electrum.GetMempoolResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHistory indicates an expected call of GetHistory. +func (mr *MockRPCMockRecorder) GetHistory(ctx, scripthash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHistory", reflect.TypeOf((*MockRPC)(nil).GetHistory), ctx, scripthash) +} + +// GetRawTransaction mocks base method. +func (m *MockRPC) GetRawTransaction(ctx context.Context, txHash string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRawTransaction", ctx, txHash) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRawTransaction indicates an expected call of GetRawTransaction. +func (mr *MockRPCMockRecorder) GetRawTransaction(ctx, txHash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRawTransaction", reflect.TypeOf((*MockRPC)(nil).GetRawTransaction), ctx, txHash) +} + +// SubscribeHeaders mocks base method. +func (m *MockRPC) SubscribeHeaders(ctx context.Context) (<-chan *electrum.SubscribeHeadersResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeHeaders", ctx) + ret0, _ := ret[0].(<-chan *electrum.SubscribeHeadersResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubscribeHeaders indicates an expected call of SubscribeHeaders. +func (mr *MockRPCMockRecorder) SubscribeHeaders(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeHeaders", reflect.TypeOf((*MockRPC)(nil).SubscribeHeaders), ctx) +} diff --git a/lwk/conf_builder.go b/lwk/conf_builder.go index 75e3099e..13c0a0b5 100644 --- a/lwk/conf_builder.go +++ b/lwk/conf_builder.go @@ -19,14 +19,14 @@ func (b *confBuilder) DefaultConf() (*confBuilder, error) { switch b.network { case NetworkTestnet: lwkEndpoint = "http://localhost:32111" - electrumEndpoint = "tcp://blockstream.info:465" + electrumEndpoint = "ssl://blockstream.info:465" case NetworkRegtest: lwkEndpoint = "http://localhost:32112" electrumEndpoint = "tcp://localhost:60401" default: // mainnet is the default port lwkEndpoint = "http://localhost:32110" - electrumEndpoint = "tcp://blockstream.info:995" + electrumEndpoint = "ssl://blockstream.info:995" } lwkURL, err := NewConfURL(lwkEndpoint) if err != nil { diff --git a/lwk/config.go b/lwk/config.go index 1071e76b..28f5206a 100644 --- a/lwk/config.go +++ b/lwk/config.go @@ -32,6 +32,10 @@ func (c *Conf) GetElectrumEndpoint() string { return c.electrumEndpoint.Host } +func (c *Conf) IsElectrumWithTLS() bool { + return c.electrumEndpoint.Scheme == "ssl" +} + func (c *Conf) GetNetwork() string { return c.network.String() } @@ -53,6 +57,19 @@ func (c *Conf) GetChain() *network.Network { } } +func (c *Conf) GetAssetID() string { + switch c.network { + case NetworkMainnet: + return network.Liquid.AssetID + case NetworkRegtest: + return network.Regtest.AssetID + case NetworkTestnet: + return network.Testnet.AssetID + default: + return network.Testnet.AssetID + } +} + func (c *Conf) Enabled() bool { return Validate( c.electrumEndpoint, diff --git a/lwk/electrumtxwatcher.go b/lwk/electrumtxwatcher.go index 5598b9ca..dd2faf68 100644 --- a/lwk/electrumtxwatcher.go +++ b/lwk/electrumtxwatcher.go @@ -11,9 +11,12 @@ import ( "github.com/elementsproject/peerswap/swap" ) -// initialBlockHeaderSubscriptionTimeout is -// the initial block header subscription timeout. -const initialBlockHeaderSubscriptionTimeout = 1000 * time.Second +const ( + // initialBlockHeaderSubscriptionTimeout is + // the initial block header subscription timeout. + initialBlockHeaderSubscriptionTimeout = 1000 * time.Second + blockHeaderSubscriptionTicker = 30 * time.Second +) type electrumTxWatcher struct { electrumClient electrum.RPC @@ -21,12 +24,17 @@ type electrumTxWatcher struct { subscriber electrum.BlockHeaderSubscriber confirmationCallback func(swapId string, txHex string, err error) error csvCallback func(swapId string) error + // resubscribeTicker periodically resubscribes to the block header subscription. + // The connection with the electrum client is + // disconnected after a certain period of time. + resubscribeTicker *time.Ticker } func NewElectrumTxWatcher(electrumClient electrum.RPC) (*electrumTxWatcher, error) { r := &electrumTxWatcher{ - electrumClient: electrumClient, - subscriber: electrum.NewLiquidBlockHeaderSubscriber(), + electrumClient: electrumClient, + subscriber: electrum.NewLiquidBlockHeaderSubscriber(), + resubscribeTicker: time.NewTicker(blockHeaderSubscriptionTicker), } return r, nil } @@ -40,6 +48,9 @@ func (r *electrumTxWatcher) StartWatchingTxs() error { go func() { for { select { + case <-ctx.Done(): + log.Infof("Context canceled, stopping watching txs.") + return case blockHeader, ok := <-headerSubscription: if !ok { log.Infof("Header subscription closed, stopping watching txs.") @@ -47,15 +58,17 @@ func (r *electrumTxWatcher) StartWatchingTxs() error { } r.blockHeight = electrum.BlocKHeight(blockHeader.Height) log.Infof("New block received. block height:%d", r.blockHeight) - err := r.subscriber.Update(ctx, r.blockHeight) + err = r.subscriber.Update(ctx, r.blockHeight) if err != nil { log.Infof("Error notifying tx observers: %v", err) continue } - - case <-ctx.Done(): - log.Infof("Context canceled, stopping watching txs.") - return + case <-r.resubscribeTicker.C: + headerSubscription, err = r.electrumClient.SubscribeHeaders(ctx) + if err != nil { + log.Infof("Error reloading electrum client: %v", err) + continue + } } } }() @@ -65,6 +78,7 @@ func (r *electrumTxWatcher) StartWatchingTxs() error { // waitForInitialBlockHeaderSubscription waits for the initial block header subscription to be confirmed. func (r *electrumTxWatcher) waitForInitialBlockHeaderSubscription(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, initialBlockHeaderSubscriptionTimeout) + const heartbeatInterval = 100 * time.Millisecond defer cancel() for { select { @@ -76,6 +90,7 @@ func (r *electrumTxWatcher) waitForInitialBlockHeaderSubscription(ctx context.Co return nil } } + time.Sleep(heartbeatInterval) } } diff --git a/lwk/electrumtxwatcher_test.go b/lwk/electrumtxwatcher_test.go index 29b99151..a05a925f 100644 --- a/lwk/electrumtxwatcher_test.go +++ b/lwk/electrumtxwatcher_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/checksum0/go-electrum/electrum" + mock_txwatcher "github.com/elementsproject/peerswap/electrum/mock" "github.com/elementsproject/peerswap/lwk" - mock_txwatcher "github.com/elementsproject/peerswap/lwk/mock" "github.com/elementsproject/peerswap/onchain" "github.com/elementsproject/peerswap/swap" "github.com/stretchr/testify/assert" @@ -36,7 +36,7 @@ func TestElectrumTxWatcher_Callback(t *testing.T) { targetTXHeight int32 = 100 ) - electrumRPC := mock_txwatcher.NewMockelectrumRPC(gomock.NewController(t)) + electrumRPC := mock_txwatcher.NewMockRPC(gomock.NewController(t)) headerResultChan := make(chan *electrum.SubscribeHeadersResult, 1) electrumRPC.EXPECT().SubscribeHeaders(gomock.Any()). Return(headerResultChan, nil) @@ -94,7 +94,7 @@ func TestElectrumTxWatcher_Callback(t *testing.T) { targetTXHeight = int32(100) ) - electrumRPC := mock_txwatcher.NewMockelectrumRPC(gomock.NewController(t)) + electrumRPC := mock_txwatcher.NewMockRPC(gomock.NewController(t)) headerResultChan := make(chan *electrum.SubscribeHeadersResult, 1) electrumRPC.EXPECT().SubscribeHeaders(gomock.Any()). Return(headerResultChan, nil) diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 0bb66124..d284cd7c 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -3,66 +3,95 @@ package lwk import ( "context" "errors" - "log" + "math" "strings" + "time" - "github.com/checksum0/go-electrum/electrum" - + "github.com/elementsproject/peerswap/electrum" + "github.com/elementsproject/peerswap/log" "github.com/elementsproject/peerswap/swap" "github.com/elementsproject/peerswap/wallet" - "github.com/vulpemventures/go-elements/network" ) -// Satoshi represents a satoshi value. +// Satoshi represents a Satoshi value. type Satoshi = uint64 -// SatPerKVByte represents a fee rate in sat/kb. -type SatPerKVByte = float64 - const ( - minimumSatPerByte SatPerKVByte = 0.1 // 1 kb = 1000 bytes - kb float64 = 1000 + kb = 1000 + btcToSatoshiExp = 8 + // TODO: Basically, the inherited ctx should be used + // and there is no need to specify a timeout here. + // Set up here because ctx is not inherited throughout the current codebase. + defaultContextTimeout = time.Second * 5 + minimumSatPerByte SatPerKVByte = 0.1 ) +// SatPerKVByte represents a fee rate in sat/kb. +type SatPerKVByte float64 + +func SatPerKVByteFromFeeBTCPerKb(feeBTCPerKb float64) SatPerKVByte { + s := SatPerKVByte(feeBTCPerKb * math.Pow10(btcToSatoshiExp) / kb) + if s < minimumSatPerByte { + log.Infof("using minimum fee: %v.", minimumSatPerByte) + return minimumSatPerByte + } + return s +} + +func (s SatPerKVByte) GetSatPerKVByte() float64 { + return float64(s) +} + +func (s SatPerKVByte) GetFee(txSize int64) Satoshi { + return Satoshi(s.GetSatPerKVByte() * float64(txSize)) +} + // LWKRpcWallet uses the elementsd rpc wallet type LWKRpcWallet struct { - walletName string - signerName string + c *Conf lwkClient *lwkclient - electrumClient *electrum.Client + electrumClient electrum.RPC } -func NewLWKRpcWallet(lwkClient *lwkclient, electrumClient *electrum.Client, walletName, signerName string) (*LWKRpcWallet, error) { - if lwkClient == nil || electrumClient == nil { - return nil, errors.New("rpc client is nil") +func NewLWKRpcWallet(ctx context.Context, c *Conf) (*LWKRpcWallet, error) { + if !c.Enabled() { + return nil, errors.New("LWKRpcWallet is not enabled") } - if walletName == "" || signerName == "" { - return nil, errors.New("wallet name or signer name is empty") + ec, err := electrum.NewElectrumClient(ctx, c.GetElectrumEndpoint(), c.IsElectrumWithTLS()) + if err != nil { + return nil, err } rpcWallet := &LWKRpcWallet{ - walletName: walletName, - signerName: signerName, - lwkClient: lwkClient, - electrumClient: electrumClient, + lwkClient: NewLwk(c.GetLWKEndpoint()), + electrumClient: ec, + c: c, } - err := rpcWallet.setupWallet(context.Background()) + err = rpcWallet.setupWallet(ctx) // Evaluate rpcWallet.setupWallet(ctx) before the return statement if err != nil { return nil, err } return rpcWallet, nil } +// GetElectrumClient returns the electrum client. +func (c *LWKRpcWallet) GetElectrumClient() electrum.RPC { + return c.electrumClient +} + // setupWallet checks if the swap wallet is already loaded in elementsd, if not it loads/creates it func (r *LWKRpcWallet) setupWallet(ctx context.Context) error { - res, err := r.lwkClient.walletDetails(ctx, &walletDetailsRequest{ - WalletName: r.walletName, + timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout) + defer cancel() + res, err := r.lwkClient.walletDetails(timeoutCtx, &walletDetailsRequest{ + WalletName: r.c.GetWalletName(), }) if err != nil { // 32008 is the error code for wallet not found of lwk if strings.HasPrefix(err.Error(), "-32008") { - return r.createWallet(ctx, r.walletName, r.signerName) + log.Infof("wallet not found, creating wallet with name %s", r.c.GetWalletName) + return r.createWallet(timeoutCtx, r.c.GetWalletName(), r.c.GetSignerName()) } return err } @@ -70,8 +99,8 @@ func (r *LWKRpcWallet) setupWallet(ctx context.Context) error { if len(signers) != 1 { return errors.New("invalid number of signers") } - if signers[0].Name != r.signerName { - return errors.New("signer name is not correct. expected: " + r.signerName + " got: " + signers[0].Name) + if signers[0].Name != r.c.GetSignerName() { + return errors.New("signer name is not correct. expected: " + r.c.GetSignerName() + " got: " + signers[0].Name) } return nil } @@ -111,7 +140,8 @@ func (r *LWKRpcWallet) createWallet(ctx context.Context, walletName, signerName // CreateFundedTransaction takes a tx with outputs and adds inputs in order to spend the tx func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningParams, asset []byte) (txid, rawTx string, fee Satoshi, err error) { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() feerate := float64(r.getFeePerKb(ctx)) * kb fundedTx, err := r.lwkClient.send(ctx, &sendRequest{ Addressees: []*unvalidatedAddressee{ @@ -120,21 +150,21 @@ func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningPar Satoshi: swapParams.Amount, }, }, - WalletName: r.walletName, + WalletName: r.c.GetWalletName(), FeeRate: &feerate, }) if err != nil { return "", "", 0, err } signed, err := r.lwkClient.sign(ctx, &signRequest{ - SignerName: r.signerName, + SignerName: r.c.GetSignerName(), Pset: fundedTx.Pset, }) if err != nil { return "", "", 0, err } broadcasted, err := r.lwkClient.broadcast(ctx, &broadcastRequest{ - WalletName: r.walletName, + WalletName: r.c.GetWalletName(), Pset: signed.Pset, }) if err != nil { @@ -149,21 +179,23 @@ func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningPar // GetBalance returns the balance in sats func (r *LWKRpcWallet) GetBalance() (Satoshi, error) { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() balance, err := r.lwkClient.balance(ctx, &balanceRequest{ - WalletName: r.walletName, + WalletName: r.c.GetWalletName(), }) if err != nil { return 0, err } - return uint64(balance.Balance[network.Regtest.AssetID]), nil + return uint64(balance.Balance[r.c.GetAssetID()]), nil } // GetAddress returns a new blech32 address func (r *LWKRpcWallet) GetAddress() (string, error) { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() address, err := r.lwkClient.address(ctx, &addressRequest{ - WalletName: r.walletName}) + WalletName: r.c.GetWalletName()}) if err != nil { return "", err } @@ -172,9 +204,10 @@ func (r *LWKRpcWallet) GetAddress() (string, error) { // SendToAddress sends an amount to an address func (r *LWKRpcWallet) SendToAddress(address string, amount Satoshi) (string, error) { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() sendres, err := r.lwkClient.send(ctx, &sendRequest{ - WalletName: r.walletName, + WalletName: r.c.GetWalletName(), Addressees: []*unvalidatedAddressee{ { Address: address, @@ -187,14 +220,14 @@ func (r *LWKRpcWallet) SendToAddress(address string, amount Satoshi) (string, er } signed, err := r.lwkClient.sign(ctx, &signRequest{ - SignerName: r.signerName, + SignerName: r.c.GetSignerName(), Pset: sendres.Pset, }) if err != nil { - log.Fatal(err) + return "", err } broadcastres, err := r.lwkClient.broadcast(ctx, &broadcastRequest{ - WalletName: r.walletName, + WalletName: r.c.GetWalletName(), Pset: signed.Pset, }) if err != nil { @@ -204,7 +237,8 @@ func (r *LWKRpcWallet) SendToAddress(address string, amount Satoshi) (string, er } func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() res, err := r.electrumClient.BroadcastTransaction(ctx, txHex) if err != nil { return "", err @@ -214,21 +248,16 @@ func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { func (r *LWKRpcWallet) getFeePerKb(ctx context.Context) SatPerKVByte { feeBTCPerKb, err := r.electrumClient.GetFee(ctx, wallet.LiquidTargetBlocks) - satPerByte := float64(feeBTCPerKb) * math.Pow10(int(8)) / kb - if satPerByte < minimumSatPerByte { - satPerByte = minimumSatPerByte - } if err != nil { - satPerByte = minimumSatPerByte + log.Infof("error getting fee: %v.", err) } - return satPerByte + return SatPerKVByteFromFeeBTCPerKb(float64(feeBTCPerKb)) } func (r *LWKRpcWallet) GetFee(txSize int64) (Satoshi, error) { - ctx := context.Background() - // assume largest witness - fee := r.getFeePerKb(ctx) * float64(txSize) - return Satoshi(fee), nil + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() + return r.getFeePerKb(ctx).GetFee(txSize), nil } func (r *LWKRpcWallet) SetLabel(txID, address, label string) error { diff --git a/lwk/lwkwallet_test.go b/lwk/lwkwallet_test.go new file mode 100644 index 00000000..2650d279 --- /dev/null +++ b/lwk/lwkwallet_test.go @@ -0,0 +1,32 @@ +package lwk_test + +import ( + "testing" + + "github.com/elementsproject/peerswap/lwk" + "github.com/stretchr/testify/assert" +) + +func TestSatPerKVByteFromFeeBTCPerKb(t *testing.T) { + t.Parallel() + t.Run("below minimum minimumSatPerByte", func(t *testing.T) { + t.Parallel() + var ( + txsize int64 = 1000 + FeeBTCPerKb = 0.0000001 + ) + got := lwk.SatPerKVByteFromFeeBTCPerKb(FeeBTCPerKb).GetFee(txsize) + want := lwk.Satoshi(100) + assert.Equal(t, want, got) + }) + t.Run("above minimum minimumSatPerByte", func(t *testing.T) { + t.Parallel() + var ( + txsize int64 = 1000 + FeeBTCPerKb = 0.000002 + ) + got := lwk.SatPerKVByteFromFeeBTCPerKb(FeeBTCPerKb).GetFee(txsize) + want := lwk.Satoshi(200) + assert.Equal(t, want, got) + }) +} diff --git a/lwk/mock/electrumRPC.go b/lwk/mock/electrumRPC.go deleted file mode 100644 index 6fd3ffef..00000000 --- a/lwk/mock/electrumRPC.go +++ /dev/null @@ -1,86 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: lwk/electrumRPC.go -// -// Generated by this command: -// -// mockgen -source=lwk/electrumRPC.go -destination=lwk/mock/electrumRPC.go -// - -// Package mock_txwatcher is a generated GoMock package. -package mock_txwatcher - -import ( - context "context" - reflect "reflect" - - electrum "github.com/checksum0/go-electrum/electrum" - gomock "go.uber.org/mock/gomock" -) - -// MockelectrumRPC is a mock of electrumRPC interface. -type MockelectrumRPC struct { - ctrl *gomock.Controller - recorder *MockelectrumRPCMockRecorder -} - -// MockelectrumRPCMockRecorder is the mock recorder for MockelectrumRPC. -type MockelectrumRPCMockRecorder struct { - mock *MockelectrumRPC -} - -// NewMockelectrumRPC creates a new mock instance. -func NewMockelectrumRPC(ctrl *gomock.Controller) *MockelectrumRPC { - mock := &MockelectrumRPC{ctrl: ctrl} - mock.recorder = &MockelectrumRPCMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockelectrumRPC) EXPECT() *MockelectrumRPCMockRecorder { - return m.recorder -} - -// GetHistory mocks base method. -func (m *MockelectrumRPC) GetHistory(ctx context.Context, scripthash string) ([]*electrum.GetMempoolResult, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetHistory", ctx, scripthash) - ret0, _ := ret[0].([]*electrum.GetMempoolResult) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetHistory indicates an expected call of GetHistory. -func (mr *MockelectrumRPCMockRecorder) GetHistory(ctx, scripthash any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHistory", reflect.TypeOf((*MockelectrumRPC)(nil).GetHistory), ctx, scripthash) -} - -// GetRawTransaction mocks base method. -func (m *MockelectrumRPC) GetRawTransaction(ctx context.Context, txHash string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRawTransaction", ctx, txHash) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRawTransaction indicates an expected call of GetRawTransaction. -func (mr *MockelectrumRPCMockRecorder) GetRawTransaction(ctx, txHash any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRawTransaction", reflect.TypeOf((*MockelectrumRPC)(nil).GetRawTransaction), ctx, txHash) -} - -// SubscribeHeaders mocks base method. -func (m *MockelectrumRPC) SubscribeHeaders(ctx context.Context) (<-chan *electrum.SubscribeHeadersResult, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubscribeHeaders", ctx) - ret0, _ := ret[0].(<-chan *electrum.SubscribeHeadersResult) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SubscribeHeaders indicates an expected call of SubscribeHeaders. -func (mr *MockelectrumRPCMockRecorder) SubscribeHeaders(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeHeaders", reflect.TypeOf((*MockelectrumRPC)(nil).SubscribeHeaders), ctx) -}