feat: auto-swaps (#1266)

Also fixes #1303

## TODOs

- [x] Do final testing with LDK/LND on mainnet
- [x] Improve fee display on the frontend, currently looks scary
- [x] ~Shift Swaps from settings~ (Agreeed to do in follow-up)
- [x] ~Add option to do one-time swaps~ (Agreeed to do in follow-up)

### Minor
- [x] `LDK_NETWORK` config var should now be `NETWORK`
- [x] ~Use LNClient method to publish transactions~ (Agreeed to do in
follow-up)

### Testing
- [ ] Phoenix
- [ ] Cashu
- [x] Wails
This commit is contained in:
Adithya Vardhan 2025-05-14 21:31:50 +05:30 committed by GitHub
commit f41f96335e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1260 additions and 140 deletions

View file

@ -35,4 +35,8 @@ FRONTEND_URL=http://localhost:5173
#LND_ADDRESS=127.0.0.1:10001
#LND_MACAROON_FILE=/home/YOUR_USERNAME/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon
#LDK_VSS_URL="http://localhost:8090/vss"
#LDK_VSS_URL="http://localhost:8090/vss"
# Boltz API
#BOLTZ_API=https://api.testnet.boltz.exchange
#NETWORK=testnet

View file

@ -159,12 +159,14 @@ For more information refer to:
The following configuration options can be set as environment variables or in a .env file
- `RELAY`: default: "wss://relay.getalby.com/v1"
- `JWT_SECRET`: a randomly generated secret string. (only needed in http mode)
- `DATABASE_URI`: a sqlite filename or postgres URL. Default is SQLite DB `nwc.db` without a path, which will be put in the user home directory: $XDG_DATA_HOME/albyhub/nwc.db
- `PORT`: the port on which the app should listen on (default: 8080)
- `WORK_DIR`: directory to store NWC data files. Default: $XDG_DATA_HOME/albyhub
- `LOG_LEVEL`: log level for the application. Higher is more verbose. Default: 4 (info)
- `AUTO_UNLOCK_PASSWORD`: provide unlock password to auto-unlock Alby Hub on startup (e.g. after a machine restart). Unlock password still be required to access the interface.
- `JWT_SECRET`: A randomly generated secret string. (only needed in http mode)
- `DATABASE_URI`: A sqlite filename or postgres URL. Default is SQLite DB `nwc.db` without a path, which will be put in the user home directory: $XDG_DATA_HOME/albyhub/nwc.db
- `PORT`: The port on which the app should listen on (default: 8080)
- `WORK_DIR`: Directory to store NWC data files. Default: $XDG_DATA_HOME/albyhub
- `LOG_LEVEL`: Log level for the application. Higher is more verbose. Default: 4 (info)
- `AUTO_UNLOCK_PASSWORD`: Provide unlock password to auto-unlock Alby Hub on startup (e.g. after a machine restart). Unlock password still be required to access the interface.
- `BOLTZ_API`: The api which provides auto swaps functionality. Default: "https://api.boltz.exchange"
- `NETWORK`: On-chain network used for auto swaps. Should match the backend network. Default: "bitcoin"
### Migrating the database (Sqlite <-> Postgres)
@ -204,14 +206,14 @@ _To configure via env, the following parameters must be provided:_
##### Mutinynet
- `MEMPOOL_API=https://mutinynet.com/api`
- `LDK_NETWORK=signet`
- `NETWORK=signet`
- `LDK_ESPLORA_SERVER=https://mutinynet.com/api`
- `LDK_GOSSIP_SOURCE=https://rgs.mutinynet.com/snapshot`
##### Testnet (Not recommended - try Mutinynet)
- `MEMPOOL_API=https://mempool.space/testnet/api`
- `LDK_NETWORK=testnet`
- `NETWORK=testnet`
- `LDK_ESPLORA_SERVER=https://mempool.space/testnet/api`
- `LDK_GOSSIP_SOURCE=https://rapidsync.lightningdevkit.org/testnet/snapshot`

View file

@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"sync"
"time"
@ -543,6 +544,83 @@ func (api *api) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnec
return api.svc.GetLNClient().GetNodeConnectionInfo(ctx)
}
func (api *api) GetAutoSwapsConfig() (*GetAutoSwapsConfigResponse, error) {
swapBalanceThresholdStr, _ := api.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
swapAmountStr, _ := api.cfg.Get(config.AutoSwapAmountKey, "")
swapDestination, _ := api.cfg.Get(config.AutoSwapDestinationKey, "")
enabled := swapBalanceThresholdStr != "" &&
swapAmountStr != "" &&
swapDestination != ""
var swapBalanceThreshold, swapAmount uint64
if enabled {
var err error
if swapBalanceThreshold, err = strconv.ParseUint(swapBalanceThresholdStr, 10, 64); err != nil {
return nil, fmt.Errorf("invalid autoswap balance threshold: %w", err)
}
if swapAmount, err = strconv.ParseUint(swapAmountStr, 10, 64); err != nil {
return nil, fmt.Errorf("invalid autoswap amount: %w", err)
}
}
swapFees, err := api.svc.GetSwapsService().CalculateFee()
if err != nil {
logger.Logger.WithError(err).Error("failed to calculate fee info")
return nil, err
}
return &GetAutoSwapsConfigResponse{
Enabled: enabled,
BalanceThreshold: swapBalanceThreshold,
SwapAmount: swapAmount,
Destination: swapDestination,
AlbyServiceFee: swapFees.AlbyServiceFee,
BoltzServiceFee: swapFees.BoltzServiceFee,
BoltzNetworkFee: swapFees.BoltzNetworkFee,
}, nil
}
func (api *api) EnableAutoSwaps(ctx context.Context, enableAutoSwapsRequest *EnableAutoSwapsRequest) error {
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")
return err
}
err = api.cfg.SetUpdate(config.AutoSwapAmountKey, strconv.FormatUint(enableAutoSwapsRequest.SwapAmount, 10), "")
if err != nil {
logger.Logger.WithError(err).Error("Failed to save autoswap amount to config")
return err
}
err = api.cfg.SetUpdate(config.AutoSwapDestinationKey, enableAutoSwapsRequest.Destination, "")
if err != nil {
logger.Logger.WithError(err).Error("Failed to save autoswap destination to config")
return err
}
return api.svc.StartAutoSwaps()
}
func (api *api) DisableAutoSwaps() error {
if err := api.cfg.SetUpdate(config.AutoSwapBalanceThresholdKey, "", ""); err != nil {
logger.Logger.WithError(err).Error("Failed to remove autoswap balance threshold")
return err
}
if err := api.cfg.SetUpdate(config.AutoSwapAmountKey, "", ""); err != nil {
logger.Logger.WithError(err).Error("Failed to remove autoswap amount")
return err
}
if err := api.cfg.SetUpdate(config.AutoSwapDestinationKey, "", ""); err != nil {
logger.Logger.WithError(err).Error("Failed to remove autoswap destination")
return err
}
api.svc.GetSwapsService().StopAutoSwaps()
return nil
}
func (api *api) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")

View file

@ -59,6 +59,9 @@ type API interface {
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
Health(ctx context.Context) (*HealthResponse, error)
SetCurrency(currency string) error
GetAutoSwapsConfig() (*GetAutoSwapsConfigResponse, error)
DisableAutoSwaps() error
EnableAutoSwaps(ctx context.Context, autoSwapsRequest *EnableAutoSwapsRequest) error
GetCustomNodeCommands() (*CustomNodeCommandsResponse, error)
ExecuteCustomNodeCommand(ctx context.Context, command string) (interface{}, error)
}
@ -114,6 +117,22 @@ type CreateAppRequest struct {
UnlockPassword string `json:"unlockPassword"`
}
type EnableAutoSwapsRequest struct {
BalanceThreshold uint64 `json:"balanceThreshold"`
SwapAmount uint64 `json:"swapAmount"`
Destination string `json:"destination"`
}
type GetAutoSwapsConfigResponse struct {
Enabled bool `json:"enabled"`
BalanceThreshold uint64 `json:"balanceThreshold"`
SwapAmount uint64 `json:"swapAmount"`
Destination string `json:"destination"`
AlbyServiceFee float64 `json:"albyServiceFee"`
BoltzServiceFee float64 `json:"boltzServiceFee"`
BoltzNetworkFee uint64 `json:"boltzNetworkFee"`
}
type StartRequest struct {
UnlockPassword string `json:"unlockPassword"`
}

