diff --git a/go.mod b/go.mod index 90540f96..b343ab69 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.2 github.com/btcsuite/btcd/btcutil/psbt v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 - github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a + github.com/elementsproject/glightning v0.0.0-20240802020216-b4e19b004ca4 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 github.com/jessevdk/go-flags v1.5.0 diff --git a/go.sum b/go.sum index f8289c28..71f3c6e2 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,8 @@ github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 h1:sE4tvxWw01v7K3M github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a h1:xnVQmVqGmSs3m8zPQF4iYEYiUAmJx8MlT9vJ3lAaOjc= github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a/go.mod h1:YAdIeSyx8VEhDCtEaGOJLmWNpPaQ3x4vYSAj9Vrppdo= +github.com/elementsproject/glightning v0.0.0-20240802020216-b4e19b004ca4 h1:7CXEOi0uTeMrwLfFmHsbBS5yRfpSAHALwa9k9Rtl1Vw= +github.com/elementsproject/glightning v0.0.0-20240802020216-b4e19b004ca4/go.mod h1:YAdIeSyx8VEhDCtEaGOJLmWNpPaQ3x4vYSAj9Vrppdo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/lwk/error.go b/lwk/error.go new file mode 100644 index 00000000..f947a1d8 --- /dev/null +++ b/lwk/error.go @@ -0,0 +1,37 @@ +package lwk + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" +) + +// electrumRPCError represents the structure of an RPC error response +type electrumRPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// Regular expression to match RPC error messages with any prefix +var re = regexp.MustCompile(`^(.*) RPC error: (.*)$`) + +// parseRPCError parses an error and extracts the RPC error code and message if present +func parseRPCError(err error) (*electrumRPCError, error) { + var rpcErr electrumRPCError + errStr := err.Error() + + matches := re.FindStringSubmatch(errStr) + + if len(matches) == 3 { // Prefix and JSON payload extracted successfully + errJSON := matches[2] + if jerr := json.Unmarshal([]byte(errJSON), &rpcErr); jerr != nil { + return nil, fmt.Errorf("error parsing rpc error: %v", jerr) + } + } else { + // If no RPC error pattern is found, return the original error + return nil, errors.New(errStr) + } + + return &rpcErr, nil +} diff --git a/lwk/error_test.go b/lwk/error_test.go new file mode 100644 index 00000000..ad0fcef7 --- /dev/null +++ b/lwk/error_test.go @@ -0,0 +1,54 @@ +package lwk + +import ( + "errors" + "testing" +) + +func TestParseRPCError(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + err error + expectedCode int + expectedMsg string + wantErr bool + }{ + "Valid RPC error": { + err: errors.New("sendrawtransaction RPC error: {\"code\":-26,\"message\":\"min relay fee not met\"}"), + expectedCode: -26, + expectedMsg: "min relay fee not met", + wantErr: false, + }, + "Invalid JSON payload": { + + err: errors.New("RPC error: {invalid json}"), + expectedCode: 0, + expectedMsg: "", + wantErr: true, + }, + "No RPC error pattern": { + err: errors.New("Some other error"), + expectedCode: 0, + expectedMsg: "", + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + rpcErr, err := parseRPCError(tc.err) + if (err != nil) != tc.wantErr { + t.Errorf("wantErr: %v, got error: %v", tc.wantErr, err) + } + if err == nil { + if rpcErr.Code != tc.expectedCode { + t.Errorf("expected code: %d, got: %d", tc.expectedCode, rpcErr.Code) + } + if rpcErr.Message != tc.expectedMsg { + t.Errorf("expected message: %s, got: %s", tc.expectedMsg, rpcErr.Message) + } + } + }) + } +} diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 00a9a6cc..419645fd 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -3,6 +3,7 @@ package lwk import ( "context" "errors" + "fmt" "math" "strings" @@ -258,6 +259,13 @@ func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { defer cancel() res, err := r.electrumClient.BroadcastTransaction(ctx, txHex) if err != nil { + rpcErr, pErr := parseRPCError(err) + if pErr != nil { + return "", fmt.Errorf("error parsing rpc error: %v", pErr) + } + if rpcErr.Code == -26 { + return "", wallet.MinRelayFeeNotMetError + } return "", err } return res, nil diff --git a/onchain/liquid.go b/onchain/liquid.go index c7ae7de5..cc9a49e6 100644 --- a/onchain/liquid.go +++ b/onchain/liquid.go @@ -81,14 +81,38 @@ func (l *LiquidOnChain) CreateOpeningTransaction(swapParams *swap.OpeningParams) return txHex, blindedScriptAddr, txId, fee, vout, nil } +// feeAmountPlaceholder is a placeholder for the fee amount +const feeAmountPlaceholder = uint64(500) + func (l *LiquidOnChain) CreatePreimageSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams) (string, string, string, error) { + fee, err := l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, true))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + txId, txHex, newAddr, err := l.createPreimageSpendingTransaction(swapParams, claimParams, fee) + if err == nil { + return txId, txHex, newAddr, nil + } + if !errors.Is(err, wallet.MinRelayFeeNotMetError) { + return "", "", "", err + } + fee, err = l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, false))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + return l.createPreimageSpendingTransaction(swapParams, claimParams, fee) +} + +func (l *LiquidOnChain) createPreimageSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, fee uint64) (string, string, string, error) { newAddr, err := l.liquidWallet.GetAddress() if err != nil { return "", "", "", err } l.AddBlindingRandomFactors(claimParams) - tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, 0, 0) + tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, 0, fee) if err != nil { return "", "", "", err } @@ -118,12 +142,33 @@ func (l *LiquidOnChain) CreatePreimageSpendingTransaction(swapParams *swap.Openi } func (l *LiquidOnChain) CreateCsvSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams) (txId, txHex, address string, error error) { + fee, err := l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, true))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + txId, txHex, newAddr, err := l.createCsvSpendingTransaction(swapParams, claimParams, fee) + if err == nil { + return txId, txHex, newAddr, nil + } + if !errors.Is(err, wallet.MinRelayFeeNotMetError) { + return "", "", "", err + } + fee, err = l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, false))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + return l.createCsvSpendingTransaction(swapParams, claimParams, fee) +} + +func (l *LiquidOnChain) createCsvSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, fee uint64) (txId, txHex, address string, error error) { newAddr, err := l.liquidWallet.GetAddress() if err != nil { return "", "", "", err } l.AddBlindingRandomFactors(claimParams) - tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, LiquidCsv, 0) + tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, LiquidCsv, fee) if err != nil { return "", "", "", err } @@ -140,11 +185,28 @@ func (l *LiquidOnChain) CreateCsvSpendingTransaction(swapParams *swap.OpeningPar } func (l *LiquidOnChain) CreateCoopSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, takerSigner swap.Signer) (txId, txHex, address string, error error) { - refundAddr, err := l.NewAddress() + fee, err := l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindCoop, true))) if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + txId, txHex, newAddr, err := l.createCoopSpendingTransaction(swapParams, claimParams, takerSigner, fee) + if err == nil { + return txId, txHex, newAddr, nil + } + if !errors.Is(err, wallet.MinRelayFeeNotMetError) { return "", "", "", err } - refundFee, err := l.liquidWallet.GetFee(int64(l.getCoopClaimTxSize())) + fee, err = l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindCoop, false))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + return l.createCoopSpendingTransaction(swapParams, claimParams, takerSigner, fee) +} + +func (l *LiquidOnChain) createCoopSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, takerSigner swap.Signer, fee uint64) (txId, txHex, address string, error error) { + refundAddr, err := l.NewAddress() if err != nil { return "", "", "", err } @@ -156,7 +218,7 @@ func (l *LiquidOnChain) CreateCoopSpendingTransaction(swapParams *swap.OpeningPa if err != nil { return "", "", "", err } - spendingTx, sigHash, err := l.createSpendingTransaction(claimParams.OpeningTxHex, swapParams.Amount, 0, l.asset, redeemScript, refundAddr, refundFee, swapParams.BlindingKey, claimParams.EphemeralKey, claimParams.OutputAssetBlindingFactor, claimParams.BlindingSeed) + spendingTx, sigHash, err := l.createSpendingTransaction(claimParams.OpeningTxHex, swapParams.Amount, 0, l.asset, redeemScript, refundAddr, fee, swapParams.BlindingKey, claimParams.EphemeralKey, claimParams.OutputAssetBlindingFactor, claimParams.BlindingSeed) if err != nil { return "", "", "", err } @@ -235,6 +297,9 @@ func (l *LiquidOnChain) prepareSpendingTransaction(swapParams *swap.OpeningParam // CreateSpendingTransaction returns the spendningTransaction for the swap func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmount uint64, csv uint32, asset, redeemScript []byte, redeemAddr string, preparedFee uint64, blindingKey, ephemeralPrivKey *btcec.PrivateKey, outputAbf, seed []byte) (tx *transaction.Transaction, sigHash [32]byte, err error) { + if preparedFee == 0 { + return nil, [32]byte{}, errors.New("fee must be set other than 0") + } firstTx, err := transaction.NewTxFromHex(openingTxHex) if err != nil { log.Infof("error creating first tx %s, %v", openingTxHex, err) @@ -263,16 +328,7 @@ func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmoun return nil, [32]byte{}, errors.New(fmt.Sprintf("Tx value is not equal to the swap contract expected: %v, tx: %v", swapAmount, ubRes.Value)) } - feeAmountPlaceholder := uint64(500) - fee := preparedFee - if preparedFee == 0 { - fee, err = l.liquidWallet.GetFee(int64(l.getClaimTxSize())) - if err != nil { - fee = feeAmountPlaceholder - } - } - - outputValue := ubRes.Value - fee + outputValue := ubRes.Value - preparedFee finalVbfArgs := confidential.FinalValueBlindingFactorArgs{ InValues: []uint64{ubRes.Value}, @@ -366,7 +422,7 @@ func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmoun spendingTx.Outputs = append(spendingTx.Outputs, receiverOutput) // add feeoutput - feeValue, _ := elementsutil.ValueToBytes(fee) + feeValue, _ := elementsutil.ValueToBytes(preparedFee) feeScript := []byte{} feeOutput := transaction.NewTxOutput(asset, feeValue, feeScript) spendingTx.Outputs = append(spendingTx.Outputs, feeOutput) @@ -378,12 +434,33 @@ func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmoun return spendingTx, sigHash, nil } -func (l *LiquidOnChain) getClaimTxSize() int { - return 1350 -} +type transactionKind string + +const ( + transactionKindPreimageSpending transactionKind = "preimage" + transactionKindCoop transactionKind = "coop" + transactionKindOpening transactionKind = "open" + transactionKindCSV transactionKind = "csv" +) -func (l *LiquidOnChain) getCoopClaimTxSize() int { - return 1360 +func getEstimatedTxSize(t transactionKind, ctDiscount bool) int { + txsize := 0 + switch t { + case transactionKindPreimageSpending: + txsize = 1350 + case transactionKindCoop: + txsize = 1360 + case transactionKindOpening: + txsize = EstimatedOpeningConfidentialTxSizeBytes + case transactionKindCSV: + txsize = 1350 + default: + return 1360 + } + if ctDiscount { + return txsize / 4 + } + return txsize } func (l *LiquidOnChain) TxIdFromHex(txHex string) (string, error) { @@ -529,12 +606,12 @@ func (l *LiquidOnChain) Blech32ToScript(blech32Addr string) ([]byte, error) { } func (l *LiquidOnChain) GetRefundFee() (uint64, error) { - return l.liquidWallet.GetFee(int64(l.getClaimTxSize())) + return l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindCoop, false))) } // GetFlatOpeningTXFee returns an estimate of the fee for the opening transaction. func (l *LiquidOnChain) GetFlatOpeningTXFee() (uint64, error) { - return l.liquidWallet.GetFee(EstimatedOpeningConfidentialTxSizeBytes) + return l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindOpening, true))) } func (l *LiquidOnChain) GetAsset() string { diff --git a/test/lwk_cln_test.go b/test/lwk_cln_test.go index a2dbf877..fed4bdf7 100644 --- a/test/lwk_cln_test.go +++ b/test/lwk_cln_test.go @@ -99,7 +99,7 @@ func Test_ClnCln_LWK_SwapIn(t *testing.T) { bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9))) defer func() { if t.Failed() { - filter := os.Getenv("PEERSWAP_TEST_FILTER") + filter := "" pprintFail( tailableProcess{ p: bitcoind.DaemonProcess, diff --git a/wallet/elementsrpcwallet.go b/wallet/elementsrpcwallet.go index 624d6313..a4476fbb 100644 --- a/wallet/elementsrpcwallet.go +++ b/wallet/elementsrpcwallet.go @@ -3,11 +3,12 @@ package wallet import ( "errors" "fmt" - "math" + "strings" "github.com/elementsproject/glightning/gelements" + "github.com/elementsproject/glightning/jrpc2" "github.com/elementsproject/peerswap/log" "github.com/elementsproject/peerswap/swap" "github.com/vulpemventures/go-elements/address" @@ -20,6 +21,11 @@ var ( AlreadyLoadedError = errors.New("wallet is already loaded") ) +const ( + // https://github.com/ElementsProject/elements/releases/tag/elements-23.2.2 + elementsdFeeDiscountedVersion = 230202 +) + type RpcClient interface { GetNewAddress(addrType int) (string, error) SendToAddress(address string, amount string) (string, error) @@ -35,6 +41,7 @@ type RpcClient interface { EstimateFee(blocks uint32, mode string) (*gelements.FeeResponse, error) SetLabel(address, label string) error Ping() (bool, error) + GetNetworkInfo() (*gelements.NetworkInfo, error) } // ElementsRpcWallet uses the elementsd rpc wallet @@ -92,8 +99,12 @@ func (r *ElementsRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.Openi if err != nil { return "", "", 0, err } + feerate, err := r.getFeeRate() + if err != nil { + return "", "", 0, err + } fundedTx, err := r.rpcClient.FundRawWithOptions(txHex, &gelements.FundRawOptions{ - FeeRate: fmt.Sprintf("%f", r.getFeeRate()), + FeeRate: fmt.Sprintf("%f", feerate), }, nil) if err != nil { @@ -110,27 +121,6 @@ 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() @@ -188,25 +178,49 @@ func (r *ElementsRpcWallet) SendToAddress(address string, amount uint64) (string } func (r *ElementsRpcWallet) SendRawTx(txHex string) (string, error) { - return r.rpcClient.SendRawTx(txHex) + raw, err := r.rpcClient.SendRawTx(txHex) + if err != nil { + errWithCode, ok := err.(*jrpc2.RpcError) + if ok && errWithCode.Code == -26 { + return "", MinRelayFeeNotMetError + } + } + return raw, err } -func (r *ElementsRpcWallet) GetFee(txSize int64) (uint64, error) { +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, 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 + log.Debugf(" Errors encountered during fee estimation process: %v", feeRes.Errors) + return minFeeRateBTCPerKb, nil } - // assume largest witness - fee := satPerByte * float64(txSize) + return math.Max(minFeeRateBTCPerKb, feeRes.FeeRate), nil +} + +const ( + // 1 kb = 1000 bytes + kb = 1000 + btcToSatoshiExp = 8 +) +func (r *ElementsRpcWallet) GetFee(txSize int64) (uint64, error) { + feeRate, err := r.getFeeRate() + if err != nil { + return 0, fmt.Errorf("error getting fee rate: %v", err) + } + satPerByte := feeRate * math.Pow10(btcToSatoshiExp) / kb + fee := satPerByte * float64(txSize) return uint64(fee), nil } diff --git a/wallet/wallet.go b/wallet/wallet.go index 9c2ed177..bf9bfa4c 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -7,7 +7,8 @@ import ( ) var ( - NotEnoughBalanceError = errors.New("Not enough balance on utxos") + NotEnoughBalanceError = errors.New("Not enough balance on utxos") + MinRelayFeeNotMetError = errors.New("MinRelayFee not met") ) const (