mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: bark backend (#2374)
* feat: bark backend * fix: use real preimage * fix: balance shows as 0 on startup after hours offline period * chore: add bark-specific warnings to setup security page * chore: update setup security page * chore: use notifications instead of polling * feat: bark sub-wallet support * fix: tests * feat: add env variables for mainnet support * chore: add custom node commands for debugging, log next notification * fix: notification handling * fix: fees for outgoing payments, pending payment handling, decrease required tx confirmations * chore: bump bark version * chore: use better bark image * fix: back button logic and imported mnemonic state in onboarding flow * chore: update bark copy on security page * chore: improve copy for first transaction checklist item * fix: backup copy based on backend * fix: remove unused/unnecessary code
This commit is contained in:
parent
56b1ce895f
commit
84e56a23db
25 changed files with 1161 additions and 137 deletions
10
README.md
10
README.md
|
|
@ -220,6 +220,7 @@ Can be configured via env or the UI
|
|||
- `CLN_LIGHTNING_DIR`: CLN's lightning directory containing the grpc certificates, usually `~/.lightning/<network>`
|
||||
|
||||
Optional for hold invoice methods support:
|
||||
|
||||
- `CLN_ADDRESS_HOLD`: the CLN hold plugin grpc address (grpc-host and grpc-port), e.g. `127.0.0.1:9738`
|
||||
|
||||
If you are copying the certificates to another machine make sure you get the `ca.pem`, `client.pem` and `client-key.pem` from the lightning directory and optionally from the `hold` directory inside the lightning directory and keep the sub-directory structure of the hold directory.
|
||||
|
|
@ -283,6 +284,15 @@ _To configure via env, the following parameters must be provided:_
|
|||
|
||||
See [Phoenixd](scripts/linux-x86_64/phoenixd/README.md)
|
||||
|
||||
### Bark
|
||||
|
||||
Bark connects to an [Ark](https://second.tech/) server. It can be configured via env.
|
||||
|
||||
- `LN_BACKEND_TYPE`: BARK
|
||||
- `BARK_SERVER`: the Ark server URL. For signet use `https://ark.signet.2nd.dev`
|
||||
- `BARK_ESPLORA_SERVER`: the Esplora server URL used for chain data. For signet use `https://esplora.signet.2nd.dev`.
|
||||
- `BARK_SERVER_ACCESS_TOKEN`: an optional access token required by the Ark server (pre-public mainnet launch).
|
||||
|
||||
### Alby OAuth
|
||||
|
||||
Create an OAuth client at the [Alby Developer Portal](https://getalby.com/developer) and set your `ALBY_OAUTH_CLIENT_ID` and `ALBY_OAUTH_CLIENT_SECRET` in your .env. If not running locally, you'll also need to change your `BASE_URL`.
|
||||
|
|
|
|||
11
api/api.go
11
api/api.go
|
|
@ -1688,6 +1688,17 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error {
|
|||
return errors.New("no unlock password provided")
|
||||
}
|
||||
|
||||
// Bark and Cashu both store wallet state on local disk and have no
|
||||
// remote-backup mechanism, so they cannot run in environments without
|
||||
// persistent volumes (e.g. Alby Cloud). The default OAuth client ID
|
||||
// identifies a local / self-hosted deployment.
|
||||
if !api.cfg.GetEnv().IsDefaultClientId() {
|
||||
switch setupRequest.LNBackendType {
|
||||
case config.BarkBackendType, config.CashuBackendType:
|
||||
return fmt.Errorf("%s backend is not supported in this environment (no persistent storage)", setupRequest.LNBackendType)
|
||||
}
|
||||
}
|
||||
|
||||
err = api.cfg.SaveUnlockPasswordCheck(setupRequest.UnlockPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6
|
|||
backendType, _ := svc.cfg.Get("LNBackendType", "")
|
||||
if backendType != config.LDKBackendType &&
|
||||
backendType != config.LNDBackendType &&
|
||||
backendType != config.PhoenixBackendType {
|
||||
backendType != config.PhoenixBackendType &&
|
||||
backendType != config.BarkBackendType {
|
||||
return nil, "", fmt.Errorf(
|
||||
"sub-wallets are currently not supported on your node backend. Try LDK or LND")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const (
|
|||
PhoenixBackendType = "PHOENIX"
|
||||
CashuBackendType = "CASHU"
|
||||
CLNBackendType = "CLN"
|
||||
BarkBackendType = "BARK"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -63,6 +64,9 @@ type AppConfig struct {
|
|||
CLNAddress string `envconfig:"CLN_ADDRESS"`
|
||||
CLNLightningDir string `envconfig:"CLN_LIGHTNING_DIR"`
|
||||
CLNAddressHold string `envconfig:"CLN_ADDRESS_HOLD"`
|
||||
BarkServer string `envconfig:"BARK_SERVER" default:"https://ark.second.tech"`
|
||||
BarkEsploraServer string `envconfig:"BARK_ESPLORA_SERVER" default:"https://mempool.second.tech/api"`
|
||||
BarkServerAccessToken string `envconfig:"BARK_SERVER_ACCESS_TOKEN"`
|
||||
}
|
||||
|
||||
func (c *AppConfig) IsDefaultClientId() bool {
|
||||
|
|
|
|||
BIN
frontend/src/assets/images/node/bark.jpg
Normal file
BIN
frontend/src/assets/images/node/bark.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
|
|
@ -82,8 +82,7 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
|
|||
: []),
|
||||
{
|
||||
title: "Send or receive your first payment",
|
||||
description:
|
||||
"Use your newly opened channel to make a transaction on the Lightning Network.",
|
||||
description: "Add funds to your wallet, then make your first payment.",
|
||||
checked: hasTransaction,
|
||||
to: "/wallet",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -32,4 +32,9 @@ export const backendTypeConfigs: Record<BackendType, BackendTypeConfig> = {
|
|||
hasChannelManagement: true,
|
||||
hasNodeBackup: false,
|
||||
},
|
||||
BARK: {
|
||||
hasMnemonic: true,
|
||||
hasChannelManagement: false,
|
||||
hasNodeBackup: false,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import { SetupFinish } from "src/screens/setup/SetupFinish";
|
|||
import { SetupNode } from "src/screens/setup/SetupNode";
|
||||
import { SetupPassword } from "src/screens/setup/SetupPassword";
|
||||
import { SetupSecurity } from "src/screens/setup/SetupSecurity";
|
||||
import { BarkForm } from "src/screens/setup/node/BarkForm";
|
||||
import { CLNForm } from "src/screens/setup/node/CLNForm";
|
||||
import { CashuForm } from "src/screens/setup/node/CashuForm";
|
||||
import { LDKForm } from "src/screens/setup/node/LDKForm";
|
||||
|
|
@ -560,6 +561,10 @@ const routes: RouteObject[] = [
|
|||
path: "cln",
|
||||
element: <CLNForm />,
|
||||
},
|
||||
{
|
||||
path: "bark",
|
||||
element: <BarkForm />,
|
||||
},
|
||||
{
|
||||
path: "preset",
|
||||
element: <PresetNodeForm />,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export function ConnectAlbyAccount({ connectUrl }: ConnectAlbyAccountProps) {
|
|||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center gap-2 mt-10">
|
||||
<div className="flex flex-col items-center justify-center gap-2 mt-5">
|
||||
<LinkButton to={connectUrl || "/alby/auth"} size="lg">
|
||||
Connect
|
||||
</LinkButton>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ import { InfoResponse, MnemonicResponse } from "src/types";
|
|||
import { request } from "src/utils/request";
|
||||
|
||||
export default function Backup() {
|
||||
const { data: info, hasMnemonic } = useInfo();
|
||||
const {
|
||||
data: info,
|
||||
hasMnemonic,
|
||||
hasChannelManagement,
|
||||
hasNodeBackup,
|
||||
} = useInfo();
|
||||
const { data: me } = useAlbyMe();
|
||||
const [unlockPassword, setUnlockPassword] = useState("");
|
||||
const [decryptedMnemonic, setDecryptedMnemonic] = useState("");
|
||||
|
|
@ -80,9 +85,11 @@ export default function Backup() {
|
|||
description={
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
Backup your recovery phrase and channel states. These backups are
|
||||
for disaster recovery only. To migrate your node, please use the
|
||||
migration tool.{" "}
|
||||
Backup your recovery phrase
|
||||
{hasChannelManagement && " and channel states"}. These backups are
|
||||
for disaster recovery only.
|
||||
{hasNodeBackup &&
|
||||
" To migrate your node, please use the migration tool."}{" "}
|
||||
</span>
|
||||
<a
|
||||
href="https://guides.getalby.com/user-guide/alby-hub/backups-and-recover"
|
||||
|
|
@ -104,9 +111,10 @@ export default function Backup() {
|
|||
<h3 className="text-lg font-medium">Recovery Phrase</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your recovery phrase is a group of 12 random words that back
|
||||
up your wallet on-chain balance. Using them is the only way to
|
||||
recover access to your wallet on another machine or when you
|
||||
lose your unlock password.
|
||||
up your wallet{" "}
|
||||
{info?.backendType === "LDK" ? "on-chain balance" : "balance"}
|
||||
. Using them is the only way to recover access to your wallet
|
||||
on another machine or when you lose your unlock password.
|
||||
</p>
|
||||
</div>
|
||||
<Alert variant="destructive">
|
||||
|
|
@ -117,6 +125,16 @@ export default function Backup() {
|
|||
phrase, you will lose access to your funds.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{info?.backendType === "BARK" && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle>Bark Support Coming Soon</AlertTitle>
|
||||
<AlertDescription>
|
||||
During the beta period, your recovery phrase is not
|
||||
sufficient to restore your funds.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{info?.backendType === "CASHU" && <CashuMnemonicWarning />}
|
||||
|
||||
<div>
|
||||
|
|
@ -166,113 +184,117 @@ export default function Backup() {
|
|||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Channels Backup</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your lightning balance can only be recovered on-chain by closing
|
||||
your lightning channels. In case of recovery of your Alby Hub a
|
||||
request will be sent to your peers to close your existing
|
||||
channels. To recover the funds from these channels, a channel
|
||||
backup needs to be created every time you open a new channel.
|
||||
</p>
|
||||
</div>
|
||||
{hasChannelManagement && (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Channels Backup</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your lightning balance can only be recovered on-chain by closing
|
||||
your lightning channels. In case of recovery of your Alby Hub a
|
||||
request will be sent to your peers to close your existing
|
||||
channels. To recover the funds from these channels, a channel
|
||||
backup needs to be created every time you open a new channel.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex gap-2 mb-1 items-center">
|
||||
<h3 className="text-sm font-medium">Automatic Channels Backup</h3>
|
||||
<div>
|
||||
<div className="flex gap-2 mb-1 items-center">
|
||||
<h3 className="text-sm font-medium">
|
||||
Automatic Channels Backup
|
||||
</h3>
|
||||
{info?.albyAccountConnected ? (
|
||||
<Badge variant={"positive"}>Active</Badge>
|
||||
) : (
|
||||
<Badge>Recommended</Badge>
|
||||
)}
|
||||
</div>
|
||||
{info?.albyAccountConnected ? (
|
||||
<Badge variant={"positive"}>Active</Badge>
|
||||
<>
|
||||
<p className="text-muted-foreground text-sm mb-8">
|
||||
Your channel state is backed up automatically after each
|
||||
channel creation. Using an external recovery tool and your
|
||||
recovery phrase, you can recover your funds from channels to
|
||||
your on-chain balance as long as your channel partners are
|
||||
online.
|
||||
</p>
|
||||
{info?.vssSupported && (
|
||||
<>
|
||||
<div className="flex gap-2 mb-1 items-center">
|
||||
<h3 className="text-sm font-medium">
|
||||
Dynamic Channels Backup With Instant Recovery
|
||||
</h3>
|
||||
{me?.subscription.plan_code && info.ldkVssEnabled ? (
|
||||
<Badge variant={"positive"}>Active</Badge>
|
||||
) : (
|
||||
<Badge className="shrink-0">Alby Cloud</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
When enabled, your channels state is dynamically updated
|
||||
and stored end-to-end encrypted by Alby's Versioned
|
||||
Storage Service. This allows you to recover your
|
||||
lightning balance with your recovery phrase alone,
|
||||
without having to close your channels.
|
||||
</p>
|
||||
|
||||
{!info.ldkVssEnabled &&
|
||||
(!me?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<Button variant="secondary" size={"lg"}>
|
||||
Upgrade to Enable Dynamic Channels Backup
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<DynamicChannelsBackupDialog info={info} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Badge>Recommended</Badge>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Link your Alby Account to enable automatic channel backups
|
||||
after each channel creation.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
className="flex gap-2 justify-center"
|
||||
onClick={() => navigate("/alby/account")}
|
||||
>
|
||||
<Link2Icon />
|
||||
Link Alby Account to Enable
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2 items-center">
|
||||
<h3 className="text-sm font-medium">
|
||||
Manual Channels Backup
|
||||
</h3>
|
||||
<Badge variant={"positive"}>Active</Badge>
|
||||
</div>
|
||||
<p>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
To backup your channels state manually, without Alby
|
||||
Account linked, follow the
|
||||
</span>{" "}
|
||||
<ExternalLink
|
||||
to="https://guides.getalby.com/user-guide/alby-hub/backups-and-recover#alby-hub-self-hosted-without-an-alby-account"
|
||||
className="underline inline-flex items-center text-sm"
|
||||
>
|
||||
manual backups guide
|
||||
<ExternalLinkIcon className="size-4 ml-1" />
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{info?.albyAccountConnected ? (
|
||||
<>
|
||||
<p className="text-muted-foreground text-sm mb-8">
|
||||
Your channel state is backed up automatically after each
|
||||
channel creation. Using an external recovery tool and your
|
||||
recovery phrase, you can recover your funds from channels to
|
||||
your on-chain balance as long as your channel partners are
|
||||
online.
|
||||
</p>
|
||||
{info?.vssSupported && (
|
||||
<>
|
||||
<div className="flex gap-2 mb-1 items-center">
|
||||
<h3 className="text-sm font-medium">
|
||||
Dynamic Channels Backup With Instant Recovery
|
||||
</h3>
|
||||
{me?.subscription.plan_code && info.ldkVssEnabled ? (
|
||||
<Badge variant={"positive"}>Active</Badge>
|
||||
) : (
|
||||
<Badge className="shrink-0">Alby Cloud</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
When enabled, your channels state is dynamically updated
|
||||
and stored end-to-end encrypted by Alby's Versioned
|
||||
Storage Service. This allows you to recover your lightning
|
||||
balance with your recovery phrase alone, without having to
|
||||
close your channels.
|
||||
</p>
|
||||
|
||||
{!info.ldkVssEnabled &&
|
||||
(!me?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<Button variant="secondary" size={"lg"}>
|
||||
Upgrade to Enable Dynamic Channels Backup
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<DynamicChannelsBackupDialog info={info} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Link your Alby Account to enable automatic channel backups
|
||||
after each channel creation.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
className="flex gap-2 justify-center"
|
||||
onClick={() => navigate("/alby/account")}
|
||||
>
|
||||
<Link2Icon />
|
||||
Link Alby Account to Enable
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2 items-center">
|
||||
<h3 className="text-sm font-medium">
|
||||
Manual Channels Backup
|
||||
</h3>
|
||||
<Badge variant={"positive"}>Active</Badge>
|
||||
</div>
|
||||
<p>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
To backup your channels state manually, without Alby
|
||||
Account linked, follow the
|
||||
</span>{" "}
|
||||
<ExternalLink
|
||||
to="https://guides.getalby.com/user-guide/alby-hub/backups-and-recover#alby-hub-self-hosted-without-an-alby-account"
|
||||
className="underline inline-flex items-center text-sm"
|
||||
>
|
||||
manual backups guide
|
||||
<ExternalLinkIcon className="size-4 ml-1" />
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasMnemonic && !info?.vssSupported && (
|
||||
{!hasMnemonic && !hasChannelManagement && !info?.vssSupported && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No recovery phrase or channel state backup present.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -50,8 +50,9 @@ export function ImportMnemonic() {
|
|||
mnemonic,
|
||||
nextBackupReminder: sixMonthsLater.toISOString(),
|
||||
});
|
||||
setupStore.setHasImportedMnemonic(true);
|
||||
|
||||
navigate(`/setup/security`);
|
||||
navigate(`/setup/node`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { useEffect } from "react";
|
||||
import Container from "src/components/Container";
|
||||
import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader";
|
||||
import { LinkButton } from "src/components/ui/custom/link-button";
|
||||
import useSetupStore from "src/state/SetupStore";
|
||||
|
||||
export function SetupAdvanced() {
|
||||
useEffect(() => {
|
||||
// in case the user goes back, reset the imported mnemonic flag
|
||||
useSetupStore.getState().setHasImportedMnemonic(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="grid gap-5">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Button } from "src/components/ui/button";
|
|||
import { cn } from "src/lib/utils";
|
||||
import { BackendType } from "src/types";
|
||||
|
||||
import bark from "src/assets/images/node/bark.jpg";
|
||||
import cashu from "src/assets/images/node/cashu.png";
|
||||
import cln from "src/assets/images/node/cln.png";
|
||||
import lnd from "src/assets/images/node/lnd.png";
|
||||
|
|
@ -42,6 +43,10 @@ const backendTypeDisplayConfigs: Partial<
|
|||
title: "CLN",
|
||||
icon: <img src={cln} />,
|
||||
},
|
||||
BARK: {
|
||||
title: "Bark",
|
||||
icon: <img src={bark} />,
|
||||
},
|
||||
};
|
||||
|
||||
const backendTypeDisplayConfigList = Object.entries(
|
||||
|
|
@ -64,7 +69,8 @@ export function SetupNode() {
|
|||
navigate(`/setup/node/${selectedBackendType.toLowerCase()}`);
|
||||
}
|
||||
|
||||
const hasImportedMnemonic = !!setupStore.nodeInfo.mnemonic;
|
||||
const hasImportedMnemonic =
|
||||
!!setupStore.nodeInfo.mnemonic && setupStore.hasImportedMnemonic;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import {
|
||||
ClockIcon,
|
||||
HandCoinsIcon,
|
||||
HardDriveIcon,
|
||||
LandmarkIcon,
|
||||
ShieldAlertIcon,
|
||||
UnlockIcon,
|
||||
|
|
@ -36,17 +38,18 @@ export function SetupSecurity() {
|
|||
/>
|
||||
|
||||
<div className="flex flex-col gap-6 w-full mt-6">
|
||||
{store.nodeInfo.backendType !== "CASHU" && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="shrink-0">
|
||||
<HandCoinsIcon className="size-6" />
|
||||
{store.nodeInfo.backendType !== "CASHU" &&
|
||||
store.nodeInfo.backendType !== "BARK" && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="shrink-0">
|
||||
<HandCoinsIcon className="size-6" />
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Alby Hub is a spending wallet - do not keep all your savings
|
||||
on it!
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Alby Hub is a spending wallet - do not keep all your savings on
|
||||
it!
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{store.nodeInfo.backendType === "CASHU" && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="shrink-0">
|
||||
|
|
@ -58,6 +61,31 @@ export function SetupSecurity() {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{store.nodeInfo.backendType === "BARK" && (
|
||||
<>
|
||||
<div className="flex gap-3 items-center">
|
||||
<LandmarkIcon className="size-6 shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Bark is in beta - use at your own risk!
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<ClockIcon className="size-6 shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Your funds will be refreshed periodically which will incur a
|
||||
small fee.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<HardDriveIcon className="size-6 shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
During beta, your funds{" "}
|
||||
<span className="underline">cannot</span> be recovered from
|
||||
your recovery phrase alone.
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="shrink-0">
|
||||
<UnlockIcon className="size-6" />
|
||||
|
|
@ -81,7 +109,7 @@ export function SetupSecurity() {
|
|||
choose the LDK node type.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
) : store.nodeInfo.backendType === "BARK" ? null : (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="shrink-0">
|
||||
<ShieldAlertIcon className="size-6" />
|
||||
|
|
|
|||
33
frontend/src/screens/setup/node/BarkForm.tsx
Normal file
33
frontend/src/screens/setup/node/BarkForm.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import useSetupStore from "src/state/SetupStore";
|
||||
|
||||
import * as bip39 from "@scure/bip39";
|
||||
import Loading from "src/components/Loading";
|
||||
|
||||
export function BarkForm() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (!useSetupStore.getState().nodeInfo.mnemonic) {
|
||||
useSetupStore.getState().updateNodeInfo({
|
||||
mnemonic: bip39.generateMnemonic(wordlist, 128),
|
||||
});
|
||||
}
|
||||
useSetupStore.getState().updateNodeInfo({
|
||||
backendType: "BARK",
|
||||
});
|
||||
navigate("/setup/security", {
|
||||
replace: true,
|
||||
});
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>Loading... · Alby Hub</title>
|
||||
<Loading />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,7 +21,9 @@ export function LDKForm() {
|
|||
useSetupStore.getState().updateNodeInfo({
|
||||
backendType: "LDK",
|
||||
});
|
||||
navigate("/setup/security");
|
||||
navigate("/setup/security", {
|
||||
replace: true,
|
||||
});
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ export function PresetNodeForm() {
|
|||
});
|
||||
}
|
||||
|
||||
navigate("/setup/security");
|
||||
navigate("/setup/security", {
|
||||
replace: true,
|
||||
});
|
||||
}, [info, navigate, searchParams]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@ import { create } from "zustand";
|
|||
interface SetupStore {
|
||||
readonly nodeInfo: SetupNodeInfo;
|
||||
readonly unlockPassword: string;
|
||||
readonly hasImportedMnemonic: boolean;
|
||||
updateNodeInfo(nodeInfo: SetupNodeInfo): void;
|
||||
setUnlockPassword(unlockPassword: string): void;
|
||||
setHasImportedMnemonic(hasImportedMnemonic: boolean): void;
|
||||
}
|
||||
|
||||
const useSetupStore = create<SetupStore>((set) => ({
|
||||
nodeInfo: {},
|
||||
unlockPassword: "",
|
||||
hasImportedMnemonic: false,
|
||||
updateNodeInfo: (nodeInfo) =>
|
||||
set((state) => ({
|
||||
nodeInfo: { ...state.nodeInfo, ...nodeInfo },
|
||||
})),
|
||||
setUnlockPassword: (unlockPassword) => set({ unlockPassword }),
|
||||
setHasImportedMnemonic: (hasImportedMnemonic) => set({ hasImportedMnemonic }),
|
||||
}));
|
||||
|
||||
export default useSetupStore;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
WalletMinimalIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export type BackendType = "LND" | "LDK" | "PHOENIX" | "CASHU" | "CLN";
|
||||
export type BackendType = "LND" | "LDK" | "PHOENIX" | "CASHU" | "CLN" | "BARK";
|
||||
|
||||
export type Nip47RequestMethod =
|
||||
| "get_info"
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -19,6 +19,7 @@ require (
|
|||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tyler-smith/go-bip39 v1.1.0
|
||||
github.com/wailsapp/wails/v2 v2.12.0
|
||||
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.6.2
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
google.golang.org/grpc v1.79.3
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -689,6 +689,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
|||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.6.2 h1:UwqTPL4dmG4N+RDJaci6D2MhiWvBbBsCss4nDbYt8TM=
|
||||
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.6.2/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0=
|
||||
|
|
|
|||
864
lnclient/bark/bark.go
Normal file
864
lnclient/bark/bark.go
Normal file
|
|
@ -0,0 +1,864 @@
|
|||
package bark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||
"github.com/sirupsen/logrus"
|
||||
bark "gitlab.com/ark-bitcoin/bark-ffi-bindings/golang/bark"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/events"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/notifications"
|
||||
)
|
||||
|
||||
const (
|
||||
// Subsystem name reported on movements produced when a lightning receive is
|
||||
// claimed (see bark's Subsystem::LIGHTNING_RECEIVE).
|
||||
lightningReceiveSubsystem = "lightning_receive"
|
||||
// Subsystem name reported on movements produced for outgoing lightning
|
||||
// payments (see bark's Subsystem::LIGHTNING_SEND).
|
||||
lightningSendSubsystem = "lightning_send"
|
||||
// Movement status reported once a movement has settled. A movement first
|
||||
// appears as "pending" and is updated to this once complete.
|
||||
movementStatusSuccessful = "successful"
|
||||
// Movement status reported when a send was definitively not paid.
|
||||
movementStatusFailed = "failed"
|
||||
// Grace period to allow the notification loop to unwind on shutdown.
|
||||
shutdownGracePeriod = 10 * time.Second
|
||||
)
|
||||
|
||||
// Config holds the user-configurable settings for connecting to an Ark server.
|
||||
type Config struct {
|
||||
// Network is the bitcoin network name (e.g. "signet", "bitcoin").
|
||||
Network string
|
||||
// ServerAddress is the Ark server URL.
|
||||
ServerAddress string
|
||||
// EsploraAddress is the Esplora server URL used for chain data.
|
||||
EsploraAddress string
|
||||
// ServerAccessToken is an optional access token required by some Ark
|
||||
// servers (currently used to gate mainnet access ahead of a public launch).
|
||||
ServerAccessToken string
|
||||
}
|
||||
|
||||
type BarkService struct {
|
||||
wallet *bark.Wallet
|
||||
workDir string
|
||||
network string
|
||||
eventPublisher events.EventPublisher
|
||||
pubkey string
|
||||
cancelFn context.CancelFunc
|
||||
loopWg sync.WaitGroup
|
||||
// payment_hash -> waiter that handleLightningSendMovement signals.
|
||||
inflightSends map[string]chan sendResult
|
||||
inflightSendsMtx sync.Mutex
|
||||
}
|
||||
|
||||
type sendResult struct {
|
||||
preimage string
|
||||
feeMsat uint64
|
||||
err error
|
||||
}
|
||||
|
||||
// parseNetwork maps an Alby Hub network name onto a bark network.
|
||||
func parseNetwork(network string) (bark.Network, error) {
|
||||
switch network {
|
||||
case "bitcoin", "mainnet":
|
||||
return bark.NetworkBitcoin, nil
|
||||
case "testnet":
|
||||
return bark.NetworkTestnet, nil
|
||||
case "signet":
|
||||
return bark.NetworkSignet, nil
|
||||
case "regtest":
|
||||
return bark.NetworkRegtest, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported bark network: %q", network)
|
||||
}
|
||||
}
|
||||
|
||||
func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, workDir, mnemonic string, config Config) (lnclient.LNClient, error) {
|
||||
if mnemonic == "" {
|
||||
return nil, errors.New("no mnemonic configured")
|
||||
}
|
||||
if workDir == "" {
|
||||
return nil, errors.New("no bark work directory configured")
|
||||
}
|
||||
if config.ServerAddress == "" {
|
||||
return nil, errors.New("no bark server address configured")
|
||||
}
|
||||
|
||||
network, err := parseNetwork(config.Network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Usually, you have two wait 2 blocks. You can set nb_min_round_confirmations=0 to make it go faster.
|
||||
roundTxRequiredConfirmations := uint32(0)
|
||||
|
||||
cfg := bark.Config{
|
||||
ServerAddress: config.ServerAddress,
|
||||
Network: network,
|
||||
RoundTxRequiredConfirmations: &roundTxRequiredConfirmations,
|
||||
}
|
||||
esploraAddress := config.EsploraAddress
|
||||
if esploraAddress != "" {
|
||||
cfg.EsploraAddress = &esploraAddress
|
||||
}
|
||||
if config.ServerAccessToken != "" {
|
||||
token := config.ServerAccessToken
|
||||
cfg.ServerAccessToken = &token
|
||||
}
|
||||
|
||||
_, statErr := os.Stat(workDir)
|
||||
isFirstSetup := statErr != nil && errors.Is(statErr, os.ErrNotExist)
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"workDir": workDir,
|
||||
"isFirstSetup": isFirstSetup,
|
||||
}).Info("Opening Bark wallet")
|
||||
|
||||
var wallet *bark.Wallet
|
||||
if isFirstSetup {
|
||||
wallet, err = bark.WalletCreate(mnemonic, cfg, workDir, false)
|
||||
} else {
|
||||
wallet, err = bark.WalletOpen(mnemonic, cfg, workDir)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open bark wallet: %w", err)
|
||||
}
|
||||
|
||||
// Bark provides a built-in background daemon that periodically syncs with
|
||||
// the Ark server and blockchain, participates in rounds, and — crucially for
|
||||
// us — claims incoming lightning receives via the mailbox (it long-polls for
|
||||
// payment notifications and reveals the preimage, crediting the balance). We
|
||||
// don't poll for receives ourselves; instead we observe the resulting wallet
|
||||
// notifications (see runNotificationLoop) to emit payment-received events.
|
||||
if err := wallet.RunDaemon(nil); err != nil {
|
||||
logger.Logger.WithError(err).Warn("Bark daemon failed to start")
|
||||
}
|
||||
|
||||
loopCtx, cancelFn := context.WithCancel(context.Background())
|
||||
bs := &BarkService{
|
||||
wallet: wallet,
|
||||
workDir: workDir,
|
||||
network: config.Network,
|
||||
eventPublisher: eventPublisher,
|
||||
pubkey: wallet.Fingerprint(),
|
||||
cancelFn: cancelFn,
|
||||
inflightSends: make(map[string]chan sendResult),
|
||||
}
|
||||
|
||||
// Run maintenance immediately on startup so a wallet that was briefly
|
||||
// offline refreshes any VTXOs that drifted towards expiry before they are
|
||||
// swept by the server. This is fire-and-forget as it may join an Ark round
|
||||
// and take some time.
|
||||
go func() {
|
||||
if err := bs.wallet.Maintenance(); err != nil {
|
||||
logger.Logger.WithError(err).Warn("Bark startup maintenance failed")
|
||||
}
|
||||
}()
|
||||
|
||||
bs.loopWg.Add(1)
|
||||
go bs.runNotificationLoop(loopCtx)
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// runNotificationLoop consumes the wallet's notification stream and publishes a
|
||||
// payment-received event whenever the daemon claims an incoming lightning
|
||||
// receive. The daemon does the actual claiming (it long-polls the mailbox and
|
||||
// reveals the preimage); claiming a receive produces a lightning-receive
|
||||
// movement, which surfaces here as a MovementCreated notification. This is
|
||||
// event-driven — NextNotification blocks until something happens — so we no
|
||||
// longer poll every few seconds.
|
||||
func (bs *BarkService) runNotificationLoop(ctx context.Context) {
|
||||
defer bs.loopWg.Done()
|
||||
|
||||
notifications := bs.wallet.Notifications()
|
||||
defer notifications.Destroy()
|
||||
|
||||
// NextNotification blocks; CancelNextNotificationWait unblocks it (returning
|
||||
// nil) so the loop can exit promptly on shutdown.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
notifications.CancelNextNotificationWait()
|
||||
}()
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
notif, err := notifications.NextNotification()
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Debug("Bark NextNotification failed")
|
||||
// Back off briefly so a persistent error doesn't spin the loop.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
continue
|
||||
}
|
||||
if notif == nil {
|
||||
// nil is returned when the wait was cancelled (shutdown) or the
|
||||
// notification source was shut down permanently.
|
||||
return
|
||||
}
|
||||
bs.handleNotification(*notif)
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BarkService) handleNotification(notif bark.WalletNotification) {
|
||||
logger.Logger.WithFields(notificationLogFields(notif)).Debug("Received Bark notification")
|
||||
|
||||
var movement bark.Movement
|
||||
switch n := notif.(type) {
|
||||
case bark.WalletNotificationMovementCreated:
|
||||
movement = n.Movement
|
||||
case bark.WalletNotificationMovementUpdated:
|
||||
movement = n.Movement
|
||||
default:
|
||||
// Channel lagging and other kinds carry no movement to act on.
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(movement.SubsystemName, lightningReceiveSubsystem):
|
||||
bs.handleLightningReceiveMovement(movement)
|
||||
case strings.Contains(movement.SubsystemName, lightningSendSubsystem):
|
||||
bs.handleLightningSendMovement(movement)
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) {
|
||||
// A receive is only credited once its movement settles. We always hold the
|
||||
// preimage for our own receives, so PreimageRevealed isn't a useful signal;
|
||||
// the balance is credited when the movement status reaches "successful".
|
||||
if movement.Status != movementStatusSuccessful {
|
||||
return
|
||||
}
|
||||
|
||||
paymentHash, ok := paymentHashFromMovement(movement)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
receive, err := bs.wallet.LightningReceiveStatus(paymentHash)
|
||||
if err != nil || receive == nil {
|
||||
logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Warn("Failed to look up claimed Bark receive")
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := bs.lightningReceiveToTransaction(receive)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithField("paymentHash", receive.PaymentHash).Warn("Failed to convert claimed Bark receive to transaction")
|
||||
return
|
||||
}
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"paymentHash": receive.PaymentHash,
|
||||
"amountSats": receive.AmountSats,
|
||||
}).Info("Bark lightning receive claimed")
|
||||
bs.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: tx,
|
||||
})
|
||||
}
|
||||
|
||||
// handleLightningSendMovement delivers a terminal lightning_send outcome to
|
||||
// the SendPaymentSync waiter for the matching payment_hash. If no waiter is
|
||||
// registered (e.g. the hub was restarted mid-send and SendPaymentSync's
|
||||
// goroutine is gone) it falls back to publishing nwc_lnclient_payment_sent /
|
||||
// _failed so the transactions service can recover the db transaction state.
|
||||
func (bs *BarkService) handleLightningSendMovement(movement bark.Movement) {
|
||||
if movement.Status != movementStatusSuccessful && movement.Status != movementStatusFailed {
|
||||
return
|
||||
}
|
||||
|
||||
var meta struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
PaymentPreimage string `json:"payment_preimage"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(movement.MetadataJson), &meta); err != nil || meta.PaymentHash == "" {
|
||||
logger.Logger.WithError(err).WithField("movementId", movement.Id).Debug("Bark lightning send movement missing payment_hash")
|
||||
return
|
||||
}
|
||||
|
||||
if movement.Status == movementStatusFailed {
|
||||
bs.deliverSendResult(meta.PaymentHash, sendResult{err: errors.New("bark lightning send failed")}, func() {
|
||||
bs.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_lnclient_payment_failed",
|
||||
Properties: &lnclient.PaymentFailedEventProperties{
|
||||
Transaction: &lnclient.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: meta.PaymentHash,
|
||||
},
|
||||
Reason: "bark lightning send failed",
|
||||
},
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if meta.PaymentPreimage == "" {
|
||||
logger.Logger.WithField("paymentHash", meta.PaymentHash).Error("Bark lightning send reported successful but preimage is missing from movement metadata")
|
||||
bs.deliverSendResult(meta.PaymentHash, sendResult{err: errors.New("bark lightning send completed without a preimage")}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
feeMsat := movement.OffchainFeeSats * 1000
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"paymentHash": meta.PaymentHash,
|
||||
"feeMsat": feeMsat,
|
||||
}).Info("Bark lightning send completed")
|
||||
|
||||
bs.deliverSendResult(meta.PaymentHash, sendResult{preimage: meta.PaymentPreimage, feeMsat: feeMsat}, func() {
|
||||
settledAt := time.Now().Unix()
|
||||
bs.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_lnclient_payment_sent",
|
||||
Properties: &lnclient.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: meta.PaymentHash,
|
||||
Preimage: meta.PaymentPreimage,
|
||||
FeesPaidMsat: int64(feeMsat),
|
||||
SettledAt: &settledAt,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// deliverSendResult delivers to the SendPaymentSync waiter if present, else
|
||||
// runs fallback (used to publish an event for the hub-restart recovery path).
|
||||
func (bs *BarkService) deliverSendResult(paymentHash string, res sendResult, fallback func()) {
|
||||
if ch, ok := bs.takeInflightSend(paymentHash); ok {
|
||||
ch <- res
|
||||
return
|
||||
}
|
||||
if fallback != nil {
|
||||
fallback()
|
||||
}
|
||||
}
|
||||
|
||||
func paymentHashFromMovement(movement bark.Movement) (string, bool) {
|
||||
var meta struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(movement.MetadataJson), &meta); err != nil || meta.PaymentHash == "" {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"movementId": movement.Id,
|
||||
"subsystemName": movement.SubsystemName,
|
||||
}).Debug("Bark lightning movement missing payment_hash")
|
||||
return "", false
|
||||
}
|
||||
return meta.PaymentHash, true
|
||||
}
|
||||
|
||||
// notificationLogFields turns a Bark wallet notification into structured log
|
||||
// fields describing its concrete type, rather than logging the raw interface
|
||||
// pointer (which would just print an address).
|
||||
func notificationLogFields(notif bark.WalletNotification) logrus.Fields {
|
||||
switch n := notif.(type) {
|
||||
case bark.WalletNotificationMovementCreated:
|
||||
return movementLogFields("movement_created", n.Movement)
|
||||
case bark.WalletNotificationMovementUpdated:
|
||||
return movementLogFields("movement_updated", n.Movement)
|
||||
case bark.WalletNotificationChannelLagging:
|
||||
return logrus.Fields{"kind": "channel_lagging"}
|
||||
default:
|
||||
return logrus.Fields{"kind": fmt.Sprintf("%T", notif)}
|
||||
}
|
||||
}
|
||||
|
||||
func movementLogFields(kind string, m bark.Movement) logrus.Fields {
|
||||
return logrus.Fields{
|
||||
"kind": kind,
|
||||
"movementId": m.Id,
|
||||
"status": m.Status,
|
||||
"subsystemName": m.SubsystemName,
|
||||
"subsystemKind": m.SubsystemKind,
|
||||
"metadataJson": m.MetadataJson,
|
||||
"intendedBalanceSats": m.IntendedBalanceSats,
|
||||
"effectiveBalanceSats": m.EffectiveBalanceSats,
|
||||
"offchainFeeSats": m.OffchainFeeSats,
|
||||
"sentToAddresses": m.SentToAddresses,
|
||||
"receivedOnAddresses": m.ReceivedOnAddresses,
|
||||
"inputVtxoIds": m.InputVtxoIds,
|
||||
"outputVtxoIds": m.OutputVtxoIds,
|
||||
"exitedVtxoIds": m.ExitedVtxoIds,
|
||||
"createdAt": m.CreatedAt,
|
||||
"updatedAt": m.UpdatedAt,
|
||||
"completedAt": m.CompletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BarkService) MakeInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (*lnclient.Transaction, error) {
|
||||
if amountMsat <= 0 {
|
||||
return nil, errors.New("0-amount invoices not supported")
|
||||
}
|
||||
if amountMsat%1000 != 0 {
|
||||
return nil, errors.New("amount must be a whole number of sats")
|
||||
}
|
||||
|
||||
var desc *string
|
||||
if description != "" {
|
||||
desc = &description
|
||||
}
|
||||
|
||||
invoice, err := bs.wallet.Bolt11Invoice(uint64(amountMsat/1000), desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bark Bolt11Invoice failed: %w", err)
|
||||
}
|
||||
|
||||
paymentRequest, err := decodepay.Decodepay(invoice.Invoice)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithField("bolt11", invoice.Invoice).Error("Failed to decode bark-generated bolt11 invoice")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
|
||||
|
||||
// The preimage is generated alongside the invoice but is not returned by
|
||||
// Bolt11Invoice. Fetch it via the receive status so consumers can rely on
|
||||
// lookup_invoice exposing the real preimage.
|
||||
var preimage string
|
||||
receive, err := bs.wallet.LightningReceiveStatus(paymentRequest.PaymentHash)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithField("paymentHash", paymentRequest.PaymentHash).Error("Failed to fetch bark receive status for preimage")
|
||||
return nil, err
|
||||
}
|
||||
preimage = receive.PaymentPreimage
|
||||
if preimage == "" {
|
||||
return nil, errors.New("no preimage available")
|
||||
}
|
||||
|
||||
return &lnclient.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
Invoice: invoice.Invoice,
|
||||
Preimage: preimage,
|
||||
PaymentHash: paymentRequest.PaymentHash,
|
||||
AmountMsat: amountMsat,
|
||||
CreatedAt: int64(paymentRequest.CreatedAt),
|
||||
ExpiresAt: &expiresAtUnix,
|
||||
Description: paymentRequest.Description,
|
||||
DescriptionHash: paymentRequest.DescriptionHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) SendPaymentSync(invoice string, amountMsat *uint64) (*lnclient.PayInvoiceResponse, error) {
|
||||
// 0-amount invoices not supported initially — keeps the surface minimal.
|
||||
if amountMsat != nil {
|
||||
return nil, errors.New("0-amount invoices not supported")
|
||||
}
|
||||
|
||||
paymentRequest, decodeErr := decodepay.Decodepay(invoice)
|
||||
if decodeErr != nil {
|
||||
return nil, fmt.Errorf("failed to decode invoice: %w", decodeErr)
|
||||
}
|
||||
paymentHash := paymentRequest.PaymentHash
|
||||
|
||||
// Register a waiter BEFORE initiating the send so a notification that
|
||||
// arrives before this goroutine reaches the receive cannot be missed.
|
||||
resultCh := make(chan sendResult, 1)
|
||||
if err := bs.registerInflightSend(paymentHash, resultCh); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer bs.clearInflightSend(paymentHash)
|
||||
|
||||
if _, err := bs.wallet.PayLightningInvoice(invoice, nil); err != nil {
|
||||
return nil, fmt.Errorf("bark PayLightningInvoice failed: %w", err)
|
||||
}
|
||||
|
||||
// Block until handleLightningSendMovement delivers a terminal result.
|
||||
res := <-resultCh
|
||||
if res.err != nil {
|
||||
return nil, res.err
|
||||
}
|
||||
return &lnclient.PayInvoiceResponse{
|
||||
Preimage: res.preimage,
|
||||
FeeMsat: res.feeMsat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) registerInflightSend(paymentHash string, ch chan sendResult) error {
|
||||
bs.inflightSendsMtx.Lock()
|
||||
defer bs.inflightSendsMtx.Unlock()
|
||||
if _, exists := bs.inflightSends[paymentHash]; exists {
|
||||
return fmt.Errorf("a bark lightning send is already in flight for payment hash %s", paymentHash)
|
||||
}
|
||||
bs.inflightSends[paymentHash] = ch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) clearInflightSend(paymentHash string) {
|
||||
bs.inflightSendsMtx.Lock()
|
||||
defer bs.inflightSendsMtx.Unlock()
|
||||
delete(bs.inflightSends, paymentHash)
|
||||
}
|
||||
|
||||
func (bs *BarkService) takeInflightSend(paymentHash string) (chan sendResult, bool) {
|
||||
bs.inflightSendsMtx.Lock()
|
||||
defer bs.inflightSendsMtx.Unlock()
|
||||
ch, ok := bs.inflightSends[paymentHash]
|
||||
if ok {
|
||||
delete(bs.inflightSends, paymentHash)
|
||||
}
|
||||
return ch, ok
|
||||
}
|
||||
|
||||
func (bs *BarkService) LookupInvoice(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) {
|
||||
return nil, errors.New("this method should not be called")
|
||||
}
|
||||
|
||||
func (bs *BarkService) lightningReceiveToTransaction(receive *bark.LightningReceive) (*lnclient.Transaction, error) {
|
||||
paymentRequest, err := decodepay.Decodepay(receive.Invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
|
||||
|
||||
tx := &lnclient.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
Invoice: receive.Invoice,
|
||||
PaymentHash: receive.PaymentHash,
|
||||
AmountMsat: paymentRequest.MSatoshi,
|
||||
CreatedAt: int64(paymentRequest.CreatedAt),
|
||||
ExpiresAt: &expiresAtUnix,
|
||||
Description: paymentRequest.Description,
|
||||
DescriptionHash: paymentRequest.DescriptionHash,
|
||||
}
|
||||
if receive.PreimageRevealed {
|
||||
now := time.Now().Unix()
|
||||
tx.SettledAt = &now
|
||||
tx.Preimage = receive.PaymentPreimage
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetBalances(ctx context.Context, includeInactiveChannels bool) (*lnclient.BalancesResponse, error) {
|
||||
balance, err := bs.wallet.Balance()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spendableMsat := int64(balance.SpendableSats) * 1000
|
||||
|
||||
return &lnclient.BalancesResponse{
|
||||
Onchain: lnclient.OnchainBalanceResponse{
|
||||
PendingBalancesDetails: []lnclient.PendingBalanceDetails{},
|
||||
PendingSweepBalancesDetails: []lnclient.PendingBalanceDetails{},
|
||||
},
|
||||
Lightning: lnclient.LightningBalanceResponse{
|
||||
TotalSpendableMsat: spendableMsat,
|
||||
NextMaxSpendableMsat: spendableMsat,
|
||||
NextMaxSpendableMPPMsat: spendableMsat,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetInfo(ctx context.Context) (*lnclient.NodeInfo, error) {
|
||||
return &lnclient.NodeInfo{
|
||||
Alias: "Bark",
|
||||
Color: "#897FFF",
|
||||
Pubkey: bs.pubkey,
|
||||
Network: bs.network,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) {
|
||||
return &lnclient.NodeStatus{
|
||||
IsReady: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) {
|
||||
return &lnclient.NodeConnectionInfo{
|
||||
Pubkey: bs.pubkey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetPubkey() string {
|
||||
return bs.pubkey
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetSupportedNIP47Methods() []string {
|
||||
return []string{"pay_invoice", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetSupportedNIP47NotificationTypes() []string {
|
||||
// payment_received is emitted from runNotificationLoop when the daemon
|
||||
// claims an incoming receive; payment_sent is emitted by the transactions
|
||||
// service when our synchronous SendPaymentSync succeeds.
|
||||
return []string{
|
||||
notifications.PAYMENT_RECEIVED_NOTIFICATION,
|
||||
notifications.PAYMENT_SENT_NOTIFICATION,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BarkService) Shutdown() error {
|
||||
if bs.cancelFn != nil {
|
||||
bs.cancelFn()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
bs.loopWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(shutdownGracePeriod):
|
||||
logger.Logger.Warn("Timed out waiting for Bark background loops to stop")
|
||||
}
|
||||
}
|
||||
if err := bs.wallet.StopDaemon(); err != nil {
|
||||
logger.Logger.WithError(err).Warn("Bark StopDaemon failed")
|
||||
}
|
||||
bs.wallet.Destroy()
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- unsupported / stubbed methods ---
|
||||
|
||||
func (bs *BarkService) SendKeysend(amountMsat uint64, destination string, customRecords []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
|
||||
return nil, errors.New("keysend not supported")
|
||||
}
|
||||
|
||||
func (bs *BarkService) MakeHoldInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, paymentHash string, minCltvExpiryDelta *uint64) (*lnclient.Transaction, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (bs *BarkService) SettleHoldInvoice(ctx context.Context, preimage string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (bs *BarkService) CancelHoldInvoice(ctx context.Context, paymentHash string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (bs *BarkService) ListChannels(ctx context.Context) ([]lnclient.Channel, error) {
|
||||
return []lnclient.Channel{}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) UpdateChannel(ctx context.Context, updateChannelRequest *lnclient.UpdateChannelRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) DisconnectPeer(ctx context.Context, peerId string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetNewOnchainAddress(ctx context.Context) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) {
|
||||
return &lnclient.OnchainBalanceResponse{}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) RedeemOnchainFunds(ctx context.Context, toAddress string, amountSat uint64, feeRate *uint64, sendAll bool) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (bs *BarkService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetNetworkGraph(ctx context.Context, nodeIds []string) (lnclient.NetworkGraphResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) SignMessage(ctx context.Context, message string) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (bs *BarkService) GetStorageDir() (string, error) {
|
||||
return bs.workDir, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) ResetRouter(key string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (bs *BarkService) UpdateLastWalletSyncRequest() {}
|
||||
|
||||
func (bs *BarkService) MakeOffer(ctx context.Context, description string) (string, error) {
|
||||
return "", errors.New("not supported")
|
||||
}
|
||||
|
||||
func (bs *BarkService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
|
||||
const (
|
||||
nodeCommandDebug = "debug"
|
||||
nodeCommandClaimLightningReceives = "claimlightningreceives"
|
||||
nodeCommandRunMaintenance = "runmaintenance"
|
||||
)
|
||||
|
||||
func (bs *BarkService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return []lnclient.CustomNodeCommandDef{
|
||||
{
|
||||
Name: nodeCommandDebug,
|
||||
Description: "Dump the wallet's balance breakdown, VTXOs, pending lightning receives, movement history and Ark server info. Useful for debugging a receive that did not credit your balance.",
|
||||
Args: nil,
|
||||
},
|
||||
{
|
||||
Name: nodeCommandClaimLightningReceives,
|
||||
Description: "Attempt to claim any pending/unclaimed lightning receives. Use this if an invoice was paid but the funds have not shown up in your balance.",
|
||||
Args: nil,
|
||||
},
|
||||
{
|
||||
Name: nodeCommandRunMaintenance,
|
||||
Description: "Run wallet maintenance, which progresses pending rounds and refreshes VTXOs. Use this to nudge funds that are stuck 'pending in round'.",
|
||||
Args: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BarkService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
switch command.Name {
|
||||
case nodeCommandDebug:
|
||||
return bs.executeCommandDebug()
|
||||
case nodeCommandClaimLightningReceives:
|
||||
return bs.executeCommandClaimLightningReceives()
|
||||
case nodeCommandRunMaintenance:
|
||||
return bs.executeCommandRunMaintenance()
|
||||
}
|
||||
|
||||
return nil, lnclient.ErrUnknownCustomNodeCommand
|
||||
}
|
||||
|
||||
func (bs *BarkService) executeCommandDebug() (*lnclient.CustomNodeCommandResponse, error) {
|
||||
// Sync first so we report current state rather than a stale snapshot (the
|
||||
// same pattern GetBalances uses before reading the balance).
|
||||
if err := bs.wallet.Sync(); err != nil {
|
||||
logger.Logger.WithError(err).Warn("Bark sync failed before collecting debug info")
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"network": bs.network,
|
||||
"pubkey": bs.pubkey,
|
||||
}
|
||||
|
||||
if balance, err := bs.wallet.Balance(); err != nil {
|
||||
response["balanceError"] = err.Error()
|
||||
} else {
|
||||
response["balance"] = balance
|
||||
}
|
||||
|
||||
if claimable, err := bs.wallet.ClaimableLightningReceiveBalanceSats(); err != nil {
|
||||
response["claimableLightningReceiveSatsError"] = err.Error()
|
||||
} else {
|
||||
response["claimableLightningReceiveSats"] = claimable
|
||||
}
|
||||
|
||||
if vtxos, err := bs.wallet.Vtxos(); err != nil {
|
||||
response["vtxosError"] = err.Error()
|
||||
} else {
|
||||
response["vtxos"] = vtxos
|
||||
}
|
||||
|
||||
if spendable, err := bs.wallet.SpendableVtxos(); err != nil {
|
||||
response["spendableVtxosError"] = err.Error()
|
||||
} else {
|
||||
response["spendableVtxos"] = spendable
|
||||
}
|
||||
|
||||
if pending, err := bs.wallet.PendingLightningReceives(); err != nil {
|
||||
response["pendingLightningReceivesError"] = err.Error()
|
||||
} else {
|
||||
response["pendingLightningReceives"] = pending
|
||||
}
|
||||
|
||||
if history, err := bs.wallet.History(); err != nil {
|
||||
response["historyError"] = err.Error()
|
||||
} else {
|
||||
response["history"] = history
|
||||
}
|
||||
|
||||
// Round state explains funds stuck in PendingInRoundSats: such funds sit in a
|
||||
// round whose funding tx is waiting for confirmations (6 on mainnet), which
|
||||
// the daemon progresses automatically once confirmed.
|
||||
if rounds, err := bs.wallet.PendingRoundStates(); err != nil {
|
||||
response["pendingRoundStatesError"] = err.Error()
|
||||
} else {
|
||||
response["pendingRoundStates"] = rounds
|
||||
}
|
||||
|
||||
if nextRoundStartTime, err := bs.wallet.NextRoundStartTime(); err != nil {
|
||||
response["nextRoundStartTimeError"] = err.Error()
|
||||
} else {
|
||||
response["nextRoundStartTime"] = nextRoundStartTime
|
||||
}
|
||||
|
||||
if arkInfo := bs.wallet.ArkInfo(); arkInfo != nil {
|
||||
response["arkInfo"] = arkInfo
|
||||
}
|
||||
|
||||
return &lnclient.CustomNodeCommandResponse{
|
||||
Response: response,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) executeCommandRunMaintenance() (*lnclient.CustomNodeCommandResponse, error) {
|
||||
if err := bs.wallet.Maintenance(); err != nil {
|
||||
return nil, fmt.Errorf("failed to run maintenance: %w", err)
|
||||
}
|
||||
|
||||
logger.Logger.Debug("Ran Bark maintenance")
|
||||
|
||||
balance, err := bs.wallet.Balance()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("maintenance succeeded but failed to read balance: %w", err)
|
||||
}
|
||||
|
||||
return &lnclient.CustomNodeCommandResponse{
|
||||
Response: map[string]interface{}{
|
||||
"message": "Maintenance completed.",
|
||||
"balance": balance,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bs *BarkService) executeCommandClaimLightningReceives() (*lnclient.CustomNodeCommandResponse, error) {
|
||||
if err := bs.wallet.Sync(); err != nil {
|
||||
logger.Logger.WithError(err).Warn("Bark sync failed before claiming lightning receives")
|
||||
}
|
||||
|
||||
// wait=false: attempt to claim what is already claimable without blocking on
|
||||
// the server long-polling for not-yet-arrived payments.
|
||||
claimed, err := bs.wallet.TryClaimAllLightningReceives(false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to claim lightning receives: %w", err)
|
||||
}
|
||||
|
||||
logger.Logger.WithField("count", len(claimed)).Info("Attempted to claim Bark lightning receives")
|
||||
|
||||
return &lnclient.CustomNodeCommandResponse{
|
||||
Response: map[string]interface{}{
|
||||
"claimedCount": len(claimed),
|
||||
"claimed": claimed,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/getAlby/hub/config"
|
||||
"github.com/getAlby/hub/events"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/lnclient/bark"
|
||||
"github.com/getAlby/hub/lnclient/cashu"
|
||||
"github.com/getAlby/hub/lnclient/cln"
|
||||
"github.com/getAlby/hub/lnclient/ldk"
|
||||
|
|
@ -373,6 +374,17 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e
|
|||
cashuWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "cashu")
|
||||
|
||||
lnClient, err = cashu.NewCashuService(svc.cfg, cashuWorkdir, mnemonic, cashuMintUrl)
|
||||
case config.BarkBackendType:
|
||||
mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey)
|
||||
env := svc.cfg.GetEnv()
|
||||
barkWorkdir := path.Join(env.Workdir, "bark")
|
||||
|
||||
lnClient, err = bark.NewBarkService(ctx, svc.eventPublisher, barkWorkdir, mnemonic, bark.Config{
|
||||
Network: svc.cfg.GetNetwork(),
|
||||
ServerAddress: env.BarkServer,
|
||||
EsploraAddress: env.BarkEsploraServer,
|
||||
ServerAccessToken: env.BarkServerAccessToken,
|
||||
})
|
||||
case config.CLNBackendType:
|
||||
CLNAddress, _ := svc.cfg.Get("CLNAddress", encryptionKey)
|
||||
CLNLightningDir, _ := svc.cfg.Get("CLNLightningDir", encryptionKey)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ func TestSendPaymentSync_SelfPaymentDetection_WithoutIncomingTransaction(t *test
|
|||
assert.Equal(t, uint64(123000), transaction.AmountMsat)
|
||||
}
|
||||
|
||||
// Self-payment detection is based solely on having an incoming transaction for
|
||||
// the same invoice, not on the invoice payee matching our node pubkey. A
|
||||
// mismatching pubkey must not prevent detection: some backends (e.g. Bark)
|
||||
// don't own the node behind the invoice, and the payee is unavailable for
|
||||
// private BOLT12 payments.
|
||||
func TestSendPaymentSync_SelfPaymentDetection_DifferentPubkey(t *testing.T) {
|
||||
svc, err := tests.CreateTestService(t)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -89,6 +94,6 @@ func TestSendPaymentSync_SelfPaymentDetection_DifferentPubkey(t *testing.T) {
|
|||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, transaction)
|
||||
assert.False(t, transaction.SelfPayment)
|
||||
assert.True(t, transaction.SelfPayment)
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,16 +301,16 @@ func (svc *transactionsService) SendPaymentSync(payReq string, amountMsat *uint6
|
|||
return nil, errors.New("this invoice has expired")
|
||||
}
|
||||
|
||||
// A payment is a self-payment if we have an incoming transaction for the
|
||||
// exact same invoice (i.e. it was generated by this hub).
|
||||
selfPayment := false
|
||||
if paymentRequest.Payee != "" && paymentRequest.Payee == lnClient.GetPubkey() {
|
||||
var incomingTransaction db.Transaction
|
||||
result := svc.db.Limit(1).Find(&incomingTransaction, &db.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
PaymentRequest: payReq,
|
||||
})
|
||||
if result.Error == nil && result.RowsAffected > 0 {
|
||||
selfPayment = true
|
||||
}
|
||||
var incomingTransaction db.Transaction
|
||||
result := svc.db.Limit(1).Find(&incomingTransaction, &db.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
PaymentRequest: payReq,
|
||||
})
|
||||
if result.Error == nil && result.RowsAffected > 0 {
|
||||
selfPayment = true
|
||||
}
|
||||
|
||||
var dbTransaction db.Transaction
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue