From ae9ae9980b5f3ae0be3bd30cb854d62b607353f7 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 27 Mar 2024 06:45:27 +0900 Subject: [PATCH 01/29] txwatcher: delete deadcode remove blockwatcher.go and associated block height functions The blockwatcher.go file and its associated functions for tracking block height changes have been removed. block height tracking is no longer necessary and has been replaced by a different mechanism. --- txwatcher/blockwatcher.go | 95 --------------------------------------- txwatcher/rpctxwatcher.go | 44 ------------------ 2 files changed, 139 deletions(-) delete mode 100644 txwatcher/blockwatcher.go diff --git a/txwatcher/blockwatcher.go b/txwatcher/blockwatcher.go deleted file mode 100644 index 14773357..00000000 --- a/txwatcher/blockwatcher.go +++ /dev/null @@ -1,95 +0,0 @@ -package txwatcher - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/elementsproject/peerswap/log" -) - -type ChainRPC interface { - GetBlockHeight() (uint64, error) -} - -type BlockWatcher struct { - sync.Mutex - - subscriber map[string]func(next uint64) - chainRRC ChainRPC -} - -func NewBlockWatcher(chainRPC ChainRPC) *BlockWatcher { - return &BlockWatcher{ - subscriber: map[string]func(next uint64){}, - chainRRC: chainRPC, - } -} - -func (b *BlockWatcher) Watch(ctx context.Context, interval time.Duration) error { - tick := time.NewTicker(interval) - defer tick.Stop() - current, err := b.chainRRC.GetBlockHeight() - if err != nil { - return fmt.Errorf("could not get initial block height: %v", err) - } - for { - select { - case <-ctx.Done(): - return ErrContextCanceled - case <-tick.C: - next, err := b.chainRRC.GetBlockHeight() - if err != nil { - log.Debugf("could not get block height: %v", err) - } - if next < current { - // Todo: think about shutting down here since something really bad happened. - log.Debugf( - "did not expect block height to decrease: current=%d, next=%d", - current, - next, - ) - continue - } - if next == current+1 { - // Got next block, announce it! - go b.announce(next) - continue - } - if next > current { - // Skipped a block, that is not bad but we may want to log it - // anyways. - log.Debugf( - "skipped some blocks: current=%d, next=%d", - current, - next, - ) - go b.announce(next) - continue - } - } - } -} - -func (b *BlockWatcher) announce(nextHeight uint64) { - b.Lock() - defer b.Unlock() - for _, handler := range b.subscriber { - go handler(nextHeight) - } -} - -func (b *BlockWatcher) Subscribe(id string, handler func(uint64)) { - b.Lock() - defer b.Unlock() - // We don't care about double subscriptions for now. - b.subscriber[id] = handler -} - -func (b *BlockWatcher) Unbscribe(id string) { - b.Lock() - defer b.Unlock() - // This is a noop if id is not in the map. - delete(b.subscriber, id) -} diff --git a/txwatcher/rpctxwatcher.go b/txwatcher/rpctxwatcher.go index 8a8cca64..ffbae41e 100644 --- a/txwatcher/rpctxwatcher.go +++ b/txwatcher/rpctxwatcher.go @@ -18,7 +18,6 @@ type BlockchainRpc interface { GetTxOut(txid string, vout uint32) (*TxOutResp, error) GetBlockHash(height uint32) (string, error) GetRawtransactionWithBlockHash(txId string, blockHash string) (string, error) - GetBlockHeightByHash(blockhash string) (uint32, error) } type TxOutResp struct { @@ -217,31 +216,6 @@ func (l *BlockchainRpcTxWatcher) AddWaitForConfirmationTx(swapId, txId string, v newBlock <- uint32(height) } -func (s *BlockchainRpcTxWatcher) CheckTxConfirmed(swapId string, txId string, vout uint32) string { - res, err := s.blockchain.GetTxOut(txId, vout) - if err != nil { - log.Infof("watchlist fetchtx err: %v", err) - return "" - } - if res == nil { - return "" - } - if !(res.Confirmations >= s.requiredConfs) { - log.Infof("tx %s on swap %s does not have enough confirmations", txId, swapId) - return "" - } - if s.txCallback == nil { - return "" - } - txHex, err := s.TxHexFromId(res, txId) - if err != nil { - log.Infof("watchlist txfrom hex err: %v", err) - return "" - } - - return txHex -} - func (l *BlockchainRpcTxWatcher) checkTxAboveCsvHight(txId string, vout uint32) (bool, error) { res, err := l.blockchain.GetTxOut(txId, vout) if err != nil { @@ -300,24 +274,6 @@ func (l *BlockchainRpcTxWatcher) AddCsvCallback(f func(swapId string) error) { l.csvPassedCallback = f } -func (l *BlockchainRpcTxWatcher) TxHexFromId(resp *TxOutResp, txId string) (string, error) { - blockheight, err := l.blockchain.GetBlockHeightByHash(resp.BestBlockHash) - if err != nil { - return "", err - } - - blockhash, err := l.blockchain.GetBlockHash(uint32(blockheight) - resp.Confirmations + 1) - if err != nil { - return "", err - } - - rawTxHex, err := l.blockchain.GetRawtransactionWithBlockHash(txId, blockhash) - if err != nil { - return "", err - } - return rawTxHex, nil -} - func (l *BlockchainRpcTxWatcher) observationLoop( ctx context.Context, swapId, From 0735dabbb0a03b4cecebb7dd4c04f89d0209328a Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 27 Mar 2024 06:52:03 +0900 Subject: [PATCH 02/29] multi: refactor interfaces onchain wallet `CreateFundedTransaction` and `FinalizeFundedTransaction` have merged interfaces, CreateAndBroadcastTransaction. This is because there was originally no reason to separate them, and because lwk uses the psbt format, transacrion conversion needs to be performed. The liquid wallet interface should not have a unique liquid wallet-dependent definition. However, there were some that depended on gelements, so they were separated out. --- clightning/clightning.go | 10 +- clightning/clightning_commands.go | 6 - clightning/clightning_dev.go | 238 ------------------------------ clightning/clightning_wallet.go | 31 ++-- cmd/peerswap-plugin/main.go | 26 +++- cmd/peerswaplnd/peerswapd/main.go | 57 ++++++- go.mod | 9 +- go.sum | 4 +- lnd/lnd_wallet.go | 32 ++-- lwk/lwkwallet.go | 202 +++++++++++++++++++++++++ onchain/liquid.go | 97 +++--------- onchain/liquid_test.go | 2 +- peerswaprpc/server.go | 24 +-- swap/actions.go | 7 +- swap/services.go | 6 +- swap/swap_out_sender_test.go | 8 +- wallet/elementsrpcwallet.go | 72 +++++++-- wallet/wallet.go | 13 +- 18 files changed, 415 insertions(+), 429 deletions(-) delete mode 100644 clightning/clightning_dev.go create mode 100644 lwk/lwkwallet.go diff --git a/clightning/clightning.go b/clightning/clightning.go index 0995441c..b01af74c 100644 --- a/clightning/clightning.go +++ b/clightning/clightning.go @@ -16,7 +16,6 @@ import ( "github.com/elementsproject/glightning/gbitcoin" "github.com/elementsproject/peerswap/onchain" - "github.com/elementsproject/glightning/gelements" "github.com/elementsproject/glightning/glightning" "github.com/elementsproject/glightning/jrpc2" "github.com/elementsproject/peerswap/lightning" @@ -64,14 +63,12 @@ type ClightningClient struct { glightning *glightning.Lightning Plugin *glightning.Plugin - liquidWallet *wallet.ElementsRpcWallet + liquidWallet wallet.Wallet swaps *swap.SwapService requestedSwaps *swap.RequestedSwapsPrinter policy PolicyReloader pollService *poll.Service - Gelements *gelements.Elements - gbitcoin *gbitcoin.Bitcoin bitcoinChain *onchain.BitcoinOnChain bitcoinNetwork *chaincfg.Params @@ -323,14 +320,13 @@ func (cl *ClightningClient) GetPreimage() (lightning.Preimage, error) { } // SetupClients injects the required services -func (cl *ClightningClient) SetupClients(liquidWallet *wallet.ElementsRpcWallet, +func (cl *ClightningClient) SetupClients(liquidWallet wallet.Wallet, swaps *swap.SwapService, - policy PolicyReloader, requestedSwaps *swap.RequestedSwapsPrinter, elements *gelements.Elements, + policy PolicyReloader, requestedSwaps *swap.RequestedSwapsPrinter, bitcoin *gbitcoin.Bitcoin, bitcoinChain *onchain.BitcoinOnChain, pollService *poll.Service) { cl.liquidWallet = liquidWallet cl.requestedSwaps = requestedSwaps cl.swaps = swaps - cl.Gelements = elements cl.policy = policy cl.gbitcoin = bitcoin cl.pollService = pollService diff --git a/clightning/clightning_commands.go b/clightning/clightning_commands.go index 14b1e240..7d3ffe23 100644 --- a/clightning/clightning_commands.go +++ b/clightning/clightning_commands.go @@ -247,9 +247,6 @@ func (l *SwapOut) Call() (jrpc2.Result, error) { if !l.cl.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - if l.cl.Gelements == nil { - return nil, errors.New("peerswap was not started with liquid node config") - } } else if strings.Compare(l.Asset, "btc") == 0 { if !l.cl.swaps.BitcoinEnabled { @@ -366,9 +363,6 @@ func (l *SwapIn) Call() (jrpc2.Result, error) { if !l.cl.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - if l.cl.Gelements == nil { - return nil, errors.New("peerswap was not started with liquid node config") - } liquidBalance, err := l.cl.liquidWallet.GetBalance() if err != nil { return nil, err diff --git a/clightning/clightning_dev.go b/clightning/clightning_dev.go deleted file mode 100644 index d80725c7..00000000 --- a/clightning/clightning_dev.go +++ /dev/null @@ -1,238 +0,0 @@ -//go:build dev -// +build dev - -package clightning - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - - "github.com/elementsproject/glightning/jrpc2" - "github.com/elementsproject/peerswap/lightning" -) - -var ( - regtestOpReturnAddress = "ert1qfkht0df45q00kzyayagw6vqhfhe8ve7z7wecm0xsrkgmyulewlzqumq3ep" -) - -func init() { - devmethods = append(devmethods, &FaucetMethod{}, &GenerateMethod{}, &BigInvoice{}, &BigPay{}, &DebugCrashMethod{}) -} - -type DebugCrashMethod struct { - cl *ClightningClient `json:"-"` -} - -func (g *DebugCrashMethod) Description() string { - return "crashes the plugin" -} - -func (g *DebugCrashMethod) LongDescription() string { - return "" -} - -func (g *DebugCrashMethod) Get(client *ClightningClient) jrpc2.ServerMethod { - return &DebugCrashMethod{ - cl: client, - } -} - -func (g *DebugCrashMethod) Name() string { - return "peerswap-dev-debugcrash" -} - -func (g *DebugCrashMethod) New() interface{} { - return &DebugCrashMethod{ - cl: g.cl, - } -} - -func (g *DebugCrashMethod) Call() (jrpc2.Result, error) { - panic("debug") - return "", nil -} - -type FaucetMethod struct { - cl *ClightningClient `json:"-"` -} - -func (g *FaucetMethod) Description() string { - return "faucets liquid funds to local liquidWallet" -} - -func (g *FaucetMethod) LongDescription() string { - return "" -} - -func (g *FaucetMethod) Get(client *ClightningClient) jrpc2.ServerMethod { - return &FaucetMethod{ - cl: client, - } -} - -func (g *FaucetMethod) Name() string { - return "peerswap-dev-liquid-faucet" -} - -func (g *FaucetMethod) New() interface{} { - return &FaucetMethod{ - cl: g.cl, - } -} - -func (g *FaucetMethod) Call() (jrpc2.Result, error) { - addr, err := g.cl.liquidWallet.GetAddress() - if err != nil { - return nil, err - } - res, err := faucet(addr) - return res, err -} - -type GenerateMethod struct { - amount int `json:"amount` - - cl *ClightningClient `json:"-"` -} - -func (g *GenerateMethod) Description() string { - return "generates liquid blocks" -} - -func (g *GenerateMethod) LongDescription() string { - return "" -} - -func (g *GenerateMethod) Get(client *ClightningClient) jrpc2.ServerMethod { - return &GenerateMethod{ - cl: client, - } -} - -func (g *GenerateMethod) Name() string { - return "peerswap-dev-liquid-generate" -} - -func (g *GenerateMethod) New() interface{} { - return &GenerateMethod{ - cl: g.cl, - } -} - -func (g *GenerateMethod) Call() (jrpc2.Result, error) { - if g.amount == 0 { - g.amount = 1 - } - res, err := g.cl.Gelements.GenerateToAddress(regtestOpReturnAddress, uint(g.amount)) - return res, err -} - -type BigInvoice struct { - SatAmt uint64 - cl *ClightningClient -} - -func (b *BigInvoice) Name() string { - return "peerswap-biginvoice" -} - -func (b *BigInvoice) New() interface{} { - return &BigInvoice{ - cl: b.cl, - SatAmt: b.SatAmt, - } -} - -func (b *BigInvoice) Call() (jrpc2.Result, error) { - if b.SatAmt == 0 { - b.SatAmt = 50000000 - } - preimage, err := lightning.GetPreimage() - if err != nil { - return nil, err - } - invoice, err := b.cl.glightning.CreateInvoice(uint64(b.SatAmt*1000), randomString(), randomString(), 72000000, nil, preimage.String(), false) - if err != nil { - return nil, err - } - return invoice.Bolt11, nil -} - -func (b *BigInvoice) Get(client *ClightningClient) jrpc2.ServerMethod { - return &BigInvoice{ - cl: client, - } -} - -func (b *BigInvoice) Description() string { - return "biginvoice" -} - -func (b *BigInvoice) LongDescription() string { - return "biginvoice" -} - -type BigPay struct { - Payreq string - ChannelId string - cl *ClightningClient -} - -func (b *BigPay) Get(client *ClightningClient) jrpc2.ServerMethod { - return &BigPay{cl: client} -} - -func (b *BigPay) Description() string { - return "bigpay" -} - -func (b *BigPay) LongDescription() string { - return "bigpay" -} - -func (b *BigPay) Name() string { - return "peerswap-bigpay" -} - -func (b *BigPay) New() interface{} { - return &BigPay{cl: b.cl} -} - -func (b *BigPay) Call() (jrpc2.Result, error) { - res, err := b.cl.RebalancePayment(b.Payreq, b.ChannelId) - if err != nil { - return nil, err - } - return res, nil -} - -func faucet(address string) (string, error) { - baseURL := "http://localhost:3001" - - url := fmt.Sprintf("%s/faucet", baseURL) - payload := map[string]string{"address": address} - body, _ := json.Marshal(payload) - - resp, err := http.Post(url, "application/json", bytes.NewBuffer(body)) - if err != nil { - return "", err - } - - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - if res := string(data); len(res) <= 0 || strings.Contains(res, "sendtoaddress") { - return "", fmt.Errorf("cannot fund address with faucet: %s", res) - } - - respBody := map[string]string{} - if err := json.Unmarshal(data, &respBody); err != nil { - return "", err - } - return respBody["txId"], nil -} diff --git a/clightning/clightning_wallet.go b/clightning/clightning_wallet.go index af9347c8..3170079a 100644 --- a/clightning/clightning_wallet.go +++ b/clightning/clightning_wallet.go @@ -13,10 +13,10 @@ import ( "github.com/elementsproject/peerswap/version" ) -func (cl *ClightningClient) CreateOpeningTransaction(swapParams *swap.OpeningParams) (unpreparedTxHex, address string, fee uint64, vout uint32, err error) { +func (cl *ClightningClient) CreateOpeningTransaction(swapParams *swap.OpeningParams) (unpreparedTxHex, address, txId string, fee uint64, vout uint32, err error) { addr, err := cl.bitcoinChain.CreateOpeningAddress(swapParams, onchain.BitcoinCsv) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } outputs := []*glightning.Outputs{ { @@ -26,7 +26,7 @@ func (cl *ClightningClient) CreateOpeningTransaction(swapParams *swap.OpeningPar } prepRes, err := cl.glightning.PrepareTx(outputs, &glightning.FeeRate{Directive: glightning.Urgent}, nil) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } // Backwards compatibility layer. Since `v23.05`, `preparetx` returns a @@ -34,41 +34,30 @@ func (cl *ClightningClient) CreateOpeningTransaction(swapParams *swap.OpeningPar // conversion of the psbt (from v2 to v0). isV2, err := version.CompareVersionStrings(cl.Version(), "v23.05") if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } if isV2 { res, err := cl.glightning.SetPSBTVersion(prepRes.Psbt, 0) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } prepRes.Psbt = res.Psbt } fee, err = cl.bitcoinChain.GetFeeSatsFromTx(prepRes.Psbt, prepRes.UnsignedTx) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } _, vout, err = cl.bitcoinChain.GetVoutAndVerify(prepRes.UnsignedTx, swapParams) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } - cl.hexToIdMap[prepRes.UnsignedTx] = prepRes.TxId - return prepRes.UnsignedTx, addr, fee, vout, nil -} - -func (cl *ClightningClient) BroadcastOpeningTx(unpreparedTxHex string) (txId, txHex string, error error) { - var unpreparedTxId string - var ok bool - if unpreparedTxId, ok = cl.hexToIdMap[unpreparedTxHex]; !ok { - return "", "", errors.New("tx was not prepared not found in map") - } - delete(cl.hexToIdMap, unpreparedTxHex) - sendRes, err := cl.glightning.SendTx(unpreparedTxId) + sendRes, err := cl.glightning.SendTx(prepRes.TxId) if err != nil { - return "", "", errors.New(fmt.Sprintf("tx was not prepared %v", err)) + return "", "", "", 0, 0, errors.New(fmt.Sprintf("tx was not prepared %v", err)) } - return sendRes.TxId, sendRes.SignedTx, nil + return sendRes.SignedTx, addr, sendRes.TxId, fee, vout, nil } func (cl *ClightningClient) CreatePreimageSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams) (txId, txHex, address string, err error) { diff --git a/cmd/peerswap-plugin/main.go b/cmd/peerswap-plugin/main.go index 470d50bf..444fbe73 100644 --- a/cmd/peerswap-plugin/main.go +++ b/cmd/peerswap-plugin/main.go @@ -164,8 +164,8 @@ func run(ctx context.Context, lightningPlugin *clightning.ClightningClient) erro // liquid var liquidOnChainService *onchain.LiquidOnChain - var liquidTxWatcher *txwatcher.BlockchainRpcTxWatcher - var liquidRpcWallet *wallet.ElementsRpcWallet + var liquidTxWatcher swap.TxWatcher + var liquidRpcWallet wallet.Wallet var liquidCli *gelements.Elements var liquidEnabled bool @@ -202,7 +202,25 @@ func run(ctx context.Context, lightningPlugin *clightning.ClightningClient) erro return err } - liquidOnChainService = onchain.NewLiquidOnChain(liquidCli, liquidRpcWallet, liquidChain) + liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, liquidChain) + supportedAssets = append(supportedAssets, "lbtc") + log.Infof("Liquid swaps enabled") + } else if config.LWK != nil && config.LWK.Enabled() { + liquidEnabled = true + ec, err2 := electrum.NewClientTCP(ctx, config.LWK.GetElectrumEndpoint()) + 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) + if err != nil { + return err + } + liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, config.LWK.GetChain()) supportedAssets = append(supportedAssets, "lbtc") log.Infof("Liquid swaps enabled") } else { @@ -363,7 +381,7 @@ func run(ctx context.Context, lightningPlugin *clightning.ClightningClient) erro defer pollService.Stop() sp := swap.NewRequestedSwapsPrinter(requestedSwapStore) - lightningPlugin.SetupClients(liquidRpcWallet, swapService, pol, sp, liquidCli, bitcoinCli, bitcoinOnChainService, pollService) + lightningPlugin.SetupClients(liquidRpcWallet, swapService, pol, sp, bitcoinCli, bitcoinOnChainService, pollService) // We are ready to accept and handle requests. // FIXME: Once we reworked the recovery service (non-blocking) we want to diff --git a/cmd/peerswaplnd/peerswapd/main.go b/cmd/peerswaplnd/peerswapd/main.go index 4c822614..7f4a4e32 100644 --- a/cmd/peerswaplnd/peerswapd/main.go +++ b/cmd/peerswaplnd/peerswapd/main.go @@ -211,9 +211,58 @@ func run() error { if err != nil { return err } - liquidRpcWallet, err = wallet.NewRpcWallet(liquidCli, liquidConfig.RpcWallet) - if err != nil { - return err + if cfg.ElementsConfig.RpcUser != "" { + supportedAssets = append(supportedAssets, "lbtc") + log.Infof("Liquid swaps enabled") + liquidConfig := cfg.ElementsConfig + + // This call is blocking, waiting for elements to come alive and sync. + liquidCli, err = elements.NewClient( + liquidConfig.RpcUser, + liquidConfig.RpcPassword, + liquidConfig.RpcHost, + liquidConfig.RpcCookieFilePath, + liquidConfig.RpcPort, + ) + if err != nil { + return err + } + liquidRpcWallet, err = wallet.NewRpcWallet(liquidCli, liquidConfig.RpcWallet) + if err != nil { + return err + } + + // txwatcher + liquidTxWatcher = txwatcher.NewBlockchainRpcTxWatcher(ctx, txwatcher.NewElementsCli(liquidCli), onchain.LiquidConfs, onchain.LiquidCsv) + + // LiquidChain + liquidChain, err := getLiquidChain(liquidCli) + if err != nil { + return err + } + liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, liquidChain) + } else if cfg.LWKConfig != nil { + 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.GetElectrumEndpoint()), + ec, cfg.LWKConfig.GetWalletName(), cfg.LWKConfig.GetSignerName()) + if err != nil { + return err + } + cfg.LiquidEnabled = true + liquidTxWatcher, err = lwk.NewElectrumTxWatcher(ec) + if err != nil { + return err + } + liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, cfg.LWKConfig.GetChain()) + supportedAssets = append(supportedAssets, "lbtc") + } else { + return errors.New("Liquid swaps enabled but no config found") } // txwatcher @@ -224,7 +273,7 @@ func run() error { if err != nil { return err } - liquidOnChainService = onchain.NewLiquidOnChain(liquidCli, liquidRpcWallet, liquidChain) + liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, liquidChain) } else { log.Infof("Liquid swaps disabled") } diff --git a/go.mod b/go.mod index bb096b64..b32d0374 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.4.0 // indirect @@ -138,7 +138,12 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -require github.com/pelletier/go-toml/v2 v2.0.5 +require ( + github.com/pelletier/go-toml/v2 v2.0.5 + github.com/pkg/errors v0.9.1 + go.uber.org/zap v1.23.0 + golang.org/x/sys v0.1.0 +) require ( github.com/lightningnetwork/lnd/clock v1.1.0 // indirect diff --git a/go.sum b/go.sum index 3cea8da6..a330a6ca 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,8 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= @@ -665,8 +667,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yusukeshimizu/glightning v0.0.0-20240214001938-06d9e0562297 h1:j/R71Kv5Cvgds6OrqTPKLx2/sBL3WTt1xyMETp5qHf8= -github.com/yusukeshimizu/glightning v0.0.0-20240214001938-06d9e0562297/go.mod h1:YAdIeSyx8VEhDCtEaGOJLmWNpPaQ3x4vYSAj9Vrppdo= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= diff --git a/lnd/lnd_wallet.go b/lnd/lnd_wallet.go index 9f1bdec3..f5b00c28 100644 --- a/lnd/lnd_wallet.go +++ b/lnd/lnd_wallet.go @@ -16,10 +16,10 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/walletrpc" ) -func (l *Client) CreateOpeningTransaction(swapParams *swap.OpeningParams) (unpreparedTxHex, address string, fee uint64, vout uint32, err error) { +func (l *Client) CreateOpeningTransaction(swapParams *swap.OpeningParams) (rawTxHex, address, txId string, fee uint64, vout uint32, err error) { addr, err := l.bitcoinOnChain.CreateOpeningAddress(swapParams, onchain.BitcoinCsv) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } fundPsbtTemplate := &walletrpc.TxTemplate{ @@ -32,55 +32,51 @@ func (l *Client) CreateOpeningTransaction(swapParams *swap.OpeningParams) (unpre Fees: &walletrpc.FundPsbtRequest_TargetConf{TargetConf: 3}, }) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } unsignedPacket, err := psbt.NewFromRawBytes(bytes.NewReader(fundRes.FundedPsbt), false) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } bytesBuffer := new(bytes.Buffer) err = unsignedPacket.Serialize(bytesBuffer) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } finalizeRes, err := l.walletClient.FinalizePsbt(l.ctx, &walletrpc.FinalizePsbtRequest{ FundedPsbt: bytesBuffer.Bytes(), }) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } psbtString := base64.StdEncoding.EncodeToString(finalizeRes.SignedPsbt) - rawTxHex := hex.EncodeToString(finalizeRes.RawFinalTx) + rawTxHex = hex.EncodeToString(finalizeRes.RawFinalTx) fee, err = l.bitcoinOnChain.GetFeeSatsFromTx(psbtString, rawTxHex) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } _, vout, err = l.bitcoinOnChain.GetVoutAndVerify(rawTxHex, swapParams) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } - return rawTxHex, address, fee, vout, nil -} - -func (l *Client) BroadcastOpeningTx(unpreparedTxHex string) (txId, txHex string, error error) { - txBytes, err := hex.DecodeString(unpreparedTxHex) + txBytes, err := hex.DecodeString(rawTxHex) if err != nil { - return "", "", err + return "", "", "", 0, 0, err } openingTx := wire.NewMsgTx(2) err = openingTx.Deserialize(bytes.NewReader(txBytes)) if err != nil { - return "", "", err + return "", "", "", 0, 0, err } _, err = l.walletClient.PublishTransaction(l.ctx, &walletrpc.Transaction{TxHex: txBytes}) if err != nil { - return "", "", err + return "", "", "", 0, 0, err } - return openingTx.TxHash().String(), unpreparedTxHex, nil + return rawTxHex, addr, openingTx.TxHash().String(), fee, vout, nil } func (l *Client) CreatePreimageSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams) (string, string, string, error) { diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go new file mode 100644 index 00000000..61c64619 --- /dev/null +++ b/lwk/lwkwallet.go @@ -0,0 +1,202 @@ +package lwk + +import ( + "context" + "errors" + "log" + + "github.com/checksum0/go-electrum/electrum" + + "github.com/elementsproject/peerswap/swap" + "github.com/elementsproject/peerswap/wallet" + "github.com/vulpemventures/go-elements/network" + "github.com/vulpemventures/go-elements/psetv2" +) + +const ( + kiloByte = 1000 + minimumSatPerByte = 0.1 +) + +// LWKRpcWallet uses the elementsd rpc wallet +type LWKRpcWallet struct { + walletName string + signerName string + lwkClient *lwkclient + electrumClient *electrum.Client +} + +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") + } + if walletName == "" || signerName == "" { + return nil, errors.New("wallet name or signer name is empty") + } + rpcWallet := &LWKRpcWallet{ + walletName: walletName, + signerName: signerName, + lwkClient: lwkClient, + electrumClient: electrumClient, + } + err := rpcWallet.setupWallet() + if err != nil { + return nil, err + } + return rpcWallet, nil +} + +// setupWallet checks if the swap wallet is already loaded in elementsd, if not it loads/creates it +func (r *LWKRpcWallet) setupWallet() error { + ctx := context.Background() + res, err := r.lwkClient.walletDetails(ctx, &walletDetailsRequest{ + WalletName: r.walletName, + }) + if err != nil { + return err + } + signers := res.Signers + 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) + } + return nil +} + +// 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 uint64, err error) { + ctx := context.Background() + fundedTx, err := r.lwkClient.send(ctx, &sendRequest{ + Addressees: []*unvalidatedAddressee{ + { + Address: swapParams.OpeningAddress, + Satoshi: swapParams.Amount, + }, + }, + WalletName: r.walletName, + }) + if err != nil { + return "", "", 0, err + } + signed, err := r.lwkClient.sign(ctx, &signRequest{ + SignerName: r.signerName, + Pset: fundedTx.Pset, + }) + if err != nil { + return "", "", 0, err + } + broadcasted, err := r.lwkClient.broadcast(ctx, &broadcastRequest{ + WalletName: r.walletName, + Pset: signed.Pset, + }) + if err != nil { + return "", "", 0, err + } + p, err := psetv2.NewPsetFromBase64(signed.Pset) + if err != nil { + return "", "", 0, err + } + err = psetv2.FinalizeAll(p) + if err != nil { + return "", "", 0, err + } + tx, err := psetv2.Extract(p) + if err != nil { + return "", "", 0, err + } + txhex, err := tx.ToHex() + if err != nil { + return "", "", 0, err + } + return broadcasted.Txid, txhex, 0, nil +} + +// GetBalance returns the balance in sats +func (r *LWKRpcWallet) GetBalance() (uint64, error) { + ctx := context.Background() + balance, err := r.lwkClient.balance(ctx, &balanceRequest{ + WalletName: r.walletName, + }) + if err != nil { + return 0, err + } + return uint64(balance.Balance[network.Regtest.AssetID]), nil +} + +// GetAddress returns a new blech32 address +func (r *LWKRpcWallet) GetAddress() (string, error) { + ctx := context.Background() + address, err := r.lwkClient.address(ctx, &addressRequest{ + WalletName: r.walletName}) + if err != nil { + return "", err + } + return address.Address, nil +} + +// SendToAddress sends an amount to an address +func (r *LWKRpcWallet) SendToAddress(address string, amount uint64) (string, error) { + ctx := context.Background() + sendres, err := r.lwkClient.send(ctx, &sendRequest{ + WalletName: r.walletName, + Addressees: []*unvalidatedAddressee{ + { + Address: address, + Satoshi: amount, + }, + }, + }) + if err != nil { + return "", err + } + + signed, err := r.lwkClient.sign(ctx, &signRequest{ + SignerName: r.signerName, + Pset: sendres.Pset, + }) + if err != nil { + log.Fatal(err) + } + broadcastres, err := r.lwkClient.broadcast(ctx, &broadcastRequest{ + WalletName: r.walletName, + Pset: signed.Pset, + }) + if err != nil { + return "", err + } + return broadcastres.Txid, nil +} + +func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { + ctx := context.Background() + res, err := r.electrumClient.BroadcastTransaction(ctx, txHex) + if err != nil { + return "", err + } + return res, nil +} + +func (r *LWKRpcWallet) GetFee(txSize int64) (uint64, error) { + ctx := context.Background() + feeRes, err := r.electrumClient.GetFee(ctx, wallet.LiquidTargetBlocks) + + satPerByte := float64(feeRes) / float64(kiloByte) + if satPerByte < minimumSatPerByte { + satPerByte = minimumSatPerByte + } + if err != nil { + satPerByte = minimumSatPerByte + } + // assume largest witness + fee := satPerByte * float64(txSize) + + return uint64(fee), nil +} + +func (r *LWKRpcWallet) SetLabel(txID, address, label string) error { + // TODO: call set label + return nil +} diff --git a/onchain/liquid.go b/onchain/liquid.go index 8dbd457d..1d7b9ded 100644 --- a/onchain/liquid.go +++ b/onchain/liquid.go @@ -9,16 +9,16 @@ import ( "fmt" "github.com/elementsproject/peerswap/log" + "github.com/elementsproject/peerswap/wallet" "github.com/btcsuite/btcd/btcec/v2" "github.com/vulpemventures/go-elements/confidential" "github.com/vulpemventures/go-elements/pset" "github.com/btcsuite/btcd/txscript" - "github.com/elementsproject/glightning/gelements" "github.com/elementsproject/peerswap/lightning" "github.com/elementsproject/peerswap/swap" - "github.com/elementsproject/peerswap/wallet" + "github.com/vulpemventures/go-elements/address" address2 "github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/elementsutil" @@ -28,25 +28,23 @@ import ( ) const ( - LiquidCsv = 60 - LiquidConfs = 2 - LiquidTargetBlocks = 7 + LiquidCsv = 60 + LiquidConfs = 2 ) type LiquidOnChain struct { - elements *gelements.Elements liquidWallet wallet.Wallet network *network.Network asset []byte } -func NewLiquidOnChain(elements *gelements.Elements, wallet wallet.Wallet, network *network.Network) *LiquidOnChain { +func NewLiquidOnChain(wallet wallet.Wallet, network *network.Network) *LiquidOnChain { lbtc := append( []byte{0x01}, elementsutil.ReverseBytes(h2b(network.AssetID))..., ) - return &LiquidOnChain{elements: elements, liquidWallet: wallet, network: network, asset: lbtc} + return &LiquidOnChain{liquidWallet: wallet, network: network, asset: lbtc} } func (l *LiquidOnChain) GetCSVHeight() uint32 { @@ -57,58 +55,26 @@ func (l *LiquidOnChain) GetOnchainBalance() (uint64, error) { return l.liquidWallet.GetBalance() } -func (l *LiquidOnChain) CreateOpeningTransaction(swapParams *swap.OpeningParams) (unpreparedTxHex, newAddress string, fee uint64, vout uint32, err error) { +func (l *LiquidOnChain) CreateOpeningTransaction(swapParams *swap.OpeningParams) (txHex, address, txid string, fee uint64, vout uint32, err error) { redeemScript, err := ParamsToTxScript(swapParams, LiquidCsv) if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } scriptPubKey := []byte{0x00, 0x20} witnessProgram := sha256.Sum256(redeemScript) scriptPubKey = append(scriptPubKey, witnessProgram[:]...) redeemPayment, _ := payment.FromScript(scriptPubKey, l.network, swapParams.BlindingKey.PubKey()) - sats, _ := elementsutil.ValueToBytes(swapParams.Amount) - blindedScriptAddr, err := redeemPayment.ConfidentialWitnessScriptHash() if err != nil { - return "", "", 0, 0, err + return "", "", "", 0, 0, err } swapParams.OpeningAddress = blindedScriptAddr - outputscript, err := address.ToOutputScript(blindedScriptAddr) - if err != nil { - return "", "", 0, 0, err - } - - output := transaction.NewTxOutput(l.asset, sats, outputscript) - output.Nonce = swapParams.BlindingKey.PubKey().SerializeCompressed() - - tx := transaction.NewTx(2) - tx.Outputs = append(tx.Outputs, output) - - unpreparedTxHex, fee, err = l.liquidWallet.CreateFundedTransaction(tx) - if err != nil { - return "", "", 0, 0, err - } - - vout, err = l.VoutFromTxHex(unpreparedTxHex, redeemScript) - if err != nil { - return "", "", 0, 0, err - } - - return unpreparedTxHex, blindedScriptAddr, fee, vout, nil -} - -func (l *LiquidOnChain) BroadcastOpeningTx(unpreparedTxHex string) (string, string, error) { - txHex, err := l.liquidWallet.FinalizeFundedTransaction(unpreparedTxHex) - if err != nil { - return "", "", err - } - - txId, err := l.elements.SendRawTx(txHex) + txId, txHex, fee, err := l.liquidWallet.CreateAndBroadcastTransaction(swapParams, l.asset) if err != nil { - return "", "", err + return "", "", "", 0, 0, err } - return txId, txHex, nil + return txHex, blindedScriptAddr, txId, fee, vout, nil } func (l *LiquidOnChain) CreatePreimageSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams) (string, string, string, error) { @@ -140,11 +106,7 @@ func (l *LiquidOnChain) CreatePreimageSpendingTransaction(swapParams *swap.Openi return "", "", "", err } - txHex, err = tx.ToHex() - if err != nil { - return "", "", "", err - } - txId, err := l.elements.SendRawTx(txHex) + txId, err := l.liquidWallet.SendRawTx(txHex) if err != nil { return "", "", "", err } @@ -166,7 +128,7 @@ func (l *LiquidOnChain) CreateCsvSpendingTransaction(swapParams *swap.OpeningPar if err != nil { return "", "", "", err } - txId, err = l.elements.SendRawTx(txHex) + txId, err = l.liquidWallet.SendRawTx(txHex) if err != nil { return "", "", "", err } @@ -178,7 +140,7 @@ func (l *LiquidOnChain) CreateCoopSpendingTransaction(swapParams *swap.OpeningPa if err != nil { return "", "", "", err } - refundFee, err := l.getFee(l.getCoopClaimTxSize()) + refundFee, err := l.liquidWallet.GetFee(int64(l.getCoopClaimTxSize())) if err != nil { return "", "", "", err } @@ -209,7 +171,7 @@ func (l *LiquidOnChain) CreateCoopSpendingTransaction(swapParams *swap.OpeningPa if err != nil { return "", "", "", err } - txId, err = l.elements.SendRawTx(txHex) + txId, err = l.liquidWallet.SendRawTx(txHex) if err != nil { return "", "", "", err } @@ -228,7 +190,7 @@ func (l *LiquidOnChain) Name() string { } func (l *LiquidOnChain) SetLabel(txID, address, label string) error { - return l.elements.SetLabel(address, label) + return l.liquidWallet.SetLabel(txID, address, label) } func (l *LiquidOnChain) AddBlindingRandomFactors(claimParams *swap.ClaimParams) (err error) { @@ -300,7 +262,7 @@ func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmoun feeAmountPlaceholder := uint64(500) fee := preparedFee if preparedFee == 0 { - fee, err = l.getFee(l.getClaimTxSize()) + fee, err = l.liquidWallet.GetFee(int64(l.getClaimTxSize())) if err != nil { fee = feeAmountPlaceholder } @@ -562,34 +524,15 @@ func (l *LiquidOnChain) Blech32ToScript(blech32Addr string) ([]byte, error) { return blechPayment.WitnessScript, nil } -func (l *LiquidOnChain) getFee(txSize int) (uint64, error) { - feeRes, err := l.elements.EstimateFee(LiquidTargetBlocks, "ECONOMICAL") - if err != nil { - return 0, err - } - satPerByte := float64(feeRes.SatPerKb()) / float64(1000) - if satPerByte < 0.1 { - satPerByte = 0.1 - } - if len(feeRes.Errors) > 0 { - //todo sane default sat per byte - satPerByte = 0.1 - } - // assume largest witness - fee := satPerByte * float64(txSize) - - return uint64(fee), nil -} - func (l *LiquidOnChain) GetRefundFee() (uint64, error) { - return l.getFee(l.getClaimTxSize()) + return l.liquidWallet.GetFee(int64(l.getClaimTxSize())) } // GetFlatSwapOutFee returns an estimate of the fee for the opening transaction. // The size is a calculate 2672 bytes for 3 inputs and 3 ouputs of which 2 are // blinded. An additional safety margin is added for a total of 3000 bytes. func (l *LiquidOnChain) GetFlatSwapOutFee() (uint64, error) { - return l.getFee(3000) + return l.liquidWallet.GetFee(3000) } func (l *LiquidOnChain) GetAsset() string { diff --git a/onchain/liquid_test.go b/onchain/liquid_test.go index 6fbde8cc..4070acf4 100644 --- a/onchain/liquid_test.go +++ b/onchain/liquid_test.go @@ -8,7 +8,7 @@ import ( ) func Test_ScriptAddress(t *testing.T) { - liquidOnCain := NewLiquidOnChain(nil, nil, &network.Testnet) + liquidOnCain := NewLiquidOnChain(nil, &network.Testnet) swapParams := &swap.OpeningParams{ TakerPubkey: "02752e1beeeeb6472959117a0aa5d172900680c033ddf86b1a8318311e2b10223f", MakerPubkey: "02c30ff537639962f493d326a77f1c6cb591ee3d21ca8d89194bb69cb288f497e8", diff --git a/peerswaprpc/server.go b/peerswaprpc/server.go index 2fd8fe89..9756d6e8 100644 --- a/peerswaprpc/server.go +++ b/peerswaprpc/server.go @@ -27,8 +27,7 @@ type PeerswapServer struct { pollService *poll.Service policy *policy.Policy - Gelements *gelements.Elements - lnd lnrpc.LightningClient + lnd lnrpc.LightningClient sigchan chan os.Signal @@ -79,7 +78,7 @@ func (p *PeerswapServer) Stop(ctx context.Context, empty *Empty) (*Empty, error) } func NewPeerswapServer(liquidWallet wallet.Wallet, swaps *swap.SwapService, requestedSwaps *swap.RequestedSwapsPrinter, pollService *poll.Service, policy *policy.Policy, gelements *gelements.Elements, lnd lnrpc.LightningClient, sigchan chan os.Signal) *PeerswapServer { - return &PeerswapServer{liquidWallet: liquidWallet, swaps: swaps, requestedSwaps: requestedSwaps, pollService: pollService, policy: policy, Gelements: gelements, lnd: lnd, sigchan: sigchan} + return &PeerswapServer{liquidWallet: liquidWallet, swaps: swaps, requestedSwaps: requestedSwaps, pollService: pollService, policy: policy, lnd: lnd, sigchan: sigchan} } func (p *PeerswapServer) SwapOut(ctx context.Context, request *SwapOutRequest) (*SwapResponse, error) { @@ -115,9 +114,6 @@ func (p *PeerswapServer) SwapOut(ctx context.Context, request *SwapOutRequest) ( if !p.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - if p.Gelements == nil { - return nil, errors.New("peerswap was not started with liquid node config") - } } else if strings.Compare(request.Asset, "btc") == 0 { if !p.swaps.BitcoinEnabled { @@ -224,9 +220,7 @@ func (p *PeerswapServer) SwapIn(ctx context.Context, request *SwapInRequest) (*S if !p.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - if p.Gelements == nil { - return nil, errors.New("peerswap was not started with liquid node config") - } + liquidBalance, err := p.liquidWallet.GetBalance() if err != nil { return nil, err @@ -470,9 +464,7 @@ func (p *PeerswapServer) LiquidGetAddress(ctx context.Context, request *GetAddre if !p.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - if p.Gelements == nil { - return nil, errors.New("peerswap was not started with liquid node config") - } + res, err := p.liquidWallet.GetAddress() if err != nil { return nil, err @@ -484,9 +476,7 @@ func (p *PeerswapServer) LiquidGetBalance(ctx context.Context, request *GetBalan if !p.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - if p.Gelements == nil { - return nil, errors.New("peerswap was not started with liquid node config") - } + res, err := p.liquidWallet.GetBalance() if err != nil { return nil, err @@ -499,9 +489,7 @@ func (p *PeerswapServer) LiquidSendToAddress(ctx context.Context, request *SendT if !p.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - if p.Gelements == nil { - return nil, errors.New("peerswap was not started with liquid node config") - } + if request.Address == "" { return nil, errors.New("address not set") } diff --git a/swap/actions.go b/swap/actions.go index bf458e5b..a043ea96 100644 --- a/swap/actions.go +++ b/swap/actions.go @@ -239,7 +239,7 @@ func (c *CreateAndBroadcastOpeningTransaction) Execute(services *SwapServices, s } // Create the opening transaction - txHex, address, _, vout, err := wallet.CreateOpeningTransaction(&OpeningParams{ + txHex, address, txId, _, vout, err := wallet.CreateOpeningTransaction(&OpeningParams{ TakerPubkey: swap.GetTakerPubkey(), MakerPubkey: swap.GetMakerPubkey(), ClaimPaymentHash: preimage.Hash().String(), @@ -250,11 +250,6 @@ func (c *CreateAndBroadcastOpeningTransaction) Execute(services *SwapServices, s return swap.HandleError(err) } - txId, txHex, err := wallet.BroadcastOpeningTx(txHex) - if err != nil { - // todo: idempotent states - return swap.HandleError(err) - } err = wallet.SetLabel(txId, address, labels.Opening(swap.GetId().Short())) if err != nil { log.Infof("Error labeling transaction. txid: %s, label: %s, error: %v", diff --git a/swap/services.go b/swap/services.go index a16b3de9..6e70fa39 100644 --- a/swap/services.go +++ b/swap/services.go @@ -60,6 +60,7 @@ type TxWatcher interface { AddConfirmationCallback(func(swapId string, txHex string, err error) error) AddCsvCallback(func(swapId string) error) GetBlockHeight() (uint32, error) + StartWatchingTxs() error } type Validator interface { @@ -69,12 +70,11 @@ type Validator interface { } type Wallet interface { - CreateOpeningTransaction(swapParams *OpeningParams) (unpreparedTxHex, address string, fee uint64, vout uint32, err error) - BroadcastOpeningTx(unpreparedTxHex string) (txId, txHex string, error error) + SetLabel(txID, address, label string) error + CreateOpeningTransaction(swapParams *OpeningParams) (unpreparedTxHex, address, txid string, fee uint64, vout uint32, err error) CreatePreimageSpendingTransaction(swapParams *OpeningParams, claimParams *ClaimParams) (txId, txHex, address string, err error) CreateCsvSpendingTransaction(swapParams *OpeningParams, claimParams *ClaimParams) (txId, txHex, address string, error error) CreateCoopSpendingTransaction(swapParams *OpeningParams, claimParams *ClaimParams, takerSigner Signer) (txId, txHex, address string, error error) - SetLabel(txID, address, label string) error GetOutputScript(params *OpeningParams) ([]byte, error) NewAddress() (string, error) GetRefundFee() (uint64, error) diff --git a/swap/swap_out_sender_test.go b/swap/swap_out_sender_test.go index 746e637d..ca8a97ab 100644 --- a/swap/swap_out_sender_test.go +++ b/swap/swap_out_sender_test.go @@ -478,8 +478,8 @@ func (d *dummyChain) GetFlatSwapOutFee() (uint64, error) { return 100, nil } -func (d *dummyChain) CreateOpeningTransaction(swapParams *OpeningParams) (unpreparedTxHex, address string, fee uint64, vout uint32, err error) { - return "txhex", "addr", 0, 0, nil +func (d *dummyChain) CreateOpeningTransaction(swapParams *OpeningParams) (unpreparedTxHex, address, txid string, fee uint64, vout uint32, err error) { + return "txhex", "address", getRandom32ByteHexString(), 0, 0, nil } func (d *dummyChain) AddCsvCallback(f func(swapId string) error) { @@ -490,10 +490,6 @@ func (d *dummyChain) NewAddress() (string, error) { return "addr", nil } -func (d *dummyChain) BroadcastOpeningTx(unpreparedTxHex string) (txId, txHex string, error error) { - return getRandom32ByteHexString(), "txhex", nil -} - func (d *dummyChain) AddConfirmationCallback(f func(swapId string, txHex string, err error) error) { d.txConfirmedFunc = f } diff --git a/wallet/elementsrpcwallet.go b/wallet/elementsrpcwallet.go index ed38abc0..06bf11ee 100644 --- a/wallet/elementsrpcwallet.go +++ b/wallet/elementsrpcwallet.go @@ -6,6 +6,9 @@ import ( "strings" "github.com/elementsproject/glightning/gelements" + "github.com/elementsproject/peerswap/swap" + "github.com/vulpemventures/go-elements/address" + "github.com/vulpemventures/go-elements/elementsutil" "github.com/vulpemventures/go-elements/transaction" ) @@ -26,6 +29,8 @@ type RpcClient interface { BlindRawTransaction(txHex string) (string, error) SignRawTransactionWithWallet(txHex string) (gelements.SignRawTransactionWithWalletRes, error) SendRawTx(txHex string) (string, error) + EstimateFee(blocks uint32, mode string) (*gelements.FeeResponse, error) + SetLabel(address, label string) error } // ElementsRpcWallet uses the elementsd rpc wallet @@ -62,26 +67,40 @@ func (r *ElementsRpcWallet) FinalizeTransaction(rawTx string) (string, error) { return finalized.Hex, nil } -// CreateFundedTransaction takes a tx with outputs and adds inputs in order to spend the tx -func (r *ElementsRpcWallet) CreateFundedTransaction(preparedTx *transaction.Transaction) (rawTx string, fee uint64, err error) { - txHex, err := preparedTx.ToHex() +// CreateAndBroadcastTransaction takes a tx with outputs and adds inputs in order to spend the tx +func (r *ElementsRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningParams, + asset []byte) (txid, rawTx string, fee uint64, err error) { + outputscript, err := address.ToOutputScript(swapParams.OpeningAddress) if err != nil { - return "", 0, err + return "", "", 0, err } - fundedTx, err := r.rpcClient.FundRawTx(txHex) + sats, err := elementsutil.ValueToBytes(swapParams.Amount) if err != nil { - return "", 0, err + return "", "", 0, err } - return fundedTx.TxString, gelements.ConvertBtc(fundedTx.Fee), nil -} + output := transaction.NewTxOutput(asset, sats, outputscript) + output.Nonce = swapParams.BlindingKey.PubKey().SerializeCompressed() + + tx := transaction.NewTx(2) + tx.Outputs = append(tx.Outputs, output) -// FinalizeAndBroadcastFundedTransaction finalizes a tx and broadcasts it -func (r *ElementsRpcWallet) FinalizeFundedTransaction(rawTx string) (txId string, err error) { - finalized, err := r.FinalizeTransaction(rawTx) + txHex, err := tx.ToHex() if err != nil { - return "", err + return "", "", 0, err + } + fundedTx, err := r.rpcClient.FundRawTx(txHex) + if err != nil { + return "", "", 0, err + } + finalized, err := r.FinalizeTransaction(fundedTx.TxString) + if err != nil { + return "", "", 0, err } - return finalized, nil + txid, err = r.SendRawTx(finalized) + if err != nil { + return "", "", 0, err + } + return txid, finalized, gelements.ConvertBtc(fundedTx.Fee), nil } // setupWallet checks if the swap wallet is already loaded in elementsd, if not it loads/creates it @@ -140,6 +159,33 @@ func (r *ElementsRpcWallet) SendToAddress(address string, amount uint64) (string return txId, nil } +func (r *ElementsRpcWallet) SendRawTx(txHex string) (string, error) { + return r.rpcClient.SendRawTx(txHex) +} + +func (r *ElementsRpcWallet) GetFee(txSize int64) (uint64, error) { + feeRes, err := r.rpcClient.EstimateFee(LiquidTargetBlocks, "ECONOMICAL") + if err != nil { + return 0, err + } + satPerByte := float64(feeRes.SatPerKb()) / float64(1000) + if satPerByte < 0.1 { + satPerByte = 0.1 + } + if len(feeRes.Errors) > 0 { + //todo sane default sat per byte + satPerByte = 0.1 + } + // assume largest witness + fee := satPerByte * float64(txSize) + + return uint64(fee), nil +} + +func (r *ElementsRpcWallet) SetLabel(txID, address, label string) error { + return r.rpcClient.SetLabel(address, label) +} + // satsToAmountString returns the amount in btc from sats func satsToAmountString(sats uint64) string { bitcoinAmt := float64(sats) / 100000000 diff --git a/wallet/wallet.go b/wallet/wallet.go index e9308471..b0a016b3 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -2,17 +2,24 @@ package wallet import ( "errors" - "github.com/vulpemventures/go-elements/transaction" + + "github.com/elementsproject/peerswap/swap" ) var ( NotEnoughBalanceError = errors.New("Not enough balance on utxos") ) +const ( + LiquidTargetBlocks = 7 +) + type Wallet interface { GetAddress() (string, error) SendToAddress(string, uint64) (string, error) GetBalance() (uint64, error) - CreateFundedTransaction(preparedTx *transaction.Transaction) (rawTx string, fee uint64, err error) - FinalizeFundedTransaction(unpreparedTx string) (preparedTxHex string, err error) + CreateAndBroadcastTransaction(swapParams *swap.OpeningParams, asset []byte) (txid, rawTx string, fee uint64, err error) + SendRawTx(rawTx string) (txid string, err error) + GetFee(txSize int64) (uint64, error) + SetLabel(txID, address, label string) error } From c1dd0084466eef9558daf1b58ab900dea27a90c6 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 27 Mar 2024 08:52:22 +0900 Subject: [PATCH 03/29] lwk: lwk package lwkclient is a library for interacting directly with lwk server via json rpc. LWKRpcWallet acts as an onchain wallet for peerswap It uses lwk and electrs. - Implement API structure with request handling, including retry logic and response body draining. - Provide configuration options for HTTP client with retry logic - Utilize JSON-RPC 2.0 for request and response handling. - Ensure proper error handling and logging. - Create lwkclient with methods for address generation, sending transactions, signing, broadcasting, balance querying, and wallet details retrieval. - Implement LWKRpcWallet with necessary wallet operations - Ensure wallet setup and transaction creation/broadcasting - Add utility functions for balance retrieval and fee estimation. --- lwk/api.go | 66 +++++++++++++++ lwk/client.go | 214 +++++++++++++++++++++++++++++++++++++++++++++++ lwk/lwkwallet.go | 39 +++++---- lwk/option.go | 102 ++++++++++++++++++++++ shell.nix | 1 + 5 files changed, 408 insertions(+), 14 deletions(-) create mode 100644 lwk/api.go create mode 100644 lwk/client.go create mode 100644 lwk/option.go diff --git a/lwk/api.go b/lwk/api.go new file mode 100644 index 00000000..ad612314 --- /dev/null +++ b/lwk/api.go @@ -0,0 +1,66 @@ +package lwk + +import ( + "io" + "net/http" + "sync/atomic" + + "github.com/elementsproject/glightning/jrpc2" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type api struct { + BaseURL string + logger *zap.Logger + httpClient *retryablehttp.Client + interceptors []InterceptorFunc + requestCounter int64 +} + +func NewAPI(baseURL string) *api { + return &api{ + BaseURL: baseURL, + logger: zap.NewNop(), + httpClient: defaultHttpClient(), + } +} + +// for now, use a counter as the id for requests +func (a *api) nextID() *jrpc2.Id { + val := atomic.AddInt64(&a.requestCounter, 1) + return jrpc2.NewIdAsInt(val) +} + +func (a *api) do(req *http.Request) (*http.Response, error) { + e := a.call + is := a.interceptors + for i := len(is) - 1; i >= 0; i-- { + e = is[i](e) + } + return e(req) +} + +func (a *api) call(req *http.Request) (*http.Response, error) { + req.Header.Set("Content-Type", "application/json") + rReq, err := retryablehttp.FromRequest(req) + if err != nil { + return nil, errors.Wrap(err, "failed to create api request") + } + res, err := a.httpClient.Do(rReq) + if err != nil { + return nil, errors.Wrap(err, "failed to call api request") + } + return res, nil +} + +func (a *api) drain(res *http.Response) { + defer func() { + _ = res.Body.Close() + }() + _, err := io.Copy(io.Discard, res.Body) + if err != nil { + a.logger.Warn("failed to drain response body") + } +} diff --git a/lwk/client.go b/lwk/client.go new file mode 100644 index 00000000..d9fa0663 --- /dev/null +++ b/lwk/client.go @@ -0,0 +1,214 @@ +package lwk + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/elementsproject/glightning/jrpc2" +) + +type lwkclient struct { + api api +} + +func NewLwk(endpoint string) *lwkclient { + return &lwkclient{ + api: *NewAPI(endpoint), + } +} + +func (l *lwkclient) request(ctx context.Context, m jrpc2.Method, resp interface{}) error { + id := l.api.nextID() + mr := &jrpc2.Request{Id: id, Method: m} + jbytes, err := json.Marshal(mr) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, l.api.BaseURL, bytes.NewBuffer(jbytes)) + if err != nil { + return err + } + rezp, err := l.api.do(req) + if err != nil { + return err + } + defer l.api.drain(rezp) + switch rezp.StatusCode { + case http.StatusUnauthorized: + return errors.New("authorization failed: Incorrect user or password") + case http.StatusBadRequest, http.StatusNotFound, http.StatusInternalServerError: + // do nothing + default: + if rezp.StatusCode > http.StatusBadRequest { + return errors.New(fmt.Sprintf("server returned HTTP error %d", rezp.StatusCode)) + } else if rezp.ContentLength == 0 { + return errors.New("no response from server") + } + } + + var rawResp jrpc2.RawResponse + + decoder := json.NewDecoder(rezp.Body) + err = decoder.Decode(&rawResp) + if err != nil { + return err + } + + if rawResp.Error != nil { + return rawResp.Error + } + return json.Unmarshal(rawResp.Raw, resp) +} + +type addressRequest struct { + Index *uint32 `json:"index,omitempty"` + WalletName string `json:"name"` + Signer *string `json:"signer,omitempty"` +} + +func (r *addressRequest) Name() string { + return "address" +} + +type addressResponse struct { + Address string `json:"address"` + Index *uint32 `json:"index,omitempty"` +} + +func (l *lwkclient) address(ctx context.Context, req *addressRequest) (*addressResponse, error) { + var resp addressResponse + err := l.request(ctx, req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type unvalidatedAddressee struct { + Address string `json:"address"` + Asset string `json:"asset"` + Satoshi uint64 `json:"satoshi"` +} + +type sendRequest struct { + Addressees []*unvalidatedAddressee `json:"addressees"` + // Optional fee rate in sat/vb + FeeRate *float64 `json:"fee_rate,omitempty"` + WalletName string `json:"name"` +} + +type sendResponse struct { + Pset string `json:"pset"` +} + +func (s *sendRequest) Name() string { + return "send_many" +} + +func (l *lwkclient) send(ctx context.Context, s *sendRequest) (*sendResponse, error) { + var resp sendResponse + err := l.request(ctx, s, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type signRequest struct { + SignerName string `json:"name"` + Pset string `json:"pset"` +} + +type signResponse struct { + Pset string `json:"pset"` +} + +func (s *signRequest) Name() string { + return "sign" +} + +func (l *lwkclient) sign(ctx context.Context, s *signRequest) (*signResponse, error) { + var resp signResponse + err := l.request(ctx, s, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type broadcastRequest struct { + DryRun bool `json:"dry_run"` + WalletName string `json:"name"` + Pset string `json:"pset"` +} + +type broadcastResponse struct { + Txid string `json:"txid"` +} + +func (b *broadcastRequest) Name() string { + return "broadcast" +} + +func (l *lwkclient) broadcast(ctx context.Context, b *broadcastRequest) (*broadcastResponse, error) { + var resp broadcastResponse + err := l.request(ctx, b, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type balanceRequest struct { + WalletName string `json:"name"` + WithTickers bool `json:"with_tickers"` +} + +func (b *balanceRequest) Name() string { + return "balance" +} + +type balanceResponse struct { + Balance map[string]int64 `json:"balance"` +} + +func (l *lwkclient) balance(ctx context.Context, b *balanceRequest) (*balanceResponse, error) { + var resp balanceResponse + err := l.request(ctx, b, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type walletDetailsRequest struct { + WalletName string `json:"name"` +} + +func (w *walletDetailsRequest) Name() string { + return "wallet_details" +} + +type signerDetails struct { + Fingerprint string `json:"fingerprint"` + Name string `json:"name"` +} + +type walletDetailsResponse struct { + Signers []signerDetails `json:"signers"` + Type string `json:"type"` + Warnings string `json:"warnings"` +} + +func (l *lwkclient) walletDetails(ctx context.Context, w *walletDetailsRequest) (*walletDetailsResponse, error) { + var resp walletDetailsResponse + err := l.request(ctx, w, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 61c64619..e9c57afe 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -4,6 +4,7 @@ import ( "context" "errors" "log" + "math" "github.com/checksum0/go-electrum/electrum" @@ -13,9 +14,14 @@ import ( "github.com/vulpemventures/go-elements/psetv2" ) +// Satoshi represents a satoshi value. +type Satoshi = uint64 + +// SatPerKVByte represents a fee rate in sat/kb. +type SatPerKVByte = uint64 + const ( - kiloByte = 1000 - minimumSatPerByte = 0.1 + minimumSatPerByte = 200 ) // LWKRpcWallet uses the elementsd rpc wallet @@ -67,8 +73,9 @@ func (r *LWKRpcWallet) setupWallet() error { // 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 uint64, err error) { + asset []byte) (txid, rawTx string, fee SatPerKVByte, err error) { ctx := context.Background() + feerate := float64(r.getFeePerKb(ctx)) fundedTx, err := r.lwkClient.send(ctx, &sendRequest{ Addressees: []*unvalidatedAddressee{ { @@ -77,6 +84,7 @@ func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningPar }, }, WalletName: r.walletName, + FeeRate: &feerate, }) if err != nil { return "", "", 0, err @@ -111,11 +119,11 @@ func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningPar if err != nil { return "", "", 0, err } - return broadcasted.Txid, txhex, 0, nil + return broadcasted.Txid, txhex, SatPerKVByte(feerate), nil } // GetBalance returns the balance in sats -func (r *LWKRpcWallet) GetBalance() (uint64, error) { +func (r *LWKRpcWallet) GetBalance() (Satoshi, error) { ctx := context.Background() balance, err := r.lwkClient.balance(ctx, &balanceRequest{ WalletName: r.walletName, @@ -138,7 +146,7 @@ func (r *LWKRpcWallet) GetAddress() (string, error) { } // SendToAddress sends an amount to an address -func (r *LWKRpcWallet) SendToAddress(address string, amount uint64) (string, error) { +func (r *LWKRpcWallet) SendToAddress(address string, amount Satoshi) (string, error) { ctx := context.Background() sendres, err := r.lwkClient.send(ctx, &sendRequest{ WalletName: r.walletName, @@ -179,21 +187,24 @@ func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { return res, nil } -func (r *LWKRpcWallet) GetFee(txSize int64) (uint64, error) { - ctx := context.Background() - feeRes, err := r.electrumClient.GetFee(ctx, wallet.LiquidTargetBlocks) - - satPerByte := float64(feeRes) / float64(kiloByte) +func (r *LWKRpcWallet) getFeePerKb(ctx context.Context) SatPerKVByte { + feeBTCPerKb, err := r.electrumClient.GetFee(ctx, wallet.LiquidTargetBlocks) + // convert to sat per byte + satPerByte := uint64(float64(feeBTCPerKb) * math.Pow10(int(8))) if satPerByte < minimumSatPerByte { satPerByte = minimumSatPerByte } if err != nil { satPerByte = minimumSatPerByte } - // assume largest witness - fee := satPerByte * float64(txSize) + return satPerByte +} - return uint64(fee), nil +func (r *LWKRpcWallet) GetFee(txSize int64) (Satoshi, error) { + ctx := context.Background() + // assume largest witness + fee := r.getFeePerKb(ctx) * uint64(txSize) + return fee, nil } func (r *LWKRpcWallet) SetLabel(txID, address, label string) error { diff --git a/lwk/option.go b/lwk/option.go new file mode 100644 index 00000000..94f7a66a --- /dev/null +++ b/lwk/option.go @@ -0,0 +1,102 @@ +package lwk + +import ( + "context" + "net" + "net/http" + "time" + + "github.com/hashicorp/go-retryablehttp" + "go.uber.org/zap" +) + +type Option struct { + ConnTimeout time.Duration + ReadTimeOut time.Duration + RetryWaitMin time.Duration + RetryWaitMax time.Duration + RetryMax int +} +type LogWrapper struct { + logger *zap.Logger +} + +func (a *api) WithLogger(logger *zap.Logger) *api { + a.logger = logger + a.httpClient.Logger = &LogWrapper{logger: logger} + return a +} + +func (a *api) WithOption(option *Option) *api { + setHttpClientOption(a.httpClient, option) + return a +} + +type ( + InterceptorFunc func(RequestHandlerFunc) RequestHandlerFunc + RequestHandlerFunc func(*http.Request) (*http.Response, error) +) + +func (a *api) WithInterceptors(is ...InterceptorFunc) *api { + a.interceptors = is + return a +} + +func defaultOption() *Option { + return &Option{ + ConnTimeout: 10 * time.Second, + ReadTimeOut: 10 * time.Second, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 3 * time.Second, + RetryMax: 1, + } +} + +func defaultHttpClient() *retryablehttp.Client { + c := retryablehttp.NewClient() + c.HTTPClient = &http.Client{} + c.Backoff = retryablehttp.LinearJitterBackoff // use jitter + c.ErrorHandler = nil // not used + c.Logger = nil // disable default logger + c.CheckRetry = checkRetry + setHttpClientOption(c, defaultOption()) + return c +} + +func checkRetry(ctx context.Context, res *http.Response, err error) (bool, error) { + doRetry, err := retryablehttp.ErrorPropagatedRetryPolicy(ctx, res, err) + if doRetry && res != nil { + // if a response is received, retry is terminated + return false, nil + } + return doRetry, err +} + +func setHttpClientOption(c *retryablehttp.Client, o *Option) { + if o.ConnTimeout > 0 { + c.HTTPClient.Transport = transportWithTimeout(o.ConnTimeout) + } + if o.ReadTimeOut > 0 { + c.HTTPClient.Timeout = o.ReadTimeOut + } + if o.RetryWaitMin > 0 { + c.RetryWaitMin = o.RetryWaitMin + } + if o.RetryWaitMax > 0 { + c.RetryWaitMax = o.RetryWaitMax + } + c.RetryMax = o.RetryMax +} + +func transportWithTimeout(d time.Duration) *http.Transport { + // clone default transport + dtp, ok := http.DefaultTransport.(*http.Transport) + if !ok { + return nil + } + tp := dtp.Clone() + // set timeout + dial := &net.Dialer{Timeout: d, KeepAlive: 30 * time.Second} + tp.DialContext = (dial).DialContext + return tp +} diff --git a/shell.nix b/shell.nix index f00f1e6a..4d2e7333 100644 --- a/shell.nix +++ b/shell.nix @@ -23,4 +23,5 @@ stdenv.mkDerivation rec { . ./contrib/startup_regtest.sh setup_alias ''; + hardeningDisable = [ "all" ]; } From baaf69dcb1d4c6bc59c7323fd582b6126201bb04 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 27 Mar 2024 08:54:03 +0900 Subject: [PATCH 04/29] lwk: txwatcher tx watcher is a function that monitors onchain status and notifies opening tx and closing tx. Introduce ElectrumRPC interface and electrumTxWatcher for Electrum-based transaction watching. Also, add generated mock for electrumRPC interface. Mock generation is performed by go.uber.org/mock/mockgen. --- Makefile | 10 ++- electrum/block_subscriber.go | 65 +++++++++++++++ electrum/electrum.go | 13 +++ electrum/tx_observer.go | 149 ++++++++++++++++++++++++++++++++++ lnd/txwatcher.go | 3 + lwk/electrumtxwatcher.go | 110 +++++++++++++++++++++++++ lwk/electrumtxwatcher_test.go | 122 ++++++++++++++++++++++++++++ lwk/mock/electrumRPC.go | 86 ++++++++++++++++++++ swap/swap_out_sender_test.go | 4 + 9 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 electrum/block_subscriber.go create mode 100644 electrum/electrum.go create mode 100644 electrum/tx_observer.go create mode 100644 lwk/electrumtxwatcher.go create mode 100644 lwk/electrumtxwatcher_test.go create mode 100644 lwk/mock/electrumRPC.go diff --git a/Makefile b/Makefile index fa589ffa..46bc0e9c 100644 --- a/Makefile +++ b/Makefile @@ -185,12 +185,12 @@ TOOLS_DIR := ${CURDIR}/tools tool: ## Install an individual dependent tool. @cd $(TOOLS_DIR) && env GOBIN=$(TOOLS_DIR)/bin go install -trimpath github.com/golangci/golangci-lint/cmd/golangci-lint + @cd $(TOOLS_DIR) && env GOBIN=$(TOOLS_DIR)/bin go install -trimpath go.uber.org/mock/mockgen@latest .PHONY: clean clean: ## clean project directory. env GOBIN=${TOOLS_DIR}/bin && @rm -rf ${GOBIN} $(TOOLS_DIR)/bin - .PHONY: lint lint: lint/golangci-lint lint: ## Lint source. @@ -203,3 +203,11 @@ lint/golangci-lint: ## Lint source with golangci-lint. .PHONY: lint/fix lint/fix: ## Lint and fix source. @${MAKE} lint/golangci-lint args='--fix' + + +.PHONY: mockgen +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 diff --git a/electrum/block_subscriber.go b/electrum/block_subscriber.go new file mode 100644 index 00000000..b810dced --- /dev/null +++ b/electrum/block_subscriber.go @@ -0,0 +1,65 @@ +package electrum + +import ( + "context" + + "github.com/elementsproject/peerswap/log" +) + +type BlocKHeight uint32 + +func (b BlocKHeight) Confirmed() bool { + return b > 0 +} + +func (b BlocKHeight) Height() uint32 { + return uint32(b) +} + +type BlockHeaderSubscriber interface { + Register(tx TXObserver) + Deregister(o TXObserver) + Update(ctx context.Context, blockHeight BlocKHeight) error +} + +type liquidBlockHeaderSubscriber struct { + txObservers []TXObserver +} + +func NewLiquidBlockHeaderSubscriber() *liquidBlockHeaderSubscriber { + return &liquidBlockHeaderSubscriber{} +} + +var _ BlockHeaderSubscriber = (*liquidBlockHeaderSubscriber)(nil) + +func (h *liquidBlockHeaderSubscriber) Register(tx TXObserver) { + h.txObservers = append(h.txObservers, tx) +} + +func (h *liquidBlockHeaderSubscriber) Deregister(o TXObserver) { + h.txObservers = remove(h.txObservers, o) +} + +func (h *liquidBlockHeaderSubscriber) Update(ctx context.Context, blockHeight BlocKHeight) error { + for _, observer := range h.txObservers { + callbacked, err := observer.Callback(ctx, blockHeight) + if callbacked && err == nil { + // callbacked and no error, remove observer + h.Deregister(observer) + } + if err != nil { + log.Infof("Error in callback: %v", err) + } + } + return nil +} + +func remove(observerList []TXObserver, observerToRemove TXObserver) []TXObserver { + newObservers := make([]TXObserver, len(observerList)-1) + for _, observer := range observerList { + if observer.GetSwapID() != observerToRemove.GetSwapID() { + newObservers = append(newObservers, observer) + } + } + return newObservers +} diff --git a/electrum/electrum.go b/electrum/electrum.go new file mode 100644 index 00000000..4a224ff8 --- /dev/null +++ b/electrum/electrum.go @@ -0,0 +1,13 @@ +package electrum + +import ( + "context" + + "github.com/checksum0/go-electrum/electrum" +) + +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) +} diff --git a/electrum/tx_observer.go b/electrum/tx_observer.go new file mode 100644 index 00000000..1819a6b4 --- /dev/null +++ b/electrum/tx_observer.go @@ -0,0 +1,149 @@ +package electrum + +import ( + "context" + "crypto/sha256" + "fmt" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/checksum0/go-electrum/electrum" + "github.com/elementsproject/peerswap/onchain" + "github.com/elementsproject/peerswap/swap" +) + +type TXObserver interface { + GetSwapID() swap.SwapId + // Callback calls the callback function if the condition is match. + // Returns true if the callback function is called. + Callback(context.Context, BlocKHeight) (bool, error) +} + +type scriptPubKey struct { + txscript.PkScript +} + +func NewScriptPubKey(script []byte) (scriptPubKey, error) { + s, err := txscript.ParsePkScript(script) + if err != nil { + return scriptPubKey{s}, fmt.Errorf("failed to parse script: %w", err) + } + return scriptPubKey{s}, nil +} +func (s *scriptPubKey) scriptHash() string { + hash := sha256.Sum256(s.Script()) + reversedHash := make([]byte, len(hash)) + for i, b := range hash { + reversedHash[len(hash)-1-i] = b + } + return fmt.Sprintf("%X", reversedHash) +} + +type confirmationCallback = func(swapId string, txHex string, err error) error + +type observeOpeningTX struct { + swapID swap.SwapId + txID *chainhash.Hash + scriptPubkey scriptPubKey + electrumClient RPC + cb confirmationCallback +} + +var _ TXObserver = (*observeOpeningTX)(nil) + +func NewObserveOpeningTX( + swapID swap.SwapId, + txID *chainhash.Hash, + scriptPubkey scriptPubKey, + electrumClient RPC, + cb confirmationCallback) observeOpeningTX { + return observeOpeningTX{ + swapID: swapID, + txID: txID, + scriptPubkey: scriptPubkey, + electrumClient: electrumClient, + cb: cb, + } +} + +func (o *observeOpeningTX) GetSwapID() swap.SwapId { + return o.swapID +} + +func getHeight(hs []*electrum.GetMempoolResult, txID *chainhash.Hash) BlocKHeight { + for _, h := range hs { + hh, err := chainhash.NewHashFromStr(h.Hash) + if err != nil { + continue + } + if hh.IsEqual(txID) { + return BlocKHeight(h.Height) + } + } + return 0 +} + +func (o *observeOpeningTX) Callback(ctx context.Context, currentHeight BlocKHeight) (bool, error) { + hs, err := o.electrumClient.GetHistory(ctx, o.scriptPubkey.scriptHash()) + if err != nil { + return false, fmt.Errorf("failed to get history: %w", err) + } + if !(getHeight(hs, o.txID).Confirmed()) { + return false, fmt.Errorf("the transaction is unconfirmed") + } + rawTx, err := o.electrumClient.GetRawTransaction(ctx, o.txID.String()) + if err != nil { + return false, fmt.Errorf("failed to get raw transaction: %w", err) + } + if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidConfs)) { + return false, fmt.Errorf("not enough confirmations for opening transaction. txhash: %s.height: %d, current: %d", + o.txID.String(), getHeight(hs, o.txID).Height(), currentHeight.Height()) + } + return true, o.cb(o.swapID.String(), rawTx, nil) +} + +type csvCallback = func(swapId string) error + +type observeCSVTX struct { + swapID swap.SwapId + txID *chainhash.Hash + scriptPubkey scriptPubKey + electrumClient RPC + cb csvCallback +} + +var _ TXObserver = (*observeCSVTX)(nil) + +func NewobserveCSVTX( + swapID swap.SwapId, + txID *chainhash.Hash, + scriptPubkey scriptPubKey, + electrumClient RPC, + cb csvCallback) observeCSVTX { + return observeCSVTX{ + swapID: swapID, + txID: txID, + scriptPubkey: scriptPubkey, + electrumClient: electrumClient, + cb: cb, + } +} + +func (o *observeCSVTX) GetSwapID() swap.SwapId { + return o.swapID +} + +func (o *observeCSVTX) Callback(ctx context.Context, currentHeight BlocKHeight) (bool, error) { + hs, err := o.electrumClient.GetHistory(ctx, o.scriptPubkey.scriptHash()) + if err != nil { + return false, fmt.Errorf("failed to get history: %w", err) + } + if !(getHeight(hs, o.txID).Confirmed()) { + return false, fmt.Errorf("the transaction is unconfirmed") + } + if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidCsv)) { + return false, fmt.Errorf("not enough confirmations for csv transaction. txhash: %s.height: %d, current: %d", + o.txID.String(), getHeight(hs, o.txID).Height(), currentHeight.Height()) + } + return true, o.cb(o.swapID.String()) +} diff --git a/lnd/txwatcher.go b/lnd/txwatcher.go index aecd8d63..7aac68df 100644 --- a/lnd/txwatcher.go +++ b/lnd/txwatcher.go @@ -77,6 +77,9 @@ func NewTxWatcher(ctx context.Context, cc *grpc.ClientConn, network *chaincfg.Pa func (t *TxWatcher) Start() error { return nil } +func (t *TxWatcher) StartWatchingTxs() error { + return nil +} func (t *TxWatcher) Stop() error { t.cancel() diff --git a/lwk/electrumtxwatcher.go b/lwk/electrumtxwatcher.go new file mode 100644 index 00000000..b4cf6f5b --- /dev/null +++ b/lwk/electrumtxwatcher.go @@ -0,0 +1,110 @@ +package lwk + +import ( + "context" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/elementsproject/peerswap/electrum" + "github.com/elementsproject/peerswap/log" + "github.com/elementsproject/peerswap/swap" +) + +type electrumTxWatcher struct { + electrumClient electrum.RPC + blockHeight electrum.BlocKHeight + subscriber electrum.BlockHeaderSubscriber + confirmationCallback func(swapId string, txHex string, err error) error + csvCallback func(swapId string) error +} + +func NewElectrumTxWatcher(electrumClient electrum.RPC) (*electrumTxWatcher, error) { + r := &electrumTxWatcher{ + electrumClient: electrumClient, + subscriber: electrum.NewLiquidBlockHeaderSubscriber(), + } + return r, nil +} + +func (r *electrumTxWatcher) StartWatchingTxs() error { + ctx := context.Background() + headerSubscription, err := r.electrumClient.SubscribeHeaders(ctx) + if err != nil { + return err + } + go func() { + for { + select { + case blockHeader, ok := <-headerSubscription: + if !ok { + log.Infof("Header subscription closed, stopping watching txs.") + return + } + r.blockHeight = electrum.BlocKHeight(blockHeader.Height) + log.Infof("New block received. block height:%d", 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 + } + } + }() + return nil +} + +func (r *electrumTxWatcher) AddWaitForConfirmationTx(swapIDStr, txIDStr string, vout, startingHeight uint32, scriptpubkeyByte []byte) { + swapID := swap.NewSwapId() + err := swapID.FromString(swapIDStr) + if err != nil { + log.Infof("Error parsing swapID: %v", err) + return + } + txID, err := chainhash.NewHashFromStr(txIDStr) + if err != nil { + log.Infof("Error parsing txID: %v", err) + return + } + scrypt, err := electrum.NewScriptPubKey(scriptpubkeyByte) + if err != nil { + log.Infof("Error parsing scriptpubkey: %v", err) + return + } + tx := electrum.NewObserveOpeningTX(*swapID, txID, scrypt, r.electrumClient, r.confirmationCallback) + r.subscriber.Register(&tx) +} + +func (r *electrumTxWatcher) AddConfirmationCallback(f func(swapId string, txHex string, err error) error) { + r.confirmationCallback = f +} +func (r *electrumTxWatcher) AddCsvCallback(f func(swapId string) error) { + r.csvCallback = f +} + +func (r *electrumTxWatcher) GetBlockHeight() (uint32, error) { + return r.blockHeight.Height(), nil +} + +func (r *electrumTxWatcher) AddWaitForCsvTx(swapIDStr, txIDStr string, vout, startingHeight uint32, scriptpubkeyByte []byte) { + swapID := swap.NewSwapId() + err := swapID.FromString(swapIDStr) + if err != nil { + log.Infof("Error parsing swapID: %v", err) + return + } + txID, err := chainhash.NewHashFromStr(txIDStr) + if err != nil { + log.Infof("Error parsing txID: %v", err) + return + } + scrypt, err := electrum.NewScriptPubKey(scriptpubkeyByte) + if err != nil { + log.Infof("Error parsing scriptpubkey: %v", err) + return + } + tx := electrum.NewobserveCSVTX(*swapID, txID, scrypt, r.electrumClient, r.csvCallback) + r.subscriber.Register(&tx) +} diff --git a/lwk/electrumtxwatcher_test.go b/lwk/electrumtxwatcher_test.go new file mode 100644 index 00000000..44135b8f --- /dev/null +++ b/lwk/electrumtxwatcher_test.go @@ -0,0 +1,122 @@ +package lwk_test + +import ( + "testing" + + "github.com/checksum0/go-electrum/electrum" + "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" + "go.uber.org/mock/gomock" +) + +func TestElectrumTxWatcher_Callback(t *testing.T) { + t.Parallel() + t.Run("confirmed opening transaction", func(t *testing.T) { + t.Parallel() + var ( + wantSwapID = swap.NewSwapId().String() + wantTxID = "1" // Single digit hash. + wantTxHex = "testb" + wantscriptpubkey = []byte{ + // OP_0 + 0x00, + // OP_DATA_32 + 0x20, + // <32-byte script hash> + 0xec, 0x6f, 0x7a, 0x5a, 0xa8, 0xf2, 0xb1, 0x0c, + 0xa5, 0x15, 0x04, 0x52, 0x3a, 0x60, 0xd4, 0x03, + 0x06, 0xf6, 0x96, 0xcd, 0x06, 0xf6, 0x96, 0xcd, + 0x06, 0xf6, 0x96, 0xcd, 0x06, 0xf6, 0x96, 0xcd, + } + callbackChan = make(chan string) + targetTXHeight int32 = 100 + ) + + electrumRPC := mock_txwatcher.NewMockelectrumRPC(gomock.NewController(t)) + headerResultChan := make(chan *electrum.SubscribeHeadersResult, 1) + electrumRPC.EXPECT().SubscribeHeaders(gomock.Any()). + Return(headerResultChan, nil) + electrumRPC.EXPECT().GetHistory(gomock.Any(), gomock.Any()).Return([]*electrum.GetMempoolResult{ + { + Hash: wantTxID, + Height: targetTXHeight, + }, + }, nil) + electrumRPC.EXPECT().GetRawTransaction(gomock.Any(), gomock.Any()).Return(wantTxHex, nil) + + r, err := lwk.NewElectrumTxWatcher(electrumRPC) + assert.NoError(t, err) + r.AddConfirmationCallback( + func(swapId string, txHex string, err error) error { + assert.Equal(t, wantSwapID, swapId) + assert.Equal(t, wantTxHex, txHex) + assert.NoError(t, err) + callbackChan <- swapId + return nil + }, + ) + err = r.StartWatchingTxs() + assert.NoError(t, err) + r.AddWaitForConfirmationTx(wantSwapID, wantTxID, 0, 0, wantscriptpubkey) + headerResultChan <- &electrum.SubscribeHeadersResult{ + Height: onchain.LiquidConfs + targetTXHeight + 1, + } + + assert.Equal(t, <-callbackChan, wantSwapID) + }) + + t.Run("confirmed csv transaction", func(t *testing.T) { + t.Parallel() + var ( + wantSwapID = swap.NewSwapId().String() + wantTxID = "1" // Single digit hash. + wantscriptpubkey = []byte{ + // OP_0 + 0x00, + // OP_DATA_32 + 0x20, + // <32-byte script hash> + 0xec, 0x6f, 0x7a, 0x5a, 0xa8, 0xf2, 0xb1, 0x0c, + 0xa5, 0x15, 0x04, 0x52, 0x3a, 0x60, 0xd4, 0x03, + 0x06, 0xf6, 0x96, 0xcd, 0x06, 0xf6, 0x96, 0xcd, + 0x06, 0xf6, 0x96, 0xcd, 0x06, 0xf6, 0x96, 0xcd, + } + callbackChan = make(chan string) + targetTXHeight = int32(100) + ) + + electrumRPC := mock_txwatcher.NewMockelectrumRPC(gomock.NewController(t)) + headerResultChan := make(chan *electrum.SubscribeHeadersResult, 1) + electrumRPC.EXPECT().SubscribeHeaders(gomock.Any()). + Return(headerResultChan, nil) + electrumRPC.EXPECT().GetHistory(gomock.Any(), gomock.Any()).Return([]*electrum.GetMempoolResult{ + { + Hash: wantTxID, + Height: targetTXHeight, + }, + }, nil) + + r, err := lwk.NewElectrumTxWatcher(electrumRPC) + assert.NoError(t, err) + r.AddCsvCallback( + func(swapId string) error { + assert.Equal(t, wantSwapID, swapId) + assert.NoError(t, err) + callbackChan <- swapId + return nil + }, + ) + err = r.StartWatchingTxs() + assert.NoError(t, err) + r.AddWaitForCsvTx(wantSwapID, wantTxID, 0, 0, wantscriptpubkey) + headerResultChan <- &electrum.SubscribeHeadersResult{ + Height: onchain.LiquidCsv + targetTXHeight + 1, + } + + assert.Equal(t, <-callbackChan, wantSwapID) + }) + +} diff --git a/lwk/mock/electrumRPC.go b/lwk/mock/electrumRPC.go new file mode 100644 index 00000000..6fd3ffef --- /dev/null +++ b/lwk/mock/electrumRPC.go @@ -0,0 +1,86 @@ +// 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) +} diff --git a/swap/swap_out_sender_test.go b/swap/swap_out_sender_test.go index ca8a97ab..2ca0ee6c 100644 --- a/swap/swap_out_sender_test.go +++ b/swap/swap_out_sender_test.go @@ -411,6 +411,10 @@ type dummyChain struct { returnGetCSVHeight uint32 } +func (d *dummyChain) StartWatchingTxs() error { + return nil +} + func (d *dummyChain) SetBalance(balance uint64) { d.balance = balance } From ab80a530558cea60d98bbe9b9fca41c60366b775 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 27 Mar 2024 08:56:05 +0900 Subject: [PATCH 05/29] multi: lwk configuration support for Liquid swaps Input can be in toml or ini format, but each is managed as the same config. To make it loosely coupled, used the builder pattern to set item. I also add validation of the config. The config file cannot be modified from the outside. --- clightning/config.go | 70 ++++++++++++++- clightning/lwkconfig_test.go | 59 +++++++++++++ cmd/peerswap-plugin/main.go | 7 +- cmd/peerswaplnd/config.go | 70 ++++++++++++++- cmd/peerswaplnd/config_test.go | 59 +++++++++++++ cmd/peerswaplnd/peerswapd/main.go | 41 +++------ go.mod | 20 +++-- go.sum | 33 +++++--- lwk/client.go | 91 ++++++++++++++++++++ lwk/conf_builder.go | 88 +++++++++++++++++++ lwk/conf_builder_test.go | 86 +++++++++++++++++++ lwk/config.go | 136 ++++++++++++++++++++++++++++++ lwk/config_test.go | 78 +++++++++++++++++ lwk/lwkwallet.go | 42 ++++++++- lwk/validator.go | 21 +++++ 15 files changed, 845 insertions(+), 56 deletions(-) create mode 100644 clightning/lwkconfig_test.go create mode 100644 cmd/peerswaplnd/config_test.go create mode 100644 lwk/conf_builder.go create mode 100644 lwk/conf_builder_test.go create mode 100644 lwk/config.go create mode 100644 lwk/config_test.go create mode 100644 lwk/validator.go diff --git a/clightning/config.go b/clightning/config.go index 9ecf1e8c..98e14452 100644 --- a/clightning/config.go +++ b/clightning/config.go @@ -13,6 +13,7 @@ import ( "github.com/elementsproject/glightning/glightning" "github.com/elementsproject/peerswap/log" + "github.com/elementsproject/peerswap/lwk" "github.com/pelletier/go-toml/v2" ) @@ -58,6 +59,7 @@ type Config struct { PolicyPath string Bitcoin *BitcoinConf Liquid *LiquidConf + LWK *lwk.Conf } func (c Config) String() string { @@ -200,11 +202,77 @@ func ReadFromFile() Processor { c.Liquid.RpcWallet = fileConf.Liquid.RpcWallet c.Liquid.LiquidSwaps = fileConf.Liquid.LiquidSwaps } - + lc, err := LWKConfigFromToml(filepath.Join(c.PeerswapDir, defaultConfigFileName)) + if err != nil { + return nil, err + } + c.LWK = lc return c, nil } } +func LWKConfigFromToml(filePath string) (*lwk.Conf, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + type LwkConfig struct { + SignerName string + WalletName string + LWKEndpoint string + ElectrumEndpoint string + Network string + LiquidSwaps *bool + } + var cfg struct { + LWK *LwkConfig + } + err = toml.Unmarshal(data, &cfg) + if err != nil { + return nil, err + } + if cfg.LWK == nil { + return nil, nil + } + + ln, err := lwk.NewlwkNetwork("liquid-testnet") + if err != nil { + return nil, err + } + if cfg.LWK.Network != "" { + n, e := lwk.NewlwkNetwork(cfg.LWK.Network) + if e != nil { + return nil, e + } + ln = n + } + c, err := lwk.NewConfBuilder(ln).DefaultConf() + if err != nil { + return nil, err + } + if cfg.LWK.WalletName != "" { + c.SetWalletName(lwk.NewConfName(cfg.LWK.WalletName)) + } + if cfg.LWK.SignerName != "" { + c.SetSignerName(lwk.NewConfName(cfg.LWK.SignerName)) + } + if cfg.LWK.LWKEndpoint != "" { + lwkEndpoint, err := lwk.NewConfURL(cfg.LWK.LWKEndpoint) + if err != nil { + return nil, err + } + c.SetLWKEndpoint(*lwkEndpoint) + } + if cfg.LWK.ElectrumEndpoint != "" { + electrumEndpoint, err := lwk.NewConfURL(cfg.LWK.ElectrumEndpoint) + if err != nil { + return nil, err + } + c.SetElectrumEndpoint(*electrumEndpoint) + } + return c.SetLiquidSwaps(*cfg.LWK.LiquidSwaps).Build() +} + func PeerSwapFallback() Processor { return func(c *Config) (*Config, error) { if c.PolicyPath == "" { diff --git a/clightning/lwkconfig_test.go b/clightning/lwkconfig_test.go new file mode 100644 index 00000000..c5e46e88 --- /dev/null +++ b/clightning/lwkconfig_test.go @@ -0,0 +1,59 @@ +package clightning_test + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/elementsproject/peerswap/clightning" + "github.com/stretchr/testify/assert" + "github.com/vulpemventures/go-elements/network" +) + +func TestLWKConfigFromToml(t *testing.T) { + t.Parallel() + t.Run("valid toml config", func(t *testing.T) { + t.Parallel() + file := ` + [LWK] + signername="signername" + walletname="walletname" + lwkendpoint="http://localhost:32110" + network="liquid" + liquidswaps=true + ` + filePath := filepath.Join(t.TempDir(), "peerswap.conf") + assert.NoError(t, os.WriteFile(filePath, []byte(file), fs.ModePerm)) + got, err := clightning.LWKConfigFromToml(filePath) + if err != nil { + t.Errorf("LWKConfigFromToml() error = %v", err) + return + } + assert.Equal(t, got.GetChain(), &network.Liquid) + assert.Equal(t, got.GetElectrumEndpoint(), "blockstream.info:995") + assert.Equal(t, got.GetLWKEndpoint(), "http://localhost:32110") + assert.Equal(t, got.GetLiquidSwaps(), true) + assert.Equal(t, got.GetNetwork(), "liquid") + }) + t.Run("default toml config", func(t *testing.T) { + t.Parallel() + file := ` + [LWK] + network="liquid-testnet" + liquidswaps=true + ` + filePath := filepath.Join(t.TempDir(), "peerswap.conf") + assert.NoError(t, os.WriteFile(filePath, []byte(file), fs.ModePerm)) + got, err := clightning.LWKConfigFromToml(filePath) + if err != nil { + t.Errorf("LWKConfigFromToml() error = %v", err) + return + } + assert.Equal(t, got.GetChain(), &network.Testnet) + assert.Equal(t, got.GetElectrumEndpoint(), "blockstream.info:465") + assert.Equal(t, got.GetLWKEndpoint(), "http://localhost:32111") + assert.Equal(t, got.GetLiquidSwaps(), true) + assert.Equal(t, got.GetNetwork(), "liquid-testnet") + }) +} diff --git a/cmd/peerswap-plugin/main.go b/cmd/peerswap-plugin/main.go index 444fbe73..fcd9ba89 100644 --- a/cmd/peerswap-plugin/main.go +++ b/cmd/peerswap-plugin/main.go @@ -9,9 +9,11 @@ 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" + "github.com/elementsproject/peerswap/lwk" "github.com/elementsproject/peerswap/version" "golang.org/x/sys/unix" @@ -169,7 +171,7 @@ func run(ctx context.Context, lightningPlugin *clightning.ClightningClient) erro var liquidCli *gelements.Elements var liquidEnabled bool - if *config.Liquid.LiquidSwaps && liquidWanted(config) { + if *config.Liquid.LiquidSwaps && elementsWanted(config) { liquidEnabled = true log.Infof("Starting elements client with rpcuser: %s, rpcpassword:******, rpccookie: %s, rpcport: %d, rpchost: %s", config.Liquid.RpcUser, @@ -411,7 +413,7 @@ func run(ctx context.Context, lightningPlugin *clightning.ClightningClient) erro return nil } -func liquidWanted(cfg *clightning.Config) bool { +func elementsWanted(cfg *clightning.Config) bool { return cfg.Liquid.RpcUser != "" && cfg.Liquid.RpcPassword != "" } @@ -431,6 +433,7 @@ func getLiquidChain(li *gelements.Elements) (*network.Network, error) { return &network.Testnet, nil } } + func getBitcoinChain(li *glightning.Lightning) (*chaincfg.Params, error) { gi, err := li.GetInfo() if err != nil { diff --git a/cmd/peerswaplnd/config.go b/cmd/peerswaplnd/config.go index 435add64..87dcdc46 100644 --- a/cmd/peerswaplnd/config.go +++ b/cmd/peerswaplnd/config.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/btcsuite/btcd/btcutil" + "github.com/elementsproject/peerswap/lwk" + "github.com/jessevdk/go-flags" ) type LogLevel uint8 @@ -45,6 +47,7 @@ type PeerSwapConfig struct { LndConfig *LndConfig `group:"Lnd Grpc config" namespace:"lnd"` ElementsConfig *OnchainConfig `group:"Elements Rpc Config" namespace:"elementsd"` + LWKConfig *lwk.Conf LiquidEnabled bool `long:"liquidswaps" description:"enable bitcoin peerswaps"` BitcoinEnabled bool `long:"bitcoinswaps" description:"enable bitcoin peerswaps"` @@ -74,7 +77,8 @@ func (p *PeerSwapConfig) Validate() error { return err } p.LiquidEnabled = true - + } else if p.LWKConfig.Enabled() { + p.LiquidEnabled = true } return nil } @@ -161,3 +165,67 @@ func defaultLiquidConfig() *OnchainConfig { LiquidSwaps: true, } } + +func LWKFromIniFileConfig(filePath string) (*lwk.Conf, error) { + type LWK struct { + SignerName string `long:"signername" description:"name of the signer"` + WalletName string `long:"walletname" description:"name of the wallet"` + LWKEndpoint string `long:"lwkendpoint" description:"endpoint for the liquid wallet kit"` + ElectrumEndpoint string `long:"elementsendpoint" description:"endpoint for the elements rpc"` + Network string `long:"network" description:"network to use"` + LiquidSwaps bool `long:"liquidswaps" description:"enable liquid swaps"` + } + type IniConf struct { + LWK *LWK `group:"Elements Rpc Config" namespace:"lwk"` + } + cfg := &IniConf{} + if _, err := os.Stat(filePath); err == nil { + fileParser := flags.NewParser(cfg, flags.Default|flags.IgnoreUnknown) + err = flags.NewIniParser(fileParser).ParseFile(filePath) + if err != nil { + return nil, err + } + } + flagParser := flags.NewParser(cfg, flags.Default|flags.IgnoreUnknown) + if _, err := flagParser.Parse(); err != nil { + return nil, err + } + + ln, err := lwk.NewlwkNetwork("liquid-testnet") + if err != nil { + return nil, err + } + + if cfg.LWK.Network != "" { + n, e := lwk.NewlwkNetwork(cfg.LWK.Network) + if e != nil { + return nil, e + } + ln = n + } + c, err := lwk.NewConfBuilder(ln).DefaultConf() + if err != nil { + return nil, err + } + if cfg.LWK.WalletName != "" { + c.SetWalletName(lwk.NewConfName(cfg.LWK.WalletName)) + } + if cfg.LWK.SignerName != "" { + c.SetSignerName(lwk.NewConfName(cfg.LWK.SignerName)) + } + if cfg.LWK.LWKEndpoint != "" { + lwkEndpoint, err := lwk.NewConfURL(cfg.LWK.LWKEndpoint) + if err != nil { + return nil, err + } + c.SetLWKEndpoint(*lwkEndpoint) + } + if cfg.LWK.ElectrumEndpoint != "" { + electrumEndpoint, err := lwk.NewConfURL(cfg.LWK.ElectrumEndpoint) + if err != nil { + return nil, err + } + c.SetElectrumEndpoint(*electrumEndpoint) + } + return c.SetLiquidSwaps(cfg.LWK.LiquidSwaps).Build() +} diff --git a/cmd/peerswaplnd/config_test.go b/cmd/peerswaplnd/config_test.go new file mode 100644 index 00000000..1f0ac728 --- /dev/null +++ b/cmd/peerswaplnd/config_test.go @@ -0,0 +1,59 @@ +package peerswaplnd_test + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/elementsproject/peerswap/cmd/peerswaplnd" + "github.com/stretchr/testify/assert" + "github.com/vulpemventures/go-elements/network" +) + +func TestLWKFromIniFileConfig(t *testing.T) { + t.Parallel() + t.Run("valid ini config", func(t *testing.T) { + t.Parallel() + file := ` + lwk.signername=signername + lwk.walletname=walletname + lwk.lwkendpoint=http://localhost:32110 + lwk.network=liquid + lwk.liquidswaps=true + ` + + filePath := filepath.Join(t.TempDir(), "peerswap.conf") + assert.NoError(t, os.WriteFile(filePath, []byte(file), fs.ModePerm)) + got, err := peerswaplnd.LWKFromIniFileConfig(filePath) + if err != nil { + t.Errorf("LWKConfigFromToml() error = %v", err) + return + } + assert.Equal(t, got.GetChain(), &network.Liquid) + assert.Equal(t, got.GetElectrumEndpoint(), "blockstream.info:995") + assert.Equal(t, got.GetLWKEndpoint(), "http://localhost:32110") + assert.Equal(t, got.GetLiquidSwaps(), true) + assert.Equal(t, got.GetNetwork(), "liquid") + }) + t.Run("default ini config", func(t *testing.T) { + t.Parallel() + file := ` + lwk.network=liquid-testnet + lwk.liquidswaps=true + ` + + filePath := filepath.Join(t.TempDir(), "peerswap.conf") + assert.NoError(t, os.WriteFile(filePath, []byte(file), fs.ModePerm)) + got, err := peerswaplnd.LWKFromIniFileConfig(filePath) + if err != nil { + t.Errorf("LWKConfigFromToml() error = %v", err) + return + } + assert.Equal(t, got.GetChain(), &network.Testnet) + assert.Equal(t, got.GetElectrumEndpoint(), "blockstream.info:465") + assert.Equal(t, got.GetLWKEndpoint(), "http://localhost:32111") + assert.Equal(t, got.GetLiquidSwaps(), true) + assert.Equal(t, got.GetNetwork(), "liquid-testnet") + }) +} diff --git a/cmd/peerswaplnd/peerswapd/main.go b/cmd/peerswaplnd/peerswapd/main.go index 7f4a4e32..b13d22e5 100644 --- a/cmd/peerswaplnd/peerswapd/main.go +++ b/cmd/peerswaplnd/peerswapd/main.go @@ -16,10 +16,13 @@ import ( "syscall" "time" + "github.com/checksum0/go-electrum/electrum" "github.com/elementsproject/peerswap/elements" "github.com/elementsproject/peerswap/isdev" "github.com/elementsproject/peerswap/lnd" "github.com/elementsproject/peerswap/log" + "github.com/elementsproject/peerswap/lwk" + "github.com/elementsproject/peerswap/version" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -192,25 +195,10 @@ func run() error { // setup liquid stuff var liquidOnChainService *onchain.LiquidOnChain - var liquidTxWatcher *txwatcher.BlockchainRpcTxWatcher - var liquidRpcWallet *wallet.ElementsRpcWallet + var liquidTxWatcher swap.TxWatcher + var liquidRpcWallet wallet.Wallet var liquidCli *gelements.Elements if cfg.LiquidEnabled { - supportedAssets = append(supportedAssets, "lbtc") - log.Infof("Liquid swaps enabled") - liquidConfig := cfg.ElementsConfig - - // This call is blocking, waiting for elements to come alive and sync. - liquidCli, err = elements.NewClient( - liquidConfig.RpcUser, - liquidConfig.RpcPassword, - liquidConfig.RpcHost, - liquidConfig.RpcPasswordFile, - liquidConfig.RpcPort, - ) - if err != nil { - return err - } if cfg.ElementsConfig.RpcUser != "" { supportedAssets = append(supportedAssets, "lbtc") log.Infof("Liquid swaps enabled") @@ -241,7 +229,7 @@ func run() error { return err } liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, liquidChain) - } else if cfg.LWKConfig != nil { + } 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 { @@ -249,7 +237,7 @@ func run() error { } // This call is blocking, waiting for elements to come alive and sync. - liquidRpcWallet, err = lwk.NewLWKRpcWallet(lwk.NewLwk(cfg.LWKConfig.GetElectrumEndpoint()), + liquidRpcWallet, err = lwk.NewLWKRpcWallet(lwk.NewLwk(cfg.LWKConfig.GetLWKEndpoint()), ec, cfg.LWKConfig.GetWalletName(), cfg.LWKConfig.GetSignerName()) if err != nil { return err @@ -264,16 +252,6 @@ func run() error { } else { return errors.New("Liquid swaps enabled but no config found") } - - // txwatcher - liquidTxWatcher = txwatcher.NewBlockchainRpcTxWatcher(ctx, txwatcher.NewElementsCli(liquidCli), onchain.LiquidConfs, onchain.LiquidCsv) - - // LiquidChain - liquidChain, err := getLiquidChain(liquidCli) - if err != nil { - return err - } - liquidOnChainService = onchain.NewLiquidOnChain(liquidRpcWallet, liquidChain) } else { log.Infof("Liquid swaps disabled") } @@ -543,6 +521,11 @@ func loadConfig() (*peerswaplnd.PeerSwapConfig, error) { return nil, err } + lc, err := peerswaplnd.LWKFromIniFileConfig(cfg.ConfigFile) + if err != nil { + return nil, err + } + cfg.LWKConfig = lc return cfg, nil } diff --git a/go.mod b/go.mod index b32d0374..c40deb68 100644 --- a/go.mod +++ b/go.mod @@ -119,16 +119,16 @@ require ( go.opentelemetry.io/proto/otlp v0.19.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.23.0 // indirect + go.uber.org/zap v1.23.0 golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/net v0.1.0 // indirect golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect - golang.org/x/sys v0.0.0-20221010160319-abe0a0adba9c - golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/sys v0.1.0 + golang.org/x/term v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect - golang.org/x/tools v0.1.12 // indirect + golang.org/x/tools v0.2.0 // indirect google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect @@ -139,13 +139,15 @@ require ( ) require ( + github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9 + github.com/hashicorp/go-retryablehttp v0.7.5 github.com/pelletier/go-toml/v2 v2.0.5 github.com/pkg/errors v0.9.1 - go.uber.org/zap v1.23.0 - golang.org/x/sys v0.1.0 + go.uber.org/mock v0.4.0 ) require ( + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/lightningnetwork/lnd/clock v1.1.0 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect github.com/lightningnetwork/lnd/kvdb v1.3.1 // indirect diff --git a/go.sum b/go.sum index a330a6ca..c12ba272 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9 h1:PEkrrCdN0F0wgeof+V8dwMabAYccVBgJfqysVdlT51U= +github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9/go.mod h1:EjLxYzaf/28gOdSRlifeLfjoOA6aUjtJZhwaZPnjL9c= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -329,9 +331,15 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -742,6 +750,8 @@ go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -810,8 +820,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -862,8 +872,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= -golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -950,13 +960,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010160319-abe0a0adba9c h1:HrzHaDANRim3rGzWYCr7M3sX225iVkbhOmo/pzs+1kQ= -golang.org/x/sys v0.0.0-20221010160319-abe0a0adba9c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -965,8 +975,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1030,8 +1041,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/lwk/client.go b/lwk/client.go index d9fa0663..c895df17 100644 --- a/lwk/client.go +++ b/lwk/client.go @@ -212,3 +212,94 @@ func (l *lwkclient) walletDetails(ctx context.Context, w *walletDetailsRequest) } return &resp, nil } + +type generateSignerRequest struct { +} + +func (r *generateSignerRequest) Name() string { + return "generate_signer" +} + +type generateSignerResponse struct { + Mnemonic string `json:"mnemonic"` +} + +func (l *lwkclient) generateSigner(ctx context.Context) (*generateSignerResponse, error) { + var resp generateSignerResponse + err := l.request(ctx, &generateSignerRequest{}, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type loadSoftwareSignerRequest struct { + Mnemonic string `json:"mnemonic"` + SignerName string `json:"name"` +} + +func (r *loadSoftwareSignerRequest) Name() string { + return "signer_load_software" +} + +type loadSoftwareSignerResponse struct { + Fingerprint string `json:"fingerprint"` + ID string `json:"id"` + Name string `json:"name"` + Xpub string `json:"xpub"` +} + +func (l *lwkclient) loadSoftwareSigner(ctx context.Context, req *loadSoftwareSignerRequest) (*loadSoftwareSignerResponse, error) { + var resp loadSoftwareSignerResponse + err := l.request(ctx, req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type singlesigDescriptorRequest struct { + DescriptorBlindingKey string `json:"descriptor_blinding_key"` + SignerName string `json:"name"` + SinglesigKind string `json:"singlesig_kind"` +} + +func (r *singlesigDescriptorRequest) Name() string { + return "singlesig_descriptor" +} + +type singlesigDescriptorResponse struct { + Descriptor string `json:"descriptor"` +} + +func (l *lwkclient) singlesigDescriptor(ctx context.Context, req *singlesigDescriptorRequest) (*singlesigDescriptorResponse, error) { + var resp singlesigDescriptorResponse + err := l.request(ctx, req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +type loadWalletRequest struct { + Descriptor string `json:"descriptor"` + WalletName string `json:"name"` +} + +func (r *loadWalletRequest) Name() string { + return "load_wallet" +} + +type loadWalletResponse struct { + Descriptor string `json:"descriptor"` + Name string `json:"name"` +} + +func (l *lwkclient) loadWallet(ctx context.Context, req *loadWalletRequest) (*loadWalletResponse, error) { + var resp loadWalletResponse + err := l.request(ctx, req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} diff --git a/lwk/conf_builder.go b/lwk/conf_builder.go new file mode 100644 index 00000000..b8b1bb16 --- /dev/null +++ b/lwk/conf_builder.go @@ -0,0 +1,88 @@ +package lwk + +type confBuilder struct { + Conf +} + +func NewConfBuilder(network LwkNetwork) *confBuilder { + return &confBuilder{ + Conf: Conf{ + network: network, + }, + } +} + +func (b *confBuilder) DefaultConf() (*confBuilder, error) { + var ( + lwkEndpoint, electrumEndpoint string + ) + switch b.network { + case NetworkTestnet: + lwkEndpoint = "http://localhost:32111" + electrumEndpoint = "blockstream.info:465" + case NetworkRegtest: + lwkEndpoint = "http://localhost:32112" + electrumEndpoint = "localhost:60401" + default: + // mainnet is the default port + lwkEndpoint = "http://localhost:32110" + electrumEndpoint = "blockstream.info:995" + } + lwkURL, err := NewConfURL(lwkEndpoint) + if err != nil { + return nil, err + } + elementsURL, err := NewConfURL(electrumEndpoint) + if err != nil { + return nil, err + } + b.signerName = "defaultPeerswapSigner" + b.walletName = "defaultPeerswapWallet" + b.lwkEndpoint = *lwkURL + b.electrumEndpoint = *elementsURL + return b, nil +} + +func (b *confBuilder) SetSignerName(name confname) *confBuilder { + b.signerName = name + return b +} + +func (b *confBuilder) SetWalletName(name confname) *confBuilder { + b.walletName = name + return b +} + +func (b *confBuilder) SetLWKEndpoint(endpoint confurl) *confBuilder { + b.lwkEndpoint = endpoint + return b +} + +func (b *confBuilder) SetElectrumEndpoint(endpoint confurl) *confBuilder { + b.electrumEndpoint = endpoint + return b +} + +func (b *confBuilder) SetLiquidSwaps(swaps bool) *confBuilder { + b.liquidSwaps = swaps + return b +} + +func (b *confBuilder) Build() (*Conf, error) { + if err := Validate( + b.electrumEndpoint, + b.lwkEndpoint, + b.network, + b.signerName, + b.walletName); err != nil { + return nil, err + } + return &Conf{ + signerName: b.signerName, + walletName: b.walletName, + lwkEndpoint: b.lwkEndpoint, + electrumEndpoint: b.electrumEndpoint, + network: b.network, + liquidSwaps: b.liquidSwaps, + }, nil +} diff --git a/lwk/conf_builder_test.go b/lwk/conf_builder_test.go new file mode 100644 index 00000000..9886f8b9 --- /dev/null +++ b/lwk/conf_builder_test.go @@ -0,0 +1,86 @@ +package lwk_test + +import ( + "testing" + + "github.com/elementsproject/peerswap/lwk" + "github.com/vulpemventures/go-elements/network" +) + +func Test_confBuilder_DefaultConf(t *testing.T) { + t.Parallel() + b, err := lwk.NewConfBuilder(lwk.NetworkTestnet).DefaultConf() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + c, err := b.Build() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c.GetChain() != &network.Testnet { + t.Fatalf("unexpected chain: %v", c.GetChain()) + } + if c.GetElectrumEndpoint() != "blockstream.info:465" { + t.Fatalf("unexpected electrum endpoint: %v", c.GetElectrumEndpoint()) + } + if c.GetLWKEndpoint() != "http://localhost:32111" { + t.Fatalf("unexpected lwk endpoint: %v", c.GetLWKEndpoint()) + } + if c.GetLiquidSwaps() != true { + t.Fatalf("unexpected liquid swaps: %v", c.GetLiquidSwaps()) + } + if c.GetNetwork() != lwk.NetworkTestnet.String() { + t.Fatalf("unexpected network: %v", c.GetNetwork()) + } + if c.GetSignerName() != "defaultPeerswapSigner" { + t.Fatalf("unexpected signer name: %v", c.GetSignerName()) + } + if c.GetWalletName() != "defaultPeerswapWallet" { + t.Fatalf("unexpected wallet name: %v", c.GetWalletName()) + } +} + +func Test_confBuilder_SetConfs(t *testing.T) { + t.Parallel() + t.Run("OK if it called with valid arguments", func(t *testing.T) { + b, err := lwk.NewConfBuilder(lwk.NetworkTestnet).DefaultConf() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + c, err := b.SetSignerName("testSigner").SetWalletName("testSigner").Build() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c.GetChain() != &network.Testnet { + t.Fatalf("unexpected chain: %v", c.GetChain()) + } + if c.GetElectrumEndpoint() != "blockstream.info:465" { + t.Fatalf("unexpected electrum endpoint: %v", c.GetElectrumEndpoint()) + } + if c.GetLWKEndpoint() != "http://localhost:32111" { + t.Fatalf("unexpected lwk endpoint: %v", c.GetLWKEndpoint()) + } + if c.GetLiquidSwaps() != true { + t.Fatalf("unexpected liquid swaps: %v", c.GetLiquidSwaps()) + } + if c.GetNetwork() != lwk.NetworkTestnet.String() { + t.Fatalf("unexpected network: %v", c.GetNetwork()) + } + if c.GetSignerName() != "testSigner" { + t.Fatalf("unexpected signer name: %v", c.GetSignerName()) + } + if c.GetWalletName() != "testSigner" { + t.Fatalf("unexpected wallet name: %v", c.GetWalletName()) + } + }) + t.Run("Error if it called with empty signer name", func(t *testing.T) { + b, err := lwk.NewConfBuilder(lwk.NetworkTestnet).DefaultConf() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, err = b.SetSignerName("").Build() + if err == nil { + t.Fatalf("expected error, got nil") + } + }) +} diff --git a/lwk/config.go b/lwk/config.go new file mode 100644 index 00000000..1071e76b --- /dev/null +++ b/lwk/config.go @@ -0,0 +1,136 @@ +package lwk + +import ( + "errors" + "net/url" + + "github.com/vulpemventures/go-elements/network" +) + +type Conf struct { + signerName confname + walletName confname + lwkEndpoint confurl + electrumEndpoint confurl + network LwkNetwork + liquidSwaps bool +} + +func (c *Conf) GetSignerName() string { + return c.signerName.String() +} + +func (c *Conf) GetWalletName() string { + return c.walletName.String() +} + +func (c *Conf) GetLWKEndpoint() string { + return c.lwkEndpoint.String() +} + +func (c *Conf) GetElectrumEndpoint() string { + return c.electrumEndpoint.Host +} + +func (c *Conf) GetNetwork() string { + return c.network.String() +} + +func (c *Conf) GetLiquidSwaps() bool { + return c.liquidSwaps +} + +func (c *Conf) GetChain() *network.Network { + switch c.network { + case NetworkMainnet: + return &network.Liquid + case NetworkRegtest: + return &network.Regtest + case NetworkTestnet: + return &network.Testnet + default: + return &network.Testnet + } +} + +func (c *Conf) Enabled() bool { + return Validate( + c.electrumEndpoint, + c.lwkEndpoint, + c.network, + c.signerName, + c.walletName) == nil && c.liquidSwaps +} + +type LwkNetwork string + +const ( + NetworkMainnet LwkNetwork = "liquid" + NetworkTestnet LwkNetwork = "liquid-testnet" + NetworkRegtest LwkNetwork = "liquid-regtest" +) + +func (n LwkNetwork) String() string { + return string(n) +} + +func NewlwkNetwork(lekNetwork string) (LwkNetwork, error) { + switch lekNetwork { + case "liquid": + return NetworkMainnet, nil + case "liquid-testnet": + return NetworkTestnet, nil + case "liquid-regtest": + return NetworkRegtest, nil + default: + return "", errors.New("invalid network") + } +} + +func (n LwkNetwork) validate() error { + switch n { + case NetworkMainnet, NetworkTestnet, NetworkRegtest: + return nil + default: + return errors.New("invalid network") + } +} + +type confname string + +func NewConfName(name string) confname { + return confname(name) +} + +func (n confname) validate() error { + if n == "" { + return errors.New("name must be set") + } + return nil +} + +func (n confname) String() string { + return string(n) +} + +type confurl struct { + *url.URL +} + +func NewConfURL(endpoint string) (*confurl, error) { + u, err := url.ParseRequestURI(endpoint) + if err != nil { + return nil, err + } + return &confurl{u}, nil +} + +func (n confurl) validate() error { + if n.URL == nil { + return errors.New("url must be set") + } + if n.URL.String() == "" { + return errors.New("could not parse url") + } + return nil +} diff --git a/lwk/config_test.go b/lwk/config_test.go new file mode 100644 index 00000000..738ce32b --- /dev/null +++ b/lwk/config_test.go @@ -0,0 +1,78 @@ +package lwk_test + +import ( + "testing" + + "github.com/elementsproject/peerswap/lwk" + "github.com/stretchr/testify/assert" +) + +func TestNewlwkNetwork(t *testing.T) { + t.Parallel() + tests := map[string]struct { + network string + want lwk.LwkNetwork + }{ + "mainnet": { + network: "liquid", + want: lwk.NetworkMainnet, + }, + "testnet": { + network: "liquid-testnet", + want: lwk.NetworkTestnet, + }, + "regtest": { + network: "liquid-regtest", + want: lwk.NetworkRegtest, + }, + } + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + t.Parallel() + got, err := lwk.NewlwkNetwork(tt.network) + if err != nil { + t.Errorf("NewlwkNetwork() error = %v", err) + } + if got != tt.want { + t.Errorf("NewlwkNetwork() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewConfURL(t *testing.T) { + t.Parallel() + tests := map[string]struct { + endpoint string + want string + wantErr bool + }{ + "valid url": { + endpoint: "http://localhost:32111", + want: "http://localhost:32111", + }, + "without protocol": { + endpoint: "localhost:32111", + want: "localhost:32111", + }, + "invalid url": { + endpoint: "invalid url", + wantErr: true, + }, + } + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + t.Parallel() + got, err := lwk.NewConfURL(tt.endpoint) + if tt.wantErr { + assert.Error(t, err) + return + } + if got.String() != tt.want { + t.Errorf("NewConfURL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index e9c57afe..227116f1 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -5,6 +5,7 @@ import ( "errors" "log" "math" + "strings" "github.com/checksum0/go-electrum/electrum" @@ -45,7 +46,7 @@ func NewLWKRpcWallet(lwkClient *lwkclient, electrumClient *electrum.Client, wall lwkClient: lwkClient, electrumClient: electrumClient, } - err := rpcWallet.setupWallet() + err := rpcWallet.setupWallet(context.Background()) if err != nil { return nil, err } @@ -53,12 +54,15 @@ func NewLWKRpcWallet(lwkClient *lwkclient, electrumClient *electrum.Client, wall } // setupWallet checks if the swap wallet is already loaded in elementsd, if not it loads/creates it -func (r *LWKRpcWallet) setupWallet() error { - ctx := context.Background() +func (r *LWKRpcWallet) setupWallet(ctx context.Context) error { res, err := r.lwkClient.walletDetails(ctx, &walletDetailsRequest{ WalletName: r.walletName, }) 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) + } return err } signers := res.Signers @@ -71,6 +75,38 @@ func (r *LWKRpcWallet) setupWallet() error { return nil } +func (r *LWKRpcWallet) createWallet(ctx context.Context, walletName, signerName string) error { + res, err := r.lwkClient.generateSigner(ctx) + if err != nil { + return err + } + _, err = r.lwkClient.loadSoftwareSigner(ctx, &loadSoftwareSignerRequest{ + Mnemonic: res.Mnemonic, + SignerName: signerName, + }) + // 32011 is the error code for signer already loaded + if err != nil && !strings.HasPrefix(err.Error(), "-32011") { + return err + } + descres, err := r.lwkClient.singlesigDescriptor(ctx, &singlesigDescriptorRequest{ + SignerName: signerName, + DescriptorBlindingKey: "slip77", + SinglesigKind: "wpkh", + }) + if err != nil { + return err + } + _, err = r.lwkClient.loadWallet(ctx, &loadWalletRequest{ + Descriptor: descres.Descriptor, + WalletName: walletName, + }) + // 32011 is the error code for wallet already loaded + if err != nil && !strings.HasPrefix(err.Error(), "-32009") { + return err + } + return nil +} + // 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 SatPerKVByte, err error) { diff --git a/lwk/validator.go b/lwk/validator.go new file mode 100644 index 00000000..2ddc277f --- /dev/null +++ b/lwk/validator.go @@ -0,0 +1,21 @@ +package lwk + +// Validator is a generic interface for validating sub configurations. +type Validator interface { + // Validate returns an error if a particular configuration is invalid or + // insane. + validate() error +} + +// Validate accepts a variadic list of Validators and checks that each one +// passes its Validate method. An error is returned from the first Validator +// that fails. +func Validate(validators ...Validator) error { + for _, validator := range validators { + if err := validator.validate(); err != nil { + return err + } + } + + return nil +} From ff46495c153859a458de3f37c4f174731829e388 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 27 Mar 2024 08:56:50 +0900 Subject: [PATCH 06/29] test: add integration test for LWK The integration test for LWK is added to ensure that the swap functionality works as expected with the LWK and electrs support. The setup functions are implemented to create the necessary test environment, including lwk, Bitcoin and Liquid nodes, as well as two lightning nodes with the PeerSwap. This setup is crucial for testing the swap-in process in an environment that closely mimics production. also, added Electrs and LWK structs for testing framework. - Implement `Electrs` struct to manage electrs daemon processes - Implement `LWK` struct to manage LWK daemon processes - Provide constructors for both structs to initialize with dynamic ports and configurations - Include methods to run the processes and connect --- .github/workflows/ci.yml | 2 +- Makefile | 14 + electrum/tx_observer.go | 4 +- go.mod | 2 +- lwk/conf_builder.go | 6 +- lwk/conf_builder_test.go | 4 +- lwk/lwkwallet.go | 17 +- test/bitcoin_cln_test.go | 2 - test/lwk_cln_test.go | 996 +++++++++++++++++++++++++++++++++ test/lwk_lnd_test.go | 1128 ++++++++++++++++++++++++++++++++++++++ test/setup.go | 536 ++++++++++++++++++ test/testcases.go | 38 ++ test/utils.go | 2 +- testframework/config.go | 2 +- testframework/electrs.go | 58 ++ testframework/lwk.go | 35 ++ 16 files changed, 2818 insertions(+), 28 deletions(-) create mode 100644 test/lwk_cln_test.go create mode 100644 test/lwk_lnd_test.go create mode 100644 testframework/electrs.go create mode 100644 testframework/lwk.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 930764ca..45bd48ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - test-vector: [bitcoin-cln, bitcoin-lnd, liquid-cln, liquid-lnd, misc-integration] + test-vector: [bitcoin-cln, bitcoin-lnd, liquid-cln, liquid-lnd, misc-integration, lwk-cln, lwk-lnd] steps: - name: Checkout code diff --git a/Makefile b/Makefile index 46bc0e9c..eadd0d3f 100644 --- a/Makefile +++ b/Makefile @@ -127,6 +127,20 @@ test-liquid-lnd: test-bins ./test .PHONY: test-liquid-lnd +test-lwk-cln: test-bins + ${INTEGRATION_TEST_ENV} go test ${INTEGRATION_TEST_OPTS} \ + -run '^('\ + 'Test_ClnCln_LWK_SwapIn)'\ + ./test +.PHONY: test-lwk-cln + +test-lwk-lnd: test-bins + ${INTEGRATION_TEST_ENV} go test ${INTEGRATION_TEST_OPTS} \ + -run '^('\ + 'Test_LndLnd_LWK_SwapIn)'\ + ./test +.PHONY: test-lwk-lnd + test-misc-integration: test-bins ${INTEGRATION_TEST_ENV} go test ${INTEGRATION_TEST_OPTS} \ -run '^('\ diff --git a/electrum/tx_observer.go b/electrum/tx_observer.go index 1819a6b4..514dcee6 100644 --- a/electrum/tx_observer.go +++ b/electrum/tx_observer.go @@ -95,7 +95,7 @@ func (o *observeOpeningTX) Callback(ctx context.Context, currentHeight BlocKHeig if err != nil { return false, fmt.Errorf("failed to get raw transaction: %w", err) } - if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidConfs)) { + if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidConfs)-1) { return false, fmt.Errorf("not enough confirmations for opening transaction. txhash: %s.height: %d, current: %d", o.txID.String(), getHeight(hs, o.txID).Height(), currentHeight.Height()) } @@ -141,7 +141,7 @@ func (o *observeCSVTX) Callback(ctx context.Context, currentHeight BlocKHeight) if !(getHeight(hs, o.txID).Confirmed()) { return false, fmt.Errorf("the transaction is unconfirmed") } - if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidCsv)) { + if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidCsv-1)) { return false, fmt.Errorf("not enough confirmations for csv transaction. txhash: %s.height: %d, current: %d", o.txID.String(), getHeight(hs, o.txID).Height(), currentHeight.Height()) } diff --git a/go.mod b/go.mod index c40deb68..8c59c00c 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.13.0 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -147,7 +148,6 @@ require ( ) require ( - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/lightningnetwork/lnd/clock v1.1.0 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect github.com/lightningnetwork/lnd/kvdb v1.3.1 // indirect diff --git a/lwk/conf_builder.go b/lwk/conf_builder.go index b8b1bb16..75e3099e 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 = "blockstream.info:465" + electrumEndpoint = "tcp://blockstream.info:465" case NetworkRegtest: lwkEndpoint = "http://localhost:32112" - electrumEndpoint = "localhost:60401" + electrumEndpoint = "tcp://localhost:60401" default: // mainnet is the default port lwkEndpoint = "http://localhost:32110" - electrumEndpoint = "blockstream.info:995" + electrumEndpoint = "tcp://blockstream.info:995" } lwkURL, err := NewConfURL(lwkEndpoint) if err != nil { diff --git a/lwk/conf_builder_test.go b/lwk/conf_builder_test.go index 9886f8b9..81398223 100644 --- a/lwk/conf_builder_test.go +++ b/lwk/conf_builder_test.go @@ -26,7 +26,7 @@ func Test_confBuilder_DefaultConf(t *testing.T) { if c.GetLWKEndpoint() != "http://localhost:32111" { t.Fatalf("unexpected lwk endpoint: %v", c.GetLWKEndpoint()) } - if c.GetLiquidSwaps() != true { + if c.GetLiquidSwaps() { t.Fatalf("unexpected liquid swaps: %v", c.GetLiquidSwaps()) } if c.GetNetwork() != lwk.NetworkTestnet.String() { @@ -60,7 +60,7 @@ func Test_confBuilder_SetConfs(t *testing.T) { if c.GetLWKEndpoint() != "http://localhost:32111" { t.Fatalf("unexpected lwk endpoint: %v", c.GetLWKEndpoint()) } - if c.GetLiquidSwaps() != true { + if c.GetLiquidSwaps() { t.Fatalf("unexpected liquid swaps: %v", c.GetLiquidSwaps()) } if c.GetNetwork() != lwk.NetworkTestnet.String() { diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 227116f1..acc95f6d 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -12,7 +12,6 @@ import ( "github.com/elementsproject/peerswap/swap" "github.com/elementsproject/peerswap/wallet" "github.com/vulpemventures/go-elements/network" - "github.com/vulpemventures/go-elements/psetv2" ) // Satoshi represents a satoshi value. @@ -139,23 +138,11 @@ func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningPar if err != nil { return "", "", 0, err } - p, err := psetv2.NewPsetFromBase64(signed.Pset) + hex, err := r.electrumClient.GetRawTransaction(ctx, broadcasted.Txid) if err != nil { return "", "", 0, err } - err = psetv2.FinalizeAll(p) - if err != nil { - return "", "", 0, err - } - tx, err := psetv2.Extract(p) - if err != nil { - return "", "", 0, err - } - txhex, err := tx.ToHex() - if err != nil { - return "", "", 0, err - } - return broadcasted.Txid, txhex, SatPerKVByte(feerate), nil + return broadcasted.Txid, hex, 0, nil } // GetBalance returns the balance in sats diff --git a/test/bitcoin_cln_test.go b/test/bitcoin_cln_test.go index 0b3c09f5..07251459 100644 --- a/test/bitcoin_cln_test.go +++ b/test/bitcoin_cln_test.go @@ -170,7 +170,6 @@ func Test_ClnCln_Bitcoin_SwapIn(t *testing.T) { go func() { var response map[string]interface{} lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) - }() preimageClaimTest(t, params) }) @@ -723,7 +722,6 @@ func Test_ClnLnd_Bitcoin_SwapIn(t *testing.T) { }() csvClaimTest(t, params) }) - } func Test_ClnLnd_Bitcoin_SwapOut(t *testing.T) { diff --git a/test/lwk_cln_test.go b/test/lwk_cln_test.go new file mode 100644 index 00000000..69f67abb --- /dev/null +++ b/test/lwk_cln_test.go @@ -0,0 +1,996 @@ +package test + +import ( + "math" + "os" + "testing" + + "github.com/elementsproject/peerswap/clightning" + "github.com/elementsproject/peerswap/swap" + "github.com/stretchr/testify/require" +) + +func Test_ClnCln_LWK_SwapIn(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + coopClaimTest(t, params) + }) + + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + csvClaimTest(t, params) + }) +} + +func Test_ClnCln_LWK_SwapOut(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + coopClaimTest(t, params) + }) + + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + csvClaimTest(t, params) + }) +} + +func Test_ClnLnd_LWK_SwapIn(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_LND) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapd.DaemonProcess, + makerPeerswap: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + // We need to run this in a go routine as the Request call is blocking and sometimes does not return. + var response map[string]interface{} + err := lightningds[1].(*CLightningNodeWithLiquid).Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_LND) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapd.DaemonProcess, + makerPeerswap: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + // We need to run this in a go routine as the Request call is blocking and sometimes does not return. + var response map[string]interface{} + lightningds[1].(*CLightningNodeWithLiquid).Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + }() + coopClaimTest(t, params) + }) + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_LND) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapd.DaemonProcess, + makerPeerswap: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + // We need to run this in a go routine as the Request call is blocking and sometimes does not return. + var response map[string]interface{} + lightningds[1].(*CLightningNodeWithLiquid).Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + }() + csvClaimTest(t, params) + }) +} + +func Test_ClnLnd_LWK_SwapOut(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_CLN) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + makerPeerswap: peerswapd.DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + // We need to run this in a go routine as the Request call is blocking and sometimes does not return. + var response map[string]interface{} + lightningds[0].(*CLightningNodeWithLiquid).Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_CLN) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + makerPeerswap: peerswapd.DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + // We need to run this in a go routine as the Request call is blocking and sometimes does not return. + var response map[string]interface{} + lightningds[0].(*CLightningNodeWithLiquid).Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + }() + coopClaimTest(t, params) + }) + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_CLN) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + makerPeerswap: peerswapd.DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + // We need to run this in a go routine as the Request call is blocking and sometimes does not return. + var response map[string]interface{} + lightningds[0].(*CLightningNodeWithLiquid).Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + }() + csvClaimTest(t, params) + }) +} diff --git a/test/lwk_lnd_test.go b/test/lwk_lnd_test.go new file mode 100644 index 00000000..1c992ab0 --- /dev/null +++ b/test/lwk_lnd_test.go @@ -0,0 +1,1128 @@ +package test + +import ( + "context" + "math" + "os" + "testing" + + "github.com/elementsproject/peerswap/peerswaprpc" + "github.com/elementsproject/peerswap/swap" + "github.com/stretchr/testify/require" +) + +func Test_LndLnd_LWK_SwapIn(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapds, scid, electrsd, lwk := lndlndLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrsd.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapds[0].DaemonProcess, + makerPeerswap: peerswapds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapds[1].PeerswapClient.SwapIn(ctx, &peerswaprpc.SwapInRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapds, scid, electrsd, lwk := lndlndLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrsd.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapds[0].DaemonProcess, + makerPeerswap: peerswapds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapds[1].PeerswapClient.SwapIn(ctx, &peerswaprpc.SwapInRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + coopClaimTest(t, params) + }) + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapds, scid, electrsd, lwk := lndlndLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrsd.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapds[0].DaemonProcess, + makerPeerswap: peerswapds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapds[1].PeerswapClient.SwapIn(ctx, &peerswaprpc.SwapInRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + csvClaimTest(t, params) + }) +} + +func Test_LndLnd_LWK_SwapOut(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapds, scid, electrsd, lwk := lndlndLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrsd.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapds[0].DaemonProcess, + makerPeerswap: peerswapds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapds[0].PeerswapClient.SwapOut(ctx, &peerswaprpc.SwapOutRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapds, scid, electrsd, lwk := lndlndLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrsd.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapds[0].DaemonProcess, + makerPeerswap: peerswapds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapds[0].PeerswapClient.SwapOut(ctx, &peerswaprpc.SwapOutRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + coopClaimTest(t, params) + }) + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapds, scid, electrsd, lwk := lndlndLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[0].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapds[1].DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrsd.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapds[0].DaemonProcess, + makerPeerswap: peerswapds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapds[1].PeerswapClient.SwapOut(ctx, &peerswaprpc.SwapOutRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + csvClaimTest(t, params) + }) +} + +func Test_LndCln_LWK_SwapIn(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_CLN) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[1].(*LndNodeWithLiquid).ChanIdFromScid(scid) + if err != nil { + t.Fatalf("ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + makerPeerswap: peerswapd.DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapd.PeerswapClient.SwapIn(ctx, &peerswaprpc.SwapInRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_CLN) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[1].(*LndNodeWithLiquid).ChanIdFromScid(scid) + if err != nil { + t.Fatalf("ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + makerPeerswap: peerswapd.DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapd.PeerswapClient.SwapIn(ctx, &peerswaprpc.SwapInRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + coopClaimTest(t, params) + }) + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_CLN) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[1].(*LndNodeWithLiquid).ChanIdFromScid(scid) + if err != nil { + t.Fatalf("ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].(*CLightningNodeWithLiquid).DaemonProcess, + makerPeerswap: peerswapd.DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapd.PeerswapClient.SwapIn(ctx, &peerswaprpc.SwapInRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + csvClaimTest(t, params) + }) +} + +func Test_LndCln_LWK_SwapOut(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_LND) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].(*LndNodeWithLiquid).ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapd.DaemonProcess, + makerPeerswap: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapd.PeerswapClient.SwapOut(ctx, &peerswaprpc.SwapOutRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_LND) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].(*LndNodeWithLiquid).ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapd.DaemonProcess, + makerPeerswap: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapd.PeerswapClient.SwapOut(ctx, &peerswaprpc.SwapOutRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + coopClaimTest(t, params) + }) + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, peerswapd, scid, electrs, lwk := mixedLWKSetup(t, uint64(math.Pow10(9)), FUNDER_LND) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].(*LndNodeWithLiquid).DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: peerswapd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + lcid, err := lightningds[0].(*LndNodeWithLiquid).ChanIdFromScid(scid) + if err != nil { + t.Fatalf("lightingds[0].ChanIdFromScid() %v", err) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: peerswapd.DaemonProcess, + makerPeerswap: lightningds[1].(*CLightningNodeWithLiquid).DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + peerswapd.PeerswapClient.SwapOut(ctx, &peerswaprpc.SwapOutRequest{ + ChannelId: lcid, + SwapAmount: params.swapAmt, + Asset: asset, + }) + }() + csvClaimTest(t, params) + }) +} diff --git a/test/setup.go b/test/setup.go index 2f547e06..155ac042 100644 --- a/test/setup.go +++ b/test/setup.go @@ -800,3 +800,539 @@ func (n *LndNodeWithLiquid) GetBtcBalanceSat() (uint64, error) { } return r.SatAmount, nil } + +func clnclnLWKSetup(t *testing.T, fundAmt uint64) (*testframework.BitcoinNode, + *testframework.LiquidNode, []*CLightningNodeWithLiquid, string, + *testframework.Electrs, *testframework.LWK) { + /// Get PeerSwap plugin path and test dir + _, filename, _, _ := runtime.Caller(0) + pathToPlugin := filepath.Join(filename, "..", "..", "out", "test-builds", "peerswap") + testDir := t.TempDir() + + // Setup nodes (1 bitcoind, 1 liquidd, 2 lightningd) + bitcoind, err := testframework.NewBitcoinNode(testDir, 1) + if err != nil { + t.Fatalf("could not create bitcoind %v", err) + } + t.Cleanup(bitcoind.Kill) + + liquidd, err := testframework.NewLiquidNode(testDir, bitcoind, 1) + if err != nil { + t.Fatal("error creating liquidd node", err) + } + t.Cleanup(liquidd.Kill) + + electrsd, err := testframework.NewElectrs(testDir, 1, liquidd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(electrsd.Process.Kill) + + lwk, err := testframework.NewLWK(testDir, 1, electrsd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(lwk.Process.Kill) + + var lightningds []*testframework.CLightningNode + for i := 1; i <= 2; i++ { + lightningd, err := testframework.NewCLightningNode(testDir, bitcoind, i) + if err != nil { + t.Fatalf("could not create liquidd %v", err) + } + t.Cleanup(lightningd.Kill) + defer printFailedFiltered(t, lightningd.DaemonProcess) + + // Create policy file and accept all peers + err = os.MkdirAll(filepath.Join(lightningd.GetDataDir(), "peerswap"), os.ModePerm) + if err != nil { + t.Fatal("could not create dir", err) + } + err = os.WriteFile(filepath.Join(lightningd.GetDataDir(), "peerswap", "policy.conf"), []byte("accept_all_peers=1\n"), os.ModePerm) + if err != nil { + t.Fatal("could not create policy file", err) + } + + // Set wallet name + walletName := fmt.Sprintf("swap%d", i) + + // Create config file + fileConf := struct { + LWK struct { + SignerName string + WalletName string + LWKEndpoint string + ElectrumEndpoint string + Network string + LiquidSwaps bool + } + }{ + LWK: struct { + SignerName string + WalletName string + LWKEndpoint string + ElectrumEndpoint string + Network string + LiquidSwaps bool + }{ + WalletName: walletName, + SignerName: walletName + "-" + "signer", + LiquidSwaps: true, + LWKEndpoint: lwk.RPCURL.String(), + ElectrumEndpoint: electrsd.RPCURL.String(), + Network: "liquid-regtest", + }, + } + data, err := toml.Marshal(fileConf) + require.NoError(t, err) + + configPath := filepath.Join(lightningd.GetDataDir(), "peerswap", "peerswap.conf") + os.WriteFile( + configPath, + data, + os.ModePerm, + ) + + // Use lightningd with --developer turned on + lightningd.WithCmd("lightningd") + + // Add plugin to cmd line options + lightningd.AppendCmdLine([]string{ + "--dev-bitcoind-poll=1", + "--dev-fast-gossip", + "--large-channels", + fmt.Sprint("--plugin=", pathToPlugin), + }) + + lightningds = append(lightningds, lightningd) + } + + // Start nodes + err = bitcoind.Run(true) + if err != nil { + t.Fatalf("bitcoind.Run() got err %v", err) + } + + err = liquidd.Run(true) + if err != nil { + t.Fatalf("Run() got err %v", err) + } + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, testframework.TIMEOUT) + defer cancel() + require.NoError(t, electrsd.Run(ctx)) + lwk.Process.Run() + + for _, lightningd := range lightningds { + err = lightningd.Run(true, true) + if err != nil { + t.Fatalf("lightningd.Run() got err %v", err) + } + err = lightningd.WaitForLog("peerswap initialized", testframework.TIMEOUT) + if err != nil { + t.Fatalf("lightningd.WaitForLog() got err %v", err) + } + } + + // Give liquid funds to nodes to have something to swap. + for _, lightningd := range lightningds { + var result clightning.GetAddressResponse + require.NoError(t, lightningd.Rpc.Request(&clightning.LiquidGetAddress{}, &result)) + _, err = liquidd.Rpc.Call("sendtoaddress", result.LiquidAddress, 10., "", "", false, false, 1, "UNSET") + require.NoError(t, err) + _ = liquidd.GenerateBlocks(20) + require.NoError(t, + testframework.WaitFor(func() bool { + var balance clightning.GetBalanceResponse + require.NoError(t, lightningd.Rpc.Request(&clightning.LiquidGetBalance{}, &balance)) + return balance.LiquidBalance >= 1000000000 + }, testframework.TIMEOUT)) + } + + // Lock txs. + _, err = liquidd.Rpc.Call("generatetoaddress", 1, testframework.LBTC_BURN) + require.NoError(t, err) + require.NoError(t, liquidd.GenerateBlocks(20)) + + // Setup channel ([0] fundAmt(10^7) ---- 0 [1]). + scid, err := lightningds[0].OpenChannel(lightningds[1], fundAmt, 0, true, true, true) + if err != nil { + t.Fatalf("lightingds[0].OpenChannel() %v", err) + } + + // Sync peer polling + var result interface{} + err = lightningds[0].Rpc.Request(&clightning.ReloadPolicyFile{}, &result) + if err != nil { + t.Fatalf("ListPeers %v", err) + } + err = lightningds[1].Rpc.Request(&clightning.ReloadPolicyFile{}, &result) + if err != nil { + t.Fatalf("ListPeers %v", err) + } + + syncPoll(&clnPollableNode{lightningds[0]}, &clnPollableNode{lightningds[1]}) + + return bitcoind, liquidd, []*CLightningNodeWithLiquid{{lightningds[0]}, {lightningds[1]}}, scid, electrsd, lwk +} + +func lndlndLWKSetup(t *testing.T, fundAmt uint64) (*testframework.BitcoinNode, + *testframework.LiquidNode, []*LndNodeWithLiquid, []*PeerSwapd, string, + *testframework.Electrs, *testframework.LWK) { + // Get PeerSwap plugin path and test dir + _, filename, _, _ := runtime.Caller(0) + pathToPlugin := filepath.Join(filename, "..", "..", "out", "test-builds", "peerswapd") + testDir := t.TempDir() + + // Setup nodes (1 bitcoind, 1 liquidd, 2 lightningd, 2 peerswapd) + bitcoind, err := testframework.NewBitcoinNode(testDir, 1) + if err != nil { + t.Fatalf("could not create bitcoind %v", err) + } + t.Cleanup(bitcoind.Kill) + + liquidd, err := testframework.NewLiquidNode(testDir, bitcoind, 1) + if err != nil { + t.Fatal("error creating liquidd node", err) + } + t.Cleanup(liquidd.Kill) + electrsd, err := testframework.NewElectrs(testDir, 1, liquidd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(electrsd.Process.Kill) + + lwk, err := testframework.NewLWK(testDir, 1, electrsd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(lwk.Process.Kill) + + var lightningds []*testframework.LndNode + for i := 1; i <= 2; i++ { + extraConfig := map[string]string{"protocol.wumbo-channels": "true"} + lightningd, err := testframework.NewLndNode(testDir, bitcoind, i, extraConfig) + if err != nil { + t.Fatalf("could not create liquidd %v", err) + } + t.Cleanup(lightningd.Kill) + defer printFailedFiltered(t, lightningd.DaemonProcess) + + lightningds = append(lightningds, lightningd) + } + + var peerswapds []*PeerSwapd + for i, lightningd := range lightningds { + // Set wallet name + walletName := fmt.Sprintf("swap%d", i) + extraConfig := map[string]string{ + "lwk.signername": walletName + "-" + "signer", + "lwk.walletname": walletName, + "lwk.lwkendpoint": lwk.RPCURL.String(), + "lwk.elementsendpoint": electrsd.RPCURL.String(), + "lwk.network": "liquid-regtest", + "lwk.liquidswaps": "true", + } + + peerswapd, err := NewPeerSwapd(testDir, pathToPlugin, &LndConfig{LndHost: fmt.Sprintf("localhost:%d", lightningd.RpcPort), TlsPath: lightningd.TlsPath, MacaroonPath: lightningd.MacaroonPath}, extraConfig, i+1) + if err != nil { + t.Fatalf("could not create peerswapd %v", err) + } + t.Cleanup(peerswapd.Kill) + + // Create policy file and accept all peers + err = os.WriteFile(filepath.Join(peerswapd.DataDir, "..", "policy.conf"), []byte("accept_all_peers=1\n"), os.ModePerm) + if err != nil { + t.Fatal("could not create policy file", err) + } + + peerswapds = append(peerswapds, peerswapd) + } + + // Start nodes + err = bitcoind.Run(true) + if err != nil { + t.Fatalf("bitcoind.Run() got err %v", err) + } + + err = liquidd.Run(true) + if err != nil { + t.Fatalf("Run() got err %v", err) + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, testframework.TIMEOUT) + defer cancel() + require.NoError(t, electrsd.Run(ctx)) + lwk.Process.Run() + + for _, lightningd := range lightningds { + err = lightningd.Run(true, true) + if err != nil { + t.Fatalf("lightningd.Run() got err %v", err) + } + } + + for _, peerswapd := range peerswapds { + err = peerswapd.Run(true) + if err != nil { + t.Fatalf("peerswapd.Run() got err %v", err) + } + err = peerswapd.WaitForLog("peerswapd grpc listening on", testframework.TIMEOUT) + if err != nil { + t.Fatalf("peerswapd.WaitForLog() got err %v", err) + } + } + + // Give liquid funds to nodes to have something to swap. + for _, peerswapd := range peerswapds { + r, err := peerswapd.PeerswapClient.LiquidGetAddress(context.Background(), &peerswaprpc.GetAddressRequest{}) + require.NoError(t, err) + _, err = liquidd.Rpc.Call("sendtoaddress", r.Address, 10., "", "", false, false, 1, "UNSET") + require.NoError(t, err) + _ = liquidd.GenerateBlocks(20) + require.NoError(t, + testframework.WaitFor(func() bool { + b, err := peerswapd.PeerswapClient.LiquidGetBalance(ctx, &peerswaprpc.GetBalanceRequest{}) + require.NoError(t, err) + return b.GetSatAmount() >= 1000000000 + }, testframework.TIMEOUT)) + } + + // Lock txs. + _, err = liquidd.Rpc.Call("generatetoaddress", 1, testframework.LBTC_BURN) + require.NoError(t, err) + + // Setup channel ([0] fundAmt(10^7) ---- 0 [1]) + scid, err := lightningds[0].OpenChannel(lightningds[1], fundAmt, 0, true, true, true) + if err != nil { + t.Fatalf("lightingds[0].OpenChannel() %v", err) + } + + // Give btc to node [1] in order to initiate swap-in. + _, err = lightningds[1].FundWallet(10*fundAmt, true) + if err != nil { + t.Fatalf("lightningds[1].FundWallet() %v", err) + } + + syncPoll(&peerswapPollableNode{peerswapds[0], lightningds[0].Id()}, &peerswapPollableNode{peerswapds[1], lightningds[1].Id()}) + return bitcoind, liquidd, []*LndNodeWithLiquid{{lightningds[0], peerswapds[0]}, {lightningds[1], peerswapds[1]}}, peerswapds, scid, electrsd, lwk +} + +func mixedLWKSetup(t *testing.T, fundAmt uint64, funder fundingNode) (*testframework.BitcoinNode, + *testframework.LiquidNode, []testframework.LightningNode, *PeerSwapd, string, + *testframework.Electrs, *testframework.LWK) { + // Get PeerSwap plugin path and test dir + _, filename, _, _ := runtime.Caller(0) + peerswapdPath := filepath.Join(filename, "..", "..", "out", "test-builds", "peerswapd") + peerswapPluginPath := filepath.Join(filename, "..", "..", "out", "test-builds", "peerswap") + testDir := t.TempDir() + + // Setup nodes (1 bitcoind, 1 liquid, 1 cln, 1 lnd, 1 peerswapd) + bitcoind, err := testframework.NewBitcoinNode(testDir, 1) + if err != nil { + t.Fatalf("could not create bitcoind %v", err) + } + t.Cleanup(bitcoind.Kill) + + liquidd, err := testframework.NewLiquidNode(testDir, bitcoind, 1) + if err != nil { + t.Fatal("error creating liquidd node", err) + } + t.Cleanup(liquidd.Kill) + electrsd, err := testframework.NewElectrs(testDir, 1, liquidd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(electrsd.Process.Kill) + + lwk, err := testframework.NewLWK(testDir, 1, electrsd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(lwk.Process.Kill) + + // cln + cln, err := testframework.NewCLightningNode(testDir, bitcoind, 1) + if err != nil { + t.Fatalf("could not create cln %v", err) + } + t.Cleanup(cln.Kill) + defer printFailedFiltered(t, cln.DaemonProcess) + + // Create policy file and accept all peers + err = os.MkdirAll(filepath.Join(cln.GetDataDir(), "peerswap"), os.ModePerm) + if err != nil { + t.Fatal("could not create dir", err) + } + err = os.WriteFile(filepath.Join(cln.GetDataDir(), "peerswap", "policy.conf"), []byte("accept_all_peers=1\n"), os.ModePerm) + if err != nil { + t.Fatal("could not create policy file", err) + } + + walletNameCln := "cln-test-wallet-1" + + // Create config file + fileConf := struct { + LWK struct { + SignerName string + WalletName string + LWKEndpoint string + ElectrumEndpoint string + Network string + LiquidSwaps bool + } + }{ + LWK: struct { + SignerName string + WalletName string + LWKEndpoint string + ElectrumEndpoint string + Network string + LiquidSwaps bool + }{ + WalletName: walletNameCln, + SignerName: walletNameCln + "-" + "signer", + LiquidSwaps: true, + LWKEndpoint: lwk.RPCURL.String(), + ElectrumEndpoint: electrsd.RPCURL.String(), + Network: "liquid-regtest", + }, + } + data, err := toml.Marshal(fileConf) + require.NoError(t, err) + + configPath := filepath.Join(cln.GetDataDir(), "peerswap", "peerswap.conf") + os.WriteFile( + configPath, + data, + os.ModePerm, + ) + + // Use lightningd with --developer turned on + cln.WithCmd("lightningd") + + // Add plugin to cmd line options + cln.AppendCmdLine([]string{ + "--dev-bitcoind-poll=1", + "--dev-fast-gossip", + "--large-channels", + fmt.Sprint("--plugin=", peerswapPluginPath), + }) + + // lnd + extraConfigLnd := map[string]string{"protocol.wumbo-channels": "true"} + lnd, err := testframework.NewLndNode(testDir, bitcoind, 1, extraConfigLnd) + if err != nil { + t.Fatalf("could not create lnd %v", err) + } + t.Cleanup(lnd.Kill) + + walletNameLnd := "lnd-test-wallet-1" + // peerswapd + extraConfig := map[string]string{ + "lwk.signername": walletNameLnd + "-" + "signer", + "lwk.walletname": walletNameLnd, + "lwk.lwkendpoint": lwk.RPCURL.String(), + "lwk.elementsendpoint": electrsd.RPCURL.String(), + "lwk.network": "liquid-regtest", + "lwk.liquidswaps": "true", + } + + peerswapd, err := NewPeerSwapd(testDir, peerswapdPath, &LndConfig{LndHost: fmt.Sprintf("localhost:%d", lnd.RpcPort), TlsPath: lnd.TlsPath, MacaroonPath: lnd.MacaroonPath}, extraConfig, 1) + if err != nil { + t.Fatalf("could not create peerswapd %v", err) + } + t.Cleanup(peerswapd.Kill) + defer printFailed(t, peerswapd.DaemonProcess) + + // Start nodes + err = bitcoind.Run(true) + if err != nil { + t.Fatalf("bitcoind.Run() got err %v", err) + } + + err = liquidd.Run(true) + if err != nil { + t.Fatalf("Run() got err %v", err) + } + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, testframework.TIMEOUT) + defer cancel() + require.NoError(t, electrsd.Run(ctx)) + lwk.Process.Run() + + err = cln.Run(true, true) + if err != nil { + t.Fatalf("cln.Run() got err %v", err) + } + err = cln.WaitForLog("peerswap initialized", testframework.TIMEOUT) + if err != nil { + t.Fatalf("cln.WaitForLog() got err %v", err) + } + + err = lnd.Run(true, true) + if err != nil { + t.Fatalf("lnd.Run() got err %v", err) + } + + err = peerswapd.Run(true) + if err != nil { + t.Fatalf("peerswapd.Run() got err %v", err) + } + err = peerswapd.WaitForLog("peerswapd grpc listening on", testframework.TIMEOUT) + if err != nil { + t.Fatalf("peerswapd.WaitForLog() got err %v", err) + } + + // Give liquid funds to nodes to have something to swap. + var lar clightning.GetAddressResponse + cln.Rpc.Request(&clightning.LiquidGetAddress{}, &lar) + _, err = liquidd.Rpc.Call("sendtoaddress", lar.LiquidAddress, 10., "", "", false, false, 1, "UNSET") + require.NoError(t, err) + _ = liquidd.GenerateBlocks(20) + require.NoError(t, + testframework.WaitFor(func() bool { + var balance clightning.GetBalanceResponse + require.NoError(t, cln.Rpc.Request(&clightning.LiquidGetBalance{}, &balance)) + return balance.LiquidBalance >= 1000000000 + }, testframework.TIMEOUT)) + + r, err := peerswapd.PeerswapClient.LiquidGetAddress(context.Background(), &peerswaprpc.GetAddressRequest{}) + require.NoError(t, err) + _, err = liquidd.Rpc.Call("sendtoaddress", r.Address, 10., "", "", false, false, 1, "UNSET") + require.NoError(t, err) + _ = liquidd.GenerateBlocks(20) + require.NoError(t, + testframework.WaitFor(func() bool { + b, err := peerswapd.PeerswapClient.LiquidGetBalance(context.Background(), &peerswaprpc.GetBalanceRequest{}) + require.NoError(t, err) + return b.GetSatAmount() >= 1000000000 + }, testframework.TIMEOUT)) + + // Lock txs. + _, err = liquidd.Rpc.Call("generatetoaddress", 1, testframework.LBTC_BURN) + require.NoError(t, err) + + var lightningds []testframework.LightningNode + switch funder { + case FUNDER_CLN: + lightningds = append(lightningds, &CLightningNodeWithLiquid{cln}) + lightningds = append(lightningds, &LndNodeWithLiquid{lnd, peerswapd}) + + case FUNDER_LND: + lightningds = append(lightningds, &LndNodeWithLiquid{lnd, peerswapd}) + lightningds = append(lightningds, &CLightningNodeWithLiquid{cln}) + default: + t.Fatalf("unknown fundingNode %s", funder) + } + + // Setup channel ([0] fundAmt(10^7) ---- 0 [1]) + scid, err := lightningds[0].OpenChannel(lightningds[1], fundAmt, 0, true, true, true) + if err != nil { + t.Fatalf("cln.OpenChannel() %v", err) + } + + syncPoll(&clnPollableNode{cln}, &peerswapPollableNode{peerswapd, lnd.Id()}) + return bitcoind, liquidd, lightningds, peerswapd, scid, electrsd, lwk +} diff --git a/test/testcases.go b/test/testcases.go index 578e3c66..4559d667 100644 --- a/test/testcases.go +++ b/test/testcases.go @@ -106,6 +106,16 @@ func coopClaimTest(t *testing.T, params *testParams) { // Check no invoice was paid. testframework.RequireWaitForChannelBalance(t, params.takerNode, params.scid, float64(setTakerFunds), 1., testframework.TIMEOUT) + // Wait for balance change + require.NoError( + testframework.WaitFor(func() bool { + b, err2 := params.makerNode.GetBtcBalanceSat() + if err2 != nil { + return false + } + return b != params.origMakerWallet-commitFee-params.swapAmt + }, testframework.TIMEOUT)) + // Check Wallet balance. // Expect: // - [0] before @@ -182,6 +192,24 @@ func preimageClaimTest(t *testing.T, params *testParams) { t.Fatal("unknown role") } + // Wait for balance change + require.NoError( + testframework.WaitFor(func() bool { + b, err2 := params.takerNode.GetBtcBalanceSat() + if err2 != nil { + return false + } + return b != params.origTakerWallet + }, testframework.TIMEOUT)) + require.NoError( + testframework.WaitFor(func() bool { + b, err2 := params.makerNode.GetBtcBalanceSat() + if err2 != nil { + return false + } + return b != params.origMakerWallet + }, testframework.TIMEOUT)) + // Check Wallet balance. // Expect: (WITHOUT PREMIUM) // - taker -> before - claim_fee + swapamt @@ -286,6 +314,16 @@ func csvClaimTest(t *testing.T, params *testParams) { // Check channel and wallet balance require.True(testframework.AssertWaitForChannelBalance(t, params.makerNode, params.scid, float64(params.origMakerBalance+premium), 1., testframework.TIMEOUT)) + // Wait for balance change + require.NoError( + testframework.WaitFor(func() bool { + b, err2 := params.makerNode.GetBtcBalanceSat() + if err2 != nil { + return false + } + return b != params.origMakerWallet-commitFee-params.swapAmt + }, testframework.TIMEOUT)) + balance, err := params.makerNode.GetBtcBalanceSat() require.NoError(err) require.InDelta(params.origMakerWallet-commitFee-claimFee, balance, 1., "expected %d, got %d", diff --git a/test/utils.go b/test/utils.go index 0e7ad169..8708b1a5 100644 --- a/test/utils.go +++ b/test/utils.go @@ -63,7 +63,7 @@ func printFailedFiltered(t *testing.T, process *testframework.DaemonProcess) { tailableProcess{ p: process, filter: filter, - lines: defaultLines, + lines: 3000, }, ) } diff --git a/testframework/config.go b/testframework/config.go index 246a628c..c534e857 100644 --- a/testframework/config.go +++ b/testframework/config.go @@ -14,7 +14,7 @@ func setTimeout() time.Duration { if os.Getenv("SLOW_MACHINE") == "1" { return 420 * time.Second } - return 180 * time.Second + return 150 * time.Second } func WriteConfig(filename string, config map[string]string, regtestConfig map[string]string, sectionName string) { diff --git a/testframework/electrs.go b/testframework/electrs.go new file mode 100644 index 00000000..f617b918 --- /dev/null +++ b/testframework/electrs.go @@ -0,0 +1,58 @@ +package testframework + +import ( + "context" + "fmt" + "net/url" + + "github.com/cenkalti/backoff/v4" + "github.com/checksum0/go-electrum/electrum" +) + +type Electrs struct { + Process *DaemonProcess + RPCURL *url.URL +} + +func NewElectrs(testDir string, id int, elements *LiquidNode) (*Electrs, error) { + rpcPort, err := GetFreePort() + if err != nil { + return nil, err + } + u, err := url.Parse(fmt.Sprintf("tcp://127.0.0.1:%d", rpcPort)) + if err != nil { + return nil, err + } + monitoringPort, err := GetFreePort() + if err != nil { + return nil, err + } + cmdLine := []string{ + "electrs", + "-v", + "--network=liquidregtest", + fmt.Sprintf("--daemon-rpc-addr=127.0.0.1:%d", elements.RpcPort), + fmt.Sprintf("--electrum-rpc-addr=%s", u.Host), + fmt.Sprintf("--cookie=%s", elements.RpcUser+":"+elements.RpcPassword), + fmt.Sprintf("--daemon-dir=%s", elements.DataDir), + fmt.Sprintf("--monitoring-addr=127.0.0.1:%d", monitoringPort), + fmt.Sprintf("--db-dir=%s", testDir), + + "--jsonrpc-import", + } + return &Electrs{ + Process: NewDaemonProcess(cmdLine, fmt.Sprintf("electrs-%d", id)), + RPCURL: u, + }, nil +} + +func (e *Electrs) Run(ctx context.Context) error { + e.Process.Run() + return backoff.Retry(func() error { + ec, err := electrum.NewClientTCP(ctx, e.RPCURL.Host) + if err != nil { + return err + } + return ec.Ping(ctx) + }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5)) +} diff --git a/testframework/lwk.go b/testframework/lwk.go new file mode 100644 index 00000000..e3c79194 --- /dev/null +++ b/testframework/lwk.go @@ -0,0 +1,35 @@ +package testframework + +import ( + "fmt" + "net/url" +) + +type LWK struct { + Process *DaemonProcess + RPCURL *url.URL +} + +func NewLWK(testDir string, id int, electrs *Electrs) (*LWK, error) { + rpcPort, err := GetFreePort() + if err != nil { + return nil, err + } + u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", rpcPort)) + if err != nil { + return nil, err + } + cmdLine := []string{ + "lwk_cli", + "--network=regtest", + fmt.Sprintf("--addr=%s", u.Host), + "server", + "start", + fmt.Sprintf("--electrum-url=%s", electrs.RPCURL.Host), + fmt.Sprintf("--datadir=%s", testDir), + } + return &LWK{ + Process: NewDaemonProcess(cmdLine, fmt.Sprintf("lwk-%d", id)), + RPCURL: u, + }, nil +} From 8ad9a7a07ee3c060e62037bc5fb9634fdf364bed Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 27 Mar 2024 08:57:15 +0900 Subject: [PATCH 07/29] doc: add LWK setup guide The doc introduce configuration instructions for a new wallet kit (LWK) to support L-BTC swaps, providing users with additional options and flexibility for their swap setups. --- README.md | 2 ++ docs/setup_cln.md | 8 ++++++ docs/setup_lnd.md | 12 +++++++++ docs/setup_lwk.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 docs/setup_lwk.md diff --git a/README.md b/README.md index 9d916ed6..6aeaedda 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ To run PeerSwap as a standalone daemon with LND, see the [LND setup guide](./doc To run Elements for L-BTC swaps, see the [Elements setup guide](./docs/setup_elementsd.md). +To run LWK for L-BTC swaps, see the [LWK setup guide](./docs/setup_lwk.md). + > **Note** > Most of the benefits of PeerSwap come from using L-BTC. Swaps using L-BTC are more private, faster, and avoid the mainchain blockchain during high fee environments that can make swaps uneconomical. diff --git a/docs/setup_cln.md b/docs/setup_cln.md index 1f4a5e9f..452b0511 100644 --- a/docs/setup_cln.md +++ b/docs/setup_cln.md @@ -62,6 +62,7 @@ cookiefilepath="/path/to/auth/.cookie" ## If set this will be used for authentic bitcoinswaps=true ## If set to false, BTC mainchain swaps are disabled # Liquid section +# Select either Liquid or LWK # Liquid rpc connection settings. [Liquid] rpcuser="user" @@ -71,6 +72,13 @@ rpcport=1234 rpcpasswordfile="/path/to/auth/.cookie" ## If set this will be used for authentication rpcwallet="swap-wallet" ## (default: peerswap) liquidswaps=true ## If set to false, L-BTC swaps are disabled + +# LWK section +# LWK rpc connection settings. +[LWK] +signername=signername +walletname=walletname +liquidswaps=true ## If set to false, L-BTC swaps are disabled ``` In order to check if your daemon is setup correctly run diff --git a/docs/setup_lnd.md b/docs/setup_lnd.md index 371f88ae..e0774556 100644 --- a/docs/setup_lnd.md +++ b/docs/setup_lnd.md @@ -76,6 +76,18 @@ elementsd.rpcwallet=peerswap EOF ``` +LWK BTC and L-BTC swaps config. Replace the RPC parameters as needed: +```bash +cat < ~/.peerswap/peerswap.conf +lnd.tlscertpath=/home//.lnd/tls.cert +lnd.macaroonpath=/home//.lnd/data/chain/bitcoin/mainnet/admin.macaroon +lwk.signername=signername +lwk.walletname=walletname +lwk.Network=liquid +lwk.liquidswaps=true +EOF +``` + ### Policy On first startup of the plugin a policy file will be generated (default path: `~/.peerswap/policy.conf`) in which trusted nodes will be specified. diff --git a/docs/setup_lwk.md b/docs/setup_lwk.md new file mode 100644 index 00000000..f94235a8 --- /dev/null +++ b/docs/setup_lwk.md @@ -0,0 +1,68 @@ +# Setup `lwk` + +**[Liquid Wallet Kit](https://github.com/Blockstream/lwk/tree/master)** is a collection of Rust crates for [Liquid](https://liquid.net) Wallets and is used for PeerSwap L-BTC swaps. +To set up `lwk` for PeerSwap, follow the steps here. +lwk is currently under development and changes are being made. +**peerswap has been tested only with [cli_0.3.0](https://github.com/Blockstream/lwk/tree/cli_0.3.0)**. + +## wallet +peerswap assumes a wallet with blinding-key set in singlesig to lwk. + +```sh +MNEMONIC=$(lwk_cli signer generate | jq -r .mnemonic) +lwk_cli signer load-software --mnemonic "$MNEMONIC" --signer +DESCRIPTOR=$(lwk_cli signer singlesig-desc --signer --descriptor-blinding-key slip77 --kind wpkh | jq -r .descriptor) +lwk_cli wallet load --wallet -d "$DESCRIPTOR" +``` + + +Below is an example of an appropriate descriptor. +Confidential Transactions with [SLIP-0077](https://github.com/satoshilabs/slips/blob/master/slip-0077.md), P2WPKH output with the specified xpub. +```sh +"ct(slip77(220b6575205a476aac5a8c09f497ab084c13c269a7345846e617698f9beda171),elwpkh([4cd32cc8/84h/1h/0h]tpubDDbFo41vfUWdQMSjEjYVBNgEamvzpJWWqLspuDvStJyaCXC1EKxGyvABFCbax3k5adihtmWakYokMMWV67rZMjLjSuMnHSxKmZS92gKwbNw/<0;1>/*))" +``` + +## server +peerswap uses lwk's json rpc, so you need to start lwk's server. +Follow [lwk's document](https://github.com/Blockstream/lwk) to start the server. + +```sh +lwk_cli server start +``` + +## electrum +peerswap uses [esplora-electrs](https://github.com/Blockstream/electrs) to communicate with the luqiud chain. +By default, peerswap connects to `blockstream.info:995` used by lwk, so no configuration is needed. + +If you want to use your own chain, follow the instructions in [esplora-electrs](https://github.com/Blockstream/electrs) to start Electrum JSON-RPC server. + +## config file +The following settings are available +* wallet name +* signer name +* lwk endpoint : lwk jsonrpc endpoint +* electrumEndpoint : electrum JSON-RPC serverのendpoint +* network : `liquid`、`liquid-testnet`.`"liquid-regtest` +* liquidSwaps : `true` if used + +Set up in INI (.ini) File Format for lnd and in toml format for cln + +### example +Example configuration in lnd +```sh +lwk.signername=signername +lwk.walletname=walletname +lwk.lwkendpoint=http://localhost:32110 +lwk.network=liquid +lwk.liquidswaps=true +``` + +Example configuration in cln +```sh +[LWK] +signername="signername" +walletname="walletname" +lwkendpoint="http://localhost:32110" +network="liquid" +liquidswaps=true +``` \ No newline at end of file From cdd89ea4af39c78756bd6fc9266ef9a249f9e44d Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 1 May 2024 09:08:12 +0900 Subject: [PATCH 08/29] nix: add lwk and blockstream-electrs packages restructure nixpkgs imports and package references. The changes introduce two new packages (`lwk` and `blockstream-electrs`). --- packages.nix | 65 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/packages.nix b/packages.nix index 0953ef19..e37e3c91 100644 --- a/packages.nix +++ b/packages.nix @@ -1,32 +1,47 @@ let -# Pinning to revision f54322490f509985fa8be4ac9304f368bd8ab924 -# - cln v24.02.1 -# - lnd v0.17.4-beta -# - bitcoin v26.0 -# - elements v23.2.1 + fetchNixpkgs = rev: fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; + # Pinning to revision f54322490f509985fa8be4ac9304f368bd8ab924 + # - cln v24.02.1 + # - lnd v0.17.4-beta + # - bitcoin v26.0 + # - elements v23.2.1 + rev1 = "f54322490f509985fa8be4ac9304f368bd8ab924"; + nixpkgs1 = fetchNixpkgs rev1; + pkgs1 = import nixpkgs1 {}; -rev = "f54322490f509985fa8be4ac9304f368bd8ab924"; -nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; -pkgs = import nixpkgs {}; + # Override priority for bitcoin as /bin/bitcoin_test will + # confilict with /bin/bitcoin_test from elementsd. + bitcoind = (pkgs1.bitcoind.overrideAttrs (attrs: { + meta = attrs.meta or {} // { + priority = 0; + }; + })); + # lwk: init at 0.3.0 #292522 + # https://github.com/NixOS/nixpkgs/pull/292522/commits/2b3750792b2e4b52f472b6e6d88a6b02b6536c43 + rev2 = "2b3750792b2e4b52f472b6e6d88a6b02b6536c43"; + nixpkgs2 = fetchNixpkgs rev2; + pkgs2 = import nixpkgs2 {}; + # blockstream-electrs: init at 0.4.1 #299761 + # https://github.com/NixOS/nixpkgs/pull/299761/commits/680d27ad847801af781e0a99e4b87ed73965c69a + rev3 = "680d27ad847801af781e0a99e4b87ed73965c69a"; + nixpkgs3 = fetchNixpkgs rev3; + pkgs3 = import nixpkgs3 {}; + blockstream-electrs = pkgs3.blockstream-electrs.overrideAttrs (oldAttrs: { + cargoBuildFlags = [ "--features liquid" "--bin electrs" ]; + }); -# Override priority for bitcoin as /bin/bitcoin_test will -# confilict with /bin/bitcoin_test from elementsd. -bitcoind = (pkgs.bitcoind.overrideAttrs (attrs: { - meta = attrs.meta or {} // { - priority = 0; - }; -})); - -in with pkgs; +in { execs = { - clightning = clightning; + clightning = pkgs1.clightning; bitcoind = bitcoind; - elementsd = elementsd; - mermaid = nodePackages.mermaid-cli; - lnd = lnd; - }; - testpkgs = [ go bitcoind elementsd lnd ]; - devpkgs = [ go_1_22 gotools bitcoind elementsd clightning lnd ]; + elementsd = pkgs1.elementsd; + mermaid = pkgs1.nodePackages.mermaid-cli; + lnd = pkgs1.lnd; + lwk = pkgs2.lwk; + electrs = blockstream-electrs; -} \ No newline at end of file + }; + testpkgs = [ pkgs1.go pkgs1.bitcoind pkgs1.elementsd pkgs1.lnd pkgs2.lwk blockstream-electrs]; + devpkgs = [ pkgs1.go_1_22 pkgs1.gotools pkgs1.bitcoind pkgs1.elementsd pkgs1.clightning pkgs1.lnd pkgs2.lwk blockstream-electrs]; +} From 3fe81f69110b1cf0e902a3525ecb9044edae2959 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 2 May 2024 15:08:46 +0900 Subject: [PATCH 09/29] wip: claim_coop case --- lwk/lwkwallet.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index acc95f6d..0bb66124 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -18,10 +18,12 @@ import ( type Satoshi = uint64 // SatPerKVByte represents a fee rate in sat/kb. -type SatPerKVByte = uint64 +type SatPerKVByte = float64 const ( - minimumSatPerByte = 200 + minimumSatPerByte SatPerKVByte = 0.1 + // 1 kb = 1000 bytes + kb float64 = 1000 ) // LWKRpcWallet uses the elementsd rpc wallet @@ -108,9 +110,9 @@ 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 SatPerKVByte, err error) { + asset []byte) (txid, rawTx string, fee Satoshi, err error) { ctx := context.Background() - feerate := float64(r.getFeePerKb(ctx)) + feerate := float64(r.getFeePerKb(ctx)) * kb fundedTx, err := r.lwkClient.send(ctx, &sendRequest{ Addressees: []*unvalidatedAddressee{ { @@ -212,8 +214,7 @@ func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { func (r *LWKRpcWallet) getFeePerKb(ctx context.Context) SatPerKVByte { feeBTCPerKb, err := r.electrumClient.GetFee(ctx, wallet.LiquidTargetBlocks) - // convert to sat per byte - satPerByte := uint64(float64(feeBTCPerKb) * math.Pow10(int(8))) + satPerByte := float64(feeBTCPerKb) * math.Pow10(int(8)) / kb if satPerByte < minimumSatPerByte { satPerByte = minimumSatPerByte } @@ -226,8 +227,8 @@ func (r *LWKRpcWallet) getFeePerKb(ctx context.Context) SatPerKVByte { func (r *LWKRpcWallet) GetFee(txSize int64) (Satoshi, error) { ctx := context.Background() // assume largest witness - fee := r.getFeePerKb(ctx) * uint64(txSize) - return fee, nil + fee := r.getFeePerKb(ctx) * float64(txSize) + return Satoshi(fee), nil } func (r *LWKRpcWallet) SetLabel(txID, address, label string) error { From 383a1439a4631b877076102d9b2535fda8065a5f Mon Sep 17 00:00:00 2001 From: bruwbird Date: Fri, 10 May 2024 11:40:33 +0900 Subject: [PATCH 10/29] txwather: wait for initial block header sub wait for initial block header subscription to handle potential stalls. remove goroutines for Tx watchers. These do not need to be goroutine on the caller's side. --- cmd/peerswap-plugin/main.go | 24 +++++------ cmd/peerswaplnd/peerswapd/main.go | 12 +++--- lwk/electrumtxwatcher.go | 28 +++++++++++- lwk/electrumtxwatcher_test.go | 23 +++++++--- test/testcases.go | 71 ++++++------------------------- testframework/utils.go | 21 +++++++++ 6 files changed, 92 insertions(+), 87 deletions(-) diff --git a/cmd/peerswap-plugin/main.go b/cmd/peerswap-plugin/main.go index fcd9ba89..b065d6cb 100644 --- a/cmd/peerswap-plugin/main.go +++ b/cmd/peerswap-plugin/main.go @@ -350,23 +350,19 @@ func run(ctx context.Context, lightningPlugin *clightning.ClightningClient) erro swapService := swap.NewSwapService(swapServices) if liquidTxWatcher != nil && liquidEnabled { - go func() { - err := liquidTxWatcher.StartWatchingTxs() - if err != nil { - log.Infof("%v", err) - os.Exit(1) - } - }() + err := liquidTxWatcher.StartWatchingTxs() + if err != nil { + log.Infof("%v", err) + os.Exit(1) + } } if bitcoinTxWatcher != nil { - go func() { - err := bitcoinTxWatcher.StartWatchingTxs() - if err != nil { - log.Infof("%v", err) - os.Exit(1) - } - }() + err := bitcoinTxWatcher.StartWatchingTxs() + if err != nil { + log.Infof("%v", err) + os.Exit(1) + } } err = swapService.Start() diff --git a/cmd/peerswaplnd/peerswapd/main.go b/cmd/peerswaplnd/peerswapd/main.go index b13d22e5..45c9a784 100644 --- a/cmd/peerswaplnd/peerswapd/main.go +++ b/cmd/peerswaplnd/peerswapd/main.go @@ -340,13 +340,11 @@ func run() error { swapService := swap.NewSwapService(swapServices) if liquidTxWatcher != nil { - go func() { - err := liquidTxWatcher.StartWatchingTxs() - if err != nil { - log.Infof("%v", err) - os.Exit(1) - } - }() + err := liquidTxWatcher.StartWatchingTxs() + if err != nil { + log.Infof("%v", err) + os.Exit(1) + } } err = swapService.Start() diff --git a/lwk/electrumtxwatcher.go b/lwk/electrumtxwatcher.go index b4cf6f5b..5598b9ca 100644 --- a/lwk/electrumtxwatcher.go +++ b/lwk/electrumtxwatcher.go @@ -2,6 +2,8 @@ package lwk import ( "context" + "fmt" + "time" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/elementsproject/peerswap/electrum" @@ -9,6 +11,10 @@ import ( "github.com/elementsproject/peerswap/swap" ) +// initialBlockHeaderSubscriptionTimeout is +// the initial block header subscription timeout. +const initialBlockHeaderSubscriptionTimeout = 1000 * time.Second + type electrumTxWatcher struct { electrumClient electrum.RPC blockHeight electrum.BlocKHeight @@ -53,7 +59,24 @@ func (r *electrumTxWatcher) StartWatchingTxs() error { } } }() - return nil + return r.waitForInitialBlockHeaderSubscription(ctx) +} + +// 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) + defer cancel() + for { + select { + case <-ctx.Done(): + log.Infof("Initial block header subscription timeout.") + return ctx.Err() + default: + if r.blockHeight.Confirmed() { + return nil + } + } + } } func (r *electrumTxWatcher) AddWaitForConfirmationTx(swapIDStr, txIDStr string, vout, startingHeight uint32, scriptpubkeyByte []byte) { @@ -85,6 +108,9 @@ func (r *electrumTxWatcher) AddCsvCallback(f func(swapId string) error) { } func (r *electrumTxWatcher) GetBlockHeight() (uint32, error) { + if !r.blockHeight.Confirmed() { + return 0, fmt.Errorf("block height not confirmed") + } return r.blockHeight.Height(), nil } diff --git a/lwk/electrumtxwatcher_test.go b/lwk/electrumtxwatcher_test.go index 44135b8f..29b99151 100644 --- a/lwk/electrumtxwatcher_test.go +++ b/lwk/electrumtxwatcher_test.go @@ -1,6 +1,7 @@ package lwk_test import ( + "sync" "testing" "github.com/checksum0/go-electrum/electrum" @@ -58,13 +59,18 @@ func TestElectrumTxWatcher_Callback(t *testing.T) { return nil }, ) - err = r.StartWatchingTxs() - assert.NoError(t, err) + var wg sync.WaitGroup + wg.Add(1) + go func() { + err = r.StartWatchingTxs() + assert.NoError(t, err) + wg.Done() + }() r.AddWaitForConfirmationTx(wantSwapID, wantTxID, 0, 0, wantscriptpubkey) headerResultChan <- &electrum.SubscribeHeadersResult{ Height: onchain.LiquidConfs + targetTXHeight + 1, } - + wg.Wait() assert.Equal(t, <-callbackChan, wantSwapID) }) @@ -109,13 +115,18 @@ func TestElectrumTxWatcher_Callback(t *testing.T) { return nil }, ) - err = r.StartWatchingTxs() - assert.NoError(t, err) + var wg sync.WaitGroup + wg.Add(1) + go func() { + err = r.StartWatchingTxs() + assert.NoError(t, err) + wg.Done() + }() r.AddWaitForCsvTx(wantSwapID, wantTxID, 0, 0, wantscriptpubkey) headerResultChan <- &electrum.SubscribeHeadersResult{ Height: onchain.LiquidCsv + targetTXHeight + 1, } - + wg.Wait() assert.Equal(t, <-callbackChan, wantSwapID) }) diff --git a/test/testcases.go b/test/testcases.go index 4559d667..6943b0b0 100644 --- a/test/testcases.go +++ b/test/testcases.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/elementsproject/peerswap/swap" "github.com/elementsproject/peerswap/testframework" @@ -106,28 +107,14 @@ func coopClaimTest(t *testing.T, params *testParams) { // Check no invoice was paid. testframework.RequireWaitForChannelBalance(t, params.takerNode, params.scid, float64(setTakerFunds), 1., testframework.TIMEOUT) - // Wait for balance change - require.NoError( - testframework.WaitFor(func() bool { - b, err2 := params.makerNode.GetBtcBalanceSat() - if err2 != nil { - return false - } - return b != params.origMakerWallet-commitFee-params.swapAmt - }, testframework.TIMEOUT)) - // Check Wallet balance. // Expect: // - [0] before // - [1] before - commitment_fee - claim_fee - balance, err := params.takerNode.GetBtcBalanceSat() - require.NoError(err) - require.EqualValues(params.origTakerWallet, float64(balance), "expected %d, got %d", params.origTakerWallet, balance) - - balance, err = params.makerNode.GetBtcBalanceSat() - require.NoError(err) - require.InDelta((params.origMakerWallet - commitFee - claimFee), float64(balance), 1., "expected %d, got %d", - (params.origMakerWallet - commitFee - claimFee), balance) + testframework.AssertOnchainBalanceInDelta(t, + params.takerNode, params.origTakerWallet, 1, time.Second*30) + testframework.AssertOnchainBalanceInDelta(t, + params.makerNode, params.origMakerWallet-commitFee-claimFee, 1, time.Second*30) } func preimageClaimTest(t *testing.T, params *testParams) { @@ -192,37 +179,14 @@ func preimageClaimTest(t *testing.T, params *testParams) { t.Fatal("unknown role") } - // Wait for balance change - require.NoError( - testframework.WaitFor(func() bool { - b, err2 := params.takerNode.GetBtcBalanceSat() - if err2 != nil { - return false - } - return b != params.origTakerWallet - }, testframework.TIMEOUT)) - require.NoError( - testframework.WaitFor(func() bool { - b, err2 := params.makerNode.GetBtcBalanceSat() - if err2 != nil { - return false - } - return b != params.origMakerWallet - }, testframework.TIMEOUT)) - // Check Wallet balance. // Expect: (WITHOUT PREMIUM) // - taker -> before - claim_fee + swapamt // - maker -> before - commitment_fee - swapamt - balance, err := params.takerNode.GetBtcBalanceSat() - require.NoError(err) - require.InDelta(params.origTakerWallet-claimFee+params.swapAmt, float64(balance), 1., "expected %d, got %d", - params.origTakerWallet-claimFee+params.swapAmt, balance) - - balance, err = params.makerNode.GetBtcBalanceSat() - require.NoError(err) - require.InDelta((params.origMakerWallet - commitFee - params.swapAmt), float64(balance), 1., "expected %d, got %d", - (params.origMakerWallet - commitFee - params.swapAmt), balance) + testframework.AssertOnchainBalanceInDelta(t, + params.takerNode, params.origTakerWallet-claimFee+params.swapAmt, 1, time.Second*10) + testframework.AssertOnchainBalanceInDelta(t, + params.makerNode, params.origMakerWallet-commitFee-params.swapAmt, 1, time.Second*10) // Check latest invoice memo should be of the form "swap-in btc claim " bolt11, err := params.makerNode.GetLatestInvoice() @@ -314,20 +278,9 @@ func csvClaimTest(t *testing.T, params *testParams) { // Check channel and wallet balance require.True(testframework.AssertWaitForChannelBalance(t, params.makerNode, params.scid, float64(params.origMakerBalance+premium), 1., testframework.TIMEOUT)) - // Wait for balance change - require.NoError( - testframework.WaitFor(func() bool { - b, err2 := params.makerNode.GetBtcBalanceSat() - if err2 != nil { - return false - } - return b != params.origMakerWallet-commitFee-params.swapAmt - }, testframework.TIMEOUT)) - - balance, err := params.makerNode.GetBtcBalanceSat() - require.NoError(err) - require.InDelta(params.origMakerWallet-commitFee-claimFee, balance, 1., "expected %d, got %d", - params.origMakerWallet-commitFee-claimFee, balance) + // Check Wallet balance. + testframework.AssertOnchainBalanceInDelta(t, + params.makerNode, params.origMakerWallet-commitFee-claimFee, 1, time.Second*10) require.NoError(params.makerPeerswap.WaitForLog( fmt.Sprintf("added peer %s to suspicious peer list", params.takerNode.Id()), diff --git a/testframework/utils.go b/testframework/utils.go index 162f35fc..d367b06b 100644 --- a/testframework/utils.go +++ b/testframework/utils.go @@ -12,6 +12,8 @@ import ( "sync" "testing" "time" + + "github.com/stretchr/testify/require" ) // WaitFunc returns just a bool value to check if @@ -105,6 +107,25 @@ func AssertWaitForChannelBalance(t *testing.T, node LightningNode, scid string, return true } +func AssertOnchainBalanceInDelta(t *testing.T, + node LightningNode, expected, delta uint64, timeout time.Duration) { + var actual uint64 + t.Helper() + err := WaitFor(func() bool { + var err error + actual, err = node.GetBtcBalanceSat() + require.NoError(t, err) + if expected >= actual { + return expected-actual <= delta + } else { + return actual-expected <= delta + } + }, timeout) + if err != nil { + t.Errorf("expected: %d, got: %d, err: %v", expected, actual, err) + } +} + func RequireWaitForChannelBalance(t *testing.T, node LightningNode, scid string, expected, delta float64, timeout time.Duration) { actual, err := waitForChannelBalance(t, node, scid, expected, delta, timeout) if err != nil { From 381daa12d888820a52ef6c69dba4e3e7e9c8e3cd Mon Sep 17 00:00:00 2001 From: bruwbird Date: Mon, 13 May 2024 09:38:54 +0900 Subject: [PATCH 11/29] 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 | 38 ++++++--- lwk/electrumtxwatcher_test.go | 6 +- lwk/lwkwallet.go | 135 ++++++++++++++++++------------ lwk/lwkwallet_test.go | 32 +++++++ lwk/mock/electrumRPC.go | 86 ------------------- 13 files changed, 379 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..92462146 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,22 +48,30 @@ 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.") return } + if r.blockHeight.Confirmed() && blockHeader.Height <= int32(r.blockHeight.Height()) { + continue + } 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 +81,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 +93,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) -} From 1041bedb664a74a6aa3e88004fb86bbeb80bdef5 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Fri, 24 May 2024 08:46:47 +0900 Subject: [PATCH 12/29] test: add integration test for LWK and liquid The integration test for LWK and liquid is added to ensure that the swap functionality works as expected with the LWK and electrs support. --- test/lwk_cln_test.go | 486 +++++++++++++++++++++++++++++++++++++++++++ test/setup.go | 241 +++++++++++++++++++++ 2 files changed, 727 insertions(+) diff --git a/test/lwk_cln_test.go b/test/lwk_cln_test.go index 69f67abb..1e5dddde 100644 --- a/test/lwk_cln_test.go +++ b/test/lwk_cln_test.go @@ -994,3 +994,489 @@ func Test_ClnLnd_LWK_SwapOut(t *testing.T) { csvClaimTest(t, params) }) } + +func Test_ClnCln_LWKElements_SwapIn(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKLiquidSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + // filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKLiquidSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + coopClaimTest(t, params) + }) + + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKLiquidSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_IN, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapIn{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + csvClaimTest(t, params) + }) +} + +func Test_ClnCln_LWKLiquid_SwapOut(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + t.Run("claim_normal", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKLiquidSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + preimageClaimTest(t, params) + }) + t.Run("claim_coop", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + coopClaimTest(t, params) + }) + + t.Run("claim_csv", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + // Do swap. + go func() { + var response map[string]interface{} + err := lightningds[0].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.NoError(err) + + }() + csvClaimTest(t, params) + }) +} diff --git a/test/setup.go b/test/setup.go index 155ac042..903f23c5 100644 --- a/test/setup.go +++ b/test/setup.go @@ -1336,3 +1336,244 @@ func mixedLWKSetup(t *testing.T, fundAmt uint64, funder fundingNode) (*testframe syncPoll(&clnPollableNode{cln}, &peerswapPollableNode{peerswapd, lnd.Id()}) return bitcoind, liquidd, lightningds, peerswapd, scid, electrsd, lwk } + +func clnclnLWKLiquidSetup(t *testing.T, fundAmt uint64) (*testframework.BitcoinNode, + *testframework.LiquidNode, []*CLightningNodeWithLiquid, string, + *testframework.Electrs, *testframework.LWK) { + /// Get PeerSwap plugin path and test dir + _, filename, _, _ := runtime.Caller(0) + pathToPlugin := filepath.Join(filename, "..", "..", "out", "test-builds", "peerswap") + testDir := t.TempDir() + + // Setup nodes (1 bitcoind, 1 liquidd, 2 lightningd) + bitcoind, err := testframework.NewBitcoinNode(testDir, 1) + if err != nil { + t.Fatalf("could not create bitcoind %v", err) + } + t.Cleanup(bitcoind.Kill) + + liquidd, err := testframework.NewLiquidNode(testDir, bitcoind, 1) + if err != nil { + t.Fatal("error creating liquidd node", err) + } + t.Cleanup(liquidd.Kill) + + electrsd, err := testframework.NewElectrs(testDir, 1, liquidd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(electrsd.Process.Kill) + + lwk, err := testframework.NewLWK(testDir, 1, electrsd) + if err != nil { + t.Fatal("error creating electrsd node", err) + } + t.Cleanup(lwk.Process.Kill) + + var lightningds []*testframework.CLightningNode + + lightningd, err := testframework.NewCLightningNode(testDir, bitcoind, 1) + if err != nil { + t.Fatalf("could not create liquidd %v", err) + } + t.Cleanup(lightningd.Kill) + defer printFailedFiltered(t, lightningd.DaemonProcess) + + // Create policy file and accept all peers + err = os.MkdirAll(filepath.Join(lightningd.GetDataDir(), "peerswap"), os.ModePerm) + if err != nil { + t.Fatal("could not create dir", err) + } + err = os.WriteFile(filepath.Join(lightningd.GetDataDir(), "peerswap", "policy.conf"), []byte("accept_all_peers=1\n"), os.ModePerm) + if err != nil { + t.Fatal("could not create policy file", err) + } + + walletName := "swapElements" + + // Create config file + fileConf := struct { + Liquid struct { + RpcUser string + RpcPassword string + RpcHost string + RpcPort uint + RpcWallet string + } + }{ + Liquid: struct { + RpcUser string + RpcPassword string + RpcHost string + RpcPort uint + RpcWallet string + }{ + RpcUser: liquidd.RpcUser, + RpcPassword: liquidd.RpcPassword, + RpcHost: "http://127.0.0.1", + RpcPort: uint(liquidd.RpcPort), + RpcWallet: walletName, + }, + } + + data, err := toml.Marshal(fileConf) + require.NoError(t, err) + + configPath := filepath.Join(lightningd.GetDataDir(), "peerswap", "peerswap.conf") + os.WriteFile( + configPath, + data, + os.ModePerm, + ) + + // Use lightningd with --developer turned on + lightningd.WithCmd("lightningd") + + // Add plugin to cmd line options + lightningd.AppendCmdLine([]string{ + "--dev-bitcoind-poll=1", + "--dev-fast-gossip", + "--large-channels", + fmt.Sprint("--plugin=", pathToPlugin), + }) + + lightningds = append(lightningds, lightningd) + + lightningd, err = testframework.NewCLightningNode(testDir, bitcoind, 2) + if err != nil { + t.Fatalf("could not create liquidd %v", err) + } + t.Cleanup(lightningd.Kill) + defer printFailedFiltered(t, lightningd.DaemonProcess) + + // Create policy file and accept all peers + err = os.MkdirAll(filepath.Join(lightningd.GetDataDir(), "peerswap"), os.ModePerm) + if err != nil { + t.Fatal("could not create dir", err) + } + err = os.WriteFile(filepath.Join(lightningd.GetDataDir(), "peerswap", "policy.conf"), []byte("accept_all_peers=1\n"), os.ModePerm) + if err != nil { + t.Fatal("could not create policy file", err) + } + + // Set wallet name + walletName2 := "swapLWK" + + // Create config file + fileConf2 := struct { + LWK struct { + SignerName string + WalletName string + LWKEndpoint string + ElectrumEndpoint string + Network string + LiquidSwaps bool + } + }{ + LWK: struct { + SignerName string + WalletName string + LWKEndpoint string + ElectrumEndpoint string + Network string + LiquidSwaps bool + }{ + WalletName: walletName2, + SignerName: walletName2 + "-" + "signer", + LiquidSwaps: true, + LWKEndpoint: lwk.RPCURL.String(), + ElectrumEndpoint: electrsd.RPCURL.String(), + Network: "liquid-regtest", + }, + } + data, err = toml.Marshal(fileConf2) + require.NoError(t, err) + + configPath = filepath.Join(lightningd.GetDataDir(), "peerswap", "peerswap.conf") + os.WriteFile( + configPath, + data, + os.ModePerm, + ) + + // Use lightningd with --developer turned on + lightningd.WithCmd("lightningd") + + // Add plugin to cmd line options + lightningd.AppendCmdLine([]string{ + "--dev-bitcoind-poll=1", + "--dev-fast-gossip", + "--large-channels", + fmt.Sprint("--plugin=", pathToPlugin), + }) + + lightningds = append(lightningds, lightningd) + + // Start nodes + err = bitcoind.Run(true) + if err != nil { + t.Fatalf("bitcoind.Run() got err %v", err) + } + + err = liquidd.Run(true) + if err != nil { + t.Fatalf("Run() got err %v", err) + } + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, testframework.TIMEOUT) + defer cancel() + require.NoError(t, electrsd.Run(ctx)) + lwk.Process.Run() + + for _, lightningd := range lightningds { + err = lightningd.Run(true, true) + if err != nil { + t.Fatalf("lightningd.Run() got err %v", err) + } + err = lightningd.WaitForLog("peerswap initialized", testframework.TIMEOUT) + if err != nil { + t.Fatalf("lightningd.WaitForLog() got err %v", err) + } + } + + // Give liquid funds to nodes to have something to swap. + for _, lightningd := range lightningds { + var result clightning.GetAddressResponse + require.NoError(t, lightningd.Rpc.Request(&clightning.LiquidGetAddress{}, &result)) + _, err = liquidd.Rpc.Call("sendtoaddress", result.LiquidAddress, 10., "", "", false, false, 1, "UNSET") + require.NoError(t, err) + _ = liquidd.GenerateBlocks(20) + require.NoError(t, + testframework.WaitFor(func() bool { + var balance clightning.GetBalanceResponse + require.NoError(t, lightningd.Rpc.Request(&clightning.LiquidGetBalance{}, &balance)) + return balance.LiquidBalance >= 1000000000 + }, testframework.TIMEOUT)) + } + + // Lock txs. + _, err = liquidd.Rpc.Call("generatetoaddress", 1, testframework.LBTC_BURN) + require.NoError(t, err) + require.NoError(t, liquidd.GenerateBlocks(20)) + + // Setup channel ([0] fundAmt(10^7) ---- 0 [1]). + scid, err := lightningds[0].OpenChannel(lightningds[1], fundAmt, 0, true, true, true) + if err != nil { + t.Fatalf("lightingds[0].OpenChannel() %v", err) + } + + // Sync peer polling + var result interface{} + err = lightningds[0].Rpc.Request(&clightning.ReloadPolicyFile{}, &result) + if err != nil { + t.Fatalf("ListPeers %v", err) + } + err = lightningds[1].Rpc.Request(&clightning.ReloadPolicyFile{}, &result) + if err != nil { + t.Fatalf("ListPeers %v", err) + } + + syncPoll(&clnPollableNode{lightningds[0]}, &clnPollableNode{lightningds[1]}) + + return bitcoind, liquidd, []*CLightningNodeWithLiquid{{lightningds[0]}, {lightningds[1]}}, scid, electrsd, lwk +} From 58c516c01043013adebeecd37a6ac7977df735ee Mon Sep 17 00:00:00 2001 From: bruwbird Date: Sun, 26 May 2024 10:18:35 +0900 Subject: [PATCH 13/29] lwk: check if the version is supported Validate lwk version before setting up the wallet. lwk is in a state where breaking changes occur frequently, making it difficult to guarantee operation except with a specific version. --- lwk/client.go | 21 +++++++++++++++++++++ lwk/lwkwallet.go | 15 +++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lwk/client.go b/lwk/client.go index c895df17..16ed7e68 100644 --- a/lwk/client.go +++ b/lwk/client.go @@ -303,3 +303,24 @@ func (l *lwkclient) loadWallet(ctx context.Context, req *loadWalletRequest) (*lo } return &resp, nil } + +type versionRequest struct { +} + +func (r *versionRequest) Name() string { + return "version" +} + +type versionResponse struct { + Version string `json:"version"` + Network string `json:"network"` +} + +func (l *lwkclient) version(ctx context.Context) (*versionResponse, error) { + var resp versionResponse + err := l.request(ctx, &versionRequest{}, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index d284cd7c..8fce0221 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -26,6 +26,7 @@ const ( // Set up here because ctx is not inherited throughout the current codebase. defaultContextTimeout = time.Second * 5 minimumSatPerByte SatPerKVByte = 0.1 + supportedVersion = "0.3.0" ) // SatPerKVByte represents a fee rate in sat/kb. @@ -53,6 +54,7 @@ type LWKRpcWallet struct { c *Conf lwkClient *lwkclient electrumClient electrum.RPC + lwkVersion string } func NewLWKRpcWallet(ctx context.Context, c *Conf) (*LWKRpcWallet, error) { @@ -80,10 +82,23 @@ func (c *LWKRpcWallet) GetElectrumClient() electrum.RPC { return c.electrumClient } +func (r *LWKRpcWallet) IsSupportedVersion() bool { + return r.lwkVersion == supportedVersion +} + // 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 { timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout) defer cancel() + vres, err := r.lwkClient.version(timeoutCtx) + if err != nil { + return err + } + r.lwkVersion = vres.Version + if !r.IsSupportedVersion() { + return errors.New("unsupported lwk version. expected: " + supportedVersion + " got: " + r.lwkVersion) + } + res, err := r.lwkClient.walletDetails(timeoutCtx, &walletDetailsRequest{ WalletName: r.c.GetWalletName(), }) From 53e56fdd26ced8b6eea231d9242b82fe1bf18487 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 29 May 2024 15:04:04 +0900 Subject: [PATCH 14/29] config: make lwk-related settings clear To make lwk-related settings easier to understand, the following changes have been made. * Simplification of default settings in documentation * Clarified logging in case of incorrect configuration --- clightning/config.go | 4 ++-- cmd/peerswaplnd/config.go | 4 ++-- docs/setup_lwk.md | 4 +--- lwk/conf_builder.go | 8 +++---- lwk/config.go | 47 +++++++++++++++++++++++++++++++-------- lwk/config_test.go | 40 +++++++++++++++++++++++++++++++-- 6 files changed, 85 insertions(+), 22 deletions(-) diff --git a/clightning/config.go b/clightning/config.go index 98e14452..5f19976b 100644 --- a/clightning/config.go +++ b/clightning/config.go @@ -257,14 +257,14 @@ func LWKConfigFromToml(filePath string) (*lwk.Conf, error) { c.SetSignerName(lwk.NewConfName(cfg.LWK.SignerName)) } if cfg.LWK.LWKEndpoint != "" { - lwkEndpoint, err := lwk.NewConfURL(cfg.LWK.LWKEndpoint) + lwkEndpoint, err := lwk.NewLWKURL(cfg.LWK.LWKEndpoint) if err != nil { return nil, err } c.SetLWKEndpoint(*lwkEndpoint) } if cfg.LWK.ElectrumEndpoint != "" { - electrumEndpoint, err := lwk.NewConfURL(cfg.LWK.ElectrumEndpoint) + electrumEndpoint, err := lwk.NewElectrsURL(cfg.LWK.ElectrumEndpoint) if err != nil { return nil, err } diff --git a/cmd/peerswaplnd/config.go b/cmd/peerswaplnd/config.go index 87dcdc46..b7fe1c36 100644 --- a/cmd/peerswaplnd/config.go +++ b/cmd/peerswaplnd/config.go @@ -214,14 +214,14 @@ func LWKFromIniFileConfig(filePath string) (*lwk.Conf, error) { c.SetSignerName(lwk.NewConfName(cfg.LWK.SignerName)) } if cfg.LWK.LWKEndpoint != "" { - lwkEndpoint, err := lwk.NewConfURL(cfg.LWK.LWKEndpoint) + lwkEndpoint, err := lwk.NewLWKURL(cfg.LWK.LWKEndpoint) if err != nil { return nil, err } c.SetLWKEndpoint(*lwkEndpoint) } if cfg.LWK.ElectrumEndpoint != "" { - electrumEndpoint, err := lwk.NewConfURL(cfg.LWK.ElectrumEndpoint) + electrumEndpoint, err := lwk.NewElectrsURL(cfg.LWK.ElectrumEndpoint) if err != nil { return nil, err } diff --git a/docs/setup_lwk.md b/docs/setup_lwk.md index f94235a8..db7c81ce 100644 --- a/docs/setup_lwk.md +++ b/docs/setup_lwk.md @@ -42,7 +42,7 @@ The following settings are available * signer name * lwk endpoint : lwk jsonrpc endpoint * electrumEndpoint : electrum JSON-RPC serverのendpoint -* network : `liquid`、`liquid-testnet`.`"liquid-regtest` +* network : **`liquid`, `liquid-testnet`, `liquid-regtest`** * liquidSwaps : `true` if used Set up in INI (.ini) File Format for lnd and in toml format for cln @@ -52,7 +52,6 @@ Example configuration in lnd ```sh lwk.signername=signername lwk.walletname=walletname -lwk.lwkendpoint=http://localhost:32110 lwk.network=liquid lwk.liquidswaps=true ``` @@ -62,7 +61,6 @@ Example configuration in cln [LWK] signername="signername" walletname="walletname" -lwkendpoint="http://localhost:32110" network="liquid" liquidswaps=true ``` \ No newline at end of file diff --git a/lwk/conf_builder.go b/lwk/conf_builder.go index 13c0a0b5..72f7be63 100644 --- a/lwk/conf_builder.go +++ b/lwk/conf_builder.go @@ -28,11 +28,11 @@ func (b *confBuilder) DefaultConf() (*confBuilder, error) { lwkEndpoint = "http://localhost:32110" electrumEndpoint = "ssl://blockstream.info:995" } - lwkURL, err := NewConfURL(lwkEndpoint) + lwkURL, err := NewLWKURL(lwkEndpoint) if err != nil { return nil, err } - elementsURL, err := NewConfURL(electrumEndpoint) + elementsURL, err := NewElectrsURL(electrumEndpoint) if err != nil { return nil, err } @@ -53,12 +53,12 @@ func (b *confBuilder) SetWalletName(name confname) *confBuilder { return b } -func (b *confBuilder) SetLWKEndpoint(endpoint confurl) *confBuilder { +func (b *confBuilder) SetLWKEndpoint(endpoint lwkurl) *confBuilder { b.lwkEndpoint = endpoint return b } -func (b *confBuilder) SetElectrumEndpoint(endpoint confurl) *confBuilder { +func (b *confBuilder) SetElectrumEndpoint(endpoint electsurl) *confBuilder { b.electrumEndpoint = endpoint return b } diff --git a/lwk/config.go b/lwk/config.go index 28f5206a..ec026b05 100644 --- a/lwk/config.go +++ b/lwk/config.go @@ -2,6 +2,7 @@ package lwk import ( "errors" + "fmt" "net/url" "github.com/vulpemventures/go-elements/network" @@ -10,8 +11,8 @@ import ( type Conf struct { signerName confname walletName confname - lwkEndpoint confurl - electrumEndpoint confurl + lwkEndpoint lwkurl + electrumEndpoint electsurl network LwkNetwork liquidSwaps bool } @@ -100,7 +101,7 @@ func NewlwkNetwork(lekNetwork string) (LwkNetwork, error) { case "liquid-regtest": return NetworkRegtest, nil default: - return "", errors.New("invalid network") + return "", fmt.Errorf("expected liquid, liquid-testnet or liquid-regtest, got %s", lekNetwork) } } @@ -130,24 +131,52 @@ func (n confname) String() string { return string(n) } -type confurl struct { +type lwkurl struct { *url.URL } -func NewConfURL(endpoint string) (*confurl, error) { +func NewLWKURL(endpoint string) (*lwkurl, error) { u, err := url.ParseRequestURI(endpoint) if err != nil { return nil, err } - return &confurl{u}, nil + return &lwkurl{u}, nil } -func (n confurl) validate() error { - if n.URL == nil { +func (u lwkurl) validate() error { + if u.URL == nil { return errors.New("url must be set") } - if n.URL.String() == "" { + if u.URL.String() == "" { return errors.New("could not parse url") } + if u.Scheme != "http" && u.Scheme != "https" { + return fmt.Errorf("expected http or https scheme, got %s", u.Scheme) + } + return nil +} + +type electsurl struct { + *url.URL +} + +func NewElectrsURL(endpoint string) (*electsurl, error) { + u, err := url.ParseRequestURI(endpoint) + if err != nil { + return nil, err + } + return &electsurl{u}, nil +} + +func (u electsurl) validate() error { + if u.URL == nil { + return errors.New("url must be set") + } + if u.URL.String() == "" { + return errors.New("could not parse url") + } + if u.Scheme != "ssl" && u.Scheme != "tcp" { + return fmt.Errorf("expected ssl or tcp scheme, got %s", u.Scheme) + } return nil } diff --git a/lwk/config_test.go b/lwk/config_test.go index 738ce32b..9f09205f 100644 --- a/lwk/config_test.go +++ b/lwk/config_test.go @@ -41,7 +41,7 @@ func TestNewlwkNetwork(t *testing.T) { } } -func TestNewConfURL(t *testing.T) { +func TestLWKURL(t *testing.T) { t.Parallel() tests := map[string]struct { endpoint string @@ -65,7 +65,43 @@ func TestNewConfURL(t *testing.T) { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() - got, err := lwk.NewConfURL(tt.endpoint) + got, err := lwk.NewLWKURL(tt.endpoint) + if tt.wantErr { + assert.Error(t, err) + return + } + if got.String() != tt.want { + t.Errorf("NewConfURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestElectrsURL(t *testing.T) { + t.Parallel() + tests := map[string]struct { + endpoint string + want string + wantErr bool + }{ + "valid url": { + endpoint: "ssl://localhost:32111", + want: "ssl://localhost:32111", + }, + "without protocol": { + endpoint: "localhost:32111", + want: "localhost:32111", + }, + "invalid url": { + endpoint: "invalid url", + wantErr: true, + }, + } + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + t.Parallel() + got, err := lwk.NewElectrsURL(tt.endpoint) if tt.wantErr { assert.Error(t, err) return From 83603a2d4f7b1da71406c46185fafdaaa5e2aa86 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 30 May 2024 09:10:04 +0900 Subject: [PATCH 15/29] config: include LWK configuration in the output add LWKConfig details to PeerSwapConfig String to include LWK configuration in the output. --- cmd/peerswaplnd/config.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/peerswaplnd/config.go b/cmd/peerswaplnd/config.go index b7fe1c36..2d380a4a 100644 --- a/cmd/peerswaplnd/config.go +++ b/cmd/peerswaplnd/config.go @@ -62,12 +62,17 @@ func (p *PeerSwapConfig) String() string { if p.LndConfig != nil { lndString = fmt.Sprintf("host: %s, macaroonpath %s, tlspath %s", p.LndConfig.LndHost, p.LndConfig.MacaroonPath, p.LndConfig.TlsCertPath) } + var lwkConf string + if p.LWKConfig != nil { + lwkConf = fmt.Sprintf("lwk: signername: %s, walletname: %s, lwkendpoint: %s, electrumendpoint: %s, network: %s, liquidswaps: %v", p.LWKConfig.GetSignerName(), p.LWKConfig.GetWalletName(), p.LWKConfig.GetLWKEndpoint(), p.LWKConfig.GetElectrumEndpoint(), p.LWKConfig.GetNetwork(), p.LWKConfig.GetLiquidSwaps()) + } if p.DataDir != DefaultDatadir && p.PolicyFile == DefaultPolicyFile { p.PolicyFile = filepath.Join(p.DataDir, "policy.conf") } - return fmt.Sprintf("Host %s, ConfigFile %s, Datadir %s, Bitcoin enabled: %v, Lnd Config: %s, elements: %s", p.Host, p.ConfigFile, p.DataDir, p.BitcoinEnabled, lndString, liquidString) + return fmt.Sprintf("Host %s, ConfigFile %s, Datadir %s, Bitcoin enabled: %v, Lnd Config: %s, elements: %s, lwk config: %s", + p.Host, p.ConfigFile, p.DataDir, p.BitcoinEnabled, lndString, liquidString, lwkConf) } func (p *PeerSwapConfig) Validate() error { From d1f6a8905b27a0f67a1b19404983b50b5da78ce4 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 30 May 2024 12:51:46 +0900 Subject: [PATCH 16/29] lwk: ensure thread safety Add race detector flag to go test command for better concurrency checks. Fixes have been implemented for the race conditions detected in the above tests. Fixed a bug in the slice remove process. --- Makefile | 2 +- electrum/block_subscriber.go | 24 +++++++------- electrum/block_subscriber_test.go | 53 +++++++++++++++++++++++++++++++ lwk/electrumtxwatcher.go | 11 +++++++ lwk/electrumtxwatcher_test.go | 1 - 5 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 electrum/block_subscriber_test.go diff --git a/Makefile b/Makefile index c89d44c4..746cee4f 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ ${TEST_BIN_DIR}/peerswap: # Test section. Has commads for local and ci testing. test: - PAYMENT_RETRY_TIME=5 go test -tags dev -tags fast_test -timeout=10m -v ./... + PAYMENT_RETRY_TIME=5 go test -tags dev -tags fast_test -race -timeout=10m -v ./... .PHONY: test test-integration: test-bins diff --git a/electrum/block_subscriber.go b/electrum/block_subscriber.go index b810dced..483d180e 100644 --- a/electrum/block_subscriber.go +++ b/electrum/block_subscriber.go @@ -2,6 +2,7 @@ package electrum import ( "context" + "sync" "github.com/elementsproject/peerswap/log" ) @@ -24,6 +25,7 @@ type BlockHeaderSubscriber interface { type liquidBlockHeaderSubscriber struct { txObservers []TXObserver + mu sync.Mutex } func NewLiquidBlockHeaderSubscriber() *liquidBlockHeaderSubscriber { @@ -33,14 +35,24 @@ func NewLiquidBlockHeaderSubscriber() *liquidBlockHeaderSubscriber { var _ BlockHeaderSubscriber = (*liquidBlockHeaderSubscriber)(nil) func (h *liquidBlockHeaderSubscriber) Register(tx TXObserver) { + h.mu.Lock() + defer h.mu.Unlock() h.txObservers = append(h.txObservers, tx) } func (h *liquidBlockHeaderSubscriber) Deregister(o TXObserver) { - h.txObservers = remove(h.txObservers, o) + newObservers := make([]TXObserver, 0, len(h.txObservers)) + for _, observer := range h.txObservers { + if observer.GetSwapID() != o.GetSwapID() { + newObservers = append(newObservers, observer) + } + } + h.txObservers = newObservers } func (h *liquidBlockHeaderSubscriber) Update(ctx context.Context, blockHeight BlocKHeight) error { + h.mu.Lock() + defer h.mu.Unlock() for _, observer := range h.txObservers { callbacked, err := observer.Callback(ctx, blockHeight) if callbacked && err == nil { @@ -53,13 +65,3 @@ func (h *liquidBlockHeaderSubscriber) Update(ctx context.Context, blockHeight Bl } return nil } - -func remove(observerList []TXObserver, observerToRemove TXObserver) []TXObserver { - newObservers := make([]TXObserver, len(observerList)-1) - for _, observer := range observerList { - if observer.GetSwapID() != observerToRemove.GetSwapID() { - newObservers = append(newObservers, observer) - } - } - return newObservers -} diff --git a/electrum/block_subscriber_test.go b/electrum/block_subscriber_test.go new file mode 100644 index 00000000..a58d3132 --- /dev/null +++ b/electrum/block_subscriber_test.go @@ -0,0 +1,53 @@ +package electrum + +import ( + "context" + "sync" + "testing" + + "github.com/elementsproject/peerswap/swap" +) + +type testtxo struct { + swapid swap.SwapId +} + +func (t *testtxo) GetSwapID() swap.SwapId { + return t.swapid +} + +func (t *testtxo) Callback(context.Context, BlocKHeight) (bool, error) { + return true, nil +} + +func TestConcurrentUpdate(t *testing.T) { + t.Parallel() + observers := make([]TXObserver, 10) + for i := range observers { + observers[i] = &testtxo{ + swapid: *swap.NewSwapId(), + } + } + + h := &liquidBlockHeaderSubscriber{ + txObservers: observers, + } + + var wg sync.WaitGroup + const concurrency = 100 + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := h.Update(context.Background(), BlocKHeight(0)) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + }() + } + wg.Wait() + + if len(h.txObservers) != 0 { + t.Errorf("Expected length %d, but got %d", 0, len(h.txObservers)) + } +} diff --git a/lwk/electrumtxwatcher.go b/lwk/electrumtxwatcher.go index 92462146..3ded06db 100644 --- a/lwk/electrumtxwatcher.go +++ b/lwk/electrumtxwatcher.go @@ -3,6 +3,7 @@ package lwk import ( "context" "fmt" + "sync" "time" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -28,6 +29,7 @@ type electrumTxWatcher struct { // The connection with the electrum client is // disconnected after a certain period of time. resubscribeTicker *time.Ticker + mu sync.Mutex } func NewElectrumTxWatcher(electrumClient electrum.RPC) (*electrumTxWatcher, error) { @@ -59,7 +61,9 @@ func (r *electrumTxWatcher) StartWatchingTxs() error { if r.blockHeight.Confirmed() && blockHeader.Height <= int32(r.blockHeight.Height()) { continue } + r.mu.Lock() r.blockHeight = electrum.BlocKHeight(blockHeader.Height) + r.mu.Unlock() log.Infof("New block received. block height:%d", r.blockHeight) err = r.subscriber.Update(ctx, r.blockHeight) if err != nil { @@ -89,9 +93,12 @@ func (r *electrumTxWatcher) waitForInitialBlockHeaderSubscription(ctx context.Co log.Infof("Initial block header subscription timeout.") return ctx.Err() default: + r.mu.Lock() if r.blockHeight.Confirmed() { + r.mu.Unlock() return nil } + r.mu.Unlock() } time.Sleep(heartbeatInterval) } @@ -119,9 +126,13 @@ func (r *electrumTxWatcher) AddWaitForConfirmationTx(swapIDStr, txIDStr string, } func (r *electrumTxWatcher) AddConfirmationCallback(f func(swapId string, txHex string, err error) error) { + r.mu.Lock() + defer r.mu.Unlock() r.confirmationCallback = f } func (r *electrumTxWatcher) AddCsvCallback(f func(swapId string) error) { + r.mu.Lock() + defer r.mu.Unlock() r.csvCallback = f } diff --git a/lwk/electrumtxwatcher_test.go b/lwk/electrumtxwatcher_test.go index a05a925f..1f4091bc 100644 --- a/lwk/electrumtxwatcher_test.go +++ b/lwk/electrumtxwatcher_test.go @@ -110,7 +110,6 @@ func TestElectrumTxWatcher_Callback(t *testing.T) { r.AddCsvCallback( func(swapId string) error { assert.Equal(t, wantSwapID, swapId) - assert.NoError(t, err) callbackChan <- swapId return nil }, From f2ee81892ed3e67c88b619152b9a24bcc5b7c42f Mon Sep 17 00:00:00 2001 From: bruwbird Date: Fri, 31 May 2024 18:41:17 +0900 Subject: [PATCH 17/29] electrum: proper log output of tx watcher To optimise the log output of the transaction watcher, only output to the required debug log. Also, swap.ErrDoesNotExist is now handled in the callback. --- electrum/block_subscriber.go | 18 +++++++-- electrum/block_subscriber_test.go | 66 ++++++++++++++++++++++++++++++- electrum/tx_observer.go | 13 +++--- lwk/electrumtxwatcher.go | 2 +- lwk/lwkwallet.go | 3 +- 5 files changed, 88 insertions(+), 14 deletions(-) diff --git a/electrum/block_subscriber.go b/electrum/block_subscriber.go index 483d180e..b067751f 100644 --- a/electrum/block_subscriber.go +++ b/electrum/block_subscriber.go @@ -2,9 +2,11 @@ package electrum import ( "context" + "errors" "sync" "github.com/elementsproject/peerswap/log" + "github.com/elementsproject/peerswap/swap" ) type BlocKHeight uint32 @@ -55,13 +57,21 @@ func (h *liquidBlockHeaderSubscriber) Update(ctx context.Context, blockHeight Bl defer h.mu.Unlock() for _, observer := range h.txObservers { callbacked, err := observer.Callback(ctx, blockHeight) - if callbacked && err == nil { - // callbacked and no error, remove observer - h.Deregister(observer) + if callbacked { + if err == nil || errors.Is(err, swap.ErrSwapDoesNotExist) { + // callbacked and no error, remove observer + h.Deregister(observer) + } } - if err != nil { + if err != nil && !errors.Is(err, swap.ErrSwapDoesNotExist) { log.Infof("Error in callback: %v", err) } } return nil } + +func (h *liquidBlockHeaderSubscriber) Count() int { + h.mu.Lock() + defer h.mu.Unlock() + return len(h.txObservers) +} diff --git a/electrum/block_subscriber_test.go b/electrum/block_subscriber_test.go index a58d3132..c76deb06 100644 --- a/electrum/block_subscriber_test.go +++ b/electrum/block_subscriber_test.go @@ -9,14 +9,22 @@ import ( ) type testtxo struct { - swapid swap.SwapId + swapid swap.SwapId + getSwapID func() swap.SwapId + callback func(context.Context, BlocKHeight) (bool, error) } func (t *testtxo) GetSwapID() swap.SwapId { + if t.getSwapID != nil { + return t.getSwapID() + } return t.swapid } -func (t *testtxo) Callback(context.Context, BlocKHeight) (bool, error) { +func (t *testtxo) Callback(ctx context.Context, b BlocKHeight) (bool, error) { + if t.callback != nil { + return t.callback(ctx, b) + } return true, nil } @@ -51,3 +59,57 @@ func TestConcurrentUpdate(t *testing.T) { t.Errorf("Expected length %d, but got %d", 0, len(h.txObservers)) } } + +func Test_liquidBlockHeaderSubscriber_Update(t *testing.T) { + t.Parallel() + var blockHeight BlocKHeight = 10 + tests := map[string]struct { + txObservers []TXObserver + count int + wantErr bool + }{ + "no observers": { + txObservers: nil, + }, + "observers": { + txObservers: []TXObserver{ + &testtxo{}, + &testtxo{}, + }, + }, + "swap does not exists": { + txObservers: []TXObserver{ + &testtxo{ + callback: func(context.Context, BlocKHeight) (bool, error) { + return true, swap.ErrSwapDoesNotExist + }, + }, + }, + }, + "error in callback": { + txObservers: []TXObserver{ + &testtxo{ + callback: func(context.Context, BlocKHeight) (bool, error) { + return true, swap.ErrEventRejected + }, + }, + }, + count: 1, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + h := &liquidBlockHeaderSubscriber{ + txObservers: tt.txObservers, + } + if err := h.Update(context.Background(), blockHeight); (err != nil) != tt.wantErr { + t.Errorf("liquidBlockHeaderSubscriber.Update() error = %v, wantErr %v", err, tt.wantErr) + } + if len(h.txObservers) != tt.count { + t.Errorf("Expected length %d, but got %d", tt.count, len(h.txObservers)) + } + }) + } +} diff --git a/electrum/tx_observer.go b/electrum/tx_observer.go index 514dcee6..75ba2f35 100644 --- a/electrum/tx_observer.go +++ b/electrum/tx_observer.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/checksum0/go-electrum/electrum" + "github.com/elementsproject/peerswap/log" "github.com/elementsproject/peerswap/onchain" "github.com/elementsproject/peerswap/swap" ) @@ -93,11 +94,11 @@ func (o *observeOpeningTX) Callback(ctx context.Context, currentHeight BlocKHeig } rawTx, err := o.electrumClient.GetRawTransaction(ctx, o.txID.String()) if err != nil { - return false, fmt.Errorf("failed to get raw transaction: %w", err) + log.Debugf("failed to get raw transaction: %s", o.txID.String()) + return false, nil } if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidConfs)-1) { - return false, fmt.Errorf("not enough confirmations for opening transaction. txhash: %s.height: %d, current: %d", - o.txID.String(), getHeight(hs, o.txID).Height(), currentHeight.Height()) + return false, nil } return true, o.cb(o.swapID.String(), rawTx, nil) } @@ -139,11 +140,11 @@ func (o *observeCSVTX) Callback(ctx context.Context, currentHeight BlocKHeight) return false, fmt.Errorf("failed to get history: %w", err) } if !(getHeight(hs, o.txID).Confirmed()) { - return false, fmt.Errorf("the transaction is unconfirmed") + log.Debugf("the transaction is unconfirmed. txhash: %s", o.txID.String()) + return false, nil } if !(currentHeight.Height() >= getHeight(hs, o.txID).Height()+uint32(onchain.LiquidCsv-1)) { - return false, fmt.Errorf("not enough confirmations for csv transaction. txhash: %s.height: %d, current: %d", - o.txID.String(), getHeight(hs, o.txID).Height(), currentHeight.Height()) + return false, nil } return true, o.cb(o.swapID.String()) } diff --git a/lwk/electrumtxwatcher.go b/lwk/electrumtxwatcher.go index 3ded06db..2cf688e6 100644 --- a/lwk/electrumtxwatcher.go +++ b/lwk/electrumtxwatcher.go @@ -64,7 +64,7 @@ func (r *electrumTxWatcher) StartWatchingTxs() error { r.mu.Lock() r.blockHeight = electrum.BlocKHeight(blockHeader.Height) r.mu.Unlock() - log.Infof("New block received. block height:%d", r.blockHeight) + log.Debugf("New block received. block height:%d", r.blockHeight) err = r.subscriber.Update(ctx, r.blockHeight) if err != nil { log.Infof("Error notifying tx observers: %v", err) diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 8fce0221..e84afd38 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -35,7 +35,8 @@ 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) + log.Debugf("Using minimum fee rate of %v sat/kw", + minimumSatPerByte) return minimumSatPerByte } return s From 3d455693552166f0a80eb53443fbfaf40d0663ee Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 6 Jun 2024 09:40:18 +0900 Subject: [PATCH 18/29] clightning: add LWK configuration options add LWK configuration options to PeerswapClightningConfig. Add new fields to support LWK configuration, including signer name, wallet name, endpoint, Electrum endpoint, network, and liquid swaps. This config is just used for a console print. --- clightning/clightning.go | 8 ++++++++ clightning/options.go | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/clightning/clightning.go b/clightning/clightning.go index b01af74c..ea0d9ec2 100644 --- a/clightning/clightning.go +++ b/clightning/clightning.go @@ -355,6 +355,14 @@ func (cl *ClightningClient) SetPeerswapConfig(config *Config) { LiquidDisabled: *config.Liquid.LiquidSwaps, PeerswapDir: config.PeerswapDir, } + if config.LWK != nil { + cl.peerswapConfig.LWKSignerName = config.LWK.GetSignerName() + cl.peerswapConfig.LWKWalletName = config.LWK.GetWalletName() + cl.peerswapConfig.LWKEndpoint = config.LWK.GetLWKEndpoint() + cl.peerswapConfig.ElectrumEndpoint = config.LWK.GetElectrumEndpoint() + cl.peerswapConfig.LWKNetwork = config.LWK.GetNetwork() + cl.peerswapConfig.LWKLiquidSwaps = config.LWK.GetLiquidSwaps() + } } func (cl *ClightningClient) GetLightningRpc() *glightning.Lightning { diff --git a/clightning/options.go b/clightning/options.go index e91b295a..4d676ef1 100644 --- a/clightning/options.go +++ b/clightning/options.go @@ -55,6 +55,13 @@ type PeerswapClightningConfig struct { LiquidRpcWallet string `json:"liquid.rpcwallet"` LiquidDisabled bool `json:"liquid.disabled"` + LWKSignerName string `json:"lwksignername"` + LWKWalletName string `json:"lwkwalletname"` + LWKEndpoint string `json:"lwkendpoint"` + ElectrumEndpoint string `json:"electrumendpoint"` + LWKNetwork string `json:"lwknetwork"` + LWKLiquidSwaps bool `json:"lwkliquidswaps"` + PeerswapDir string `json:"peerswap-dir"` } From aa9550b8740d5dab29a638dc43af028052092b9e Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 13 Jun 2024 08:24:55 +0900 Subject: [PATCH 19/29] cln+lnd: liveness check add a plugin hook in the case of core lightning and an interceptor in the case of lnd, and pre-install a dead/alive monitor for each daemon. --- clightning/clightning.go | 24 ++++++++++++++++++++++++ cmd/peerswaplnd/peerswapd/main.go | 21 ++++++++++++++++++++- electrum/client.go | 4 ++++ electrum/electrum.go | 1 + electrum/mock/electrum.go | 14 ++++++++++++++ go.mod | 2 +- go.sum | 2 -- lwk/lwkwallet.go | 11 +++++++++++ wallet/elementsrpcwallet.go | 5 +++++ wallet/wallet.go | 1 + 10 files changed, 81 insertions(+), 4 deletions(-) diff --git a/clightning/clightning.go b/clightning/clightning.go index ea0d9ec2..e158796f 100644 --- a/clightning/clightning.go +++ b/clightning/clightning.go @@ -131,6 +131,7 @@ func NewClightningClient(ctx context.Context) (*ClightningClient, <-chan interfa cl.Plugin = glightning.NewPlugin(cl.onInit) err := cl.Plugin.RegisterHooks(&glightning.Hooks{ CustomMsgReceived: cl.OnCustomMsg, + RpcCommand: cl.OnRPCCommand, }) if err != nil { return nil, nil, err @@ -408,6 +409,29 @@ func (cl *ClightningClient) OnCustomMsg(event *glightning.CustomMsgReceivedEvent return event.Continue(), nil } +type Message struct { + Message string `json:"message"` +} + +func (cl *ClightningClient) OnRPCCommand(event *glightning.RpcCommandEvent) (*glightning.RpcCommandResponse, error) { + if cl.gbitcoin != nil { + ok, err := cl.gbitcoin.Ping() + if err != nil || !ok { + log.Infof("trying to send command %s, but failed to connect: %v", event.Cmd.MethodName, err) + return event.ReturnError("bitcoin_unavailable", -1) + } + } + + if cl.liquidWallet != nil { + ok, err := cl.liquidWallet.Ping() + if err != nil || !ok { + log.Infof("trying to send command %s, but failed to connect: %v", event.Cmd.MethodName, err) + return event.ReturnError("liquid_unavailable", -1) + } + } + return event.Continue(), nil +} + // AddMessageHandler adds a listener for incoming peermessages func (cl *ClightningClient) AddMessageHandler(f func(peerId string, msgType string, payload []byte) error) { cl.msgHandlers = append(cl.msgHandlers, f) diff --git a/cmd/peerswaplnd/peerswapd/main.go b/cmd/peerswaplnd/peerswapd/main.go index 04554373..acedb0d1 100644 --- a/cmd/peerswaplnd/peerswapd/main.go +++ b/cmd/peerswaplnd/peerswapd/main.go @@ -397,7 +397,9 @@ func run() error { } defer lis.Close() - grpcSrv := grpc.NewServer() + grpcSrv := grpc.NewServer( + grpc.UnaryInterceptor(livenessCheckInterceptor(liquidRpcWallet)), + ) peerswaprpc.RegisterPeerSwapServer(grpcSrv, peerswaprpcServer) @@ -439,6 +441,23 @@ func run() error { return nil } +func livenessCheckInterceptor(liquidWallet wallet.Wallet) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if liquidWallet != nil { + ok, err := liquidWallet.Ping() + if err != nil { + log.Infof("trying to send command %s, but failed to connect: %v", info.FullMethod, err) + return nil, fmt.Errorf("liquid backend not reachable: %v", err) + } + if !ok { + log.Infof("trying to send command %s, but failed to connect", info.FullMethod) + return nil, errors.New("liquid backend not reachable") + } + } + return handler(ctx, req) + } +} + func getBitcoinChain(ctx context.Context, li lnrpc.LightningClient) (*chaincfg.Params, error) { gi, err := li.GetInfo(ctx, &lnrpc.GetInfoRequest{}) if err != nil { diff --git a/electrum/client.go b/electrum/client.go index fac23ed9..6e35557e 100644 --- a/electrum/client.go +++ b/electrum/client.go @@ -84,3 +84,7 @@ func (c *electrumClient) GetFee(ctx context.Context, target uint32) (float32, er } return c.client.GetFee(ctx, target) } + +func (c *electrumClient) Ping(ctx context.Context) error { + return c.reconnect(ctx) +} diff --git a/electrum/electrum.go b/electrum/electrum.go index 43dd0c60..332f7208 100644 --- a/electrum/electrum.go +++ b/electrum/electrum.go @@ -12,4 +12,5 @@ type RPC interface { GetRawTransaction(ctx context.Context, txHash string) (string, error) BroadcastTransaction(ctx context.Context, rawTx string) (string, error) GetFee(ctx context.Context, target uint32) (float32, error) + Ping(ctx context.Context) error } diff --git a/electrum/mock/electrum.go b/electrum/mock/electrum.go index b6ff9a0a..44313eea 100644 --- a/electrum/mock/electrum.go +++ b/electrum/mock/electrum.go @@ -100,6 +100,20 @@ func (mr *MockRPCMockRecorder) GetRawTransaction(ctx, txHash any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRawTransaction", reflect.TypeOf((*MockRPC)(nil).GetRawTransaction), ctx, txHash) } +// Ping mocks base method. +func (m *MockRPC) Ping(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Ping", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Ping indicates an expected call of Ping. +func (mr *MockRPCMockRecorder) Ping(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockRPC)(nil).Ping), ctx) +} + // SubscribeHeaders mocks base method. func (m *MockRPC) SubscribeHeaders(ctx context.Context) (<-chan *electrum.SubscribeHeadersResult, error) { m.ctrl.T.Helper() diff --git a/go.mod b/go.mod index 8c59c00c..90540f96 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.4.0 // indirect diff --git a/go.sum b/go.sum index c12ba272..f8289c28 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,6 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= -github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index e84afd38..6009ae13 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -280,3 +280,14 @@ func (r *LWKRpcWallet) SetLabel(txID, address, label string) error { // TODO: call set label return nil } + +func (r *LWKRpcWallet) Ping() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() + _, err := r.lwkClient.version(ctx) + if err != nil { + return false, errors.New("lwk connection failed: " + err.Error()) + } + err = r.electrumClient.Ping(ctx) + return err == nil, err +} diff --git a/wallet/elementsrpcwallet.go b/wallet/elementsrpcwallet.go index 06bf11ee..6f36cf41 100644 --- a/wallet/elementsrpcwallet.go +++ b/wallet/elementsrpcwallet.go @@ -31,6 +31,7 @@ type RpcClient interface { SendRawTx(txHex string) (string, error) EstimateFee(blocks uint32, mode string) (*gelements.FeeResponse, error) SetLabel(address, label string) error + Ping() (bool, error) } // ElementsRpcWallet uses the elementsd rpc wallet @@ -191,3 +192,7 @@ func satsToAmountString(sats uint64) string { bitcoinAmt := float64(sats) / 100000000 return fmt.Sprintf("%f", bitcoinAmt) } + +func (r *ElementsRpcWallet) Ping() (bool, error) { + return r.rpcClient.Ping() +} diff --git a/wallet/wallet.go b/wallet/wallet.go index b0a016b3..9c2ed177 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -22,4 +22,5 @@ type Wallet interface { SendRawTx(rawTx string) (txid string, err error) GetFee(txSize int64) (uint64, error) SetLabel(txID, address, label string) error + Ping() (bool, error) } From 1d98fe2d2f63d7c8c3c095fb72ffd33797d9944a Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 13 Jun 2024 08:25:20 +0900 Subject: [PATCH 20/29] lwk: set prime seconds This way, it prevents many automated clients from attacking the server at the same time. For example, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89 and 97 are good numbers. --- lwk/electrumtxwatcher.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lwk/electrumtxwatcher.go b/lwk/electrumtxwatcher.go index 2cf688e6..b357d5a5 100644 --- a/lwk/electrumtxwatcher.go +++ b/lwk/electrumtxwatcher.go @@ -16,7 +16,10 @@ const ( // initialBlockHeaderSubscriptionTimeout is // the initial block header subscription timeout. initialBlockHeaderSubscriptionTimeout = 1000 * time.Second - blockHeaderSubscriptionTicker = 30 * time.Second + // Set prime seconds. + // This way, it prevents many automated clients from attacking the server at the same time. + // For example, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89 and 97 are good numbers. + blockHeaderSubscriptionTicker = 37 * time.Second ) type electrumTxWatcher struct { From cd24c3599158a12a403e995e5662c1e6d151239e Mon Sep 17 00:00:00 2001 From: bruwbird Date: Sun, 16 Jun 2024 10:25:16 +0900 Subject: [PATCH 21/29] cln+lnd: liveness check on swapout This change causes the liquid daemon to be monitored for dead or alive at the start of the swapout execution. Previously, at the start of a swapout, it would succeed even if the liquid daemon was not running, If a subsequent process required the liquid daemon, an error would occur. In addition, cross-checking by interceptors and hooks has been discontinued. This is because hooks by glightning are unstable and currently only swapout needs to be monitored for life and death. --- clightning/clightning.go | 20 -------------------- clightning/clightning_commands.go | 3 +++ cmd/peerswaplnd/peerswapd/main.go | 21 +-------------------- peerswaprpc/server.go | 3 +++ 4 files changed, 7 insertions(+), 40 deletions(-) diff --git a/clightning/clightning.go b/clightning/clightning.go index e158796f..8ddfbd9e 100644 --- a/clightning/clightning.go +++ b/clightning/clightning.go @@ -131,7 +131,6 @@ func NewClightningClient(ctx context.Context) (*ClightningClient, <-chan interfa cl.Plugin = glightning.NewPlugin(cl.onInit) err := cl.Plugin.RegisterHooks(&glightning.Hooks{ CustomMsgReceived: cl.OnCustomMsg, - RpcCommand: cl.OnRPCCommand, }) if err != nil { return nil, nil, err @@ -413,25 +412,6 @@ type Message struct { Message string `json:"message"` } -func (cl *ClightningClient) OnRPCCommand(event *glightning.RpcCommandEvent) (*glightning.RpcCommandResponse, error) { - if cl.gbitcoin != nil { - ok, err := cl.gbitcoin.Ping() - if err != nil || !ok { - log.Infof("trying to send command %s, but failed to connect: %v", event.Cmd.MethodName, err) - return event.ReturnError("bitcoin_unavailable", -1) - } - } - - if cl.liquidWallet != nil { - ok, err := cl.liquidWallet.Ping() - if err != nil || !ok { - log.Infof("trying to send command %s, but failed to connect: %v", event.Cmd.MethodName, err) - return event.ReturnError("liquid_unavailable", -1) - } - } - return event.Continue(), nil -} - // AddMessageHandler adds a listener for incoming peermessages func (cl *ClightningClient) AddMessageHandler(f func(peerId string, msgType string, payload []byte) error) { cl.msgHandlers = append(cl.msgHandlers, f) diff --git a/clightning/clightning_commands.go b/clightning/clightning_commands.go index 7d3ffe23..2b87b822 100644 --- a/clightning/clightning_commands.go +++ b/clightning/clightning_commands.go @@ -247,6 +247,9 @@ func (l *SwapOut) Call() (jrpc2.Result, error) { if !l.cl.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } + if ok, perr := l.cl.liquidWallet.Ping(); perr != nil || !ok { + return nil, fmt.Errorf("liquid wallet not reachable: %v", perr) + } } else if strings.Compare(l.Asset, "btc") == 0 { if !l.cl.swaps.BitcoinEnabled { diff --git a/cmd/peerswaplnd/peerswapd/main.go b/cmd/peerswaplnd/peerswapd/main.go index acedb0d1..04554373 100644 --- a/cmd/peerswaplnd/peerswapd/main.go +++ b/cmd/peerswaplnd/peerswapd/main.go @@ -397,9 +397,7 @@ func run() error { } defer lis.Close() - grpcSrv := grpc.NewServer( - grpc.UnaryInterceptor(livenessCheckInterceptor(liquidRpcWallet)), - ) + grpcSrv := grpc.NewServer() peerswaprpc.RegisterPeerSwapServer(grpcSrv, peerswaprpcServer) @@ -441,23 +439,6 @@ func run() error { return nil } -func livenessCheckInterceptor(liquidWallet wallet.Wallet) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - if liquidWallet != nil { - ok, err := liquidWallet.Ping() - if err != nil { - log.Infof("trying to send command %s, but failed to connect: %v", info.FullMethod, err) - return nil, fmt.Errorf("liquid backend not reachable: %v", err) - } - if !ok { - log.Infof("trying to send command %s, but failed to connect", info.FullMethod) - return nil, errors.New("liquid backend not reachable") - } - } - return handler(ctx, req) - } -} - func getBitcoinChain(ctx context.Context, li lnrpc.LightningClient) (*chaincfg.Params, error) { gi, err := li.GetInfo(ctx, &lnrpc.GetInfoRequest{}) if err != nil { diff --git a/peerswaprpc/server.go b/peerswaprpc/server.go index 9756d6e8..5da424d3 100644 --- a/peerswaprpc/server.go +++ b/peerswaprpc/server.go @@ -114,6 +114,9 @@ func (p *PeerswapServer) SwapOut(ctx context.Context, request *SwapOutRequest) ( if !p.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } + if ok, perr := p.liquidWallet.Ping(); perr != nil || !ok { + return nil, fmt.Errorf("liquid wallet not reachable: %v", perr) + } } else if strings.Compare(request.Asset, "btc") == 0 { if !p.swaps.BitcoinEnabled { From 2a3b3f4ef885adbd9da69aec27ca3e11b320eac1 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 19 Jun 2024 12:36:41 +0900 Subject: [PATCH 22/29] test: add tests for backend down add tests for backend down scenarios in LWK. Add tests to handle scenarios where the backend services (LWK and Electrs) are down during swaps. --- test/lwk_cln_test.go | 160 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/test/lwk_cln_test.go b/test/lwk_cln_test.go index 1e5dddde..a2dbf877 100644 --- a/test/lwk_cln_test.go +++ b/test/lwk_cln_test.go @@ -1480,3 +1480,163 @@ func Test_ClnCln_LWKLiquid_SwapOut(t *testing.T) { csvClaimTest(t, params) }) } + +func Test_ClnCln_LWKLiquid_BackendDown(t *testing.T) { + IsIntegrationTest(t) + t.Parallel() + t.Run("lwkdown", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKLiquidSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + lwk.Process.Kill() + + // Do swap. + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.Error(err) + }) + t.Run("electrsdown", func(t *testing.T) { + t.Parallel() + require := require.New(t) + + bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKLiquidSetup(t, uint64(math.Pow10(9))) + defer func() { + if t.Failed() { + filter := os.Getenv("PEERSWAP_TEST_FILTER") + pprintFail( + tailableProcess{ + p: bitcoind.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: liquidd.DaemonProcess, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[0].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: lightningds[1].DaemonProcess, + filter: filter, + lines: defaultLines, + }, + tailableProcess{ + p: electrs.Process, + lines: defaultLines, + }, + tailableProcess{ + p: lwk.Process, + lines: defaultLines, + }, + ) + } + }() + + var channelBalances []uint64 + var walletBalances []uint64 + for _, lightningd := range lightningds { + b, err := lightningd.GetBtcBalanceSat() + require.NoError(err) + walletBalances = append(walletBalances, b) + + b, err = lightningd.GetChannelBalanceSat(scid) + require.NoError(err) + channelBalances = append(channelBalances, b) + } + + params := &testParams{ + swapAmt: channelBalances[0] / 2, + scid: scid, + origTakerWallet: walletBalances[0], + origMakerWallet: walletBalances[1], + origTakerBalance: channelBalances[0], + origMakerBalance: channelBalances[1], + takerNode: lightningds[0], + makerNode: lightningds[1], + takerPeerswap: lightningds[0].DaemonProcess, + makerPeerswap: lightningds[1].DaemonProcess, + chainRpc: liquidd.RpcProxy, + chaind: liquidd, + confirms: LiquidConfirms, + csv: LiquidCsv, + swapType: swap.SWAPTYPE_OUT, + } + asset := "lbtc" + + electrs.Process.Kill() + + // Do swap. + var response map[string]interface{} + err := lightningds[1].Rpc.Request(&clightning.SwapOut{SatAmt: params.swapAmt, ShortChannelId: params.scid, Asset: asset}, &response) + require.Error(err) + }) + +} From 8ed026de86aeb4aab3eb5b874c2f7eb5eac410a3 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 20 Jun 2024 15:48:03 +0900 Subject: [PATCH 23/29] nix: update pinned nixpkgs revision --- packages.nix | 10 +++++----- testframework/clightning.go | 15 +++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages.nix b/packages.nix index e37e3c91..12789ab5 100644 --- a/packages.nix +++ b/packages.nix @@ -1,11 +1,11 @@ let fetchNixpkgs = rev: fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; - # Pinning to revision f54322490f509985fa8be4ac9304f368bd8ab924 - # - cln v24.02.1 - # - lnd v0.17.4-beta - # - bitcoin v26.0 + # Pinning to revision 755b915a158c9d588f08e9b08da9f7f3422070cc + # - cln v24.05 + # - lnd v0.18.0-beta + # - bitcoin v27.1 # - elements v23.2.1 - rev1 = "f54322490f509985fa8be4ac9304f368bd8ab924"; + rev1 = "755b915a158c9d588f08e9b08da9f7f3422070cc"; nixpkgs1 = fetchNixpkgs rev1; pkgs1 = import nixpkgs1 {}; diff --git a/testframework/clightning.go b/testframework/clightning.go index be606c3e..05e60f72 100644 --- a/testframework/clightning.go +++ b/testframework/clightning.go @@ -11,6 +11,7 @@ import ( "time" "github.com/elementsproject/glightning/glightning" + "github.com/elementsproject/peerswap/clightning" "github.com/elementsproject/peerswap/lightning" ) @@ -219,17 +220,15 @@ func (n *CLightningNode) GetChannelBalanceSat(scid string) (sats uint64, err err func (n *CLightningNode) GetScid(remote LightningNode) (string, error) { scid := "" err := WaitForWithErr(func() (bool, error) { - peers, err := n.Rpc.ListPeers() + var res clightning.ListPeerChannelsResponse + err := n.Rpc.Request(clightning.ListPeerChannelsRequest{}, &res) if err != nil { return false, fmt.Errorf("ListPeers() %w", err) } - for _, peer := range peers { - if peer.Id == remote.Id() { - if peer.Channels != nil { - scid = peer.Channels[0].ShortChannelId - return scid != "", nil - } - return false, nil + for _, c := range res.Channels { + if c.PeerId == remote.Id() { + scid = c.ShortChannelId + return scid != "", nil } } return false, fmt.Errorf("peer not found") From 6c6c802f87df0e92ae8d5c445c0beeecab3fcaf1 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 26 Jun 2024 09:06:15 +0900 Subject: [PATCH 24/29] electrum: ensure client is properly reinitialized ensure client is properly reinitialized before subscribing to headers to prevent potential stale connections. --- electrum/client.go | 13 ++++++++++--- electrum/electrum.go | 1 + electrum/mock/electrum.go | 14 ++++++++++++++ lwk/electrumtxwatcher.go | 11 +++++++++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/electrum/client.go b/electrum/client.go index 6e35557e..cccdd93c 100644 --- a/electrum/client.go +++ b/electrum/client.go @@ -50,10 +50,17 @@ func newClient(ctx context.Context, endpoint string, isTLS bool) (*electrum.Clie 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 +func (c *electrumClient) Reboot(ctx context.Context) error { + c.client.Shutdown() + client, err := newClient(ctx, c.endpoint, c.isTLS) + if err != nil { + return err } + c.client = client + return nil +} + +func (c *electrumClient) SubscribeHeaders(ctx context.Context) (<-chan *electrum.SubscribeHeadersResult, error) { return c.client.SubscribeHeaders(ctx) } diff --git a/electrum/electrum.go b/electrum/electrum.go index 332f7208..fc853b30 100644 --- a/electrum/electrum.go +++ b/electrum/electrum.go @@ -13,4 +13,5 @@ type RPC interface { BroadcastTransaction(ctx context.Context, rawTx string) (string, error) GetFee(ctx context.Context, target uint32) (float32, error) Ping(ctx context.Context) error + Reboot(ctx context.Context) error } diff --git a/electrum/mock/electrum.go b/electrum/mock/electrum.go index 44313eea..7340d325 100644 --- a/electrum/mock/electrum.go +++ b/electrum/mock/electrum.go @@ -114,6 +114,20 @@ func (mr *MockRPCMockRecorder) Ping(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockRPC)(nil).Ping), ctx) } +// Reboot mocks base method. +func (m *MockRPC) Reboot(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Reboot", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Reboot indicates an expected call of Reboot. +func (mr *MockRPCMockRecorder) Reboot(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reboot", reflect.TypeOf((*MockRPC)(nil).Reboot), ctx) +} + // SubscribeHeaders mocks base method. func (m *MockRPC) SubscribeHeaders(ctx context.Context) (<-chan *electrum.SubscribeHeadersResult, error) { m.ctrl.T.Helper() diff --git a/lwk/electrumtxwatcher.go b/lwk/electrumtxwatcher.go index b357d5a5..2c09b92f 100644 --- a/lwk/electrumtxwatcher.go +++ b/lwk/electrumtxwatcher.go @@ -29,7 +29,7 @@ type electrumTxWatcher struct { 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 + // Because the connection with the electrum client is // disconnected after a certain period of time. resubscribeTicker *time.Ticker mu sync.Mutex @@ -74,9 +74,16 @@ func (r *electrumTxWatcher) StartWatchingTxs() error { continue } case <-r.resubscribeTicker.C: + // The old subsription topic will remain in the memory + // and needs to be cleared by rebooting. + err := r.electrumClient.Reboot(ctx) + if err != nil { + log.Infof("Error rebooting electrum client: %v", err) + continue + } headerSubscription, err = r.electrumClient.SubscribeHeaders(ctx) if err != nil { - log.Infof("Error reloading electrum client: %v", err) + log.Infof("Error subsribe headers: %v", err) continue } } From c4e914521ff8bda02e53decb79510b22523324bb Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 26 Jun 2024 09:07:20 +0900 Subject: [PATCH 25/29] swap: add exponential backoff and jitter mechanism --- swap/fsm.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/swap/fsm.go b/swap/fsm.go index ddfd057a..064658bb 100644 --- a/swap/fsm.go +++ b/swap/fsm.go @@ -3,6 +3,8 @@ package swap import ( "errors" "fmt" + "math" + "math/rand" "sync" "time" @@ -26,6 +28,11 @@ const ( // NoOp represents a no-op event. NoOp EventType = "NoOp" + + // exponentialBackoffBase is the base value for the exponential backoff as milliseconds + exponentialBackoffBase int = 1000 + // exponentialBackoffCap is the maximum value for the exponential backoff as milliseconds + exponentialBackoffCap int = 20000 ) // StateType represents an extensible state type in the state machine. @@ -243,6 +250,7 @@ func (s *SwapStateMachine) SendEvent(event EventType, eventCtx EventContext) (bo return false, nil case Event_OnRetry: s.retries++ + s.exponentialBackoffAndJitter() if s.retries > 20 { s.retries = 0 return false, nil @@ -257,6 +265,17 @@ func (s *SwapStateMachine) SendEvent(event EventType, eventCtx EventContext) (bo } } +// exponentialBackoffAndJitter is function to wait for +// exponential backoff and jitter. +func (s *SwapStateMachine) exponentialBackoffAndJitter() { + temp := exponentialBackoffBase * int(math.Pow(2, float64(s.retries))) + if temp > exponentialBackoffCap { + temp = exponentialBackoffCap + } + sleep := rand.Intn(temp) + time.Sleep(time.Duration(sleep) * time.Millisecond) +} + // Recover tries to continue from the current state, by doing the associated Action func (s *SwapStateMachine) Recover() (bool, error) { log.Infof("[Swap:%s]: Recovering from state %s", s.SwapId.String(), s.Current) From 22f4e66acd44f0612878b1ffbb4570900955626b Mon Sep 17 00:00:00 2001 From: bruwbird Date: Wed, 26 Jun 2024 09:23:46 +0900 Subject: [PATCH 26/29] swap: add missing Event_OnTimeout transition The Event_OnRetry is designed to execute 20 retries and stop the process if it still fails. This patch adds the missing Event_OnTimeout transition to transit to coop close if the lwk is not restarted. --- swap/swap_out_sender.go | 1 + 1 file changed, 1 insertion(+) diff --git a/swap/swap_out_sender.go b/swap/swap_out_sender.go index 8a292b82..0cd43455 100644 --- a/swap/swap_out_sender.go +++ b/swap/swap_out_sender.go @@ -106,6 +106,7 @@ func getSwapOutSenderStates() States { Events: Events{ Event_ActionSucceeded: State_ClaimedPreimage, Event_OnRetry: State_SwapOutSender_ClaimSwap, + Event_OnTimeout: State_SwapOutSender_SendPrivkey, }, }, State_SwapOutSender_SendPrivkey: { From 45fb41479ebb6a66b92fdd6dc162cc35b502764c Mon Sep 17 00:00:00 2001 From: bruwbird Date: Sat, 29 Jun 2024 17:17:46 +0900 Subject: [PATCH 27/29] multi: using nix flake and upgrading lwk The version of lwk has been raised to cli_0.5.1. Change to use flake to use cli_0.5.1 in nix as well. Using flake allows you to run nix develop instead of nix-shell. Just uses the flake on nix shell. For the nix-env addon users. --- .github/workflows/ci.yml | 31 ++---- ci.nix | 5 - docs/setup_lwk.md | 2 +- flake.lock | 216 +++++++++++++++++++++++++++++++++++++++ flake.nix | 60 +++++++++++ lwk/client.go | 19 ++-- lwk/lwkwallet.go | 7 +- packages.nix | 47 --------- shell.nix | 29 +----- 9 files changed, 301 insertions(+), 115 deletions(-) delete mode 100644 ci.nix create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 packages.nix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45bd48ed..b85965f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,30 +34,13 @@ jobs: strategy: matrix: test-vector: [bitcoin-cln, bitcoin-lnd, liquid-cln, liquid-lnd, misc-integration, lwk-cln, lwk-lnd] - steps: - name: Checkout code - uses: actions/checkout@v3 - - - name: "Cache Nix store" - uses: actions/cache@v3.0.8 - id: nix-cache - with: - path: /tmp/nixcache - key: nix-${{ hashFiles('packages.nix') }} - - - name: Install Nix - uses: cachix/install-nix-action@v20 + uses: actions/checkout@v4 with: - nix_path: nixpkgs=channel:nixos-unstable - - - name: "Import Nix store cache" - if: "steps.nix-cache.outputs.cache-hit == 'true'" - run: "nix-store --import < /tmp/nixcache" - - - name: Run tests - run: nix-shell --run "make test-${{matrix.test-vector}}" - - - name: "Export Nix store cache" - if: "steps.nix-cache.outputs.cache-hit != 'true'" - run: "nix-store --export $(find /nix/store -maxdepth 1 -name '*-*') > /tmp/nixcache" \ No newline at end of file + fetch-depth: 0 + - name: Install Nix and run tests + uses: determinateSystems/nix-installer-action@v12 + - uses: DeterminateSystems/magic-nix-cache-action@v7 + - uses: DeterminateSystems/flake-checker-action@v8 + - run: nix-shell --run "make test-${{matrix.test-vector}}" \ No newline at end of file diff --git a/ci.nix b/ci.nix deleted file mode 100644 index bb754f6a..00000000 --- a/ci.nix +++ /dev/null @@ -1,5 +0,0 @@ -let - peerswap-pkgs = import ./packages.nix; -in with peerswap-pkgs; [ - testpkgs -] \ No newline at end of file diff --git a/docs/setup_lwk.md b/docs/setup_lwk.md index db7c81ce..8f1a3cff 100644 --- a/docs/setup_lwk.md +++ b/docs/setup_lwk.md @@ -3,7 +3,7 @@ **[Liquid Wallet Kit](https://github.com/Blockstream/lwk/tree/master)** is a collection of Rust crates for [Liquid](https://liquid.net) Wallets and is used for PeerSwap L-BTC swaps. To set up `lwk` for PeerSwap, follow the steps here. lwk is currently under development and changes are being made. -**peerswap has been tested only with [cli_0.3.0](https://github.com/Blockstream/lwk/tree/cli_0.3.0)**. +**peerswap has been tested only with [cli_0.5.1](https://github.com/Blockstream/lwk/releases/tag/cli_0.5.1)**. ## wallet peerswap assumes a wallet with blinding-key set in singlesig to lwk. diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..3a661310 --- /dev/null +++ b/flake.lock @@ -0,0 +1,216 @@ +{ + "nodes": { + "crane": { + "inputs": { + "nixpkgs": [ + "lwk-flake", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1719685792, + "narHash": "sha256-WIoVERD4AN6CmfGSRPy3mfPx2dDbRHgzP2V8z6aNbaY=", + "owner": "ipetkov", + "repo": "crane", + "rev": "aa5dcd0518a422dfd545d565f0d5a25971fea52a", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "electrs-flake": { + "inputs": { + "crane": [ + "lwk-flake", + "crane" + ], + "flake-utils": [ + "lwk-flake", + "flake-utils" + ], + "nixpkgs": [ + "lwk-flake", + "nixpkgs" + ], + "rust-overlay": [ + "lwk-flake", + "rust-overlay" + ] + }, + "locked": { + "lastModified": 1714129864, + "narHash": "sha256-Qe07R/8qbaj5J8TSYJRxYfPY0BShFJHrCe+LuGTXf+4=", + "owner": "blockstream", + "repo": "electrs", + "rev": "efc1fec8b0f96b5663d7257a0c2cffd8ef143219", + "type": "github" + }, + "original": { + "owner": "blockstream", + "repo": "electrs", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "lwk-flake": { + "inputs": { + "crane": "crane", + "electrs-flake": "electrs-flake", + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ], + "registry-flake": "registry-flake", + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1718716287, + "narHash": "sha256-rThaCGm+cfFwcAfwUzULZVZ/fdpD+nD37PyKhjWSX2M=", + "owner": "blockstream", + "repo": "lwk", + "rev": "14bac284fe712dd6fdbbbe82bda179a2a236b2fa", + "type": "github" + }, + "original": { + "owner": "blockstream", + "repo": "lwk", + "rev": "14bac284fe712dd6fdbbbe82bda179a2a236b2fa", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1711719691, + "narHash": "sha256-o/yHbl1XlgdhwBWCde6ch4OyJnI0qkBvXVEo0ZBerFc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f54322490f509985fa8be4ac9304f368bd8ab924", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f54322490f509985fa8be4ac9304f368bd8ab924", + "type": "github" + } + }, + "nixpkgs2": { + "locked": { + "lastModified": 1711632680, + "narHash": "sha256-JSC40xJKTVMQCf5jGCGvi0xGjF0sdaXIMwzxrvZZbjI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "680d27ad847801af781e0a99e4b87ed73965c69a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "680d27ad847801af781e0a99e4b87ed73965c69a", + "type": "github" + } + }, + "registry-flake": { + "inputs": { + "crane": [ + "lwk-flake", + "crane" + ], + "flake-utils": [ + "lwk-flake", + "flake-utils" + ], + "nixpkgs": [ + "lwk-flake", + "nixpkgs" + ], + "rust-overlay": [ + "lwk-flake", + "rust-overlay" + ] + }, + "locked": { + "lastModified": 1713271724, + "narHash": "sha256-0TRBGEJROTxtv1+ISeZ+vGriAd999v6lJ1L8aFdlNV4=", + "owner": "blockstream", + "repo": "asset_registry", + "rev": "f1c62fd793652827d3387e9aa4fca13c76de334e", + "type": "github" + }, + "original": { + "owner": "blockstream", + "ref": "flake", + "repo": "asset_registry", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "lwk-flake": "lwk-flake", + "nixpkgs": "nixpkgs", + "nixpkgs2": "nixpkgs2" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "lwk-flake", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1719627476, + "narHash": "sha256-LBfULF+2sCaWmkjmj1LkkGrAS/E9ZdXU1A5wWKjt9p0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "5be53be9e5c766fc72fc5d65ba8a566cc0c3217f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..0a47acbd --- /dev/null +++ b/flake.nix @@ -0,0 +1,60 @@ +{ + inputs = { + # Pinning to revision f54322490f509985fa8be4ac9304f368bd8ab924 + # - cln v24.02.1 + # - lnd v0.17.4-beta + # - bitcoin v26.0 + # - elements v23.2.1 + nixpkgs.url = "github:NixOS/nixpkgs/f54322490f509985fa8be4ac9304f368bd8ab924"; + flake-utils.url = "github:numtide/flake-utils"; + # blockstream-electrs: init at 0.4.1 #299761 + # https://github.com/NixOS/nixpkgs/pull/299761/commits/680d27ad847801af781e0a99e4b87ed73965c69a + nixpkgs2.url = "github:NixOS/nixpkgs/680d27ad847801af781e0a99e4b87ed73965c69a"; + # lwk: init at wasm_0.6.3 #14bac28 + # https://github.com/Blockstream/lwk/releases/tag/wasm_0.6.3 + lwk-flake = { + url = "github:blockstream/lwk/14bac284fe712dd6fdbbbe82bda179a2a236b2fa"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + }; + }; + }; + outputs = { self, nixpkgs, nixpkgs2, flake-utils, lwk-flake }: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { + system = system; + }; + pkgs2 = import nixpkgs2 { + system = system; + }; + blockstream-electrs = pkgs2.blockstream-electrs.overrideAttrs (oldAttrs: { + cargoBuildFlags = [ "--features liquid" "--bin electrs" ]; + }); + bitcoind = pkgs.bitcoind.overrideAttrs (attrs: { + meta = attrs.meta or { } // { priority = 0; }; + }); + lwk = lwk-flake.packages.${system}.bin; + in + with pkgs; + { + devShells.default = mkShell { + buildInputs = [ + go_1_22 + gotools + blockstream-electrs + bitcoind + elementsd + clightning + lnd + lwk + ]; + # Cannot run the debugger without this + # see https://github.com/go-delve/delve/issues/3085 + hardeningDisable = [ "all" ]; + }; + } + ); +} diff --git a/lwk/client.go b/lwk/client.go index 16ed7e68..8affdf30 100644 --- a/lwk/client.go +++ b/lwk/client.go @@ -68,10 +68,12 @@ type addressRequest struct { Index *uint32 `json:"index,omitempty"` WalletName string `json:"name"` Signer *string `json:"signer,omitempty"` + WithTextQr bool `json:"with_text_qr"` + WithUriQr *uint8 `json:"with_uri_qr,omitempty"` } func (r *addressRequest) Name() string { - return "address" + return "wallet_address" } type addressResponse struct { @@ -106,7 +108,7 @@ type sendResponse struct { } func (s *sendRequest) Name() string { - return "send_many" + return "wallet_send_many" } func (l *lwkclient) send(ctx context.Context, s *sendRequest) (*sendResponse, error) { @@ -128,7 +130,7 @@ type signResponse struct { } func (s *signRequest) Name() string { - return "sign" + return "signer_sign" } func (l *lwkclient) sign(ctx context.Context, s *signRequest) (*signResponse, error) { @@ -151,7 +153,7 @@ type broadcastResponse struct { } func (b *broadcastRequest) Name() string { - return "broadcast" + return "wallet_broadcast" } func (l *lwkclient) broadcast(ctx context.Context, b *broadcastRequest) (*broadcastResponse, error) { @@ -169,7 +171,7 @@ type balanceRequest struct { } func (b *balanceRequest) Name() string { - return "balance" + return "wallet_balance" } type balanceResponse struct { @@ -217,7 +219,7 @@ type generateSignerRequest struct { } func (r *generateSignerRequest) Name() string { - return "generate_signer" + return "signer_generate" } type generateSignerResponse struct { @@ -236,6 +238,7 @@ func (l *lwkclient) generateSigner(ctx context.Context) (*generateSignerResponse type loadSoftwareSignerRequest struct { Mnemonic string `json:"mnemonic"` SignerName string `json:"name"` + Persist bool `json:"persist"` } func (r *loadSoftwareSignerRequest) Name() string { @@ -265,7 +268,7 @@ type singlesigDescriptorRequest struct { } func (r *singlesigDescriptorRequest) Name() string { - return "singlesig_descriptor" + return "signer_singlesig_descriptor" } type singlesigDescriptorResponse struct { @@ -287,7 +290,7 @@ type loadWalletRequest struct { } func (r *loadWalletRequest) Name() string { - return "load_wallet" + return "wallet_load" } type loadWalletResponse struct { diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 6009ae13..7e59c279 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -26,7 +26,7 @@ const ( // Set up here because ctx is not inherited throughout the current codebase. defaultContextTimeout = time.Second * 5 minimumSatPerByte SatPerKVByte = 0.1 - supportedVersion = "0.3.0" + supportedCLIVersion = "0.5.1" ) // SatPerKVByte represents a fee rate in sat/kb. @@ -84,7 +84,7 @@ func (c *LWKRpcWallet) GetElectrumClient() electrum.RPC { } func (r *LWKRpcWallet) IsSupportedVersion() bool { - return r.lwkVersion == supportedVersion + return r.lwkVersion == supportedCLIVersion } // setupWallet checks if the swap wallet is already loaded in elementsd, if not it loads/creates it @@ -97,7 +97,7 @@ func (r *LWKRpcWallet) setupWallet(ctx context.Context) error { } r.lwkVersion = vres.Version if !r.IsSupportedVersion() { - return errors.New("unsupported lwk version. expected: " + supportedVersion + " got: " + r.lwkVersion) + return errors.New("unsupported lwk version. expected: " + supportedCLIVersion + " got: " + r.lwkVersion) } res, err := r.lwkClient.walletDetails(timeoutCtx, &walletDetailsRequest{ @@ -129,6 +129,7 @@ func (r *LWKRpcWallet) createWallet(ctx context.Context, walletName, signerName _, err = r.lwkClient.loadSoftwareSigner(ctx, &loadSoftwareSignerRequest{ Mnemonic: res.Mnemonic, SignerName: signerName, + Persist: true, }) // 32011 is the error code for signer already loaded if err != nil && !strings.HasPrefix(err.Error(), "-32011") { diff --git a/packages.nix b/packages.nix deleted file mode 100644 index 12789ab5..00000000 --- a/packages.nix +++ /dev/null @@ -1,47 +0,0 @@ -let - fetchNixpkgs = rev: fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; - # Pinning to revision 755b915a158c9d588f08e9b08da9f7f3422070cc - # - cln v24.05 - # - lnd v0.18.0-beta - # - bitcoin v27.1 - # - elements v23.2.1 - rev1 = "755b915a158c9d588f08e9b08da9f7f3422070cc"; - nixpkgs1 = fetchNixpkgs rev1; - pkgs1 = import nixpkgs1 {}; - - # Override priority for bitcoin as /bin/bitcoin_test will - # confilict with /bin/bitcoin_test from elementsd. - bitcoind = (pkgs1.bitcoind.overrideAttrs (attrs: { - meta = attrs.meta or {} // { - priority = 0; - }; - })); - # lwk: init at 0.3.0 #292522 - # https://github.com/NixOS/nixpkgs/pull/292522/commits/2b3750792b2e4b52f472b6e6d88a6b02b6536c43 - rev2 = "2b3750792b2e4b52f472b6e6d88a6b02b6536c43"; - nixpkgs2 = fetchNixpkgs rev2; - pkgs2 = import nixpkgs2 {}; - # blockstream-electrs: init at 0.4.1 #299761 - # https://github.com/NixOS/nixpkgs/pull/299761/commits/680d27ad847801af781e0a99e4b87ed73965c69a - rev3 = "680d27ad847801af781e0a99e4b87ed73965c69a"; - nixpkgs3 = fetchNixpkgs rev3; - pkgs3 = import nixpkgs3 {}; - blockstream-electrs = pkgs3.blockstream-electrs.overrideAttrs (oldAttrs: { - cargoBuildFlags = [ "--features liquid" "--bin electrs" ]; - }); - -in -{ - execs = { - clightning = pkgs1.clightning; - bitcoind = bitcoind; - elementsd = pkgs1.elementsd; - mermaid = pkgs1.nodePackages.mermaid-cli; - lnd = pkgs1.lnd; - lwk = pkgs2.lwk; - electrs = blockstream-electrs; - - }; - testpkgs = [ pkgs1.go pkgs1.bitcoind pkgs1.elementsd pkgs1.lnd pkgs2.lwk blockstream-electrs]; - devpkgs = [ pkgs1.go_1_22 pkgs1.gotools pkgs1.bitcoind pkgs1.elementsd pkgs1.clightning pkgs1.lnd pkgs2.lwk blockstream-electrs]; -} diff --git a/shell.nix b/shell.nix index 4d2e7333..2b2e144d 100644 --- a/shell.nix +++ b/shell.nix @@ -1,27 +1,2 @@ -let - peerswap-pkgs = import ./packages.nix; -in -{ pkgs ? (import {})}: -let - execs = peerswap-pkgs.execs; -in with pkgs; -stdenv.mkDerivation rec { - name = "peerswap-dev-env"; - nativeBuildInputs = [openssl]; - buildInputs = [peerswap-pkgs.devpkgs ]; - - shellHook = '' - alias lightning-cli='${execs.clightning}/bin/lightning-cli' - alias lightningd='${execs.clightning}/bin/lightningd' - alias bitcoind='${execs.bitcoind}/bin/bitcoind' - alias bitcoin-cli='${execs.bitcoind}/bin/bitcoin-cli' - alias elementsd='${execs.elementsd}/bin/elementsd' - alias elements-cli='${execs.elementsd}/bin/elements-cli' - alias lnd='${execs.lnd}/bin/lnd' - alias lncli='${execs.lnd}/bin/lncli' - - . ./contrib/startup_regtest.sh - setup_alias - ''; - hardeningDisable = [ "all" ]; -} +# Just uses the flake. For the nix-env addon users. +(builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default From 7045055a00ffb06466bf9312fc23cb1a41e3e364 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Sat, 29 Jun 2024 17:18:17 +0900 Subject: [PATCH 28/29] lwkwallet: refactor fee calculations Variable and function names have been corrected to eliminate confusion with fee calculations. Test cases have also been refactored. --- lwk/lwkwallet.go | 38 +++++++++++++++++++------------------- lwk/lwkwallet_test.go | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 7e59c279..4a673390 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -17,6 +17,9 @@ import ( // Satoshi represents a Satoshi value. type Satoshi = uint64 +// SatPerVByte represents a fee rate in sat/vb. +type SatPerVByte float64 + const ( // 1 kb = 1000 bytes kb = 1000 @@ -24,30 +27,27 @@ const ( // 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 + defaultContextTimeout = time.Second * 5 + minimumFee SatPerVByte = 0.1 supportedCLIVersion = "0.5.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.Debugf("Using minimum fee rate of %v sat/kw", - minimumSatPerByte) - return minimumSatPerByte +func SatPerVByteFromFeeBTCPerKb(feeBTCPerKb float64) SatPerVByte { + s := SatPerVByte(feeBTCPerKb * math.Pow10(btcToSatoshiExp) / kb) + if s < minimumFee { + log.Debugf("using minimum fee rate of %v sat/vbyte", + minimumFee) + return minimumFee } return s } -func (s SatPerKVByte) GetSatPerKVByte() float64 { +func (s SatPerVByte) getValue() float64 { return float64(s) } -func (s SatPerKVByte) GetFee(txSize int64) Satoshi { - return Satoshi(s.GetSatPerKVByte() * float64(txSize)) +func (s SatPerVByte) GetFee(txSizeBytes int64) Satoshi { + return Satoshi(s.getValue() * float64(txSizeBytes)) } // LWKRpcWallet uses the elementsd rpc wallet @@ -159,7 +159,7 @@ func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningPar asset []byte) (txid, rawTx string, fee Satoshi, err error) { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() - feerate := float64(r.getFeePerKb(ctx)) * kb + feerate := r.getFeeSatPerVByte(ctx).getValue() * kb fundedTx, err := r.lwkClient.send(ctx, &sendRequest{ Addressees: []*unvalidatedAddressee{ { @@ -263,18 +263,18 @@ func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { return res, nil } -func (r *LWKRpcWallet) getFeePerKb(ctx context.Context) SatPerKVByte { +func (r *LWKRpcWallet) getFeeSatPerVByte(ctx context.Context) SatPerVByte { feeBTCPerKb, err := r.electrumClient.GetFee(ctx, wallet.LiquidTargetBlocks) if err != nil { log.Infof("error getting fee: %v.", err) } - return SatPerKVByteFromFeeBTCPerKb(float64(feeBTCPerKb)) + return SatPerVByteFromFeeBTCPerKb(float64(feeBTCPerKb)) } -func (r *LWKRpcWallet) GetFee(txSize int64) (Satoshi, error) { +func (r *LWKRpcWallet) GetFee(txSizeBytes int64) (Satoshi, error) { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() - return r.getFeePerKb(ctx).GetFee(txSize), nil + return r.getFeeSatPerVByte(ctx).GetFee(txSizeBytes), nil } func (r *LWKRpcWallet) SetLabel(txID, address, label string) error { diff --git a/lwk/lwkwallet_test.go b/lwk/lwkwallet_test.go index 2650d279..ee13885e 100644 --- a/lwk/lwkwallet_test.go +++ b/lwk/lwkwallet_test.go @@ -9,24 +9,44 @@ import ( func TestSatPerKVByteFromFeeBTCPerKb(t *testing.T) { t.Parallel() - t.Run("below minimum minimumSatPerByte", func(t *testing.T) { + t.Run("above minimumSatPerByte", func(t *testing.T) { t.Parallel() var ( - txsize int64 = 1000 - FeeBTCPerKb = 0.0000001 + FeeBTCPerKb = 0.0001 ) - got := lwk.SatPerKVByteFromFeeBTCPerKb(FeeBTCPerKb).GetFee(txsize) - want := lwk.Satoshi(100) + got := lwk.SatPerVByteFromFeeBTCPerKb(FeeBTCPerKb) + assert.Equal(t, 10.0, float64(got)) + }) + t.Run("below minimumSatPerByte", func(t *testing.T) { + t.Parallel() + var ( + FeeBTCPerKb = 0.0000002 + ) + got := lwk.SatPerVByteFromFeeBTCPerKb(FeeBTCPerKb) + assert.Equal(t, 0.1, float64(got)) + }) +} + +func TestGetFee(t *testing.T) { + t.Parallel() + t.Run("above minimumSatPerByte", func(t *testing.T) { + t.Parallel() + var ( + txsize int64 = 250 + FeeBTCPerKb = 0.0001 + ) + got := lwk.SatPerVByteFromFeeBTCPerKb(FeeBTCPerKb).GetFee(txsize) + want := lwk.Satoshi(2500) assert.Equal(t, want, got) }) t.Run("above minimum minimumSatPerByte", func(t *testing.T) { t.Parallel() var ( txsize int64 = 1000 - FeeBTCPerKb = 0.000002 + FeeBTCPerKb = 0.0000002 ) - got := lwk.SatPerKVByteFromFeeBTCPerKb(FeeBTCPerKb).GetFee(txsize) - want := lwk.Satoshi(200) + got := lwk.SatPerVByteFromFeeBTCPerKb(FeeBTCPerKb).GetFee(txsize) + want := lwk.Satoshi(100) assert.Equal(t, want, got) }) } From 66fead062425ff3b408b9b489307997eb0a2b0ba Mon Sep 17 00:00:00 2001 From: bruwbird Date: Mon, 1 Jul 2024 10:20:00 +0900 Subject: [PATCH 29/29] lwkwallet: implement SetLabel method implement SetLabel method to call walletSetTxMemo for setting transaction memos. --- lwk/client.go | 17 +++++++++++++++++ lwk/lwkwallet.go | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lwk/client.go b/lwk/client.go index 8affdf30..2254b197 100644 --- a/lwk/client.go +++ b/lwk/client.go @@ -327,3 +327,20 @@ func (l *lwkclient) version(ctx context.Context) (*versionResponse, error) { } return &resp, nil } + +type WalletSetTxMemoRequest struct { + Memo string `json:"memo"` + WalletName string `json:"name"` + Txid string `json:"txid"` +} + +func (r *WalletSetTxMemoRequest) Name() string { + return "wallet_set_tx_memo" +} + +type WalletSetTxMemoResponse struct { +} + +func (l *lwkclient) walletSetTxMemo(ctx context.Context, req *WalletSetTxMemoRequest) error { + return l.request(ctx, req, &WalletSetTxMemoResponse{}) +} diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 4a673390..00a9a6cc 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -278,8 +278,13 @@ func (r *LWKRpcWallet) GetFee(txSizeBytes int64) (Satoshi, error) { } func (r *LWKRpcWallet) SetLabel(txID, address, label string) error { - // TODO: call set label - return nil + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() + return r.lwkClient.walletSetTxMemo(ctx, &WalletSetTxMemoRequest{ + WalletName: r.c.GetWalletName(), + Txid: txID, + Memo: label, + }) } func (r *LWKRpcWallet) Ping() (bool, error) {