Skip to content

Commit

Permalink
Merge pull request #302 from ElementsProject/wallet/fee-rate-estimatio
Browse files Browse the repository at this point in the history
wallet: add fee rate estimation to FundRawTx
  • Loading branch information
YusukeShimizu authored Aug 2, 2024
2 parents e825b73 + 9dc5c51 commit 5935fb4
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 65 deletions.
26 changes: 4 additions & 22 deletions clightning/clightning_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}

Expand Down
4 changes: 2 additions & 2 deletions clightning/clightning_wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
4 changes: 2 additions & 2 deletions lnd/lnd_wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
12 changes: 7 additions & 5 deletions onchain/liquid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 4 additions & 19 deletions peerswaprpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}

Expand Down
12 changes: 4 additions & 8 deletions swap/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}

Expand Down Expand Up @@ -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)
Expand Down
44 changes: 41 additions & 3 deletions swap/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion swap/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion swap/swap_out_sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
31 changes: 29 additions & 2 deletions wallet/elementsrpcwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
Expand Down

0 comments on commit 5935fb4

Please sign in to comment.