feat: show pending and failed outgoing transactions (#682)

* feat: show pending and failed outgoing transactions

* fix: use gray color for pending outgoing transactions

* fix: list transactions query and date display of unsettled transactions

* feat: add env variable to log db queries

* chore: add extra list transactions filter tests
This commit is contained in:
Roland 2024-09-20 23:48:33 +07:00 committed by GitHub
parent bfb79a3493
commit e6f1d5b0e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 455 additions and 103 deletions

View file

@ -23,6 +23,7 @@ FRONTEND_URL=http://localhost:5173
#RELAY=ws://localhost:7447/v1
#PORT=8080
#LOG_DB_QUERIES=true
# Alby OAuth configuration
#ALBY_OAUTH_CLIENT_SECRET=

View file

@ -208,6 +208,7 @@ type ListTransactionsResponse = []Transaction
// TODO: camelCase
type Transaction struct {
Type string `json:"type"`
State string `json:"state"`
Invoice string `json:"invoice"`
Description string `json:"description"`
DescriptionHash string `json:"descriptionHash"`

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"github.com/getAlby/hub/logger"
@ -38,7 +39,7 @@ func (api *api) ListTransactions(ctx context.Context, limit uint64, offset uint6
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
transactions, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, false, nil, api.svc.GetLNClient(), nil)
transactions, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, api.svc.GetLNClient(), nil)
if err != nil {
return nil, err
}
@ -99,6 +100,7 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction {
return &Transaction{
Type: transaction.Type,
State: strings.ToLower(transaction.State),
Invoice: transaction.PaymentRequest,
Description: transaction.Description,
DescriptionHash: transaction.DescriptionHash,

View file

@ -41,6 +41,7 @@ type AppConfig struct {
DdProfilerEnabled bool `envconfig:"DD_PROFILER_ENABLED" default:"false"`
EnableAdvancedSetup bool `envconfig:"ENABLE_ADVANCED_SETUP" default:"true"`
AutoUnlockPassword string `envconfig:"AUTO_UNLOCK_PASSWORD"`
LogDBQueries bool `envconfig:"LOG_DB_QUERIES" default:"false"`
}
func (c *AppConfig) IsDefaultClientId() bool {

View file

@ -7,11 +7,18 @@ import (
"github.com/getAlby/hub/logger"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
gorm_logger "gorm.io/gorm/logger"
)
func NewDB(uri string) (*gorm.DB, error) {
func NewDB(uri string, logDBQueries bool) (*gorm.DB, error) {
config := &gorm.Config{}
if logDBQueries {
config.Logger = gorm_logger.Default.LogMode(gorm_logger.Info)
}
// avoid SQLITE_BUSY errors with _txlock=IMMEDIATE
gormDB, err := gorm.Open(sqlite.Open(uri+"?_txlock=IMMEDIATE"), &gorm.Config{})
gormDB, err := gorm.Open(sqlite.Open(uri+"?_txlock=IMMEDIATE"), config)
if err != nil {
return nil, err
}

View file

@ -39,7 +39,7 @@ export default function AppAvatar({ app, className }: Props) {
/>
)}
{!image && (
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-sm font-medium capitalize">
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-sm font-medium capitalize pointer-events-none">
{app.name.charAt(0)}
</span>
)}

View file

@ -7,6 +7,7 @@ import {
ChevronDown,
ChevronUp,
CopyIcon,
XIcon,
} from "lucide-react";
import React from "react";
import { Link } from "react-router-dom";
@ -38,7 +39,20 @@ function TransactionItem({ tx }: Props) {
const { toast } = useToast();
const [showDetails, setShowDetails] = React.useState(false);
const type = tx.type;
const Icon = tx.type == "outgoing" ? ArrowUpIcon : ArrowDownIcon;
const typeStateText =
type == "incoming"
? "Received"
: tx.state === "settled" // we only fetch settled incoming payments
? "Sent"
: 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)
@ -48,6 +62,48 @@ function TransactionItem({ tx }: Props) {
copyToClipboard(text, toast);
};
const typeStateIcon = (
<div className="flex items-center">
<div
className={cn(
"flex justify-center items-center rounded-full w-10 h-10 md:w-14 md:h-14 relative",
tx.state === "failed"
? "bg-red-100 dark:bg-red-950"
: tx.state === "pending"
? "bg-gray-100 dark:bg-gray-950"
: type === "outgoing"
? "bg-orange-100 dark:bg-orange-950"
: "bg-green-100 dark:bg-emerald-950"
)}
>
<Icon
strokeWidth={3}
className={cn(
"w-6 h-6 md:w-8 md:h-8",
tx.state === "failed"
? "stroke-rose-400 dark:stroke-red-600"
: tx.state === "pending"
? "stroke-gray-400 dark:stroke-gray-600"
: type === "outgoing"
? "stroke-orange-400 dark:stroke-amber-600"
: "stroke-green-400 dark:stroke-emerald-500"
)}
/>
{app && (
<div
className="absolute -bottom-1 -right-1"
title={`${typeStateText} via ${app.name === "getalby.com" ? "Alby Account" : app.name}`}
>
<AppAvatar
app={app}
className="border-none p-0 rounded-full w-[18px] h-[18px] md:w-6 md:h-6 shadow-sm"
/>
</div>
)}
</div>
</div>
);
return (
<Dialog
onOpenChange={(open) => {
@ -57,46 +113,21 @@ function TransactionItem({ tx }: Props) {
}}
>
<DialogTrigger className="p-3 mb-4 hover:bg-muted/50 data-[state=selected]:bg-muted cursor-pointer rounded-md slashed-zero transaction sensitive">
<div className="flex gap-3">
<div className="flex items-center">
<div
className={cn(
"flex justify-center items-center rounded-full w-10 h-10 md:w-14 md:h-14 relative",
type === "outgoing"
? "bg-orange-100 dark:bg-orange-950"
: "bg-green-100 dark:bg-emerald-950"
)}
>
<Icon
strokeWidth={3}
className={cn(
"w-6 h-6 md:w-8 md:h-8",
type === "outgoing"
? "stroke-orange-400 dark:stroke-amber-600"
: "stroke-green-400 dark:stroke-emerald-500"
)}
/>
{app && (
<div
className="absolute -bottom-1 -right-1"
title={`${type == "incoming" ? "Received" : "Sent"} via ${app.name === "getalby.com" ? "Alby Account" : app.name}`}
>
<AppAvatar
app={app}
className="border-none p-0 rounded-full w-[18px] h-[18px] md:w-6 md:h-6 shadow-sm"
/>
</div>
)}
</div>
</div>
<div
className={cn(
"flex gap-3",
tx.state === "pending" && "animate-pulse"
)}
>
{typeStateIcon}
<div className="overflow-hidden mr-3 max-w-full text-left flex flex-col items-start justify-center">
<div>
<p className="flex items-center gap-2 truncate">
<span className="md:text-xl font-semibold">
{type == "incoming" ? "Received" : "Sent"}
{typeStateText}
</span>
<span className="text-xs md:text-base truncate text-muted-foreground">
{dayjs(tx.settledAt).fromNow()}
{dayjs(tx.settledAt || tx.createdAt).fromNow()}
</span>
</p>
</div>
@ -132,57 +163,38 @@ function TransactionItem({ tx }: Props) {
</DialogTrigger>
<DialogContent className="slashed-zero">
<DialogHeader>
<DialogTitle>{`${type == "outgoing" ? "Sent" : "Received"} Bitcoin`}</DialogTitle>
<DialogTitle
className={cn(tx.state === "pending" && "animate-pulse")}
>{`${typeStateText} Bitcoin Payment`}</DialogTitle>
<DialogDescription className="text-start text-foreground">
<div className="flex items-center mt-6">
<div
className={cn(
"flex justify-center items-center rounded-full w-10 h-10 md:w-14 md:h-14",
type === "outgoing"
? "bg-orange-100 dark:bg-orange-950"
: "bg-green-100 dark:bg-emerald-950"
)}
>
<Icon
strokeWidth={3}
className={cn(
"w-6 h-6 md:w-8 md:h-8",
type === "outgoing"
? "stroke-orange-400 dark:stroke-amber-600"
: "stroke-green-400 dark:stroke-emerald-500"
)}
/>
</div>
<div
className={cn(
"flex items-center mt-6",
tx.state === "pending" && "animate-pulse"
)}
>
{typeStateIcon}
<div className="ml-4">
<p className="text-xl md:text-2xl font-semibold">
{new Intl.NumberFormat().format(Math.floor(tx.amount / 1000))}{" "}
{Math.floor(tx.amount / 1000) == 1 ? "sat" : "sats"}
</p>
{/* <p className="text-sm md:text-base text-muted-foreground">
Fiat Amount
</p> */}
</div>
</div>
{app && (
<div className="mt-8">
<p>App</p>
<Link to={`/apps/${app.nostrPubkey}`}>
<div className="flex items-center justify-start gap-1 mt-1">
<AppAvatar
app={app}
className="border-none p-0 rounded-full w-6 h-6"
/>
<p className="text-muted-foreground">
{app.name === "getalby.com" ? "Alby Account" : app.name}
</p>
</div>
<p className="font-semibold">
{app.name === "getalby.com" ? "Alby Account" : app.name}
</p>
</Link>
</div>
)}
<div className="mt-6">
<p>Date & Time</p>
<p className="text-muted-foreground">
{dayjs(tx.settledAt)
{dayjs(tx.settledAt || tx.createdAt)
.tz(dayjs.tz.guess())
.format("D MMMM YYYY, HH:mm")}
</p>

View file

@ -1,17 +1,22 @@
import { ArrowUp } from "lucide-react";
import { ArrowUp, InfoIcon } 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";
import { useTransactions } from "src/hooks/useTransactions";
import dayjs from "dayjs";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
import { LinkButton } from "src/components/ui/button";
import { useInfo } from "src/hooks/useInfo";
export default function Send() {
const { hasChannelManagement } = useInfo();
const { data: balances } = useBalances();
const { data: channels } = useChannels();
const { data: transactions } = useTransactions();
if (!balances || !channels) {
return <Loading />;
@ -23,6 +28,26 @@ export default function Send() {
title="Send"
description="Pay a lightning invoice created by any bitcoin lightning wallet"
/>
{transactions?.some(
(tx) =>
tx.state === "pending" &&
dayjs().diff(dayjs(tx.createdAt)) <
1000 * 60 * 60 * 24 /* payment pending in last 24h */
/* TODO: remove diff check when expired transactions are marked as failed */
) && (
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle>Pending Payment</AlertTitle>
<AlertDescription>
<div className="mb-2">
You have one or more payments that have not settled.
</div>
<LinkButton to={"/wallet"} size={"sm"}>
View Payments
</LinkButton>
</AlertDescription>
</Alert>
)}
<div className="flex gap-12 w-full">
<div className="w-full max-w-lg">
<Outlet />

View file

@ -390,6 +390,7 @@ export type BalancesResponse = {
export type Transaction = {
type: "incoming" | "outgoing";
state: "settled" | "pending" | "failed";
appId: number | undefined;
invoice: string;
description: string;

View file

@ -48,7 +48,8 @@ func (controller *nip47Controller) HandleListTransactionsEvent(ctx context.Conte
transactionType = &listParams.Type
}
dbTransactions, err := controller.transactionsService.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid, transactionType, controller.lnClient, &appId)
// TODO: listParams.Unpaid needs to be updated to support ability to fetch only unpaid outgoing transactions
dbTransactions, err := controller.transactionsService.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid, listParams.Unpaid, transactionType, controller.lnClient, &appId)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"params": listParams,

View file

@ -63,7 +63,7 @@ func TestHandleListTransactionsEvent(t *testing.T) {
SettledAt: &settledAt,
State: constants.TRANSACTION_STATE_SETTLED,
AppId: &app.ID,
CreatedAt: time.Now().Add(time.Duration(-i) * time.Hour),
UpdatedAt: time.Now().Add(time.Duration(-i) * time.Hour),
}).Error
assert.NoError(t, err)
}

View file

@ -80,7 +80,7 @@ func NewService(ctx context.Context) (*service, error) {
}
}
gormDB, err := db.NewDB(appConfig.DatabaseUri)
gormDB, err := db.NewDB(appConfig.DatabaseUri, appConfig.LogDBQueries)
if err != nil {
return nil, err
}

View file

@ -17,7 +17,7 @@ import (
const testDB = "test.db"
func CreateTestService() (svc *TestService, err error) {
gormDb, err := db.NewDB(testDB)
gormDb, err := db.NewDB(testDB, true)
if err != nil {
return nil, err
}

View file

@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestListTransactions(t *testing.T) {
func TestListTransactions_Paid(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
@ -35,10 +35,18 @@ func TestListTransactions(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, nil, svc.LNClient, nil)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, false, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(incomingTransactions))
assert.Equal(t, uint64(123000), incomingTransactions[0].AmountMsat)
@ -47,7 +55,7 @@ func TestListTransactions(t *testing.T) {
assert.Zero(t, incomingTransactions[0].FeeReserveMsat)
}
func TestListTransactions_Unsettled(t *testing.T) {
func TestListTransactions_UnpaidIncoming(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
@ -62,6 +70,16 @@ func TestListTransactions_Unsettled(t *testing.T) {
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now(),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_FAILED,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-1 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
@ -70,13 +88,166 @@ func TestListTransactions_Unsettled(t *testing.T) {
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_FAILED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, nil, svc.LNClient, nil)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, true, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 2, len(incomingTransactions))
assert.Equal(t, 3, len(incomingTransactions))
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransactions[0].State)
assert.Equal(t, constants.TRANSACTION_STATE_FAILED, incomingTransactions[1].State)
assert.Equal(t, constants.TRANSACTION_STATE_PENDING, incomingTransactions[2].State)
for _, transaction := range incomingTransactions {
assert.Equal(t, constants.TRANSACTION_TYPE_INCOMING, transaction.Type)
}
}
func TestListTransactions_UnpaidOutgoing(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
mockPreimage := tests.MockLNClientTransaction.Preimage
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now(),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_FAILED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-1 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_FAILED,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
outgoingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, false, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 3, len(outgoingTransactions))
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, outgoingTransactions[0].State)
assert.Equal(t, constants.TRANSACTION_STATE_FAILED, outgoingTransactions[1].State)
assert.Equal(t, constants.TRANSACTION_STATE_PENDING, outgoingTransactions[2].State)
for _, transaction := range outgoingTransactions {
assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type)
}
}
func TestListTransactions_Unpaid(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
mockPreimage := tests.MockLNClientTransaction.Preimage
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now(),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_FAILED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-1 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_FAILED,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
UpdatedAt: time.Now().Add(-2 * time.Second),
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
outgoingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, true, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 5, len(outgoingTransactions))
}
func TestListTransactions_Limit(t *testing.T) {
@ -95,7 +266,7 @@ func TestListTransactions_Limit(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "first",
CreatedAt: time.Now().Add(1 * time.Minute),
UpdatedAt: time.Now().Add(1 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
@ -109,7 +280,7 @@ func TestListTransactions_Limit(t *testing.T) {
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 0, false, nil, svc.LNClient, nil)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 0, false, false, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(incomingTransactions))
assert.Equal(t, "first", incomingTransactions[0].Description)
@ -131,7 +302,7 @@ func TestListTransactions_Offset(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "first",
CreatedAt: time.Now().Add(3 * time.Minute),
UpdatedAt: time.Now().Add(3 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
@ -141,7 +312,7 @@ func TestListTransactions_Offset(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "second",
CreatedAt: time.Now().Add(2 * time.Minute),
UpdatedAt: time.Now().Add(2 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
@ -151,7 +322,7 @@ func TestListTransactions_Offset(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "third",
CreatedAt: time.Now().Add(1 * time.Minute),
UpdatedAt: time.Now().Add(1 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
@ -165,7 +336,7 @@ func TestListTransactions_Offset(t *testing.T) {
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 2, false, nil, svc.LNClient, nil)
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 2, false, false, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(incomingTransactions))
assert.Equal(t, "third", incomingTransactions[0].Description)
@ -187,6 +358,7 @@ func TestListTransactions_FromUntil(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "first",
UpdatedAt: time.Now().Add(10 * time.Minute),
CreatedAt: time.Now().Add(10 * time.Minute),
})
svc.DB.Create(&db.Transaction{
@ -197,6 +369,7 @@ func TestListTransactions_FromUntil(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "second",
UpdatedAt: time.Now().Add(5 * time.Minute),
CreatedAt: time.Now().Add(5 * time.Minute),
})
svc.DB.Create(&db.Transaction{
@ -207,12 +380,138 @@ func TestListTransactions_FromUntil(t *testing.T) {
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "third",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
incomingTransactions, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, nil, svc.LNClient, nil)
incomingTransactions, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, false, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(incomingTransactions))
assert.Equal(t, "second", incomingTransactions[0].Description)
}
func TestListTransactions_FromUntilUnpaidOutgoing(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
mockPreimage := tests.MockLNClientTransaction.Preimage
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "first",
UpdatedAt: time.Now().Add(10 * time.Minute),
CreatedAt: time.Now().Add(10 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "second",
UpdatedAt: time.Now().Add(5 * time.Minute),
CreatedAt: time.Now().Add(5 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "second",
UpdatedAt: time.Now().Add(5 * time.Minute),
CreatedAt: time.Now().Add(5 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "third",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
incomingTransactions, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, true, false, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(incomingTransactions))
assert.Equal(t, "second", incomingTransactions[0].Description)
assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, incomingTransactions[0].Type)
}
func TestListTransactions_FromUntilUnpaidIncoming(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
mockPreimage := tests.MockLNClientTransaction.Preimage
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "first",
UpdatedAt: time.Now().Add(10 * time.Minute),
CreatedAt: time.Now().Add(10 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "second",
UpdatedAt: time.Now().Add(5 * time.Minute),
CreatedAt: time.Now().Add(5 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "second",
UpdatedAt: time.Now().Add(5 * time.Minute),
CreatedAt: time.Now().Add(5 * time.Minute),
})
svc.DB.Create(&db.Transaction{
State: constants.TRANSACTION_STATE_PENDING,
Type: constants.TRANSACTION_TYPE_INCOMING,
PaymentRequest: tests.MockLNClientTransaction.Invoice,
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
Preimage: &mockPreimage,
AmountMsat: 123000,
Description: "third",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
incomingTransactions, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, true, nil, svc.LNClient, nil)
assert.NoError(t, err)
assert.Equal(t, 1, len(incomingTransactions))
assert.Equal(t, "second", incomingTransactions[0].Description)
assert.Equal(t, constants.TRANSACTION_TYPE_INCOMING, incomingTransactions[0].Type)
}

View file

@ -35,7 +35,7 @@ type TransactionsService interface {
events.EventSubscriber
MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error)
ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, transactionType *string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error)
ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, transactionType *string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error)
SendPaymentSync(ctx context.Context, payReq string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
}
@ -474,16 +474,19 @@ func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHa
return &transaction, nil
}
func (svc *transactionsService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, transactionType *string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error) {
func (svc *transactionsService) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, transactionType *string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error) {
svc.checkUnsettledTransactions(ctx, lnClient)
// TODO: add other filtering and pagination
tx := svc.db
tx = tx.Order("settled_at desc, created_at desc")
if !unpaid {
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))
} else if unpaidIncoming && !unpaidOutgoing {
tx = tx.Where(tx.Where("state == ?", constants.TRANSACTION_STATE_SETTLED).
Or("type == ?", constants.TRANSACTION_TYPE_INCOMING))
}
if transactionType != nil {
@ -497,13 +500,6 @@ func (svc *transactionsService) ListTransactions(ctx context.Context, from, unti
tx = tx.Where("created_at <= ?", time.Unix(int64(until), 0))
}
if limit > 0 {
tx = tx.Limit(int(limit))
}
if offset > 0 {
tx = tx.Offset(int(offset))
}
if appId != nil {
var app db.App
result := svc.db.Limit(1).Find(&app, &db.App{
@ -517,9 +513,15 @@ func (svc *transactionsService) ListTransactions(ctx context.Context, from, unti
}
}
if limit != 0 {
tx = tx.Order("updated_at desc")
if limit > 0 {
tx = tx.Limit(int(limit))
}
if offset > 0 {
tx = tx.Offset(int(offset))
}
result := tx.Find(&transactions)
if result.Error != nil {
logger.Logger.WithError(result.Error).Error("Failed to list DB transactions")