View file

@ -65,7 +65,7 @@ func (api *api) SendPayment(ctx context.Context, invoice string, amountMsat *uin
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
transaction, err := api.svc.GetTransactionsService().SendPaymentSync(ctx, invoice, amountMsat, nil, api.svc.GetLNClient(), nil, nil)
transaction, err := api.svc.GetTransactionsService().SendPaymentSync(ctx, invoice, amountMsat, nil, api.svc.GetLNClient(), nil, nil, nil)
if err != nil {
return nil, err
}
@ -142,7 +142,7 @@ func (api *api) TopupIsolatedApp(ctx context.Context, userApp *db.App, amountMsa
return err
}
_, err = api.svc.GetTransactionsService().SendPaymentSync(ctx, transaction.PaymentRequest, nil, nil, api.svc.GetLNClient(), nil, nil)
_, err = api.svc.GetTransactionsService().SendPaymentSync(ctx, transaction.PaymentRequest, nil, nil, api.svc.GetLNClient(), nil, nil, nil)
return err
}

View file

@ -146,6 +146,20 @@ func (cfg *config) GetRelayUrl() string {
return relayUrl
}
func (cfg *config) GetNetwork() string {
env := cfg.GetEnv()
if env.Network != "" {
return env.Network
}
if env.LDKNetwork != "" {
return env.LDKNetwork
}
return "bitcoin"
}
func (cfg *config) Get(key string, encryptionKey string) (string, error) {
return cfg.get(key, encryptionKey, cfg.db)
}

View file

@ -8,7 +8,10 @@ const (
)
const (
OnchainAddressKey = "OnchainAddress"
OnchainAddressKey = "OnchainAddress"
AutoSwapBalanceThresholdKey = "AutoSwapBalanceThreshold"
AutoSwapAmountKey = "AutoSwapAmount"
AutoSwapDestinationKey = "AutoSwapDestination"
)
type AppConfig struct {
@ -23,7 +26,8 @@ type AppConfig struct {
JWTSecret string `envconfig:"JWT_SECRET"`
LogLevel string `envconfig:"LOG_LEVEL" default:"4"`
LogToFile bool `envconfig:"LOG_TO_FILE" default:"true"`
LDKNetwork string `envconfig:"LDK_NETWORK" default:"bitcoin"`
Network string `envconfig:"NETWORK"`
LDKNetwork string `envconfig:"LDK_NETWORK"`
LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.getalbypro.com"` // TODO: remove LDK prefix
LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"`
LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"`
@ -44,6 +48,7 @@ type AppConfig struct {
EnableAdvancedSetup bool `envconfig:"ENABLE_ADVANCED_SETUP" default:"true"`
AutoUnlockPassword string `envconfig:"AUTO_UNLOCK_PASSWORD"`
LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"`
BoltzApi string `envconfig:"BOLTZ_API" default:"https://api.boltz.exchange"`
}
func (c *AppConfig) IsDefaultClientId() bool {
@ -56,6 +61,7 @@ type Config interface {
SetUpdate(key string, value string, encryptionKey string) error
GetJWTSecret() string
GetRelayUrl() string
GetNetwork() string
GetEnv() *AppConfig
CheckUnlockPassword(password string) bool
ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error

View file

@ -47,6 +47,8 @@ const (
// accounting for encryption and other metadata in the response, this is set to 4096 characters
const INVOICE_METADATA_MAX_LENGTH = 4096
const SEND_PAYMENT_TIMEOUT = 50
// errors used by NIP-47 and the transaction service
const (
ERROR_INTERNAL = "INTERNAL"

View file

@ -104,6 +104,7 @@ export default function SettingsLayout() {
<aside className="flex flex-col justify-between lg:w-1/5">
<nav className="flex flex-wrap lg:flex-col lg:space-y-1">
<MenuItem to="/settings">General</MenuItem>
<MenuItem to="/settings/swaps">Swaps</MenuItem>
{info?.autoUnlockPasswordSupported && (
<MenuItem to="/settings/auto-unlock">Auto Unlock</MenuItem>
)}

View file

@ -32,7 +32,7 @@ const RadioGroupItem = React.forwardRef<
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
<CheckIcon className="h-3.5 w-3.5" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);

View file

@ -0,0 +1,16 @@
import useSWR, { SWRConfiguration } from "swr";
import { SwapsSettingsResponse } from "src/types";
import { swrFetcher } from "src/utils/swr";
const pollConfiguration: SWRConfiguration = {
refreshInterval: 5 * 60 * 1000, // 5 minutes
};
export function useSwaps(poll = true) {
return useSWR<SwapsSettingsResponse>(
"/api/settings/swaps",
swrFetcher,
poll ? pollConfiguration : undefined
);
}

View file

@ -54,6 +54,7 @@ import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword"
import DebugTools from "src/screens/settings/DebugTools";
import DeveloperSettings from "src/screens/settings/DeveloperSettings";
import Settings from "src/screens/settings/Settings";
import Swaps from "src/screens/settings/Swaps";
import { ImportMnemonic } from "src/screens/setup/ImportMnemonic";
import { RestoreNode } from "src/screens/setup/RestoreNode";
@ -189,6 +190,10 @@ const routes = [
element: <AutoUnlock />,
handle: { crumb: () => "Auto Unlock" },
},
{
path: "swaps",
element: <Swaps />,
},
{
path: "change-unlock-password",
element: <ChangeUnlockPassword />,

View file

@ -0,0 +1,261 @@
import { ClipboardPasteIcon, XCircleIcon } from "lucide-react";
import { useState } from "react";
import Loading from "src/components/Loading";
import SettingsHeader from "src/components/SettingsHeader";
import { Button } from "src/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "src/components/ui/card";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import { LoadingButton } from "src/components/ui/loading-button";
import { RadioGroup, RadioGroupItem } from "src/components/ui/radio-group";
import { useToast } from "src/components/ui/use-toast";
import { useOnchainAddress } from "src/hooks/useOnchainAddress";
import { useSwaps } from "src/hooks/useSwaps";
import { request } from "src/utils/request";
function Swaps() {
const { toast } = useToast();
// TODO: Optimize by setting this from the backend
const { data: onchainAddress } = useOnchainAddress();
const { data: swapsSettings, mutate } = useSwaps();
const [swapTo, setSwapTo] = useState("hub");
const [balanceThreshold, setBalanceThreshold] = useState("");
const [swapAmount, setSwapAmount] = useState("");
const [destination, setDestination] = useState("");
const [loading, setLoading] = useState(false);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setLoading(true);
await request("/api/settings/swaps", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
swapAmount: parseInt(swapAmount),
balanceThreshold: parseInt(balanceThreshold),
destination: swapTo === "hub" ? onchainAddress : destination,
}),
});
toast({ title: "Saved successfully." });
await mutate();
} catch (error) {
toast({
title: "Saving swap settings failed",
description: (error as Error).message,
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const onDeactivate = async (e: React.FormEvent) => {
e.preventDefault();
try {
setLoading(true);
await request("/api/settings/swaps", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
toast({ title: "Deactivated successfully." });
await mutate();
} catch (error) {
toast({
title: "Deactivating auto swaps failed",
description: (error as Error).message,
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const paste = async () => {
const text = await navigator.clipboard.readText();
setDestination(text.trim());
};
if (!onchainAddress || !swapsSettings) {
return <Loading />;
}
return (
<>
<SettingsHeader
title="Swaps"
description="Automatically swap lightning to on-chain funds, a fee of 1.5% applies to all swaps."
/>
{!swapsSettings.enabled ? (
<form onSubmit={onSubmit} className="flex flex-col gap-4">
<div className="grid gap-1.5">
<Label>Spending balance threshold</Label>
<Input
type="number"
placeholder="Swap out as soon as this amount is reached"
value={balanceThreshold}
onChange={(e) => setBalanceThreshold(e.target.value)}
/>
</div>
<div className="grid gap-1.5">
<Label>Swap amount</Label>
<Input
type="number"
placeholder="How much do you want to swap out?"
value={swapAmount}
min={50000}
onChange={(e) => setSwapAmount(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Should be 50000 sats
</p>
</div>
<Label>Destination</Label>
<RadioGroup
defaultValue="normal"
value={swapTo}
onValueChange={(val) => {
setSwapTo(val);
if (val == "hub") {
setDestination(onchainAddress);
} else {
setDestination("");
}
}}
className="flex gap-4 flex-row"
>
<div className="flex items-start space-x-2 mb-2">
<RadioGroupItem value="hub" id="hub" className="shrink-0" />
<Label
htmlFor="hub"
className="text-primary font-medium cursor-pointer"
>
Alby Hub 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>
{swapTo == "external" && (
<div className="grid gap-1.5">
<Label>Receiving on-chain address</Label>
<div className="flex gap-2 mb-4">
<Input
placeholder="bc1..."
value={destination}
onChange={(e) => setDestination(e.target.value)}
/>
<Button
type="button"
variant="outline"
className="px-2"
onClick={paste}
>
<ClipboardPasteIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
<div className="flex items-center justify-between border-t py-4">
<Label>Fee</Label>
<p className="text-muted-foreground text-sm">
{swapsSettings.albyServiceFee + swapsSettings.boltzServiceFee}% +
on-chain fees
</p>
</div>
<LoadingButton
loading={loading}
disabled={
!balanceThreshold || (swapTo == "external" && !destination)
}
>
Enable Auto Swaps
</LoadingButton>
</form>
) : (
<Card className="w-full hidden md:block self-start">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="font-medium">Active Recurring Swap</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Alby Hub will try to perform a swap every time the balance reaches
the threshold.
</p>
<div className="mt-6 space-y-4 text-sm">
<div className="flex justify-between items-center">
<span className="font-medium">Type</span>
<span className="text-muted-foreground text-right">
Lightning to On-chain
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Destination</span>
<span className="text-muted-foreground text-right">
{swapsSettings.destination}
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Spending Balance Threshold</span>
<span className="text-muted-foreground text-right">
{new Intl.NumberFormat().format(
swapsSettings.balanceThreshold
)}{" "}
sats
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Swap amount</span>
<span className="text-muted-foreground text-right">
{new Intl.NumberFormat().format(swapsSettings.swapAmount)}{" "}
sats
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-medium">Fee</span>
<span className="text-muted-foreground text-right">
{swapsSettings.albyServiceFee + swapsSettings.boltzServiceFee}
% + on-chain fees
</span>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-end">
<Button onClick={onDeactivate} disabled={loading} variant="outline">
<XCircleIcon className="h-4 w-4 mr-2" />
Deactivate
</Button>
</CardFooter>
</Card>
)}
</>
);
}
export default Swaps;

View file

@ -190,6 +190,16 @@ export type AppMetadata = { app_store_app_id?: string } & Record<
unknown
>;
export type SwapsSettingsResponse = {
enabled: boolean;
balanceThreshold: number;
swapAmount: number;
destination: string;
albyServiceFee: number;
boltzServiceFee: number;
boltzNetworkFee: number;
};
export interface MnemonicResponse {
mnemonic: string;
}

20
go.mod
View file

@ -1,8 +1,6 @@
module github.com/getAlby/hub
go 1.24.1
toolchain go1.24.2
go 1.24.2
require (
github.com/adrg/xdg v0.5.3
@ -73,6 +71,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fergusstrange/embedded-postgres v1.29.0 // indirect
github.com/frankban/quicktest v1.14.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
@ -85,7 +84,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-migrate/migrate/v4 v4.18.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
@ -148,6 +147,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -173,7 +173,7 @@ require (
github.com/samber/lo v1.49.1 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@ -184,6 +184,9 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
github.com/vulpemventures/go-elements v0.5.5 // indirect
github.com/vulpemventures/go-secp256k1-zkp v1.1.6 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
@ -216,13 +219,13 @@ require (
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
@ -246,6 +249,7 @@ require (
)
require (
github.com/BoltzExchange/boltz-client/v2 v2.6.0
github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
@ -258,7 +262,7 @@ require (
github.com/lightningnetwork/lnd v0.19.0-beta.rc3
github.com/sirupsen/logrus v1.9.3
github.com/tyler-smith/go-bip32 v1.0.0
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
gorm.io/datatypes v1.2.5
)

34
go.sum
View file

@ -6,6 +6,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BoltzExchange/boltz-client/v2 v2.6.0 h1:TB7w3/p8fvymAcuaFpv9yozE3Ga6igtqVvtg7AH1Ohs=
github.com/BoltzExchange/boltz-client/v2 v2.6.0/go.mod h1:bCuK5Lus9QY8L6z+W95wmkeO6U9OuaysRhRbI/d/fi0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/appsec-internal-go v1.9.0 h1:cGOneFsg0JTRzWl5U2+og5dbtyW3N8XaYwc5nXe39Vw=
github.com/DataDog/appsec-internal-go v1.9.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
@ -200,6 +202,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
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=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fergusstrange/embedded-postgres v1.29.0 h1:Uv8hdhoiaNMuH0w8UuGXDHr60VoAQPFdgx7Qf3bzXJM=
@ -213,8 +217,8 @@ github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/getAlby/ldk-node-go v0.0.0-20250503035148-4f935f853d83 h1:eOkG4g/8IFSK7zyNDr9X10yxBsqCgVfU2WDd70RLfBw=
@ -250,8 +254,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
@ -646,8 +650,8 @@ github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@ -701,6 +705,12 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 h1:CTcw80hz/Sw8hqlKX5ZYvBUF5gAHSHwdjXxRf/cjDcI=
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:GXBJykxW2kUcktGdsgyay7uwwWvkljASfljNcT0mbh8=
github.com/vulpemventures/go-elements v0.5.5 h1:oN76qcussRvVn8jkwaCI9+thmP9UeXwrzghAoWFzLAg=
github.com/vulpemventures/go-elements v0.5.5/go.mod h1:Tvhb+rZWv3lxoI5CdK03J3V+e2QVr/7UAnCYILxFSq4=
github.com/vulpemventures/go-secp256k1-zkp v1.1.6 h1:BmsrmXRLUibwa75Qkk8yELjpzCzlAjYFGLiLiOdq7Xo=
github.com/vulpemventures/go-secp256k1-zkp v1.1.6/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
@ -819,8 +829,8 @@ golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZP
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -862,8 +872,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
@ -962,8 +972,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -158,6 +158,9 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
restrictedApiGroup.GET("/health", httpSvc.healthHandler)
restrictedApiGroup.GET("/commands", httpSvc.getCustomNodeCommandsHandler)
restrictedApiGroup.POST("/command", httpSvc.execCustomNodeCommandHandler)
restrictedApiGroup.GET("/settings/swaps", httpSvc.getAutoSwapsConfigHandler)
restrictedApiGroup.POST("/settings/swaps", httpSvc.enableAutoSwapsHandler)
restrictedApiGroup.DELETE("/settings/swaps", httpSvc.disableAutoSwapsHandler)
httpSvc.albyHttpSvc.RegisterSharedRoutes(restrictedApiGroup, e)
}
@ -1155,3 +1158,44 @@ func (httpSvc *HttpService) healthHandler(c echo.Context) error {
return c.JSON(http.StatusOK, healthResponse)
}
func (httpSvc *HttpService) getAutoSwapsConfigHandler(c echo.Context) error {
getAutoSwapsConfigResponse, err := httpSvc.api.GetAutoSwapsConfig()
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to get swap settings: %v", err),
})
}
return c.JSON(http.StatusOK, getAutoSwapsConfigResponse)
}
func (httpSvc *HttpService) enableAutoSwapsHandler(c echo.Context) error {
var enableAutoSwapsRequest api.EnableAutoSwapsRequest
if err := c.Bind(&enableAutoSwapsRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}
err := httpSvc.api.EnableAutoSwaps(c.Request().Context(), &enableAutoSwapsRequest)
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to save swap settings: %v", err),
})
}
return c.NoContent(http.StatusNoContent)
}
func (httpSvc *HttpService) disableAutoSwapsHandler(c echo.Context) error {
err := httpSvc.api.DisableAutoSwaps()
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: err.Error(),
})
}
return c.NoContent(http.StatusNoContent)
}

View file

@ -72,7 +72,7 @@ func (cs *CashuService) Shutdown() error {
return cs.wallet.Shutdown()
}
func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64) (response *lnclient.PayInvoiceResponse, err error) {
func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64, timeoutSeconds *int64) (response *lnclient.PayInvoiceResponse, err error) {
// TODO: support 0-amount invoices
if amount != nil {
return nil, errors.New("0-amount invoices not supported")

View file

@ -26,6 +26,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
@ -435,7 +436,12 @@ func getMaxTotalRoutingFeeLimit(amountMsat uint64) ldk_node.MaxTotalRoutingFeeLi
}
}
func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64, timeoutSeconds *int64) (*lnclient.PayInvoiceResponse, error) {
sendPaymentTimeout := int64(constants.SEND_PAYMENT_TIMEOUT)
if timeoutSeconds != nil {
sendPaymentTimeout = *timeoutSeconds
}
paymentRequest, err := decodepay.Decodepay(invoice)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
@ -485,59 +491,67 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amoun
fee := uint64(0)
preimage := ""
for start := time.Now(); time.Since(start) < time.Second*50; {
event := <-ldkEventSubscription
timeout := time.Second * time.Duration(sendPaymentTimeout)
timer := time.NewTimer(timeout)
defer timer.Stop()
eventPaymentSuccessful, isEventPaymentSuccessfulEvent := (*event).(ldk_node.EventPaymentSuccessful)
eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed)
if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash {
logger.Logger.Info("Got payment success event")
payment := ls.node.Payment(paymentHash)
if payment == nil {
logger.Logger.WithField("payment_hash", paymentHash).Error("Couldn't find payment by payment hash")
return nil, errors.New("payment not found")
}
if eventPaymentSuccessful.PaymentPreimage == nil {
logger.Logger.WithField("payment_hash", paymentHash).Error("No payment preimage in payment success event")
return nil, errors.New("payment preimage not found")
}
preimage = *eventPaymentSuccessful.PaymentPreimage
if eventPaymentSuccessful.FeePaidMsat != nil {
fee = *eventPaymentSuccessful.FeePaidMsat
}
break
}
if isEventPaymentFailedEvent && eventPaymentFailed.PaymentHash != nil && *eventPaymentFailed.PaymentHash == paymentHash {
failureReasonMessage := ls.getPaymentFailReason(&eventPaymentFailed)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-timer.C:
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
"reason": failureReasonMessage,
}).Error("Received payment failed event")
"paymentHash": paymentHash,
}).Warn("Timed out waiting for payment to be sent")
return nil, lnclient.NewTimeoutError()
return nil, fmt.Errorf("received payment failed event: %s", failureReasonMessage)
case ev := <-ldkEventSubscription:
switch event := (*ev).(type) {
case ldk_node.EventPaymentSuccessful:
if event.PaymentHash != paymentHash {
continue
}
logger.Logger.Info("Got payment success event")
payment := ls.node.Payment(paymentHash)
if payment == nil {
logger.Logger.WithField("payment_hash", paymentHash).Error("Couldn't find payment by payment hash")
return nil, errors.New("payment not found")
}
if event.PaymentPreimage == nil {
logger.Logger.WithField("payment_hash", paymentHash).Error("No payment preimage in payment success event")
return nil, errors.New("payment preimage not found")
}
preimage = *event.PaymentPreimage
if event.FeePaidMsat != nil {
fee = *event.FeePaidMsat
}
logger.Logger.WithFields(logrus.Fields{
"duration": time.Since(paymentStart).Milliseconds(),
"fee": fee,
}).Info("Successful payment")
return &lnclient.PayInvoiceResponse{
Preimage: preimage,
Fee: fee,
}, nil
case ldk_node.EventPaymentFailed:
if event.PaymentHash != nil && *event.PaymentHash == paymentHash {
failureReasonMessage := ls.getPaymentFailReason(&event)
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
"reason": failureReasonMessage,
}).Error("Received payment failed event")
return nil, fmt.Errorf("received payment failed event: %s", failureReasonMessage)
}
}
}
}
if preimage == "" {
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).Warn("Timed out waiting for payment to be sent")
return nil, lnclient.NewTimeoutError()
}
logger.Logger.WithFields(logrus.Fields{
"duration": time.Since(paymentStart).Milliseconds(),
"fee": fee,
}).Info("Successful payment")
return &lnclient.PayInvoiceResponse{
Preimage: preimage,
Fee: fee,
}, nil
}
func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {

View file

@ -19,6 +19,7 @@ import (
"google.golang.org/grpc/status"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/lnclient/lnd/wrapper"
@ -284,9 +285,14 @@ func (svc *LNDService) Shutdown() error {
return nil
}
func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64, timeoutSeconds *int64) (*lnclient.PayInvoiceResponse, error) {
const MAX_PARTIAL_PAYMENTS = 16
const SEND_PAYMENT_TIMEOUT = 50
sendPaymentTimeout := int64(constants.SEND_PAYMENT_TIMEOUT)
if timeoutSeconds != nil {
sendPaymentTimeout = *timeoutSeconds
}
paymentRequest, err := decodepay.Decodepay(payReq)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
@ -302,7 +308,7 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string, amoun
sendRequest := &routerrpc.SendPaymentRequest{
PaymentRequest: payReq,
MaxParts: MAX_PARTIAL_PAYMENTS,
TimeoutSeconds: SEND_PAYMENT_TIMEOUT,
TimeoutSeconds: int32(sendPaymentTimeout),
FeeLimitMsat: int64(transactions.CalculateFeeReserveMsat(paymentAmountMsat)),
}

View file

@ -56,7 +56,7 @@ type NodeConnectionInfo struct {
}
type LNClient interface {
SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*PayInvoiceResponse, error)
SendPaymentSync(ctx context.Context, payReq string, amount *uint64, timeoutSeconds *int64) (*PayInvoiceResponse, error)
SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []TLVRecord, preimage string) (*PayKeysendResponse, error)
GetPubkey() string
GetInfo(ctx context.Context) (info *NodeInfo, err error)

View file

@ -350,7 +350,7 @@ func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string
return transaction, nil
}
func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64, timeoutSeconds *int64) (*lnclient.PayInvoiceResponse, error) {
// TODO: support 0-amount invoices
if amount != nil {
return nil, errors.New("0-amount invoices not supported")

View file

@ -59,7 +59,7 @@ func (controller *nip47Controller) pay(ctx context.Context, bolt11 string, amoun
"bolt11": bolt11,
}).Info("Sending payment")
transaction, err := controller.transactionsService.SendPaymentSync(ctx, bolt11, amount, metadata, controller.lnClient, &app.ID, &requestEventId)
transaction, err := controller.transactionsService.SendPaymentSync(ctx, bolt11, amount, metadata, controller.lnClient, &app.ID, &requestEventId, nil)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,

View file

@ -8,12 +8,14 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/service/keys"
"github.com/getAlby/hub/swaps"
"github.com/getAlby/hub/transactions"
)
type Service interface {
StartApp(encryptionKey string) error
StopApp()
StartAutoSwaps() error
Shutdown()
// TODO: remove getters (currently used by http / wails services)
@ -21,6 +23,7 @@ type Service interface {
GetEventPublisher() events.EventPublisher
GetLNClient() lnclient.LNClient
GetTransactionsService() transactions.TransactionsService
GetSwapsService() swaps.SwapsService
GetDB() *gorm.DB
GetConfig() config.Config
GetKeys() keys.Keys

View file

@ -19,6 +19,7 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/service/keys"
"github.com/getAlby/hub/swaps"
"github.com/getAlby/hub/transactions"
"github.com/getAlby/hub/version"
@ -35,6 +36,7 @@ type service struct {
db *gorm.DB
lnClient lnclient.LNClient
transactionsService transactions.TransactionsService
swapsService swaps.SwapsService
albyOAuthSvc alby.AlbyOAuthService
eventPublisher events.EventPublisher
ctx context.Context
@ -115,6 +117,8 @@ func NewService(ctx context.Context) (*service, error) {
albyOAuthSvc := alby.NewAlbyOAuthService(gormDB, cfg, keys, eventPublisher)
transactionsSvc := transactions.NewTransactionsService(gormDB, eventPublisher)
var wg sync.WaitGroup
svc := &service{
cfg: cfg,
@ -123,7 +127,8 @@ func NewService(ctx context.Context) (*service, error) {
eventPublisher: eventPublisher,
albyOAuthSvc: albyOAuthSvc,
nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher, albyOAuthSvc),
transactionsService: transactions.NewTransactionsService(gormDB, eventPublisher),
transactionsService: transactionsSvc,
swapsService: swaps.NewSwapsService(cfg, eventPublisher, transactionsSvc),
db: gormDB,
keys: keys,
}
@ -250,6 +255,10 @@ func (svc *service) GetTransactionsService() transactions.TransactionsService {
return svc.transactionsService
}
func (svc *service) GetSwapsService() swaps.SwapsService {
return svc.swapsService
}
func (svc *service) GetKeys() keys.Keys {
return svc.keys
}

View file

@ -286,6 +286,11 @@ func (svc *service) StartApp(encryptionKey string) error {
return err
}
err = svc.StartAutoSwaps()
if err != nil {
logger.Logger.WithError(err).Error("Couldn't enable auto swaps")
}
svc.publishAllAppInfoEvents()
svc.startupState = "Connecting To Relay"
@ -300,6 +305,10 @@ func (svc *service) StartApp(encryptionKey string) error {
return nil
}
func (svc *service) StartAutoSwaps() error {
return svc.GetSwapsService().EnableAutoSwaps(svc.ctx, svc.lnClient)
}
func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) error {
if svc.lnClient != nil {
logger.Logger.Error("LNClient already started")
@ -343,7 +352,7 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e
setStartupState := func(startupState string) {
svc.startupState = startupState
}
lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, mnemonic, ldkWorkdir, svc.cfg.GetEnv().LDKNetwork, vssToken, setStartupState)
lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, mnemonic, ldkWorkdir, svc.cfg.GetNetwork(), vssToken, setStartupState)
case config.PhoenixBackendType:
PhoenixdAddress, _ := svc.cfg.Get("PhoenixdAddress", encryptionKey)
PhoenixdAuthorization, _ := svc.cfg.Get("PhoenixdAuthorization", encryptionKey)

View file

@ -11,6 +11,7 @@ func (svc *service) StopApp() {
if svc.appCancelFn != nil {
logger.Logger.Info("Stopping app...")
svc.appCancelFn()
svc.swapsService.StopAutoSwaps()
svc.wg.Wait()
logger.Logger.Info("app stopped")
}

415
swaps/swaps_service.go Normal file
View file

@ -0,0 +1,415 @@
package swaps
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/BoltzExchange/boltz-client/v2/pkg/boltz"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/transactions"
"github.com/sirupsen/logrus"
)
type swapsService struct {
cancelFn context.CancelFunc
cfg config.Config
eventPublisher events.EventPublisher
transactionsService transactions.TransactionsService
boltzApi *boltz.Api
}
type SwapsService interface {
EnableAutoSwaps(ctx context.Context, lnClient lnclient.LNClient) error
StopAutoSwaps()
CalculateFee() (*SwapFees, error)
ReverseSwap(ctx context.Context, amount uint64, destination string, lnClient lnclient.LNClient) error
}
const (
AlbySwapServiceFee = 1.0
)
type FeeRates struct {
FastestFee uint64 `json:"fastestFee"`
HalfHourFee uint64 `json:"halfHourFee"`
HourFee uint64 `json:"hourFee"`
EconomyFee uint64 `json:"economyFee"`
MinimumFee uint64 `json:"minimumFee"`
}
type SwapFees struct {
AlbyServiceFee float64 `json:"albyServiceFee"`
BoltzServiceFee float64 `json:"boltzServiceFee"`
BoltzNetworkFee uint64 `json:"boltzNetworkFee"`
}
func NewSwapsService(cfg config.Config, eventPublisher events.EventPublisher, transactionsService transactions.TransactionsService) *swapsService {
return &swapsService{
cfg: cfg,
eventPublisher: eventPublisher,
transactionsService: transactionsService,
boltzApi: &boltz.Api{URL: cfg.GetEnv().BoltzApi},
}
}
func (svc *swapsService) EnableAutoSwaps(ctx context.Context, lnClient lnclient.LNClient) error {
// stop any existing swap process
svc.StopAutoSwaps()
ctx, cancelFn := context.WithCancel(ctx)
swapDestination, _ := svc.cfg.Get(config.AutoSwapDestinationKey, "")
balanceThresholdStr, _ := svc.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
amountStr, _ := svc.cfg.Get(config.AutoSwapAmountKey, "")
if swapDestination == "" || balanceThresholdStr == "" || amountStr == "" {
cancelFn()
return errors.New("auto swap not configured")
}
parsedBalanceThreshold, err := strconv.ParseUint(balanceThresholdStr, 10, 64)
if err != nil {
cancelFn()
return errors.New("invalid auto swap configuration")
}
amount, err := strconv.ParseUint(amountStr, 10, 64)
if err != nil {
cancelFn()
return errors.New("invalid auto swap configuration")
}
logger.Logger.Info("Starting auto swap workflow")
go func() {
ticker := time.NewTicker(1 * time.Hour)
for {
select {
case <-ticker.C:
logger.Logger.Debug("Checking to see if we can swap")
balance, err := lnClient.GetBalances(ctx, false)
if err != nil {
logger.Logger.WithError(err).Error("Failed to get balance")
return
}
lightningBalance := uint64(balance.Lightning.TotalSpendable)
balanceThresholdMilliSats := parsedBalanceThreshold * 1000
if lightningBalance >= balanceThresholdMilliSats {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"destination": swapDestination,
}).Info("Initiating swap")
err := svc.ReverseSwap(ctx, amount, swapDestination, lnClient)
if err != nil {
logger.Logger.WithError(err).Error("Failed to swap")
}
} else {
logger.Logger.Info("Threshold requirements not met for swap, ignoring")
}
case <-ctx.Done():
logger.Logger.Info("Stopping auto swap workflow")
return
}
}
}()
svc.cancelFn = cancelFn
return nil
}
func (svc *swapsService) StopAutoSwaps() {
if svc.cancelFn != nil {
logger.Logger.Info("Stopping swap service...")
svc.cancelFn()
logger.Logger.Info("swap service stopped")
}
}
func (svc *swapsService) ReverseSwap(ctx context.Context, amount uint64, destination string, lnClient lnclient.LNClient) error {
var network, err = boltz.ParseChain(svc.cfg.GetNetwork())
if err != nil {
return err
}
ourKeys, err := btcec.NewPrivateKey()
if err != nil {
return err
}
preimage := make([]byte, 32)
_, err = rand.Read(preimage)
if err != nil {
return err
}
preimageHash := sha256.Sum256(preimage)
reversePairs, err := svc.boltzApi.GetReversePairs()
if err != nil {
return fmt.Errorf("could not get reverse pairs: %s", err)
}
pair := boltz.Pair{From: boltz.CurrencyBtc, To: boltz.CurrencyBtc}
pairInfo, err := boltz.FindPair(pair, reversePairs)
if err != nil {
return fmt.Errorf("could not find reverse pair: %s", err)
}
fees := pairInfo.Fees
serviceFeePercentage := boltz.Percentage(fees.Percentage)
serviceFee := boltz.CalculatePercentage(serviceFeePercentage, amount)
networkFee := fees.MinerFees.Lockup + fees.MinerFees.Claim
logger.Logger.WithFields(logrus.Fields{
"serviceFee": serviceFee,
"networkFee": networkFee,
}).Info("Calculated fees for swap")
albyFee := &boltz.ExtraFees{
Percentage: AlbySwapServiceFee,
Id: "albyServiceFee",
}
swap, err := svc.boltzApi.CreateReverseSwap(boltz.CreateReverseSwapRequest{
From: boltz.CurrencyBtc,
To: boltz.CurrencyBtc,
ClaimPublicKey: ourKeys.PubKey().SerializeCompressed(),
PreimageHash: preimageHash[:],
InvoiceAmount: amount,
Description: "Boltz swap invoice",
PairHash: pairInfo.Hash,
ReferralId: "alby",
ExtraFees: albyFee,
})
if err != nil {
return fmt.Errorf("could not create swap: %s", err)
}
boltzPubKey, err := btcec.ParsePubKey(swap.RefundPublicKey)
if err != nil {
return err
}
tree := swap.SwapTree.Deserialize()
if err := tree.Init(boltz.CurrencyBtc, true, ourKeys, boltzPubKey); err != nil {
return err
}
if err := tree.Check(boltz.ReverseSwap, swap.TimeoutBlockHeight, preimageHash[:]); err != nil {
return err
}
logger.Logger.WithField("swap", swap).Info("Swap created")
boltzWs := svc.boltzApi.NewWebsocket()
if err := boltzWs.Connect(); err != nil {
return fmt.Errorf("could not connect to Boltz websocket: %w", err)
}
defer func() {
if err := boltzWs.Close(); err != nil {
logger.Logger.WithError(err).Error("Failed to close boltz websocket")
}
}()
if err := boltzWs.Subscribe([]string{swap.Id}); err != nil {
return err
}
paymentErrorCh := make(chan error, 1)
updatesCh := boltzWs.Updates
for {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-paymentErrorCh:
return err
case update, ok := <-updatesCh:
if !ok {
return errors.New("boltz websocket closed unexpectedly")
}
parsedStatus := boltz.ParseEvent(update.Status)
switch parsedStatus {
case boltz.SwapCreated:
logger.Logger.WithFields(logrus.Fields{
"swap": swap,
"update": update,
}).Info("Paying the swap invoice")
err := lnClient.SendPaymentProbes(ctx, swap.Invoice)
if err != nil {
logger.Logger.WithField("swapId", swap.Id).Info("Couldn't probe invoice payment, terminating swap")
return err
}
go func() {
metadata := map[string]interface{}{
"swapId": swap.Id,
"onchainAmount": swap.OnchainAmount,
"refundPubkey": swap.RefundPublicKey,
}
sendPaymentTimeout := int64(3600)
_, err := svc.transactionsService.SendPaymentSync(ctx, swap.Invoice, nil, metadata, lnClient, nil, nil, &sendPaymentTimeout)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"swap": swap,
"update": update,
}).Error("Error paying the swap invoice")
paymentErrorCh <- err
return
}
logger.Logger.WithField("swapId", swap.Id).Info("Initiated swap invoice payment")
}()
case boltz.TransactionMempool:
logger.Logger.WithFields(logrus.Fields{
"swapId": swap.Id,
"transaction": update.Transaction,
}).Info("Lockup transaction found in mempool")
case boltz.TransactionConfirmed:
logger.Logger.WithFields(logrus.Fields{
"swapId": swap.Id,
"transaction": update.Transaction,
}).Info("Lockup transaction confirmed in mempool")
lockupTransaction, err := boltz.NewTxFromHex(boltz.CurrencyBtc, update.Transaction.Hex, nil)
if err != nil {
return err
}
vout, _, err := lockupTransaction.FindVout(network, swap.LockupAddress)
if err != nil {
return err
}
feeRates, err := svc.getFeeRates()
if err != nil {
return err
}
claimTransaction, _, err := boltz.ConstructTransaction(
network,
boltz.CurrencyBtc,
[]boltz.OutputDetails{
{
SwapId: swap.Id,
SwapType: boltz.ReverseSwap,
Address: destination,
LockupTransaction: lockupTransaction,
Vout: vout,
Preimage: preimage,
PrivateKey: ourKeys,
SwapTree: tree,
Cooperative: true,
},
},
float64(feeRates.FastestFee),
svc.boltzApi,
)
if err != nil {
return fmt.Errorf("could not create claim transaction: %w", err)
}
txHex, err := claimTransaction.Serialize()
if err != nil {
return fmt.Errorf("could not serialize claim transaction: %w", err)
}
// TODO: Replace with LNClient method
txId, err := svc.boltzApi.BroadcastTransaction(boltz.CurrencyBtc, txHex)
if err != nil {
return fmt.Errorf("could not broadcast transaction: %w", err)
}
logger.Logger.WithField("txId", txId).Info("Transaction broadcasted")
case boltz.InvoiceSettled:
logger.Logger.WithField("swapId", swap.Id).Info("Swap succeeded")
svc.eventPublisher.Publish(&events.Event{
Event: "nwc_swap_succeeded",
Properties: map[string]interface{}{
"swapId": swap.Id,
"invoice": swap.Invoice,
"onchainAmount": swap.OnchainAmount,
"refundPubkey": swap.RefundPublicKey,
},
})
return nil
}
}
}
}
func (svc *swapsService) CalculateFee() (*SwapFees, error) {
reversePairs, err := svc.boltzApi.GetReversePairs()
if err != nil {
return nil, fmt.Errorf("could not get reverse pairs: %s", err)
}
pair := boltz.Pair{From: boltz.CurrencyBtc, To: boltz.CurrencyBtc}
pairInfo, err := boltz.FindPair(pair, reversePairs)
if err != nil {
return nil, fmt.Errorf("could not find reverse pair: %s", err)
}
fees := pairInfo.Fees
networkFee := fees.MinerFees.Lockup + fees.MinerFees.Claim
return &SwapFees{
AlbyServiceFee: AlbySwapServiceFee,
BoltzServiceFee: fees.Percentage,
BoltzNetworkFee: networkFee,
}, nil
}
func (svc *swapsService) getFeeRates() (*FeeRates, error) {
url := svc.cfg.GetEnv().MempoolApi + "/v1/fees/recommended"
client := http.Client{
Timeout: time.Second * 10,
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": url,
}).Error("Failed to create http request")
return nil, err
}
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": url,
}).Error("Failed to send request")
return nil, err
}
defer res.Body.Close()
body, readErr := io.ReadAll(res.Body)
if readErr != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": url,
}).Error("Failed to read response body")
return nil, errors.New("failed to read response body")
}
var rates FeeRates
jsonErr := json.Unmarshal(body, &rates)
if jsonErr != nil {
logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{
"url": url,
}).Error("Failed to deserialize json")
return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body))
}
return &rates, nil
}

