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 } 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()