mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: encrypt xpub for auto swap outs (#1973)
* feat: encrypt xpub for auto swap out * fix: include encryptionKey in the cache key (sha256) * fix: make cache 2 level nested map * chore: comment * fix: missing import * fix: use ValidateXpub * fix: cleanup * fix: cleanup * fix: better name * fix: cleanup * fix: cleanup * fix: cleanup * fix: open dialog to ask password on autoswap xpub * fix: reject extended private keys * fix: fix amount comp * chore: improve autoswap destination handling and form submission flow * fix: clear cached xpub when enabling autoswaps * fix: autoswap destination validation and xpub handling * chore: add mutex for auto swap xpub * chore: add copy icon for destination in auto swap info --------- Co-authored-by: fmar <fmar@fmar.dev> Co-authored-by: anon <anon@anon.com> Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
This commit is contained in:
parent
06b21139bc
commit
354099cbff
6 changed files with 408 additions and 244 deletions
39
api/api.go
39
api/api.go
|
|
@ -810,10 +810,18 @@ func (api *api) RefundSwap(refundSwapRequest *RefundSwapRequest) error {
|
|||
}
|
||||
|
||||
func (api *api) GetAutoSwapConfig() (*GetAutoSwapConfigResponse, error) {
|
||||
if api.svc.GetSwapsService() == nil {
|
||||
return nil, errors.New("SwapsService not started")
|
||||
}
|
||||
|
||||
swapOutBalanceThresholdStr, _ := api.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
|
||||
swapOutAmountStr, _ := api.cfg.Get(config.AutoSwapAmountKey, "")
|
||||
swapOutDestination, _ := api.cfg.Get(config.AutoSwapDestinationKey, "")
|
||||
|
||||
if xpub := api.svc.GetSwapsService().GetDecryptedAutoSwapXpub(); xpub != "" {
|
||||
swapOutDestination = xpub
|
||||
}
|
||||
|
||||
swapOutEnabled := swapOutBalanceThresholdStr != "" && swapOutAmountStr != ""
|
||||
var swapOutBalanceThreshold, swapOutAmount uint64
|
||||
if swapOutEnabled {
|
||||
|
|
@ -984,6 +992,30 @@ func (api *api) InitiateSwapIn(ctx context.Context, initiateSwapInRequest *Initi
|
|||
}
|
||||
|
||||
func (api *api) EnableAutoSwapOut(ctx context.Context, enableAutoSwapsRequest *EnableAutoSwapRequest) error {
|
||||
if api.svc.GetSwapsService() == nil {
|
||||
return errors.New("SwapsService not started")
|
||||
}
|
||||
|
||||
encryptionKey := ""
|
||||
if enableAutoSwapsRequest.Destination != "" {
|
||||
switch enableAutoSwapsRequest.DestinationType {
|
||||
case "address":
|
||||
if err := api.svc.GetSwapsService().ValidateAddress(enableAutoSwapsRequest.Destination); err != nil {
|
||||
return err
|
||||
}
|
||||
case "xpub":
|
||||
if !api.cfg.CheckUnlockPassword(enableAutoSwapsRequest.UnlockPassword) {
|
||||
return errors.New("invalid unlock password")
|
||||
}
|
||||
if err := api.svc.GetSwapsService().ValidateXpub(enableAutoSwapsRequest.Destination); err != nil {
|
||||
return err
|
||||
}
|
||||
encryptionKey = enableAutoSwapsRequest.UnlockPassword
|
||||
default:
|
||||
return errors.New("destination type must be address or xpub")
|
||||
}
|
||||
}
|
||||
|
||||
err := api.cfg.SetUpdate(config.AutoSwapBalanceThresholdKey, strconv.FormatUint(enableAutoSwapsRequest.BalanceThreshold, 10), "")
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to save autoswap balance threshold to config")
|
||||
|
|
@ -996,16 +1028,13 @@ func (api *api) EnableAutoSwapOut(ctx context.Context, enableAutoSwapsRequest *E
|
|||
return err
|
||||
}
|
||||
|
||||
err = api.cfg.SetUpdate(config.AutoSwapDestinationKey, enableAutoSwapsRequest.Destination, "")
|
||||
err = api.cfg.SetUpdate(config.AutoSwapDestinationKey, enableAutoSwapsRequest.Destination, encryptionKey)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to save autoswap destination to config")
|
||||
return err
|
||||
}
|
||||
|
||||
if api.svc.GetSwapsService() == nil {
|
||||
return errors.New("SwapsService not started")
|
||||
}
|
||||
return api.svc.GetSwapsService().EnableAutoSwapOut()
|
||||
return api.svc.GetSwapsService().EnableAutoSwapOut(enableAutoSwapsRequest.UnlockPassword)
|
||||
}
|
||||
|
||||
func (api *api) DisableAutoSwap() error {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,8 @@ type EnableAutoSwapRequest struct {
|
|||
BalanceThreshold uint64 `json:"balanceThreshold"`
|
||||
SwapAmount uint64 `json:"swapAmount"`
|
||||
Destination string `json:"destination"`
|
||||
DestinationType string `json:"destinationType"`
|
||||
UnlockPassword string `json:"unlockPassword"`
|
||||
}
|
||||
|
||||
type GetAutoSwapConfigResponse struct {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
ArrowDownUpIcon,
|
||||
ClipboardPasteIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
MoveRightIcon,
|
||||
XCircleIcon,
|
||||
} from "lucide-react";
|
||||
|
|
@ -11,14 +12,24 @@ import AppHeader from "src/components/AppHeader";
|
|||
import ExternalLink from "src/components/ExternalLink";
|
||||
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
|
||||
import Loading from "src/components/Loading";
|
||||
import PasswordInput from "src/components/password/PasswordInput";
|
||||
import ResponsiveLinkButton from "src/components/ResponsiveLinkButton";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "src/components/ui/alert-dialog";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import { LoadingButton } from "src/components/ui/custom/loading-button";
|
||||
import { Input } from "src/components/ui/input";
|
||||
import { Label } from "src/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "src/components/ui/radio-group";
|
||||
import { useBalances } from "src/hooks/useBalances";
|
||||
import { useAutoSwapsConfig, useSwapInfo } from "src/hooks/useSwaps";
|
||||
import { copyToClipboard } from "src/lib/clipboard";
|
||||
import { AutoSwapConfig } from "src/types";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
|
|
@ -54,7 +65,6 @@ export default function AutoSwap() {
|
|||
}
|
||||
|
||||
function AutoSwapOutForm() {
|
||||
const { data: balances } = useBalances();
|
||||
const { mutate } = useAutoSwapsConfig();
|
||||
const { data: swapInfo } = useSwapInfo("out");
|
||||
|
||||
|
|
@ -65,18 +75,35 @@ function AutoSwapOutForm() {
|
|||
const [externalType, setExternalType] = useState<"address" | "xpub">(
|
||||
"address"
|
||||
);
|
||||
const [unlockPassword, setUnlockPassword] = useState("");
|
||||
const [showUnlockPasswordDialog, setShowUnlockPasswordDialog] =
|
||||
useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (swapAmount > balanceThreshold) {
|
||||
if (Number(swapAmount) > Number(balanceThreshold)) {
|
||||
toast.info(
|
||||
"Balance threshold must be greater than or equal to swap amount"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (externalType === "xpub" && !isInternalSwap) {
|
||||
setShowUnlockPasswordDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await submitAutoSwap();
|
||||
};
|
||||
|
||||
const onConfirmUnlockPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await submitAutoSwap(unlockPassword);
|
||||
};
|
||||
|
||||
const submitAutoSwap = async (password?: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await request("/api/autoswap", {
|
||||
|
|
@ -88,8 +115,12 @@ function AutoSwapOutForm() {
|
|||
swapAmount: parseInt(swapAmount),
|
||||
balanceThreshold: parseInt(balanceThreshold),
|
||||
destination,
|
||||
destinationType: !isInternalSwap ? externalType : undefined,
|
||||
unlockPassword: password,
|
||||
}),
|
||||
});
|
||||
setUnlockPassword("");
|
||||
setShowUnlockPasswordDialog(false);
|
||||
toast("Auto swap enabled successfully");
|
||||
await mutate();
|
||||
} catch (error) {
|
||||
|
|
@ -106,198 +137,238 @@ function AutoSwapOutForm() {
|
|||
setDestination(text.trim());
|
||||
};
|
||||
|
||||
if (!balances || !swapInfo) {
|
||||
if (!swapInfo) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-6">
|
||||
<div>
|
||||
<h2 className="font-medium text-foreground flex items-center gap-1">
|
||||
Lightning <MoveRightIcon /> On-chain
|
||||
</h2>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Setup automatic swap of lightning funds into your on-chain balance
|
||||
every time a set threshold is reached.
|
||||
</p>
|
||||
<p className="mt-2 text-muted-foreground flex gap-2 items-center text-sm">
|
||||
<ClockIcon className="w-4 h-4" />
|
||||
Swaps will be made once per hour
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Spending balance threshold</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in satoshis"
|
||||
value={balanceThreshold}
|
||||
min={swapAmount}
|
||||
onChange={(e) => setBalanceThreshold(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Swap out as soon as this amount is reached
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Swap amount</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in satoshis"
|
||||
value={swapAmount}
|
||||
min={swapInfo.minAmount}
|
||||
max={swapInfo.maxAmount}
|
||||
onChange={(e) => setSwapAmount(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Minimum <FormattedBitcoinAmount amount={swapInfo.minAmount * 1000} />
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Label>Swap to</Label>
|
||||
<RadioGroup
|
||||
defaultValue="normal"
|
||||
value={isInternalSwap ? "internal" : "external"}
|
||||
onValueChange={() => {
|
||||
setDestination("");
|
||||
setInternalSwap(!isInternalSwap);
|
||||
}}
|
||||
className="flex gap-4 flex-row"
|
||||
>
|
||||
<div className="flex items-start space-x-2 mb-2">
|
||||
<RadioGroupItem
|
||||
value="internal"
|
||||
id="internal"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="internal"
|
||||
className="text-primary font-medium cursor-pointer"
|
||||
>
|
||||
On-chain balance
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem
|
||||
value="external"
|
||||
id="external"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="external"
|
||||
className="text-primary font-medium cursor-pointer"
|
||||
>
|
||||
External on-chain wallet
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{!isInternalSwap && (
|
||||
<div className="grid gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Destination Type</Label>
|
||||
<RadioGroup
|
||||
value={externalType}
|
||||
onValueChange={(value) => {
|
||||
setExternalType(value as "address" | "xpub");
|
||||
setDestination("");
|
||||
}}
|
||||
className="flex gap-4 flex-row"
|
||||
>
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem
|
||||
value="address"
|
||||
id="address"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<div className="grid gap-1.5">
|
||||
<Label
|
||||
htmlFor="address"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
Single Address
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Send to the same address each time
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="xpub" id="xpub" className="shrink-0" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label
|
||||
htmlFor="xpub"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
XPUB
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Generate new addresses from extended public key
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>
|
||||
{externalType === "address"
|
||||
? "Receiving on-chain address"
|
||||
: "Extended Public Key (XPUB)"}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={externalType === "address" ? "bc1..." : "xpub..."}
|
||||
value={destination}
|
||||
onChange={(e) => setDestination(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="px-2"
|
||||
onClick={paste}
|
||||
>
|
||||
<ClipboardPasteIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{externalType === "address"
|
||||
? "Enter a Bitcoin address to receive swapped funds"
|
||||
: "Enter an XPUB to automatically generate new addresses for each swap"}
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-6">
|
||||
<div>
|
||||
<h2 className="font-medium text-foreground flex items-center gap-1">
|
||||
Lightning <MoveRightIcon /> On-chain
|
||||
</h2>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Setup automatic swap of lightning funds into your on-chain balance
|
||||
every time a set threshold is reached.
|
||||
</p>
|
||||
<p className="mt-2 text-muted-foreground flex gap-2 items-center text-sm">
|
||||
<ClockIcon className="w-4 h-4" />
|
||||
Swaps will be made once per hour
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between border-t pt-4">
|
||||
<Label>Fee</Label>
|
||||
{swapInfo ? (
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Spending balance threshold</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in satoshis"
|
||||
value={balanceThreshold}
|
||||
min={swapAmount}
|
||||
onChange={(e) => setBalanceThreshold(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Swap out as soon as this amount is reached
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Swap amount</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in satoshis"
|
||||
value={swapAmount}
|
||||
min={swapInfo.minAmount}
|
||||
max={swapInfo.maxAmount}
|
||||
onChange={(e) => setSwapAmount(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Minimum{" "}
|
||||
<FormattedBitcoinAmount amount={swapInfo.minAmount * 1000} />
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Label>Swap to</Label>
|
||||
<RadioGroup
|
||||
value={isInternalSwap ? "internal" : "external"}
|
||||
onValueChange={() => {
|
||||
setDestination("");
|
||||
setInternalSwap(!isInternalSwap);
|
||||
}}
|
||||
className="flex gap-4 flex-row"
|
||||
>
|
||||
<div className="flex items-start space-x-2 mb-2">
|
||||
<RadioGroupItem
|
||||
value="internal"
|
||||
id="internal"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="internal"
|
||||
className="text-primary font-medium cursor-pointer"
|
||||
>
|
||||
On-chain balance
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem
|
||||
value="external"
|
||||
id="external"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="external"
|
||||
className="text-primary font-medium cursor-pointer"
|
||||
>
|
||||
External on-chain wallet
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{!isInternalSwap && (
|
||||
<div className="grid gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Destination Type</Label>
|
||||
<RadioGroup
|
||||
value={externalType}
|
||||
onValueChange={(value) => {
|
||||
setExternalType(value as "address" | "xpub");
|
||||
setDestination("");
|
||||
}}
|
||||
className="flex gap-4 flex-row"
|
||||
>
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem
|
||||
value="address"
|
||||
id="address"
|
||||
className="shrink-0"
|
||||
/>
|
||||
<div className="grid gap-1.5">
|
||||
<Label
|
||||
htmlFor="address"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
Single Address
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Send to the same address each time
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="xpub" id="xpub" className="shrink-0" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label
|
||||
htmlFor="xpub"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
XPUB
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Generate new addresses from extended public key
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>
|
||||
{externalType === "address"
|
||||
? "Receiving on-chain address"
|
||||
: "Extended Public Key (XPUB)"}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={
|
||||
externalType === "address" ? "bc1..." : "xpub..."
|
||||
}
|
||||
value={destination}
|
||||
onChange={(e) => setDestination(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="px-2"
|
||||
onClick={paste}
|
||||
>
|
||||
<ClipboardPasteIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{externalType === "address"
|
||||
? "Enter a Bitcoin address to receive swapped funds"
|
||||
: "Enter an XPUB to automatically generate new addresses for each swap. You will be asked to enter your unlock password to encrypt it safely"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between border-t pt-4">
|
||||
<Label>Fee</Label>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{swapInfo.albyServiceFee + swapInfo.boltzServiceFee}% + on-chain
|
||||
fees
|
||||
</p>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<LoadingButton className="w-full" loading={loading}>
|
||||
Begin Auto Swap
|
||||
</LoadingButton>
|
||||
<p className="text-xs text-muted-foreground text-right">
|
||||
powered by{" "}
|
||||
<ExternalLink
|
||||
to="https://boltz.exchange"
|
||||
className="font-medium text-foreground"
|
||||
>
|
||||
boltz.exchange
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<LoadingButton className="w-full" loading={loading}>
|
||||
Begin Auto Swap
|
||||
</LoadingButton>
|
||||
<p className="text-xs text-muted-foreground text-right">
|
||||
powered by{" "}
|
||||
<ExternalLink
|
||||
to="https://boltz.exchange"
|
||||
className="font-medium text-foreground"
|
||||
>
|
||||
boltz.exchange
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
<AlertDialog
|
||||
open={showUnlockPasswordDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowUnlockPasswordDialog(open);
|
||||
if (!open) {
|
||||
setUnlockPassword("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<form onSubmit={onConfirmUnlockPassword}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Auto Swap Setup</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
Please enter your unlock password to encrypt and securely
|
||||
store the XPUB.
|
||||
</p>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="unlockPassword">Unlock Password</Label>
|
||||
<PasswordInput
|
||||
id="unlockPassword"
|
||||
onChange={setUnlockPassword}
|
||||
autoFocus
|
||||
value={unlockPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="mt-3">
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<Button type="submit" disabled={!unlockPassword || loading}>
|
||||
Confirm
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -350,10 +421,16 @@ function ActiveSwapOutConfig({ swapConfig }: { swapConfig: AutoSwapConfig }) {
|
|||
</div>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="font-medium">Destination</div>
|
||||
<div className="truncate text-muted-foreground text-right">
|
||||
{swapConfig.destination
|
||||
? swapConfig.destination
|
||||
: "On-chain Balance"}
|
||||
<div className="flex min-w-0 items-center justify-end gap-2 text-muted-foreground">
|
||||
<div className="truncate text-right">
|
||||
{swapConfig.destination || "On-chain Balance"}
|
||||
</div>
|
||||
{swapConfig.destination && (
|
||||
<CopyIcon
|
||||
className="cursor-pointer size-4 shrink-0"
|
||||
onClick={() => copyToClipboard(swapConfig.destination)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
|
|
@ -384,11 +461,7 @@ function ActiveSwapOutConfig({ swapConfig }: { swapConfig: AutoSwapConfig }) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onDeactivate()}
|
||||
disabled={loading}
|
||||
variant="outline"
|
||||
>
|
||||
<Button onClick={onDeactivate} disabled={loading} variant="outline">
|
||||
<XCircleIcon />
|
||||
Deactivate Auto Swap
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -56,17 +56,21 @@ export default function SwapOutStatus() {
|
|||
{swapStatus === "PENDING" && <Loading className="w-4 h-4 mr-2" />}
|
||||
{statusText[swapStatus]}
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center justify-center gap-2 text-muted-foreground text-sm">
|
||||
<CardDescription className="text-muted-foreground text-sm">
|
||||
{swap.autoSwap && (
|
||||
<span>Auto swap{swap.usedXpub && <> to xpub</>} • </span>
|
||||
<p className="text-center mb-1">
|
||||
Auto swap{swap.usedXpub && <> to xpub</>}
|
||||
</p>
|
||||
)}
|
||||
Swap ID: {swap.id}{" "}
|
||||
<CopyIcon
|
||||
className="cursor-pointer text-muted-foreground size-4"
|
||||
onClick={() => {
|
||||
copyToClipboard(swap.id);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
Swap ID: {swap.id}{" "}
|
||||
<CopyIcon
|
||||
className="cursor-pointer text-muted-foreground size-4"
|
||||
onClick={() => {
|
||||
copyToClipboard(swap.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ func (svc *service) StartApp(encryptionKey string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
svc.swapsService = swaps.NewSwapsService(ctx, svc.db, svc.cfg, svc.keys, svc.eventPublisher, svc.GetLNClient(), svc.transactionsService)
|
||||
svc.swapsService = swaps.NewSwapsService(ctx, svc.db, svc.cfg, svc.keys, svc.eventPublisher, svc.GetLNClient(), svc.transactionsService, encryptionKey)
|
||||
|
||||
svc.publishAllAppInfoEvents()
|
||||
|
||||
|
|
|
|||
|
|
@ -37,23 +37,25 @@ import (
|
|||
type Swap = db.Swap
|
||||
|
||||
type swapsService struct {
|
||||
autoSwapOutCancelFn context.CancelFunc
|
||||
db *gorm.DB
|
||||
ctx context.Context
|
||||
lnClient lnclient.LNClient
|
||||
cfg config.Config
|
||||
keys keys.Keys
|
||||
eventPublisher events.EventPublisher
|
||||
transactionsService transactions.TransactionsService
|
||||
boltzApi *boltz.Api
|
||||
boltzWs *boltz.Websocket
|
||||
swapListeners map[string]chan boltz.SwapUpdate
|
||||
swapListenersLock sync.Mutex
|
||||
autoSwapOutCancelFn context.CancelFunc
|
||||
db *gorm.DB
|
||||
ctx context.Context
|
||||
lnClient lnclient.LNClient
|
||||
cfg config.Config
|
||||
keys keys.Keys
|
||||
eventPublisher events.EventPublisher
|
||||
transactionsService transactions.TransactionsService
|
||||
boltzApi *boltz.Api
|
||||
boltzWs *boltz.Websocket
|
||||
swapListeners map[string]chan boltz.SwapUpdate
|
||||
swapListenersLock sync.Mutex
|
||||
autoSwapOutXpubLock sync.Mutex
|
||||
autoSwapOutDecryptedXpub string
|
||||
}
|
||||
|
||||
type SwapsService interface {
|
||||
StopAutoSwapOut()
|
||||
EnableAutoSwapOut() error
|
||||
EnableAutoSwapOut(encryptionKey string) error
|
||||
SwapOut(amount uint64, destination string, autoSwap, usedXpubDerivation bool) (*SwapResponse, error)
|
||||
SwapIn(amount uint64, autoSwap bool) (*SwapResponse, error)
|
||||
GetSwapOutInfo() (*SwapInfo, error)
|
||||
|
|
@ -61,6 +63,9 @@ type SwapsService interface {
|
|||
RefundSwap(swapId, address string, enableRetries bool) error
|
||||
GetSwap(swapId string) (*Swap, error)
|
||||
ListSwaps() ([]Swap, error)
|
||||
GetDecryptedAutoSwapXpub() string
|
||||
ValidateAddress(address string) error
|
||||
ValidateXpub(xpub string) error
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -101,7 +106,7 @@ type SwapResponse struct {
|
|||
}
|
||||
|
||||
func NewSwapsService(ctx context.Context, db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher,
|
||||
lnClient lnclient.LNClient, transactionsService transactions.TransactionsService) SwapsService {
|
||||
lnClient lnclient.LNClient, transactionsService transactions.TransactionsService, encryptionKey string) SwapsService {
|
||||
boltzApi := &boltz.Api{URL: cfg.GetEnv().BoltzApi}
|
||||
boltzWs := boltzApi.NewWebsocket()
|
||||
|
||||
|
|
@ -137,7 +142,7 @@ func NewSwapsService(ctx context.Context, db *gorm.DB, cfg config.Config, keys k
|
|||
}
|
||||
}()
|
||||
|
||||
err := svc.EnableAutoSwapOut()
|
||||
err := svc.EnableAutoSwapOut(encryptionKey)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Couldn't enable auto swaps")
|
||||
}
|
||||
|
|
@ -153,13 +158,28 @@ func (svc *swapsService) StopAutoSwapOut() {
|
|||
svc.autoSwapOutCancelFn()
|
||||
logger.Logger.Info("Auto swap out service stopped")
|
||||
}
|
||||
svc.autoSwapOutXpubLock.Lock()
|
||||
svc.autoSwapOutDecryptedXpub = ""
|
||||
svc.autoSwapOutXpubLock.Unlock()
|
||||
}
|
||||
|
||||
func (svc *swapsService) EnableAutoSwapOut() error {
|
||||
func (svc *swapsService) EnableAutoSwapOut(encryptionKey string) error {
|
||||
svc.StopAutoSwapOut()
|
||||
|
||||
ctx, cancelFn := context.WithCancel(svc.ctx)
|
||||
swapDestination, _ := svc.cfg.Get(config.AutoSwapDestinationKey, "")
|
||||
svc.autoSwapOutXpubLock.Lock()
|
||||
svc.autoSwapOutDecryptedXpub = ""
|
||||
svc.autoSwapOutXpubLock.Unlock()
|
||||
|
||||
swapDestination, _ := svc.cfg.Get(config.AutoSwapDestinationKey, encryptionKey)
|
||||
if swapDestination != "" {
|
||||
if err := svc.ValidateXpub(swapDestination); err == nil {
|
||||
svc.autoSwapOutXpubLock.Lock()
|
||||
svc.autoSwapOutDecryptedXpub = swapDestination
|
||||
svc.autoSwapOutXpubLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
balanceThresholdStr, _ := svc.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
|
||||
amountStr, _ := svc.cfg.Get(config.AutoSwapAmountKey, "")
|
||||
|
||||
|
|
@ -202,15 +222,17 @@ func (svc *swapsService) EnableAutoSwapOut() error {
|
|||
|
||||
actualDestination := swapDestination
|
||||
var usedXpubDerivation bool
|
||||
if swapDestination != "" {
|
||||
if err := svc.validateXpub(swapDestination); err == nil {
|
||||
actualDestination, err = svc.getNextUnusedAddressFromXpub()
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to get next address from xpub")
|
||||
continue
|
||||
}
|
||||
usedXpubDerivation = true
|
||||
// Check if we have a decrypted XPUB in memory
|
||||
svc.autoSwapOutXpubLock.Lock()
|
||||
hasDecryptedXpub := svc.autoSwapOutDecryptedXpub != ""
|
||||
svc.autoSwapOutXpubLock.Unlock()
|
||||
if hasDecryptedXpub {
|
||||
actualDestination, err = svc.getNextUnusedAddressFromXpub()
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to get next address from xpub")
|
||||
continue
|
||||
}
|
||||
usedXpubDerivation = true
|
||||
}
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
|
|
@ -1428,7 +1450,7 @@ func (svc *swapsService) bumpAutoswapXpubIndex(swapId uint) {
|
|||
}).Info("Updated xpub index start for swap address")
|
||||
}
|
||||
|
||||
func (svc *swapsService) deriveAddressFromXpub(xpub string, index uint32) (string, error) {
|
||||
func (svc *swapsService) getChainParams() (*chaincfg.Params, error) {
|
||||
var netParams *chaincfg.Params
|
||||
switch svc.cfg.GetNetwork() {
|
||||
case "bitcoin", "mainnet":
|
||||
|
|
@ -1440,7 +1462,16 @@ func (svc *swapsService) deriveAddressFromXpub(xpub string, index uint32) (strin
|
|||
case "signet":
|
||||
netParams = &chaincfg.SigNetParams
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported network: %s", svc.cfg.GetNetwork())
|
||||
return nil, fmt.Errorf("unsupported network: %s", svc.cfg.GetNetwork())
|
||||
}
|
||||
|
||||
return netParams, nil
|
||||
}
|
||||
|
||||
func (svc *swapsService) deriveAddressFromXpub(xpub string, index uint32) (string, error) {
|
||||
netParams, err := svc.getChainParams()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
extPubKey, err := hdkeychain.NewKeyFromString(xpub)
|
||||
|
|
@ -1487,13 +1518,12 @@ func (svc *swapsService) checkAddressHasTransactions(address string, esploraApiR
|
|||
}
|
||||
|
||||
func (svc *swapsService) getNextUnusedAddressFromXpub() (string, error) {
|
||||
destination, _ := svc.cfg.Get(config.AutoSwapDestinationKey, "")
|
||||
// Use the decrypted XPUB from memory (already decrypted during EnableAutoSwapOut)
|
||||
svc.autoSwapOutXpubLock.Lock()
|
||||
destination := svc.autoSwapOutDecryptedXpub
|
||||
svc.autoSwapOutXpubLock.Unlock()
|
||||
if destination == "" {
|
||||
return "", errors.New("no destination configured")
|
||||
}
|
||||
|
||||
if err := svc.validateXpub(destination); err != nil {
|
||||
return "", errors.New("destination is not a valid XPUB")
|
||||
return "", errors.New("no XPUB configured")
|
||||
}
|
||||
|
||||
indexStr, err := svc.cfg.Get(config.AutoSwapXpubIndexStart, "")
|
||||
|
|
@ -1559,10 +1589,36 @@ func (svc *swapsService) getNextUnusedAddressFromXpub() (string, error) {
|
|||
return "", fmt.Errorf("could not find unused address within %d addresses starting from index %d", addressLookAheadLimit, index)
|
||||
}
|
||||
|
||||
func (svc *swapsService) validateXpub(xpub string) error {
|
||||
_, err := hdkeychain.NewKeyFromString(xpub)
|
||||
func (svc *swapsService) ValidateAddress(address string) error {
|
||||
netParams, err := svc.getChainParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = btcutil.DecodeAddress(address, netParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid bitcoin address: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *swapsService) ValidateXpub(xpub string) error {
|
||||
extendedKey, err := hdkeychain.NewKeyFromString(xpub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid xpub: %w", err)
|
||||
}
|
||||
|
||||
if extendedKey.IsPrivate() {
|
||||
return fmt.Errorf("private extended key not allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *swapsService) GetDecryptedAutoSwapXpub() string {
|
||||
svc.autoSwapOutXpubLock.Lock()
|
||||
defer svc.autoSwapOutXpubLock.Unlock()
|
||||
|
||||
return svc.autoSwapOutDecryptedXpub
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue