Skip to content

Commit

Permalink
feat: withdraw custom amount from savings (#597)
Browse files Browse the repository at this point in the history
* feat: withdraw custom amount from savings

* feat: simplify withdraw onchain funds ui
  • Loading branch information
rolznz authored Sep 4, 2024
1 parent 6e78d81 commit ae43099
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 131 deletions.
4 changes: 2 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,11 +575,11 @@ func (api *api) SignMessage(ctx context.Context, message string) (*SignMessageRe
}, nil
}

func (api *api) RedeemOnchainFunds(ctx context.Context, toAddress string) (*RedeemOnchainFundsResponse, error) {
func (api *api) RedeemOnchainFunds(ctx context.Context, toAddress string, amount 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)
txId, err := api.svc.GetLNClient().RedeemOnchainFunds(ctx, toAddress, amount, sendAll)
if err != nil {
return nil, err
}
Expand Down
4 changes: 3 additions & 1 deletion api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,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) (*RedeemOnchainFundsResponse, error)
RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, sendAll bool) (*RedeemOnchainFundsResponse, error)
GetBalances(ctx context.Context) (*BalancesResponse, error)
ListTransactions(ctx context.Context, limit uint64, offset uint64) (*ListTransactionsResponse, error)
SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error)
Expand Down Expand Up @@ -187,6 +187,8 @@ type UpdateChannelRequest = lnclient.UpdateChannelRequest

type RedeemOnchainFundsRequest struct {
ToAddress string `json:"toAddress"`
Amount uint64 `json:"amount"`
SendAll bool `json:"sendAll"`
}

type RedeemOnchainFundsResponse struct {
Expand Down
294 changes: 187 additions & 107 deletions frontend/src/screens/wallet/WithdrawOnchainFunds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "src/components/ui/alert-dialog";
import { Button } from "src/components/ui/button";
import { Checkbox } from "src/components/ui/checkbox";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
Expand All @@ -21,72 +31,63 @@ export default function WithdrawOnchainFunds() {
const { toast } = useToast();
const { data: balances } = useBalances();
const [onchainAddress, setOnchainAddress] = React.useState("");
const [confirmOnchainAddress, setConfirmOnchainAddress] = React.useState("");
const [checkedConfirmation, setCheckedConfirmation] = React.useState(false);
const [amount, setAmount] = React.useState("");
const [sendAll, setSendAll] = React.useState(false);
const [transactionId, setTransactionId] = React.useState("");
const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false);

const copy = (text: string) => {
copyToClipboard(text, toast);
};

const redeemFunds = React.useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 100));
if (!onchainAddress) {
throw new Error("No onchain address");
}

if (onchainAddress !== confirmOnchainAddress) {
throw new Error(
"Onchain addresses do not match. Please check the onchain addresses you provided"
);
}

if (!checkedConfirmation) {
throw new Error("Please confirm");
}
} catch (error) {
console.error(error);
toast({
title: "Something went wrong",
description: "" + error,
variant: "destructive",
});
setLoading(false);
return;
const redeemFunds = React.useCallback(async () => {
setLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 100));
if (!onchainAddress) {
throw new Error("No onchain address");
}
} catch (error) {
console.error(error);
toast({
title: "Something went wrong",
description: "" + error,
variant: "destructive",
});
setLoading(false);
return;
}

try {
const response = await request<RedeemOnchainFundsResponse>(
"/api/wallet/redeem-onchain-funds",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ toAddress: onchainAddress }),
}
);
console.info("Redeemed onchain funds", response);
if (!response?.txId) {
throw new Error("No address in response");
try {
const response = await request<RedeemOnchainFundsResponse>(
"/api/wallet/redeem-onchain-funds",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
toAddress: onchainAddress,
amount: +amount,
sendAll,
}),
}
setTransactionId(response.txId);
} catch (error) {
console.error(error);
toast({
variant: "destructive",
title: "Failed to redeem onchain funds",
description: "" + error,
});
);
console.info("Redeemed onchain funds", response);
if (!response?.txId) {
throw new Error("No address in response");
}
setLoading(false);
},
[checkedConfirmation, confirmOnchainAddress, onchainAddress, toast]
);
setTransactionId(response.txId);
} catch (error) {
console.error(error);
toast({
variant: "destructive",
title: "Failed to redeem onchain funds",
description: "" + error,
});
}
setLoading(false);
}, [amount, onchainAddress, sendAll, toast]);

if (transactionId) {
return (
Expand Down Expand Up @@ -139,26 +140,93 @@ export default function WithdrawOnchainFunds() {
/>

<div className="max-w-lg">
{!!balances?.onchain.reserved && (
<Alert className="mb-4">
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Channel Anchor Reserves will be depleted</AlertTitle>
<AlertDescription>
You have channels open and this withdrawal will use some or all of
your anchor reserves to publish the transaction, which may make it
harder to close channels without depositing additional onchain
funds to your savings balance.
</AlertDescription>
</Alert>
)}
<p>
Your savings balance will be withdrawn to the onchain bitcoin wallet
address you specify below. Please make sure you are the owner of this
address and that it is for an{" "}
<span className="font-bold">external wallet</span> that you control
and have the seed phrase for.
address you specify below.
</p>
<form onSubmit={redeemFunds} className="grid gap-5 mt-4">
<form
onSubmit={(e) => {
e.preventDefault();
setConfirmDialogOpen(true);
}}
className="grid gap-5 mt-4"
>
<div className="">
<Label htmlFor="amount">Amount</Label>
<div className="flex justify-between items-center mb-1">
<p className="text-sm text-muted-foreground">
Current onchain balance:{" "}
{new Intl.NumberFormat().format(balances.onchain.spendable)}{" "}
sats
</p>
<div className="flex items-center gap-1">
<Checkbox
id="send-all"
onCheckedChange={() => setSendAll(!sendAll)}
/>
<Label htmlFor="send-all" className="text-xs">
Send All
</Label>
</div>
</div>
{!sendAll && (
<Input
id="amount"
type="number"
value={amount}
required
onChange={(e) => {
setAmount(e.target.value);
}}
/>
)}
{sendAll && (
<Alert className="mt-4">
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Entire wallet balance will be sent</AlertTitle>
<AlertDescription>
Your entire wallet balance
{balances.onchain.reserved > 0 && (
<>
{" "}
including reserves (
{new Intl.NumberFormat().format(
balances.onchain.reserved
)}{" "}
sats)
</>
)}{" "}
will be sent minus onchain transaction fees. The exact amount
cannot be determined until the payment is made.
{balances.onchain.reserved && (
<>
{" "}
You have channels open and this withdrawal will deplete
your anchor reserves, which may make it harder to close
channels without depositing additional onchain funds to
your savings balance.
</>
)}
</AlertDescription>
</Alert>
)}
{!!balances?.onchain.reserved &&
!sendAll &&
+amount > balances.onchain.spendable * 0.9 && (
<Alert className="mt-4">
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>
Channel Anchor Reserves may be depleted
</AlertTitle>
<AlertDescription>
You have channels open and this withdrawal may deplete your
anchor reserves, which may make it harder to close channels
without depositing additional onchain funds to your savings
balance.
</AlertDescription>
</Alert>
)}
</div>
<div className="">
<Label htmlFor="onchain-address">Onchain Address</Label>
<Input
Expand All @@ -171,41 +239,53 @@ export default function WithdrawOnchainFunds() {
}}
/>
</div>
<div className="">
<Label htmlFor="confirm-onchain-address">
Confirm Onchain Address
</Label>
<Input
id="confirm-onchain-address"
type="text"
value={confirmOnchainAddress}
required
onChange={(e) => {
setConfirmOnchainAddress(e.target.value);
}}
/>
</div>
<div>
<div className="flex items-center mt-5">
<Checkbox
id="confirm"
required
onCheckedChange={() =>
setCheckedConfirmation(!checkedConfirmation)
}
/>
<Label htmlFor="confirm" className="ml-2">
I'm the owner of this wallet address and{" "}
<span className="bold">I realize no-one can help me</span> if I
send my funds to the wrong address. This transaction cannot be
reversed.
</Label>
</div>
</div>

<p className="text-sm text-muted-foreground">
Please double-check the destination address. This transaction cannot
be reversed.
</p>

<div>
<LoadingButton loading={isLoading} type="submit">
Withdraw
</LoadingButton>
<AlertDialog
onOpenChange={setConfirmDialogOpen}
open={confirmDialogOpen}
>
<Button>Withdraw</Button>

<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Confirm Onchain Transaction
</AlertDialogTitle>
<AlertDialogDescription>
<p>
Please confirm your payment to{" "}
<span className="font-bold">{onchainAddress}</span>
</p>
<p className="mt-4">
Amount:{" "}
<span className="font-bold">
{sendAll ? (
"entire savings balance"
) : (
<>{new Intl.NumberFormat().format(+amount)} sats</>
)}
</span>
</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>

<LoadingButton
loading={isLoading}
onClick={() => redeemFunds()}
>
Confirm
</LoadingButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</form>
</div>
Expand Down
2 changes: 1 addition & 1 deletion http/http_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ func (httpSvc *HttpService) redeemOnchainFundsHandler(c echo.Context) error {
})
}

redeemOnchainFundsResponse, err := httpSvc.api.RedeemOnchainFunds(ctx, redeemOnchainFundsRequest.ToAddress)
redeemOnchainFundsResponse, err := httpSvc.api.RedeemOnchainFunds(ctx, redeemOnchainFundsRequest.ToAddress, redeemOnchainFundsRequest.Amount, redeemOnchainFundsRequest.SendAll)

if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Expand Down
6 changes: 5 additions & 1 deletion lnclient/breez/breez.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,11 @@ func (bs *BreezService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchai
}, nil
}

func (bs *BreezService) RedeemOnchainFunds(ctx context.Context, toAddress string) (txId string, err error) {
func (bs *BreezService) RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, sendAll bool) (txId string, err error) {
if !sendAll {
return "", errors.New("only send all is supported")
}

if toAddress == "" {
return "", errors.New("No address provided")
}
Expand Down
2 changes: 1 addition & 1 deletion lnclient/cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (cs *CashuService) GetOnchainBalance(ctx context.Context) (*lnclient.Onchai
}, nil
}

func (cs *CashuService) RedeemOnchainFunds(ctx context.Context, toAddress string) (string, error) {
func (cs *CashuService) RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, sendAll bool) (string, error) {
return "", nil
}

Expand Down
Loading

0 comments on commit ae43099

Please sign in to comment.