View file

@ -78,7 +78,7 @@ func NewMockLn() (*MockLn, error) {
return &MockLn{}, nil
}
func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string, amount *uint64, timeoutSeconds *int64) (*lnclient.PayInvoiceResponse, error) {
if len(mln.PayInvoiceResponses) > 0 {
response := mln.PayInvoiceResponses[0]
err := mln.PayInvoiceErrors[0]

View file

@ -307,6 +307,51 @@ func (_c *MockConfig_GetJWTSecret_Call) RunAndReturn(run func() string) *MockCon
return _c
}
// GetNetwork provides a mock function with no fields
func (_m *MockConfig) GetNetwork() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetNetwork")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockConfig_GetNetwork_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNetwork'
type MockConfig_GetNetwork_Call struct {
*mock.Call
}
// GetNetwork is a helper method to define mock.On call
func (_e *MockConfig_Expecter) GetNetwork() *MockConfig_GetNetwork_Call {
return &MockConfig_GetNetwork_Call{Call: _e.mock.On("GetNetwork")}
}
func (_c *MockConfig_GetNetwork_Call) Run(run func()) *MockConfig_GetNetwork_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConfig_GetNetwork_Call) Return(_a0 string) *MockConfig_GetNetwork_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConfig_GetNetwork_Call) RunAndReturn(run func() string) *MockConfig_GetNetwork_Call {
_c.Call.Return(run)
return _c
}
// GetRelayUrl provides a mock function with no fields
func (_m *MockConfig) GetRelayUrl() string {
ret := _m.Called()

View file

@ -1572,9 +1572,9 @@ func (_c *MockLNClient_SendPaymentProbes_Call) RunAndReturn(run func(context.Con
return _c
}
// SendPaymentSync provides a mock function with given fields: ctx, payReq, amount
func (_m *MockLNClient) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
ret := _m.Called(ctx, payReq, amount)
// SendPaymentSync provides a mock function with given fields: ctx, payReq, amount, timeoutSeconds
func (_m *MockLNClient) SendPaymentSync(ctx context.Context, payReq string, amount *uint64, timeoutSeconds *int64) (*lnclient.PayInvoiceResponse, error) {
ret := _m.Called(ctx, payReq, amount, timeoutSeconds)
if len(ret) == 0 {
panic("no return value specified for SendPaymentSync")
@ -1582,19 +1582,19 @@ func (_m *MockLNClient) SendPaymentSync(ctx context.Context, payReq string, amou
var r0 *lnclient.PayInvoiceResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, *uint64) (*lnclient.PayInvoiceResponse, error)); ok {
return rf(ctx, payReq, amount)
if rf, ok := ret.Get(0).(func(context.Context, string, *uint64, *int64) (*lnclient.PayInvoiceResponse, error)); ok {
return rf(ctx, payReq, amount, timeoutSeconds)
}
if rf, ok := ret.Get(0).(func(context.Context, string, *uint64) *lnclient.PayInvoiceResponse); ok {
r0 = rf(ctx, payReq, amount)
if rf, ok := ret.Get(0).(func(context.Context, string, *uint64, *int64) *lnclient.PayInvoiceResponse); ok {
r0 = rf(ctx, payReq, amount, timeoutSeconds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*lnclient.PayInvoiceResponse)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, *uint64) error); ok {
r1 = rf(ctx, payReq, amount)
if rf, ok := ret.Get(1).(func(context.Context, string, *uint64, *int64) error); ok {
r1 = rf(ctx, payReq, amount, timeoutSeconds)
} else {
r1 = ret.Error(1)
}
@ -1611,13 +1611,14 @@ type MockLNClient_SendPaymentSync_Call struct {
// - ctx context.Context
// - payReq string
// - amount *uint64
func (_e *MockLNClient_Expecter) SendPaymentSync(ctx interface{}, payReq interface{}, amount interface{}) *MockLNClient_SendPaymentSync_Call {
return &MockLNClient_SendPaymentSync_Call{Call: _e.mock.On("SendPaymentSync", ctx, payReq, amount)}
// - timeoutSeconds *int64
func (_e *MockLNClient_Expecter) SendPaymentSync(ctx interface{}, payReq interface{}, amount interface{}, timeoutSeconds interface{}) *MockLNClient_SendPaymentSync_Call {
return &MockLNClient_SendPaymentSync_Call{Call: _e.mock.On("SendPaymentSync", ctx, payReq, amount, timeoutSeconds)}
}
func (_c *MockLNClient_SendPaymentSync_Call) Run(run func(ctx context.Context, payReq string, amount *uint64)) *MockLNClient_SendPaymentSync_Call {
func (_c *MockLNClient_SendPaymentSync_Call) Run(run func(ctx context.Context, payReq string, amount *uint64, timeoutSeconds *int64)) *MockLNClient_SendPaymentSync_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(*uint64))
run(args[0].(context.Context), args[1].(string), args[2].(*uint64), args[3].(*int64))
})
return _c
}
@ -1627,7 +1628,7 @@ func (_c *MockLNClient_SendPaymentSync_Call) Return(_a0 *lnclient.PayInvoiceResp
return _c
}
func (_c *MockLNClient_SendPaymentSync_Call) RunAndReturn(run func(context.Context, string, *uint64) (*lnclient.PayInvoiceResponse, error)) *MockLNClient_SendPaymentSync_Call {
func (_c *MockLNClient_SendPaymentSync_Call) RunAndReturn(run func(context.Context, string, *uint64, *int64) (*lnclient.PayInvoiceResponse, error)) *MockLNClient_SendPaymentSync_Call {
_c.Call.Return(run)
return _c
}

View file

@ -16,6 +16,8 @@ import (
mock "github.com/stretchr/testify/mock"
swaps "github.com/getAlby/hub/swaps"
transactions "github.com/getAlby/hub/transactions"
)
@ -359,6 +361,53 @@ func (_c *MockService_GetStartupState_Call) RunAndReturn(run func() string) *Moc
return _c
}
// GetSwapsService provides a mock function with no fields
func (_m *MockService) GetSwapsService() swaps.SwapsService {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetSwapsService")
}
var r0 swaps.SwapsService
if rf, ok := ret.Get(0).(func() swaps.SwapsService); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(swaps.SwapsService)
}
}
return r0
}
// MockService_GetSwapsService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSwapsService'
type MockService_GetSwapsService_Call struct {
*mock.Call
}
// GetSwapsService is a helper method to define mock.On call
func (_e *MockService_Expecter) GetSwapsService() *MockService_GetSwapsService_Call {
return &MockService_GetSwapsService_Call{Call: _e.mock.On("GetSwapsService")}
}
func (_c *MockService_GetSwapsService_Call) Run(run func()) *MockService_GetSwapsService_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockService_GetSwapsService_Call) Return(_a0 swaps.SwapsService) *MockService_GetSwapsService_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockService_GetSwapsService_Call) RunAndReturn(run func() swaps.SwapsService) *MockService_GetSwapsService_Call {
_c.Call.Return(run)
return _c
}
// GetTransactionsService provides a mock function with no fields
func (_m *MockService) GetTransactionsService() transactions.TransactionsService {
ret := _m.Called()
@ -529,6 +578,51 @@ func (_c *MockService_StartApp_Call) RunAndReturn(run func(string) error) *MockS
return _c
}
// StartAutoSwaps provides a mock function with no fields
func (_m *MockService) StartAutoSwaps() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for StartAutoSwaps")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockService_StartAutoSwaps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartAutoSwaps'
type MockService_StartAutoSwaps_Call struct {
*mock.Call
}
// StartAutoSwaps is a helper method to define mock.On call
func (_e *MockService_Expecter) StartAutoSwaps() *MockService_StartAutoSwaps_Call {
return &MockService_StartAutoSwaps_Call{Call: _e.mock.On("StartAutoSwaps")}
}
func (_c *MockService_StartAutoSwaps_Call) Run(run func()) *MockService_StartAutoSwaps_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockService_StartAutoSwaps_Call) Return(_a0 error) *MockService_StartAutoSwaps_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockService_StartAutoSwaps_Call) RunAndReturn(run func() error) *MockService_StartAutoSwaps_Call {
_c.Call.Return(run)
return _c
}
// StopApp provides a mock function with no fields
func (_m *MockService) StopApp() {
_m.Called()

View file

@ -28,7 +28,7 @@ func TestSendPaymentSync_App_NoPermission(t *testing.T) {
assert.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.Equal(t, "app does not have pay_invoice scope", err.Error())
@ -57,7 +57,7 @@ func TestSendPaymentSync_App_WithPermission(t *testing.T) {
assert.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -94,7 +94,7 @@ func TestSendPaymentSync_App_BudgetExceeded(t *testing.T) {
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewQuotaExceededError())
@ -141,7 +141,7 @@ func TestSendPaymentSync_App_BudgetExceeded_SettledPayment(t *testing.T) {
assert.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewQuotaExceededError())
@ -180,7 +180,7 @@ func TestSendPaymentSync_App_BudgetExceeded_UnsettledPayment(t *testing.T) {
assert.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewQuotaExceededError())
@ -220,7 +220,7 @@ func TestSendPaymentSync_App_BudgetNotExceeded_FailedPayment(t *testing.T) {
assert.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)

View file

@ -37,7 +37,7 @@ func TestSendPaymentSync_IsolatedApp_NoBalance(t *testing.T) {
assert.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewInsufficientBalanceError())
@ -79,7 +79,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficient(t *testing.T) {
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewInsufficientBalanceError())
@ -125,7 +125,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -174,7 +174,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_OutstandingPayment(t *t
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewInsufficientBalanceError())
@ -220,7 +220,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_SettledPayment(t *testi
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewInsufficientBalanceError())
@ -265,7 +265,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient_UnrelatedPayment(t *testi
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -313,7 +313,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient_FailedPayment(t *testing.
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -355,7 +355,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficientThenSufficient(t *testin
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.Error(t, err)
assert.ErrorIs(t, err, NewInsufficientBalanceError())
@ -368,7 +368,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficientThenSufficient(t *testin
AmountMsat: 10000, // add extra to cover fee reserves max of(10 sats or 1%)
})
transaction, err = transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err = transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)

View file

@ -32,7 +32,7 @@ func TestSendPaymentSync_NoApp(t *testing.T) {
}
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, metadata, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, metadata, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -62,7 +62,7 @@ func TestSendPaymentSync_0Amount(t *testing.T) {
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
amount := uint64(1234)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.Mock0AmountInvoice, &amount, metadata, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.Mock0AmountInvoice, &amount, metadata, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
assert.Equal(t, amount, transaction.AmountMsat)
@ -82,7 +82,7 @@ func TestSendPaymentSync_MetadataTooLarge(t *testing.T) {
metadata["randomkey"] = strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-15) // json encoding adds 16 characters
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, metadata, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, metadata, svc.LNClient, nil, nil, nil)
assert.Error(t, err)
assert.Equal(t, fmt.Sprintf("encoded payment metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, constants.INVOICE_METADATA_MAX_LENGTH+1), err.Error())
@ -104,7 +104,7 @@ func TestSendPaymentSync_Duplicate_AlreadyPaid(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.Error(t, err)
assert.Equal(t, "this invoice has already been paid", err.Error())
@ -126,7 +126,7 @@ func TestSendPaymentSync_Duplicate_Pending(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.Error(t, err)
assert.Equal(t, "there is already a payment pending for this invoice", err.Error())
@ -148,7 +148,7 @@ func TestSendPaymentSync_Duplicate_Failed(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
_, err = transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil)
_, err = transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
}
@ -308,7 +308,7 @@ func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) {
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.Error(t, err)
assert.Nil(t, transaction)
@ -338,7 +338,7 @@ func TestSendPaymentSync_PendingHasFeeReserve(t *testing.T) {
svc.LNClient.(*tests.MockLn).PayInvoiceResponses = append(svc.LNClient.(*tests.MockLn).PayInvoiceResponses, nil)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.Error(t, err)
assert.Nil(t, transaction)
@ -365,7 +365,7 @@ func TestConsumeEvent_DoesNotMarkFailedAsSuccessful(t *testing.T) {
svc.LNClient.(*tests.MockLn).PayInvoiceErrors = append(svc.LNClient.(*tests.MockLn).PayInvoiceErrors, errors.New("some error"))
svc.LNClient.(*tests.MockLn).PayInvoiceResponses = append(svc.LNClient.(*tests.MockLn).PayInvoiceResponses, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.Error(t, err)
assert.Nil(t, transaction)

View file

@ -34,7 +34,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToNoApp(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -86,7 +86,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToIsolatedApp(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -139,7 +139,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToApp(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, nil, nil)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -208,7 +208,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToNoApp(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -283,7 +283,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -365,7 +365,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)
@ -457,7 +457,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToSelf(t *testing.T) {
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, nil, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(123000), transaction.AmountMsat)

View file

@ -38,7 +38,7 @@ type TransactionsService interface {
MakeInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error)
ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, transactionType *string, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool) (transactions []Transaction, totalCount uint64, err error)
SendPaymentSync(ctx context.Context, payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
SendPaymentSync(ctx context.Context, payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, timeoutSeconds *int64) (*Transaction, error)
SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
}
@ -188,7 +188,7 @@ func (svc *transactionsService) MakeInvoice(ctx context.Context, amount uint64,
return &dbTransaction, nil
}
func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, timeoutSeconds *int64) (*Transaction, error) {
var metadataBytes []byte
if metadata != nil {
var err error
@ -293,7 +293,7 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
if selfPayment {
response, err = svc.interceptSelfPayment(paymentRequest.PaymentHash)
} else {
response, err = lnClient.SendPaymentSync(ctx, payReq, amountMsat)
response, err = lnClient.SendPaymentSync(ctx, payReq, amountMsat, timeoutSeconds)
}
if err != nil {

View file

@ -976,6 +976,53 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: commandResponse, Error: ""}
case "/api/settings/swaps":
switch method {
case "GET":
autoSwapsConfig, err := app.api.GetAutoSwapsConfig()
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to get auto swaps configuration")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: autoSwapsConfig, Error: ""}
case "POST":
enableAutoSwapsRequest := &api.EnableAutoSwapsRequest{}
err := json.Unmarshal([]byte(body), enableAutoSwapsRequest)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to decode request to wails router")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
err = app.api.EnableAutoSwaps(ctx, enableAutoSwapsRequest)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to enable swaps")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: nil, Error: ""}
case "DELETE":
err := app.api.DisableAutoSwaps()
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to disable swaps")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: nil, Error: ""}
}
}
if strings.HasPrefix(route, "/api/log/") {