mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
Merge branch 'master' into feat/auto-swaps
This commit is contained in:
commit
6caa2aa7d0
35 changed files with 1096 additions and 568 deletions
|
|
@ -1113,6 +1113,12 @@ func (api *api) SyncWallet() error {
|
|||
api.svc.GetLNClient().UpdateLastWalletSyncRequest()
|
||||
return nil
|
||||
}
|
||||
func (api *api) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
if api.svc.GetLNClient() == nil {
|
||||
return nil, errors.New("LNClient not started")
|
||||
}
|
||||
return api.svc.GetLNClient().ListOnchainTransactions(ctx)
|
||||
}
|
||||
|
||||
func (api *api) GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error) {
|
||||
var err error
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type API interface {
|
|||
RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, sendAll bool) (*RedeemOnchainFundsResponse, error)
|
||||
GetBalances(ctx context.Context) (*BalancesResponse, error)
|
||||
ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error)
|
||||
ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error)
|
||||
SendPayment(ctx context.Context, invoice string, amountMsat *uint64) (*SendPaymentResponse, error)
|
||||
CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error)
|
||||
LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,13 @@ func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64,
|
|||
if api.svc.GetLNClient() == nil {
|
||||
return nil, errors.New("LNClient not started")
|
||||
}
|
||||
transactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, api.svc.GetLNClient(), appId, true)
|
||||
|
||||
forceFilterByAppId := false
|
||||
if appId != nil {
|
||||
forceFilterByAppId = true
|
||||
}
|
||||
|
||||
transactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, api.svc.GetLNClient(), appId, forceFilterByAppId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
44
db/migrations/202504231037_add_indexes.go
Normal file
44
db/migrations/202504231037_add_indexes.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var _202504231037_add_indexes = &gormigrate.Migration{
|
||||
ID: "202504231037_add_indexes",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := tx.Exec(`
|
||||
DROP INDEX IF EXISTS idx_transactions_request_event_id;
|
||||
DROP INDEX IF EXISTS idx_transactions_created_at;
|
||||
DROP INDEX IF EXISTS idx_transactions_settled_at;
|
||||
DROP INDEX IF EXISTS idx_transactions_app_id_type_state_created_at_settled_at_payment_hash;
|
||||
`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Exec(`
|
||||
CREATE INDEX idx_transactions_state_type ON transactions(state, type);
|
||||
CREATE INDEX idx_transactions_state_type_updated_at ON transactions(state, type, updated_at);
|
||||
CREATE INDEX idx_transactions_app_id_state_type_updated_at ON transactions(app_id, state, type, updated_at);
|
||||
CREATE INDEX idx_transactions_payment_hash_settled_at_created_at ON transactions(payment_hash, settled_at, created_at);
|
||||
`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ func Migrate(gormDB *gorm.DB) error {
|
|||
_202408291715_app_metadata,
|
||||
_202410141503_add_wallet_pubkey,
|
||||
_202412212345_fix_types,
|
||||
_202504231037_add_indexes,
|
||||
})
|
||||
|
||||
return m.Migrate()
|
||||
|
|
|
|||
3
fly.toml
3
fly.toml
|
|
@ -6,7 +6,8 @@
|
|||
app = 'nwc'
|
||||
primary_region = 'lax'
|
||||
swap_size_mb = 2048
|
||||
kill_timeout = 120
|
||||
# Add a kill timeout 30s longer than LDK node shutdown timeout
|
||||
kill_timeout = 330
|
||||
|
||||
[build]
|
||||
image = 'ghcr.io/getalby/hub:latest'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import { ListTodoIcon, LucideIcon, ZapIcon } from "lucide-react";
|
||||
import { ReactElement } from "react";
|
||||
import {
|
||||
HeartIcon,
|
||||
ListTodoIcon,
|
||||
LucideIcon,
|
||||
XIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import React, { ReactElement } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import {
|
||||
|
|
@ -9,13 +15,24 @@ import {
|
|||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
import { Progress } from "src/components/ui/progress";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { localStorageKeys, SUPPORT_ALBY_CONNECTION_NAME } from "src/constants";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useApps } from "src/hooks/useApps";
|
||||
import { useOnboardingData } from "src/hooks/useOnboardingData";
|
||||
import useChannelOrderStore from "src/state/ChannelOrderStore";
|
||||
|
||||
function SidebarHint() {
|
||||
const { isLoading, checklistItems } = useOnboardingData();
|
||||
const { data: apps } = useApps();
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
const { order } = useChannelOrderStore();
|
||||
const location = useLocation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [hiddenUntil, setHiddenUntil] = React.useState(
|
||||
localStorage.getItem(localStorageKeys.supportAlbySidebarHintHiddenUntil)
|
||||
);
|
||||
|
||||
// User has a channel order
|
||||
if (
|
||||
|
|
@ -67,6 +84,42 @@ function SidebarHint() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const showSupport =
|
||||
apps &&
|
||||
apps.filter((x) => x.name == SUPPORT_ALBY_CONNECTION_NAME).length === 0 &&
|
||||
!albyMe?.subscription.plan_code;
|
||||
|
||||
if (
|
||||
!location.pathname.startsWith("/support-alby") &&
|
||||
showSupport &&
|
||||
(!hiddenUntil || new Date() >= new Date(hiddenUntil))
|
||||
) {
|
||||
return (
|
||||
<SidebarHintCard
|
||||
onClose={() => {
|
||||
// Set the date to the next 21st of the month
|
||||
const now = new Date();
|
||||
const next21st = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() + (now.getDate() >= 21 ? 1 : 0),
|
||||
21
|
||||
).toString();
|
||||
localStorage.setItem(
|
||||
localStorageKeys.supportAlbySidebarHintHiddenUntil,
|
||||
next21st
|
||||
);
|
||||
setHiddenUntil(next21st);
|
||||
toast({ title: "No worries, we'll remind you again!" });
|
||||
}}
|
||||
icon={HeartIcon}
|
||||
title="Support Alby Hub"
|
||||
description="See how you can support the development of Alby Hub"
|
||||
buttonText="Become a Supporter"
|
||||
buttonLink="/support-alby"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type SidebarHintCardProps = {
|
||||
|
|
@ -75,6 +128,7 @@ type SidebarHintCardProps = {
|
|||
buttonText: string;
|
||||
buttonLink: string;
|
||||
icon: LucideIcon;
|
||||
onClose?: () => void;
|
||||
};
|
||||
function SidebarHintCard({
|
||||
title,
|
||||
|
|
@ -82,12 +136,21 @@ function SidebarHintCard({
|
|||
icon: Icon,
|
||||
buttonText,
|
||||
buttonLink,
|
||||
onClose,
|
||||
}: SidebarHintCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="p-4">
|
||||
<Icon className="h-8 w-8 mb-4" />
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{onClose && (
|
||||
<button
|
||||
className="absolute top-4 right-4 text-muted-foreground hover:text-primary"
|
||||
onClick={onClose}
|
||||
>
|
||||
<XIcon name="X" />
|
||||
</button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-0">
|
||||
<div className="text-muted-foreground mb-4 text-sm">{description}</div>
|
||||
|
|
|
|||
|
|
@ -35,11 +35,20 @@ type Props = {
|
|||
tx: Transaction;
|
||||
};
|
||||
|
||||
function safeNpubEncode(hex: string): string | undefined {
|
||||
try {
|
||||
return nip19.npubEncode(hex);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function TransactionItem({ tx }: Props) {
|
||||
const { data: apps } = useApps();
|
||||
const { toast } = useToast();
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const type = tx.type;
|
||||
|
||||
const typeStateText =
|
||||
type == "incoming"
|
||||
? "Received"
|
||||
|
|
@ -48,17 +57,31 @@ function TransactionItem({ tx }: Props) {
|
|||
: tx.state === "pending"
|
||||
? "Sending"
|
||||
: "Failed";
|
||||
|
||||
const Icon =
|
||||
tx.state === "failed"
|
||||
? XIcon
|
||||
: tx.type == "outgoing"
|
||||
? ArrowUpIcon
|
||||
: ArrowDownIcon;
|
||||
const app =
|
||||
tx.appId !== undefined
|
||||
? apps?.find((app) => app.id === tx.appId)
|
||||
|
||||
const app = React.useMemo(
|
||||
() =>
|
||||
tx.appId != null ? apps?.find((app) => app.id === tx.appId) : undefined,
|
||||
[apps, tx.appId]
|
||||
);
|
||||
|
||||
const pubkey = tx.metadata?.nostr?.pubkey;
|
||||
const npub = pubkey ? safeNpubEncode(pubkey) : undefined;
|
||||
|
||||
const from = tx.metadata?.payer_data?.name
|
||||
? `from ${tx.metadata.payer_data.name}`
|
||||
: npub
|
||||
? `zap from ${npub.substring(0, 12)}...`
|
||||
: undefined;
|
||||
|
||||
const eventId = tx.metadata?.nostr?.tags?.find((t) => t[0] === "e")?.[1];
|
||||
|
||||
const copy = (text: string) => {
|
||||
copyToClipboard(text, toast);
|
||||
};
|
||||
|
|
@ -105,17 +128,6 @@ function TransactionItem({ tx }: Props) {
|
|||
</div>
|
||||
);
|
||||
|
||||
let from;
|
||||
|
||||
if (tx.metadata?.payer_data?.name) {
|
||||
from = "from " + tx.metadata.payer_data.name;
|
||||
} else if (tx.metadata?.nostr) {
|
||||
const npub = nip19.npubEncode(tx.metadata.nostr.pubkey);
|
||||
from = "zap from " + npub.substring(0, 12) + "...";
|
||||
}
|
||||
|
||||
const eventId = tx.metadata?.nostr?.tags.find((t) => t[0] === "e")?.[1];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
|
|
@ -144,7 +156,7 @@ function TransactionItem({ tx }: Props) {
|
|||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm md:text-base text-muted-foreground break-all w-full truncate">
|
||||
<p className="text-sm md:text-base text-muted-foreground break-all line-clamp-1">
|
||||
{tx.description}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -231,7 +243,7 @@ function TransactionItem({ tx }: Props) {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
{tx.metadata?.nostr && eventId && (
|
||||
{tx.metadata?.nostr && eventId && npub && (
|
||||
<div className="mt-6">
|
||||
<p>
|
||||
<ExternalLink
|
||||
|
|
@ -243,7 +255,7 @@ function TransactionItem({ tx }: Props) {
|
|||
Nostr Zap
|
||||
</ExternalLink>{" "}
|
||||
<span className="text-muted-foreground break-all">
|
||||
from {nip19.npubEncode(tx.metadata.nostr.pubkey)}
|
||||
from {npub}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function UserAvatar({ className }: { className?: string }) {
|
|||
return (
|
||||
<Avatar className={cn("h-8 w-8 rounded-lg", className)}>
|
||||
<AvatarImage src={albyMe?.avatar} alt="Avatar" />
|
||||
<AvatarFallback className="rounded-lg">
|
||||
<AvatarFallback className="font-medium rounded-lg">
|
||||
{(albyMe?.name || albyMe?.email || "SN").substring(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
|
|
|||
121
frontend/src/components/channels/OnchainTransactionsTable.tsx
Normal file
121
frontend/src/components/channels/OnchainTransactionsTable.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import dayjs from "dayjs";
|
||||
import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
|
||||
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table";
|
||||
import { useOnchainTransactions } from "src/hooks/useOnchainTransactions";
|
||||
import { cn } from "src/lib/utils";
|
||||
|
||||
export function OnchainTransactionsTable() {
|
||||
const { data: transactions } = useOnchainTransactions();
|
||||
|
||||
if (!transactions?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">On-Chain Transactions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{transactions.map((tx) => {
|
||||
const Icon = tx.type == "outgoing" ? ArrowUpIcon : ArrowDownIcon;
|
||||
return (
|
||||
<TableRow
|
||||
key={tx.txId}
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`https://mempool.space/tx/${tx.txId}`,
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
>
|
||||
<TableCell className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-center items-center rounded-full w-10 h-10 relative",
|
||||
tx.state === "unconfirmed"
|
||||
? "bg-blue-100 dark:bg-sky-950 animate-pulse"
|
||||
: tx.type === "outgoing"
|
||||
? "bg-orange-100 dark:bg-amber-950"
|
||||
: "bg-green-100 dark:bg-emerald-950"
|
||||
)}
|
||||
title={`${tx.numConfirmations} confirmations`}
|
||||
>
|
||||
<Icon
|
||||
strokeWidth={3}
|
||||
className={cn(
|
||||
"w-6 h-6",
|
||||
tx.state === "unconfirmed"
|
||||
? "stroke-blue-500 dark:stroke-sky-500"
|
||||
: tx.type === "outgoing"
|
||||
? "stroke-orange-500 dark:stroke-amber-500"
|
||||
: "stroke-green-500 dark:stroke-teal-500"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:flex md:gap-2 md:items-center">
|
||||
<p className="font-semibold text-lg">
|
||||
{tx.type == "outgoing"
|
||||
? tx.state === "confirmed"
|
||||
? "Sent"
|
||||
: "Sending"
|
||||
: tx.state === "confirmed"
|
||||
? "Received"
|
||||
: "Receiving"}
|
||||
</p>
|
||||
<p
|
||||
className="text-muted-foreground"
|
||||
title={dayjs(tx.createdAt * 1000)
|
||||
.local()
|
||||
.format("D MMMM YYYY, HH:mm")}
|
||||
>
|
||||
{dayjs(tx.createdAt * 1000)
|
||||
.local()
|
||||
.fromNow()}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="flex flex-row gap-1">
|
||||
<p
|
||||
className={cn(
|
||||
tx.type == "incoming" &&
|
||||
"text-green-600 dark:text-emerald-500"
|
||||
)}
|
||||
>
|
||||
{tx.type == "outgoing" ? "-" : "+"}
|
||||
<span className="font-medium">
|
||||
{new Intl.NumberFormat().format(tx.amountSat)}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{tx.amountSat == 1 ? "sat" : "sats"}
|
||||
</p>
|
||||
</div>
|
||||
<FormattedFiatAmount
|
||||
className="text-xs"
|
||||
amount={tx.amountSat}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { ArrowDownIcon } from "lucide-react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import AppHeader from "src/components/AppHeader";
|
||||
import BalanceCard from "src/components/BalanceCard";
|
||||
import Loading from "src/components/Loading";
|
||||
import { useBalances } from "src/hooks/useBalances";
|
||||
import { useChannels } from "src/hooks/useChannels";
|
||||
|
|
@ -19,22 +17,23 @@ export default function ReceiveLayout() {
|
|||
|
||||
return (
|
||||
<div className="grid gap-5">
|
||||
<AppHeader title="Receive" />
|
||||
<div className="flex gap-12 w-full">
|
||||
<div className="w-full max-w-lg">
|
||||
<Outlet />
|
||||
</div>
|
||||
{hasChannelManagement && (
|
||||
<BalanceCard
|
||||
balance={balances.lightning.totalReceivable}
|
||||
title="Receiving Capacity"
|
||||
buttonTitle="Increase"
|
||||
buttonLink="/channels/incoming"
|
||||
BalanceCardIcon={ArrowDownIcon}
|
||||
hasChannelManagement
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<AppHeader
|
||||
title="Receive"
|
||||
contentRight={
|
||||
hasChannelManagement && (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-muted-foreground">Receive Limit:</span>
|
||||
<div className="balance sensitive slashed-zero">
|
||||
{new Intl.NumberFormat().format(
|
||||
Math.floor(balances.lightning.totalReceivable / 1000)
|
||||
)}{" "}
|
||||
sats
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export const localStorageKeys = {
|
|||
setupReturnTo: "setupReturnTo",
|
||||
channelOrder: "channelOrder",
|
||||
authToken: "authToken",
|
||||
supportAlbySidebarHintHiddenUntil: "supportAlbySidebarHintHiddenUntil",
|
||||
};
|
||||
|
||||
export const ONCHAIN_DUST_SATS = 1000;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// src/hooks/useOnboardingData.ts
|
||||
|
||||
import { SUPPORT_ALBY_CONNECTION_NAME } from "src/constants";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useApps } from "src/hooks/useApps";
|
||||
import { useChannels } from "src/hooks/useChannels";
|
||||
|
|
@ -55,12 +54,6 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
|
|||
const hasCustomApp =
|
||||
apps && apps.find((x) => x.name !== "getalby.com") !== undefined;
|
||||
const hasTransaction = transactions.totalCount > 0;
|
||||
const hasSetupSupportPayment = !!(
|
||||
(apps &&
|
||||
apps.find((x) => x.name === SUPPORT_ALBY_CONNECTION_NAME) !==
|
||||
undefined) ||
|
||||
albyMe?.subscription.plan_code
|
||||
);
|
||||
|
||||
const checklistItems: Omit<ChecklistItem, "disabled">[] = [
|
||||
...(hasChannelManagement
|
||||
|
|
@ -110,17 +103,6 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(!info.oauthRedirect
|
||||
? [
|
||||
{
|
||||
title: "Support Alby",
|
||||
description:
|
||||
"Set up a monthly contribution to help us build the future of digital payments.",
|
||||
checked: hasSetupSupportPayment,
|
||||
to: "/support-alby",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const nextStep = checklistItems.find((x) => !x.checked);
|
||||
|
|
|
|||
15
frontend/src/hooks/useOnchainTransactions.ts
Normal file
15
frontend/src/hooks/useOnchainTransactions.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { OnchainTransaction } from "src/types";
|
||||
import { swrFetcher } from "src/utils/swr";
|
||||
import useSWR, { SWRConfiguration } from "swr";
|
||||
|
||||
const pollConfiguration: SWRConfiguration = {
|
||||
refreshInterval: 30000,
|
||||
};
|
||||
|
||||
export function useOnchainTransactions() {
|
||||
return useSWR<OnchainTransaction[]>(
|
||||
"/api/node/transactions",
|
||||
swrFetcher,
|
||||
pollConfiguration
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { ExternalLinkIcon, HeartIcon } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import AppHeader from "src/components/AppHeader";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
|
|
@ -28,7 +29,6 @@ import { LightningMessageboardWidget } from "src/components/home/widgets/Lightni
|
|||
import { NodeStatusWidget } from "src/components/home/widgets/NodeStatusWidget";
|
||||
import { OnchainFeesWidget } from "src/components/home/widgets/OnchainFeesWidget";
|
||||
import { WhatsNewWidget } from "src/components/home/widgets/WhatsNewWidget";
|
||||
import { UpgradeDialog } from "src/components/UpgradeDialog";
|
||||
|
||||
function getGreeting(name: string | undefined) {
|
||||
const hours = new Date().getHours();
|
||||
|
|
@ -60,19 +60,29 @@ function Home() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AppHeader
|
||||
title={getGreeting(albyMe?.name)}
|
||||
contentRight={
|
||||
<UpgradeDialog>
|
||||
<Button variant="premium">Upgrade</Button>
|
||||
</UpgradeDialog>
|
||||
}
|
||||
/>
|
||||
<AppHeader title={getGreeting(albyMe?.name)} />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5 items-start justify-start">
|
||||
{/* LEFT */}
|
||||
<div className="grid gap-5">
|
||||
<OnboardingChecklist />
|
||||
<WhatsNewWidget />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Support Alby</CardTitle>
|
||||
<CardDescription>
|
||||
Upgrade to Pro or setup a recurring payment to support the
|
||||
development of Alby Hub, Alby Go and the NWC ecosystem.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Link to="/support-alby">
|
||||
<Button variant="outline">
|
||||
<HeartIcon className="w-4 h-4 mr-2" />
|
||||
Become a Supporter
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{info.albyAccountConnected && (
|
||||
<ExternalLink to="https://www.getalby.com/dashboard">
|
||||
<Card>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import {
|
||||
CodeIcon,
|
||||
HandCoins,
|
||||
PlusCircleIcon,
|
||||
RefreshCwIcon,
|
||||
SparklesIcon,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import AppHeader from "src/components/AppHeader";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import { Badge } from "src/components/ui/badge";
|
||||
import { Button, LinkButton } from "src/components/ui/button";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -155,208 +156,187 @@ function SupportAlby() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-full max-w-screen-md mx-auto flex flex-col justify-center">
|
||||
<div className="flex flex-col items-center justify-center gap-6">
|
||||
<section className="text-center">
|
||||
<h2 className="text-3xl font-semibold mb-4 max-sm:mt-8">
|
||||
Your Support Matters
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-balance">
|
||||
We are committed to elevating the Bitcoin ecosystem by offering
|
||||
reliable, efficient, and user-friendly software solutions for
|
||||
seamless transactions. With your help, we can keep pushing
|
||||
boundaries and evolving Alby Hub into something even more
|
||||
extraordinary.
|
||||
</p>
|
||||
</section>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Why Your Contribution Is Important</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="flex flex-col gap-5 text-sm">
|
||||
<li className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<PlusCircleIcon className="w-4 h-4 mr-2" />
|
||||
Unlock New Features
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Your support empowers us to design and implement
|
||||
cutting-edge{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/issues"
|
||||
>
|
||||
features
|
||||
</ExternalLink>{" "}
|
||||
that enhance your experience and keep us at the forefront of
|
||||
technology.
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex flex-col ">
|
||||
<div className="flex flex-row items-center">
|
||||
<RefreshCwIcon className="w-4 h-4 mr-2" />
|
||||
Ensure Continuous Improvement
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
With your contributions, we can provide{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/releases"
|
||||
>
|
||||
regular updates
|
||||
</ExternalLink>{" "}
|
||||
and ongoing maintenance, ensuring everything runs smoothly
|
||||
and efficiently for all users.
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex flex-col ">
|
||||
<div className="flex flex-row items-center">
|
||||
<CodeIcon className="w-4 h-4 mr-2" />
|
||||
Support Open-Source Freedom
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Your support helps us keep Alby Hub true to the principles
|
||||
of{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/blob/master/LICENSE"
|
||||
>
|
||||
free and open-source software
|
||||
</ExternalLink>{" "}
|
||||
and remains accessible for everyone to use, modify and
|
||||
improve.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-2 gap-3 w-full">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
<div>Unlock Pro</div>
|
||||
</div>
|
||||
<Badge variant="outline">ACCOUNT REQUIRED</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Upgrade your Alby Account to Pro, support Alby and enjoy
|
||||
additional perks.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex justify-center">
|
||||
<UpgradeDialog>
|
||||
<Button variant="premium">Unlock Pro</Button>
|
||||
</UpgradeDialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Become a Supporter</CardTitle>
|
||||
<CardDescription>
|
||||
Support Alby with recurring #value4value payments.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex justify-center">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<DialogTrigger asChild>
|
||||
<Button>Become a Supporter</Button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<DialogContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Become a Supporter</DialogTitle>
|
||||
<DialogDescription>
|
||||
A new app connection will be established to facilitate
|
||||
monthly payments to Alby. You can cancel it anytime
|
||||
through the connections page.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3 my-5">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label htmlFor="amount" className="text-right mt-2">
|
||||
Amount <br></br>
|
||||
<span className="font-normal text-muted-foreground">
|
||||
(sats / month)
|
||||
</span>
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="amount"
|
||||
value={amount}
|
||||
required
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-1 mt-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("3000")}
|
||||
>
|
||||
🙏 3000
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("6000")}
|
||||
>
|
||||
💪 6000
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("10000")}
|
||||
>
|
||||
✨ 10000
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="comment" className="text-right">
|
||||
Name{" "}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="sender-name"
|
||||
value={senderName}
|
||||
onChange={(e) => setSenderName(e.target.value)}
|
||||
placeholder={`Nickname, npub, @twitter, etc.`}
|
||||
className="col-span-3"
|
||||
/>
|
||||
<AppHeader
|
||||
title="Support Alby Hub"
|
||||
description="We are committed to elevating the Bitcoin ecosystem by offering reliable, efficient, and user-friendly software solutions for seamless transactions. With your help, we can keep pushing boundaries and evolving Alby Hub into something extraordinary."
|
||||
/>
|
||||
<h2 className="text-2xl font-semibold">Become a Supporter</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="grow">
|
||||
<CardTitle>Upgrade to Pro</CardTitle>
|
||||
<CardDescription>
|
||||
Upgrade your Alby Account to Pro for a small fee and enjoy
|
||||
additional perks that come with it!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex justify-end">
|
||||
<UpgradeDialog>
|
||||
<Button>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="grow">
|
||||
<CardTitle>Donate to Alby Hub development</CardTitle>
|
||||
<CardDescription>
|
||||
Set up a recurring value4value payment to support the development
|
||||
of Alby Hub, Alby Go, and the NWC ecosystem.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 grow" />
|
||||
<CardFooter className="flex justify-end">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<HandCoins className="w-4 h-4 mr-2" />
|
||||
Setup Donation
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<DialogContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Become a Supporter</DialogTitle>
|
||||
<DialogDescription>
|
||||
A new app connection will be established to facilitate
|
||||
monthly payments to Alby. You can cancel it anytime
|
||||
through the connections page.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3 my-5">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label htmlFor="amount" className="text-right mt-2">
|
||||
Amount <br></br>
|
||||
<span className="font-normal text-muted-foreground">
|
||||
(sats / month)
|
||||
</span>
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="amount"
|
||||
value={amount}
|
||||
required
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-1 mt-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("3000")}
|
||||
>
|
||||
🙏 3000
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("6000")}
|
||||
>
|
||||
💪 6000
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setAmount("10000")}
|
||||
>
|
||||
✨ 10000
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="comment" className="text-right">
|
||||
Name{" "}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
(optional)
|
||||
</span>
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="sender-name"
|
||||
value={senderName}
|
||||
onChange={(e) => setSenderName(e.target.value)}
|
||||
placeholder={`Nickname, npub, @twitter, etc.`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
disabled={!!isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Complete Setup
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<LinkButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
to="/"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
Maybe later
|
||||
</LinkButton>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
disabled={!!isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Complete Setup
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Why Your Contribution Is Important
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-5">
|
||||
<li className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<PlusCircleIcon className="w-4 h-4 mr-2" />
|
||||
Unlock New Features
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Your support empowers us to design and implement cutting-edge{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/issues"
|
||||
>
|
||||
features
|
||||
</ExternalLink>{" "}
|
||||
that enhance your experience and keep us at the forefront of
|
||||
technology.
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex flex-col ">
|
||||
<div className="flex flex-row items-center">
|
||||
<RefreshCwIcon className="w-4 h-4 mr-2" />
|
||||
Ensure Continuous Improvement
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
With your contributions, we can provide{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/releases"
|
||||
>
|
||||
regular updates
|
||||
</ExternalLink>{" "}
|
||||
and ongoing maintenance, ensuring everything runs smoothly and
|
||||
efficiently for all users.
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex flex-col ">
|
||||
<div className="flex flex-row items-center">
|
||||
<CodeIcon className="w-4 h-4 mr-2" />
|
||||
Support Open-Source Freedom
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Your support helps us keep Alby Hub true to the principles of{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://github.com/getAlby/hub/blob/master/LICENSE"
|
||||
>
|
||||
free and open-source software
|
||||
</ExternalLink>{" "}
|
||||
and remains accessible for everyone to use, modify and improve.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import AppHeader from "src/components/AppHeader.tsx";
|
|||
import { ChannelsCards } from "src/components/channels/ChannelsCards.tsx";
|
||||
import { ChannelsTable } from "src/components/channels/ChannelsTable.tsx";
|
||||
import { HealthCheckAlert } from "src/components/channels/HealthcheckAlert";
|
||||
import { OnchainTransactionsTable } from "src/components/channels/OnchainTransactionsTable.tsx";
|
||||
import { SwapDialogs } from "src/components/channels/SwapDialogs";
|
||||
import EmptyState from "src/components/EmptyState.tsx";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
|
|
@ -572,6 +573,7 @@ export default function Channels() {
|
|||
|
||||
<ChannelsTable channels={channels} nodes={nodes} />
|
||||
<ChannelsCards channels={channels} nodes={nodes} />
|
||||
<OnchainTransactionsTable />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CopyIcon, EditIcon } from "lucide-react";
|
||||
import { CopyIcon, PencilIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Loading from "src/components/Loading";
|
||||
|
|
@ -35,34 +35,38 @@ export default function Receive() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-5">
|
||||
{info?.albyAccountConnected && me?.lightning_address && (
|
||||
<div className="flex flex-col items-center justify-center gap-6 w-full sm:w-64">
|
||||
<div className="relative flex flex-col items-center justify-center w-full">
|
||||
<QRCode value={me.lightning_address} className="w-full h-auto" />
|
||||
<UserAvatar className="w-14 h-auto absolute border-4 border-white" />
|
||||
<div className="w-full max-w-lg">
|
||||
<div className="grid gap-5">
|
||||
{info?.albyAccountConnected && me?.lightning_address && (
|
||||
<div className="flex flex-col items-center justify-center gap-6 border rounded-xl w-full md:max-w-xs p-4 md:p-6">
|
||||
<div className="relative flex flex-col items-center justify-center">
|
||||
<QRCode value={me.lightning_address} className="w-full h-auto" />
|
||||
<UserAvatar className="w-14 h-14 absolute border-4 border-white bg-white" />
|
||||
</div>
|
||||
<p className="text-center font-semibold break-all">
|
||||
{me.lightning_address}
|
||||
</p>
|
||||
<div className="flex gap-4 w-full">
|
||||
<LinkButton
|
||||
to="invoice"
|
||||
variant="outline"
|
||||
className="flex-1 flex gap-2 items-center justify-center"
|
||||
>
|
||||
<PencilIcon className="w-4 h-4" /> Amount
|
||||
</LinkButton>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
copyToClipboard(me.lightning_address, toast);
|
||||
}}
|
||||
className="flex-1 flex gap-2 items-center justify-center"
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" /> Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-semibold break-all">{me.lightning_address}</p>
|
||||
<div className="flex gap-4 w-full">
|
||||
<LinkButton
|
||||
to="invoice"
|
||||
variant="outline"
|
||||
className="flex-1 flex gap-2 items-center justify-center"
|
||||
>
|
||||
<EditIcon className="w-4 h-4" /> Amount
|
||||
</LinkButton>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
copyToClipboard(me.lightning_address, toast);
|
||||
}}
|
||||
className="flex-1 flex gap-2 items-center justify-center"
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" /> Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AlertTriangleIcon, CircleCheckIcon, CopyIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
|
||||
import Loading from "src/components/Loading";
|
||||
import QRCode from "src/components/QRCode";
|
||||
|
|
@ -13,6 +14,7 @@ import { Button } from "src/components/ui/button";
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
|
|
@ -20,6 +22,7 @@ import { Input } from "src/components/ui/input";
|
|||
import { Label } from "src/components/ui/label";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useBalances } from "src/hooks/useBalances";
|
||||
|
||||
import { useInfo } from "src/hooks/useInfo";
|
||||
|
|
@ -29,7 +32,8 @@ import { CreateInvoiceRequest, Transaction } from "src/types";
|
|||
import { request } from "src/utils/request";
|
||||
|
||||
export default function ReceiveInvoice() {
|
||||
const { hasChannelManagement } = useInfo();
|
||||
const { data: info, hasChannelManagement } = useInfo();
|
||||
const { data: me } = useAlbyMe();
|
||||
const { data: balances } = useBalances();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
|
@ -51,7 +55,7 @@ export default function ReceiveInvoice() {
|
|||
}
|
||||
}, [invoiceData, toast]);
|
||||
|
||||
if (!balances) {
|
||||
if (!balances || !info || (info.albyAccountConnected && !me)) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
|
@ -94,136 +98,157 @@ export default function ReceiveInvoice() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-5">
|
||||
{hasChannelManagement &&
|
||||
parseInt(amount || "0") * 1000 >=
|
||||
0.8 * balances.lightning.totalReceivable && (
|
||||
<Alert>
|
||||
<AlertTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>Low receiving capacity</AlertTitle>
|
||||
<AlertDescription>
|
||||
You likely won't be able to receive payments until you{" "}
|
||||
<Link className="underline" to="/channels/incoming">
|
||||
increase your receiving capacity.
|
||||
</Link>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="flex gap-12 w-full">
|
||||
<div className="w-full max-w-lg">
|
||||
{transaction ? (
|
||||
<>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">
|
||||
{paymentDone ? "Payment Received" : "Invoice"}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center gap-4">
|
||||
{paymentDone ? (
|
||||
<>
|
||||
<CircleCheckIcon className="w-32 h-32 mb-1" />
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<p>
|
||||
Received{" "}
|
||||
{Math.floor((invoiceData?.amount ?? 0) / 1000)} sats
|
||||
</p>
|
||||
<FormattedFiatAmount
|
||||
amount={Math.floor((invoiceData?.amount ?? 0) / 1000)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<p className="text-xl slashed-zero">
|
||||
{new Intl.NumberFormat().format(parseInt(amount))}{" "}
|
||||
sats
|
||||
</p>
|
||||
<FormattedFiatAmount amount={parseInt(amount)} />
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2 text-sm">
|
||||
<Loading className="w-4 h-4" />
|
||||
<p>Waiting for payment</p>
|
||||
</div>
|
||||
<QRCode value={transaction.invoice} className="w-full" />
|
||||
<div>
|
||||
<Button onClick={copy} variant="outline">
|
||||
<CopyIcon className="w-4 h-4 mr-2" />
|
||||
Copy Invoice
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{paymentDone && (
|
||||
<>
|
||||
<Button
|
||||
className="mt-4 w-full"
|
||||
onClick={() => {
|
||||
setPaymentDone(false);
|
||||
setTransaction(null);
|
||||
}}
|
||||
>
|
||||
Receive Another Payment
|
||||
</Button>
|
||||
<Link to="/wallet">
|
||||
<Button
|
||||
className="mt-4 w-full"
|
||||
onClick={() => {
|
||||
setPaymentDone(false);
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
Back To Wallet
|
||||
</Button>
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
<div className="w-full md:max-w-xl">
|
||||
<div className="grid gap-5">
|
||||
{hasChannelManagement &&
|
||||
parseInt(amount || "0") * 1000 >=
|
||||
0.8 * balances.lightning.totalReceivable && (
|
||||
<Alert>
|
||||
<AlertTriangleIcon className="h-4 w-4" />
|
||||
<AlertTitle>Low receiving capacity</AlertTitle>
|
||||
<AlertDescription>
|
||||
You likely won't be able to receive payments until you{" "}
|
||||
<Link className="underline" to="/channels/incoming">
|
||||
increase your receiving capacity.
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||
<div>
|
||||
<Label htmlFor="amount">Amount</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
value={amount?.toString()}
|
||||
placeholder="Amount in Satoshi..."
|
||||
onChange={(e) => {
|
||||
setAmount(e.target.value.trim());
|
||||
}}
|
||||
min={1}
|
||||
autoFocus
|
||||
/>
|
||||
<FormattedFiatAmount amount={+amount} className="mt-2" />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div>
|
||||
{transaction ? (
|
||||
<div className="flex flex-col items-center justify-center gap-6 border rounded-xl w-full md:max-w-xs p-4 md:p-6">
|
||||
{!paymentDone ? (
|
||||
<>
|
||||
<div className="flex flex-row items-center gap-2 font-medium">
|
||||
<Loading className="w-4 h-4" />
|
||||
<p>Waiting for payment</p>
|
||||
</div>
|
||||
<div className="relative flex flex-col items-center justify-center">
|
||||
<QRCode value={transaction.invoice} className="w-full" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<p className="text-xl font-semibold slashed-zero">
|
||||
{new Intl.NumberFormat().format(parseInt(amount))} sats
|
||||
</p>
|
||||
<FormattedFiatAmount amount={parseInt(amount)} />
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={copy} variant="outline">
|
||||
<CopyIcon className="w-4 h-4 mr-2" />
|
||||
Copy Invoice
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center font-medium">
|
||||
Payment Received!
|
||||
</div>
|
||||
<div className="relative flex flex-col items-center justify-center">
|
||||
<CircleCheckIcon className="w-64 h-64 mb-1" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<p className="text-xl font-semibold slashed-zero">
|
||||
{new Intl.NumberFormat().format(parseInt(amount))} sats
|
||||
</p>
|
||||
<FormattedFiatAmount amount={parseInt(amount)} />
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setPaymentDone(false);
|
||||
setTransaction(null);
|
||||
}}
|
||||
>
|
||||
Receive Another Payment
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
type="text"
|
||||
value={description}
|
||||
placeholder="For e.g. who is sending this payment?"
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
disabled={!amount}
|
||||
>
|
||||
Create Invoice
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||
<div>
|
||||
<Label htmlFor="amount">Amount</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
value={amount?.toString()}
|
||||
placeholder="Amount in Satoshi..."
|
||||
onChange={(e) => {
|
||||
setAmount(e.target.value.trim());
|
||||
}}
|
||||
min={1}
|
||||
autoFocus
|
||||
/>
|
||||
<FormattedFiatAmount amount={+amount} className="mt-2" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
type="text"
|
||||
value={description}
|
||||
placeholder="For e.g. who is sending this payment?"
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LoadingButton
|
||||
className="w-full md:w-auto"
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
disabled={!amount}
|
||||
>
|
||||
Create Invoice
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!transaction &&
|
||||
(!info?.albyAccountConnected || !me?.lightning_address) && (
|
||||
<LightningAddressCard />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LightningAddressCard() {
|
||||
return (
|
||||
<Card className="w-full self-start">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="font-semibold text-lg">
|
||||
Get Your Free Lightning Address
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3 text-muted-foreground">
|
||||
<p>
|
||||
Create free Alby Account and link it with your Alby Hub to get a
|
||||
convenient <span className="text-foreground">@getalby.com</span>{" "}
|
||||
lightning address and other perks:
|
||||
</p>
|
||||
<ul className="flex flex-col gap-1">
|
||||
<li>• Lightning address & Nostr identifier,</li>
|
||||
<li>• Personal tipping page,</li>
|
||||
<li>• Access to podcasting 2.0 apps,</li>
|
||||
<li>• Buy bitcoin directly to your wallet,</li>
|
||||
<li>• Useful email Alby Hub notifications.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
<ExternalLink to="https://getalby.com/auth/users/new">
|
||||
<Button variant="secondary">Create Alby Account</Button>
|
||||
</ExternalLink>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function ConfirmPayment() {
|
|||
<FormattedFiatAmount amount={amount || invoice.satoshi} />
|
||||
</div>
|
||||
{invoice.description && (
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 break-all">
|
||||
<Label>Description</Label>
|
||||
<p className="text-muted-foreground">{invoice.description}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -502,6 +502,15 @@ export type Boostagram = {
|
|||
valueMsatTotal: number;
|
||||
};
|
||||
|
||||
export type OnchainTransaction = {
|
||||
amountSat: number;
|
||||
createdAt: number;
|
||||
type: "incoming" | "outgoing";
|
||||
state: "confirmed" | "unconfirmed";
|
||||
numConfirmations: number;
|
||||
txId: string;
|
||||
};
|
||||
|
||||
export type ListTransactionsResponse = {
|
||||
transactions: Transaction[];
|
||||
totalCount: number;
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -5,7 +5,7 @@ go 1.24.2
|
|||
require (
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/elnosh/gonuts v0.4.0
|
||||
github.com/getAlby/ldk-node-go v0.0.0-20250409032721-a0b2d497fc2c
|
||||
github.com/getAlby/ldk-node-go v0.0.0-20250503035148-4f935f853d83
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.4
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/nbd-wtf/go-nostr v0.51.10
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -221,8 +221,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
|||
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-20250409032721-a0b2d497fc2c h1:16rFwWZ9W3Ru0nUUgcZoyRxkykJjZx0okhTXscLhRRk=
|
||||
github.com/getAlby/ldk-node-go v0.0.0-20250409032721-a0b2d497fc2c/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
|
||||
github.com/getAlby/ldk-node-go v0.0.0-20250503035148-4f935f853d83 h1:eOkG4g/8IFSK7zyNDr9X10yxBsqCgVfU2WDd70RLfBw=
|
||||
github.com/getAlby/ldk-node-go v0.0.0-20250503035148-4f935f853d83/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|||
restrictedApiGroup.GET("/node/status", httpSvc.nodeStatusHandler)
|
||||
restrictedApiGroup.GET("/node/network-graph", httpSvc.nodeNetworkGraphHandler)
|
||||
restrictedApiGroup.POST("/node/migrate-storage", httpSvc.migrateNodeStorageHandler)
|
||||
restrictedApiGroup.GET("/node/transactions", httpSvc.listOnchainTransactionsHandler)
|
||||
restrictedApiGroup.GET("/peers", httpSvc.listPeers)
|
||||
restrictedApiGroup.POST("/peers", httpSvc.connectPeerHandler)
|
||||
restrictedApiGroup.DELETE("/peers/:peerId", httpSvc.disconnectPeerHandler)
|
||||
|
|
@ -597,6 +598,20 @@ func (httpSvc *HttpService) listTransactionsHandler(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, transactions)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) listOnchainTransactionsHandler(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
transactions, err := httpSvc.api.ListOnchainTransactions(ctx)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, transactions)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) walletSyncHandler(c echo.Context) error {
|
||||
httpSvc.api.SyncWallet()
|
||||
|
||||
|
|
|
|||
|
|
@ -551,3 +551,7 @@ func (cs *CashuService) executeCommandResetWallet() (*lnclient.CustomNodeCommand
|
|||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *CashuService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import (
|
|||
"github.com/getAlby/hub/lsp"
|
||||
"github.com/getAlby/hub/service/keys"
|
||||
"github.com/getAlby/hub/transactions"
|
||||
"github.com/getAlby/hub/utils"
|
||||
)
|
||||
|
||||
type LDKService struct {
|
||||
|
|
@ -69,8 +68,6 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
|
|||
return nil, err
|
||||
}
|
||||
|
||||
logDirPath := filepath.Join(newpath, "./logs")
|
||||
|
||||
ldkConfig := ldk_node.DefaultConfig()
|
||||
listeningAddresses := strings.Split(cfg.GetEnv().LDKListeningAddresses, ",")
|
||||
|
||||
|
|
@ -111,15 +108,13 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
|
|||
}
|
||||
|
||||
ldkConfig.ListeningAddresses = &listeningAddresses
|
||||
ldkConfig.LogDirPath = &logDirPath
|
||||
logLevel, err := strconv.Atoi(cfg.GetEnv().LDKLogLevel)
|
||||
if err == nil {
|
||||
// LogLevelGossip is added due to bug in go bindings which uses an enum that starts at 1 instead of 0
|
||||
// If LogLevelGossip is changed to 0, this addition can be removed
|
||||
ldkConfig.LogLevel = ldk_node.LogLevel(logLevel) + ldk_node.LogLevelGossip
|
||||
}
|
||||
logLevel, _ := strconv.Atoi(cfg.GetEnv().LDKLogLevel)
|
||||
// LogLevelGossip is added due to bug in go bindings which uses an enum that starts at 1 instead of 0
|
||||
ldkLogger := NewLDKLogger(ldk_node.LogLevel(logLevel) + ldk_node.LogLevelGossip)
|
||||
ldkConfig.TransientNetworkGraph = cfg.GetEnv().LDKTransientNetworkGraph
|
||||
|
||||
builder := ldk_node.BuilderFromConfig(ldkConfig)
|
||||
builder.SetCustomLogger(ldkLogger)
|
||||
builder.SetNodeAlias("Alby Hub") // TODO: allow users to customize
|
||||
builder.SetEntropyBip39Mnemonic(mnemonic, nil)
|
||||
builder.SetNetwork(network)
|
||||
|
|
@ -130,11 +125,6 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
|
|||
}
|
||||
builder.SetStorageDirPath(filepath.Join(newpath, "./storage"))
|
||||
|
||||
// TODO: remove when https://github.com/lightningdevkit/rust-lightning/issues/2914 is merged
|
||||
// LDK default HTLC inflight value is 10% of the channel size. If an LSPS service is configured this will be set to 0.
|
||||
// The liquidity source below is not used because we do not use the native LDK-node LSPS2 API.
|
||||
builder.SetLiquiditySourceLsps2("52.88.33.119:9735", lsp.OlympusLSP().Pubkey, nil)
|
||||
|
||||
migrateStorage, _ := cfg.Get("LdkMigrateStorage", "")
|
||||
if migrateStorage == "VSS" {
|
||||
err = cfg.SetUpdate("LdkMigrateStorage", "", "")
|
||||
|
|
@ -192,20 +182,9 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
|
|||
|
||||
eventPublisher.RegisterSubscriber(&ls)
|
||||
|
||||
// TODO: remove when LDK supports this
|
||||
deleteOldLDKLogs(logDirPath)
|
||||
go func() {
|
||||
// delete old LDK logs every 24 hours
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
deleteOldLDKLogs(logDirPath)
|
||||
case <-ldkCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
// TODO: remove after 2026-01-01 - we now log to app logs rather than ldk log files
|
||||
// this line is just left to cleanup old logs after the update
|
||||
deleteOldLDKLogs(filepath.Join(newpath, "./logs"))
|
||||
|
||||
// check for and forward new LDK events to LDKEventBroadcaster (through ldkEventConsumer)
|
||||
go func() {
|
||||
|
|
@ -438,8 +417,8 @@ func (ls *LDKService) Shutdown() error {
|
|||
} else {
|
||||
logger.Logger.Info("LDK stop node succeeded")
|
||||
}
|
||||
case <-time.After(120 * time.Second):
|
||||
logger.Logger.Error("Timeout shutting down LDK node after 120 seconds")
|
||||
case <-time.After(5 * time.Minute):
|
||||
logger.Logger.Error("Timeout shutting down LDK node after 5 minutes")
|
||||
}
|
||||
|
||||
logger.Logger.Debug("Destroying LDK node object")
|
||||
|
|
@ -520,19 +499,12 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amoun
|
|||
return nil, errors.New("payment not found")
|
||||
}
|
||||
|
||||
bolt11PaymentKind, ok := payment.Kind.(ldk_node.PaymentKindBolt11)
|
||||
|
||||
if !ok {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"payment": payment,
|
||||
}).Error("Payment is not a bolt11 kind")
|
||||
}
|
||||
|
||||
if bolt11PaymentKind.Preimage == nil {
|
||||
logger.Logger.WithField("payment_hash", paymentHash).Error("No payment preimage for payment hash")
|
||||
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 = *bolt11PaymentKind.Preimage
|
||||
|
||||
preimage = *eventPaymentSuccessful.PaymentPreimage
|
||||
|
||||
if eventPaymentSuccessful.FeePaidMsat != nil {
|
||||
fee = *eventPaymentSuccessful.FeePaidMsat
|
||||
|
|
@ -591,7 +563,7 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
|
|||
MaxTotalRoutingFeeMsat: &maxTotalRoutingFeeMsat,
|
||||
}
|
||||
|
||||
paymentHash, err := checkLDKErr(ls.node.SpontaneousPayment().Send(amount, destination, sendingParams, customTlvs, &preimage))
|
||||
paymentHash, err := checkLDKErr(ls.node.SpontaneousPayment().SendWithTlvsAndPreimage(amount, destination, sendingParams, customTlvs, &preimage))
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Keysend failed")
|
||||
return nil, err
|
||||
|
|
@ -664,7 +636,7 @@ func (ls *LDKService) getMaxSpendable() uint64 {
|
|||
return spendable
|
||||
}
|
||||
|
||||
func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, _descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
|
||||
func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
|
||||
|
||||
if time.Duration(expiry)*time.Second > maxInvoiceExpiry {
|
||||
return nil, errors.New("expiry is too long")
|
||||
|
|
@ -688,9 +660,18 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description
|
|||
expiry = lnclient.DEFAULT_INVOICE_EXPIRY
|
||||
}
|
||||
|
||||
// TODO: support passing description hash
|
||||
var descriptionType ldk_node.Bolt11InvoiceDescription
|
||||
descriptionType = ldk_node.Bolt11InvoiceDescriptionDirect{
|
||||
Description: description,
|
||||
}
|
||||
if description == "" && descriptionHash != "" {
|
||||
descriptionType = ldk_node.Bolt11InvoiceDescriptionHash{
|
||||
Hash: descriptionHash,
|
||||
}
|
||||
}
|
||||
|
||||
invoice, err := checkLDKErr(ls.node.Bolt11Payment().Receive(uint64(amount),
|
||||
description,
|
||||
descriptionType,
|
||||
uint32(expiry)))
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -710,7 +691,7 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description
|
|||
expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
|
||||
expiresAt = &expiresAtUnix
|
||||
description = paymentRequest.Description
|
||||
descriptionHash := paymentRequest.DescriptionHash
|
||||
descriptionHash = paymentRequest.DescriptionHash
|
||||
|
||||
payment := ls.node.Payment(paymentRequest.PaymentHash)
|
||||
|
||||
|
|
@ -747,9 +728,10 @@ func (ls *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (tr
|
|||
return transaction, nil
|
||||
}
|
||||
|
||||
// TODO: throw an error if this method is called (it shouldn't be any more because this LNClient supports notifications)
|
||||
func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []lnclient.Transaction, err error) {
|
||||
transactions = []lnclient.Transaction{}
|
||||
// this method shouldn't be any more because this LNClient supports notifications
|
||||
return nil, errors.New("this method should not be called")
|
||||
/*transactions = []lnclient.Transaction{}
|
||||
|
||||
// TODO: support pagination
|
||||
payments := ls.node.ListPayments()
|
||||
|
|
@ -797,6 +779,57 @@ func (ls *LDKService) ListTransactions(ctx context.Context, from, until, limit,
|
|||
|
||||
// logger.Logger.WithField("transactions", transactions).Debug("Listed transactions")
|
||||
|
||||
return transactions, nil*/
|
||||
}
|
||||
|
||||
func (ls *LDKService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
transactions := []lnclient.OnchainTransaction{}
|
||||
for _, payment := range ls.node.ListPayments() {
|
||||
onchainPaymentKind, isOnchainPaymentKind := payment.Kind.(ldk_node.PaymentKindOnchain)
|
||||
if !isOnchainPaymentKind {
|
||||
continue
|
||||
}
|
||||
|
||||
transactionType := "incoming"
|
||||
if payment.Direction == ldk_node.PaymentDirectionOutbound {
|
||||
transactionType = "outgoing"
|
||||
}
|
||||
|
||||
var amountMsat uint64
|
||||
if payment.AmountMsat != nil {
|
||||
amountMsat = *payment.AmountMsat
|
||||
}
|
||||
var status string
|
||||
var height uint32
|
||||
var numConfirmations uint32
|
||||
switch onchainPaymentStatus := onchainPaymentKind.Status.(type) {
|
||||
case ldk_node.ConfirmationStatusConfirmed:
|
||||
status = "confirmed"
|
||||
height = onchainPaymentStatus.Height
|
||||
nodeStatus := ls.node.Status()
|
||||
numConfirmations = nodeStatus.CurrentBestBlock.Height - height
|
||||
case ldk_node.ConfirmationStatusUnconfirmed:
|
||||
status = "unconfirmed"
|
||||
}
|
||||
|
||||
createdAt := payment.CreatedAt
|
||||
if createdAt == 0 {
|
||||
createdAt = payment.LatestUpdateTimestamp
|
||||
}
|
||||
|
||||
transactions = append(transactions, lnclient.OnchainTransaction{
|
||||
AmountSat: amountMsat / 1000,
|
||||
CreatedAt: createdAt,
|
||||
State: status,
|
||||
Type: transactionType,
|
||||
NumConfirmations: numConfirmations,
|
||||
TxId: onchainPaymentKind.Txid,
|
||||
})
|
||||
|
||||
}
|
||||
sort.SliceStable(transactions, func(i, j int) bool {
|
||||
return transactions[i].CreatedAt > transactions[j].CreatedAt
|
||||
})
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
|
|
@ -1151,7 +1184,7 @@ func (ls *LDKService) RedeemOnchainFunds(ctx context.Context, toAddress string,
|
|||
if !sendAll {
|
||||
// NOTE: this may fail if user does not reserve enough for the onchain transaction
|
||||
// and can also drain the anchor reserves if the user provides a too high amount.
|
||||
txId, err := checkLDKErr(ls.node.OnchainPayment().SendToAddress(toAddress, amount))
|
||||
txId, err := checkLDKErr(ls.node.OnchainPayment().SendToAddress(toAddress, amount, nil))
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("SendToAddress failed")
|
||||
return "", err
|
||||
|
|
@ -1159,8 +1192,7 @@ func (ls *LDKService) RedeemOnchainFunds(ctx context.Context, toAddress string,
|
|||
return txId, nil
|
||||
}
|
||||
|
||||
// TODO: this could be improved to preserve anchor reserves once LDK supports this
|
||||
txId, err := checkLDKErr(ls.node.OnchainPayment().SendAllToAddress(toAddress))
|
||||
txId, err := checkLDKErr(ls.node.OnchainPayment().SendAllToAddress(toAddress, false, nil))
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("SendAllToAddress failed")
|
||||
return "", err
|
||||
|
|
@ -1224,8 +1256,8 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails)
|
|||
preimage = *bolt11PaymentKind.Preimage
|
||||
}
|
||||
settledAt = &createdAt // fallback settledAt to created at time
|
||||
if payment.LastUpdate > 0 {
|
||||
lastUpdate := int64(payment.LastUpdate)
|
||||
if payment.LatestUpdateTimestamp > 0 {
|
||||
lastUpdate := int64(payment.LatestUpdateTimestamp)
|
||||
settledAt = &lastUpdate
|
||||
}
|
||||
}
|
||||
|
|
@ -1235,7 +1267,7 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails)
|
|||
spontaneousPaymentKind, isSpontaneousPaymentKind := payment.Kind.(ldk_node.PaymentKindSpontaneous)
|
||||
if isSpontaneousPaymentKind {
|
||||
// keysend payment
|
||||
lastUpdate := int64(payment.LastUpdate)
|
||||
lastUpdate := int64(payment.LatestUpdateTimestamp)
|
||||
createdAt = int64(payment.CreatedAt)
|
||||
// TODO: remove this check some point in the future
|
||||
// all payments after v0.6.2 will have createdAt set
|
||||
|
|
@ -1266,8 +1298,8 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails)
|
|||
}
|
||||
|
||||
var fee uint64 = 0
|
||||
if payment.FeeMsat != nil {
|
||||
fee = *payment.FeeMsat
|
||||
if payment.FeePaidMsat != nil {
|
||||
fee = *payment.FeePaidMsat
|
||||
}
|
||||
|
||||
return &lnclient.Transaction{
|
||||
|
|
@ -1363,36 +1395,7 @@ func (ls *LDKService) GetNetworkGraph(ctx context.Context, nodeIds []string) (ln
|
|||
}
|
||||
|
||||
func (ls *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) {
|
||||
config := ls.node.Config()
|
||||
logPath := ""
|
||||
if config.LogDirPath != nil {
|
||||
logPath = *config.LogDirPath
|
||||
} else {
|
||||
// Default log path if not set explicitly in the config.
|
||||
logPath = filepath.Join(config.StorageDirPath, "logs")
|
||||
}
|
||||
|
||||
allLogFiles, err := filepath.Glob(filepath.Join(logPath, "ldk_node_*.log"))
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("GetLogOutput failed to list log files")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(allLogFiles) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
// Log filenames are formatted as ldk_node_YYYY_MM_DD.log, hence they
|
||||
// naturally sort by date.
|
||||
lastLogFileName := slices.Max(allLogFiles)
|
||||
|
||||
logData, err := utils.ReadFileTail(lastLogFileName, maxLen)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("GetLogOutput failed to read log file")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logData, nil
|
||||
return []byte("Node logs are now included in application logs"), nil
|
||||
}
|
||||
|
||||
func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
|
||||
|
|
@ -1433,7 +1436,6 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
|
|||
return
|
||||
}
|
||||
|
||||
// also
|
||||
maxDustHtlcExposureFromFeeRateMultiplier := uint64(0)
|
||||
if isTrusted {
|
||||
// avoid closures like "ProcessingError: Peer sent update_fee with a feerate (62500)
|
||||
|
|
@ -1580,6 +1582,11 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
|
|||
Reason: reason,
|
||||
},
|
||||
})
|
||||
case ldk_node.EventPaymentForwarded:
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"total_fee_earned_msat": eventType.TotalFeeEarnedMsat,
|
||||
"outbound_amount_forwarded_msat": eventType.OutboundAmountForwardedMsat,
|
||||
}).Info("LDK Payment forwarded")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1725,6 +1732,22 @@ func (ls *LDKService) deleteOldLDKPayments() {
|
|||
now := time.Now()
|
||||
for _, payment := range payments {
|
||||
paymentCreatedAt := time.Unix(int64(payment.CreatedAt), 0)
|
||||
|
||||
deletablePaymentKind := false
|
||||
switch (payment.Kind).(type) {
|
||||
case ldk_node.PaymentKindBolt11:
|
||||
deletablePaymentKind = true
|
||||
case ldk_node.PaymentKindSpontaneous:
|
||||
deletablePaymentKind = true
|
||||
}
|
||||
if !deletablePaymentKind {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"created_at": paymentCreatedAt,
|
||||
"payment_id": payment.Id,
|
||||
}).Debug("Skipping undeletable payment kind")
|
||||
continue
|
||||
}
|
||||
|
||||
if paymentCreatedAt.Add(maxInvoiceExpiry).Before(now) {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"created_at": paymentCreatedAt,
|
||||
|
|
@ -1811,6 +1834,14 @@ func (ls *LDKService) getPaymentFailReason(eventPaymentFailed *ldk_node.EventPay
|
|||
failureReasonMessage = "RouteNotFound"
|
||||
case ldk_node.PaymentFailureReasonUnexpectedError:
|
||||
failureReasonMessage = "UnexpectedError"
|
||||
case ldk_node.PaymentFailureReasonUnknownRequiredFeatures:
|
||||
failureReasonMessage = "UnknownRequiredFeatures"
|
||||
case ldk_node.PaymentFailureReasonInvoiceRequestExpired:
|
||||
failureReasonMessage = "InvoiceRequestExpired"
|
||||
case ldk_node.PaymentFailureReasonInvoiceRequestRejected:
|
||||
failureReasonMessage = "InvoiceRequestRejected"
|
||||
case ldk_node.PaymentFailureReasonBlindedPathCreationFailed:
|
||||
failureReasonMessage = "BlindedPathCreationFailed"
|
||||
default:
|
||||
failureReasonMessage = "UnknownError"
|
||||
}
|
||||
|
|
|
|||
48
lnclient/ldk/ldk_logger.go
Normal file
48
lnclient/ldk/ldk_logger.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package ldk
|
||||
|
||||
import (
|
||||
// "github.com/getAlby/hub/ldk_node"
|
||||
"github.com/getAlby/hub/logger"
|
||||
|
||||
"github.com/getAlby/ldk-node-go/ldk_node"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ldkLogger struct {
|
||||
logLevel ldk_node.LogLevel
|
||||
}
|
||||
|
||||
func NewLDKLogger(logLevel ldk_node.LogLevel) ldk_node.LogWriter {
|
||||
return &ldkLogger{
|
||||
logLevel: logLevel,
|
||||
}
|
||||
}
|
||||
|
||||
func (ldkLogger *ldkLogger) Log(record ldk_node.LogRecord) {
|
||||
if record.Level >= ldkLogger.logLevel {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"log_type": "LDK-node",
|
||||
"line": record.Line,
|
||||
"module_path": record.ModulePath,
|
||||
}).Log(mapLogLevel(record.Level), record.Args)
|
||||
}
|
||||
}
|
||||
|
||||
func mapLogLevel(logLevel ldk_node.LogLevel) logrus.Level {
|
||||
switch logLevel {
|
||||
case ldk_node.LogLevelGossip:
|
||||
return logrus.TraceLevel
|
||||
case ldk_node.LogLevelTrace:
|
||||
return logrus.TraceLevel
|
||||
case ldk_node.LogLevelDebug:
|
||||
return logrus.DebugLevel
|
||||
case ldk_node.LogLevelInfo:
|
||||
return logrus.InfoLevel
|
||||
case ldk_node.LogLevelWarn:
|
||||
return logrus.WarnLevel
|
||||
case ldk_node.LogLevelError:
|
||||
return logrus.ErrorLevel
|
||||
}
|
||||
logger.Logger.WithField("log_level", logLevel).Error("Unknown LDK log level")
|
||||
return logrus.ErrorLevel
|
||||
}
|
||||
|
|
@ -1318,3 +1318,39 @@ func (svc *LNDService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCo
|
|||
func (svc *LNDService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (svc *LNDService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
resp, err := svc.client.GetTransactions(ctx, &lnrpc.GetTransactionsRequest{})
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to get onchain transactions")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transactions := []lnclient.OnchainTransaction{}
|
||||
for _, tx := range resp.Transactions {
|
||||
state := "unconfirmed"
|
||||
if tx.NumConfirmations > 0 {
|
||||
state = "confirmed"
|
||||
}
|
||||
|
||||
amountSat := tx.Amount
|
||||
txType := "incoming"
|
||||
if tx.Amount < 0 {
|
||||
amountSat = -amountSat
|
||||
txType = "outgoing"
|
||||
}
|
||||
|
||||
transactions = append(transactions, lnclient.OnchainTransaction{
|
||||
AmountSat: uint64(amountSat),
|
||||
CreatedAt: uint64(tx.TimeStamp),
|
||||
State: state,
|
||||
Type: txType,
|
||||
NumConfirmations: uint32(tx.NumConfirmations),
|
||||
TxId: tx.TxHash,
|
||||
})
|
||||
}
|
||||
sort.SliceStable(transactions, func(i, j int) bool {
|
||||
return transactions[i].CreatedAt > transactions[j].CreatedAt
|
||||
})
|
||||
return transactions, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,15 @@ type Transaction struct {
|
|||
Metadata Metadata
|
||||
}
|
||||
|
||||
type OnchainTransaction struct {
|
||||
AmountSat uint64 `json:"amountSat"`
|
||||
CreatedAt uint64 `json:"createdAt"`
|
||||
State string `json:"state"`
|
||||
Type string `json:"type"`
|
||||
NumConfirmations uint32 `json:"numConfirmations"`
|
||||
TxId string `json:"txId"`
|
||||
}
|
||||
|
||||
type NodeConnectionInfo struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
Address string `json:"address"`
|
||||
|
|
@ -54,6 +63,7 @@ type LNClient interface {
|
|||
MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Transaction, err error)
|
||||
LookupInvoice(ctx context.Context, paymentHash string) (transaction *Transaction, err error)
|
||||
ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Transaction, err error)
|
||||
ListOnchainTransactions(ctx context.Context) ([]OnchainTransaction, error)
|
||||
Shutdown() error
|
||||
ListChannels(ctx context.Context) (channels []Channel, err error)
|
||||
GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *NodeConnectionInfo, err error)
|
||||
|
|
|
|||
|
|
@ -539,3 +539,7 @@ func (svc *PhoenixService) GetCustomNodeCommandDefinitions() []lnclient.CustomNo
|
|||
func (svc *PhoenixService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (svc *PhoenixService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,8 +120,10 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
"encryption": encryption,
|
||||
}).WithError(err).Error("Failed to initialize cipher")
|
||||
|
||||
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
err = svc.db.Save(&requestEvent).Error
|
||||
err = svc.db.
|
||||
Model(&requestEvent).
|
||||
Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
|
||||
Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"appPubkey": event.PubKey,
|
||||
|
|
@ -161,8 +163,10 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
return
|
||||
}
|
||||
|
||||
requestEvent.AppId = &app.ID
|
||||
err = svc.db.Save(&requestEvent).Error
|
||||
err = svc.db.
|
||||
Model(&requestEvent).
|
||||
Update("app_id", app.ID).
|
||||
Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"appPubkey": event.PubKey,
|
||||
|
|
@ -183,8 +187,10 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
}
|
||||
svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
|
||||
|
||||
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
err = svc.db.Save(&requestEvent).Error
|
||||
err = svc.db.
|
||||
Model(&requestEvent).
|
||||
Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
|
||||
Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"appPubkey": event.PubKey,
|
||||
|
|
@ -203,8 +209,10 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
"appId": app.ID,
|
||||
}).WithError(err).Error("Failed to decrypt content")
|
||||
|
||||
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
err = svc.db.Save(&requestEvent).Error
|
||||
err = svc.db.
|
||||
Model(&requestEvent).
|
||||
Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
|
||||
Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"appPubkey": event.PubKey,
|
||||
|
|
@ -251,8 +259,10 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
"eventKind": event.Kind,
|
||||
}).WithError(err).Error("Failed to process event")
|
||||
|
||||
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
err = svc.db.Save(&requestEvent).Error
|
||||
err = svc.db.
|
||||
Model(&requestEvent).
|
||||
Update("state", db.REQUEST_EVENT_STATE_HANDLER_ERROR).
|
||||
Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"appPubkey": event.PubKey,
|
||||
|
|
@ -262,13 +272,15 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
return
|
||||
}
|
||||
|
||||
requestEvent.Method = nip47Request.Method
|
||||
requestEvent.ContentData = payload
|
||||
svc.db.Save(&requestEvent) // we ignore potential DB errors here as this only saves the method and content data
|
||||
|
||||
// we ignore potential DB errors here as this only saves the method and content data
|
||||
svc.db.Model(&requestEvent).Updates(map[string]interface{}{
|
||||
"method": nip47Request.Method,
|
||||
"content_data": payload,
|
||||
})
|
||||
// TODO: replace with a channel
|
||||
// TODO: update all previous occurrences of svc.publishResponseEvent to also use the channel
|
||||
publishResponse := func(nip47Response *models.Response, tags nostr.Tags) {
|
||||
var state string
|
||||
resp, err := svc.CreateResponse(event, nip47Response, tags, nip47Cipher, appWalletPrivKey)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
|
|
@ -276,7 +288,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
"eventKind": event.Kind,
|
||||
"appId": app.ID,
|
||||
}).WithError(err).Error("Failed to create response")
|
||||
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
state = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
} else {
|
||||
err = svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
|
||||
if err != nil {
|
||||
|
|
@ -286,18 +298,21 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
"eventKind": event.Kind,
|
||||
"appId": app.ID,
|
||||
}).WithError(err).Error("Failed to publish event")
|
||||
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
state = db.REQUEST_EVENT_STATE_HANDLER_ERROR
|
||||
} else {
|
||||
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_EXECUTED
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"requestEventNostrId": event.ID,
|
||||
"responseEventNostrId": resp.ID,
|
||||
"eventKind": event.Kind,
|
||||
"appId": app.ID,
|
||||
}).Debug("Published response")
|
||||
state = db.REQUEST_EVENT_STATE_HANDLER_EXECUTED
|
||||
}
|
||||
}
|
||||
err = svc.db.Save(&requestEvent).Error
|
||||
err = svc.db.
|
||||
Model(&requestEvent).
|
||||
Update("state", state).
|
||||
Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"appPubkey": event.PubKey,
|
||||
|
|
@ -471,9 +486,10 @@ func (svc *nip47Service) publishResponseEvent(ctx context.Context, relay nostrmo
|
|||
return err
|
||||
}
|
||||
|
||||
updateColumns := make(map[string]interface{})
|
||||
err = relay.Publish(ctx, *resp)
|
||||
if err != nil {
|
||||
responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED
|
||||
updateColumns["state"] = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"requestEventId": requestEvent.ID,
|
||||
"requestNostrEventId": requestEvent.NostrId,
|
||||
|
|
@ -482,8 +498,8 @@ func (svc *nip47Service) publishResponseEvent(ctx context.Context, relay nostrmo
|
|||
"responseNostrEventId": resp.ID,
|
||||
}).WithError(err).Error("Failed to publish reply")
|
||||
} else {
|
||||
responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED
|
||||
responseEvent.RepliedAt = time.Now()
|
||||
updateColumns["state"] = db.RESPONSE_EVENT_STATE_PUBLISH_CONFIRMED
|
||||
updateColumns["replied_at"] = time.Now()
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"requestEventId": requestEvent.ID,
|
||||
"requestNostrEventId": requestEvent.NostrId,
|
||||
|
|
@ -493,7 +509,10 @@ func (svc *nip47Service) publishResponseEvent(ctx context.Context, relay nostrmo
|
|||
}).Info("Published reply")
|
||||
}
|
||||
|
||||
err = svc.db.Save(&responseEvent).Error
|
||||
err = svc.db.
|
||||
Model(&responseEvent).
|
||||
Updates(updateColumns).
|
||||
Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"requestEventId": requestEvent.ID,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package tests
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
|
|
@ -211,3 +212,7 @@ func (mln *MockLn) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeComman
|
|||
func (mln *MockLn) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (mln *MockLn) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
return nil, errors.ErrUnsupported
|
||||
}
|
||||
|
|
|
|||
|
|
@ -998,6 +998,64 @@ func (_c *MockLNClient_ListChannels_Call) RunAndReturn(run func(context.Context)
|
|||
return _c
|
||||
}
|
||||
|
||||
// ListOnchainTransactions provides a mock function with given fields: ctx
|
||||
func (_m *MockLNClient) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListOnchainTransactions")
|
||||
}
|
||||
|
||||
var r0 []lnclient.OnchainTransaction
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]lnclient.OnchainTransaction, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []lnclient.OnchainTransaction); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]lnclient.OnchainTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockLNClient_ListOnchainTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOnchainTransactions'
|
||||
type MockLNClient_ListOnchainTransactions_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListOnchainTransactions is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockLNClient_Expecter) ListOnchainTransactions(ctx interface{}) *MockLNClient_ListOnchainTransactions_Call {
|
||||
return &MockLNClient_ListOnchainTransactions_Call{Call: _e.mock.On("ListOnchainTransactions", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockLNClient_ListOnchainTransactions_Call) Run(run func(ctx context.Context)) *MockLNClient_ListOnchainTransactions_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLNClient_ListOnchainTransactions_Call) Return(_a0 []lnclient.OnchainTransaction, _a1 error) *MockLNClient_ListOnchainTransactions_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLNClient_ListOnchainTransactions_Call) RunAndReturn(run func(context.Context) ([]lnclient.OnchainTransaction, error)) *MockLNClient_ListOnchainTransactions_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListPeers provides a mock function with given fields: ctx
|
||||
func (_m *MockLNClient) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
|
|
|||
|
|
@ -502,19 +502,25 @@ func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHa
|
|||
|
||||
tx := svc.db
|
||||
|
||||
var isIsolatedApp bool
|
||||
if appId != nil {
|
||||
var app db.App
|
||||
result := svc.db.Limit(1).Find(&app, &db.App{
|
||||
ID: *appId,
|
||||
})
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, NewNotFoundError()
|
||||
}
|
||||
if app.Isolated {
|
||||
tx = tx.Where("app_id = ?", *appId)
|
||||
err := svc.db.
|
||||
Model(&db.App{}).
|
||||
Where("id", *appId).
|
||||
Pluck("isolated", &isIsolatedApp).
|
||||
Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, NewNotFoundError()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if isIsolatedApp {
|
||||
tx = tx.Where("app_id = ?", *appId)
|
||||
}
|
||||
|
||||
if transactionType != nil {
|
||||
tx = tx.Where("type = ?", *transactionType)
|
||||
}
|
||||
|
|
@ -549,16 +555,33 @@ func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHa
|
|||
func (svc *transactionsService) 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) {
|
||||
svc.checkUnsettledTransactions(ctx, lnClient)
|
||||
|
||||
var isIsolatedApp bool
|
||||
if appId != nil {
|
||||
err := svc.db.
|
||||
Model(&db.App{}).
|
||||
Where("id", *appId).
|
||||
Pluck("isolated", &isIsolatedApp).
|
||||
Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, 0, NewNotFoundError()
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
tx := svc.db
|
||||
|
||||
if isIsolatedApp || forceFilterByAppId {
|
||||
tx = tx.Where("app_id = ?", *appId)
|
||||
}
|
||||
|
||||
if !unpaidOutgoing && !unpaidIncoming {
|
||||
tx = tx.Where("state = ?", constants.TRANSACTION_STATE_SETTLED)
|
||||
} else if unpaidOutgoing && !unpaidIncoming {
|
||||
tx = tx.Where(tx.Where("state = ?", constants.TRANSACTION_STATE_SETTLED).
|
||||
Or("type = ?", constants.TRANSACTION_TYPE_OUTGOING))
|
||||
tx = tx.Where("state = ? OR type = ?", constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_TYPE_OUTGOING)
|
||||
} else if unpaidIncoming && !unpaidOutgoing {
|
||||
tx = tx.Where(tx.Where("state = ?", constants.TRANSACTION_STATE_SETTLED).
|
||||
Or("type = ?", constants.TRANSACTION_TYPE_INCOMING))
|
||||
tx = tx.Where("state = ? OR type = ?", constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_TYPE_INCOMING)
|
||||
}
|
||||
|
||||
if transactionType != nil {
|
||||
|
|
@ -572,21 +595,6 @@ func (svc *transactionsService) ListTransactions(ctx context.Context, from, unti
|
|||
tx = tx.Where("updated_at <= ?", time.Unix(int64(until), 0))
|
||||
}
|
||||
|
||||
if appId != nil {
|
||||
var app db.App
|
||||
result := svc.db.Limit(1).Find(&app, &db.App{
|
||||
ID: *appId,
|
||||
})
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, 0, NewNotFoundError()
|
||||
}
|
||||
if app.Isolated || forceFilterByAppId {
|
||||
tx = tx.Where("app_id = ?", *appId)
|
||||
}
|
||||
}
|
||||
|
||||
tx = tx.Order("updated_at desc")
|
||||
|
||||
var totalCount64 int64
|
||||
result := tx.Model(&db.Transaction{}).Count(&totalCount64)
|
||||
if result.Error != nil {
|
||||
|
|
@ -595,6 +603,8 @@ func (svc *transactionsService) ListTransactions(ctx context.Context, from, unti
|
|||
}
|
||||
totalCount = uint64(totalCount64)
|
||||
|
||||
tx = tx.Order("updated_at desc")
|
||||
|
||||
if limit > 0 {
|
||||
tx = tx.Limit(int(limit))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -582,6 +582,12 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
return WailsRequestRouterResponse{Body: nil, Error: ""}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: *nodeStatus, Error: ""}
|
||||
case "/api/node/transactions":
|
||||
transactions, err := app.api.ListOnchainTransactions(ctx)
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: transactions, Error: ""}
|
||||
case "/api/info":
|
||||
infoResponse, err := app.api.GetInfo(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue