Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,18 @@ func (api *api) RefundSwap(refundSwapRequest *RefundSwapRequest) error {
func (api *api) GetAutoSwapConfig() (*GetAutoSwapConfigResponse, error) {
swapOutBalanceThresholdStr, _ := api.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
swapOutAmountStr, _ := api.cfg.Get(config.AutoSwapAmountKey, "")
swapOutDestination, _ := api.cfg.Get(config.AutoSwapDestinationKey, "")

swapOutDestination := ""
if api.svc.GetSwapsService() != nil {
decryptedXpub := api.svc.GetSwapsService().GetDecryptedAutoSwapXpub()
if decryptedXpub != "" {
swapOutDestination = decryptedXpub
}
}

if swapOutDestination == "" {
swapOutDestination, _ = api.cfg.Get(config.AutoSwapDestinationKey, "")
}

swapOutEnabled := swapOutBalanceThresholdStr != "" && swapOutAmountStr != ""
var swapOutBalanceThreshold, swapOutAmount uint64
Expand Down Expand Up @@ -946,16 +957,31 @@ func (api *api) EnableAutoSwapOut(ctx context.Context, enableAutoSwapsRequest *E
return err
}

err = api.cfg.SetUpdate(config.AutoSwapDestinationKey, enableAutoSwapsRequest.Destination, "")
if api.svc.GetSwapsService() == nil {
return errors.New("SwapsService not started")
}

encryptionKey := ""
if enableAutoSwapsRequest.Destination != "" {

if err := api.svc.GetSwapsService().ValidateXpub(enableAutoSwapsRequest.Destination); err == nil {
if enableAutoSwapsRequest.UnlockPassword == "" {
return errors.New("unlock password is required when using an xpub as destination")
}
if !api.cfg.CheckUnlockPassword(enableAutoSwapsRequest.UnlockPassword) {
return errors.New("invalid unlock password")
}
encryptionKey = enableAutoSwapsRequest.UnlockPassword
}
}

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 {
Expand Down
1 change: 1 addition & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ type EnableAutoSwapRequest struct {
BalanceThreshold uint64 `json:"balanceThreshold"`
SwapAmount uint64 `json:"swapAmount"`
Destination string `json:"destination"`
UnlockPassword string `json:"unlockPassword"`
}

type GetAutoSwapConfigResponse struct {
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/screens/wallet/swap/AutoSwap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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 { Button } from "src/components/ui/button";
import { LoadingButton } from "src/components/ui/custom/loading-button";
Expand Down Expand Up @@ -65,6 +66,7 @@ function AutoSwapOutForm() {
const [externalType, setExternalType] = useState<"address" | "xpub">(
"address"
);
const [unlockPassword, setUnlockPassword] = useState("");
const [loading, setLoading] = useState(false);

const onSubmit = async (e: React.FormEvent) => {
Expand All @@ -77,6 +79,14 @@ function AutoSwapOutForm() {
return;
}

const isXpub = externalType === "xpub" && !isInternalSwap;
if (isXpub && !unlockPassword) {
toast.error("Password required", {
description: "Please enter your unlock password to encrypt the XPUB",
});
return;
}

try {
setLoading(true);
await request("/api/autoswap", {
Expand All @@ -88,6 +98,7 @@ function AutoSwapOutForm() {
swapAmount: parseInt(swapAmount),
balanceThreshold: parseInt(balanceThreshold),
destination,
unlockPassword: isXpub ? unlockPassword : undefined,
}),
});
toast("Auto swap enabled successfully");
Expand Down Expand Up @@ -269,6 +280,21 @@ function AutoSwapOutForm() {
: "Enter an XPUB to automatically generate new addresses for each swap"}
</p>
</div>
{externalType === "xpub" && (
<div className="grid gap-1.5">
<Label>Unlock Password</Label>
<PasswordInput
id="unlockPassword"
value={unlockPassword}
onChange={setUnlockPassword}
placeholder="Enter your unlock password"
required
/>
<p className="text-xs text-muted-foreground">
Your password is required to encrypt the XPUB for secure storage
</p>
</div>
)}
</div>
)}

Expand Down
2 changes: 1 addition & 1 deletion service/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func (svc *service) StartApp(encryptionKey string) error {
return err
}

svc.swapsService = swaps.NewSwapsService(ctx, svc.db, svc.cfg, svc.keys, svc.eventPublisher, svc.lnClient, svc.transactionsService)
svc.swapsService = swaps.NewSwapsService(ctx, svc.db, svc.cfg, svc.keys, svc.eventPublisher, svc.lnClient, svc.transactionsService, encryptionKey)

svc.publishAllAppInfoEvents()

Expand Down
52 changes: 35 additions & 17 deletions swaps/swaps_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,21 @@ type swapsService struct {
boltzWs *boltz.Websocket
swapListeners map[string]chan boltz.SwapUpdate
swapListenersLock 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)
GetSwapInInfo() (*SwapInfo, error)
RefundSwap(swapId, address string, enableRetries bool) error
GetSwap(swapId string) (*Swap, error)
ListSwaps() ([]Swap, error)
GetDecryptedAutoSwapXpub() string
ValidateXpub(xpub string) error
}

const (
Expand Down Expand Up @@ -101,7 +104,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()

Expand Down Expand Up @@ -149,7 +152,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")
}
Expand All @@ -167,11 +170,19 @@ func (svc *swapsService) StopAutoSwapOut() {
}
}

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, "")

swapDestination, _ := svc.cfg.Get(config.AutoSwapDestinationKey, encryptionKey)

if swapDestination != "" && svc.ValidateXpub(swapDestination) == nil {
svc.autoSwapOutDecryptedXpub = swapDestination
} else {
svc.autoSwapOutDecryptedXpub = "" // Not an XPUB or empty
}

balanceThresholdStr, _ := svc.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
amountStr, _ := svc.cfg.Get(config.AutoSwapAmountKey, "")

Expand Down Expand Up @@ -214,15 +225,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
if svc.autoSwapOutDecryptedXpub != "" {
actualDestination, err = svc.getNextUnusedAddressFromXpub()
if err != nil {
logger.Logger.WithError(err).Error("Failed to get next address from xpub")
continue
}
usedXpubDerivation = true
} else if swapDestination != "" {
// Regular address (not XPUB)
actualDestination = swapDestination
}

logger.Logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -1485,12 +1498,13 @@ 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)
destination := svc.autoSwapOutDecryptedXpub
if destination == "" {
return "", errors.New("no destination configured")
return "", errors.New("no XPUB configured")
}

if err := svc.validateXpub(destination); err != nil {
if err := svc.ValidateXpub(destination); err != nil {
return "", errors.New("destination is not a valid XPUB")
}

Expand Down Expand Up @@ -1557,10 +1571,14 @@ 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 {
func (svc *swapsService) ValidateXpub(xpub string) error {
_, err := hdkeychain.NewKeyFromString(xpub)
if err != nil {
return fmt.Errorf("invalid xpub: %w", err)
}
return nil
}

func (svc *swapsService) GetDecryptedAutoSwapXpub() string {
return svc.autoSwapOutDecryptedXpub
}
Loading