From 85d10b1f5dfea112212756582614b5ad3014e992 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Thu, 18 Jul 2024 12:00:29 +0900 Subject: [PATCH 1/2] wallet: add fee rate estimation to FundRawTx This is a fix for the inconvenience caused by the default setting of fund raw tx, which sometimes set relatively high fees. add getFeeRate function to estimate optimal fee rate based on network conditions. Replace FundRawTx with FundRawWithOptions to support fee rate options. --- wallet/elementsrpcwallet.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/wallet/elementsrpcwallet.go b/wallet/elementsrpcwallet.go index 6f36cf41..624d6313 100644 --- a/wallet/elementsrpcwallet.go +++ b/wallet/elementsrpcwallet.go @@ -3,9 +3,12 @@ package wallet import ( "errors" "fmt" + + "math" "strings" "github.com/elementsproject/glightning/gelements" + "github.com/elementsproject/peerswap/log" "github.com/elementsproject/peerswap/swap" "github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/elementsutil" @@ -25,7 +28,7 @@ type RpcClient interface { CreateWallet(walletname string) (string, error) SetRpcWallet(walletname string) ListWallets() ([]string, error) - FundRawTx(txHex string) (*gelements.FundRawResult, error) + FundRawWithOptions(txstring string, options *gelements.FundRawOptions, iswitness *bool) (*gelements.FundRawResult, error) BlindRawTransaction(txHex string) (string, error) SignRawTransactionWithWallet(txHex string) (gelements.SignRawTransactionWithWalletRes, error) SendRawTx(txHex string) (string, error) @@ -89,7 +92,10 @@ func (r *ElementsRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.Openi if err != nil { return "", "", 0, err } - fundedTx, err := r.rpcClient.FundRawTx(txHex) + fundedTx, err := r.rpcClient.FundRawWithOptions(txHex, &gelements.FundRawOptions{ + FeeRate: fmt.Sprintf("%f", r.getFeeRate()), + }, nil) + if err != nil { return "", "", 0, err } @@ -104,6 +110,27 @@ func (r *ElementsRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.Openi return txid, finalized, gelements.ConvertBtc(fundedTx.Fee), nil } +const ( + // minFeeRateBTCPerKb defines the minimum fee rate in BTC/kB. + // This value is equivalent to 0.1 sat/byte. + minFeeRateBTCPerKb = 0.000001 +) + +// getFeeRate retrieves the optimal fee rate based on the current Liquid network conditions. +// Returns the recommended fee rate in BTC/kB +func (r *ElementsRpcWallet) getFeeRate() float64 { + feeRes, err := r.rpcClient.EstimateFee(LiquidTargetBlocks, "ECONOMICAL") + if err != nil || len(feeRes.Errors) > 0 { + log.Debugf("Error estimating fee: %v", err) + if len(feeRes.Errors) > 0 { + log.Debugf(" Errors encountered during fee estimation process: %v", feeRes.Errors) + } + // Return the minimum fee rate in case of an error + return minFeeRateBTCPerKb + } + return math.Max(feeRes.FeeRate, minFeeRateBTCPerKb) +} + // setupWallet checks if the swap wallet is already loaded in elementsd, if not it loads/creates it func (r *ElementsRpcWallet) setupWallet() error { loadedWallets, err := r.rpcClient.ListWallets() From 9dc5c5175d59eabd611c993b09c97fd151f1acf1 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Sun, 28 Jul 2024 13:59:56 +0900 Subject: [PATCH 2/2] multi: fix the preconditions on swap-in/out Fix pre-conditions in swap in and out, to `wallet_balance_sat > swap_amount_sat + estimated_fee This prevents change amounts being unexpectedly posted to dust. I have also integrated the check into the swap package layer as a refactor. --- clightning/clightning_commands.go | 26 +++--------------- clightning/clightning_wallet.go | 4 +-- lnd/lnd_wallet.go | 4 +-- onchain/liquid.go | 12 +++++---- peerswaprpc/server.go | 23 +++------------- swap/actions.go | 12 +++------ swap/service.go | 44 ++++++++++++++++++++++++++++--- swap/services.go | 2 +- swap/swap_out_sender_test.go | 2 +- 9 files changed, 66 insertions(+), 63 deletions(-) diff --git a/clightning/clightning_commands.go b/clightning/clightning_commands.go index a479cee5..3c93a62e 100644 --- a/clightning/clightning_commands.go +++ b/clightning/clightning_commands.go @@ -362,34 +362,16 @@ func (l *SwapIn) Call() (jrpc2.Result, error) { if !l.cl.isPeerConnected(fundingChannels.Id) { return nil, fmt.Errorf("peer is not connected") } - if l.Asset == "lbtc" { + switch l.Asset { + case "lbtc": if !l.cl.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - liquidBalance, err := l.cl.liquidWallet.GetBalance() - if err != nil { - return nil, err - } - if liquidBalance < l.SatAmt { - return nil, errors.New("Not enough balance on liquid liquidWallet") - } - } else if l.Asset == "btc" { + case "btc": if !l.cl.swaps.BitcoinEnabled { return nil, errors.New("bitcoin swaps are not enabled") } - funds, err := l.cl.glightning.ListFunds() - if err != nil { - return nil, err - } - sats := uint64(0) - for _, v := range funds.Outputs { - sats += v.AmountMilliSatoshi.MSat() / 1000 - } - - if sats < l.SatAmt+2000 { - return nil, errors.New("Not enough balance on c-lightning onchain liquidWallet") - } - } else { + default: return nil, errors.New("invalid asset (btc or lbtc)") } diff --git a/clightning/clightning_wallet.go b/clightning/clightning_wallet.go index 3170079a..705947ac 100644 --- a/clightning/clightning_wallet.go +++ b/clightning/clightning_wallet.go @@ -225,11 +225,11 @@ func (cl *ClightningClient) GetOnchainBalance() (uint64, error) { return totalBalance, nil } -// GetFlatSwapOutFee returns an estimated size for the opening transaction. This +// GetFlatOpeningTXFee returns an estimated size for the opening transaction. This // can be used to calculate the amount of the fee invoice and should cover most // but not all cases. For an explanation of the estimation see comments of the // onchain.EstimatedOpeningTxSize. -func (cl *ClightningClient) GetFlatSwapOutFee() (uint64, error) { +func (cl *ClightningClient) GetFlatOpeningTXFee() (uint64, error) { return cl.bitcoinChain.GetFee(onchain.EstimatedOpeningTxSize) } diff --git a/lnd/lnd_wallet.go b/lnd/lnd_wallet.go index f5b00c28..5d26db76 100644 --- a/lnd/lnd_wallet.go +++ b/lnd/lnd_wallet.go @@ -245,11 +245,11 @@ func (l *Client) GetRefundFee() (uint64, error) { return l.bitcoinOnChain.GetFee(250) } -// GetFlatSwapOutFee returns an estimated size for the opening transaction. This +// GetFlatOpeningTXFee returns an estimated size for the opening transaction. This // can be used to calculate the amount of the fee invoice and should cover most // but not all cases. For an explanation of the estimation see comments of the // onchain.EstimatedOpeningTxSize. -func (l *Client) GetFlatSwapOutFee() (uint64, error) { +func (l *Client) GetFlatOpeningTXFee() (uint64, error) { return l.bitcoinOnChain.GetFee(onchain.EstimatedOpeningTxSize) } diff --git a/onchain/liquid.go b/onchain/liquid.go index 1d7b9ded..c7ae7de5 100644 --- a/onchain/liquid.go +++ b/onchain/liquid.go @@ -30,6 +30,10 @@ import ( const ( LiquidCsv = 60 LiquidConfs = 2 + // EstimatedOpeningConfidentialTxSizeBytes is the estimated size of a 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. + EstimatedOpeningConfidentialTxSizeBytes = 3000 ) type LiquidOnChain struct { @@ -528,11 +532,9 @@ func (l *LiquidOnChain) GetRefundFee() (uint64, error) { 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.liquidWallet.GetFee(3000) +// GetFlatOpeningTXFee returns an estimate of the fee for the opening transaction. +func (l *LiquidOnChain) GetFlatOpeningTXFee() (uint64, error) { + return l.liquidWallet.GetFee(EstimatedOpeningConfidentialTxSizeBytes) } func (l *LiquidOnChain) GetAsset() string { diff --git a/peerswaprpc/server.go b/peerswaprpc/server.go index 5da424d3..24283dfb 100644 --- a/peerswaprpc/server.go +++ b/peerswaprpc/server.go @@ -219,31 +219,16 @@ func (p *PeerswapServer) SwapIn(ctx context.Context, request *SwapInRequest) (*S return nil, errors.New("channel is not connected") } - if request.Asset == "lbtc" { + switch request.Asset { + case "lbtc": if !p.swaps.LiquidEnabled { return nil, errors.New("liquid swaps are not enabled") } - - liquidBalance, err := p.liquidWallet.GetBalance() - if err != nil { - return nil, err - } - if liquidBalance < request.SwapAmount+1000 { - return nil, errors.New("Not enough balance on liquid wallet") - } - } else if request.Asset == "btc" { + case "btc": if !p.swaps.BitcoinEnabled { return nil, errors.New("bitcoin swaps are not enabled") } - walletbalance, err := p.lnd.WalletBalance(ctx, &lnrpc.WalletBalanceRequest{}) - if err != nil { - return nil, err - } - if uint64(walletbalance.ConfirmedBalance) < request.SwapAmount+2000 { - return nil, errors.New("Not enough balance on lnd onchain liquidWallet") - } - - } else { + default: return nil, errors.New("invalid asset (btc or lbtc)") } diff --git a/swap/actions.go b/swap/actions.go index c08e5f6b..4ec93336 100644 --- a/swap/actions.go +++ b/swap/actions.go @@ -372,22 +372,18 @@ func (c *CreateSwapOutFromRequestAction) Execute(services *SwapServices, swap *S return swap.HandleError(err) } - openingFee, err := wallet.GetFlatSwapOutFee() + openingFee, err := wallet.GetFlatOpeningTXFee() if err != nil { swap.LastErr = err return swap.HandleError(err) } - // Check if onchain balance is sufficient for swap + fees + some safety net + // Check if onchain balance is sufficient for swap + fees walletBalance, err := wallet.GetOnchainBalance() if err != nil { return swap.HandleError(err) } - - // TODO: this should be looked at in the future - safetynet := uint64(20000) - - if walletBalance < swap.GetAmount()+openingFee+safetynet { + if walletBalance < swap.GetAmount()+openingFee { return swap.HandleError(errors.New("insufficient walletbalance")) } @@ -621,7 +617,7 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev swap.OpeningTxFee = msatAmt / 1000 - expectedFee, err := wallet.GetFlatSwapOutFee() + expectedFee, err := wallet.GetFlatOpeningTXFee() if err != nil { swap.LastErr = err return swap.HandleError(err) diff --git a/swap/service.go b/swap/service.go index 7f6ac99b..5a4e45e3 100644 --- a/swap/service.go +++ b/swap/service.go @@ -447,16 +447,20 @@ func (s *SwapService) SwapIn(peer string, chain string, channelId string, initia if err != nil { return nil, err } - rs, err := s.swapServices.lightning.ReceivableMsat(channelId) if err != nil { return nil, err } - if rs <= amtSat*1000 { return nil, fmt.Errorf("exceeding receivable amount_msat: %d", rs) } - + maximumSwapAmountSat, err := s.estimateMaximumSwapAmountSat(chain) + if err != nil { + return nil, err + } + if amtSat > maximumSwapAmountSat { + return nil, fmt.Errorf("exceeding maximum swap amount: %d", maximumSwapAmountSat) + } var bitcoinNetwork string var elementsAsset string if chain == l_btc_chain { @@ -492,6 +496,40 @@ func (s *SwapService) SwapIn(peer string, chain string, channelId string, initia return swap, nil } +// estimateMaximumSwapAmountSat estimates the maximum swap amount +// in satoshis for the specified chain. +// This retrieves the on-chain balance and opening tx fee from the wallet, +// and calculates the maximum amount available for swapping. +func (s *SwapService) estimateMaximumSwapAmountSat(chain string) (uint64, error) { + if chain == l_btc_chain { + liquidBalance, err := s.swapServices.liquidWallet.GetOnchainBalance() + if err != nil { + return 0, err + } + // estimatedFee is the amount (in satoshis) of the fee for the opening transaction. + estimatedFee, err := s.swapServices.liquidWallet.GetFlatOpeningTXFee() + if err != nil { + return 0, err + } + // Calculate the available balance for swapping. + return liquidBalance - estimatedFee, nil + + } else if chain == btc_chain { + bitcoinBalance, err := s.swapServices.bitcoinWallet.GetOnchainBalance() + if err != nil { + return 0, err + } + // estimatedFee is the amount (in satoshis) of the fee for the opening transaction. + estimatedFee, err := s.swapServices.bitcoinWallet.GetFlatOpeningTXFee() + if err != nil { + return 0, err + } + // Calculate the available balance for swapping. + return bitcoinBalance - estimatedFee, nil + } + return 0, errors.New("invalid chain") +} + // OnSwapInRequestReceived creates a new swap-in process and sends the event to the swap statemachine func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, message *SwapInRequestMessage) error { err := s.swapServices.lightning.CanSpend(message.Amount * 1000) diff --git a/swap/services.go b/swap/services.go index 6e70fa39..25788d69 100644 --- a/swap/services.go +++ b/swap/services.go @@ -78,7 +78,7 @@ type Wallet interface { GetOutputScript(params *OpeningParams) ([]byte, error) NewAddress() (string, error) GetRefundFee() (uint64, error) - GetFlatSwapOutFee() (uint64, error) + GetFlatOpeningTXFee() (uint64, error) GetAsset() string GetNetwork() string GetOnchainBalance() (uint64, error) diff --git a/swap/swap_out_sender_test.go b/swap/swap_out_sender_test.go index 2ca0ee6c..68013a2d 100644 --- a/swap/swap_out_sender_test.go +++ b/swap/swap_out_sender_test.go @@ -478,7 +478,7 @@ func (d *dummyChain) GetRefundFee() (uint64, error) { return 100, nil } -func (d *dummyChain) GetFlatSwapOutFee() (uint64, error) { +func (d *dummyChain) GetFlatOpeningTXFee() (uint64, error) { return 100, nil }