- {invoice ? (
+ {transaction ? (
<>
@@ -138,7 +140,7 @@ export default function Receive() {
Waiting for payment
-
+
@@ -155,7 +157,7 @@ export default function Receive() {
className="mt-4 w-full"
onClick={() => {
setPaymentDone(false);
- setInvoice(null);
+ setTransaction(null);
}}
>
Receive Another Payment
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index cd942f40..7b5abcc1 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -10,16 +10,6 @@ import {
WalletMinimal,
} from "lucide-react";
-export const NIP_47_PAY_INVOICE_METHOD = "pay_invoice";
-export const NIP_47_GET_BALANCE_METHOD = "get_balance";
-export const NIP_47_GET_INFO_METHOD = "get_info";
-export const NIP_47_MAKE_INVOICE_METHOD = "make_invoice";
-export const NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice";
-export const NIP_47_LIST_TRANSACTIONS_METHOD = "list_transactions";
-export const NIP_47_SIGN_MESSAGE_METHOD = "sign_message";
-
-export const NIP_47_NOTIFICATIONS_PERMISSION = "notifications";
-
export type BackendType =
| "LND"
| "BREEZ"
@@ -60,19 +50,19 @@ export type Scope =
export type Nip47NotificationType = "payment_received" | "payment_sent";
-export type IconMap = {
+export type ScopeIconMap = {
[key in Scope]: LucideIcon;
};
-export const iconMap: IconMap = {
- [NIP_47_GET_BALANCE_METHOD]: WalletMinimal,
- [NIP_47_GET_INFO_METHOD]: Info,
- [NIP_47_LIST_TRANSACTIONS_METHOD]: NotebookTabs,
- [NIP_47_LOOKUP_INVOICE_METHOD]: Search,
- [NIP_47_MAKE_INVOICE_METHOD]: CirclePlus,
- [NIP_47_PAY_INVOICE_METHOD]: HandCoins,
- [NIP_47_SIGN_MESSAGE_METHOD]: PenLine,
- [NIP_47_NOTIFICATIONS_PERMISSION]: Bell,
+export const scopeIconMap: ScopeIconMap = {
+ get_balance: WalletMinimal,
+ get_info: Info,
+ list_transactions: NotebookTabs,
+ lookup_invoice: Search,
+ make_invoice: CirclePlus,
+ pay_invoice: HandCoins,
+ sign_message: PenLine,
+ notifications: Bell,
};
export type WalletCapabilities = {
@@ -90,14 +80,14 @@ export const validBudgetRenewals: BudgetRenewalType[] = [
];
export const scopeDescriptions: Record = {
- [NIP_47_GET_BALANCE_METHOD]: "Read your balance",
- [NIP_47_GET_INFO_METHOD]: "Read your node info",
- [NIP_47_LIST_TRANSACTIONS_METHOD]: "Read transaction history",
- [NIP_47_LOOKUP_INVOICE_METHOD]: "Lookup status of invoices",
- [NIP_47_MAKE_INVOICE_METHOD]: "Create invoices",
- [NIP_47_PAY_INVOICE_METHOD]: "Send payments",
- [NIP_47_SIGN_MESSAGE_METHOD]: "Sign messages",
- [NIP_47_NOTIFICATIONS_PERMISSION]: "Receive wallet notifications",
+ get_balance: "Read your balance",
+ get_info: "Read your node info",
+ list_transactions: "Read transaction history",
+ lookup_invoice: "Lookup status of invoices",
+ make_invoice: "Create invoices",
+ pay_invoice: "Send payments",
+ sign_message: "Sign messages",
+ notifications: "Receive wallet notifications",
};
export const expiryOptions: Record = {
@@ -109,8 +99,6 @@ export const expiryOptions: Record = {
export const budgetOptions: Record = {
"10k": 10_000,
- "25k": 25_000,
- "50k": 50_000,
"100k": 100_000,
"1M": 1_000_000,
Unlimited: 0,
@@ -130,6 +118,8 @@ export interface App {
updatedAt: string;
lastEventAt?: string;
expiresAt?: string;
+ isolated: boolean;
+ balance: number;
scopes: Scope[];
maxAmount: number;
@@ -138,10 +128,11 @@ export interface App {
}
export interface AppPermissions {
- scopes: Set;
+ scopes: Scope[];
maxAmount: number;
budgetRenewal: BudgetRenewalType;
expiresAt?: Date;
+ isolated: boolean;
}
export interface InfoResponse {
@@ -172,6 +163,7 @@ export interface CreateAppRequest {
expiresAt: string | undefined;
scopes: Scope[];
returnTo: string;
+ isolated: boolean;
}
export interface CreateAppResponse {
@@ -366,18 +358,18 @@ export type BalancesResponse = {
};
export type Transaction = {
- type: string;
+ type: "incoming" | "outgoing";
+ app_id: number | undefined;
invoice: string;
description: string;
description_hash: string;
- preimage: string;
+ preimage: string | undefined;
payment_hash: string;
amount: number;
fees_paid: number;
- created_at: number;
- expires_at: number;
- settled_at: number;
- metadata: string[];
+ created_at: string;
+ settled_at: string | undefined;
+ metadata: unknown;
};
export type NewChannelOrderStatus = "pay" | "success" | "opening";
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index d0a22f51..14264eb0 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -555,6 +555,13 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
+"@radix-ui/react-arrow@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a"
+ integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==
+ dependencies:
+ "@radix-ui/react-primitive" "2.0.0"
+
"@radix-ui/react-avatar@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
@@ -806,6 +813,27 @@
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-visually-hidden" "1.0.3"
+"@radix-ui/react-popover@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.1.tgz#604b783cdb3494ed4f16a58c17f0e81e61ab7775"
+ integrity sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==
+ dependencies:
+ "@radix-ui/primitive" "1.1.0"
+ "@radix-ui/react-compose-refs" "1.1.0"
+ "@radix-ui/react-context" "1.1.0"
+ "@radix-ui/react-dismissable-layer" "1.1.0"
+ "@radix-ui/react-focus-guards" "1.1.0"
+ "@radix-ui/react-focus-scope" "1.1.0"
+ "@radix-ui/react-id" "1.1.0"
+ "@radix-ui/react-popper" "1.2.0"
+ "@radix-ui/react-portal" "1.1.1"
+ "@radix-ui/react-presence" "1.1.0"
+ "@radix-ui/react-primitive" "2.0.0"
+ "@radix-ui/react-slot" "1.1.0"
+ "@radix-ui/react-use-controllable-state" "1.1.0"
+ aria-hidden "^1.1.1"
+ react-remove-scroll "2.5.7"
+
"@radix-ui/react-popper@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
@@ -823,6 +851,22 @@
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/rect" "1.0.1"
+"@radix-ui/react-popper@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a"
+ integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==
+ dependencies:
+ "@floating-ui/react-dom" "^2.0.0"
+ "@radix-ui/react-arrow" "1.1.0"
+ "@radix-ui/react-compose-refs" "1.1.0"
+ "@radix-ui/react-context" "1.1.0"
+ "@radix-ui/react-primitive" "2.0.0"
+ "@radix-ui/react-use-callback-ref" "1.1.0"
+ "@radix-ui/react-use-layout-effect" "1.1.0"
+ "@radix-ui/react-use-rect" "1.1.0"
+ "@radix-ui/react-use-size" "1.1.0"
+ "@radix-ui/rect" "1.1.0"
+
"@radix-ui/react-portal@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
@@ -1068,6 +1112,13 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/rect" "1.0.1"
+"@radix-ui/react-use-rect@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88"
+ integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==
+ dependencies:
+ "@radix-ui/rect" "1.1.0"
+
"@radix-ui/react-use-size@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
@@ -1076,6 +1127,13 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
+"@radix-ui/react-use-size@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b"
+ integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==
+ dependencies:
+ "@radix-ui/react-use-layout-effect" "1.1.0"
+
"@radix-ui/react-visually-hidden@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac"
@@ -1091,6 +1149,11 @@
dependencies:
"@babel/runtime" "^7.13.10"
+"@radix-ui/rect@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438"
+ integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==
+
"@remix-run/router@1.14.0":
version "1.14.0"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.14.0.tgz"
@@ -1782,6 +1845,11 @@ dargs@^8.0.0:
resolved "https://registry.yarnpkg.com/dargs/-/dargs-8.1.0.tgz#a34859ea509cbce45485e5aa356fef70bfcc7272"
integrity sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==
+date-fns@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
+ integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
+
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
@@ -3027,6 +3095,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+react-day-picker@^8.10.1:
+ version "8.10.1"
+ resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.1.tgz#4762ec298865919b93ec09ba69621580835b8e80"
+ integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==
+
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
diff --git a/lnclient/breez/breez.go b/lnclient/breez/breez.go
index 7b6245bd..7ad178f4 100644
--- a/lnclient/breez/breez.go
+++ b/lnclient/breez/breez.go
@@ -23,6 +23,7 @@ import (
type BreezService struct {
listener *BreezListener
svc *breez_sdk.BlockingBreezServices
+ pubkey string
}
type BreezListener struct {
}
@@ -91,6 +92,7 @@ func NewBreezService(mnemonic, apiKey, inviteCode, workDir string) (result lncli
return &BreezService{
listener: &listener,
svc: svc,
+ pubkey: nodeInfo.Id,
}, nil
}
@@ -116,12 +118,12 @@ func (bs *BreezService) SendPaymentSync(ctx context.Context, payReq string) (*ln
}
-func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) {
+func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
extraTlvs := []breez_sdk.TlvEntry{}
for _, record := range custom_records {
decodedValue, err := hex.DecodeString(record.Value)
if err != nil {
- return "", err
+ return "", "", 0, err
}
extraTlvs = append(extraTlvs, breez_sdk.TlvEntry{
FieldNumber: record.Type,
@@ -136,13 +138,13 @@ func (bs *BreezService) SendKeysend(ctx context.Context, amount uint64, destinat
}
resp, err := bs.svc.SendSpontaneousPayment(sendSpontaneousPaymentRequest)
if err != nil {
- return "", err
+ return "", "", 0, err
}
var lnDetails breez_sdk.PaymentDetailsLn
if resp.Payment.Details != nil {
lnDetails, _ = resp.Payment.Details.(breez_sdk.PaymentDetailsLn)
}
- return lnDetails.Data.PaymentPreimage, nil
+ return lnDetails.Data.PaymentHash, lnDetails.Data.PaymentPreimage, resp.Payment.FeeMsat, nil
}
func (bs *BreezService) GetBalance(ctx context.Context) (balance int64, err error) {
@@ -485,3 +487,7 @@ func (bs *BreezService) GetSupportedNIP47Methods() []string {
func (bs *BreezService) GetSupportedNIP47NotificationTypes() []string {
return []string{}
}
+
+func (bs *BreezService) GetPubkey() string {
+ return bs.pubkey
+}
diff --git a/lnclient/cashu/cashu.go b/lnclient/cashu/cashu.go
index d858a59f..206ea379 100644
--- a/lnclient/cashu/cashu.go
+++ b/lnclient/cashu/cashu.go
@@ -89,8 +89,8 @@ func (cs *CashuService) SendPaymentSync(ctx context.Context, invoice string) (re
}, nil
}
-func (cs *CashuService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) {
- return "", errors.New("Keysend not supported")
+func (cs *CashuService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
+ return "", "", 0, errors.New("Keysend not supported")
}
func (cs *CashuService) GetBalance(ctx context.Context) (balance int64, err error) {
@@ -352,3 +352,7 @@ func (cs *CashuService) GetSupportedNIP47Methods() []string {
func (cs *CashuService) GetSupportedNIP47NotificationTypes() []string {
return []string{}
}
+
+func (svc *CashuService) GetPubkey() string {
+ return ""
+}
diff --git a/lnclient/greenlight/greenlight.go b/lnclient/greenlight/greenlight.go
index 93f60fb7..3abcbf84 100644
--- a/lnclient/greenlight/greenlight.go
+++ b/lnclient/greenlight/greenlight.go
@@ -26,6 +26,7 @@ import (
type GreenlightService struct {
workdir string
client *glalby.BlockingGreenlightAlbyClient
+ pubkey string
}
const DEVICE_CREDENTIALS_KEY = "GreenlightCreds"
@@ -85,13 +86,14 @@ func NewGreenlightService(cfg config.Config, mnemonic, inviteCode, workDir, encr
log.Fatalf("unexpected response from NewBlockingGreenlightAlbyClient")
}
+ nodeInfo, err := client.GetInfo()
+
gs := GreenlightService{
workdir: newpath,
client: client,
+ pubkey: nodeInfo.Pubkey,
}
- nodeInfo, err := client.GetInfo()
-
if err != nil {
return nil, err
}
@@ -125,11 +127,12 @@ func (gs *GreenlightService) SendPaymentSync(ctx context.Context, payReq string)
}, nil
}
-func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) {
+func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
extraTlvs := []glalby.TlvEntry{}
for _, customRecord := range custom_records {
+
extraTlvs = append(extraTlvs, glalby.TlvEntry{
Ty: customRecord.Type,
Value: customRecord.Value, // glalby expects hex-encoded TLV values
@@ -145,10 +148,12 @@ func (gs *GreenlightService) SendKeysend(ctx context.Context, amount uint64, des
if err != nil {
logger.Logger.Errorf("Failed to send keysend payment: %v", err)
- return "", err
+ return "", "", 0, err
}
- return response.PaymentPreimage, nil
+ // TODO: get payment hash from response
+
+ return "", response.PaymentPreimage, 0, nil
}
func (gs *GreenlightService) GetBalance(ctx context.Context) (balance int64, err error) {
@@ -203,6 +208,7 @@ func (gs *GreenlightService) MakeInvoice(ctx context.Context, amount int64, desc
Invoice: invoice.Bolt11,
Description: description,
DescriptionHash: descriptionHash,
+ Preimage: "", // TODO: set preimage to enable self-payments
PaymentHash: invoice.PaymentHash,
ExpiresAt: &expiresAt,
Amount: amount,
@@ -682,3 +688,7 @@ func (gs *GreenlightService) GetSupportedNIP47Methods() []string {
func (gs *GreenlightService) GetSupportedNIP47NotificationTypes() []string {
return []string{}
}
+
+func (gs *GreenlightService) GetPubkey() string {
+ return gs.pubkey
+}
diff --git a/lnclient/greenlight/models.go b/lnclient/greenlight/models.go
deleted file mode 100644
index 50e06097..00000000
--- a/lnclient/greenlight/models.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package greenlight
-
-import "github.com/getAlby/hub/lnclient"
-
-type NodeInfo struct {
- ID string `json:"id"`
- Alias string `json:"alias"`
- Color string `json:"color"`
- Network string `json:"network"`
- BlockHeight uint32 `json:"blockheight"`
- // ...other fields
-}
-
-type Invoice struct {
- Bolt11 string `json:"bolt11"`
- PaymentHash string `json:"payment_hash"`
- Preimage string `json:"payment_secret"`
- ExpiresAt int64 `json:"expires_at"`
- PaidAt *int64 `json:"paid_at"`
- Label string `json:"label"`
- Description string `json:"description"`
- AmountMsat MsatValue `json:"amount_msat"`
- AmountReceivedMsat *MsatValue `json:"amount_received_msat"`
- Status int `json:"status"`
- // ...other fields
-}
-
-type Payment struct {
- PaymentHash string `json:"payment_hash"`
- Status int `json:"status"`
- Destination string `json:"destination"`
- CreatedAt int64 `json:"created_at"`
- CompletedAt int64 `json:"completed_at"`
- Bolt11 string `json:"bolt11"`
- AmountMsat MsatValue `json:"amount_msat"`
- AmountSentMsat MsatValue `json:"amount_sent_msat"`
- Preimage string `json:"preimage"`
- // ...other fields
-}
-
-type MsatValue struct {
- Msat int64 `json:"msat"`
-}
-
-type PayResponse struct {
- PaymentHash string `json:"payment_hash"`
- Preimage string `json:"payment_preimage"`
- CreatedAt float64 `json:"created_at"`
- AmountMsat MsatValue `json:"amount_msat"`
- AmountSentMsat MsatValue `json:"amount_sent_msat"`
- // ...other fields
-}
-
-type ListFundsResponse struct {
- Channels []Channel `json:"channels"`
- Outputs []Output `json:"outputs"`
-}
-
-type Output struct {
- TxId string `json:"txid"`
- AmountMsat MsatValue `json:"amount_msat"`
- Address string `json:"address"`
- // ...other fields
-}
-
-type Channel struct {
- PeerId string `json:"peer_id"`
- OurAmountMsat MsatValue `json:"our_amount_msat"`
- AmountMsat MsatValue `json:"amount_msat"`
- FundingTxId string `json:"funding_txid"`
- Id string `json:"channel_id"`
- State int `json:"state"`
-}
-
-type ConnectPeerResponse struct {
- Id string `json:"id"`
- // ...other fields
-}
-
-type Outpoint struct {
- //txid
-}
-
-type OpenChannelResponse struct {
- Tx string `json:"tx"`
- TxId string `json:"txid"`
- ChannelId string `json:"channel_id"`
- // ...other fields
-}
-
-type NewAddressResponse struct {
- Bech32 string `json:"bech32"`
-}
-
-type ListInvoicesResponse struct {
- Invoices []Invoice `json:"invoices"`
-}
-
-type ListPaymentsResponse struct {
- Payments []Payment `json:"pays"`
-}
-
-type GreenlightNodeCredentials struct {
- DeviceCert []byte `json:"deviceCert"`
- DeviceKey []byte `json:"deviceKey"`
- Seed []byte `json:"seed"`
-}
-
-// EXAMPLE ONLY: ideally the rust bindings generate this interface
-type GreenlightRustInterface interface {
- NewGreenlightService() *GreenlightRustService
-}
-
-type GreenlightRustService interface {
- /**
- Attempts to recover or register a new greenlight node and returns the credentials of the node.
- The invite code is only needed for registration.
- */
- CreateCredentials(mnemonic string, inviteCode string) (*GreenlightNodeCredentials, error)
- /**
- Starts the Greenlight node hsmd process
- */
- Start(*GreenlightNodeCredentials) error
-
- /**
- Stops the Greenlight node hsmd process
- */
- Shutdown() error
-
- /* Methods required for Greenlight LNClient - not a 1:1 mapping */
- // TODO: remove usages of lnclient.* (not a 1:1 mapping there)
- SendPaymentSync(payReq string) (preimage string, err error)
- ListChannels() ([]Channel, error)
- ListFunds() (*ListFundsResponse, error)
- MakeInvoice(amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error)
- GetInfo() (info *NodeInfo, err error)
- GetNodeConnectionInfo() (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error)
- ConnectPeer(connectPeerRequest *lnclient.ConnectPeerRequest) error
- OpenChannel(openChannelRequest *lnclient.OpenChannelRequest) (*OpenChannelResponse, error)
- GetNewOnchainAddress() (string, error)
- ListInvoices() ([]Invoice, error) // TODO: add paging params
- ListPayments() ([]Payment, error) // TODO: add paging params
- SendKeysend(amount int64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error)
- // TODO:
- // LookupInvoice(paymentHash string) (transaction *lnclient.Transaction, err error)
- // GetRoute
- // ListClosedChannels
- // ListHTLCs
- // ListPeers
- // SignMessage
- // ?ListOnchainTransactions
-}
diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go
index c1dae670..335c76d9 100644
--- a/lnclient/ldk/ldk.go
+++ b/lnclient/ldk/ldk.go
@@ -42,6 +42,7 @@ type LDKService struct {
lastSync time.Time
cfg config.Config
lastWalletSyncRequest time.Time
+ pubkey string
}
const resetRouterKey = "ResetRouter"
@@ -123,6 +124,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
ldkEventConsumer := make(chan *ldk_node.Event)
ldkCtx, cancel := context.WithCancel(ctx)
ldkEventBroadcaster := NewLDKEventBroadcaster(ldkCtx, ldkEventConsumer)
+ nodeId := node.NodeId()
ls := LDKService{
workdir: newpath,
@@ -132,6 +134,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
network: network,
eventPublisher: eventPublisher,
cfg: cfg,
+ pubkey: nodeId,
}
// TODO: remove when LDK supports this
@@ -179,7 +182,6 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
return nil, err
}
- nodeId := node.NodeId()
logger.Logger.WithFields(logrus.Fields{
"nodeId": nodeId,
"status": node.Status(),
@@ -507,8 +509,10 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc
}
}
if preimage == "" {
- // TODO: this doesn't necessarily mean it will fail - we should return a different response
- return nil, errors.New("Payment timed out")
+ logger.Logger.WithFields(logrus.Fields{
+ "paymentHash": paymentHash,
+ }).Warn("Timed out waiting for payment to be sent")
+ return nil, lnclient.NewTimeoutError()
}
logger.Logger.WithFields(logrus.Fields{
@@ -522,14 +526,14 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc
}, nil
}
-func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) {
+func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
paymentStart := time.Now()
customTlvs := []ldk_node.TlvEntry{}
for _, customRecord := range custom_records {
decodedValue, err := hex.DecodeString(customRecord.Value)
if err != nil {
- return "", err
+ return "", "", 0, err
}
customTlvs = append(customTlvs, ldk_node.TlvEntry{
Type: customRecord.Type,
@@ -540,14 +544,12 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
- paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, customTlvs)
+ paymentHash, err = ls.node.SpontaneousPayment().Send(amount, destination, customTlvs)
if err != nil {
logger.Logger.WithError(err).Error("Keysend failed")
- return "", err
+ return paymentHash, "", 0, err
}
- fee := uint64(0)
-
for start := time.Now(); time.Since(start) < time.Second*60; {
event := <-ldkEventSubscription
@@ -559,7 +561,7 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
payment := ls.node.Payment(paymentHash)
if payment == nil {
logger.Logger.Errorf("Couldn't find payment by payment hash: %v", paymentHash)
- return "", errors.New("Payment not found")
+ return paymentHash, "", 0, errors.New("Payment not found")
}
spontaneousPaymentKind, ok := payment.Kind.(ldk_node.PaymentKindSpontaneous)
@@ -572,7 +574,7 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
if spontaneousPaymentKind.Preimage == nil {
logger.Logger.Errorf("No payment preimage for payment hash: %v", paymentHash)
- return "", errors.New("Payment preimage not found")
+ return paymentHash, "", 0, errors.New("Payment preimage not found")
}
preimage = *spontaneousPaymentKind.Preimage
@@ -610,19 +612,21 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
"failureReasonMessage": failureReasonMessage,
}).Error("Received payment failed event")
- return "", fmt.Errorf("payment failed event: %v %s", failureReason, failureReasonMessage)
+ return paymentHash, "", 0, fmt.Errorf("payment failed event: %v %s", failureReason, failureReasonMessage)
}
}
if preimage == "" {
- // TODO: this doesn't necessarily mean it will fail - we should return a different response
- return "", errors.New("keysend payment timed out")
+ logger.Logger.WithFields(logrus.Fields{
+ "paymentHash": paymentHash,
+ }).Warn("Timed out waiting for keysend to be sent")
+ return paymentHash, "", 0, lnclient.NewTimeoutError()
}
logger.Logger.WithFields(logrus.Fields{
"duration": time.Since(paymentStart).Milliseconds(),
"fee": fee,
}).Info("Successful keysend payment")
- return preimage, nil
+ return paymentHash, preimage, fee, nil
}
func (ls *LDKService) GetBalance(ctx context.Context) (balance int64, err error) {
@@ -704,10 +708,13 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amount int64, description
description = paymentRequest.Description
descriptionHash = paymentRequest.DescriptionHash
+ payment := ls.node.Payment(paymentRequest.PaymentHash)
+
transaction = &lnclient.Transaction{
Type: "incoming",
Invoice: invoice,
PaymentHash: paymentRequest.PaymentHash,
+ Preimage: *payment.Kind.(ldk_node.PaymentKindBolt11).Preimage,
Amount: amount,
CreatedAt: int64(paymentRequest.CreatedAt),
ExpiresAt: expiresAt,
@@ -1299,29 +1306,67 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
},
})
case ldk_node.EventPaymentReceived:
+ if eventType.PaymentId == nil {
+ logger.Logger.WithField("payment_hash", eventType.PaymentHash).Error("payment received event has no payment ID")
+ return
+ }
+ payment := ls.node.Payment(*eventType.PaymentId)
+ if payment == nil {
+ logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment")
+ return
+ }
+
+ transaction, err := ls.ldkPaymentToTransaction(payment)
+ if err != nil {
+ logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("failed to convert LDK payment to transaction")
+ return
+ }
+
ls.eventPublisher.Publish(&events.Event{
- Event: "nwc_payment_received",
- Properties: &events.PaymentReceivedEventProperties{
- PaymentHash: eventType.PaymentHash,
- },
+ Event: "nwc_payment_received",
+ Properties: transaction,
})
case ldk_node.EventPaymentSuccessful:
- var duration uint64 = 0
- if eventType.PaymentId != nil {
- payment := ls.node.Payment(*eventType.PaymentId)
- if payment == nil {
- logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment")
- return
- }
- duration = payment.LastUpdate - payment.CreatedAt
+ if eventType.PaymentId == nil {
+ logger.Logger.WithField("payment_hash", eventType.PaymentHash).Error("payment received event has no payment ID")
+ return
+ }
+ payment := ls.node.Payment(*eventType.PaymentId)
+ if payment == nil {
+ logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment")
+ return
+ }
+
+ transaction, err := ls.ldkPaymentToTransaction(payment)
+ if err != nil {
+ logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("failed to convert LDK payment to transaction")
+ return
}
ls.eventPublisher.Publish(&events.Event{
- Event: "nwc_payment_sent",
- Properties: &events.PaymentSentEventProperties{
- PaymentHash: eventType.PaymentHash,
- Duration: duration,
- },
+ Event: "nwc_payment_sent",
+ Properties: transaction,
+ })
+ case ldk_node.EventPaymentFailed:
+ if eventType.PaymentId == nil {
+ logger.Logger.WithField("payment_hash", eventType.PaymentHash).Error("payment failed event has no payment ID")
+ return
+ }
+ payment := ls.node.Payment(*eventType.PaymentId)
+ if payment == nil {
+ logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment")
+ return
+ }
+
+ transaction, err := ls.ldkPaymentToTransaction(payment)
+ if err != nil {
+ logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("failed to convert LDK payment to transaction")
+ return
+ }
+
+ ls.eventPublisher.Publish(&events.Event{
+ Event: "nwc_payment_failed_async",
+ Properties: transaction,
})
}
}
@@ -1494,3 +1539,7 @@ func (ls *LDKService) getChannelCloseReason(event *ldk_node.EventChannelClosed)
return reason
}
+
+func (ls *LDKService) GetPubkey() string {
+ return ls.pubkey
+}
diff --git a/lnclient/lnd/lnd.go b/lnclient/lnd/lnd.go
index f1125237..a82064c0 100644
--- a/lnclient/lnd/lnd.go
+++ b/lnclient/lnd/lnd.go
@@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
decodepay "github.com/nbd-wtf/ln-decodepay"
+ "github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/lnclient/lnd/wrapper"
"github.com/getAlby/hub/logger"
@@ -24,12 +25,13 @@ import (
// "gorm.io/gorm"
"github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
)
-// wrap it again :sweat_smile:
-// todo: drop dependency on lndhub package
type LNDService struct {
client *wrapper.LNDWrapper
+ pubkey string
+ cancel context.CancelFunc
}
func (svc *LNDService) GetBalance(ctx context.Context) (balance int64, err error) {
@@ -76,47 +78,13 @@ func (svc *LNDService) ListTransactions(ctx context.Context, from, until, limit,
// this will cause retrieved amount to be less than limit
continue
}
- var paymentRequest decodepay.Bolt11
- var expiresAt *int64
- var description string
- var descriptionHash string
- if payment.PaymentRequest != "" {
- paymentRequest, err = decodepay.Decodepay(strings.ToLower(payment.PaymentRequest))
- if err != nil {
- logger.Logger.WithFields(logrus.Fields{
- "bolt11": payment.PaymentRequest,
- }).Errorf("Failed to decode bolt11 invoice: %v", err)
-
- return nil, err
- }
- expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
- expiresAt = &expiresAtUnix
- description = paymentRequest.Description
- descriptionHash = paymentRequest.DescriptionHash
- }
- var settledAt *int64
- if payment.Status == lnrpc.Payment_SUCCEEDED {
- // FIXME: how to get the actual settled at time?
- settledAtUnix := time.Unix(0, payment.CreationTimeNs).Unix()
- settledAt = &settledAtUnix
+ transaction, err := lndPaymentToTransaction(payment)
+ if err != nil {
+ return nil, err
}
- transaction := lnclient.Transaction{
- Type: "outgoing",
- Invoice: payment.PaymentRequest,
- Preimage: payment.PaymentPreimage,
- PaymentHash: payment.PaymentHash,
- Amount: payment.ValueMsat,
- FeesPaid: payment.FeeMsat,
- CreatedAt: time.Unix(0, payment.CreationTimeNs).Unix(),
- Description: description,
- DescriptionHash: descriptionHash,
- ExpiresAt: expiresAt,
- SettledAt: settledAt,
- //TODO: Metadata: (e.g. keysend),
- }
- transactions = append(transactions, transaction)
+ transactions = append(transactions, *transaction)
}
// sort by created date descending
@@ -332,19 +300,16 @@ func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string) (*lnc
}, nil
}
-func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (respPreimage string, err error) {
+func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
destBytes, err := hex.DecodeString(destination)
if err != nil {
- return "", err
+ return "", "", 0, err
}
var preImageBytes []byte
- if preimage == "" {
- preImageBytes, err = makePreimageHex()
- preimage = hex.EncodeToString(preImageBytes)
- } else {
- preImageBytes, err = hex.DecodeString(preimage)
- }
+ preImageBytes, err = makePreimageHex()
+ preimage = hex.EncodeToString(preImageBytes)
+
if err != nil || len(preImageBytes) != 32 {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
@@ -353,19 +318,19 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
"customRecords": custom_records,
"error": err,
}).Errorf("Invalid preimage")
- return "", err
+ return "", "", 0, err
}
- paymentHash := sha256.New()
- paymentHash.Write(preImageBytes)
- paymentHashBytes := paymentHash.Sum(nil)
- paymentHashHex := hex.EncodeToString(paymentHashBytes)
+ paymentHash256 := sha256.New()
+ paymentHash256.Write(preImageBytes)
+ paymentHashBytes := paymentHash256.Sum(nil)
+ paymentHash = hex.EncodeToString(paymentHashBytes)
destCustomRecords := map[uint64][]byte{}
for _, record := range custom_records {
decodedValue, err := hex.DecodeString(record.Value)
if err != nil {
- return "", err
+ return "", "", 0, err
}
destCustomRecords[record.Type] = decodedValue
}
@@ -384,46 +349,46 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"payeePubkey": destination,
- "paymentHash": paymentHashHex,
+ "paymentHash": paymentHash,
"preimage": preimage,
"customRecords": custom_records,
"error": err,
}).Errorf("Failed to send keysend payment")
- return "", err
+ return paymentHash, "", 0, err
}
if resp.PaymentError != "" {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"payeePubkey": destination,
- "paymentHash": paymentHashHex,
+ "paymentHash": paymentHash,
"preimage": preimage,
"customRecords": custom_records,
"paymentError": resp.PaymentError,
}).Errorf("Keysend payment has payment error")
- return "", errors.New(resp.PaymentError)
+ return paymentHash, "", 0, errors.New(resp.PaymentError)
}
- respPreimage = hex.EncodeToString(resp.PaymentPreimage)
- if respPreimage == "" {
+ respPreimage := hex.EncodeToString(resp.PaymentPreimage)
+ if respPreimage != preimage {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"payeePubkey": destination,
- "paymentHash": paymentHashHex,
+ "paymentHash": paymentHash,
"preimage": preimage,
"customRecords": custom_records,
"paymentError": resp.PaymentError,
- }).Errorf("No preimage in keysend response")
- return "", errors.New("no preimage in keysend response")
+ }).Errorf("Preimage in keysend response does not match")
+ return paymentHash, "", 0, errors.New("preimage in keysend response does not match")
}
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
"payeePubkey": destination,
- "paymentHash": paymentHashHex,
+ "paymentHash": paymentHash,
"preimage": preimage,
"customRecords": custom_records,
"respPreimage": respPreimage,
}).Info("Keysend payment successful")
- return respPreimage, nil
+ return paymentHash, respPreimage, uint64(resp.PaymentRoute.TotalFeesMsat), nil
}
func makePreimageHex() ([]byte, error) {
@@ -435,7 +400,7 @@ func makePreimageHex() ([]byte, error) {
return bytes, nil
}
-func NewLNDService(ctx context.Context, lndAddress, lndCertHex, lndMacaroonHex string) (result lnclient.LNClient, err error) {
+func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, lndAddress, lndCertHex, lndMacaroonHex string) (result lnclient.LNClient, err error) {
if lndAddress == "" || lndCertHex == "" || lndMacaroonHex == "" {
return nil, errors.New("one or more required LND configuration are missing")
}
@@ -454,7 +419,93 @@ func NewLNDService(ctx context.Context, lndAddress, lndCertHex, lndMacaroonHex s
return nil, err
}
- lndService := &LNDService{client: lndClient}
+ lndCtx, cancel := context.WithCancel(ctx)
+
+ lndService := &LNDService{client: lndClient, pubkey: info.IdentityPubkey, cancel: cancel}
+
+ // Subscribe to payments
+ go func() {
+ for {
+ paymentStream, err := lndClient.SubscribePayments(lndCtx, &routerrpc.TrackPaymentsRequest{
+ NoInflightUpdates: true,
+ })
+ if err != nil {
+ logger.Logger.WithError(err).Error("Error subscribing to payments")
+ continue
+ }
+ for {
+ select {
+ case <-lndCtx.Done():
+ return
+ default:
+ payment, err := paymentStream.Recv()
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to receive payment")
+ continue
+ }
+
+ var eventName string
+ switch payment.Status {
+ case lnrpc.Payment_FAILED:
+ eventName = "nwc_payment_failed_async"
+ case lnrpc.Payment_SUCCEEDED:
+ eventName = "nwc_payment_sent"
+ default:
+ continue
+ }
+
+ logger.Logger.WithFields(logrus.Fields{
+ "payment": payment,
+ }).Info("Received new payment")
+
+ transaction, err := lndPaymentToTransaction(payment)
+ if err != nil {
+ continue
+ }
+
+ eventPublisher.Publish(&events.Event{
+ Event: eventName,
+ Properties: transaction,
+ })
+ }
+ }
+ }
+ }()
+
+ // Subscribe to invoices
+ go func() {
+ for {
+ invoiceStream, err := lndClient.SubscribeInvoices(lndCtx, &lnrpc.InvoiceSubscription{})
+ if err != nil {
+ logger.Logger.WithError(err).Error("Error subscribing to invoices")
+ continue
+ }
+ for {
+ select {
+ case <-lndCtx.Done():
+ return
+ default:
+ invoice, err := invoiceStream.Recv()
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to receive invoice")
+ continue
+ }
+ if invoice.State != lnrpc.Invoice_SETTLED {
+ continue
+ }
+
+ logger.Logger.WithFields(logrus.Fields{
+ "invoice": invoice,
+ }).Info("Received new invoice")
+
+ eventPublisher.Publish(&events.Event{
+ Event: "nwc_payment_received",
+ Properties: lndInvoiceToTransaction(invoice),
+ })
+ }
+ }
+ }
+ }()
logger.Logger.Infof("Connected to LND - alias %s", info.Alias)
@@ -462,6 +513,8 @@ func NewLNDService(ctx context.Context, lndAddress, lndCertHex, lndMacaroonHex s
}
func (svc *LNDService) Shutdown() error {
+ logger.Logger.Info("cancelling LND context")
+ svc.cancel()
return nil
}
@@ -699,14 +752,54 @@ func (svc *LNDService) GetBalances(ctx context.Context) (*lnclient.BalancesRespo
}, nil
}
+func lndPaymentToTransaction(payment *lnrpc.Payment) (*lnclient.Transaction, error) {
+ var expiresAt *int64
+ var description string
+ var descriptionHash string
+ if payment.PaymentRequest != "" {
+ paymentRequest, err := decodepay.Decodepay(strings.ToLower(payment.PaymentRequest))
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payment.PaymentRequest,
+ }).Errorf("Failed to decode bolt11 invoice: %v", err)
+ return nil, err
+ }
+ expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
+ expiresAt = &expiresAtUnix
+ description = paymentRequest.Description
+ descriptionHash = paymentRequest.DescriptionHash
+ }
+
+ var settledAt *int64
+ if payment.Status == lnrpc.Payment_SUCCEEDED {
+ // FIXME: how to get the actual settled at time?
+ settledAtUnix := time.Unix(0, payment.CreationTimeNs).Unix()
+ settledAt = &settledAtUnix
+ }
+
+ return &lnclient.Transaction{
+ Type: "outgoing",
+ Invoice: payment.PaymentRequest,
+ Preimage: payment.PaymentPreimage,
+ PaymentHash: payment.PaymentHash,
+ Amount: payment.ValueMsat,
+ FeesPaid: payment.FeeMsat,
+ CreatedAt: time.Unix(0, payment.CreationTimeNs).Unix(),
+ Description: description,
+ DescriptionHash: descriptionHash,
+ ExpiresAt: expiresAt,
+ SettledAt: settledAt,
+ //TODO: Metadata: (e.g. keysend),
+ }, nil
+}
+
func lndInvoiceToTransaction(invoice *lnrpc.Invoice) *lnclient.Transaction {
var settledAt *int64
- var preimage string
+ preimage := hex.EncodeToString(invoice.RPreimage)
metadata := map[string]interface{}{}
+
if invoice.State == lnrpc.Invoice_SETTLED {
settledAt = &invoice.SettleDate
- // only set preimage if invoice is settled
- preimage = hex.EncodeToString(invoice.RPreimage)
}
var expiresAt *int64
if invoice.Expiry > 0 {
@@ -817,5 +910,9 @@ func (svc *LNDService) GetSupportedNIP47Methods() []string {
}
func (svc *LNDService) GetSupportedNIP47NotificationTypes() []string {
- return []string{}
+ return []string{"payment_received", "payment_sent"}
+}
+
+func (svc *LNDService) GetPubkey() string {
+ return svc.pubkey
}
diff --git a/lnclient/lnd/wrapper/lnd.go b/lnclient/lnd/wrapper/lnd.go
index b2d2e05b..ae1162d8 100644
--- a/lnclient/lnd/wrapper/lnd.go
+++ b/lnclient/lnd/wrapper/lnd.go
@@ -115,6 +115,10 @@ func (wrapper *LNDWrapper) SubscribeInvoices(ctx context.Context, req *lnrpc.Inv
return wrapper.client.SubscribeInvoices(ctx, req, options...)
}
+func (wrapper *LNDWrapper) SubscribePayments(ctx context.Context, req *routerrpc.TrackPaymentsRequest, options ...grpc.CallOption) (routerrpc.Router_TrackPaymentsClient, error) {
+ return wrapper.routerClient.TrackPayments(ctx, req, options...)
+}
+
func (wrapper *LNDWrapper) ListInvoices(ctx context.Context, req *lnrpc.ListInvoiceRequest, options ...grpc.CallOption) (*lnrpc.ListInvoiceResponse, error) {
return wrapper.client.ListInvoices(ctx, req, options...)
}
diff --git a/lnclient/models.go b/lnclient/models.go
index 6be72091..caf864b5 100644
--- a/lnclient/models.go
+++ b/lnclient/models.go
@@ -4,6 +4,8 @@ import (
"context"
)
+// TODO: remove JSON tags from these models (LNClient models should not be exposed directly)
+
type TLVRecord struct {
Type uint64 `json:"type"`
// hex-encoded value
@@ -21,18 +23,18 @@ type NodeInfo struct {
// TODO: use uint for fields that cannot be negative
type Transaction struct {
- Type string `json:"type"`
- Invoice string `json:"invoice"`
- Description string `json:"description"`
- DescriptionHash string `json:"description_hash"`
- Preimage string `json:"preimage"`
- PaymentHash string `json:"payment_hash"`
- Amount int64 `json:"amount"`
- FeesPaid int64 `json:"fees_paid"`
- CreatedAt int64 `json:"created_at"`
- ExpiresAt *int64 `json:"expires_at"`
- SettledAt *int64 `json:"settled_at"`
- Metadata interface{} `json:"metadata,omitempty"`
+ Type string
+ Invoice string
+ Description string
+ DescriptionHash string
+ Preimage string
+ PaymentHash string
+ Amount int64
+ FeesPaid int64
+ CreatedAt int64
+ ExpiresAt *int64
+ SettledAt *int64
+ Metadata interface{}
}
type NodeConnectionInfo struct {
@@ -43,8 +45,9 @@ type NodeConnectionInfo struct {
type LNClient interface {
SendPaymentSync(ctx context.Context, payReq string) (*PayInvoiceResponse, error)
- SendKeysend(ctx context.Context, amount uint64, destination, preimage string, customRecords []TLVRecord) (preImage string, err error)
+ SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []TLVRecord) (paymentHash string, preimage string, fee uint64, err error)
GetBalance(ctx context.Context) (balance int64, err error)
+ GetPubkey() string
GetInfo(ctx context.Context) (info *NodeInfo, err error)
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)
@@ -163,3 +166,14 @@ type NetworkGraphResponse = interface{}
// default invoice expiry in seconds (1 day)
const DEFAULT_INVOICE_EXPIRY = 86400
+
+type timeoutError struct {
+}
+
+func NewTimeoutError() error {
+ return &timeoutError{}
+}
+
+func (err *timeoutError) Error() string {
+ return "Timeout"
+}
diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go
index b40f5a1c..abaa49ed 100644
--- a/lnclient/phoenixd/phoenixd.go
+++ b/lnclient/phoenixd/phoenixd.go
@@ -68,12 +68,19 @@ type BalanceResponse struct {
type PhoenixService struct {
Address string
Authorization string
+ pubkey string
}
func NewPhoenixService(address string, authorization string) (result lnclient.LNClient, err error) {
authorizationBase64 := b64.StdEncoding.EncodeToString([]byte(":" + authorization))
phoenixService := &PhoenixService{Address: address, Authorization: authorizationBase64}
+ info, err := phoenixService.GetInfo(context.Background())
+ if err != nil {
+ return nil, err
+ }
+ phoenixService.pubkey = info.Pubkey
+
return phoenixService, nil
}
@@ -328,7 +335,7 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri
tx := &lnclient.Transaction{
Type: "incoming",
Invoice: invoiceRes.Serialized,
- Preimage: "",
+ Preimage: "", // TODO: set preimage to enable self-payments
PaymentHash: invoiceRes.PaymentHash,
FeesPaid: 0,
CreatedAt: time.Now().Unix(),
@@ -420,8 +427,8 @@ func (svc *PhoenixService) SendPaymentSync(ctx context.Context, payReq string) (
}, nil
}
-func (svc *PhoenixService) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (respPreimage string, err error) {
- return "", errors.New("not implemented")
+func (svc *PhoenixService) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
+ return "", "", 0, errors.New("not implemented")
}
func (svc *PhoenixService) RedeemOnchainFunds(ctx context.Context, toAddress string) (txId string, err error) {
@@ -526,3 +533,7 @@ func (svc *PhoenixService) GetSupportedNIP47Methods() []string {
func (svc *PhoenixService) GetSupportedNIP47NotificationTypes() []string {
return []string{}
}
+
+func (svc *PhoenixService) GetPubkey() string {
+ return svc.pubkey
+}
diff --git a/nip47/controllers/get_balance_controller.go b/nip47/controllers/get_balance_controller.go
index d1c3172b..7ffe42ba 100644
--- a/nip47/controllers/get_balance_controller.go
+++ b/nip47/controllers/get_balance_controller.go
@@ -3,7 +3,8 @@ package controllers
import (
"context"
- "github.com/getAlby/hub/lnclient"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/db/queries"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
@@ -15,47 +16,37 @@ const (
)
type getBalanceResponse struct {
- Balance int64 `json:"balance"`
+ Balance uint64 `json:"balance"`
// MaxAmount int `json:"max_amount"`
// BudgetRenewal string `json:"budget_renewal"`
}
-type getBalanceController struct {
- lnClient lnclient.LNClient
-}
-
-func NewGetBalanceController(lnClient lnclient.LNClient) *getBalanceController {
- return &getBalanceController{
- lnClient: lnClient,
- }
-}
-
// TODO: remove checkPermission - can it be a middleware?
-func (controller *getBalanceController) HandleGetBalanceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) {
- // basic permissions check
- resp := checkPermission(0)
- if resp != nil {
- publishResponse(resp, nostr.Tags{})
- return
- }
+func (controller *nip47Controller) HandleGetBalanceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc) {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
}).Info("Getting balance")
- balance, err := controller.lnClient.GetBalance(ctx)
- if err != nil {
- logger.Logger.WithFields(logrus.Fields{
- "request_event_id": requestEventId,
- }).WithError(err).Error("Failed to fetch balance")
- publishResponse(&models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_INTERNAL,
- Message: err.Error(),
- },
- }, nostr.Tags{})
- return
+ balance := uint64(0)
+ if app.Isolated {
+ balance = queries.GetIsolatedBalance(controller.db, app.ID)
+ } else {
+ balance_signed, err := controller.lnClient.GetBalance(ctx)
+ balance = uint64(balance_signed)
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "request_event_id": requestEventId,
+ }).WithError(err).Error("Failed to fetch balance")
+ publishResponse(&models.Response{
+ ResultType: nip47Request.Method,
+ Error: &models.Error{
+ Code: models.ERROR_INTERNAL,
+ Message: err.Error(),
+ },
+ }, nostr.Tags{})
+ return
+ }
}
responsePayload := &getBalanceResponse{
diff --git a/nip47/controllers/get_balance_controller_test.go b/nip47/controllers/get_balance_controller_test.go
index 65bd3378..ab8903ba 100644
--- a/nip47/controllers/get_balance_controller_test.go
+++ b/nip47/controllers/get_balance_controller_test.go
@@ -8,9 +8,12 @@ import (
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47GetBalanceJson = `
@@ -19,7 +22,7 @@ const nip47GetBalanceJson = `
}
`
-func TestHandleGetBalanceEvent_NoPermission(t *testing.T) {
+func TestHandleGetBalanceEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -29,33 +32,29 @@ func TestHandleGetBalanceEvent_NoPermission(t *testing.T) {
err = json.Unmarshal([]byte(nip47GetBalanceJson), nip47Request)
assert.NoError(t, err)
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
- NewGetBalanceController(svc.LNClient).
- HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
- assert.Nil(t, publishedResponse.Result)
- assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code)
+ assert.Equal(t, uint64(21000), publishedResponse.Result.(*getBalanceResponse).Balance)
+ assert.Nil(t, publishedResponse.Error)
}
-func TestHandleGetBalanceEvent_WithPermission(t *testing.T) {
+func TestHandleGetBalanceEvent_IsolatedApp_NoTransactions(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -65,45 +64,73 @@ func TestHandleGetBalanceEvent_WithPermission(t *testing.T) {
err = json.Unmarshal([]byte(nip47GetBalanceJson), nip47Request)
assert.NoError(t, err)
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
+ var publishedResponse *models.Response
+
+ publishResponse := func(response *models.Response, tags nostr.Tags) {
+ publishedResponse = response
}
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
+
+ assert.Equal(t, uint64(0), publishedResponse.Result.(*getBalanceResponse).Balance)
+ assert.Nil(t, publishedResponse.Error)
+}
+func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) {
+ ctx := context.TODO()
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ nip47Request := &models.Request{}
+ err = json.Unmarshal([]byte(nip47GetBalanceJson), nip47Request)
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 1000,
+ })
+ // create an unrelated transaction, should not count
+ svc.DB.Create(&db.Transaction{
+ AppId: nil,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 1000,
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
- NewGetBalanceController(svc.LNClient).
- HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
- assert.Equal(t, int64(21000), publishedResponse.Result.(*getBalanceResponse).Balance)
+ assert.Equal(t, uint64(1000), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Nil(t, publishedResponse.Error)
}
-
-// create pay_invoice permission
-// maxAmount := 1000
-// budgetRenewal := "never"
-// appPermission = &db.AppPermission{
-// AppId: app.ID,
-// App: *app,
-// RequestMethod: models.PAY_INVOICE_METHOD,
-// MaxAmount: maxAmount,
-// BudgetRenewal: budgetRenewal,
-// ExpiresAt: &expiresAt,
-// }
-// err = svc.DB.Create(appPermission).Error
-// assert.NoError(t, err)
-
-// reqEvent.ID = "test_get_balance_with_budget"
-// responses = []*models.Response{}
-// svc.nip47Svc.HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent, app, publishResponse)
-
-// assert.Equal(t, int64(21000), responses[0].Result.(*getBalanceResponse).Balance)
-// assert.Equal(t, 1000000, responses[0].Result.(*getBalanceResponse).MaxAmount)
-// assert.Equal(t, "never", responses[0].Result.(*getBalanceResponse).BudgetRenewal)
diff --git a/nip47/controllers/get_info_controller.go b/nip47/controllers/get_info_controller.go
index 7e4e69ea..eaa9ced6 100644
--- a/nip47/controllers/get_info_controller.go
+++ b/nip47/controllers/get_info_controller.go
@@ -3,11 +3,10 @@ package controllers
import (
"context"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
- permissions "github.com/getAlby/hub/nip47/permissions"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
)
@@ -23,19 +22,7 @@ type getInfoResponse struct {
Notifications []string `json:"notifications"`
}
-type getInfoController struct {
- lnClient lnclient.LNClient
- permissionsService permissions.PermissionsService
-}
-
-func NewGetInfoController(permissionsService permissions.PermissionsService, lnClient lnclient.LNClient) *getInfoController {
- return &getInfoController{
- permissionsService: permissionsService,
- lnClient: lnClient,
- }
-}
-
-func (controller *getInfoController) HandleGetInfoEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc) {
+func (controller *nip47Controller) HandleGetInfoEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc) {
supportedNotifications := []string{}
if controller.permissionsService.PermitsNotifications(app) {
supportedNotifications = controller.lnClient.GetSupportedNIP47NotificationTypes()
@@ -47,7 +34,7 @@ func (controller *getInfoController) HandleGetInfoEvent(ctx context.Context, nip
}
// basic permissions check
- hasPermission, _, _ := controller.permissionsService.HasPermission(app, permissions.GET_INFO_SCOPE, 0)
+ hasPermission, _, _ := controller.permissionsService.HasPermission(app, constants.GET_INFO_SCOPE)
if hasPermission {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
diff --git a/nip47/controllers/get_info_controller_test.go b/nip47/controllers/get_info_controller_test.go
index 26c66759..5b8482da 100644
--- a/nip47/controllers/get_info_controller_test.go
+++ b/nip47/controllers/get_info_controller_test.go
@@ -8,10 +8,12 @@ import (
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47GetInfoJson = `
@@ -20,7 +22,6 @@ const nip47GetInfoJson = `
}
`
-// TODO: info event should always return something
func TestHandleGetInfoEvent_NoPermission(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
@@ -40,21 +41,12 @@ func TestHandleGetInfoEvent_NoPermission(t *testing.T) {
appPermission := &db.AppPermission{
AppId: app.ID,
- Scope: permissions.GET_BALANCE_SCOPE,
+ Scope: constants.GET_BALANCE_SCOPE,
ExpiresAt: nil,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
@@ -62,9 +54,9 @@ func TestHandleGetInfoEvent_NoPermission(t *testing.T) {
}
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
-
- NewGetInfoController(permissionsSvc, svc.LNClient).
- HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Nil(t, publishedResponse.Error)
nodeInfo := publishedResponse.Result.(*getInfoResponse)
@@ -97,16 +89,12 @@ func TestHandleGetInfoEvent_WithPermission(t *testing.T) {
appPermission := &db.AppPermission{
AppId: app.ID,
- Scope: permissions.GET_INFO_SCOPE,
+ Scope: constants.GET_INFO_SCOPE,
ExpiresAt: nil,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
@@ -114,9 +102,9 @@ func TestHandleGetInfoEvent_WithPermission(t *testing.T) {
}
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
-
- NewGetInfoController(permissionsSvc, svc.LNClient).
- HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Nil(t, publishedResponse.Error)
nodeInfo := publishedResponse.Result.(*getInfoResponse)
@@ -149,25 +137,20 @@ func TestHandleGetInfoEvent_WithNotifications(t *testing.T) {
appPermission := &db.AppPermission{
AppId: app.ID,
- Scope: permissions.GET_INFO_SCOPE,
+ Scope: constants.GET_INFO_SCOPE,
ExpiresAt: nil,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
- // TODO: AppPermission RequestMethod needs to change to scope
appPermission = &db.AppPermission{
AppId: app.ID,
- Scope: permissions.NOTIFICATIONS_SCOPE,
+ Scope: constants.NOTIFICATIONS_SCOPE,
ExpiresAt: nil,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
@@ -175,9 +158,9 @@ func TestHandleGetInfoEvent_WithNotifications(t *testing.T) {
}
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
-
- NewGetInfoController(permissionsSvc, svc.LNClient).
- HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Nil(t, publishedResponse.Error)
nodeInfo := publishedResponse.Result.(*getInfoResponse)
diff --git a/nip47/controllers/list_transactions_controller.go b/nip47/controllers/list_transactions_controller.go
index 914e28d1..53666330 100644
--- a/nip47/controllers/list_transactions_controller.go
+++ b/nip47/controllers/list_transactions_controller.go
@@ -3,7 +3,6 @@ package controllers
import (
"context"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
@@ -23,26 +22,10 @@ type listTransactionsResponse struct {
Transactions []models.Transaction `json:"transactions"`
}
-type listTransactionsController struct {
- lnClient lnclient.LNClient
-}
-
-func NewListTransactionsController(lnClient lnclient.LNClient) *listTransactionsController {
- return &listTransactionsController{
- lnClient: lnClient,
- }
-}
-
-func (controller *listTransactionsController) HandleListTransactionsEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) {
- // basic permissions check
- resp := checkPermission(0)
- if resp != nil {
- publishResponse(resp, nostr.Tags{})
- return
- }
+func (controller *nip47Controller) HandleListTransactionsEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, appId uint, publishResponse publishFunc) {
listParams := &listTransactionsParams{}
- resp = decodeRequest(nip47Request, listParams)
+ resp := decodeRequest(nip47Request, listParams)
if resp != nil {
publishResponse(resp, nostr.Tags{})
return
@@ -59,7 +42,12 @@ func (controller *listTransactionsController) HandleListTransactionsEvent(ctx co
// make sure a sensible limit is passed
limit = maxLimit
}
- transactions, err := controller.lnClient.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid, listParams.Type)
+ var transactionType *string
+ if listParams.Type != "" {
+ transactionType = &listParams.Type
+ }
+
+ dbTransactions, err := controller.transactionsService.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid, transactionType, controller.lnClient, &appId)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"params": listParams,
@@ -76,6 +64,11 @@ func (controller *listTransactionsController) HandleListTransactionsEvent(ctx co
return
}
+ transactions := []models.Transaction{}
+ for _, dbTransaction := range dbTransactions {
+ transactions = append(transactions, *models.ToNip47Transaction(&dbTransaction))
+ }
+
responsePayload := &listTransactionsResponse{
Transactions: transactions,
}
diff --git a/nip47/controllers/list_transactions_controller_test.go b/nip47/controllers/list_transactions_controller_test.go
index dfb61857..0788d51a 100644
--- a/nip47/controllers/list_transactions_controller_test.go
+++ b/nip47/controllers/list_transactions_controller_test.go
@@ -4,21 +4,25 @@ import (
"context"
"encoding/json"
"testing"
+ "time"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47ListTransactionsJson = `
{
"method": "list_transactions",
"params": {
- "from": 1693876973,
- "until": 1694876973,
+ "from": 0,
+ "until": 0,
"limit": 10,
"offset": 0,
"type": "incoming"
@@ -26,7 +30,7 @@ const nip47ListTransactionsJson = `
}
`
-func TestHandleListTransactionsEvent_NoPermission(t *testing.T) {
+func TestHandleListTransactionsEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -36,48 +40,33 @@ func TestHandleListTransactionsEvent_NoPermission(t *testing.T) {
err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request)
assert.NoError(t, err)
- dbRequestEvent := &db.RequestEvent{}
- err = svc.DB.Create(&dbRequestEvent).Error
+ app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
+ dbRequestEvent := &db.RequestEvent{
+ AppId: &app.ID,
}
-
- var publishedResponse *models.Response
-
- publishResponse := func(response *models.Response, tags nostr.Tags) {
- publishedResponse = response
- }
-
- NewListTransactionsController(svc.LNClient).
- HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
-
- assert.Nil(t, publishedResponse.Result)
- assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code)
-}
-
-func TestHandleListTransactionsEvent_WithPermission(t *testing.T) {
- ctx := context.TODO()
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
-
- nip47Request := &models.Request{}
- err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request)
- assert.NoError(t, err)
-
- dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
+ for i, _ := range tests.MockLNClientTransactions {
+ feesPaid := uint64(tests.MockLNClientTransactions[i].FeesPaid)
+ settledAt := time.Unix(*tests.MockLNClientTransactions[i].SettledAt, 0)
+ err = svc.DB.Create(&db.Transaction{
+ Type: tests.MockLNClientTransactions[i].Type,
+ PaymentRequest: tests.MockLNClientTransactions[i].Invoice,
+ Description: tests.MockLNClientTransactions[i].Description,
+ DescriptionHash: tests.MockLNClientTransactions[i].DescriptionHash,
+ Preimage: &tests.MockLNClientTransactions[i].Preimage,
+ PaymentHash: tests.MockLNClientTransactions[i].PaymentHash,
+ AmountMsat: uint64(tests.MockLNClientTransactions[i].Amount),
+ FeeMsat: &feesPaid,
+ SettledAt: &settledAt,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ AppId: &app.ID,
+ CreatedAt: time.Now().Add(time.Duration(-i) * time.Hour),
+ }).Error
+ assert.NoError(t, err)
}
var publishedResponse *models.Response
@@ -86,20 +75,24 @@ func TestHandleListTransactionsEvent_WithPermission(t *testing.T) {
publishedResponse = response
}
- NewListTransactionsController(svc.LNClient).
- HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
assert.Nil(t, publishedResponse.Error)
assert.Equal(t, 2, len(publishedResponse.Result.(*listTransactionsResponse).Transactions))
transaction := publishedResponse.Result.(*listTransactionsResponse).Transactions[0]
- assert.Equal(t, tests.MockTransactions[0].Type, transaction.Type)
- assert.Equal(t, tests.MockTransactions[0].Invoice, transaction.Invoice)
- assert.Equal(t, tests.MockTransactions[0].Description, transaction.Description)
- assert.Equal(t, tests.MockTransactions[0].DescriptionHash, transaction.DescriptionHash)
- assert.Equal(t, tests.MockTransactions[0].Preimage, transaction.Preimage)
- assert.Equal(t, tests.MockTransactions[0].PaymentHash, transaction.PaymentHash)
- assert.Equal(t, tests.MockTransactions[0].Amount, transaction.Amount)
- assert.Equal(t, tests.MockTransactions[0].FeesPaid, transaction.FeesPaid)
- assert.Equal(t, tests.MockTransactions[0].SettledAt, transaction.SettledAt)
+ assert.Equal(t, tests.MockLNClientTransactions[0].Type, transaction.Type)
+ assert.Equal(t, tests.MockLNClientTransactions[0].Invoice, transaction.Invoice)
+ assert.Equal(t, tests.MockLNClientTransactions[0].Description, transaction.Description)
+ assert.Equal(t, tests.MockLNClientTransactions[0].DescriptionHash, transaction.DescriptionHash)
+ assert.Equal(t, tests.MockLNClientTransactions[0].Preimage, transaction.Preimage)
+ assert.Equal(t, tests.MockLNClientTransactions[0].PaymentHash, transaction.PaymentHash)
+ assert.Equal(t, tests.MockLNClientTransactions[0].Amount, transaction.Amount)
+ assert.Equal(t, tests.MockLNClientTransactions[0].FeesPaid, transaction.FeesPaid)
+ assert.Equal(t, tests.MockLNClientTransactions[0].SettledAt, transaction.SettledAt)
}
+
+// TODO: add tests for pagination args
diff --git a/nip47/controllers/lookup_invoice_controller.go b/nip47/controllers/lookup_invoice_controller.go
index de932438..44afb7b5 100644
--- a/nip47/controllers/lookup_invoice_controller.go
+++ b/nip47/controllers/lookup_invoice_controller.go
@@ -5,7 +5,6 @@ import (
"fmt"
"strings"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
@@ -22,26 +21,10 @@ type lookupInvoiceResponse struct {
models.Transaction
}
-type lookupInvoiceController struct {
- lnClient lnclient.LNClient
-}
-
-func NewLookupInvoiceController(lnClient lnclient.LNClient) *lookupInvoiceController {
- return &lookupInvoiceController{
- lnClient: lnClient,
- }
-}
-
-func (controller *lookupInvoiceController) HandleLookupInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) {
- // basic permissions check
- resp := checkPermission(0)
- if resp != nil {
- publishResponse(resp, nostr.Tags{})
- return
- }
+func (controller *nip47Controller) HandleLookupInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, appId uint, publishResponse publishFunc) {
lookupInvoiceParams := &lookupInvoiceParams{}
- resp = decodeRequest(nip47Request, lookupInvoiceParams)
+ resp := decodeRequest(nip47Request, lookupInvoiceParams)
if resp != nil {
publishResponse(resp, nostr.Tags{})
return
@@ -75,7 +58,7 @@ func (controller *lookupInvoiceController) HandleLookupInvoiceEvent(ctx context.
paymentHash = paymentRequest.PaymentHash
}
- transaction, err := controller.lnClient.LookupInvoice(ctx, paymentHash)
+ dbTransaction, err := controller.transactionsService.LookupTransaction(ctx, paymentHash, nil, controller.lnClient, &appId)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
@@ -85,16 +68,13 @@ func (controller *lookupInvoiceController) HandleLookupInvoiceEvent(ctx context.
publishResponse(&models.Response{
ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_INTERNAL,
- Message: err.Error(),
- },
+ Error: mapNip47Error(err),
}, nostr.Tags{})
return
}
responsePayload := &lookupInvoiceResponse{
- Transaction: *transaction,
+ Transaction: *models.ToNip47Transaction(dbTransaction),
}
publishResponse(&models.Response{
diff --git a/nip47/controllers/lookup_invoice_controller_test.go b/nip47/controllers/lookup_invoice_controller_test.go
index 9d8b396a..5cb46a5b 100644
--- a/nip47/controllers/lookup_invoice_controller_test.go
+++ b/nip47/controllers/lookup_invoice_controller_test.go
@@ -4,25 +4,28 @@ import (
"context"
"encoding/json"
"testing"
+ "time"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
-const nip47LookupInvoiceJson = `
+var nip47LookupInvoiceJson = `
{
"method": "lookup_invoice",
"params": {
- "payment_hash": "4ad9cd27989b514d868e755178378019903a8d78767e3fceb211af9dd00e7a94"
+ "payment_hash": "` + tests.MockLNClientTransaction.PaymentHash + `"
}
}
`
-func TestHandleLookupInvoiceEvent_NoPermission(t *testing.T) {
+func TestHandleLookupInvoiceEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -32,49 +35,30 @@ func TestHandleLookupInvoiceEvent_NoPermission(t *testing.T) {
err = json.Unmarshal([]byte(nip47LookupInvoiceJson), nip47Request)
assert.NoError(t, err)
- dbRequestEvent := &db.RequestEvent{}
- err = svc.DB.Create(&dbRequestEvent).Error
+ app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
+ dbRequestEvent := &db.RequestEvent{
+ AppId: &app.ID,
}
-
- var publishedResponse *models.Response
-
- publishResponse := func(response *models.Response, tags nostr.Tags) {
- publishedResponse = response
- }
-
- NewLookupInvoiceController(svc.LNClient).
- HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
-
- assert.Nil(t, publishedResponse.Result)
- assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code)
-}
-
-func TestHandleLookupInvoiceEvent_WithPermission(t *testing.T) {
- ctx := context.TODO()
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
-
- nip47Request := &models.Request{}
- err = json.Unmarshal([]byte(nip47LookupInvoiceJson), nip47Request)
- assert.NoError(t, err)
-
- dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
+ feesPaid := uint64(tests.MockLNClientTransaction.FeesPaid)
+ settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
+ err = svc.DB.Create(&db.Transaction{
+ Type: tests.MockLNClientTransaction.Type,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ Description: tests.MockLNClientTransaction.Description,
+ DescriptionHash: tests.MockLNClientTransaction.DescriptionHash,
+ Preimage: &tests.MockLNClientTransaction.Preimage,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ AmountMsat: uint64(tests.MockLNClientTransaction.Amount),
+ FeeMsat: &feesPaid,
+ SettledAt: &settledAt,
+ AppId: &app.ID,
+ }).Error
+ assert.NoError(t, err)
var publishedResponse *models.Response
@@ -82,18 +66,20 @@ func TestHandleLookupInvoiceEvent_WithPermission(t *testing.T) {
publishedResponse = response
}
- NewLookupInvoiceController(svc.LNClient).
- HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
assert.Nil(t, publishedResponse.Error)
transaction := publishedResponse.Result.(*lookupInvoiceResponse)
- assert.Equal(t, tests.MockTransaction.Type, transaction.Type)
- assert.Equal(t, tests.MockTransaction.Invoice, transaction.Invoice)
- assert.Equal(t, tests.MockTransaction.Description, transaction.Description)
- assert.Equal(t, tests.MockTransaction.DescriptionHash, transaction.DescriptionHash)
- assert.Equal(t, tests.MockTransaction.Preimage, transaction.Preimage)
- assert.Equal(t, tests.MockTransaction.PaymentHash, transaction.PaymentHash)
- assert.Equal(t, tests.MockTransaction.Amount, transaction.Amount)
- assert.Equal(t, tests.MockTransaction.FeesPaid, transaction.FeesPaid)
- assert.Equal(t, tests.MockTransaction.SettledAt, transaction.SettledAt)
+ assert.Equal(t, tests.MockLNClientTransaction.Type, transaction.Type)
+ assert.Equal(t, tests.MockLNClientTransaction.Invoice, transaction.Invoice)
+ assert.Equal(t, tests.MockLNClientTransaction.Description, transaction.Description)
+ assert.Equal(t, tests.MockLNClientTransaction.DescriptionHash, transaction.DescriptionHash)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, transaction.Preimage)
+ assert.Equal(t, tests.MockLNClientTransaction.PaymentHash, transaction.PaymentHash)
+ assert.Equal(t, tests.MockLNClientTransaction.Amount, transaction.Amount)
+ assert.Equal(t, tests.MockLNClientTransaction.FeesPaid, transaction.FeesPaid)
+ assert.Equal(t, tests.MockLNClientTransaction.SettledAt, transaction.SettledAt)
}
diff --git a/nip47/controllers/make_invoice_controller.go b/nip47/controllers/make_invoice_controller.go
index 29d2746a..90e10f63 100644
--- a/nip47/controllers/make_invoice_controller.go
+++ b/nip47/controllers/make_invoice_controller.go
@@ -3,7 +3,6 @@ package controllers
import (
"context"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
@@ -20,26 +19,10 @@ type makeInvoiceResponse struct {
models.Transaction
}
-type makeInvoiceController struct {
- lnClient lnclient.LNClient
-}
-
-func NewMakeInvoiceController(lnClient lnclient.LNClient) *makeInvoiceController {
- return &makeInvoiceController{
- lnClient: lnClient,
- }
-}
-
-func (controller *makeInvoiceController) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) {
- // basic permissions check
- resp := checkPermission(0)
- if resp != nil {
- publishResponse(resp, nostr.Tags{})
- return
- }
+func (controller *nip47Controller) HandleMakeInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, appId uint, publishResponse publishFunc) {
makeInvoiceParams := &makeInvoiceParams{}
- resp = decodeRequest(nip47Request, makeInvoiceParams)
+ resp := decodeRequest(nip47Request, makeInvoiceParams)
if resp != nil {
publishResponse(resp, nostr.Tags{})
return
@@ -55,7 +38,7 @@ func (controller *makeInvoiceController) HandleMakeInvoiceEvent(ctx context.Cont
expiry := makeInvoiceParams.Expiry
- transaction, err := controller.lnClient.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry)
+ transaction, err := controller.transactionsService.MakeInvoice(ctx, makeInvoiceParams.Amount, makeInvoiceParams.Description, makeInvoiceParams.DescriptionHash, expiry, controller.lnClient, &appId, &requestEventId)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
@@ -75,8 +58,9 @@ func (controller *makeInvoiceController) HandleMakeInvoiceEvent(ctx context.Cont
return
}
+ nip47Transaction := models.ToNip47Transaction(transaction)
responsePayload := &makeInvoiceResponse{
- Transaction: *transaction,
+ Transaction: *nip47Transaction,
}
publishResponse(&models.Response{
diff --git a/nip47/controllers/make_invoice_controller_test.go b/nip47/controllers/make_invoice_controller_test.go
index 2e4dd6e0..8bac1967 100644
--- a/nip47/controllers/make_invoice_controller_test.go
+++ b/nip47/controllers/make_invoice_controller_test.go
@@ -10,7 +10,9 @@ import (
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47MakeInvoiceJson = `
@@ -24,7 +26,7 @@ const nip47MakeInvoiceJson = `
}
`
-func TestHandleMakeInvoiceEvent_NoPermission(t *testing.T) {
+func TestHandleMakeInvoiceEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -34,59 +36,26 @@ func TestHandleMakeInvoiceEvent_NoPermission(t *testing.T) {
err = json.Unmarshal([]byte(nip47MakeInvoiceJson), nip47Request)
assert.NoError(t, err)
- dbRequestEvent := &db.RequestEvent{}
- err = svc.DB.Create(&dbRequestEvent).Error
+ app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
+ dbRequestEvent := &db.RequestEvent{
+ AppId: &app.ID,
}
-
- var publishedResponse *models.Response
-
- publishResponse := func(response *models.Response, tags nostr.Tags) {
- publishedResponse = response
- }
-
- NewMakeInvoiceController(svc.LNClient).
- HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
-
- assert.Nil(t, publishedResponse.Result)
- assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code)
-}
-
-func TestHandleMakeInvoiceEvent_WithPermission(t *testing.T) {
- ctx := context.TODO()
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
-
- nip47Request := &models.Request{}
- err = json.Unmarshal([]byte(nip47MakeInvoiceJson), nip47Request)
- assert.NoError(t, err)
-
- dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
- NewMakeInvoiceController(svc.LNClient).
- HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
assert.Nil(t, publishedResponse.Error)
- assert.Equal(t, tests.MockTransaction.Invoice, publishedResponse.Result.(*makeInvoiceResponse).Invoice)
+ assert.Equal(t, tests.MockLNClientTransaction.Invoice, publishedResponse.Result.(*makeInvoiceResponse).Invoice)
}
diff --git a/nip47/controllers/map_nip47_error.go b/nip47/controllers/map_nip47_error.go
new file mode 100644
index 00000000..4acc8174
--- /dev/null
+++ b/nip47/controllers/map_nip47_error.go
@@ -0,0 +1,26 @@
+package controllers
+
+import (
+ "errors"
+
+ "github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/transactions"
+)
+
+func mapNip47Error(err error) *models.Error {
+ code := models.ERROR_INTERNAL
+ if errors.Is(err, transactions.NewNotFoundError()) {
+ code = models.ERROR_NOT_FOUND
+ }
+ if errors.Is(err, transactions.NewInsufficientBalanceError()) {
+ code = models.ERROR_INSUFFICIENT_BALANCE
+ }
+ if errors.Is(err, transactions.NewQuotaExceededError()) {
+ code = models.ERROR_QUOTA_EXCEEDED
+ }
+
+ return &models.Error{
+ Code: code,
+ Message: err.Error(),
+ }
+}
diff --git a/nip47/controllers/models.go b/nip47/controllers/models.go
index 9ccec5c3..11a5180c 100644
--- a/nip47/controllers/models.go
+++ b/nip47/controllers/models.go
@@ -5,7 +5,6 @@ import (
"github.com/nbd-wtf/go-nostr"
)
-type checkPermissionFunc = func(amountMsat uint64) *models.Response
type publishFunc = func(*models.Response, nostr.Tags)
type payResponse struct {
diff --git a/nip47/controllers/multi_pay_invoice_controller.go b/nip47/controllers/multi_pay_invoice_controller.go
index f5736a44..f5476439 100644
--- a/nip47/controllers/multi_pay_invoice_controller.go
+++ b/nip47/controllers/multi_pay_invoice_controller.go
@@ -7,14 +7,11 @@ import (
"sync"
"github.com/getAlby/hub/db"
- "github.com/getAlby/hub/events"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
- "gorm.io/gorm"
)
type multiPayInvoiceElement struct {
@@ -26,21 +23,7 @@ type multiPayInvoiceParams struct {
Invoices []multiPayInvoiceElement `json:"invoices"`
}
-type multiMultiPayInvoiceController struct {
- lnClient lnclient.LNClient
- db *gorm.DB
- eventPublisher events.EventPublisher
-}
-
-func NewMultiPayInvoiceController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *multiMultiPayInvoiceController {
- return &multiMultiPayInvoiceController{
- lnClient: lnClient,
- db: db,
- eventPublisher: eventPublisher,
- }
-}
-
-func (controller *multiMultiPayInvoiceController) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc) {
+func (controller *nip47Controller) HandleMultiPayInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc) {
multiPayParams := &multiPayInvoiceParams{}
resp := decodeRequest(nip47Request, multiPayParams)
if resp != nil {
@@ -82,8 +65,8 @@ func (controller *multiMultiPayInvoiceController) HandleMultiPayInvoiceEvent(ctx
}
dTag := []string{"d", invoiceDTagValue}
- NewPayInvoiceController(controller.lnClient, controller.db, controller.eventPublisher).
- pay(ctx, bolt11, &paymentRequest, nip47Request, requestEventId, app, checkPermission, publishResponse, nostr.Tags{dTag})
+ controller.
+ pay(ctx, bolt11, &paymentRequest, nip47Request, requestEventId, app, publishResponse, nostr.Tags{dTag})
}(invoiceInfo)
}
diff --git a/nip47/controllers/multi_pay_invoice_controller_test.go b/nip47/controllers/multi_pay_invoice_controller_test.go
index ad5aeaf0..c6beec66 100644
--- a/nip47/controllers/multi_pay_invoice_controller_test.go
+++ b/nip47/controllers/multi_pay_invoice_controller_test.go
@@ -3,15 +3,20 @@ package controllers
import (
"context"
"encoding/json"
+ "errors"
"sync"
"testing"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47MultiPayJson = `
@@ -60,7 +65,7 @@ const nip47MultiPayOneMalformedInvoiceJson = `
}
`
-func TestHandleMultiPayInvoiceEvent_NoPermission(t *testing.T) {
+func TestHandleMultiPayInvoiceEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
@@ -70,6 +75,14 @@ func TestHandleMultiPayInvoiceEvent_NoPermission(t *testing.T) {
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47MultiPayJson), nip47Request)
assert.NoError(t, err)
@@ -77,19 +90,58 @@ func TestHandleMultiPayInvoiceEvent_NoPermission(t *testing.T) {
responses := []*models.Response{}
dTags := []nostr.Tags{}
+ var mu sync.Mutex
+
+ publishResponse := func(response *models.Response, tags nostr.Tags) {
+ mu.Lock()
+ defer mu.Unlock()
+ responses = append(responses, response)
+ dTags = append(dTags, tags)
+ }
+
dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
+
+ assert.Equal(t, 2, len(responses))
+ for i := 0; i < len(responses); i++ {
+ assert.Equal(t, "123preimage", responses[i].Result.(payResponse).Preimage)
+ assert.Equal(t, tests.MockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value())
+ assert.Nil(t, responses[i].Error)
}
+}
+
+func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ nip47Request := &models.Request{}
+ err = json.Unmarshal([]byte(nip47MultiPayOneMalformedInvoiceJson), nip47Request)
+ assert.NoError(t, err)
+
+ responses := []*models.Response{}
+ dTags := []nostr.Tags{}
+
var mu sync.Mutex
publishResponse := func(response *models.Response, tags nostr.Tags) {
@@ -99,19 +151,26 @@ func TestHandleMultiPayInvoiceEvent_NoPermission(t *testing.T) {
dTags = append(dTags, tags)
}
- NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse)
+ requestEvent := &db.RequestEvent{}
+ svc.DB.Save(requestEvent)
+
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, publishResponse)
assert.Equal(t, 2, len(responses))
- assert.Equal(t, 2, len(dTags))
- for i := 0; i < len(responses); i++ {
- assert.Equal(t, models.ERROR_RESTRICTED, responses[i].Error.Code)
- assert.Equal(t, tests.MockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value())
- assert.Equal(t, responses[i].Result, nil)
- }
+ assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value())
+ assert.Equal(t, models.ERROR_INTERNAL, responses[0].Error.Code)
+ assert.Nil(t, responses[0].Result)
+
+ assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
+ assert.Equal(t, "123preimage", responses[1].Result.(payResponse).Preimage)
+ assert.Nil(t, responses[1].Error)
+
}
-func TestHandleMultiPayInvoiceEvent_WithPermission(t *testing.T) {
+func TestHandleMultiPayInvoiceEvent_IsolatedApp_OneBudgetExceeded(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
@@ -120,6 +179,24 @@ func TestHandleMultiPayInvoiceEvent_WithPermission(t *testing.T) {
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ // invoices paid are 123000 millisats
+ AmountMsat: 200000,
+ })
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47MultiPayJson), nip47Request)
@@ -137,37 +214,51 @@ func TestHandleMultiPayInvoiceEvent_WithPermission(t *testing.T) {
dTags = append(dTags, tags)
}
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Equal(t, 2, len(responses))
- for i := 0; i < len(responses); i++ {
- assert.Equal(t, "123preimage", responses[i].Result.(payResponse).Preimage)
- assert.Equal(t, tests.MockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value())
- assert.Nil(t, responses[i].Error)
- }
+ assert.Equal(t, "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf", dTags[0].GetFirst([]string{"d"}).Value())
+ assert.Equal(t, "123preimage", responses[0].Result.(payResponse).Preimage)
+ assert.Nil(t, responses[0].Error)
+
+ assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
+ assert.Nil(t, responses[1].Result)
+ assert.Equal(t, models.ERROR_INSUFFICIENT_BALANCE, responses[1].Error.Code)
}
-func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) {
+func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) {
+
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
+ svc.LNClient.(*tests.MockLn).PayInvoiceResponses = []*lnclient.PayInvoiceResponse{{
+ Preimage: "123preimage",
+ }, nil}
+ svc.LNClient.(*tests.MockLn).PayInvoiceErrors = []error{nil, errors.New("Some error")}
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
nip47Request := &models.Request{}
- err = json.Unmarshal([]byte(nip47MultiPayOneMalformedInvoiceJson), nip47Request)
+ err = json.Unmarshal([]byte(nip47MultiPayJson), nip47Request)
assert.NoError(t, err)
responses := []*models.Response{}
@@ -182,44 +273,23 @@ func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) {
dTags = append(dTags, tags)
}
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
- requestEvent := &db.RequestEvent{}
- svc.DB.Save(requestEvent)
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
- NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Equal(t, 2, len(responses))
- assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value())
- assert.Equal(t, models.ERROR_INTERNAL, responses[0].Error.Code)
- assert.Nil(t, responses[0].Result)
- assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
- assert.Equal(t, "123preimage", responses[1].Result.(payResponse).Preimage)
- assert.Nil(t, responses[1].Error)
+ assert.Equal(t, "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf", dTags[0].GetFirst([]string{"d"}).Value())
+ assert.Equal(t, "123preimage", responses[0].Result.(payResponse).Preimage)
+ assert.Nil(t, responses[0].Error)
+ assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
+ assert.Nil(t, responses[1].Result)
+ assert.Equal(t, models.ERROR_INTERNAL, responses[1].Error.Code)
+ assert.Equal(t, "Some error", responses[1].Error.Message)
}
-
-// TODO: fix and re-enable this as a separate test
-// // budget overflow
-// newMaxAmount := 500
-// err = svc.DB.Model(&db.AppPermission{}).Where("app_id = ?", app.ID).Update("max_amount", newMaxAmount).Error
-// assert.NoError(t, err)
-
-// err = json.Unmarshal([]byte(nip47MultiPayOneOverflowingBudgetJson), nip47Request)
-// assert.NoError(t, err)
-
-// responses = []*models.Response{}
-// dTags = []nostr.Tags{}
-// NewMultiPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher).
-// HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, checkPermission, publishResponse)
-
-// // might be flaky because the two requests run concurrently
-// // and there's more chance that the failed respons calls the
-// // publishResponse as it's called earlier
-// assert.Equal(t, responses[0].Error.Code, models.ERROR_QUOTA_EXCEEDED)
-// assert.Equal(t, tests.MockPaymentHash500, dTags[0].GetFirst([]string{"d"}).Value())
-// assert.Equal(t, responses[1].Result.(payResponse).Preimage, "123preimage")
-// assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
diff --git a/nip47/controllers/multi_pay_keysend_controller.go b/nip47/controllers/multi_pay_keysend_controller.go
index 7f5bb225..6c91eca6 100644
--- a/nip47/controllers/multi_pay_keysend_controller.go
+++ b/nip47/controllers/multi_pay_keysend_controller.go
@@ -5,11 +5,8 @@ import (
"sync"
"github.com/getAlby/hub/db"
- "github.com/getAlby/hub/events"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
- "gorm.io/gorm"
)
type multiPayKeysendParams struct {
@@ -21,21 +18,7 @@ type multiPayKeysendElement struct {
Id string `json:"id"`
}
-type multiMultiPayKeysendController struct {
- lnClient lnclient.LNClient
- db *gorm.DB
- eventPublisher events.EventPublisher
-}
-
-func NewMultiPayKeysendController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *multiMultiPayKeysendController {
- return &multiMultiPayKeysendController{
- lnClient: lnClient,
- db: db,
- eventPublisher: eventPublisher,
- }
-}
-
-func (controller *multiMultiPayKeysendController) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc) {
+func (controller *nip47Controller) HandleMultiPayKeysendEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc) {
multiPayParams := &multiPayKeysendParams{}
resp := decodeRequest(nip47Request, multiPayParams)
if resp != nil {
@@ -55,8 +38,8 @@ func (controller *multiMultiPayKeysendController) HandleMultiPayKeysendEvent(ctx
}
dTag := []string{"d", keysendDTagValue}
- NewPayKeysendController(controller.lnClient, controller.db, controller.eventPublisher).
- pay(ctx, &keysendInfo.payKeysendParams, nip47Request, requestEventId, app, checkPermission, publishResponse, nostr.Tags{dTag})
+ controller.
+ payKeysend(ctx, &keysendInfo.payKeysendParams, nip47Request, requestEventId, app, publishResponse, nostr.Tags{dTag})
}(keysendInfo)
}
diff --git a/nip47/controllers/multi_pay_keysend_controller_test.go b/nip47/controllers/multi_pay_keysend_controller_test.go
index a1bab5a4..f02bcd0f 100644
--- a/nip47/controllers/multi_pay_keysend_controller_test.go
+++ b/nip47/controllers/multi_pay_keysend_controller_test.go
@@ -9,9 +9,12 @@ import (
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47MultiPayKeysendJson = `
@@ -65,7 +68,7 @@ const nip47MultiPayKeysendOneOverflowingBudgetJson = `
}
`
-func TestHandleMultiPayKeysendEvent_NoPermission(t *testing.T) {
+func TestHandleMultiPayKeysendEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -74,53 +77,12 @@ func TestHandleMultiPayKeysendEvent_NoPermission(t *testing.T) {
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
- nip47Request := &models.Request{}
- err = json.Unmarshal([]byte(nip47MultiPayKeysendJson), nip47Request)
- assert.NoError(t, err)
-
- dbRequestEvent := &db.RequestEvent{}
- err = svc.DB.Create(&dbRequestEvent).Error
- assert.NoError(t, err)
-
- responses := []*models.Response{}
- dTags := []nostr.Tags{}
-
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
}
-
- var mu sync.Mutex
-
- publishResponse := func(response *models.Response, tags nostr.Tags) {
- mu.Lock()
- defer mu.Unlock()
- responses = append(responses, response)
- dTags = append(dTags, tags)
- }
-
- NewMultiPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse)
-
- assert.Equal(t, 2, len(responses))
- for i := 0; i < len(responses); i++ {
- assert.Equal(t, models.ERROR_RESTRICTED, responses[i].Error.Code)
- assert.Nil(t, responses[i].Result)
- }
-
-}
-
-func TestHandleMultiPayKeysendEvent_WithPermission(t *testing.T) {
- ctx := context.TODO()
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
-
- app, _, err := tests.CreateApp(svc)
+ err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
nip47Request := &models.Request{}
@@ -134,10 +96,6 @@ func TestHandleMultiPayKeysendEvent_WithPermission(t *testing.T) {
responses := []*models.Response{}
dTags := []nostr.Tags{}
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
-
var mu sync.Mutex
publishResponse := func(response *models.Response, tags nostr.Tags) {
@@ -147,8 +105,10 @@ func TestHandleMultiPayKeysendEvent_WithPermission(t *testing.T) {
dTags = append(dTags, tags)
}
- NewMultiPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse)
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Equal(t, 2, len(responses))
for i := 0; i < len(responses); i++ {
diff --git a/nip47/controllers/nip47_controller.go b/nip47/controllers/nip47_controller.go
new file mode 100644
index 00000000..fd45e9a1
--- /dev/null
+++ b/nip47/controllers/nip47_controller.go
@@ -0,0 +1,27 @@
+package controllers
+
+import (
+ "github.com/getAlby/hub/events"
+ "github.com/getAlby/hub/lnclient"
+ "github.com/getAlby/hub/nip47/permissions"
+ "github.com/getAlby/hub/transactions"
+ "gorm.io/gorm"
+)
+
+type nip47Controller struct {
+ lnClient lnclient.LNClient
+ db *gorm.DB
+ eventPublisher events.EventPublisher
+ permissionsService permissions.PermissionsService
+ transactionsService transactions.TransactionsService
+}
+
+func NewNip47Controller(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher, permissionsService permissions.PermissionsService, transactionsService transactions.TransactionsService) *nip47Controller {
+ return &nip47Controller{
+ lnClient: lnClient,
+ db: db,
+ eventPublisher: eventPublisher,
+ permissionsService: permissionsService,
+ transactionsService: transactionsService,
+ }
+}
diff --git a/nip47/controllers/pay_invoice_controller.go b/nip47/controllers/pay_invoice_controller.go
index 6a569e06..62efb5a7 100644
--- a/nip47/controllers/pay_invoice_controller.go
+++ b/nip47/controllers/pay_invoice_controller.go
@@ -7,34 +7,18 @@ import (
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
- "gorm.io/gorm"
)
type payInvoiceParams struct {
Invoice string `json:"invoice"`
}
-type payInvoiceController struct {
- lnClient lnclient.LNClient
- db *gorm.DB
- eventPublisher events.EventPublisher
-}
-
-func NewPayInvoiceController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *payInvoiceController {
- return &payInvoiceController{
- lnClient: lnClient,
- db: db,
- eventPublisher: eventPublisher,
- }
-}
-
-func (controller *payInvoiceController) HandlePayInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) {
+func (controller *nip47Controller) HandlePayInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc, tags nostr.Tags) {
payParams := &payInvoiceParams{}
resp := decodeRequest(nip47Request, payParams)
if resp != nil {
@@ -63,36 +47,17 @@ func (controller *payInvoiceController) HandlePayInvoiceEvent(ctx context.Contex
return
}
- controller.pay(ctx, bolt11, &paymentRequest, nip47Request, requestEventId, app, checkPermission, publishResponse, tags)
+ controller.pay(ctx, bolt11, &paymentRequest, nip47Request, requestEventId, app, publishResponse, tags)
}
-func (controller *payInvoiceController) pay(ctx context.Context, bolt11 string, paymentRequest *decodepay.Bolt11, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) {
- resp := checkPermission(uint64(paymentRequest.MSatoshi))
- if resp != nil {
- publishResponse(resp, tags)
- return
- }
-
- payment := db.Payment{App: *app, RequestEventId: requestEventId, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)}
- err := controller.db.Create(&payment).Error
- if err != nil {
- publishResponse(&models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_INTERNAL,
- Message: err.Error(),
- },
- }, tags)
- return
- }
-
+func (controller *nip47Controller) pay(ctx context.Context, bolt11 string, paymentRequest *decodepay.Bolt11, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc, tags nostr.Tags) {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
"app_id": app.ID,
"bolt11": bolt11,
}).Info("Sending payment")
- response, err := controller.lnClient.SendPaymentSync(ctx, bolt11)
+ response, err := controller.transactionsService.SendPaymentSync(ctx, bolt11, controller.lnClient, &app.ID, &requestEventId)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
@@ -109,16 +74,10 @@ func (controller *payInvoiceController) pay(ctx context.Context, bolt11 string,
})
publishResponse(&models.Response{
ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_INTERNAL,
- Message: err.Error(),
- },
+ Error: mapNip47Error(err),
}, tags)
return
}
- payment.Preimage = &response.Preimage
- // TODO: save payment fee
- controller.db.Save(&payment)
controller.eventPublisher.Publish(&events.Event{
Event: "nwc_payment_succeeded",
@@ -131,8 +90,8 @@ func (controller *payInvoiceController) pay(ctx context.Context, bolt11 string,
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Result: payResponse{
- Preimage: response.Preimage,
- FeesPaid: response.Fee,
+ Preimage: *response.Preimage,
+ FeesPaid: response.FeeMsat,
},
}, tags)
}
diff --git a/nip47/controllers/pay_invoice_controller_test.go b/nip47/controllers/pay_invoice_controller_test.go
index 683a3344..4159340a 100644
--- a/nip47/controllers/pay_invoice_controller_test.go
+++ b/nip47/controllers/pay_invoice_controller_test.go
@@ -8,9 +8,12 @@ import (
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47PayInvoiceJson = `
@@ -31,7 +34,7 @@ const nip47PayJsonNoInvoice = `
}
`
-func TestHandlePayInvoiceEvent_NoPermission(t *testing.T) {
+func TestHandlePayInvoiceEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -40,43 +43,12 @@ func TestHandlePayInvoiceEvent_NoPermission(t *testing.T) {
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
- nip47Request := &models.Request{}
- err = json.Unmarshal([]byte(nip47PayInvoiceJson), nip47Request)
- assert.NoError(t, err)
-
- dbRequestEvent := &db.RequestEvent{}
- err = svc.DB.Create(&dbRequestEvent).Error
- assert.NoError(t, err)
-
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
}
-
- var publishedResponse *models.Response
-
- publishResponse := func(response *models.Response, tags nostr.Tags) {
- publishedResponse = response
- }
-
- NewPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{})
-
- assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code)
-
-}
-
-func TestHandlePayInvoiceEvent_WithPermission(t *testing.T) {
- ctx := context.TODO()
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
-
- app, _, err := tests.CreateApp(svc)
+ err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
nip47Request := &models.Request{}
@@ -87,18 +59,16 @@ func TestHandlePayInvoiceEvent_WithPermission(t *testing.T) {
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
- NewPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{})
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage)
}
@@ -112,6 +82,14 @@ func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) {
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47PayJsonNoInvoice), nip47Request)
assert.NoError(t, err)
@@ -120,19 +98,18 @@ func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) {
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
- NewPayInvoiceController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{})
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
assert.Nil(t, publishedResponse.Result)
assert.Equal(t, models.ERROR_INTERNAL, publishedResponse.Error.Code)
+ assert.Equal(t, "Failed to decode bolt11 invoice: bolt11 too short", publishedResponse.Error.Message)
}
diff --git a/nip47/controllers/pay_keysend_controller.go b/nip47/controllers/pay_keysend_controller.go
index 6aab0d29..36cb8e66 100644
--- a/nip47/controllers/pay_keysend_controller.go
+++ b/nip47/controllers/pay_keysend_controller.go
@@ -10,7 +10,6 @@ import (
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
- "gorm.io/gorm"
)
type payKeysendParams struct {
@@ -20,57 +19,24 @@ type payKeysendParams struct {
TLVRecords []lnclient.TLVRecord `json:"tlv_records"`
}
-type payKeysendController struct {
- lnClient lnclient.LNClient
- db *gorm.DB
- eventPublisher events.EventPublisher
-}
-
-func NewPayKeysendController(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher) *payKeysendController {
- return &payKeysendController{
- lnClient: lnClient,
- db: db,
- eventPublisher: eventPublisher,
- }
-}
-
-func (controller *payKeysendController) HandlePayKeysendEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) {
+func (controller *nip47Controller) HandlePayKeysendEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc, tags nostr.Tags) {
payKeysendParams := &payKeysendParams{}
resp := decodeRequest(nip47Request, payKeysendParams)
if resp != nil {
publishResponse(resp, tags)
return
}
- controller.pay(ctx, payKeysendParams, nip47Request, requestEventId, app, checkPermission, publishResponse, tags)
+ controller.payKeysend(ctx, payKeysendParams, nip47Request, requestEventId, app, publishResponse, tags)
}
-func (controller *payKeysendController) pay(ctx context.Context, payKeysendParams *payKeysendParams, nip47Request *models.Request, requestEventId uint, app *db.App, checkPermission checkPermissionFunc, publishResponse publishFunc, tags nostr.Tags) {
- resp := checkPermission(payKeysendParams.Amount)
- if resp != nil {
- publishResponse(resp, tags)
- return
- }
-
- payment := db.Payment{App: *app, RequestEventId: requestEventId, Amount: uint(payKeysendParams.Amount / 1000)}
- err := controller.db.Create(&payment).Error
- if err != nil {
- publishResponse(&models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_INTERNAL,
- Message: err.Error(),
- },
- }, tags)
- return
- }
-
+func (controller *nip47Controller) payKeysend(ctx context.Context, payKeysendParams *payKeysendParams, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc, tags nostr.Tags) {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
"appId": app.ID,
"senderPubkey": payKeysendParams.Pubkey,
}).Info("Sending keysend payment")
- preimage, err := controller.lnClient.SendKeysend(ctx, payKeysendParams.Amount, payKeysendParams.Pubkey, payKeysendParams.Preimage, payKeysendParams.TLVRecords)
+ transaction, err := controller.transactionsService.SendKeysend(ctx, payKeysendParams.Amount, payKeysendParams.Pubkey, payKeysendParams.TLVRecords, controller.lnClient, &app.ID, &requestEventId)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
@@ -87,15 +53,10 @@ func (controller *payKeysendController) pay(ctx context.Context, payKeysendParam
})
publishResponse(&models.Response{
ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_INTERNAL,
- Message: err.Error(),
- },
+ Error: mapNip47Error(err),
}, tags)
return
}
- payment.Preimage = &preimage
- controller.db.Save(&payment)
controller.eventPublisher.Publish(&events.Event{
Event: "nwc_payment_succeeded",
Properties: map[string]interface{}{
@@ -106,7 +67,7 @@ func (controller *payKeysendController) pay(ctx context.Context, payKeysendParam
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Result: payResponse{
- Preimage: preimage,
+ Preimage: *transaction.Preimage,
},
}, tags)
}
diff --git a/nip47/controllers/pay_keysend_controller_test.go b/nip47/controllers/pay_keysend_controller_test.go
index 4bd72420..224bc48a 100644
--- a/nip47/controllers/pay_keysend_controller_test.go
+++ b/nip47/controllers/pay_keysend_controller_test.go
@@ -8,9 +8,12 @@ import (
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
+ "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
+ "github.com/getAlby/hub/transactions"
)
const nip47KeysendJson = `
@@ -27,7 +30,7 @@ const nip47KeysendJson = `
}
`
-func TestHandlePayKeysendEvent_NoPermission(t *testing.T) {
+func TestHandlePayKeysendEvent(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
@@ -36,43 +39,12 @@ func TestHandlePayKeysendEvent_NoPermission(t *testing.T) {
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
- nip47Request := &models.Request{}
- err = json.Unmarshal([]byte(nip47KeysendJson), nip47Request)
- assert.NoError(t, err)
-
- dbRequestEvent := &db.RequestEvent{}
- err = svc.DB.Create(&dbRequestEvent).Error
- assert.NoError(t, err)
-
- checkPermission := func(amountMsat uint64) *models.Response {
- return &models.Response{
- ResultType: nip47Request.Method,
- Error: &models.Error{
- Code: models.ERROR_RESTRICTED,
- },
- }
- }
-
- var publishedResponse *models.Response
-
- publishResponse := func(response *models.Response, tags nostr.Tags) {
- publishedResponse = response
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
}
-
- NewPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{})
-
- assert.Nil(t, publishedResponse.Result)
- assert.Equal(t, models.ERROR_RESTRICTED, publishedResponse.Error.Code)
-}
-
-func TestHandlePayKeysendEvent_WithPermission(t *testing.T) {
- ctx := context.TODO()
- defer tests.RemoveTestService()
- svc, err := tests.CreateTestService()
- assert.NoError(t, err)
-
- app, _, err := tests.CreateApp(svc)
+ err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
nip47Request := &models.Request{}
@@ -84,18 +56,16 @@ func TestHandlePayKeysendEvent_WithPermission(t *testing.T) {
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
- checkPermission := func(amountMsat uint64) *models.Response {
- return nil
- }
-
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
- NewPayKeysendController(svc.LNClient, svc.DB, svc.EventPublisher).
- HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, checkPermission, publishResponse, nostr.Tags{})
+ permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
+ NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
+ HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
assert.Nil(t, publishedResponse.Error)
assert.Equal(t, "12345preimage", publishedResponse.Result.(payResponse).Preimage)
diff --git a/nip47/controllers/sign_message_controller.go b/nip47/controllers/sign_message_controller.go
index 10ea0ed2..bd9d688f 100644
--- a/nip47/controllers/sign_message_controller.go
+++ b/nip47/controllers/sign_message_controller.go
@@ -3,7 +3,6 @@ package controllers
import (
"context"
- "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
@@ -19,26 +18,9 @@ type signMessageResponse struct {
Signature string `json:"signature"`
}
-type signMessageController struct {
- lnClient lnclient.LNClient
-}
-
-func NewSignMessageController(lnClient lnclient.LNClient) *signMessageController {
- return &signMessageController{
- lnClient: lnClient,
- }
-}
-
-func (controller *signMessageController) HandleSignMessageEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, checkPermission checkPermissionFunc, publishResponse publishFunc) {
- // basic permissions check
- resp := checkPermission(0)
- if resp != nil {
- publishResponse(resp, nostr.Tags{})
- return
- }
-
+func (controller *nip47Controller) HandleSignMessageEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, publishResponse publishFunc) {
signParams := &signMessageParams{}
- resp = decodeRequest(nip47Request, signParams)
+ resp := decodeRequest(nip47Request, signParams)
if resp != nil {
publishResponse(resp, nostr.Tags{})
return
diff --git a/nip47/event_handler.go b/nip47/event_handler.go
index c84cbb62..c51813c1 100644
--- a/nip47/event_handler.go
+++ b/nip47/event_handler.go
@@ -11,22 +11,40 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
- controllers "github.com/getAlby/hub/nip47/controllers"
+ "github.com/getAlby/hub/nip47/controllers"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
+ nostrmodels "github.com/getAlby/hub/nostr/models"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
-func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event, lnClient lnclient.LNClient) {
+func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Relay, event *nostr.Event, lnClient lnclient.LNClient) {
var nip47Response *models.Response
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).Info("Processing Event")
+ // go-nostr already checks this, but just to be sure:
+ validEventSignature, err := event.CheckSignature()
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "requestEventNostrId": event.ID,
+ "eventKind": event.Kind,
+ }).WithError(err).Error("invalid event signature")
+ return
+ }
+ if !validEventSignature {
+ logger.Logger.WithFields(logrus.Fields{
+ "requestEventNostrId": event.ID,
+ "eventKind": event.Kind,
+ }).Error("invalid event signature")
+ return
+ }
+
ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.keys.GetNostrSecretKey())
if err != nil {
logger.Logger.WithFields(logrus.Fields{
@@ -63,7 +81,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}
- svc.publishResponseEvent(ctx, sub, &requestEvent, resp, nil)
+ svc.publishResponseEvent(ctx, relay, &requestEvent, resp, nil)
return
}
@@ -89,7 +107,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}
- svc.publishResponseEvent(ctx, sub, &requestEvent, resp, &app)
+ svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
@@ -121,7 +139,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}
- svc.publishResponseEvent(ctx, sub, &requestEvent, resp, &app)
+ svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
@@ -215,7 +233,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio
}).WithError(err).Error("Failed to create response")
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
} else {
- err = svc.publishResponseEvent(ctx, sub, &requestEvent, resp, &app)
+ err = svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
@@ -240,18 +258,27 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio
}
}
- checkPermission := func(amountMsat uint64) *models.Response {
+ logger.Logger.WithFields(logrus.Fields{
+ "requestEventNostrId": event.ID,
+ "eventKind": event.Kind,
+ "appId": app.ID,
+ "method": nip47Request.Method,
+ "params": nip47Request.Params,
+ }).Info("Handling NIP-47 request")
+
+ if nip47Request.Method != models.GET_INFO_METHOD {
scope, err := permissions.RequestMethodToScope(nip47Request.Method)
if err != nil {
- return &models.Response{
+ publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: models.ERROR_INTERNAL,
Message: err.Error(),
},
- }
+ }, nostr.Tags{})
+ return
}
- hasPermission, code, message := svc.permissionsService.HasPermission(&app, scope, amountMsat)
+ hasPermission, code, message := svc.permissionsService.HasPermission(&app, scope)
if !hasPermission {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEvent.ID,
@@ -271,67 +298,50 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, sub *nostr.Subscriptio
},
})
- return &models.Response{
+ publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: code,
Message: message,
},
- }
+ }, nostr.Tags{})
+ return
}
- return nil
}
- logger.Logger.WithFields(logrus.Fields{
- "requestEventNostrId": event.ID,
- "eventKind": event.Kind,
- "appId": app.ID,
- "method": nip47Request.Method,
- "params": nip47Request.Params,
- }).Info("Handling NIP-47 request")
+ controller := controllers.NewNip47Controller(lnClient, svc.db, svc.eventPublisher, svc.permissionsService, svc.transactionsService)
- // TODO: controllers should share a common interface
switch nip47Request.Method {
case models.MULTI_PAY_INVOICE_METHOD:
- controllers.
- NewMultiPayInvoiceController(lnClient, svc.db, svc.eventPublisher).
- HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse)
+ controller.
+ HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
case models.MULTI_PAY_KEYSEND_METHOD:
- controllers.
- NewMultiPayKeysendController(lnClient, svc.db, svc.eventPublisher).
- HandleMultiPayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse)
+ controller.
+ HandleMultiPayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
case models.PAY_INVOICE_METHOD:
- controllers.
- NewPayInvoiceController(lnClient, svc.db, svc.eventPublisher).
- HandlePayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse, nostr.Tags{})
+ controller.
+ HandlePayInvoiceEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse, nostr.Tags{})
case models.PAY_KEYSEND_METHOD:
- controllers.
- NewPayKeysendController(lnClient, svc.db, svc.eventPublisher).
- HandlePayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse, nostr.Tags{})
+ controller.
+ HandlePayKeysendEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse, nostr.Tags{})
case models.GET_BALANCE_METHOD:
- controllers.
- NewGetBalanceController(lnClient).
- HandleGetBalanceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse)
+ controller.
+ HandleGetBalanceEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
case models.MAKE_INVOICE_METHOD:
- controllers.
- NewMakeInvoiceController(lnClient).
- HandleMakeInvoiceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse)
+ controller.
+ HandleMakeInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
case models.LOOKUP_INVOICE_METHOD:
- controllers.
- NewLookupInvoiceController(lnClient).
- HandleLookupInvoiceEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse)
+ controller.
+ HandleLookupInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
case models.LIST_TRANSACTIONS_METHOD:
- controllers.
- NewListTransactionsController(lnClient).
- HandleListTransactionsEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse)
+ controller.
+ HandleListTransactionsEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
case models.GET_INFO_METHOD:
- controllers.
- NewGetInfoController(svc.permissionsService, lnClient).
- HandleGetInfoEvent(ctx, nip47Request, requestEvent.ID, &app, checkPermission, publishResponse)
+ controller.
+ HandleGetInfoEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
case models.SIGN_MESSAGE_METHOD:
- controllers.
- NewSignMessageController(lnClient).
- HandleSignMessageEvent(ctx, nip47Request, requestEvent.ID, checkPermission, publishResponse)
+ controller.
+ HandleSignMessageEvent(ctx, nip47Request, requestEvent.ID, publishResponse)
default:
publishResponse(&models.Response{
ResultType: nip47Request.Method,
@@ -370,7 +380,7 @@ func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content inter
return resp, nil
}
-func (svc *nip47Service) publishResponseEvent(ctx context.Context, sub *nostr.Subscription, requestEvent *db.RequestEvent, resp *nostr.Event, app *db.App) error {
+func (svc *nip47Service) publishResponseEvent(ctx context.Context, relay nostrmodels.Relay, requestEvent *db.RequestEvent, resp *nostr.Event, app *db.App) error {
var appId *uint
if app != nil {
appId = &app.ID
@@ -386,7 +396,7 @@ func (svc *nip47Service) publishResponseEvent(ctx context.Context, sub *nostr.Su
return err
}
- err = sub.Relay.Publish(ctx, *resp)
+ err = relay.Publish(ctx, *resp)
if err != nil {
responseEvent.State = db.RESPONSE_EVENT_STATE_PUBLISH_FAILED
logger.Logger.WithFields(logrus.Fields{
diff --git a/nip47/event_handler_test.go b/nip47/event_handler_test.go
index 2ce7545a..b9b7caf2 100644
--- a/nip47/event_handler_test.go
+++ b/nip47/event_handler_test.go
@@ -1,9 +1,12 @@
package nip47
import (
+ "context"
"encoding/json"
"testing"
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/tests"
"github.com/nbd-wtf/go-nostr"
@@ -62,6 +65,241 @@ func TestCreateResponse(t *testing.T) {
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
assert.NoError(t, err)
+ assert.Nil(t, nip47Response.Error)
assert.Equal(t, nip47Response.ResultType, unmarshalledResponse.ResultType)
assert.Equal(t, nip47Response.Result, *unmarshalledResponse.Result.(*dummyResponse))
}
+
+func TestHandleResponse_WithPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+ nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
+
+ reqPrivateKey := nostr.GeneratePrivateKey()
+ reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
+ assert.NoError(t, err)
+
+ app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.GET_BALANCE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ content := map[string]interface{}{
+ "method": models.GET_INFO_METHOD,
+ }
+
+ payloadBytes, err := json.Marshal(content)
+ assert.NoError(t, err)
+
+ msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ assert.NoError(t, err)
+
+ reqEvent := &nostr.Event{
+ Kind: models.REQUEST_KIND,
+ PubKey: reqPubkey,
+ CreatedAt: nostr.Now(),
+ Tags: nostr.Tags{},
+ Content: msg,
+ }
+ err = reqEvent.Sign(reqPrivateKey)
+ assert.NoError(t, err)
+
+ relay := tests.NewMockRelay()
+
+ nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
+
+ assert.NotNil(t, relay.PublishedEvent)
+ assert.NotEmpty(t, relay.PublishedEvent.Content)
+
+ ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
+ assert.NoError(t, err)
+
+ decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
+ assert.NoError(t, err)
+
+ unmarshalledResponse := models.Response{}
+
+ err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
+ assert.NoError(t, err)
+ assert.Nil(t, unmarshalledResponse.Error)
+ assert.Equal(t, models.GET_INFO_METHOD, unmarshalledResponse.ResultType)
+ assert.Equal(t, []interface{}{"get_balance"}, unmarshalledResponse.Result.(map[string]interface{})["methods"])
+}
+
+func TestHandleResponse_NoPermission(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+ nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
+
+ reqPrivateKey := nostr.GeneratePrivateKey()
+ reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
+ assert.NoError(t, err)
+
+ _, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ assert.NoError(t, err)
+
+ content := map[string]interface{}{
+ "method": models.GET_BALANCE_METHOD,
+ }
+
+ payloadBytes, err := json.Marshal(content)
+ assert.NoError(t, err)
+
+ msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ assert.NoError(t, err)
+
+ reqEvent := &nostr.Event{
+ Kind: models.REQUEST_KIND,
+ PubKey: reqPubkey,
+ CreatedAt: nostr.Now(),
+ Tags: nostr.Tags{},
+ Content: msg,
+ }
+ err = reqEvent.Sign(reqPrivateKey)
+ assert.NoError(t, err)
+
+ relay := tests.NewMockRelay()
+
+ nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
+
+ assert.NotNil(t, relay.PublishedEvent)
+ assert.NotEmpty(t, relay.PublishedEvent.Content)
+
+ ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
+ assert.NoError(t, err)
+
+ decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
+ assert.NoError(t, err)
+
+ unmarshalledResponse := models.Response{}
+
+ err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
+ assert.NoError(t, err)
+ assert.Nil(t, unmarshalledResponse.Result)
+ assert.Equal(t, models.GET_BALANCE_METHOD, unmarshalledResponse.ResultType)
+ assert.Equal(t, "RESTRICTED", unmarshalledResponse.Error.Code)
+ assert.Equal(t, "This app does not have the get_balance scope", unmarshalledResponse.Error.Message)
+}
+
+func TestHandleResponse_NoApp(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+ nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
+
+ reqPrivateKey := nostr.GeneratePrivateKey()
+ reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
+ assert.NoError(t, err)
+
+ app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ assert.NoError(t, err)
+
+ // delete the app
+ err = svc.DB.Delete(app).Error
+ assert.NoError(t, err)
+
+ content := map[string]interface{}{
+ "method": models.GET_BALANCE_METHOD,
+ }
+
+ payloadBytes, err := json.Marshal(content)
+ assert.NoError(t, err)
+
+ msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ assert.NoError(t, err)
+
+ reqEvent := &nostr.Event{
+ Kind: models.REQUEST_KIND,
+ PubKey: reqPubkey,
+ CreatedAt: nostr.Now(),
+ Tags: nostr.Tags{},
+ Content: msg,
+ }
+ err = reqEvent.Sign(reqPrivateKey)
+ assert.NoError(t, err)
+
+ relay := tests.NewMockRelay()
+
+ nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
+
+ assert.NotNil(t, relay.PublishedEvent)
+ assert.NotEmpty(t, relay.PublishedEvent.Content)
+
+ ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
+ assert.NoError(t, err)
+
+ decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
+ assert.NoError(t, err)
+
+ unmarshalledResponse := models.Response{}
+
+ err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
+ assert.NoError(t, err)
+ assert.Nil(t, unmarshalledResponse.Result)
+ assert.Equal(t, "", unmarshalledResponse.ResultType)
+ assert.Equal(t, "UNAUTHORIZED", unmarshalledResponse.Error.Code)
+ assert.Equal(t, "The public key does not have a wallet connected.", unmarshalledResponse.Error.Message)
+}
+
+func TestHandleResponse_IncorrectPubkey(t *testing.T) {
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+ nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
+
+ reqPrivateKey := nostr.GeneratePrivateKey()
+ reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
+ assert.NoError(t, err)
+
+ reqPrivateKey2 := nostr.GeneratePrivateKey()
+
+ app, _, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.GET_BALANCE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ _, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey2)
+ assert.NoError(t, err)
+
+ content := map[string]interface{}{
+ "method": models.GET_BALANCE_METHOD,
+ }
+
+ payloadBytes, err := json.Marshal(content)
+ assert.NoError(t, err)
+
+ msg, err := nip04.Encrypt(string(payloadBytes), ss)
+ assert.NoError(t, err)
+
+ reqEvent := &nostr.Event{
+ Kind: models.REQUEST_KIND,
+ CreatedAt: nostr.Now(),
+ Tags: nostr.Tags{},
+ Content: msg,
+ }
+ err = reqEvent.Sign(reqPrivateKey2)
+ assert.NoError(t, err)
+
+ // set a different pubkey (this will not pass validation)
+ reqEvent.PubKey = reqPubkey
+
+ relay := tests.NewMockRelay()
+
+ nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
+
+ assert.Nil(t, relay.PublishedEvent)
+}
diff --git a/nip47/models/models.go b/nip47/models/models.go
index 524a0aad..f084b948 100644
--- a/nip47/models/models.go
+++ b/nip47/models/models.go
@@ -2,8 +2,6 @@ package models
import (
"encoding/json"
-
- "github.com/getAlby/hub/lnclient"
)
const (
@@ -32,18 +30,24 @@ const (
ERROR_EXPIRED = "EXPIRED"
ERROR_RESTRICTED = "RESTRICTED"
ERROR_BAD_REQUEST = "BAD_REQUEST"
+ ERROR_NOT_FOUND = "NOT_FOUND"
OTHER = "OTHER"
)
-const (
- BUDGET_RENEWAL_DAILY = "daily"
- BUDGET_RENEWAL_WEEKLY = "weekly"
- BUDGET_RENEWAL_MONTHLY = "monthly"
- BUDGET_RENEWAL_YEARLY = "yearly"
- BUDGET_RENEWAL_NEVER = "never"
-)
-
-type Transaction = lnclient.Transaction
+type Transaction struct {
+ Type string `json:"type"`
+ Invoice string `json:"invoice"`
+ Description string `json:"description"`
+ DescriptionHash string `json:"description_hash"`
+ Preimage string `json:"preimage"`
+ PaymentHash string `json:"payment_hash"`
+ Amount int64 `json:"amount"`
+ FeesPaid int64 `json:"fees_paid"`
+ CreatedAt int64 `json:"created_at"`
+ ExpiresAt *int64 `json:"expires_at"`
+ SettledAt *int64 `json:"settled_at"`
+ Metadata interface{} `json:"metadata,omitempty"`
+}
type PayRequest struct {
Invoice string `json:"invoice"`
diff --git a/nip47/models/transactions.go b/nip47/models/transactions.go
new file mode 100644
index 00000000..7efa389f
--- /dev/null
+++ b/nip47/models/transactions.go
@@ -0,0 +1,56 @@
+package models
+
+import (
+ "encoding/json"
+
+ "github.com/getAlby/hub/logger"
+ "github.com/getAlby/hub/transactions"
+ "github.com/sirupsen/logrus"
+)
+
+func ToNip47Transaction(transaction *transactions.Transaction) *Transaction {
+ fees := int64(0)
+ if transaction.FeeMsat != nil {
+ fees = int64(*transaction.FeeMsat)
+ }
+
+ var expiresAt *int64
+ if transaction.ExpiresAt != nil {
+ expiresAtUnix := transaction.ExpiresAt.Unix()
+ expiresAt = &expiresAtUnix
+ }
+
+ var settledAt *int64
+ preimage := ""
+ if transaction.SettledAt != nil {
+ settledAtUnix := transaction.SettledAt.Unix()
+ settledAt = &settledAtUnix
+ preimage = *transaction.Preimage
+ }
+
+ var metadata interface{}
+ if transaction.Metadata != "" {
+ jsonErr := json.Unmarshal([]byte(transaction.Metadata), &metadata)
+ if jsonErr != nil {
+ logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{
+ "id": transaction.ID,
+ "metadata": transaction.Metadata,
+ }).Error("Failed to deserialize transaction metadata")
+ }
+ }
+
+ return &Transaction{
+ Type: transaction.Type,
+ Invoice: transaction.PaymentRequest,
+ Description: transaction.Description,
+ DescriptionHash: transaction.DescriptionHash,
+ Preimage: preimage,
+ PaymentHash: transaction.PaymentHash,
+ Amount: int64(transaction.AmountMsat),
+ FeesPaid: fees,
+ CreatedAt: transaction.CreatedAt.Unix(),
+ ExpiresAt: expiresAt,
+ SettledAt: settledAt,
+ Metadata: metadata,
+ }
+}
diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go
index aa365bbb..31384fa7 100644
--- a/nip47/nip47_service.go
+++ b/nip47/nip47_service.go
@@ -7,14 +7,17 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/nip47/notifications"
- permissions "github.com/getAlby/hub/nip47/permissions"
+ "github.com/getAlby/hub/nip47/permissions"
+ nostrmodels "github.com/getAlby/hub/nostr/models"
"github.com/getAlby/hub/service/keys"
+ "github.com/getAlby/hub/transactions"
"github.com/nbd-wtf/go-nostr"
"gorm.io/gorm"
)
type nip47Service struct {
permissionsService permissions.PermissionsService
+ transactionsService transactions.TransactionsService
nip47NotificationQueue notifications.Nip47NotificationQueue
cfg config.Config
keys keys.Keys
@@ -23,27 +26,31 @@ type nip47Service struct {
}
type Nip47Service interface {
+ events.EventSubscriber
StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient)
- HandleEvent(ctx context.Context, sub *nostr.Subscription, event *nostr.Event, lnClient lnclient.LNClient)
- PublishNip47Info(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) error
+ HandleEvent(ctx context.Context, relay nostrmodels.Relay, event *nostr.Event, lnClient lnclient.LNClient)
+ PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, lnClient lnclient.LNClient) error
CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error)
}
func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *nip47Service {
- nip47NotificationQueue := notifications.NewNip47NotificationQueue()
- eventPublisher.RegisterSubscriber(nip47NotificationQueue)
return &nip47Service{
- nip47NotificationQueue: nip47NotificationQueue,
+ nip47NotificationQueue: notifications.NewNip47NotificationQueue(),
cfg: cfg,
db: db,
permissionsService: permissions.NewPermissionsService(db, eventPublisher),
+ transactionsService: transactions.NewTransactionsService(db),
eventPublisher: eventPublisher,
keys: keys,
}
}
+func (svc *nip47Service) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
+ svc.nip47NotificationQueue.AddToQueue(event)
+}
+
func (svc *nip47Service) StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) {
- nip47Notifier := notifications.NewNip47Notifier(relay, svc.db, svc.cfg, svc.keys, svc.permissionsService, lnClient)
+ nip47Notifier := notifications.NewNip47Notifier(relay, svc.db, svc.cfg, svc.keys, svc.permissionsService, svc.transactionsService, lnClient)
go func() {
for {
select {
diff --git a/nip47/notifications/nip47_notification_queue.go b/nip47/notifications/nip47_notification_queue.go
index e5bb5b4b..0d0ccb8a 100644
--- a/nip47/notifications/nip47_notification_queue.go
+++ b/nip47/notifications/nip47_notification_queue.go
@@ -1,16 +1,13 @@
package notifications
import (
- "context"
- "errors"
-
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
)
type Nip47NotificationQueue interface {
- events.EventSubscriber
Channel() <-chan *events.Event
+ AddToQueue(event *events.Event)
}
type nip47NotificationQueue struct {
@@ -26,13 +23,13 @@ func NewNip47NotificationQueue() *nip47NotificationQueue {
}
}
-func (q *nip47NotificationQueue) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) error {
+func (q *nip47NotificationQueue) AddToQueue(event *events.Event) {
select {
case q.channel <- event: // Put in the channel unless it is full
- return nil
+ // successfully sent to channel
default:
+ // channel full
logger.Logger.WithField("event", event).Error("NIP47NotificationQueue channel full. Discarding value")
- return errors.New("nip-47 notification queue full")
}
}
diff --git a/nip47/notifications/nip47_notifier.go b/nip47/notifications/nip47_notifier.go
index e0e7dbd4..080da16b 100644
--- a/nip47/notifications/nip47_notifier.go
+++ b/nip47/notifications/nip47_notifier.go
@@ -3,108 +3,115 @@ package notifications
import (
"context"
"encoding/json"
- "errors"
"github.com/getAlby/hub/config"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
+ nostrmodels "github.com/getAlby/hub/nostr/models"
"github.com/getAlby/hub/service/keys"
+ "github.com/getAlby/hub/transactions"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
-type Relay interface {
- Publish(ctx context.Context, event nostr.Event) error
-}
-
type Nip47Notifier struct {
- relay Relay
- cfg config.Config
- keys keys.Keys
- lnClient lnclient.LNClient
- db *gorm.DB
- permissionsSvc permissions.PermissionsService
+ relay nostrmodels.Relay
+ cfg config.Config
+ keys keys.Keys
+ lnClient lnclient.LNClient
+ db *gorm.DB
+ permissionsSvc permissions.PermissionsService
+ transactionsService transactions.TransactionsService
}
-func NewNip47Notifier(relay Relay, db *gorm.DB, cfg config.Config, keys keys.Keys, permissionsSvc permissions.PermissionsService, lnClient lnclient.LNClient) *Nip47Notifier {
+func NewNip47Notifier(relay nostrmodels.Relay, db *gorm.DB, cfg config.Config, keys keys.Keys, permissionsSvc permissions.PermissionsService, transactionsService transactions.TransactionsService, lnClient lnclient.LNClient) *Nip47Notifier {
return &Nip47Notifier{
- relay: relay,
- cfg: cfg,
- db: db,
- lnClient: lnClient,
- permissionsSvc: permissionsSvc,
- keys: keys,
+ relay: relay,
+ cfg: cfg,
+ db: db,
+ lnClient: lnClient,
+ permissionsSvc: permissionsSvc,
+ transactionsService: transactionsService,
+ keys: keys,
}
}
-func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.Event) error {
+func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.Event) {
+
+ // TODO: should listen to transaction service events instead
+ // then self-payments will also trigger NIP-47 notifications
switch event.Event {
case "nwc_payment_received":
- paymentReceivedEventProperties, ok := event.Properties.(*events.PaymentReceivedEventProperties)
+ lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
if !ok {
logger.Logger.WithField("event", event).Error("Failed to cast event")
- return errors.New("failed to cast event")
+ return
}
- transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentReceivedEventProperties.PaymentHash)
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ transaction, err := notifier.transactionsService.LookupTransaction(ctx, lnClientTransaction.PaymentHash, &transactionType, notifier.lnClient, nil)
if err != nil {
logger.Logger.
- WithField("paymentHash", paymentReceivedEventProperties.PaymentHash).
+ WithField("paymentHash", lnClientTransaction.PaymentHash).
WithError(err).
- Error("Failed to lookup invoice by payment hash")
- return err
+ Error("Failed to lookup transaction by payment hash")
+ return
}
notification := PaymentReceivedNotification{
- Transaction: *transaction,
+ Transaction: *models.ToNip47Transaction(transaction),
}
notifier.notifySubscribers(ctx, &Notification{
Notification: notification,
NotificationType: PAYMENT_RECEIVED_NOTIFICATION,
- }, nostr.Tags{})
+ }, nostr.Tags{}, transaction.AppId)
case "nwc_payment_sent":
- paymentSentEventProperties, ok := event.Properties.(*events.PaymentSentEventProperties)
+ paymentSentEventProperties, ok := event.Properties.(*lnclient.Transaction)
if !ok {
logger.Logger.WithField("event", event).Error("Failed to cast event")
- return errors.New("failed to cast event")
+ return
}
- transaction, err := notifier.lnClient.LookupInvoice(ctx, paymentSentEventProperties.PaymentHash)
+ transactionType := constants.TRANSACTION_TYPE_OUTGOING
+ transaction, err := notifier.transactionsService.LookupTransaction(ctx, paymentSentEventProperties.PaymentHash, &transactionType, notifier.lnClient, nil)
if err != nil {
logger.Logger.
WithField("paymentHash", paymentSentEventProperties.PaymentHash).
WithError(err).
Error("Failed to lookup invoice by payment hash")
- return err
+ return
}
notification := PaymentSentNotification{
- Transaction: *transaction,
+ Transaction: *models.ToNip47Transaction(transaction),
}
notifier.notifySubscribers(ctx, &Notification{
Notification: notification,
NotificationType: PAYMENT_SENT_NOTIFICATION,
- }, nostr.Tags{})
+ }, nostr.Tags{}, transaction.AppId)
}
-
- return nil
}
-func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notification *Notification, tags nostr.Tags) {
+func (notifier *Nip47Notifier) notifySubscribers(ctx context.Context, notification *Notification, tags nostr.Tags, appId *uint) {
apps := []db.App{}
// TODO: join apps and permissions
notifier.db.Find(&apps)
for _, app := range apps {
- hasPermission, _, _ := notifier.permissionsSvc.HasPermission(&app, permissions.NOTIFICATIONS_SCOPE, 0)
+ if app.Isolated && (appId == nil || app.ID != *appId) {
+ continue
+ }
+
+ hasPermission, _, _ := notifier.permissionsSvc.HasPermission(&app, constants.NOTIFICATIONS_SCOPE)
if !hasPermission {
continue
}
diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go
index e35b31f2..bf7030fb 100644
--- a/nip47/notifications/nip47_notifier_test.go
+++ b/nip47/notifications/nip47_notifier_test.go
@@ -3,18 +3,34 @@ package notifications
import (
"context"
"encoding/json"
- "log"
"testing"
+ "time"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
+ "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
- "github.com/nbd-wtf/go-nostr"
+ "github.com/getAlby/hub/transactions"
"github.com/nbd-wtf/go-nostr/nip04"
"github.com/stretchr/testify/assert"
)
+type mockConsumer struct {
+ nip47NotificationQueue Nip47NotificationQueue
+}
+
+func NewMockConsumer(nip47NotificationQueue Nip47NotificationQueue) *mockConsumer {
+ return &mockConsumer{
+ nip47NotificationQueue: nip47NotificationQueue,
+ }
+}
+
+func (svc *mockConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
+ svc.nip47NotificationQueue.AddToQueue(event)
+}
+
func TestSendNotification_PaymentReceived(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
@@ -27,18 +43,34 @@ func TestSendNotification_PaymentReceived(t *testing.T) {
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
- Scope: permissions.NOTIFICATIONS_SCOPE,
+ Scope: constants.NOTIFICATIONS_SCOPE,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
+ feesPaid := uint64(tests.MockLNClientTransaction.FeesPaid)
+ settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
+ err = svc.DB.Create(&db.Transaction{
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ Description: tests.MockLNClientTransaction.Description,
+ DescriptionHash: tests.MockLNClientTransaction.DescriptionHash,
+ Preimage: &tests.MockLNClientTransaction.Preimage,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ AmountMsat: uint64(tests.MockLNClientTransaction.Amount),
+ FeeMsat: &feesPaid,
+ SettledAt: &settledAt,
+ AppId: &app.ID,
+ }).Error
+ assert.NoError(t, err)
+
nip47NotificationQueue := NewNip47NotificationQueue()
- svc.EventPublisher.RegisterSubscriber(nip47NotificationQueue)
+ svc.EventPublisher.RegisterSubscriber(NewMockConsumer(nip47NotificationQueue))
testEvent := &events.Event{
Event: "nwc_payment_received",
- Properties: &events.PaymentReceivedEventProperties{
- PaymentHash: tests.MockPaymentHash,
+ Properties: &lnclient.Transaction{
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
},
}
@@ -47,17 +79,18 @@ func TestSendNotification_PaymentReceived(t *testing.T) {
receivedEvent := <-nip47NotificationQueue.Channel()
assert.Equal(t, testEvent, receivedEvent)
- relay := NewMockRelay()
+ relay := tests.NewMockRelay()
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
- notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient)
+ notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
notifier.ConsumeEvent(ctx, receivedEvent)
- assert.NotNil(t, relay.publishedEvent)
- assert.NotEmpty(t, relay.publishedEvent.Content)
+ assert.NotNil(t, relay.PublishedEvent)
+ assert.NotEmpty(t, relay.PublishedEvent.Content)
- decrypted, err := nip04.Decrypt(relay.publishedEvent.Content, ss)
+ decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
assert.NoError(t, err)
unmarshalledResponse := Notification{
Notification: &PaymentReceivedNotification{},
@@ -68,15 +101,15 @@ func TestSendNotification_PaymentReceived(t *testing.T) {
assert.Equal(t, PAYMENT_RECEIVED_NOTIFICATION, unmarshalledResponse.NotificationType)
transaction := (unmarshalledResponse.Notification.(*PaymentReceivedNotification))
- assert.Equal(t, tests.MockTransaction.Type, transaction.Type)
- assert.Equal(t, tests.MockTransaction.Invoice, transaction.Invoice)
- assert.Equal(t, tests.MockTransaction.Description, transaction.Description)
- assert.Equal(t, tests.MockTransaction.DescriptionHash, transaction.DescriptionHash)
- assert.Equal(t, tests.MockTransaction.Preimage, transaction.Preimage)
- assert.Equal(t, tests.MockTransaction.PaymentHash, transaction.PaymentHash)
- assert.Equal(t, tests.MockTransaction.Amount, transaction.Amount)
- assert.Equal(t, tests.MockTransaction.FeesPaid, transaction.FeesPaid)
- assert.Equal(t, tests.MockTransaction.SettledAt, transaction.SettledAt)
+ assert.Equal(t, constants.TRANSACTION_TYPE_INCOMING, transaction.Type)
+ assert.Equal(t, tests.MockLNClientTransaction.Invoice, transaction.Invoice)
+ assert.Equal(t, tests.MockLNClientTransaction.Description, transaction.Description)
+ assert.Equal(t, tests.MockLNClientTransaction.DescriptionHash, transaction.DescriptionHash)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, transaction.Preimage)
+ assert.Equal(t, tests.MockLNClientTransaction.PaymentHash, transaction.PaymentHash)
+ assert.Equal(t, tests.MockLNClientTransaction.Amount, transaction.Amount)
+ assert.Equal(t, tests.MockLNClientTransaction.FeesPaid, transaction.FeesPaid)
+ assert.Equal(t, tests.MockLNClientTransaction.SettledAt, transaction.SettledAt)
}
func TestSendNotification_PaymentSent(t *testing.T) {
@@ -91,18 +124,34 @@ func TestSendNotification_PaymentSent(t *testing.T) {
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
- Scope: permissions.NOTIFICATIONS_SCOPE,
+ Scope: constants.NOTIFICATIONS_SCOPE,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
+ feesPaid := uint64(tests.MockLNClientTransaction.FeesPaid)
+ settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
+ err = svc.DB.Create(&db.Transaction{
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ Description: tests.MockLNClientTransaction.Description,
+ DescriptionHash: tests.MockLNClientTransaction.DescriptionHash,
+ Preimage: &tests.MockLNClientTransaction.Preimage,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ AmountMsat: uint64(tests.MockLNClientTransaction.Amount),
+ FeeMsat: &feesPaid,
+ SettledAt: &settledAt,
+ AppId: &app.ID,
+ }).Error
+ assert.NoError(t, err)
+
nip47NotificationQueue := NewNip47NotificationQueue()
- svc.EventPublisher.RegisterSubscriber(nip47NotificationQueue)
+ svc.EventPublisher.RegisterSubscriber(NewMockConsumer(nip47NotificationQueue))
testEvent := &events.Event{
Event: "nwc_payment_sent",
- Properties: &events.PaymentSentEventProperties{
- PaymentHash: tests.MockPaymentHash,
+ Properties: &lnclient.Transaction{
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
},
}
@@ -111,17 +160,18 @@ func TestSendNotification_PaymentSent(t *testing.T) {
receivedEvent := <-nip47NotificationQueue.Channel()
assert.Equal(t, testEvent, receivedEvent)
- relay := NewMockRelay()
+ relay := tests.NewMockRelay()
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
- notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient)
+ notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
notifier.ConsumeEvent(ctx, receivedEvent)
- assert.NotNil(t, relay.publishedEvent)
- assert.NotEmpty(t, relay.publishedEvent.Content)
+ assert.NotNil(t, relay.PublishedEvent)
+ assert.NotEmpty(t, relay.PublishedEvent.Content)
- decrypted, err := nip04.Decrypt(relay.publishedEvent.Content, ss)
+ decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
assert.NoError(t, err)
unmarshalledResponse := Notification{
Notification: &PaymentReceivedNotification{},
@@ -132,15 +182,15 @@ func TestSendNotification_PaymentSent(t *testing.T) {
assert.Equal(t, PAYMENT_SENT_NOTIFICATION, unmarshalledResponse.NotificationType)
transaction := (unmarshalledResponse.Notification.(*PaymentReceivedNotification))
- assert.Equal(t, tests.MockTransaction.Type, transaction.Type)
- assert.Equal(t, tests.MockTransaction.Invoice, transaction.Invoice)
- assert.Equal(t, tests.MockTransaction.Description, transaction.Description)
- assert.Equal(t, tests.MockTransaction.DescriptionHash, transaction.DescriptionHash)
- assert.Equal(t, tests.MockTransaction.Preimage, transaction.Preimage)
- assert.Equal(t, tests.MockTransaction.PaymentHash, transaction.PaymentHash)
- assert.Equal(t, tests.MockTransaction.Amount, transaction.Amount)
- assert.Equal(t, tests.MockTransaction.FeesPaid, transaction.FeesPaid)
- assert.Equal(t, tests.MockTransaction.SettledAt, transaction.SettledAt)
+ assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type)
+ assert.Equal(t, tests.MockLNClientTransaction.Invoice, transaction.Invoice)
+ assert.Equal(t, tests.MockLNClientTransaction.Description, transaction.Description)
+ assert.Equal(t, tests.MockLNClientTransaction.DescriptionHash, transaction.DescriptionHash)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, transaction.Preimage)
+ assert.Equal(t, tests.MockLNClientTransaction.PaymentHash, transaction.PaymentHash)
+ assert.Equal(t, tests.MockLNClientTransaction.Amount, transaction.Amount)
+ assert.Equal(t, tests.MockLNClientTransaction.FeesPaid, transaction.FeesPaid)
+ assert.Equal(t, tests.MockLNClientTransaction.SettledAt, transaction.SettledAt)
}
func TestSendNotificationNoPermission(t *testing.T) {
@@ -151,12 +201,16 @@ func TestSendNotificationNoPermission(t *testing.T) {
_, _, err = tests.CreateApp(svc)
assert.NoError(t, err)
+ svc.DB.Create(&db.Transaction{
+ PaymentHash: tests.MockPaymentHash,
+ })
+
nip47NotificationQueue := NewNip47NotificationQueue()
- svc.EventPublisher.RegisterSubscriber(nip47NotificationQueue)
+ svc.EventPublisher.RegisterSubscriber(NewMockConsumer(nip47NotificationQueue))
testEvent := &events.Event{
Event: "nwc_payment_received",
- Properties: &events.PaymentReceivedEventProperties{
+ Properties: &lnclient.Transaction{
PaymentHash: tests.MockPaymentHash,
},
}
@@ -166,26 +220,13 @@ func TestSendNotificationNoPermission(t *testing.T) {
receivedEvent := <-nip47NotificationQueue.Channel()
assert.Equal(t, testEvent, receivedEvent)
- relay := NewMockRelay()
+ relay := tests.NewMockRelay()
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
+ transactionsSvc := transactions.NewTransactionsService(svc.DB)
- notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, svc.LNClient)
+ notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
notifier.ConsumeEvent(ctx, receivedEvent)
- assert.Nil(t, relay.publishedEvent)
-}
-
-type mockRelay struct {
- publishedEvent *nostr.Event
-}
-
-func NewMockRelay() *mockRelay {
- return &mockRelay{}
-}
-
-func (relay *mockRelay) Publish(ctx context.Context, event nostr.Event) error {
- log.Printf("Mock Publishing event %+v", event)
- relay.publishedEvent = &event
- return nil
+ assert.Nil(t, relay.PublishedEvent)
}
diff --git a/nip47/permissions/permissions.go b/nip47/permissions/permissions.go
index 7040c5f4..1266bdaa 100644
--- a/nip47/permissions/permissions.go
+++ b/nip47/permissions/permissions.go
@@ -5,6 +5,7 @@ import (
"slices"
"time"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
@@ -15,17 +16,6 @@ import (
"gorm.io/gorm"
)
-const (
- PAY_INVOICE_SCOPE = "pay_invoice" // also covers pay_keysend and multi_* payment methods
- GET_BALANCE_SCOPE = "get_balance"
- GET_INFO_SCOPE = "get_info"
- MAKE_INVOICE_SCOPE = "make_invoice"
- LOOKUP_INVOICE_SCOPE = "lookup_invoice"
- LIST_TRANSACTIONS_SCOPE = "list_transactions"
- SIGN_MESSAGE_SCOPE = "sign_message"
- NOTIFICATIONS_SCOPE = "notifications" // covers all notification types
-)
-
type permissionsService struct {
db *gorm.DB
eventPublisher events.EventPublisher
@@ -33,8 +23,7 @@ type permissionsService struct {
// TODO: does this need to be a service?
type PermissionsService interface {
- HasPermission(app *db.App, requestMethod string, amount uint64) (result bool, code string, message string)
- GetBudgetUsage(appPermission *db.AppPermission) uint64
+ HasPermission(app *db.App, requestMethod string) (result bool, code string, message string)
GetPermittedMethods(app *db.App, lnClient lnclient.LNClient) []string
PermitsNotifications(app *db.App) bool
}
@@ -46,7 +35,7 @@ func NewPermissionsService(db *gorm.DB, eventPublisher events.EventPublisher) *p
}
}
-func (svc *permissionsService) HasPermission(app *db.App, scope string, amountMsat uint64) (result bool, code string, message string) {
+func (svc *permissionsService) HasPermission(app *db.App, scope string) (result bool, code string, message string) {
appPermission := db.AppPermission{}
findPermissionResult := svc.db.Find(&appPermission, &db.AppPermission{
@@ -69,28 +58,9 @@ func (svc *permissionsService) HasPermission(app *db.App, scope string, amountMs
return false, models.ERROR_EXPIRED, "This app has expired"
}
- if scope == PAY_INVOICE_SCOPE {
- maxAmount := appPermission.MaxAmount
- if maxAmount != 0 {
- budgetUsage := svc.GetBudgetUsage(&appPermission)
-
- if budgetUsage+amountMsat/1000 > uint64(maxAmount) {
- return false, models.ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment"
- }
- }
- }
return true, "", ""
}
-func (svc *permissionsService) GetBudgetUsage(appPermission *db.AppPermission) uint64 {
- var result struct {
- Sum uint64
- }
- // TODO: discard failed payments from this check instead of checking payments that have a preimage
- svc.db.Table("payments").Select("SUM(amount) as sum").Where("app_id = ? AND preimage IS NOT NULL AND created_at > ?", appPermission.AppId, getStartOfBudget(appPermission.BudgetRenewal)).Scan(&result)
- return result.Sum
-}
-
func (svc *permissionsService) GetPermittedMethods(app *db.App, lnClient lnclient.LNClient) []string {
appPermissions := []db.AppPermission{}
svc.db.Where("app_id = ?", app.ID).Find(&appPermissions)
@@ -114,7 +84,7 @@ func (svc *permissionsService) PermitsNotifications(app *db.App) bool {
notificationPermission := db.AppPermission{}
err := svc.db.First(¬ificationPermission, &db.AppPermission{
AppId: app.ID,
- Scope: NOTIFICATIONS_SCOPE,
+ Scope: constants.NOTIFICATIONS_SCOPE,
}).Error
if err != nil {
return false
@@ -123,30 +93,6 @@ func (svc *permissionsService) PermitsNotifications(app *db.App) bool {
return true
}
-func getStartOfBudget(budget_type string) time.Time {
- now := time.Now()
- switch budget_type {
- case models.BUDGET_RENEWAL_DAILY:
- // TODO: Use the location of the user, instead of the server
- return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
- case models.BUDGET_RENEWAL_WEEKLY:
- weekday := now.Weekday()
- var startOfWeek time.Time
- if weekday == 0 {
- startOfWeek = now.AddDate(0, 0, -6)
- } else {
- startOfWeek = now.AddDate(0, 0, -int(weekday)+1)
- }
- return time.Date(startOfWeek.Year(), startOfWeek.Month(), startOfWeek.Day(), 0, 0, 0, 0, startOfWeek.Location())
- case models.BUDGET_RENEWAL_MONTHLY:
- return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
- case models.BUDGET_RENEWAL_YEARLY:
- return time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, now.Location())
- default: //"never"
- return time.Time{}
- }
-}
-
func scopesToRequestMethods(scopes []string) []string {
requestMethods := []string{}
@@ -159,19 +105,19 @@ func scopesToRequestMethods(scopes []string) []string {
func scopeToRequestMethods(scope string) []string {
switch scope {
- case PAY_INVOICE_SCOPE:
+ case constants.PAY_INVOICE_SCOPE:
return []string{models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD}
- case GET_BALANCE_SCOPE:
+ case constants.GET_BALANCE_SCOPE:
return []string{models.GET_BALANCE_METHOD}
- case GET_INFO_SCOPE:
+ case constants.GET_INFO_SCOPE:
return []string{models.GET_INFO_METHOD}
- case MAKE_INVOICE_SCOPE:
+ case constants.MAKE_INVOICE_SCOPE:
return []string{models.MAKE_INVOICE_METHOD}
- case LOOKUP_INVOICE_SCOPE:
+ case constants.LOOKUP_INVOICE_SCOPE:
return []string{models.LOOKUP_INVOICE_METHOD}
- case LIST_TRANSACTIONS_SCOPE:
+ case constants.LIST_TRANSACTIONS_SCOPE:
return []string{models.LIST_TRANSACTIONS_METHOD}
- case SIGN_MESSAGE_SCOPE:
+ case constants.SIGN_MESSAGE_SCOPE:
return []string{models.SIGN_MESSAGE_METHOD}
}
return []string{}
@@ -195,19 +141,19 @@ func RequestMethodsToScopes(requestMethods []string) ([]string, error) {
func RequestMethodToScope(requestMethod string) (string, error) {
switch requestMethod {
case models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD:
- return PAY_INVOICE_SCOPE, nil
+ return constants.PAY_INVOICE_SCOPE, nil
case models.GET_BALANCE_METHOD:
- return GET_BALANCE_SCOPE, nil
+ return constants.GET_BALANCE_SCOPE, nil
case models.GET_INFO_METHOD:
- return GET_INFO_SCOPE, nil
+ return constants.GET_INFO_SCOPE, nil
case models.MAKE_INVOICE_METHOD:
- return MAKE_INVOICE_SCOPE, nil
+ return constants.MAKE_INVOICE_SCOPE, nil
case models.LOOKUP_INVOICE_METHOD:
- return LOOKUP_INVOICE_SCOPE, nil
+ return constants.LOOKUP_INVOICE_SCOPE, nil
case models.LIST_TRANSACTIONS_METHOD:
- return LIST_TRANSACTIONS_SCOPE, nil
+ return constants.LIST_TRANSACTIONS_SCOPE, nil
case models.SIGN_MESSAGE_METHOD:
- return SIGN_MESSAGE_SCOPE, nil
+ return constants.SIGN_MESSAGE_SCOPE, nil
}
logger.Logger.WithField("request_method", requestMethod).Error("Unsupported request method")
return "", fmt.Errorf("unsupported request method: %s", requestMethod)
@@ -215,13 +161,13 @@ func RequestMethodToScope(requestMethod string) (string, error) {
func AllScopes() []string {
return []string{
- PAY_INVOICE_SCOPE,
- GET_BALANCE_SCOPE,
- GET_INFO_SCOPE,
- MAKE_INVOICE_SCOPE,
- LOOKUP_INVOICE_SCOPE,
- LIST_TRANSACTIONS_SCOPE,
- SIGN_MESSAGE_SCOPE,
- NOTIFICATIONS_SCOPE,
+ constants.PAY_INVOICE_SCOPE,
+ constants.GET_BALANCE_SCOPE,
+ constants.GET_INFO_SCOPE,
+ constants.MAKE_INVOICE_SCOPE,
+ constants.LOOKUP_INVOICE_SCOPE,
+ constants.LIST_TRANSACTIONS_SCOPE,
+ constants.SIGN_MESSAGE_SCOPE,
+ constants.NOTIFICATIONS_SCOPE,
}
}
diff --git a/nip47/permissions/permissions_test.go b/nip47/permissions/permissions_test.go
index a5224bcc..f1eb9e38 100644
--- a/nip47/permissions/permissions_test.go
+++ b/nip47/permissions/permissions_test.go
@@ -4,6 +4,7 @@ import (
"testing"
"time"
+ "github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/tests"
@@ -19,7 +20,7 @@ func TestHasPermission_NoPermission(t *testing.T) {
assert.NoError(t, err)
permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher)
- result, code, message := permissionsSvc.HasPermission(app, PAY_INVOICE_SCOPE, 100)
+ result, code, message := permissionsSvc.HasPermission(app, constants.PAY_INVOICE_SCOPE)
assert.False(t, result)
assert.Equal(t, models.ERROR_RESTRICTED, code)
assert.Equal(t, "This app does not have the pay_invoice scope", message)
@@ -38,8 +39,8 @@ func TestHasPermission_Expired(t *testing.T) {
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
- Scope: PAY_INVOICE_SCOPE,
- MaxAmount: 100,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ MaxAmountSat: 100,
BudgetRenewal: budgetRenewal,
ExpiresAt: &expiresAt,
}
@@ -47,13 +48,14 @@ func TestHasPermission_Expired(t *testing.T) {
assert.NoError(t, err)
permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher)
- result, code, message := permissionsSvc.HasPermission(app, PAY_INVOICE_SCOPE, 100)
+ result, code, message := permissionsSvc.HasPermission(app, constants.PAY_INVOICE_SCOPE)
assert.False(t, result)
assert.Equal(t, models.ERROR_EXPIRED, code)
assert.Equal(t, "This app has expired", message)
}
-func TestHasPermission_Exceeded(t *testing.T) {
+// TODO: move to transactions service
+/*func TestHasPermission_Exceeded(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
@@ -67,7 +69,7 @@ func TestHasPermission_Exceeded(t *testing.T) {
AppId: app.ID,
App: *app,
Scope: models.PAY_INVOICE_METHOD,
- MaxAmount: 10,
+ MaxAmountSat: 10,
BudgetRenewal: budgetRenewal,
ExpiresAt: &expiresAt,
}
@@ -79,7 +81,7 @@ func TestHasPermission_Exceeded(t *testing.T) {
assert.False(t, result)
assert.Equal(t, models.ERROR_QUOTA_EXCEEDED, code)
assert.Equal(t, "Insufficient budget remaining to make payment", message)
-}
+}*/
func TestHasPermission_OK(t *testing.T) {
defer tests.RemoveTestService()
@@ -95,7 +97,7 @@ func TestHasPermission_OK(t *testing.T) {
AppId: app.ID,
App: *app,
Scope: models.PAY_INVOICE_METHOD,
- MaxAmount: 10,
+ MaxAmountSat: 10,
BudgetRenewal: budgetRenewal,
ExpiresAt: &expiresAt,
}
@@ -103,7 +105,7 @@ func TestHasPermission_OK(t *testing.T) {
assert.NoError(t, err)
permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher)
- result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD, 10*1000)
+ result, code, message := permissionsSvc.HasPermission(app, models.PAY_INVOICE_METHOD)
assert.True(t, result)
assert.Empty(t, code)
assert.Empty(t, message)
diff --git a/nip47/publish_nip47_info.go b/nip47/publish_nip47_info.go
index fe7877ae..259be1b3 100644
--- a/nip47/publish_nip47_info.go
+++ b/nip47/publish_nip47_info.go
@@ -7,10 +7,11 @@ import (
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/nip47/models"
+ nostrmodels "github.com/getAlby/hub/nostr/models"
"github.com/nbd-wtf/go-nostr"
)
-func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient) error {
+func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, lnClient lnclient.LNClient) error {
capabilities := lnClient.GetSupportedNIP47Methods()
if len(lnClient.GetSupportedNIP47NotificationTypes()) > 0 {
capabilities = append(capabilities, "notifications")
diff --git a/nostr/models/models.go b/nostr/models/models.go
new file mode 100644
index 00000000..6260675d
--- /dev/null
+++ b/nostr/models/models.go
@@ -0,0 +1,11 @@
+package models
+
+import (
+ "context"
+
+ "github.com/nbd-wtf/go-nostr"
+)
+
+type Relay interface {
+ Publish(ctx context.Context, event nostr.Event) error
+}
diff --git a/service/models.go b/service/models.go
index 3c0961b9..edb41bd7 100644
--- a/service/models.go
+++ b/service/models.go
@@ -6,6 +6,7 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/service/keys"
+ "github.com/getAlby/hub/transactions"
"gorm.io/gorm"
)
@@ -18,6 +19,7 @@ type Service interface {
GetAlbyOAuthSvc() alby.AlbyOAuthService
GetEventPublisher() events.EventPublisher
GetLNClient() lnclient.LNClient
+ GetTransactionsService() transactions.TransactionsService
GetDB() *gorm.DB
GetConfig() config.Config
GetKeys() keys.Keys
diff --git a/service/service.go b/service/service.go
index 57e71e58..36645de7 100644
--- a/service/service.go
+++ b/service/service.go
@@ -20,6 +20,7 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/service/keys"
+ "github.com/getAlby/hub/transactions"
"github.com/getAlby/hub/version"
"github.com/getAlby/hub/config"
@@ -32,15 +33,16 @@ import (
type service struct {
cfg config.Config
- db *gorm.DB
- lnClient lnclient.LNClient
- albyOAuthSvc alby.AlbyOAuthService
- eventPublisher events.EventPublisher
- ctx context.Context
- wg *sync.WaitGroup
- nip47Service nip47.Nip47Service
- appCancelFn context.CancelFunc
- keys keys.Keys
+ db *gorm.DB
+ lnClient lnclient.LNClient
+ transactionsService transactions.TransactionsService
+ albyOAuthSvc alby.AlbyOAuthService
+ eventPublisher events.EventPublisher
+ ctx context.Context
+ wg *sync.WaitGroup
+ nip47Service nip47.Nip47Service
+ appCancelFn context.CancelFunc
+ keys keys.Keys
}
func NewService(ctx context.Context) (*service, error) {
@@ -96,16 +98,22 @@ func NewService(ctx context.Context) (*service, error) {
var wg sync.WaitGroup
svc := &service{
- cfg: cfg,
- ctx: ctx,
- wg: &wg,
- eventPublisher: eventPublisher,
- albyOAuthSvc: alby.NewAlbyOAuthService(gormDB, cfg, keys, eventPublisher),
- nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher),
- db: gormDB,
- keys: keys,
+ cfg: cfg,
+ ctx: ctx,
+ wg: &wg,
+ eventPublisher: eventPublisher,
+ albyOAuthSvc: alby.NewAlbyOAuthService(gormDB, cfg, keys, eventPublisher),
+ nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher),
+ transactionsService: transactions.NewTransactionsService(gormDB),
+ db: gormDB,
+ keys: keys,
}
+ // Note: order is important here: transactions service will update transactions
+ // from payment events, which will then be consumed by the NIP-47 service to send notifications
+ // TODO: transactions service should fire its own events
+ eventPublisher.RegisterSubscriber(svc.transactionsService)
+ eventPublisher.RegisterSubscriber(svc.nip47Service)
eventPublisher.RegisterSubscriber(svc.albyOAuthSvc)
eventPublisher.Publish(&events.Event{
@@ -148,7 +156,7 @@ func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscripti
// loop through incoming events
for event := range sub.Events {
- go svc.nip47Service.HandleEvent(ctx, sub, event, svc.lnClient)
+ go svc.nip47Service.HandleEvent(ctx, sub.Relay, event, svc.lnClient)
}
logger.Logger.Info("Relay subscription events channel ended")
}()
@@ -235,6 +243,10 @@ func (svc *service) GetLNClient() lnclient.LNClient {
return svc.lnClient
}
+func (svc *service) GetTransactionsService() transactions.TransactionsService {
+ return svc.transactionsService
+}
+
func (svc *service) GetKeys() keys.Keys {
return svc.keys
}
diff --git a/service/start.go b/service/start.go
index 43731161..daf4e684 100644
--- a/service/start.go
+++ b/service/start.go
@@ -160,7 +160,7 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e
LNDAddress, _ := svc.cfg.Get("LNDAddress", encryptionKey)
LNDCertHex, _ := svc.cfg.Get("LNDCertHex", encryptionKey)
LNDMacaroonHex, _ := svc.cfg.Get("LNDMacaroonHex", encryptionKey)
- lnClient, err = lnd.NewLNDService(ctx, LNDAddress, LNDCertHex, LNDMacaroonHex)
+ lnClient, err = lnd.NewLNDService(ctx, svc.eventPublisher, LNDAddress, LNDCertHex, LNDMacaroonHex)
case config.LDKBackendType:
Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey)
LDKWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "ldk")
diff --git a/tests/create_app.go b/tests/create_app.go
index 9a72ba10..830b7d3c 100644
--- a/tests/create_app.go
+++ b/tests/create_app.go
@@ -8,6 +8,10 @@ import (
func CreateApp(svc *TestService) (app *db.App, ss []byte, err error) {
senderPrivkey := nostr.GeneratePrivateKey()
+ return CreateAppWithPrivateKey(svc, senderPrivkey)
+}
+func CreateAppWithPrivateKey(svc *TestService, senderPrivkey string) (app *db.App, ss []byte, err error) {
+
senderPubkey, err := nostr.GetPublicKey(senderPrivkey)
if err != nil {
return nil, nil, err
diff --git a/tests/create_mock_relay.go b/tests/create_mock_relay.go
new file mode 100644
index 00000000..b28f6a8f
--- /dev/null
+++ b/tests/create_mock_relay.go
@@ -0,0 +1,22 @@
+package tests
+
+import (
+ "context"
+
+ "github.com/getAlby/hub/logger"
+ "github.com/nbd-wtf/go-nostr"
+)
+
+type mockRelay struct {
+ PublishedEvent *nostr.Event
+}
+
+func NewMockRelay() *mockRelay {
+ return &mockRelay{}
+}
+
+func (relay *mockRelay) Publish(ctx context.Context, event nostr.Event) error {
+ logger.Logger.WithField("event", event).Info("Mock Publishing event")
+ relay.PublishedEvent = &event
+ return nil
+}
diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go
index b1e5d625..a9043625 100644
--- a/tests/mock_ln_client.go
+++ b/tests/mock_ln_client.go
@@ -26,14 +26,14 @@ var MockNodeInfo = lnclient.NodeInfo{
var MockTime = time.Unix(1693876963, 0)
var MockTimeUnix = MockTime.Unix()
-var MockTransactions = []lnclient.Transaction{
+var MockLNClientTransactions = []lnclient.Transaction{
{
Type: "incoming",
Invoice: MockInvoice,
Description: "mock invoice 1",
DescriptionHash: "hash1",
Preimage: "preimage1",
- PaymentHash: "payment_hash_1",
+ PaymentHash: MockPaymentHash,
Amount: 1000,
FeesPaid: 50,
SettledAt: &MockTimeUnix,
@@ -48,15 +48,18 @@ var MockTransactions = []lnclient.Transaction{
Description: "mock invoice 2",
DescriptionHash: "hash2",
Preimage: "preimage2",
- PaymentHash: "payment_hash_2",
+ PaymentHash: MockPaymentHash,
Amount: 2000,
FeesPaid: 75,
SettledAt: &MockTimeUnix,
},
}
-var MockTransaction = &MockTransactions[0]
+var MockLNClientTransaction = &MockLNClientTransactions[0]
type MockLn struct {
+ PayInvoiceResponses []*lnclient.PayInvoiceResponse
+ PayInvoiceErrors []error
+ Pubkey string
}
func NewMockLn() (*MockLn, error) {
@@ -64,13 +67,21 @@ func NewMockLn() (*MockLn, error) {
}
func (mln *MockLn) SendPaymentSync(ctx context.Context, payReq string) (*lnclient.PayInvoiceResponse, error) {
+ if len(mln.PayInvoiceResponses) > 0 {
+ response := mln.PayInvoiceResponses[0]
+ err := mln.PayInvoiceErrors[0]
+ mln.PayInvoiceResponses = mln.PayInvoiceResponses[1:]
+ mln.PayInvoiceErrors = mln.PayInvoiceErrors[1:]
+ return response, err
+ }
+
return &lnclient.PayInvoiceResponse{
Preimage: "123preimage",
}, nil
}
-func (mln *MockLn) SendKeysend(ctx context.Context, amount uint64, destination, preimage string, custom_records []lnclient.TLVRecord) (preImage string, err error) {
- return "12345preimage", nil
+func (mln *MockLn) SendKeysend(ctx context.Context, amount uint64, destination string, custom_records []lnclient.TLVRecord) (paymentHash string, preimage string, fee uint64, err error) {
+ return "paymenthash", "12345preimage", 0, nil
}
func (mln *MockLn) GetBalance(ctx context.Context) (balance int64, err error) {
@@ -82,15 +93,15 @@ func (mln *MockLn) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err er
}
func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *lnclient.Transaction, err error) {
- return MockTransaction, nil
+ return MockLNClientTransaction, nil
}
func (mln *MockLn) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
- return MockTransaction, nil
+ return MockLNClientTransaction, nil
}
func (mln *MockLn) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (invoices []lnclient.Transaction, err error) {
- return MockTransactions, nil
+ return MockLNClientTransactions, nil
}
func (mln *MockLn) Shutdown() error {
return nil
@@ -167,3 +178,10 @@ func (mln *MockLn) GetSupportedNIP47Methods() []string {
func (mln *MockLn) GetSupportedNIP47NotificationTypes() []string {
return []string{"payment_received", "payment_sent"}
}
+func (mln *MockLn) GetPubkey() string {
+ if mln.Pubkey != "" {
+ return mln.Pubkey
+ }
+
+ return "123pubkey"
+}
diff --git a/tests/test_service.go b/tests/test_service.go
index ec3a5b6b..f58cfbef 100644
--- a/tests/test_service.go
+++ b/tests/test_service.go
@@ -2,6 +2,7 @@ package tests
import (
"os"
+ "strconv"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/db"
@@ -9,6 +10,7 @@ import (
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/service/keys"
+ "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@@ -25,7 +27,7 @@ func CreateTestService() (svc *TestService, err error) {
return nil, err
}
- logger.Init("")
+ logger.Init(strconv.Itoa(int(logrus.DebugLevel)))
appConfig := &config.AppConfig{
Workdir: ".test",
diff --git a/transactions/app_payments_test.go b/transactions/app_payments_test.go
new file mode 100644
index 00000000..023c7d07
--- /dev/null
+++ b/transactions/app_payments_test.go
@@ -0,0 +1,219 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSendPaymentSync_App_NoPermission(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.Equal(t, "app does not have pay_invoice scope", err.Error())
+ assert.Nil(t, transaction)
+}
+func TestSendPaymentSync_App_WithPermission(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, "123preimage", *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+}
+
+func TestSendPaymentSync_App_BudgetExceeded(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ MaxAmountSat: 1,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, NewQuotaExceededError())
+ assert.Nil(t, transaction)
+}
+
+func TestSendPaymentSync_App_BudgetExceeded_SettledPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ MaxAmountSat: 133, // invoice is 123 sats, but we also calculate fee reserves max of(10 sats or 1%)
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ // 1 sat payment pushes app over the limit
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: 1000,
+ CreatedAt: time.Now(),
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, NewQuotaExceededError())
+ assert.Nil(t, transaction)
+}
+func TestSendPaymentSync_App_BudgetExceeded_UnsettledPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ MaxAmountSat: 133, // invoice is 123 sats, but we also calculate fee reserves max of(10 sats or 1%)
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ // 1 sat payment pushes app over the limit
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: 1000,
+ CreatedAt: time.Now(),
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, NewQuotaExceededError())
+ assert.Nil(t, transaction)
+}
+
+func TestSendPaymentSync_App_BudgetNotExceeded_FailedPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ MaxAmountSat: 133, // invoice is 123 sats, but we also calculate fee reserves max of(10 sats or 1%)
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ // 1 sat payment would push app over the limit, but it failed so its not counted
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_FAILED,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: 1000,
+ CreatedAt: time.Now(),
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, "123preimage", *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+}
diff --git a/transactions/isolated_app_payments_test.go b/transactions/isolated_app_payments_test.go
new file mode 100644
index 00000000..bb1ad6d4
--- /dev/null
+++ b/transactions/isolated_app_payments_test.go
@@ -0,0 +1,312 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSendPaymentSync_IsolatedApp_NoBalance(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, NewInsufficientBalanceError())
+ assert.Nil(t, transaction)
+}
+
+func TestSendPaymentSync_IsolatedApp_BalanceInsufficient(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 132000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, NewInsufficientBalanceError())
+ assert.Nil(t, transaction)
+}
+
+func TestSendPaymentSync_IsolatedApp_BalanceSufficient(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, "123preimage", *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+}
+
+func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_OutstandingPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: 1000,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, NewInsufficientBalanceError())
+ assert.Nil(t, transaction)
+}
+
+func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_SettledPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: 1000,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.ErrorIs(t, err, NewInsufficientBalanceError())
+ assert.Nil(t, transaction)
+}
+
+func TestSendPaymentSync_IsolatedApp_BalanceSufficient_UnrelatedPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+ svc.DB.Create(&db.Transaction{
+ AppId: nil, // unrelated to this app
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: 1000,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, "123preimage", *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+}
+
+func TestSendPaymentSync_IsolatedApp_BalanceSufficient_FailedPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_FAILED,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: 1000,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, "123preimage", *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+}
diff --git a/transactions/keysend_test.go b/transactions/keysend_test.go
new file mode 100644
index 00000000..06be53c3
--- /dev/null
+++ b/transactions/keysend_test.go
@@ -0,0 +1,257 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/lnclient"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSendKeysend(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, nil, nil)
+
+ assert.NoError(t, err)
+ assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata)
+ assert.Equal(t, uint64(1000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Zero(t, *transaction.FeeReserveMsat)
+}
+
+func TestSendKeysend_App_NoPermission(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.Error(t, err)
+ assert.Equal(t, "app does not have pay_invoice scope", err.Error())
+ assert.Nil(t, transaction)
+}
+
+func TestSendKeysend_App_WithPermission(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata)
+ assert.Equal(t, uint64(1000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+ assert.Zero(t, *transaction.FeeReserveMsat)
+}
+
+func TestSendKeysend_App_BudgetExceeded(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ MaxAmountSat: 10, // not enough for the fee reserve
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.ErrorIs(t, err, NewQuotaExceededError())
+ assert.Nil(t, transaction)
+}
+func TestSendKeysend_App_BudgetNotExceeded(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ MaxAmountSat: 11, // fee reserve (10) + keysend amount (1)
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata)
+ assert.Equal(t, uint64(1000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+ assert.Zero(t, *transaction.FeeReserveMsat)
+}
+
+func TestSendKeysend_App_BalanceExceeded(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 10000, // invoice is 1000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.ErrorIs(t, err, NewInsufficientBalanceError())
+ assert.Nil(t, transaction)
+}
+
+func TestSendKeysend_App_BalanceSufficient(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 11000, // invoice is 1000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata)
+ assert.Equal(t, uint64(1000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+ assert.Zero(t, *transaction.FeeReserveMsat)
+}
+
+func TestSendKeysend_TLVs(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{
+ {
+ Type: 7629169,
+ Value: "48656C6C6F2C20776F726C64",
+ },
+ }, svc.LNClient, nil, nil)
+
+ assert.NoError(t, err)
+ assert.Equal(t, `{"destination":"fake destination","tlv_records":[{"type":7629169,"value":"48656C6C6F2C20776F726C64"}]}`, transaction.Metadata)
+ assert.Equal(t, uint64(1000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Zero(t, *transaction.FeeReserveMsat)
+}
diff --git a/transactions/list_transactions_test.go b/transactions/list_transactions_test.go
new file mode 100644
index 00000000..7fbe4dae
--- /dev/null
+++ b/transactions/list_transactions_test.go
@@ -0,0 +1,198 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestListTransactions(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_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ })
+ 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,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, nil, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, 1, len(incomingTransactions))
+ assert.Equal(t, uint64(123000), incomingTransactions[0].AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransactions[0].State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *incomingTransactions[0].Preimage)
+ assert.Nil(t, incomingTransactions[0].FeeReserveMsat)
+}
+
+func TestListTransactions_Unsettled(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_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ })
+ 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,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, nil, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, 2, len(incomingTransactions))
+}
+
+func TestListTransactions_Limit(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_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ Description: "first",
+ CreatedAt: time.Now().Add(1 * time.Minute),
+ })
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ Description: "second",
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 0, false, nil, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, 1, len(incomingTransactions))
+ assert.Equal(t, "first", incomingTransactions[0].Description)
+}
+
+func TestListTransactions_Offset(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_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ Description: "first",
+ CreatedAt: time.Now().Add(1 * time.Minute),
+ })
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ Description: "second",
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 1, false, nil, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, 1, len(incomingTransactions))
+ assert.Equal(t, "second", incomingTransactions[0].Description)
+}
+
+func TestListTransactions_FromUntil(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_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ Description: "first",
+ CreatedAt: time.Now().Add(10 * time.Minute),
+ })
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ Description: "second",
+ CreatedAt: time.Now().Add(5 * time.Minute),
+ })
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ Description: "third",
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ 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)
+ assert.NoError(t, err)
+ assert.Equal(t, 1, len(incomingTransactions))
+ assert.Equal(t, "second", incomingTransactions[0].Description)
+}
diff --git a/transactions/lookup_transaction_test.go b/transactions/lookup_transaction_test.go
new file mode 100644
index 00000000..972ff9a5
--- /dev/null
+++ b/transactions/lookup_transaction_test.go
@@ -0,0 +1,65 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLookupTransaction_IncomingPayment(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,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, nil, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_PENDING, incomingTransaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *incomingTransaction.Preimage)
+ assert.Nil(t, incomingTransaction.FeeReserveMsat)
+}
+
+func TestLookupTransaction_OutgoingPayment(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,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, nil, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), outgoingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_PENDING, outgoingTransaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *outgoingTransaction.Preimage)
+ assert.Nil(t, outgoingTransaction.FeeReserveMsat)
+}
diff --git a/transactions/make_invoice_test.go b/transactions/make_invoice_test.go
new file mode 100644
index 00000000..15b6ab2f
--- /dev/null
+++ b/transactions/make_invoice_test.go
@@ -0,0 +1,52 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMakeInvoice_NoApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, svc.LNClient, nil, nil)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_PENDING, transaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *transaction.Preimage)
+}
+
+func TestMakeInvoice_App(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_PENDING, transaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+}
diff --git a/transactions/notifications_test.go b/transactions/notifications_test.go
new file mode 100644
index 00000000..25721bd4
--- /dev/null
+++ b/transactions/notifications_test.go
@@ -0,0 +1,179 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/events"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNotifications_ReceivedKnownPayment(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,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ transactionsService.ConsumeEvent(ctx, &events.Event{
+ Event: "nwc_payment_received",
+ Properties: tests.MockLNClientTransaction,
+ }, map[string]interface{}{})
+
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, nil, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *incomingTransaction.Preimage)
+ assert.Nil(t, incomingTransaction.FeeReserveMsat)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(1), result.RowsAffected)
+}
+
+func TestNotifications_ReceivedUnknownPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ transactionsService.ConsumeEvent(ctx, &events.Event{
+ Event: "nwc_payment_received",
+ Properties: tests.MockLNClientTransaction,
+ }, map[string]interface{}{})
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *incomingTransaction.Preimage)
+ assert.Nil(t, incomingTransaction.FeeReserveMsat)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(1), result.RowsAffected)
+}
+
+func TestNotifications_SentKnownPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ feeReserve := uint64(10000)
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ AmountMsat: 123000,
+ FeeReserveMsat: &feeReserve,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ transactionsService.ConsumeEvent(ctx, &events.Event{
+ Event: "nwc_payment_sent",
+ Properties: tests.MockLNClientTransaction,
+ }, map[string]interface{}{})
+
+ transactionType := constants.TRANSACTION_TYPE_OUTGOING
+ outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), outgoingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, outgoingTransaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *outgoingTransaction.Preimage)
+ assert.Zero(t, *outgoingTransaction.FeeReserveMsat)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(1), result.RowsAffected)
+}
+
+func TestNotifications_SentUnknownPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(0), result.RowsAffected)
+
+ transactionsService.ConsumeEvent(ctx, &events.Event{
+ Event: "nwc_payment_sent",
+ Properties: tests.MockLNClientTransaction,
+ }, map[string]interface{}{})
+
+ transactionType := constants.TRANSACTION_TYPE_OUTGOING
+ outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), outgoingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, outgoingTransaction.State)
+ assert.Equal(t, tests.MockLNClientTransaction.Preimage, *outgoingTransaction.Preimage)
+ assert.Zero(t, *outgoingTransaction.FeeReserveMsat)
+
+ transactions = []db.Transaction{}
+ result = svc.DB.Find(&transactions)
+ assert.Equal(t, int64(1), result.RowsAffected)
+}
+
+func TestNotifications_FailedKnownPayment(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ feeReserve := uint64(10000)
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ PaymentRequest: tests.MockLNClientTransaction.Invoice,
+ PaymentHash: tests.MockLNClientTransaction.PaymentHash,
+ AmountMsat: 123000,
+ FeeReserveMsat: &feeReserve,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+
+ transactionsService.ConsumeEvent(ctx, &events.Event{
+ Event: "nwc_payment_failed_async",
+ Properties: tests.MockLNClientTransaction,
+ }, map[string]interface{}{})
+
+ transactionType := constants.TRANSACTION_TYPE_OUTGOING
+ outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, constants.TRANSACTION_STATE_FAILED, outgoingTransaction.State)
+ assert.Nil(t, outgoingTransaction.Preimage)
+ assert.Zero(t, *outgoingTransaction.FeeReserveMsat)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(1), result.RowsAffected)
+}
diff --git a/transactions/payments_test.go b/transactions/payments_test.go
new file mode 100644
index 00000000..ba75a03e
--- /dev/null
+++ b/transactions/payments_test.go
@@ -0,0 +1,82 @@
+package transactions
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/lnclient"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSendPaymentSync_NoApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Zero(t, *transaction.FeeReserveMsat)
+ assert.Equal(t, "123preimage", *transaction.Preimage)
+}
+
+func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ svc.LNClient.(*tests.MockLn).PayInvoiceErrors = append(svc.LNClient.(*tests.MockLn).PayInvoiceErrors, errors.New("Some error"))
+ svc.LNClient.(*tests.MockLn).PayInvoiceResponses = append(svc.LNClient.(*tests.MockLn).PayInvoiceResponses, nil)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil)
+
+ assert.Error(t, err)
+ assert.Nil(t, transaction)
+
+ transactionType := constants.TRANSACTION_TYPE_OUTGOING
+ transaction, err = transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil)
+ assert.Nil(t, err)
+
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_FAILED, transaction.State)
+ assert.Zero(t, *transaction.FeeReserveMsat)
+ assert.Nil(t, transaction.Preimage)
+}
+
+func TestSendPaymentSync_PendingHasFeeReserve(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // timeout will leave the payment as pending
+ svc.LNClient.(*tests.MockLn).PayInvoiceErrors = append(svc.LNClient.(*tests.MockLn).PayInvoiceErrors, lnclient.NewTimeoutError())
+ svc.LNClient.(*tests.MockLn).PayInvoiceResponses = append(svc.LNClient.(*tests.MockLn).PayInvoiceResponses, nil)
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil)
+
+ assert.Error(t, err)
+ assert.Nil(t, transaction)
+
+ transactionType := constants.TRANSACTION_TYPE_OUTGOING
+ transaction, err = transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil)
+ assert.Nil(t, err)
+
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_PENDING, transaction.State)
+ assert.Equal(t, uint64(10000), *transaction.FeeReserveMsat)
+ assert.Nil(t, transaction.Preimage)
+}
diff --git a/transactions/self_payments_test.go b/transactions/self_payments_test.go
new file mode 100644
index 00000000..9c573d0d
--- /dev/null
+++ b/transactions/self_payments_test.go
@@ -0,0 +1,462 @@
+package transactions
+
+import (
+ "context"
+ "testing"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/db/queries"
+ "github.com/getAlby/hub/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSendPaymentSync_SelfPayment_NoAppToNoApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // pubkey matches mock invoice = self payment
+ svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578"
+
+ mockPreimage := "123preimage"
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, mockPreimage, *transaction.Preimage)
+ assert.True(t, transaction.SelfPayment)
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, mockPreimage, *incomingTransaction.Preimage)
+ assert.True(t, incomingTransaction.SelfPayment)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(2), result.RowsAffected)
+}
+
+func TestSendPaymentSync_SelfPayment_NoAppToIsolatedApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // pubkey matches mock invoice = self payment
+ svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578"
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ svc.DB.Save(&app)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ mockPreimage := "123preimage"
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ AppId: &app.ID,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, mockPreimage, *transaction.Preimage)
+ assert.True(t, transaction.SelfPayment)
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, mockPreimage, *incomingTransaction.Preimage)
+ assert.Equal(t, app.ID, *incomingTransaction.AppId)
+ assert.True(t, incomingTransaction.SelfPayment)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(2), result.RowsAffected)
+ // expect balance to be increased
+ assert.Equal(t, uint64(123000), queries.GetIsolatedBalance(svc.DB, app.ID))
+}
+
+func TestSendPaymentSync_SelfPayment_NoAppToApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // pubkey matches mock invoice = self payment
+ svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578"
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ mockPreimage := "123preimage"
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ AppId: &app.ID,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, mockPreimage, *transaction.Preimage)
+ assert.True(t, transaction.SelfPayment)
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, mockPreimage, *incomingTransaction.Preimage)
+ assert.Equal(t, app.ID, *incomingTransaction.AppId)
+ assert.True(t, incomingTransaction.SelfPayment)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(2), result.RowsAffected)
+}
+
+func TestSendPaymentSync_SelfPayment_IsolatedAppToNoApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // pubkey matches mock invoice = self payment
+ svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578"
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ err = svc.DB.Save(&app).Error
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ // give the isolated app 133 sats
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ mockPreimage := "123preimage"
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, mockPreimage, *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+ assert.True(t, transaction.SelfPayment)
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, mockPreimage, *incomingTransaction.Preimage)
+ assert.True(t, incomingTransaction.SelfPayment)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(3), result.RowsAffected)
+ // expect balance to be decreased
+ assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+}
+
+func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // pubkey matches mock invoice = self payment
+ svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578"
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ err = svc.DB.Save(&app).Error
+ assert.NoError(t, err)
+ app2, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ // give the isolated app 133 sats
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ mockPreimage := "123preimage"
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ AppId: &app2.ID,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, mockPreimage, *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+ assert.True(t, transaction.SelfPayment)
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, mockPreimage, *incomingTransaction.Preimage)
+ assert.Equal(t, app2.ID, *incomingTransaction.AppId)
+ assert.True(t, incomingTransaction.SelfPayment)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(3), result.RowsAffected)
+ // expect balance to be decreased
+ assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+}
+
+func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // pubkey matches mock invoice = self payment
+ svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578"
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ err = svc.DB.Save(&app).Error
+ assert.NoError(t, err)
+ app2, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app2.Isolated = true
+ err = svc.DB.Save(&app2).Error
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ // give the isolated app 133 sats
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ mockPreimage := "123preimage"
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ AppId: &app2.ID,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, mockPreimage, *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+ assert.True(t, transaction.SelfPayment)
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, mockPreimage, *incomingTransaction.Preimage)
+ assert.Equal(t, app2.ID, *incomingTransaction.AppId)
+ assert.True(t, incomingTransaction.SelfPayment)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(3), result.RowsAffected)
+ // expect balance to be decreased
+ assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
+}
+
+func TestSendPaymentSync_SelfPayment_IsolatedAppToSelf(t *testing.T) {
+ ctx := context.TODO()
+
+ defer tests.RemoveTestService()
+ svc, err := tests.CreateTestService()
+ assert.NoError(t, err)
+
+ // pubkey matches mock invoice = self payment
+ svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578"
+
+ app, _, err := tests.CreateApp(svc)
+ assert.NoError(t, err)
+ app.Isolated = true
+ err = svc.DB.Save(&app).Error
+ assert.NoError(t, err)
+
+ appPermission := &db.AppPermission{
+ AppId: app.ID,
+ App: *app,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ }
+ err = svc.DB.Create(appPermission).Error
+ assert.NoError(t, err)
+
+ // give the isolated app 133 sats
+ svc.DB.Create(&db.Transaction{
+ AppId: &app.ID,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
+ })
+
+ dbRequestEvent := &db.RequestEvent{}
+ err = svc.DB.Create(&dbRequestEvent).Error
+ assert.NoError(t, err)
+
+ mockPreimage := "123preimage"
+ svc.DB.Create(&db.Transaction{
+ State: constants.TRANSACTION_STATE_PENDING,
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentRequest: tests.MockInvoice,
+ PaymentHash: tests.MockPaymentHash,
+ Preimage: &mockPreimage,
+ AmountMsat: 123000,
+ AppId: &app.ID,
+ })
+
+ transactionsService := NewTransactionsService(svc.DB)
+ transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
+
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), transaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State)
+ assert.Equal(t, mockPreimage, *transaction.Preimage)
+ assert.Equal(t, app.ID, *transaction.AppId)
+ assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId)
+ assert.True(t, transaction.SelfPayment)
+
+ transactionType := constants.TRANSACTION_TYPE_INCOMING
+ incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat)
+ assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State)
+ assert.Equal(t, mockPreimage, *incomingTransaction.Preimage)
+ assert.Equal(t, app.ID, *incomingTransaction.AppId)
+ assert.True(t, incomingTransaction.SelfPayment)
+
+ transactions := []db.Transaction{}
+ result := svc.DB.Find(&transactions)
+ assert.Equal(t, int64(3), result.RowsAffected)
+
+ // expect balance to be unchanged
+ assert.Equal(t, uint64(133000), queries.GetIsolatedBalance(svc.DB, app.ID))
+}
diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go
new file mode 100644
index 00000000..8bd65a4f
--- /dev/null
+++ b/transactions/transactions_service.go
@@ -0,0 +1,761 @@
+package transactions
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "math"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/getAlby/hub/constants"
+ "github.com/getAlby/hub/db"
+ "github.com/getAlby/hub/db/queries"
+ "github.com/getAlby/hub/events"
+ "github.com/getAlby/hub/lnclient"
+ "github.com/getAlby/hub/logger"
+ decodepay "github.com/nbd-wtf/ln-decodepay"
+ "github.com/sirupsen/logrus"
+ "gorm.io/gorm"
+)
+
+type transactionsService struct {
+ db *gorm.DB
+}
+
+type TransactionsService interface {
+ events.EventSubscriber
+ MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, 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)
+ 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, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
+}
+
+type Transaction = db.Transaction
+
+type notFoundError struct {
+}
+
+func NewNotFoundError() error {
+ return ¬FoundError{}
+}
+
+func (err *notFoundError) Error() string {
+ return "The transaction requested was not Found"
+}
+
+type insufficientBalanceError struct {
+}
+
+func NewInsufficientBalanceError() error {
+ return &insufficientBalanceError{}
+}
+
+func (err *insufficientBalanceError) Error() string {
+ return "Insufficient balance remaining to make the requested payment"
+}
+
+type quotaExceededError struct {
+}
+
+func NewQuotaExceededError() error {
+ return "aExceededError{}
+}
+
+func (err *quotaExceededError) Error() string {
+ return "Your wallet has exceeded its spending quota"
+}
+
+func NewTransactionsService(db *gorm.DB) *transactionsService {
+ return &transactionsService{
+ db: db,
+ }
+}
+
+func (svc *transactionsService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
+ lnClientTransaction, err := lnClient.MakeInvoice(ctx, amount, description, descriptionHash, expiry)
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to create transaction")
+ return nil, err
+ }
+
+ var preimage *string
+ if lnClientTransaction.Preimage != "" {
+ preimage = &lnClientTransaction.Preimage
+ }
+
+ var metadata string
+ if lnClientTransaction.Metadata != nil {
+ metadataBytes, err := json.Marshal(lnClientTransaction.Metadata)
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to serialize transaction metadata")
+ return nil, err
+ }
+ metadata = string(metadataBytes)
+ }
+
+ var expiresAt *time.Time
+ if lnClientTransaction.ExpiresAt != nil {
+ expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
+ expiresAt = &expiresAtValue
+ }
+
+ dbTransaction := db.Transaction{
+ AppId: appId,
+ RequestEventId: requestEventId,
+ Type: lnClientTransaction.Type,
+ State: constants.TRANSACTION_STATE_PENDING,
+ AmountMsat: uint64(lnClientTransaction.Amount),
+ Description: description,
+ DescriptionHash: descriptionHash,
+ PaymentRequest: lnClientTransaction.Invoice,
+ PaymentHash: lnClientTransaction.PaymentHash,
+ ExpiresAt: expiresAt,
+ Preimage: preimage,
+ Metadata: metadata,
+ }
+ err = svc.db.Create(&dbTransaction).Error
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to create DB transaction")
+ return nil, err
+ }
+ return &dbTransaction, nil
+}
+
+func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
+ payReq = strings.ToLower(payReq)
+ paymentRequest, err := decodepay.Decodepay(payReq)
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payReq,
+ }).Errorf("Failed to decode bolt11 invoice: %v", err)
+
+ return nil, err
+ }
+
+ selfPayment := paymentRequest.Payee != "" && paymentRequest.Payee == lnClient.GetPubkey()
+
+ var dbTransaction db.Transaction
+
+ err = svc.db.Transaction(func(tx *gorm.DB) error {
+ err := svc.validateCanPay(tx, appId, uint64(paymentRequest.MSatoshi))
+ if err != nil {
+ return err
+ }
+
+ var expiresAt *time.Time
+ if paymentRequest.Expiry > 0 {
+ expiresAtValue := time.Now().Add(time.Duration(paymentRequest.Expiry) * time.Second)
+ expiresAt = &expiresAtValue
+ }
+ feeReserve := svc.calculateFeeReserve(uint64(paymentRequest.MSatoshi))
+ dbTransaction = db.Transaction{
+ AppId: appId,
+ RequestEventId: requestEventId,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ State: constants.TRANSACTION_STATE_PENDING,
+ FeeReserveMsat: &feeReserve,
+ AmountMsat: uint64(paymentRequest.MSatoshi),
+ PaymentRequest: payReq,
+ PaymentHash: paymentRequest.PaymentHash,
+ Description: paymentRequest.Description,
+ DescriptionHash: paymentRequest.DescriptionHash,
+ ExpiresAt: expiresAt,
+ SelfPayment: selfPayment,
+ // Metadata: metadata,
+ }
+ err = tx.Create(&dbTransaction).Error
+ return err
+ })
+
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payReq,
+ }).WithError(err).Error("Failed to create DB transaction")
+ return nil, err
+ }
+
+ var response *lnclient.PayInvoiceResponse
+ if selfPayment {
+ response, err = svc.interceptSelfPayment(paymentRequest.PaymentHash)
+ } else {
+ response, err = lnClient.SendPaymentSync(ctx, payReq)
+ }
+
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payReq,
+ }).WithError(err).Error("Failed to send payment")
+
+ if errors.Is(err, lnclient.NewTimeoutError()) {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payReq,
+ }).WithError(err).Error("Timed out waiting for payment to be sent. It may still succeed. Skipping update of transaction status")
+ // we cannot update the payment to failed as it still might succeed.
+ // we'll need to check the status of it later
+ return nil, err
+ }
+
+ // As the LNClient did not return a timeout error, we assume the payment definitely failed
+ feeReserve := uint64(0)
+ dbErr := svc.db.Model(&dbTransaction).Updates(&db.Transaction{
+ State: constants.TRANSACTION_STATE_FAILED,
+ FeeReserveMsat: &feeReserve,
+ }).Error
+ if dbErr != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payReq,
+ }).WithError(dbErr).Error("Failed to update DB transaction")
+ }
+
+ return nil, err
+ }
+
+ // the payment definitely succeeded
+ feeReserve := uint64(0)
+ now := time.Now()
+ dbErr := svc.db.Model(&dbTransaction).Updates(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Preimage: &response.Preimage,
+ FeeMsat: response.Fee,
+ FeeReserveMsat: &feeReserve,
+ SettledAt: &now,
+ }).Error
+ if dbErr != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": payReq,
+ }).WithError(dbErr).Error("Failed to update DB transaction")
+ }
+
+ // TODO: check the fields are updated here
+ return &dbTransaction, nil
+}
+
+func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
+
+ metadata := map[string]interface{}{}
+
+ metadata["destination"] = destination
+ metadata["tlv_records"] = customRecords
+ metadataBytes, err := json.Marshal(metadata)
+
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to marshal metadata")
+ return nil, err
+ }
+
+ var dbTransaction db.Transaction
+
+ err = svc.db.Transaction(func(tx *gorm.DB) error {
+ err := svc.validateCanPay(tx, appId, amount)
+ if err != nil {
+ return err
+ }
+
+ // NOTE: transaction is created without payment hash :scream:
+ feeReserve := svc.calculateFeeReserve(uint64(amount))
+ dbTransaction = db.Transaction{
+ AppId: appId,
+ RequestEventId: requestEventId,
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ State: constants.TRANSACTION_STATE_PENDING,
+ FeeReserveMsat: &feeReserve,
+ AmountMsat: amount,
+ Metadata: string(metadataBytes),
+ }
+ err = tx.Create(&dbTransaction).Error
+
+ return err
+ })
+
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "destination": destination,
+ "amount": amount,
+ }).WithError(err).Error("Failed to create DB transaction")
+ return nil, err
+ }
+
+ paymentHash, preimage, fee, err := lnClient.SendKeysend(ctx, amount, destination, customRecords)
+
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "destination": destination,
+ "amount": amount,
+ }).WithError(err).Error("Failed to send payment")
+
+ if errors.Is(err, lnclient.NewTimeoutError()) {
+
+ logger.Logger.WithFields(logrus.Fields{
+ "destination": destination,
+ "amount": amount,
+ }).WithError(err).Error("Timed out waiting for payment to be sent. It may still succeed. Skipping update of transaction status")
+
+ // we cannot update the payment to failed as it still might succeed.
+ // we'll need to check the status of it later
+ // but we have the payment hash now, so save it on the transaction
+ dbErr := svc.db.Model(&dbTransaction).Updates(&db.Transaction{
+ PaymentHash: paymentHash,
+ }).Error
+ if dbErr != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "destination": destination,
+ "amount": amount,
+ }).WithError(dbErr).Error("Failed to update DB transaction")
+ }
+ return nil, err
+ }
+
+ // As the LNClient did not return a timeout error, we assume the payment definitely failed
+ dbErr := svc.db.Model(&dbTransaction).Updates(&db.Transaction{
+ PaymentHash: paymentHash,
+ State: constants.TRANSACTION_STATE_FAILED,
+ }).Error
+ if dbErr != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "destination": destination,
+ "amount": amount,
+ }).WithError(dbErr).Error("Failed to update DB transaction")
+ }
+
+ return nil, err
+ }
+
+ // the payment definitely succeeded
+ now := time.Now()
+ feeReserve := uint64(0)
+ dbErr := svc.db.Model(&dbTransaction).Updates(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ PaymentHash: paymentHash,
+ Preimage: &preimage,
+ FeeMsat: &fee,
+ FeeReserveMsat: &feeReserve,
+ SettledAt: &now,
+ }).Error
+ if dbErr != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "destination": destination,
+ "amount": amount,
+ }).WithError(dbErr).Error("Failed to update DB transaction")
+ }
+
+ // TODO: check the fields are updated here
+ return &dbTransaction, nil
+}
+
+func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) {
+ transaction := db.Transaction{}
+
+ tx := svc.db
+
+ if appId != nil {
+ var app db.App
+ svc.db.Find(&app, &db.App{
+ ID: *appId,
+ })
+ if app.Isolated {
+ tx = tx.Where("app_id == ?", *appId)
+ }
+ }
+
+ if transactionType != nil {
+ tx = tx.Where("type == ?", *transactionType)
+ }
+
+ // order settled first, otherwise by created date, as there can be multiple outgoing payments
+ // for the same payment hash (if you tried to pay an invoice multiple times - e.g. the first time failed)
+ result := tx.Order("settled_at desc, created_at desc").Find(&transaction, &db.Transaction{
+ //Type: transactionType,
+ PaymentHash: paymentHash,
+ })
+
+ if result.Error != nil {
+ logger.Logger.WithError(result.Error).Error("Failed to lookup DB transaction")
+ return nil, result.Error
+ }
+
+ if result.RowsAffected == 0 {
+ logger.Logger.WithFields(logrus.Fields{
+ "payment_hash": paymentHash,
+ "app_id": appId,
+ }).WithError(result.Error).Error("Failed to lookup DB transaction")
+ return nil, NewNotFoundError()
+ }
+
+ if transaction.State == constants.TRANSACTION_STATE_PENDING {
+ svc.checkUnsettledTransaction(ctx, &transaction, lnClient)
+ }
+
+ 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) {
+ svc.checkUnsettledTransactions(ctx, lnClient)
+
+ // TODO: add other filtering and pagination
+ tx := svc.db
+
+ tx = tx.Order("created_at desc")
+
+ if !unpaid {
+ tx = tx.Where("state == ?", constants.TRANSACTION_STATE_SETTLED)
+ }
+
+ if transactionType != nil {
+ tx = tx.Where("type == ?", *transactionType)
+ }
+
+ if from > 0 {
+ tx = tx.Where("created_at >= ?", time.Unix(int64(from), 0))
+ }
+ if until > 0 {
+ 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(limit))
+ }
+
+ if appId != nil {
+ var app db.App
+ svc.db.Find(&app, &db.App{
+ ID: *appId,
+ })
+ if app.Isolated {
+ tx = tx.Where("app_id == ?", *appId)
+ }
+ }
+
+ if limit != 0 {
+ tx = tx.Limit(int(limit))
+ }
+ result := tx.Find(&transactions)
+ if result.Error != nil {
+ logger.Logger.WithError(result.Error).Error("Failed to list DB transactions")
+ return nil, result.Error
+ }
+
+ return transactions, nil
+}
+
+func (svc *transactionsService) checkUnsettledTransactions(ctx context.Context, lnClient lnclient.LNClient) {
+ // Only check unsettled transactions for clients that don't support async events
+ // checkUnsettledTransactions does not work for keysend payments!
+ if slices.Contains(lnClient.GetSupportedNIP47NotificationTypes(), "payment_received") {
+ return
+ }
+
+ // check pending payments less than a day old
+ transactions := []Transaction{}
+ result := svc.db.Where("state == ? AND created_at > ?", constants.TRANSACTION_STATE_PENDING, time.Now().Add(-24*time.Hour)).Find(&transactions)
+ if result.Error != nil {
+ logger.Logger.WithError(result.Error).Error("Failed to list DB transactions")
+ return
+ }
+ for _, transaction := range transactions {
+ svc.checkUnsettledTransaction(ctx, &transaction, lnClient)
+ }
+}
+func (svc *transactionsService) checkUnsettledTransaction(ctx context.Context, transaction *db.Transaction, lnClient lnclient.LNClient) {
+ if slices.Contains(lnClient.GetSupportedNIP47NotificationTypes(), "payment_received") {
+ return
+ }
+
+ lnClientTransaction, err := lnClient.LookupInvoice(ctx, transaction.PaymentHash)
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": transaction.PaymentRequest,
+ }).WithError(err).Error("Failed to check transaction")
+ return
+ }
+ // update transaction state
+ if lnClientTransaction.SettledAt != nil {
+ // the payment definitely succeeded
+ now := time.Now()
+ fee := uint64(lnClientTransaction.FeesPaid)
+ feeReserve := uint64(0)
+ dbErr := svc.db.Model(transaction).Updates(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ Preimage: &lnClientTransaction.Preimage,
+ FeeMsat: &fee,
+ FeeReserveMsat: &feeReserve,
+ SettledAt: &now,
+ }).Error
+ if dbErr != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "bolt11": transaction.PaymentRequest,
+ }).WithError(dbErr).Error("Failed to update DB transaction")
+ }
+ }
+}
+
+func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
+ switch event.Event {
+ case "nwc_payment_received":
+ lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
+ if !ok {
+ logger.Logger.WithField("event", event).Error("Failed to cast event")
+ return
+ }
+
+ var dbTransaction db.Transaction
+ err := svc.db.Transaction(func(tx *gorm.DB) error {
+
+ result := tx.Find(&dbTransaction, &db.Transaction{
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ PaymentHash: lnClientTransaction.PaymentHash,
+ })
+
+ if result.RowsAffected == 0 {
+ // Note: brand new payments cannot be associated with an app
+ var metadata string
+ if lnClientTransaction.Metadata != nil {
+ metadataBytes, err := json.Marshal(lnClientTransaction.Metadata)
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to serialize transaction metadata")
+ return err
+ }
+ metadata = string(metadataBytes)
+ }
+ var expiresAt *time.Time
+ if lnClientTransaction.ExpiresAt != nil {
+ expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
+ expiresAt = &expiresAtValue
+ }
+ dbTransaction = db.Transaction{
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ AmountMsat: uint64(lnClientTransaction.Amount),
+ PaymentRequest: lnClientTransaction.Invoice,
+ PaymentHash: lnClientTransaction.PaymentHash,
+ Description: lnClientTransaction.Description,
+ DescriptionHash: lnClientTransaction.DescriptionHash,
+ ExpiresAt: expiresAt,
+ Metadata: metadata,
+ }
+ err := tx.Create(&dbTransaction).Error
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "payment_hash": lnClientTransaction.PaymentHash,
+ }).WithError(err).Error("Failed to create transaction")
+ return err
+ }
+ }
+
+ settledAt := time.Now()
+ fee := uint64(lnClientTransaction.FeesPaid)
+
+ err := tx.Model(&dbTransaction).Updates(&db.Transaction{
+ FeeMsat: &fee,
+ Preimage: &lnClientTransaction.Preimage,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ SettledAt: &settledAt,
+ }).Error
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "payment_hash": lnClientTransaction.PaymentHash,
+ }).WithError(err).Error("Failed to update transaction")
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "payment_hash": lnClientTransaction.PaymentHash,
+ }).WithError(err).Error("Failed to execute DB transaction")
+ return
+ }
+ logger.Logger.WithField("id", dbTransaction.ID).Info("Marked incoming transaction as settled")
+ case "nwc_payment_sent":
+ lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
+ if !ok {
+ logger.Logger.WithField("event", event).Error("Failed to cast event")
+ return
+ }
+
+ var dbTransaction db.Transaction
+ err := svc.db.Transaction(func(tx *gorm.DB) error {
+ result := tx.Find(&dbTransaction, &db.Transaction{
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ PaymentHash: lnClientTransaction.PaymentHash,
+ })
+
+ if result.RowsAffected == 0 {
+ // Note: brand new payments cannot be associated with an app
+ var metadata string
+ if lnClientTransaction.Metadata != nil {
+ metadataBytes, err := json.Marshal(lnClientTransaction.Metadata)
+ if err != nil {
+ logger.Logger.WithError(err).Error("Failed to serialize transaction metadata")
+ return err
+ }
+ metadata = string(metadataBytes)
+ }
+ var expiresAt *time.Time
+ if lnClientTransaction.ExpiresAt != nil {
+ expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
+ expiresAt = &expiresAtValue
+ }
+ dbTransaction = db.Transaction{
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ AmountMsat: uint64(lnClientTransaction.Amount),
+ PaymentRequest: lnClientTransaction.Invoice,
+ PaymentHash: lnClientTransaction.PaymentHash,
+ Description: lnClientTransaction.Description,
+ DescriptionHash: lnClientTransaction.DescriptionHash,
+ ExpiresAt: expiresAt,
+ Metadata: metadata,
+ }
+ err := tx.Create(&dbTransaction).Error
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "payment_hash": lnClientTransaction.PaymentHash,
+ }).WithError(err).Error("Failed to create transaction")
+ return err
+ }
+ }
+
+ settledAt := time.Now()
+ fee := uint64(lnClientTransaction.FeesPaid)
+ feeReserve := uint64(0)
+ err := tx.Model(&dbTransaction).Updates(&db.Transaction{
+ FeeMsat: &fee,
+ FeeReserveMsat: &feeReserve,
+ Preimage: &lnClientTransaction.Preimage,
+ State: constants.TRANSACTION_STATE_SETTLED,
+ SettledAt: &settledAt,
+ }).Error
+ return err
+ })
+
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "payment_hash": lnClientTransaction.PaymentHash,
+ }).WithError(err).Error("Failed to update transaction")
+ return
+ }
+ logger.Logger.WithField("id", dbTransaction.ID).Info("Marked outgoing transaction as settled")
+
+ case "nwc_payment_failed_async":
+ lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
+ if !ok {
+ logger.Logger.WithField("event", event).Error("Failed to cast event")
+ return
+ }
+
+ var dbTransaction db.Transaction
+ result := svc.db.Find(&dbTransaction, &db.Transaction{
+ Type: constants.TRANSACTION_TYPE_OUTGOING,
+ PaymentHash: lnClientTransaction.PaymentHash,
+ })
+
+ // Note: this will happen for keysend payments since our transaction entry will not have a payment
+ // hash at this point
+ if result.RowsAffected == 0 {
+ logger.Logger.WithField("event", event).Error("Failed to find outgoing transaction by payment hash")
+ return
+ }
+
+ feeReserve := uint64(0)
+ err := svc.db.Model(&dbTransaction).Updates(&db.Transaction{
+ State: constants.TRANSACTION_STATE_FAILED,
+ FeeReserveMsat: &feeReserve,
+ }).Error
+ if err != nil {
+ logger.Logger.WithFields(logrus.Fields{
+ "payment_hash": lnClientTransaction.PaymentHash,
+ }).WithError(err).Error("Failed to update transaction")
+ return
+ }
+ logger.Logger.WithField("id", dbTransaction.ID).Info("Marked outgoing transaction as failed")
+ }
+}
+
+func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lnclient.PayInvoiceResponse, error) {
+ // TODO: extract into separate function
+ incomingTransaction := db.Transaction{}
+ result := svc.db.Find(&incomingTransaction, &db.Transaction{
+ Type: constants.TRANSACTION_TYPE_INCOMING,
+ State: constants.TRANSACTION_STATE_PENDING,
+ PaymentHash: paymentHash,
+ })
+ if result.Error != nil {
+ return nil, result.Error
+ }
+
+ if result.RowsAffected == 0 {
+ return nil, NewNotFoundError()
+ }
+ if incomingTransaction.Preimage == nil {
+ return nil, errors.New("preimage is not set on transaction. Self payments not supported")
+ }
+
+ // update the incoming transaction
+ now := time.Now()
+ fee := uint64(0)
+ err := svc.db.Model(&incomingTransaction).Updates(&db.Transaction{
+ State: constants.TRANSACTION_STATE_SETTLED,
+ FeeMsat: &fee,
+ SettledAt: &now,
+ SelfPayment: true,
+ }).Error
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO: publish event for self payment
+
+ return &lnclient.PayInvoiceResponse{
+ Preimage: *incomingTransaction.Preimage,
+ Fee: &fee,
+ }, nil
+}
+
+func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount uint64) error {
+ amountWithFeeReserve := amount + svc.calculateFeeReserve(amount)
+
+ // ensure balance for isolated apps
+ if appId != nil {
+ var app db.App
+ tx.Find(&app, &db.App{
+ ID: *appId,
+ })
+ var appPermission db.AppPermission
+ result := tx.Find(&appPermission, &db.AppPermission{
+ AppId: *appId,
+ Scope: constants.PAY_INVOICE_SCOPE,
+ })
+ if result.RowsAffected == 0 {
+ return errors.New("app does not have pay_invoice scope")
+ }
+
+ if app.Isolated {
+ balance := queries.GetIsolatedBalance(tx, appPermission.AppId)
+
+ if amountWithFeeReserve > balance {
+ return NewInsufficientBalanceError()
+ }
+ }
+
+ if appPermission.MaxAmountSat > 0 {
+ budgetUsageSat := queries.GetBudgetUsageSat(tx, &appPermission)
+ if int(amountWithFeeReserve/1000) > appPermission.MaxAmountSat-int(budgetUsageSat) {
+ return NewQuotaExceededError()
+ }
+ }
+ }
+
+ return nil
+}
+
+// max of 1% or 10000 millisats (10 sats)
+func (svc *transactionsService) calculateFeeReserve(amount uint64) uint64 {
+ // NOTE: LDK defaults to 1% of the payment amount + 50 sats
+ return uint64(math.Max(math.Ceil(float64(amount)*0.01), 10000))
+}