From 9b6e88f1cc35e7333152a615782587e8d6344ee9 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Thu, 29 May 2025 22:08:46 +0530 Subject: [PATCH] feat: add option to edit fee while withdrawing (#1044) * feat: add option to edit fee while withdrawing * chore: add alert if fee rate exceeds fastest fee * fix: tests * feat: add support for LDK * chore: do not accept decimal fee rates in frontend * chore: minor withdraw UI improvements * chore: make fee rate optional and update mockery * chore: remove unnecessary else * chore: do not format lottie files * feat: add lotties files to prettier ignore --------- Co-authored-by: Roland Bewick --- .prettierignore | 1 + README.md | 2 - api/api.go | 4 +- api/models.go | 9 +- .../screens/wallet/WithdrawOnchainFunds.tsx | 93 ++- http/http_service.go | 2 +- lnclient/cashu/cashu.go | 2 +- lnclient/ldk/ldk.go | 12 +- lnclient/lnd/lnd.go | 21 +- lnclient/models.go | 2 +- lnclient/phoenixd/phoenixd.go | 2 +- tests/mock_ln_client.go | 2 +- tests/mocks/Config.go | 122 +++- tests/mocks/LNClient.go | 617 ++++++++++++++---- tests/mocks/Service.go | 10 +- wails/wails_handlers.go | 2 +- 16 files changed, 734 insertions(+), 169 deletions(-) diff --git a/.prettierignore b/.prettierignore index e69de29b..c4c1df60 100644 --- a/.prettierignore +++ b/.prettierignore @@ -0,0 +1 @@ +frontend/src/assets/lotties \ No newline at end of file diff --git a/README.md b/README.md index d2f10f7f..4edf3cef 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,6 @@ Note that the PostgreSQL user account must be granted appropriate permissions to We use [testify/mock](https://github.com/stretchr/testify) to facilitate mocking in tests. Instead of writing mocks manually, we generate them using [vektra/mockery](https://github.com/vektra/mockery). To regenerate them, [install mockery](https://vektra.github.io/mockery/latest/installation) and run it in the project's root directory: -> Use `go install github.com/vektra/mockery/v2@v2.52.1` as go 1.24.0 is currently not supported by Alby Hub. - $ mockery Mockery loads its configuration from the .mockery.yaml file in the root directory of this project. To add mocks for new interfaces, add them to the configuration file and run mockery. diff --git a/api/api.go b/api/api.go index 6cc7af99..c9b093a4 100644 --- a/api/api.go +++ b/api/api.go @@ -755,11 +755,11 @@ func (api *api) SignMessage(ctx context.Context, message string) (*SignMessageRe }, nil } -func (api *api) RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, sendAll bool) (*RedeemOnchainFundsResponse, error) { +func (api *api) RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, feeRate *uint64, sendAll bool) (*RedeemOnchainFundsResponse, error) { if api.svc.GetLNClient() == nil { return nil, errors.New("LNClient not started") } - txId, err := api.svc.GetLNClient().RedeemOnchainFunds(ctx, toAddress, amount, sendAll) + txId, err := api.svc.GetLNClient().RedeemOnchainFunds(ctx, toAddress, amount, feeRate, sendAll) if err != nil { return nil, err } diff --git a/api/models.go b/api/models.go index ffeda0f1..3282cc90 100644 --- a/api/models.go +++ b/api/models.go @@ -34,7 +34,7 @@ type API interface { GetNewOnchainAddress(ctx context.Context) (string, error) GetUnusedOnchainAddress(ctx context.Context) (string, error) SignMessage(ctx context.Context, message string) (*SignMessageResponse, error) - RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, sendAll bool) (*RedeemOnchainFundsResponse, error) + RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, feeRate *uint64, sendAll bool) (*RedeemOnchainFundsResponse, error) GetBalances(ctx context.Context) (*BalancesResponse, error) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) @@ -235,9 +235,10 @@ type CloseChannelResponse = lnclient.CloseChannelResponse type UpdateChannelRequest = lnclient.UpdateChannelRequest type RedeemOnchainFundsRequest struct { - ToAddress string `json:"toAddress"` - Amount uint64 `json:"amount"` - SendAll bool `json:"sendAll"` + ToAddress string `json:"toAddress"` + Amount uint64 `json:"amount"` + FeeRate *uint64 `json:"feeRate"` + SendAll bool `json:"sendAll"` } type RedeemOnchainFundsResponse struct { diff --git a/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx b/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx index 571fd137..abad9595 100644 --- a/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx +++ b/frontend/src/screens/wallet/WithdrawOnchainFunds.tsx @@ -1,4 +1,9 @@ -import { AlertTriangleIcon, CopyIcon, ExternalLinkIcon } from "lucide-react"; +import { + AlertTriangleIcon, + ChevronDown, + CopyIcon, + ExternalLinkIcon, +} from "lucide-react"; import React from "react"; import AppHeader from "src/components/AppHeader"; import ExternalLink from "src/components/ExternalLink"; @@ -23,6 +28,8 @@ import { useToast } from "src/components/ui/use-toast"; import { ONCHAIN_DUST_SATS } from "src/constants"; import { useBalances } from "src/hooks/useBalances"; import { useChannels } from "src/hooks/useChannels"; +import { useInfo } from "src/hooks/useInfo"; +import { useMempoolApi } from "src/hooks/useMempoolApi"; import { copyToClipboard } from "src/lib/clipboard"; import { RedeemOnchainFundsResponse } from "src/types"; @@ -31,11 +38,20 @@ import { request } from "src/utils/request"; export default function WithdrawOnchainFunds() { const [isLoading, setLoading] = React.useState(false); const { toast } = useToast(); + const { data: info } = useInfo(); const { data: balances } = useBalances(); + const { data: recommendedFees } = useMempoolApi<{ + fastestFee: number; + halfHourFee: number; + economyFee: number; + minimumFee: number; + }>("/v1/fees/recommended"); const { data: channels } = useChannels(); const [onchainAddress, setOnchainAddress] = React.useState(""); const [amount, setAmount] = React.useState(""); + const [feeRate, setFeeRate] = React.useState(""); const [sendAll, setSendAll] = React.useState(false); + const [showAdvanced, setShowAdvanced] = React.useState(false); const [transactionId, setTransactionId] = React.useState(""); const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false); @@ -73,6 +89,7 @@ export default function WithdrawOnchainFunds() { toAddress: onchainAddress, amount: +amount, sendAll, + ...(feeRate && { feeRate: +feeRate }), }), } ); @@ -90,7 +107,7 @@ export default function WithdrawOnchainFunds() { }); } setLoading(false); - }, [amount, onchainAddress, sendAll, toast]); + }, [amount, feeRate, onchainAddress, sendAll, toast]); if (transactionId) { return ( @@ -123,7 +140,7 @@ export default function WithdrawOnchainFunds() { ); } - if (!balances) { + if (!info || !balances || !recommendedFees) { return ; } @@ -226,12 +243,72 @@ export default function WithdrawOnchainFunds() { setOnchainAddress(e.target.value); }} /> +

+ Please double-check the destination address. This transaction + cannot be reversed. +

- -

- Please double-check the destination address. This transaction cannot - be reversed. -

+ {(info?.backendType === "LDK" || info?.backendType === "LND") && ( + <> + {showAdvanced && ( +
+ + { + setFeeRate(e.target.value); + }} + /> +

+ {" "} + {" "} + + mempool.space + +

+
+ )} + {!showAdvanced && ( + + )} + + )}